diff --git a/.github/workflows/build_and_test.yml b/.github/workflows/build_and_test.yml index 248a5ad8f..cec8c26fe 100644 --- a/.github/workflows/build_and_test.yml +++ b/.github/workflows/build_and_test.yml @@ -2,8 +2,14 @@ name: Build and test wheels 🐍 on: push: + # feature branches get their CI from the pull_request trigger; running the + # push event there too just produced a cancelled twin whose never-started + # jobs render as 0s "failures" with raw matrix-template names in the PR + # checks list. for a branch without an open PR, use the workflow_dispatch + # trigger defined below. branches: - - '**' + - develop + - main paths-ignore: - 'python/docs/**' - 'doc_resources/**' @@ -45,9 +51,18 @@ on: default: false concurrency: - group: build-${{ github.ref }} + group: build-${{ github.head_ref || github.ref_name }} cancel-in-progress: true +# Single source of truth for the eigenpy version built from source for the +# Linux/macOS wheels. Pin exact for reproducible builds; bump deliberately +# (eigenpy >= 3.13 requires Python >= 3.10, so the wheel matrix drops 3.9). +env: + EIGENPY_VERSION: "3.13.0" + # Opt JavaScript actions into Node 24 (Node 20 is being removed from the runners). + # Silences the conda-incubator/setup-miniconda@v3 (and other node20 action) deprecation. + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" + jobs: set_matrix: name: Set OS matrix @@ -69,9 +84,9 @@ jobs: echo 'python_versions=["3.11"]' >> $GITHUB_OUTPUT elif [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.ref }}" == "refs/heads/develop" || "${{ github.ref }}" == refs/tags/* || "${{ inputs.full_matrix }}" == "true" || "${{ github.event.pull_request.base.ref }}" == "develop" || "${{ github.event.pull_request.base.ref }}" == "main" ]]; then echo 'os=["ubuntu-latest","macos-14"]' >> $GITHUB_OUTPUT - echo 'python_versions=["3.9","3.10","3.11","3.12","3.13","3.14"]' >> $GITHUB_OUTPUT + echo 'python_versions=["3.10","3.11","3.12","3.13","3.14"]' >> $GITHUB_OUTPUT else - echo 'os=["ubuntu-latest","macos-14"]' >> $GITHUB_OUTPUT + echo 'os=["ubuntu-latest"]' >> $GITHUB_OUTPUT echo 'python_versions=["3.11"]' >> $GITHUB_OUTPUT fi @@ -80,6 +95,7 @@ jobs: if: ${{ inputs.minimal != true }} needs: [set_matrix] runs-on: ${{ matrix.os }} + timeout-minutes: 30 # backstop against a hang; healthy runs are ~7 min strategy: fail-fast: false matrix: @@ -91,11 +107,22 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y wget libgmp-dev libmpfr-dev libmpc-dev libeigen3-dev libtool + sudo apt-get install -y wget libgmp-dev libmpfr-dev libmpc-dev libeigen3-dev libtool openmpi-bin libopenmpi-dev ccache - name: Install system dependencies (macOS) if: runner.os == 'macOS' - run: brew install gmp mpfr libmpc eigen@3 + run: brew install gmp mpfr libmpc eigen@3 open-mpi ccache + + # compiler cache: makes recompiles of unchanged C++ near-free. the key is + # deliberately loose (always save fresh, restore newest) -- ccache does its + # own invalidation; do not key on file hashes. + - name: Cache ccache + uses: actions/cache@v5 + with: + path: .ccache + key: ccache-cpp-${{ matrix.os }}-${{ github.sha }} + restore-keys: | + ccache-cpp-${{ matrix.os }}- - name: Build Boost base (Linux) if: runner.os == 'Linux' @@ -120,6 +147,8 @@ jobs: ./b2 install -j2 hardcode-dll-paths=true dll-path=/tmp/boost-base/lib - name: Configure + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache run: | cmake -B build \ -DCMAKE_PREFIX_PATH=/tmp/boost-base \ @@ -127,22 +156,36 @@ jobs: -DENABLE_UNIT_TESTING=ON \ -DINSTALL_DOCUMENTATION=OFF \ -DCMAKE_DISABLE_FIND_PACKAGE_Doxygen=ON \ - -DCMAKE_BUILD_TYPE=Release + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache - name: Build - run: cmake --build build --parallel 2 + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 400M + # Linux runners are 4-core/16 GB and the heaviest TU peaks ~4 GB (ADR-0019), so the full + # 4-way build fits; the 3-core/7 GB macOS runner stays at 2. See ADR-0004. + run: cmake --build build --parallel ${{ runner.os == 'Linux' && 4 || 2 }} + + - name: ccache stats + if: always() + env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + run: ccache -s || true - name: Test working-directory: build env: LD_LIBRARY_PATH: /tmp/boost-base/lib:/tmp/boost-base/lib64 DYLD_LIBRARY_PATH: /tmp/boost-base/lib - run: ctest --output-on-failure --parallel $(nproc 2>/dev/null || sysctl -n hw.ncpu) + run: ctest --output-on-failure --timeout 600 --parallel $(nproc 2>/dev/null || sysctl -n hw.ncpu) test_cpp_windows: name: C++ tests on Windows if: ${{ inputs.minimal != true }} runs-on: windows-latest + timeout-minutes: 40 # backstop against a hang; the clang-cl build+test is ~22 min env: CONDA_PKGS_DIRS: ${{ github.workspace }}\conda_pkgs steps: @@ -161,7 +204,7 @@ jobs: with: activate-environment: b2-windows environment-file: environment-win.yml - auto-activate-base: false + auto-activate: false channel-priority: strict - name: Configure and build C++ tests @@ -174,6 +217,13 @@ jobs: $env:CMAKE_GENERATOR_TOOLSET = '' $env:CMAKE_PREFIX_PATH = "$env:CONDA_PREFIX\Library" + # The GitHub Windows runner now ships Visual Studio 2026 (MSVC 14.51), whose STL + # hard-asserts (STL1000) that the compiler is Clang >= 20; the conda + # clang-cl is older. The only errors are that version gate -- the C++17 code compiles + # fine -- so we opt out of the gate. (Proper fix: ship a clang >= 20 toolchain.) + $env:CXXFLAGS = '-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH' + $env:CFLAGS = '-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH' + $boostHeader = "$env:CONDA_PREFIX\Library\include\boost\archive\archive_exception.hpp" (Get-Content $boostHeader).Replace('public virtual std::exception', 'public std::exception') | Set-Content $boostHeader @@ -181,6 +231,7 @@ jobs: -DCMAKE_PREFIX_PATH="$env:CONDA_PREFIX\Library" ` -DBUILD_PYTHON_BINDINGS=OFF ` -DENABLE_UNIT_TESTING=ON ` + -DUSE_CCACHE=OFF ` -DINSTALL_DOCUMENTATION=OFF ` -DCMAKE_DISABLE_FIND_PACKAGE_Doxygen=ON ` -DCMAKE_BUILD_TYPE=Release @@ -189,11 +240,12 @@ jobs: - name: Test shell: pwsh working-directory: build - run: ctest --output-on-failure --parallel + run: ctest --output-on-failure --timeout 600 --parallel build_macos_ubuntu_wheels: name: ${{ matrix.os }} Python-${{ matrix.python-version }} wheels - needs: [set_matrix] + needs: [set_matrix, test_cpp_unix] + if: ${{ !failure() && !cancelled() }} runs-on: ${{ matrix.os }} strategy: fail-fast: false @@ -214,11 +266,29 @@ jobs: ~/Library/Caches/Homebrew /tmp/boost_1_90_0.tar.bz2 /tmp/eigen-3.4.0.tar.gz - /tmp/eigenpy-3.12.0.tar.gz - key: ${{ runner.os }}-cibw-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml', 'python/pyproject.toml', 'CMakeLists.txt', 'core/CMakeLists.txt', '.github/workflows/build_and_test.yml') }} + /tmp/eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz + key: ${{ runner.os }}-cibw-${{ matrix.python-version }}-${{ hashFiles('pyproject.toml', 'python/pyproject.toml', 'CMakeLists.txt', 'core/CMakeLists.txt', '.github/workflows/build_and_test.yml', 'python_bindings/include/tracker_export.hpp', 'python_bindings/include/endgame_export.hpp') }} restore-keys: | ${{ runner.os }}-cibw-${{ matrix.python-version }}- ${{ runner.os }}-cibw- + # compiler cache for the bertini2 compile itself (deps are built by their + # own build systems and are out of ccache's cmake-launcher reach). the key + # is deliberately loose -- always save fresh, restore newest; ccache does + # its own invalidation. on Linux the cache lives in the repo dir because + # that is what cibuildwheel mounts into the container (as /project). + - name: Cache ccache (bertini2 compile) + uses: actions/cache@v5 + with: + path: .ccache + key: ccache-wheels-${{ runner.os }}-${{ matrix.python-version }}-${{ github.sha }} + restore-keys: | + ccache-wheels-${{ runner.os }}-${{ matrix.python-version }}- + ccache-wheels-${{ runner.os }}- + # pre-create as the runner user so docker's volume mount doesn't create it + # root-owned on the host + - name: Ensure ccache dir exists + shell: bash + run: mkdir -p .ccache - name: Set CIBW_BUILD for this Python version shell: bash run: | @@ -233,34 +303,41 @@ jobs: echo "MACOSX_DEPLOYMENT_TARGET=${SW_VERS}.0" >> $GITHUB_ENV - uses: pypa/cibuildwheel@v3.4.1 env: - # Build cp39 through cp313 for both Linux (manylinux) and macOS. + # Build cp310 through cp314 for both Linux (manylinux) and macOS (cp39 was + # dropped when eigenpy moved to >= 3.13, whose floor is Python 3.10). # Boost.Python and eigenpy are rebuilt per target Python in BEFORE_BUILD # so each wheel bundles a matching libboost_python3X. CIBW_BUILD: ${{ env.CIBW_BUILD }} CIBW_SKIP: "*-musllinux_* *-manylinux_i686" - CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_28 + CIBW_MANYLINUX_X86_64_IMAGE: manylinux_2_34 + # cibuildwheel COPIES the project into the container (no bind mount), so + # a cache written under /project is discarded. mount the host ccache dir + # explicitly; ignored on macOS (host build, no container). + CIBW_CONTAINER_ENGINE: "docker; create_args: --volume=${{ github.workspace }}/.ccache:/ccache-host" # Install system deps + download (but DO NOT build) Boost & eigenpy sources # once per container. Boost.Python is ABI-locked to a single CPython version, # so it must be rebuilt per target Python in BEFORE_BUILD — building it here - # against the manylinux2014 system python3 produces a libboost_python39 that - # all five wheels would then bundle, breaking import on cp310/311/312/313. + # against the manylinux system python3 produces a single libboost_python3X that + # all wheels would then bundle, breaking import on the other CPython versions. CIBW_BEFORE_ALL_LINUX: > yum install -y wget gmp-devel mpfr-devel libmpc-devel libtool && + { yum install -y ccache || { yum install -y epel-release && yum install -y ccache; }; } && + ccache --version && cd /tmp && { [ -f eigen-3.4.0.tar.gz ] || wget -q https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz ; } && rm -rf eigen-3.4.0 && tar xzf eigen-3.4.0.tar.gz && cmake -S eigen-3.4.0 -B eigen-3.4.0/bld -DCMAKE_INSTALL_PREFIX=/usr/local -DCMAKE_BUILD_TYPE=Release && cmake --install eigen-3.4.0/bld && - { [ -f eigenpy-3.12.0.tar.gz ] || wget -q https://github.com/stack-of-tasks/eigenpy/releases/download/v3.12.0/eigenpy-3.12.0.tar.gz ; } + { [ -f eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz ] || wget -q https://github.com/stack-of-tasks/eigenpy/releases/download/v${{ env.EIGENPY_VERSION }}/eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz ; } # On macOS, Homebrew's boost-python3 / eigenpy bottles track the current # Homebrew default Python (now 3.14), so a cp313 wheel that bundles them # imports a libboost_python314.dylib into a 3.13 interpreter and segfaults. # Install only the Python-version-independent libs from brew here; download # Boost & eigenpy sources to /tmp for the per-Python build in BEFORE_BUILD. CIBW_BEFORE_ALL_MACOS: > - brew install gmp mpfr libmpc eigen@3 && + brew install gmp mpfr libmpc eigen@3 ccache && cd /tmp && - { [ -f eigenpy-3.12.0.tar.gz ] || curl -fsSL -o eigenpy-3.12.0.tar.gz https://github.com/stack-of-tasks/eigenpy/releases/download/v3.12.0/eigenpy-3.12.0.tar.gz ; } + { [ -f eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz ] || curl -fsSL -o eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz https://github.com/stack-of-tasks/eigenpy/releases/download/v${{ env.EIGENPY_VERSION }}/eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz ; } # Build Boost and eigenpy fresh against the active per-Python interpreter # so each wheel bundles a matching libboost_python3X. @@ -276,10 +353,10 @@ jobs: cd boost_1_90_0 && ./bootstrap.sh --with-python=$(which python) --prefix=/tmp/deps-py && printf 'using python : %s : %s : %s : %s ;\n' "$PY_VER" "$(which python)" "$PY_INC" "$PY_LIB" > user-config.jam && - ./b2 install -j$(nproc) --user-config=user-config.jam python=$PY_VER && + ./b2 install -j$(nproc) --user-config=user-config.jam python=$PY_VER --without-mpi && cd /tmp && - rm -rf eigenpy-3.12.0 && tar zxf eigenpy-3.12.0.tar.gz && - cd eigenpy-3.12.0 && mkdir -p bld && cd bld && + rm -rf eigenpy-${{ env.EIGENPY_VERSION }} && tar zxf eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz && + cd eigenpy-${{ env.EIGENPY_VERSION }} && mkdir -p bld && cd bld && cmake .. -DCMAKE_PREFIX_PATH=/tmp/deps-py -DCMAKE_INSTALL_PREFIX=/tmp/deps-py -DCMAKE_BUILD_TYPE=Release -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=ON -DPython3_EXECUTABLE=$(which python) -DPython3_NumPy_INCLUDE_DIR=$(python -c "import numpy; print(numpy.get_include())") -DBUILD_TESTING=OFF -DCMAKE_INSTALL_DO_STRIP=ON && make -j2 install && find /tmp/deps-py -name '*.so*' -type f | while read f; do @@ -302,13 +379,30 @@ jobs: printf 'using python : %s : %s : %s : %s ;\n' "$PY_VER" "$(which python)" "$PY_INC" "$PY_LIB" > user-config.jam && ./b2 install -j$(sysctl -n hw.ncpu) --user-config=user-config.jam python=$PY_VER hardcode-dll-paths=true dll-path=/tmp/deps-py/lib && cd /tmp && - rm -rf eigenpy-3.12.0 && tar zxf eigenpy-3.12.0.tar.gz && - cd eigenpy-3.12.0 && mkdir -p bld && cd bld && - cmake .. -DCMAKE_PREFIX_PATH="/tmp/deps-py;/opt/homebrew" -DCMAKE_INSTALL_PREFIX=/tmp/deps-py -DPython3_EXECUTABLE=$(which python) -DPython3_NumPy_INCLUDE_DIR=$(python -c "import numpy; print(numpy.get_include())") -DBUILD_TESTING=OFF && + rm -rf eigenpy-${{ env.EIGENPY_VERSION }} && tar zxf eigenpy-${{ env.EIGENPY_VERSION }}.tar.gz && + cd eigenpy-${{ env.EIGENPY_VERSION }} && mkdir -p bld && cd bld && + cmake .. -DCMAKE_PREFIX_PATH="/tmp/deps-py;/opt/homebrew" -DCMAKE_INSTALL_PREFIX=/tmp/deps-py -DCMAKE_BUILD_TYPE=Release -DPython3_EXECUTABLE=$(which python) -DPython3_NumPy_INCLUDE_DIR=$(python -c "import numpy; print(numpy.get_include())") -DBUILD_TESTING=OFF && make -j2 install CIBW_BEFORE_BUILD: "pip install scikit-build-core numpy" - CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/tmp/deps-py/lib:/tmp/deps-py/lib64:$LD_LIBRARY_PATH CMAKE_PREFIX_PATH=/tmp/deps-py" - CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET CMAKE_PREFIX_PATH=/tmp/deps-py:/opt/homebrew DYLD_FALLBACK_LIBRARY_PATH=/tmp/deps-py/lib:/opt/homebrew/lib" + CIBW_ENVIRONMENT_LINUX: "LD_LIBRARY_PATH=/tmp/deps-py/lib:/tmp/deps-py/lib64:$LD_LIBRARY_PATH CMAKE_PREFIX_PATH=/tmp/deps-py CXXFLAGS=-w CMAKE_BUILD_PARALLEL_LEVEL=4 CCACHE_DIR=/ccache-host CCACHE_MAXSIZE=400M CMAKE_ARGS='-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache'" + CIBW_ENVIRONMENT_MACOS: "MACOSX_DEPLOYMENT_TARGET=$MACOSX_DEPLOYMENT_TARGET CMAKE_PREFIX_PATH=/tmp/deps-py:/opt/homebrew DYLD_FALLBACK_LIBRARY_PATH=/tmp/deps-py/lib:/opt/homebrew/lib CCACHE_DIR=$GITHUB_WORKSPACE/.ccache CCACHE_MAXSIZE=400M CMAKE_ARGS='-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache'" + # Full pytest suite, restored 2026-06-08 now that the uninitialized-numpy-slot + # crash is fixed in the bindings (see ADR-0006). The build image is + # manylinux_2_34 (AlmaLinux 9, MPFR 4.1; set via CIBW_MANYLINUX_X86_64_IMAGE + # above). The crash was NEVER MPFR-version specific - it is the all-zero + # mpfr/mpc "uninitialized sentinel" bug, which reproduces on any MPFR version + # (and locally); the older blame on "MPFR 3.1.6" was a misdiagnosis. Running + # the full suite here is the in-container proof of the fix. If it flakes or + # fails, revert to the import smoke test per ADR-0003 (kept there as fallback). + CIBW_TEST_REQUIRES_LINUX: "pytest pytest-timeout numpy sympy" + CIBW_TEST_COMMAND_LINUX: "cd {project} && python -m pytest python/test/ -q" + + - name: ccache size (cold run = grows from nothing; warm run = hits) + if: always() + shell: bash + run: | + du -sh .ccache 2>/dev/null || echo "no .ccache directory was produced" + if command -v ccache >/dev/null; then CCACHE_DIR=$PWD/.ccache ccache -s || true; fi - uses: actions/upload-artifact@v5 with: @@ -318,20 +412,22 @@ jobs: build_windows_wheels: name: Windows Python-${{ matrix.python-version }} wheels - if: ${{ inputs.minimal != true }} + needs: [set_matrix, test_cpp_windows] + if: ${{ inputs.minimal != true && !failure() && !cancelled() }} runs-on: windows-latest env: CONDA_PKGS_DIRS: ${{ github.workspace }}\conda_pkgs strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v5 with: persist-credentials: false - name: Cache conda packages + pip uses: actions/cache@v5 + continue-on-error: true with: path: | ${{ github.workspace }}\conda_pkgs @@ -345,7 +441,7 @@ jobs: activate-environment: b2-windows environment-file: environment-win.yml python-version: ${{ matrix.python-version }} - auto-activate-base: false + auto-activate: false channel-priority: strict - name: Build Windows @@ -361,6 +457,13 @@ jobs: $env:CMAKE_GENERATOR_TOOLSET = '' $env:CMAKE_PREFIX_PATH = "$env:CONDA_PREFIX\Library" + # The GitHub Windows runner now ships Visual Studio 2026 (MSVC 14.51), whose STL + # hard-asserts (STL1000) that the compiler is Clang >= 20; the conda + # clang-cl is older. The only errors are that version gate -- the C++17 code compiles + # fine -- so we opt out of the gate. (Proper fix: ship a clang >= 20 toolchain.) + $env:CXXFLAGS = '-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH' + $env:CFLAGS = '-D_ALLOW_COMPILER_AND_STL_VERSION_MISMATCH' + $boostHeader = "$env:CONDA_PREFIX\Library\include\boost\archive\archive_exception.hpp" (Get-Content $boostHeader).Replace('public virtual std::exception', 'public std::exception') | Set-Content $boostHeader @@ -381,14 +484,21 @@ jobs: test_wheels_linux_macos: name: Test wheels on ${{ matrix.os }} / py${{ matrix.python-version }} + # ubuntu-latest is excluded below: Linux wheels run the full pytest suite *inside* + # the manylinux_2_34 container via CIBW_TEST_COMMAND_LINUX (see + # build_macos_ubuntu_wheels). This host-runner job therefore covers macOS only; + # Windows is covered by test_wheels_windows. if: ${{ inputs.minimal != true }} needs: [set_matrix, build_macos_ubuntu_wheels] runs-on: ${{ matrix.os }} + timeout-minutes: 25 # backstop against a hang; healthy test runs are well under a minute strategy: fail-fast: false matrix: os: ${{ fromJson(needs.set_matrix.outputs.os) }} python-version: ${{ fromJson(needs.set_matrix.outputs.python_versions) }} + exclude: + - os: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -410,7 +520,7 @@ jobs: - name: Run Python tests run: | - pip install pytest + pip install pytest pytest-timeout sympy python -m pytest python/test/ -v test_wheels_windows: @@ -418,10 +528,11 @@ jobs: if: ${{ inputs.minimal != true }} needs: [build_windows_wheels] runs-on: windows-latest + timeout-minutes: 25 # backstop against a hang; healthy runs are ~6 min (was 6h before the fix) strategy: fail-fast: false matrix: - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v5 @@ -431,7 +542,7 @@ jobs: activate-environment: b2-windows environment-file: environment-win.yml python-version: ${{ matrix.python-version }} - auto-activate-base: false + auto-activate: false channel-priority: strict - name: Download wheels artifact @@ -447,5 +558,5 @@ jobs: conda install -y python=${{ matrix.python-version }} --update-deps $py = "C:\Miniconda\envs\b2-windows\python.exe" & $py -m pip install --no-index --find-links dist/ bertini2 - & $py -m pip install pytest + & $py -m pip install pytest pytest-timeout sympy & $py -m pytest python/test/ -v diff --git a/.github/workflows/build_docs.yml b/.github/workflows/build_docs.yml index 059c0746e..c517984fd 100644 --- a/.github/workflows/build_docs.yml +++ b/.github/workflows/build_docs.yml @@ -104,7 +104,7 @@ jobs: python -m pip install --upgrade pip pip install \ sphinx sphinx-rtd-theme sphinxcontrib-bibtex gitpython \ - scikit-build-core build numpy + scikit-build-core build numpy matplotlib pandas - name: Download built wheel uses: actions/download-artifact@v5 @@ -115,6 +115,13 @@ jobs: - name: Install bertini from built wheel run: pip install --no-index --find-links dist/ bertini2 + - name: Run tutorial doctests (Sphinx) + # Execute the `.. testcode::` / `>>>` blocks in the docs against the installed wheel, so + # the tutorials are verified code, not just prose. Fails the build if any doctest fails. + working-directory: python/docs + run: | + sphinx-build -b doctest --keep-going source ../../build/docs/doctest + - name: Build Python docs (Sphinx) working-directory: python/docs env: diff --git a/.github/workflows/github-gitlab-sync.yml b/.github/workflows/github-gitlab-sync.yml index 9af179e61..3dcefec42 100644 --- a/.github/workflows/github-gitlab-sync.yml +++ b/.github/workflows/github-gitlab-sync.yml @@ -1,12 +1,22 @@ -name: Sync to MPI GitLab -on: +name: Sync to GitLab mirror +on: push + +# Any repo (upstream or fork) that configures the three GITLAB_* secrets gets +# mirrored to its own GitLab target; repos without the secrets skip silently. +# This replaces the old hardcoded `if: github.repository == 'bertiniteam/b2'`, +# which prevented forks from syncing even when their secrets were set. +# Note: the `secrets` context is not available in job-level `if`, hence the +# env indirection. + jobs: sync: - if: github.repository == 'bertiniteam/b2' #only on official runs-on: ubuntu-latest + env: + GITLAB_CONFIGURED: ${{ secrets.GITLAB_URL != '' }} steps: - name: Sync to GitLab + if: env.GITLAB_CONFIGURED == 'true' # You may pin to the exact commit or the version. # uses: kujov/gitlab-sync@b19399d43e81ac88acb6eefd5588e6ebde3d7d88 uses: kujov/gitlab-sync@2.2.1 @@ -18,5 +28,4 @@ jobs: # Your GitLab Personal Access Token with required permissions gitlab_pat: ${{ secrets.GITLAB_PAT }} # Whether to force push to GitLab. Defaults to false. - - force_push: true # optional, default is false + force_push: true diff --git a/.gitignore b/.gitignore index dbc7ef640..bf1a363f6 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,31 @@ _skbuild/ build/ bld/ build_mine/ + +# Wheel / sdist output +dist/ + +# Local Claude Code config and personal working notes +.claude/ +z_notes/ +.mcp.json + +# C++ test runner logs (ctest/Boost.Test output at repo root) +bertini2_tests_*.log + +# Serialization test output files (Boost.Serialization archives) +/serialization_test_* + +# Classic Bertini CLI output files (written by bertini2_exe when run from repo root) +/main_data +/raw_data +/raw_solutions +/real_finite_solutions +/midpath_data +/finite_solutions +/nonfinite_solutions +/start +/failed_paths + +# Compiled benchmark binaries (keep the .cpp sources) +/tuning/arithmetic_cost diff --git a/CLAUDE.md b/CLAUDE.md index 2bee761e5..d30b3dc26 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -63,10 +63,24 @@ ctest --test-dir build/core ### Python Tests +`pytest` is the single way to run the Python tests (the suites are plain pytest +functions + fixtures; the old `unittest` `TextTestRunner` aggregator scripts are gone): + ```bash pytest python/test/ ``` +The multiprecision default precision is **global mutable state** +(`bertini.default_precision(n)`). An **autouse fixture in `python/test/conftest.py`** +(`_reset_precision`) resets it to a known baseline (`DEFAULT_TEST_PRECISION = 30`) before +every test and restores it afterward, so no test can inherit a neighbor's precision — do +**not** re-introduce per-test `default_precision(...)` setup. To override the precision for +a specific test, use the `precision` fixture (parametrize it indirectly, e.g. +`@pytest.mark.parametrize("precision", [30, 50, 80], indirect=True)` with a +precision-derived tolerance). When adding or debugging precision-sensitive tests, run the +file on its own (`pytest python/test/classes/.py`) to confirm it does not depend on +cross-test state. + ## Architecture The project has three layers, built in order: @@ -101,11 +115,43 @@ The project has three layers, built in order: ## CI/CD -- `.github/workflows/build-and-publish-to-pypi.yml` -- Builds wheels on Ubuntu/macOS/Windows, publishes to TestPyPI on `develop` push, PyPI on version tags (`v*.*.*`). -- Pushes to `develop` trigger TestPyPI publish; tagged releases go to PyPI with Sigstore signing and GitHub Releases. +- `.github/workflows/build_and_test.yml` -- Builds wheels on Ubuntu/macOS/Windows and runs tests. Triggered by pull requests and pushes to `develop`/`main`. +- `.github/workflows/publish.yml` -- Publishes to TestPyPI on `develop` push, PyPI on version tags (`v*.*.*`) with Sigstore signing and GitHub Releases. + +### Linux wheel test coverage + +Linux wheels are built inside a `manylinux_2_34` container (AlmaLinux 9, MPFR 4.1; set via `CIBW_MANYLINUX_X86_64_IMAGE`). The **full pytest suite runs on all three platforms** — on Linux it runs *inside* that container via `CIBW_TEST_COMMAND_LINUX`, and on macOS/Windows via the host-runner test jobs. + +This was not always so: for a while Linux ran an import smoke test only, because the suite was SIGABRT/SIGSEGV-crashing — a crash *misattributed* to the older `manylinux_2_28` container's MPFR 3.1.6. The real cause is a **version-independent** bug (uninitialized `mpfr`/`mpc` numpy slots), now fixed in the bindings. Do **not** try to fix Linux test crashes by bumping MPFR or the manylinux image (that was tried and does not work) or by building MPFR from source (specifically out of bounds). See `docs/adr/0006-eigenpy-uninitialized-numpy-slot-guards.md` for the fix and `docs/adr/0003-manylinux-no-full-pytest.md` for the (now reversed) smoke-test stopgap and its history. + +## Python Bindings — Known Pitfalls + +### eigenpy writable Ref + adjacent scalar (ADR-0001) + +Never place a writable `Eigen::Ref>` argument **adjacent** to a `mpc_complex const&` scalar argument in a Boost.Python binding. eigenpy's from-Python converter for the writable Ref writes into a static rvalue-converter slot that overlaps with the storage for adjacent `const&` scalars, corrupting them. The corrupted `mpc_complex` then triggers `MPFR_ASSERTN` → SIGABRT. + +**Rule:** If a binding takes a writable `Eigen::Ref>` and also needs scalar `ComplexT` args, pass the scalars **by value**: + +```cpp +// WRONG — start_time/end_time get corrupted +SuccessCode wrap(Eigen::Ref> result, + ComplexT const& start_time, // ← adjacent const& scalar + ComplexT const& end_time); + +// CORRECT — by-value copy is taken before the Ref converter runs +SuccessCode wrap(Eigen::Ref> result, + ComplexT start_time, // ← by value + ComplexT end_time); +``` + +Single-argument bindings and read-only `Vec const&` bindings are unaffected. See `docs/adr/0001-eigenpy-writable-ref-scalar-by-value.md`. + +## Architecture Decision Records + +`docs/adr/` contains ADRs for load-bearing design decisions — where the *why* would not be obvious from reading the code. Check there before undoing anything that looks strange. ## Conventions - C++ standard: C++17. Headers use `.hpp` extension. - License: GPL v3 with additional terms (see `licenses/`, `core/ADDITIONAL_GPL_TERMS`). -- Version is tracked in `python/bertini/_version.py` and `pyproject.toml`. +- Version is tracked in `pyproject.toml` (the `version = "..."` line), read at runtime via `importlib.metadata.version("bertini2")`. diff --git a/CMakeLists.txt b/CMakeLists.txt index 84555f8c6..3d3a1557d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -105,6 +105,16 @@ include("${JRL_CMAKE_MODULES}/base.cmake") compute_project_args(PROJECT_ARGS LANGUAGES CXX) project(${PROJECT_NAME} ${PROJECT_ARGS}) +option(USE_CCACHE "Use ccache to speed up rebuilds if available" ON) +if(USE_CCACHE) + find_program(CCACHE_PROGRAM ccache) + if(CCACHE_PROGRAM) + set(CMAKE_CXX_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + set(CMAKE_C_COMPILER_LAUNCHER "${CCACHE_PROGRAM}") + message(STATUS "ccache enabled: ${CCACHE_PROGRAM}") + endif() +endif() + include("${JRL_CMAKE_MODULES}/boost.cmake") if(BUILD_PYTHON_BINDINGS) @@ -200,8 +210,6 @@ endif(WIN32) #target_link_boost_python(${PROJECT_NAME} PUBLIC) -#set_property(TARGET bertini2_exe PROPERTY OUTPUT_NAME bertini2) - add_subdirectory(core) diff --git a/README.md b/README.md index 417df8f8c..64ce2b7f3 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,15 @@ # Quick links +- [Documentation](https://bertini2.org) +- [`bertini2` PyPI package](https://pypi.org/project/bertini2/) - [Wiki](https://github.com/bertiniteam/b2/wiki) --- # Overview -The solution of arbitrary polynomial systems is an area of active research, and has many applications in math, science and engineering. This software, Bertini 2, is a complete re-implementation of [Bertini 1](https://bertini.nd.edu) from C into C++/Python. +The solution of arbitrary polynomial systems is an area of active research, and has many applications in math, science and engineering. This software, Bertini 2, is a re-implementation of [Bertini 1](https://bertini.nd.edu) from C into C++/Python. The theoretical basis for the solution of polynomials with Bertini is a theorem which bounds the number of solutions a system may have. It sits together with the numerical computational tool of "homotopy continuation". the act of "continuing" from one system into another through a "homotopy", as depicted in the below diagram: @@ -18,29 +20,30 @@ The theoretical basis for the solution of polynomials with Bertini is a theorem # Current capabilites -Bertini 2 currently has implemented the foundations of Numerical Algebraic Geometry. Development is ongoing, and here's what we have so far: +Bertini 2 has already implemented most of the foundations of Numerical Algebraic Geometry. Development is ongoing, and here's what we have so far: -- C++ functions and types, with Python bindings. -- Through Python, runtime scriptable construction of systems and interactivity with their zero-dimensional solutions. -- Construction of multivariate polynomial and non-polynomial systems. -- Evaluation of systems and their Jacobians in double and arbitrary multiple precision, using two different methods. +- A blackbox that implements tracktype 0 (zerodim solving) for the total degree and mhom start systems. User homotopy not yet exposed +- Through Python bindings, runtime scriptable construction of systems and interactivity with their zero-dimensional solutions. +- Python construction of multivariate polynomial and non-polynomial systems, including from linear algebra operations via numpy and Bertini 2's variables and extended numeric types. +- Evaluation of systems and their Jacobians in double and arbitrary multiple precision. - Construction of the Total Degree and Multihomogeneous start systems. - Construction of homotopies (they're just systems with path variables defined). - Tracking of a start point x_0, corresponding to a particular time $t_0 \in \mathbb{C}^n$ in a homotopy $H$, from $t_0$ to $t_1$. - Running of the Power Series and Cauchy endgames, in double, multiple, and adaptive precision. -Development is ongoing, and we want your help! +See the published docs for latest the officially released documentation (which will always lag behind the doc from the dev version). --- # Missing functionality -* Parallel solving +✨ In-progress!!! + * Numerical irreducible decomposition * Membership testing * and other algorithms -Users wanting a more developed implementation are recommended to use [Bertini 1](https://bertini.nd.edu) or [homotopycontinuation.jl](https://www.juliahomotopycontinuation.org/), or one of the other packages implementing the theory. +Users wanting a more stable implementation are recommended to use [Bertini 1](https://bertini.nd.edu) or [homotopycontinuation.jl](https://www.juliahomotopycontinuation.org/), or one of the other packages implementing the theory. --- @@ -55,12 +58,12 @@ The Python package `bertini2` provides pre-built wheels for Linux, macOS, and Wi pip install bertini2 ``` -Once it's installed, you `import bertini` +Once it's installed, you `import bertini` (not `import bertini2`!) -* Linux: Python 3.9-3.14 -* MacOS (Apple Silicon): Python 3.9-3.14 -* MacOS (Intel): not supported -* Windows: Python 3.9-3.14 +* Linux: Python 3.10-3.14 +* MacOS (Apple Silicon): Python 3.10-3.14 +* MacOS (Intel): not currently supported +* Windows: Python 3.10-3.14 ## Building from source diff --git a/benchmark/README.md b/benchmark/README.md new file mode 100644 index 000000000..b1d6644e3 --- /dev/null +++ b/benchmark/README.md @@ -0,0 +1,69 @@ +# Bertini2 Parallel Benchmark + +Scripts for measuring MPI + thread parallel speedup of the `bertini2` solver. + +## Prerequisites + +- `bertini2` built with MPI support (`cmake -DENABLE_MPI=ON ...`) +- `mpirun` on `PATH` +- Python 3.7+, no external packages needed + +## Quick Start + +```bash +# From the repo root +python benchmark/run_benchmark.py \ + --bertini2 ./build/core/bertini2 \ + --input benchmark/inputs/small.b2 \ + --ranks 1 2 \ + --threads 1 2 +``` + +This runs the `small.b2` system (27 paths) with 4 combinations of ranks × threads, +prints a speedup table, and writes `benchmark_results.csv`. + +## Sample Input Files + +| File | Variables | Degree | Paths | +|------|-----------|--------|-------| +| `inputs/small.b2` | 3 | 3 | 27 | +| `inputs/medium.b2` | 5 | 3 | 243 | +| `inputs/large.b2` | 6 | 3 | 729 | +| `inputs/xlarge.b2` | 7 | 3 | 2187 | +| `inputs/huge.b2` | 9 | 3 | 19683 | + +Each system is diagonal (`xi^3 - ci = 0`) with distinct prime constants, so solutions +are known analytically and correctness is easy to verify (solution count = number of +paths for these fully real systems). + +## Options + +``` +--bertini2 PATH Path to bertini2 executable (default: ./build/core/bertini2) +--input FILE Bertini input file to solve (required) +--ranks N ... MPI rank counts to sweep (default: 1 2 4) +--threads N ... OMP thread counts per rank (default: 1) +--output FILE CSV output path (default: benchmark_results.csv) +--timeout SECS Per-run timeout (default: 600) +--repeats N Timed repeats per combo; records minimum (default: 1) +--mpirun CMD mpirun command (default: mpirun) +``` + +## Cluster Submission (SLURM) + +Edit `submit.slurm.sh` (paths, module loads, rank/thread sweep) and submit: + +```bash +sbatch benchmark/submit.slurm.sh +``` + +Results are written to `benchmark/results_.csv`. + +## Notes + +- `--bind-to none` is passed to `mpirun` automatically so that threaded workers + can use all cores on a node without affinity conflicts. +- Each (ranks, threads) combo runs in its own temp directory to avoid file conflicts. +- The serial baseline (ranks=1, threads=1) is always run first; speedup is computed + relative to that measurement. +- `OMP_NUM_THREADS` is set per-run by the script; do not set it externally when sweeping. diff --git a/benchmark/baseline_large.csv b/benchmark/baseline_large.csv new file mode 100644 index 000000000..3e225ffc9 --- /dev/null +++ b/benchmark/baseline_large.csv @@ -0,0 +1,2 @@ +ranks,threads,total_workers,wall_time_s,solutions_found,speedup_vs_serial +1,1,1,66.8036,7,1.0000 diff --git a/benchmark/baseline_large_seeded.csv b/benchmark/baseline_large_seeded.csv new file mode 100644 index 000000000..9a2fc846b --- /dev/null +++ b/benchmark/baseline_large_seeded.csv @@ -0,0 +1,2 @@ +ranks,threads,total_workers,wall_time_s,solutions_found,speedup_vs_serial +1,1,1,143.9177,7,1.0000 diff --git a/benchmark/baseline_medium.csv b/benchmark/baseline_medium.csv new file mode 100644 index 000000000..6a6c0b03c --- /dev/null +++ b/benchmark/baseline_medium.csv @@ -0,0 +1,2 @@ +ranks,threads,total_workers,wall_time_s,solutions_found,speedup_vs_serial +1,1,1,42.7632,6,1.0000 diff --git a/benchmark/baseline_medium_seeded.csv b/benchmark/baseline_medium_seeded.csv new file mode 100644 index 000000000..d8c3ed0fe --- /dev/null +++ b/benchmark/baseline_medium_seeded.csv @@ -0,0 +1,2 @@ +ranks,threads,total_workers,wall_time_s,solutions_found,speedup_vs_serial +1,1,1,44.6928,6,1.0000 diff --git a/benchmark/baseline_small.csv b/benchmark/baseline_small.csv new file mode 100644 index 000000000..fc16f98e4 --- /dev/null +++ b/benchmark/baseline_small.csv @@ -0,0 +1,2 @@ +ranks,threads,total_workers,wall_time_s,solutions_found,speedup_vs_serial +1,1,1,3.7077,4,1.0000 diff --git a/benchmark/baseline_small_seeded.csv b/benchmark/baseline_small_seeded.csv new file mode 100644 index 000000000..a221c9cb3 --- /dev/null +++ b/benchmark/baseline_small_seeded.csv @@ -0,0 +1,2 @@ +ranks,threads,total_workers,wall_time_s,solutions_found,speedup_vs_serial +1,1,1,3.3135,4,1.0000 diff --git a/benchmark/inputs/huge.b2 b/benchmark/inputs/huge.b2 new file mode 100644 index 000000000..1da7e6315 --- /dev/null +++ b/benchmark/inputs/huge.b2 @@ -0,0 +1,22 @@ +CONFIG + +tracktype: 0; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5, x6, x7, x8, x9; +function f1, f2, f3, f4, f5, f6, f7, f8, f9; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; +f6 = x6^3 - 13; +f7 = x7^3 - 17; +f8 = x8^3 - 19; +f9 = x9^3 - 23; + +END; diff --git a/benchmark/inputs/huge_seeded.b2 b/benchmark/inputs/huge_seeded.b2 new file mode 100644 index 000000000..5c51fcded --- /dev/null +++ b/benchmark/inputs/huge_seeded.b2 @@ -0,0 +1,24 @@ +CONFIG + +tracktype: 0; + +randomseed: 314159; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5, x6, x7, x8, x9; +function f1, f2, f3, f4, f5, f6, f7, f8, f9; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; +f6 = x6^3 - 13; +f7 = x7^3 - 17; +f8 = x8^3 - 19; +f9 = x9^3 - 23; + +END; diff --git a/benchmark/inputs/large.b2 b/benchmark/inputs/large.b2 new file mode 100644 index 000000000..c6f678523 --- /dev/null +++ b/benchmark/inputs/large.b2 @@ -0,0 +1,19 @@ +CONFIG + +tracktype: 0; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5, x6; +function f1, f2, f3, f4, f5, f6; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; +f6 = x6^3 - 13; + +END; diff --git a/benchmark/inputs/large_seeded.b2 b/benchmark/inputs/large_seeded.b2 new file mode 100644 index 000000000..86c1c709f --- /dev/null +++ b/benchmark/inputs/large_seeded.b2 @@ -0,0 +1,21 @@ +CONFIG + +tracktype: 0; + +randomseed: 314159; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5, x6; +function f1, f2, f3, f4, f5, f6; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; +f6 = x6^3 - 13; + +END; diff --git a/benchmark/inputs/medium.b2 b/benchmark/inputs/medium.b2 new file mode 100644 index 000000000..08077edb0 --- /dev/null +++ b/benchmark/inputs/medium.b2 @@ -0,0 +1,18 @@ +CONFIG + +tracktype: 0; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5; +function f1, f2, f3, f4, f5; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; + +END; diff --git a/benchmark/inputs/medium_seeded.b2 b/benchmark/inputs/medium_seeded.b2 new file mode 100644 index 000000000..ecabf5f36 --- /dev/null +++ b/benchmark/inputs/medium_seeded.b2 @@ -0,0 +1,20 @@ +CONFIG + +tracktype: 0; + +randomseed: 314159; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5; +function f1, f2, f3, f4, f5; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; + +END; diff --git a/benchmark/inputs/small.b2 b/benchmark/inputs/small.b2 new file mode 100644 index 000000000..16676bbcb --- /dev/null +++ b/benchmark/inputs/small.b2 @@ -0,0 +1,16 @@ +CONFIG + +tracktype: 0; + +END; + +INPUT + +variable_group x1, x2, x3; +function f1, f2, f3; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; + +END; diff --git a/benchmark/inputs/small_seeded.b2 b/benchmark/inputs/small_seeded.b2 new file mode 100644 index 000000000..5faa7e732 --- /dev/null +++ b/benchmark/inputs/small_seeded.b2 @@ -0,0 +1,18 @@ +CONFIG + +tracktype: 0; + +randomseed: 314159; + +END; + +INPUT + +variable_group x1, x2, x3; +function f1, f2, f3; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; + +END; diff --git a/benchmark/inputs/xlarge.b2 b/benchmark/inputs/xlarge.b2 new file mode 100644 index 000000000..d453c30e4 --- /dev/null +++ b/benchmark/inputs/xlarge.b2 @@ -0,0 +1,20 @@ +CONFIG + +tracktype: 0; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5, x6, x7; +function f1, f2, f3, f4, f5, f6, f7; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; +f6 = x6^3 - 13; +f7 = x7^3 - 17; + +END; diff --git a/benchmark/inputs/xlarge_seeded.b2 b/benchmark/inputs/xlarge_seeded.b2 new file mode 100644 index 000000000..40ec6f1af --- /dev/null +++ b/benchmark/inputs/xlarge_seeded.b2 @@ -0,0 +1,22 @@ +CONFIG + +tracktype: 0; + +randomseed: 314159; + +END; + +INPUT + +variable_group x1, x2, x3, x4, x5, x6, x7; +function f1, f2, f3, f4, f5, f6, f7; + +f1 = x1^3 - 2; +f2 = x2^3 - 3; +f3 = x3^3 - 5; +f4 = x4^3 - 7; +f5 = x5^3 - 11; +f6 = x6^3 - 13; +f7 = x7^3 - 17; + +END; diff --git a/benchmark/run_benchmark.py b/benchmark/run_benchmark.py new file mode 100755 index 000000000..6e1ff44cd --- /dev/null +++ b/benchmark/run_benchmark.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +Benchmark parallel speedup of the bertini2 CLI solver. + +Sweeps MPI rank counts and OMP thread counts, times each run, and emits +a CSV showing wall time and speedup relative to the serial (1 rank, 1 thread) baseline. + +Usage: + python run_benchmark.py \ + --bertini2 ./build/core/bertini2 \ + --input benchmark/inputs/medium.b2 \ + --ranks 1 2 4 8 \ + --threads 1 2 4 \ + --output results.csv + +Requirements: + - mpirun must be on PATH + - bertini2 must be compiled with MPI support (BERTINI2_HAVE_MPI) + - OMP_NUM_THREADS controls threads per MPI rank +""" + +import argparse +import csv +import math +import os +import shutil +import subprocess +import sys +import tempfile +import time + + +def parse_args(): + p = argparse.ArgumentParser(description="Benchmark bertini2 parallel speedup") + p.add_argument("--bertini2", default="./build/core/bertini2", + help="Path to bertini2 executable (default: ./build/core/bertini2)") + p.add_argument("--input", required=True, + help="Bertini input file to solve") + p.add_argument("--ranks", nargs="+", type=int, default=[1, 2, 4], + help="MPI rank counts to sweep (default: 1 2 4)") + p.add_argument("--threads", nargs="+", type=int, default=[1], + help="OMP thread counts to sweep (default: 1)") + p.add_argument("--output", default="benchmark_results.csv", + help="CSV output file (default: benchmark_results.csv)") + p.add_argument("--timeout", type=float, default=600.0, + help="Per-run timeout in seconds (default: 600)") + p.add_argument("--repeats", type=int, default=1, + help="Number of timed repeats per (ranks, threads) combo (default: 1)") + p.add_argument("--mpirun", default="mpirun", + help="mpirun command (default: mpirun)") + return p.parse_args() + + +def run_once(bertini2_path, input_file, ranks, threads, mpirun_cmd, timeout): + """ + Run bertini2 with the given parallelism settings in a fresh temp directory. + + Returns (wall_time_s, solutions_found) or (float('nan'), -1) on failure. + """ + tmpdir = tempfile.mkdtemp(prefix="b2_bench_") + try: + dest_input = os.path.join(tmpdir, "input") + shutil.copy2(input_file, dest_input) + + env = os.environ.copy() + env["OMP_NUM_THREADS"] = str(threads) + + cmd = [mpirun_cmd, "-n", str(ranks), "--bind-to", "none", + os.path.abspath(bertini2_path)] + + t0 = time.perf_counter() + try: + result = subprocess.run( + cmd, + cwd=tmpdir, + env=env, + capture_output=True, + timeout=timeout, + ) + except subprocess.TimeoutExpired: + print(f" TIMEOUT after {timeout}s", flush=True) + return float("nan"), -1 + t1 = time.perf_counter() + + if result.returncode != 0: + stderr_tail = result.stderr.decode(errors="replace")[-500:] + print(f" FAILED (exit {result.returncode}): {stderr_tail}", flush=True) + return float("nan"), -1 + + wall_time = t1 - t0 + solutions = _parse_solution_count(os.path.join(tmpdir, "main_data")) + return wall_time, solutions + + finally: + shutil.rmtree(tmpdir, ignore_errors=True) + + +def _parse_solution_count(main_data_path): + """Read solution count from the first line of main_data.""" + try: + with open(main_data_path) as f: + first_line = f.readline().strip() + return int(first_line) + except (OSError, ValueError): + return -1 + + +def validate_inputs(args): + if not os.path.isfile(args.input): + sys.exit(f"Error: input file not found: {args.input}") + if not os.path.isfile(args.bertini2): + sys.exit(f"Error: bertini2 executable not found: {args.bertini2}") + if shutil.which(args.mpirun) is None: + sys.exit(f"Error: {args.mpirun!r} not found on PATH") + + +def main(): + args = parse_args() + validate_inputs(args) + + ranks_list = sorted(set(args.ranks)) + threads_list = sorted(set(args.threads)) + + # Ensure serial baseline (1, 1) is always first + if 1 not in ranks_list: + ranks_list = [1] + ranks_list + if 1 not in threads_list: + threads_list = [1] + threads_list + + print(f"bertini2: {args.bertini2}") + print(f"input: {args.input}") + print(f"ranks: {ranks_list}") + print(f"threads: {threads_list}") + print(f"repeats: {args.repeats}") + print(f"output: {args.output}") + print() + + rows = [] + serial_time = None + + combos = [(r, t) for r in ranks_list for t in threads_list] + # Run (1,1) first regardless of order so speedup can be computed incrementally + combos = sorted(combos, key=lambda rt: (rt[0] != 1 or rt[1] != 1, rt[0], rt[1])) + + for ranks, threads in combos: + label = f"ranks={ranks}, threads={threads}" + times = [] + solutions = -1 + for rep in range(args.repeats): + rep_label = f" rep {rep+1}/{args.repeats}" if args.repeats > 1 else "" + print(f"Running {label}{rep_label} ... ", end="", flush=True) + t, sol = run_once(args.bertini2, args.input, ranks, threads, + args.mpirun, args.timeout) + if math.isnan(t): + print("failed") + times.append(float("nan")) + else: + print(f"{t:.2f}s ({sol} solutions)") + times.append(t) + solutions = sol + + valid = [t for t in times if not math.isnan(t)] + wall_time = min(valid) if valid else float("nan") + + if ranks == 1 and threads == 1 and not math.isnan(wall_time): + serial_time = wall_time + + if serial_time is not None and not math.isnan(wall_time): + speedup = serial_time / wall_time + else: + speedup = float("nan") + + rows.append({ + "ranks": ranks, + "threads": threads, + "total_workers": ranks * threads, + "wall_time_s": f"{wall_time:.4f}" if not math.isnan(wall_time) else "nan", + "solutions_found": solutions, + "speedup_vs_serial": f"{speedup:.4f}" if not math.isnan(speedup) else "nan", + }) + + # Write CSV + fieldnames = ["ranks", "threads", "total_workers", "wall_time_s", + "solutions_found", "speedup_vs_serial"] + with open(args.output, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + # Print summary table + print() + print(f"{'ranks':>6} {'threads':>7} {'workers':>7} {'time(s)':>9} {'solutions':>9} {'speedup':>8}") + print("-" * 60) + for row in rows: + print(f"{row['ranks']:>6} {row['threads']:>7} {row['total_workers']:>7} " + f"{row['wall_time_s']:>9} {row['solutions_found']:>9} {row['speedup_vs_serial']:>8}") + + print(f"\nResults written to: {args.output}") + + +if __name__ == "__main__": + main() diff --git a/benchmark/submit.slurm.sh b/benchmark/submit.slurm.sh new file mode 100755 index 000000000..af1bf365f --- /dev/null +++ b/benchmark/submit.slurm.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# SLURM job script for bertini2 parallel benchmark. +# Customize the #SBATCH directives and paths below for your cluster. + +#SBATCH --job-name=bertini2_benchmark +#SBATCH --nodes=4 # total number of nodes +#SBATCH --ntasks-per-node=1 # one MPI rank per node +#SBATCH --cpus-per-task=8 # CPU cores per rank (= OMP_NUM_THREADS) +#SBATCH --time=02:00:00 +#SBATCH --output=benchmark_%j.log +#SBATCH --error=benchmark_%j.err + +# --- Cluster-specific setup --- +# Uncomment / adjust as needed for your environment: +# module load mpi/openmpi-x86_64 +# module load python/3.10 +# source /path/to/your/conda/etc/profile.d/conda.sh && conda activate b2-ubuntu + +# --- Paths (edit these) --- +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BERTINI2="${REPO_ROOT}/build/core/bertini2" +BENCHMARK_SCRIPT="${REPO_ROOT}/benchmark/run_benchmark.py" +INPUT="${REPO_ROOT}/benchmark/inputs/large.b2" + +# Output CSV tagged with SLURM job ID +OUTPUT="${REPO_ROOT}/benchmark/results_${SLURM_JOB_ID:-local}.csv" + +# --- Sweep configuration --- +# Ranks to test. Should not exceed (--nodes * --ntasks-per-node). +RANKS="1 2 4" + +# Threads per rank. Should not exceed --cpus-per-task. +THREADS="1 2 4 8" + +# --- Run --- +python "${BENCHMARK_SCRIPT}" \ + --bertini2 "${BERTINI2}" \ + --input "${INPUT}" \ + --ranks ${RANKS} \ + --threads ${THREADS} \ + --output "${OUTPUT}" \ + --repeats 3 + +echo "Benchmark complete. Results: ${OUTPUT}" diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt index e7bc61136..1752886dc 100644 --- a/core/CMakeLists.txt +++ b/core/CMakeLists.txt @@ -15,6 +15,10 @@ set(CMAKE_CXX_EXTENSIONS OFF) #file(GLOB SOURCES src/*.cpp) option(ENABLE_UNIT_TESTING "Turn on building and running of unit tests while compiling" OFF) +option(BMP_EXPRESSION_TEMPLATES "Enable Boost.Multiprecision expression templates for mpfr_float, mpfr_complex, mpz_int, mpq_rational" ON) + +find_package(MPI) +option(ENABLE_MPI "Enable MPI parallelism (requires OpenMPI or MPICH; no Boost.MPI needed)" ${MPI_FOUND}) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Release") @@ -25,14 +29,18 @@ endif(NOT CMAKE_BUILD_TYPE) message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") message(STATUS "ENABLE_UNIT_TESTING: ${ENABLE_UNIT_TESTING}") +if(BMP_EXPRESSION_TEMPLATES) + add_compile_definitions(BMP_EXPRESSION_TEMPLATES=1) + message(STATUS "BMP expression templates: ON") +else() + message(STATUS "BMP expression templates: OFF") +endif() + if (WIN32) add_compile_options(/bigobj) add_compile_options(/EHsc) set(CMAKE_BUILD_TYPE Release) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) - set(BUILD_SHARED_LIBS TRUE) else () - # add_compile_definitions(-DBMP_EXPRESSION_TEMPLATES=1) add_compile_options( # put things for debug compile here $<$:-g> @@ -40,7 +48,9 @@ else () # and things for release here $<$:-O3> - $<$:-g> + # (debug info intentionally omitted from Release: CI strips it from + # wheels anyway, and -g bloats objects ~3-4x, exhausting disk on + # constrained build hosts.) # using O2 or O3 cuts runtime by over half. ) endif () @@ -71,6 +81,13 @@ set(_BOOST_REQUIRED_COMPONENTS thread ) +if(ENABLE_MPI AND NOT MPI_FOUND) + message(FATAL_ERROR "ENABLE_MPI=ON but no MPI installation was found.") +endif() +if(ENABLE_MPI) + message(STATUS "MPI enabled: ${MPI_CXX_COMPILER}") +endif() + find_package(Boost REQUIRED COMPONENTS ${_BOOST_REQUIRED_COMPONENTS}) # Boost 1.89.0 removed the boost_system library (it's header-only now). @@ -131,6 +148,7 @@ set(detail_headers set(function_tree_headers include/bertini2/function_tree.hpp include/bertini2/function_tree/node.hpp + include/bertini2/function_tree/gather.hpp include/bertini2/function_tree/forward_declares.hpp include/bertini2/function_tree/simplify.hpp include/bertini2/function_tree/operators/operator.hpp @@ -139,9 +157,7 @@ set(function_tree_headers include/bertini2/function_tree/symbols/differential.hpp include/bertini2/function_tree/symbols/special_number.hpp include/bertini2/function_tree/symbols/number.hpp - include/bertini2/function_tree/symbols/linear_product.hpp - include/bertini2/function_tree/roots/function.hpp - include/bertini2/function_tree/roots/jacobian.hpp + include/bertini2/function_tree/roots/named_expression.hpp include/bertini2/function_tree/operators/arithmetic.hpp include/bertini2/function_tree/operators/trig.hpp ) @@ -156,6 +172,7 @@ set(settings_headers set(functiontreeinclude_HEADERS include/bertini2/function_tree/node.hpp + include/bertini2/function_tree/gather.hpp include/bertini2/function_tree/factory.hpp include/bertini2/function_tree/forward_declares.hpp include/bertini2/function_tree/simplify.hpp @@ -173,15 +190,14 @@ set(functiontree_symbols_HEADERS include/bertini2/function_tree/symbols/differential.hpp include/bertini2/function_tree/symbols/special_number.hpp include/bertini2/function_tree/symbols/number.hpp - include/bertini2/function_tree/symbols/linear_product.hpp ) set(functiontree_roots_HEADERS - include/bertini2/function_tree/roots/function.hpp - include/bertini2/function_tree/roots/jacobian.hpp + include/bertini2/function_tree/roots/named_expression.hpp ) set(io_headers + include/bertini2/io/classic_writer.hpp include/bertini2/io/file_utilities.hpp include/bertini2/io/generators.hpp include/bertini2/io/parsing.hpp @@ -234,6 +250,9 @@ set(nag_datatypes_common_headers set(parallel_headers include/bertini2/parallel/initialize_finalize.hpp + include/bertini2/parallel/path_result.hpp + include/bertini2/parallel/manager.hpp + include/bertini2/parallel/worker.hpp ) set(parallel_rootinclude_HEADERS @@ -324,18 +343,19 @@ set(basics_sources set(function_tree_sources src/function_tree/node.cpp + src/function_tree/gather.cpp + src/function_tree/find.cpp src/function_tree/simplify.cpp + src/function_tree/canonical.cpp src/function_tree/operators/arithmetic.cpp src/function_tree/operators/trig.cpp - src/function_tree/linear_product.cpp src/function_tree/operators/operator.cpp src/function_tree/symbols/special_number.cpp src/function_tree/symbols/differential.cpp src/function_tree/symbols/symbol.cpp src/function_tree/symbols/variable.cpp src/function_tree/symbols/number.cpp - src/function_tree/roots/jacobian.cpp - src/function_tree/roots/function.cpp + src/function_tree/roots/named_expression.cpp ) set(parallel_sources @@ -358,18 +378,29 @@ set(tracking_source_files src/tracking/explicit_predictors.cpp ) +# explicit template instantiation TUs: the one home for the heavy +# endgame/zerodim instantiations (see ADR-0014); consumers get extern +# declarations from the umbrella headers and just link. +set(eti_source_files + src/eti/endgames_eti.cpp + src/eti/zero_dim_eti.cpp + src/eti/zero_dim_blackbox_eti.cpp +) + set(BERTINI2_LIBRARY_SOURCES ${basics_sources} ${function_tree_sources} ${parallel_sources} ${system_source_files} ${tracking_source_files} + ${eti_source_files} ) set(BERTINI2_EXE_SOURCES src/blackbox/bertini.cpp src/blackbox/main_mode_switch.cpp src/blackbox/argc_argv.cpp + src/blackbox/algorithm_builder.cpp ) set(BERTINI2_EXE_HEADERS @@ -392,6 +423,13 @@ add_library(bertini2 ${BERTINI2_LIBRARY_SOURCES} ${BERTINI2_LIBRARY_HEADERS}) add_library(bertini2::bertini2 ALIAS bertini2) +# The (static) library is linked into the shared _pybertini Python module, so +# its objects must be position-independent. Previously this requirement was +# accidentally satisfied because every consumer recompiled the library sources +# itself (with -fPIC); now that the library is compiled once and linked, mark +# it PIC explicitly. +set_target_properties(bertini2 PROPERTIES POSITION_INDEPENDENT_CODE ON) + target_include_directories(bertini2 SYSTEM PUBLIC ${GMP_INCLUDES} @@ -405,16 +443,7 @@ target_include_directories(bertini2 PUBLIC $ # /include ) - -# All library files -target_sources(bertini2 PUBLIC - ${BERTINI2_LIBRARY_SOURCES} - ${BERTINI2_LIBRARY_HEADERS} -) - if(WIN32) - set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS TRUE) - set(BUILD_SHARED_LIBS TRUE) target_compile_options(bertini2 PRIVATE $<$:-bigobj -MP>) target_compile_definitions(bertini2 PUBLIC "HAVE_SNPRINTF") @@ -426,6 +455,18 @@ target_link_libraries(bertini2 PUBLIC ${MPC_LIBRARIES}) target_link_libraries(bertini2 PUBLIC Eigen3::Eigen) target_link_libraries(bertini2 PUBLIC ${Boost_LIBRARIES}) +if(ENABLE_MPI) + # We use only the MPI C API. Including from C++ otherwise pulls in the deprecated + # MPI C++ bindings (the MPI:: namespace / mpicxx.h), which MPICH compiles in by default and + # which fail to build under modern C++ (clang especially) -- so the project compiled with + # OpenMPI but not MPICH. These macros tell both implementations to skip the C++ bindings. + # PUBLIC + on the compile command line, so they are defined before any include, and + # propagate to everything linking bertini2 (python bindings, blackbox). + target_compile_definitions(bertini2 PUBLIC BERTINI2_HAVE_MPI MPICH_SKIP_MPICXX OMPI_SKIP_MPICXX) + target_link_libraries(bertini2 PUBLIC MPI::MPI_CXX) + message(STATUS "bertini2 built with plain C MPI support (no Boost.MPI)") +endif() + if(SUFFIX_SO_VERSION) set_target_properties(bertini2 PROPERTIES SOVERSION ${PROJECT_VERSION}) @@ -434,6 +475,24 @@ endif(SUFFIX_SO_VERSION) add_executable(bertini2_exe) target_sources(bertini2_exe PUBLIC ${BERTINI2_EXE_SOURCES} ${BERTINI2_EXE_HEADERS}) target_link_libraries(bertini2_exe ${Boost_LIBRARIES} bertini2) +set_target_properties(bertini2_exe PROPERTIES OUTPUT_NAME bertini2) +# On MSVC/lld-link, linking an executable with OUTPUT_NAME matching the static library +# (bertini2) causes lld-link to write bertini2.lib as an import library, overwriting +# the static archive. /NOIMPLIB suppresses the import library entirely. +if(MSVC) + target_link_options(bertini2_exe PRIVATE /NOIMPLIB) +endif() + +# Precompile the four blackbox headers that collectively cover all exe source files. +# algorithm_builder.hpp is the heaviest (pulls in full tracking + NAG algorithm +# machinery); the others are lighter but still include significant Bertini subsystems. +target_precompile_headers(bertini2_exe PRIVATE + + + + + +) # todo: this should be made a devmode thing #target_compile_options(bertini2 PRIVATE -Wall -Wextra) @@ -625,20 +684,30 @@ if (ENABLE_UNIT_TESTING) set(B2_CLASS_TEST_SOURCES test/classes/boost_multiprecision_test.cpp test/classes/fundamentals_test.cpp + test/classes/precision_propagation_test.cpp test/classes/eigen_test.cpp test/classes/complex_test.cpp - test/classes/function_tree_test.cpp test/classes/function_tree_transform.cpp + test/classes/structural_hash_test.cpp + test/classes/interning_test.cpp test/classes/system_test.cpp test/classes/slp_test.cpp - test/classes/differentiate_test.cpp - test/classes/differentiate_wrt_var.cpp + test/classes/eval_expression_test.cpp + test/classes/named_expression_test.cpp test/classes/homogenization_test.cpp test/classes/start_system_test.cpp test/classes/node_serialization_test.cpp test/classes/patch_test.cpp test/classes/slice_test.cpp test/classes/m_hom_start_system.cpp + test/classes/products_of_linears_block_test.cpp + test/classes/linear_forms_block_test.cpp + test/classes/blend_block_test.cpp + test/classes/randomization_block_test.cpp + test/classes/moving_homotopy_test.cpp + test/classes/system_printing_test.cpp + test/classes/system_blocks_test.cpp + test/classes/degrees_test.cpp test/classes/class_test.cpp ) @@ -755,6 +824,8 @@ if (ENABLE_UNIT_TESTING) test/tracking_basics/amp_criteria_test.cpp test/tracking_basics/amp_tracker_test.cpp test/tracking_basics/path_observers.cpp + test/tracking_basics/expand_to_function_tree_test.cpp + test/tracking_basics/amp_jacobian_estimate_test.cpp ) add_executable(test_tracking_basics ${B2_TRACKING_BASICS_TEST}) @@ -765,3 +836,30 @@ if (ENABLE_UNIT_TESTING) enable_testing() endif (ENABLE_UNIT_TESTING) + +# lld-link (Windows) won't pull data-only object files (e.g. explicit_predictors.obj, +# which holds all ExplicitRKPredictor Butcher-tableau statics) unless every object in +# the archive is forced in. CMake 3.24+ LINK_LIBRARY:WHOLE_ARCHIVE places the archive +# in the linker's input list (not just options) with the appropriate /WHOLEARCHIVE: flag. +# +# We strip the normal "bertini2" link entry first to avoid having the archive appear +# twice. $ re-adds it and propagates transitive +# interface deps (gmp, mpfr, boost, MPI, etc.) automatically. +# /FORCE:MULTIPLE: Boost.Serialization void_cast_register and oserializer templates are +# instantiated in multiple TUs within bertini2 itself; /WHOLEARCHIVE exposes all copies +# and /FORCE:MULTIPLE picks the first (same as weak/COMDAT on Linux/macOS). +if(MSVC AND ENABLE_UNIT_TESTING) + foreach(_t test_classes test_blackbox test_classic test_endgames test_generating + test_nag_algorithms test_nag_datatypes test_pool test_settings + test_tracking_basics) + if(TARGET ${_t}) + get_target_property(_ll ${_t} LINK_LIBRARIES) + if(_ll) + list(REMOVE_ITEM _ll bertini2) + set_target_properties(${_t} PROPERTIES LINK_LIBRARIES "${_ll}") + endif() + target_link_options(${_t} PRIVATE /FORCE:MULTIPLE) + target_link_libraries(${_t} "$") + endif() + endforeach() +endif() diff --git a/core/include/bertini2/blackbox/algorithm_builder.hpp b/core/include/bertini2/blackbox/algorithm_builder.hpp index 982ec1b0f..eedb93d33 100644 --- a/core/include/bertini2/blackbox/algorithm_builder.hpp +++ b/core/include/bertini2/blackbox/algorithm_builder.hpp @@ -34,10 +34,13 @@ #pragma once #include "bertini2/io/file_utilities.hpp" +#include "bertini2/nag_algorithms/common/algorithm_base.hpp" namespace bertini { namespace blackbox { +using algorithm::AnyAlgorithm; + /** \brief A de-serializer, whose responsibility it is to construct runnable objects based on input files. @@ -46,17 +49,23 @@ This class is inspired by the SimBuilder class from Hythem Sidky's SAPHRON packa */ class AlgoBuilder { - + public: AlgoBuilder() = default; /** - \brief Method for constructing an algorithm from a bertini classic input file + \brief Construct an algorithm from pre-split CONFIG and INPUT strings. + + Call SplitIntoConfigAndInput before this. Splitting is done outside + so that the caller can broadcast the strings over MPI before invoking + ClassicBuild on all ranks. + + \returns 0 on success, nonzero on failure. */ - int ClassicBuild(boost::filesystem::path const& input_file); + int ClassicBuild(std::string const& config_str, std::string const& input_str); /** - \brief Returns a non-owning pointer to the built algorithm + \brief Returns a non-owning pointer to the built algorithm (nullptr if not yet built). */ AnyAlgorithm* GetAlg() { diff --git a/core/include/bertini2/blackbox/argc_argv.hpp b/core/include/bertini2/blackbox/argc_argv.hpp index 440e712f2..b0f6fef35 100644 --- a/core/include/bertini2/blackbox/argc_argv.hpp +++ b/core/include/bertini2/blackbox/argc_argv.hpp @@ -28,14 +28,29 @@ #pragma once +#include + namespace bertini{ /** -Main initial function for doing stuff to interpret the command-line arguments for invokation of the program. +\brief Parsed results of the command-line arguments. +*/ +struct ParsedArgs { + boost::filesystem::path input_file{"input"}; // Bertini1 default filename +}; + +/** +Parse command-line arguments and return a ParsedArgs struct. + +Recognized flags: + -f use as the input file (overrides default "input") + bare positional argument treated as input file path + --help print usage and exit + --version print version and exit -\param argc The number of arguments to the program. Must be at least one. -\param argv array of character arrays, the arguments to the program when called. +\param argc The number of arguments. +\param argv The argument strings. */ -void ParseArgcArgv(int argc, char** argv); +ParsedArgs ParseArgcArgv(int argc, char** argv); } //namespace bertini diff --git a/core/include/bertini2/blackbox/global_configs.hpp b/core/include/bertini2/blackbox/global_configs.hpp index f89030ddc..c484c1505 100644 --- a/core/include/bertini2/blackbox/global_configs.hpp +++ b/core/include/bertini2/blackbox/global_configs.hpp @@ -51,14 +51,14 @@ namespace { struct Configs { - + using Tracking = detail::TypeList; using Endgame = detail::TypeList; template - using Algorithm = detail::TypeList, classic::AlgoChoice>; + using Algorithm = detail::TypeList; template using All = detail::ListCat>; diff --git a/core/include/bertini2/blackbox/main_mode_switch.hpp b/core/include/bertini2/blackbox/main_mode_switch.hpp index a3e85ff81..acc7e2811 100644 --- a/core/include/bertini2/blackbox/main_mode_switch.hpp +++ b/core/include/bertini2/blackbox/main_mode_switch.hpp @@ -30,11 +30,16 @@ #pragma once - - +#include "bertini2/blackbox/argc_argv.hpp" namespace bertini{ - void MainModeSwitch(); + /** + Route to the appropriate algorithm based on parsed arguments and the + tracktype found in the input file's CONFIG section. + + \returns 0 on success, nonzero on failure or unimplemented tracktype. + */ + int MainModeSwitch(ParsedArgs const& args); } diff --git a/core/include/bertini2/blackbox/switches_zerodim.hpp b/core/include/bertini2/blackbox/switches_zerodim.hpp index 1176d05fd..ec7808393 100644 --- a/core/include/bertini2/blackbox/switches_zerodim.hpp +++ b/core/include/bertini2/blackbox/switches_zerodim.hpp @@ -52,6 +52,28 @@ struct ZeroDimRT }; +/** +\brief Infer which start system to use from the target system's variable-group structure. + +This replicates classic Bertini, which chooses the start system from how the user +groups the variables rather than from a dedicated setting: + +- a single affine variable group, with no homogeneous variable groups -> total degree + (the 1-homogeneous Bezout start system); +- anything else with grouping -- two or more variable groups, or one or more + homogeneous variable groups -- -> multihomogeneous, using that partition. + +User-defined homotopies are not inferred here; they come with their own start system. +*/ +inline type::Start InferStartType(System const& sys) +{ + if (sys.NumVariableGroups() == 1 && sys.NumHomVariableGroups() == 0) + return type::Start::TotalDegree; + else + return type::Start::MHom; +} + + template class SystemManagementPol, typename ... ConstTs> std::unique_ptr ZeroDimSpecifyComplete(ConstTs const& ...ts) { @@ -68,15 +90,17 @@ std::unique_ptr ZeroDimSpecifyComplete(ConstTs const& ... template std::unique_ptr ZeroDimSpecifyShouldClone(std::true_type, ConstTs const& ...ts) { - return ZeroDimSpecifyComplete::Cauchy, policy::CloneGiven>(ts...); + // honor EndgameType! until 2026-06-12 this hardcoded the Cauchy endgame, + // so selecting PowerSeries through the blackbox silently ran Cauchy. + return ZeroDimSpecifyComplete(ts...); } template std::unique_ptr ZeroDimSpecifyShouldClone(std::false_type, ConstTs const& ...ts) { - return ZeroDimSpecifyComplete::Cauchy, policy::RefToGiven>(ts...); + return ZeroDimSpecifyComplete(ts...); } @@ -94,6 +118,7 @@ std::unique_ptr ZeroDimSpecifyEndgame(ZeroDimRT const& rt return ZeroDimSpecifyShouldClone::Cauchy>(typename StorageSelector::ShouldClone(), ts...); } + throw std::runtime_error("unrecognized endgame type in ZeroDimSpecifyEndgame"); } template @@ -108,6 +133,7 @@ std::unique_ptr ZeroDimSpecifyTracker(ZeroDimRT const& rt case type::Tracker::Adaptive: return ZeroDimSpecifyEndgame(rt, ts...); } + throw std::runtime_error("unrecognized tracker type in ZeroDimSpecifyTracker"); } template @@ -122,6 +148,7 @@ std::unique_ptr ZeroDimSpecifyStart(ZeroDimRT const& rt, case type::Start::User: throw std::runtime_error("trying to use generic zero dim with user homotopy. use the specific UserBlaBla instead"); } + throw std::runtime_error("unrecognized start system type in ZeroDimSpecifyStart"); } template diff --git a/core/include/bertini2/common/config.hpp b/core/include/bertini2/common/config.hpp index 0e9e9ed71..d880e3d9e 100644 --- a/core/include/bertini2/common/config.hpp +++ b/core/include/bertini2/common/config.hpp @@ -61,6 +61,7 @@ namespace bertini MinTrackTimeReached, SecurityMaxNormReached, CycleNumTooHigh, + FailedToSelectPrecisionAndStepsize, }; diff --git a/core/include/bertini2/detail/configured.hpp b/core/include/bertini2/detail/configured.hpp index a8cd60c26..8727a730c 100644 --- a/core/include/bertini2/detail/configured.hpp +++ b/core/include/bertini2/detail/configured.hpp @@ -53,7 +53,7 @@ namespace bertini { \code template const - typename std::enable_if::BaseRealType>>::value, T>::type + typename std::enable_if::BaseRealT>>::value, T>::type & Get() const { return Config::template Get(); @@ -61,7 +61,7 @@ namespace bertini { template const - typename std::enable_if::BaseRealType>>::value, T>::type + typename std::enable_if::BaseRealT>>::value, T>::type & Get() const { return EndgameBase::template Get(); @@ -69,14 +69,14 @@ namespace bertini { template - typename std::enable_if::BaseRealType>>::value, void>::type + typename std::enable_if::BaseRealT>>::value, void>::type Set(T const& t) { Config::template Set(t); } template - typename std::enable_if::BaseRealType>>::value, void>::type + typename std::enable_if::BaseRealT>>::value, void>::type Set(T const& t) { EndgameBase::template Set(t); diff --git a/core/include/bertini2/detail/events.hpp b/core/include/bertini2/detail/events.hpp index bedd7c1de..4970bf84e 100644 --- a/core/include/bertini2/detail/events.hpp +++ b/core/include/bertini2/detail/events.hpp @@ -42,6 +42,20 @@ namespace bertini { \brief Strawman Event type, enabling polymorphism. This class is abstract, and should never actually be created. + + \par Lifetime contract (important!) + Events are short-lived stack temporaries. An observable emits one with + `NotifyObservers(SomeEvent(*this))`, so the event lives only for the + duration of that `NotifyObservers` call — i.e. only while `Observe()` is + running. Moreover, the concrete event subclasses hold their payload by + `const&` into the emitter's internal buffers (e.g. `resulting_point_`, + `start_point_`), which may themselves be reused on the next step. + + Therefore an observer **must not** store a reference or pointer to an event + (or to anything it returns by reference) past the return of `Observe()`. + If you need to keep data, copy it out by value inside `Observe()`. This is + exactly what the Python observer layer does: it reads values during the call + and copies them into its own storage. */ class AnyEvent { BOOST_TYPE_INDEX_REGISTER_CLASS diff --git a/core/include/bertini2/detail/observable.hpp b/core/include/bertini2/detail/observable.hpp index 62b975f2a..ed08f12b5 100644 --- a/core/include/bertini2/detail/observable.hpp +++ b/core/include/bertini2/detail/observable.hpp @@ -37,77 +37,289 @@ #include "bertini2/detail/observer.hpp" #include "bertini2/detail/events.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +#include + namespace bertini{ + /** + \brief Thrown when an observer is attached to an observable it cannot observe. + + The Python bindings translate this to a TypeError. + */ + struct IncompatibleObserver : public std::runtime_error + { + using std::runtime_error::runtime_error; + }; + /** \brief An abstract observable type, maintaining a list of observers, who can be notified in case of Events. - + Some known observable types are Tracker and Endgame. */ class Observable - { + { public: virtual ~Observable() = default; + Observable() = default; + + /** + Copies do NOT inherit the watcher list: observers subscribed to the source + object, not to the copy. This makes copies safe to use on other threads — + a fresh copy notifies nobody until observers are explicitly added to it. + Copy-assignment likewise leaves the target's own watchers untouched. + */ + Observable(Observable const&) {} + Observable& operator=(Observable const&) { return *this; } + /** - \brief Add an observer, to observe this observable. + \brief Add an observer (non-owning) to observe this observable. + + Stores only a raw reference: the caller is responsible for keeping the + observer alive while it is attached. This is the right overload for + stack-allocated C++ observers added and removed within a scope. For an + observer whose lifetime you would rather have tracked, see the + shared_ptr overload. + + Observers that override SubscribedEventTypes() with a non-empty list are + registered in a type-indexed map so NotifyObservers only calls them for + events they declared interest in. Observers returning an empty list (the + default) are placed in a catch-all list and receive every event. + + Safe to call from inside an observer's Observe() (i.e. during a + notification): the add is deferred and applied once the current + notification loop finishes. This is what lets a "meta-observer" attach + new observers in response to events. + + \throws IncompatibleObserver if the observer's ObservedKind() is not one + this observable can be observed as. */ void AddObserver(AnyObserver& new_observer) const { - if (find_if(begin(current_watchers_), end(current_watchers_), [&](const auto& held_obs) - { return &held_obs.get() == &new_observer; })==end(current_watchers_)) - - current_watchers_.push_back(std::ref(new_observer)); + RejectIfIncompatible(new_observer); + Watcher w; + w.raw = &new_observer; // non-owning: owned stays null + EnqueueOrApplyAdd(std::move(w)); } /** - \brief Remove an observer from this observable. + \brief Add an observer whose lifetime is co-owned by this observable. + + The observable keeps a shared_ptr, so the observer stays alive (and keeps + receiving events) for as long as it is attached, even after the caller + drops their own reference — "attach it and forget it", with no dangling. + It is released when RemoveObserver is called or this observable is + destroyed. (Stack-allocated C++ observers can't be shared_ptr-owned; use + the reference overload for those.) + + \throws IncompatibleObserver if incompatible (see the reference overload). */ - void RemoveObserver(AnyObserver& observer) const + void AddObserver(std::shared_ptr const& new_observer) const { + RejectIfIncompatible(*new_observer); + Watcher w; + w.owned = new_observer; // owning: keeps the observer alive + w.raw = new_observer.get(); + EnqueueOrApplyAdd(std::move(w)); + } - auto new_end = std::remove_if(current_watchers_.begin(), current_watchers_.end(), - [&](const auto& held_obs) - { return &held_obs.get() == &observer; }); + /** + \brief Remove an observer from this observable. - current_watchers_.erase(new_end, current_watchers_.end()); + Like AddObserver, this is safe to call during a notification: the removal + is deferred until the current notification loop finishes. + */ + void RemoveObserver(AnyObserver& observer) const + { + if (dispatch_depth_ > 0) + { + PendingOp op; + op.kind = PendingOp::Remove; + op.watcher.raw = &observer; + pending_ops_.push_back(std::move(op)); + } + else + RemoveWatcher(&observer); + } + /** + \brief Whether this observable may be observed as the given type. - // current_watchers_.erase(std::remove(current_watchers_.begin(), current_watchers_.end(), std::ref(observer)), current_watchers_.end()); + Drives the compatibility check in AddObserver. The default accepts the + wildcard `typeid(void)` and this observable's own dynamic type. An + observable that emits its events templated on a base type (so a single + observer type serves many concrete observables) should also accept that + base type — e.g. ZeroDim accepts AnyZeroDim. + */ + virtual bool ObservableIsA(std::type_index t) const + { + return t == std::type_index(typeid(void)) + || t == std::type_index(typeid(*this)); } protected: /** - \brief Sends an Event (more particularly, AnyEvent) to all watching observers of this object. + \brief Sends an event to observers that subscribed to its exact dynamic type, + then to all catch-all (untyped) observers. + + Mutations of the observer lists are deferred for the duration of a + notification (see AddObserver/RemoveObserver), so iterating the live lists + here is safe even when an observer subscribes/unsubscribes mid-dispatch. + An observer that returns ObserveResult::Unsubscribe is dropped once the + (possibly nested) notification completes. The depth counter keeps this + correct under re-entrant emission. + */ + void NotifyObservers(AnyEvent const& e) const + { + DispatchEvent(e); + } + + void NotifyObservers(AnyEvent& e) const + { + DispatchEvent(e); + } - This function could potentially be improved by filtering on the observer's desired event types, if known at compile time. This could potentially be a performance bottleneck (hopefully not!) since filtering can use `dynamic_cast`ing. One hopes this cost is overwhelmed by things like linear algebra and system evaluation. + private: - \param e The event to emit. Its type should be derived from AnyEvent. + /** + One entry in an observer list. Either non-owning (`owned` is null; the + caller guarantees the observer's lifetime) or owning (`owned` co-owns the + observer, keeping it alive while attached). `raw` is always the identity + used for dedup and removal, and the pointer dispatched through. */ - void NotifyObservers(AnyEvent const& e) const + struct Watcher + { + std::shared_ptr owned; // non-null iff owning + AnyObserver* raw = nullptr; // identity + the pointer events go to + }; + + using ObserverList = std::vector; + + struct PendingOp { + enum Kind { Add, Remove } kind; + Watcher watcher; // Add: the watcher (carries `owned` for owning adds); Remove: identity in .raw + }; - for (auto& obs : current_watchers_) - obs.get().Observe(e); + void RejectIfIncompatible(AnyObserver const& obs) const + { + if (!ObservableIsA(obs.ObservedKind())) + throw IncompatibleObserver( + "this observable (" + boost::core::demangle(typeid(*this).name()) + + ") does not accept an observer for " + + boost::core::demangle(obs.ObservedKind().name())); + } + void EnqueueOrApplyAdd(Watcher w) const + { + if (dispatch_depth_ > 0) + { + PendingOp op; + op.kind = PendingOp::Add; + op.watcher = std::move(w); // carries `owned`, so the observer stays alive until drained + pending_ops_.push_back(std::move(op)); + } + else + AddWatcher(std::move(w)); } - void NotifyObservers(AnyEvent & e) const + void AddWatcher(Watcher const& w) const { + auto present_in = [&](ObserverList const& c) { + return std::find_if(c.begin(), c.end(), + [&](Watcher const& x){ return x.raw == w.raw; }) != c.end(); + }; + + auto types = w.raw->SubscribedEventTypes(); + if (types.empty()) + { + if (!present_in(untyped_watchers_)) + untyped_watchers_.push_back(w); + } + else + { + for (auto& ti : types) + { + auto& bucket = typed_watchers_[ti]; + if (std::find_if(bucket.begin(), bucket.end(), + [&](Watcher const& x){ return x.raw == w.raw; }) == bucket.end()) + bucket.push_back(w); + } + } + } - for (auto& obs : current_watchers_) - obs.get().Observe(e); + void RemoveWatcher(AnyObserver* who) const + { + auto erase_from = [&](ObserverList& c) { + c.erase(std::remove_if(c.begin(), c.end(), + [&](Watcher const& x){ return x.raw == who; }), + c.end()); + }; + erase_from(untyped_watchers_); + for (auto& [ti, bucket] : typed_watchers_) + erase_from(bucket); } + void DrainPendingOps() const + { + // Apply in request order so that, within one notification, a remove + // followed by a re-add (or vice versa) lands on the intended state. + for (auto& op : pending_ops_) + { + if (op.kind == PendingOp::Add) + AddWatcher(op.watcher); + else + RemoveWatcher(op.watcher.raw); + } + pending_ops_.clear(); + } - private: + void DispatchEvent(AnyEvent const& e) const + { + ++dispatch_depth_; + + auto run = [&](ObserverList& bucket) { + for (auto& w : bucket) + { + // w.raw stays valid for the whole call: owning entries hold a + // shared_ptr, non-owning ones are the caller's responsibility. + if (w.raw->Observe(e) == ObserveResult::Unsubscribe) + { + PendingOp op; + op.kind = PendingOp::Remove; + op.watcher.raw = w.raw; + pending_ops_.push_back(std::move(op)); + } + } + }; + + auto it = typed_watchers_.find(std::type_index(typeid(e))); + if (it != typed_watchers_.end()) + run(it->second); + run(untyped_watchers_); + + --dispatch_depth_; + if (dispatch_depth_ == 0) + DrainPendingOps(); + } - using ObserverContainer = std::vector>; + mutable std::unordered_map typed_watchers_; + mutable ObserverList untyped_watchers_; - mutable ObserverContainer current_watchers_; + mutable unsigned dispatch_depth_ = 0; + mutable std::vector pending_ops_; }; } // namespace bertini diff --git a/core/include/bertini2/detail/observer.hpp b/core/include/bertini2/detail/observer.hpp index fbdb6d109..e7cd5ed8a 100644 --- a/core/include/bertini2/detail/observer.hpp +++ b/core/include/bertini2/detail/observer.hpp @@ -36,7 +36,9 @@ #define BERTINI_DETAIL_OBSERVER_HPP #include +#include #include +#include #include @@ -48,6 +50,19 @@ namespace bertini{ + /** + \brief The result an observer returns from Observe(), telling the observable + whether to keep sending it events. + + Returning `Unsubscribe` is the clean, dispatch-safe way for an observer to + stop receiving events: the observable removes it *after* the current + notification loop finishes (no mid-iteration mutation of the observer list). + This replaces the old pattern of calling `RemoveObserver(*this)` from inside + `Observe()`. + */ + enum class ObserveResult { KeepObserving, Unsubscribe }; + + /** \brief Strawman base class for Observer objects. @@ -60,12 +75,36 @@ namespace bertini{ /** \brief Observe the observable object being observed. This is probably in response to NotifyObservers. - + This virtual function must be overridden by actual observers, defining how they observe the observable they are observing, probably filtering events and doing something specific for different ones. \param e The event which was emitted by the observed object. + \return `ObserveResult::KeepObserving` to keep receiving events, or + `ObserveResult::Unsubscribe` to ask the observable to drop this + observer once the current notification finishes. + */ + virtual ObserveResult Observe(AnyEvent const& e) = 0; + + /** + \brief Declares which event types this observer wants to receive. + + Return a non-empty vector to opt into type-indexed dispatch: the observable will + only call Observe() for events whose dynamic type exactly matches one of the + returned type_index values. Return an empty vector (the default) to receive + every event — this is the correct choice for observers that handle many or all + event types, and is the automatic behavior for Python-defined observers. + */ + virtual std::vector SubscribedEventTypes() const { return {}; } + + /** + \brief The type this observer expects to observe. + + Used by Observable::AddObserver to reject attaching an observer to an + incompatible observable (which would otherwise silently never fire). + The default is `typeid(void)`, a wildcard meaning "attach me anywhere"; + Observer reports `typeid(ObservedT)`. */ - virtual void Observe(AnyEvent const& e) = 0; + virtual std::type_index ObservedKind() const { return typeid(void); } }; @@ -83,7 +122,7 @@ namespace bertini{ public: virtual ~Observer() = default; - + std::type_index ObservedKind() const override { return typeid(ObservedT); } }; @@ -107,13 +146,26 @@ namespace bertini{ /** \brief Observe override which calls the overrides for the types you glued together. + The bundle keeps observing as long as at least one of its children still + wants events; it asks to unsubscribe only once every glued-in observer has + returned Unsubscribe. + \param e The emitted event which caused observation. */ - void Observe(AnyEvent const& e) override - { + ObserveResult Observe(AnyEvent const& e) override + { using namespace boost::fusion; - auto f = [&e](auto &obs) { obs.Observe(e); }; + bool keep = false; + // Route through the AnyObserver interface: a glued-in observer may be a + // TypedObserver, whose typed Observe(ConcreteEvent const&) overloads hide + // the untyped Observe(AnyEvent const&) for ordinary name lookup. The + // virtual call reaches the right dispatcher in every case. + auto f = [&e,&keep](auto &obs) { + if (static_cast(obs).Observe(e) == ObserveResult::KeepObserving) + keep = true; + }; for_each(observers_, f); + return keep ? ObserveResult::KeepObserving : ObserveResult::Unsubscribe; } std::tuple...> observers_; @@ -121,6 +173,68 @@ namespace bertini{ }; + /** + \brief CRTP base that turns an observer into a set of typed Observe() overloads. + + Instead of writing one `Observe(AnyEvent const&)` body full of `dynamic_cast`s + (and a matching hand-written `SubscribedEventTypes()`), derive from this and + provide one `ObserveResult OnEvent(ConcreteEvent const&)` overload per event you + care about. This helper: + - implements `SubscribedEventTypes()` from `ConcreteEvents...`, so the + observable routes only those exact event types here; and + - implements the single `Observe(AnyEvent const&)` virtual once, doing the + one downcast centrally and dispatching to the matching `OnEvent` overload on + `Derived`. + + The per-event handler is named `OnEvent` (not an `Observe` overload) so it does + not hide the `Observe(AnyEvent const&)` virtual -- keeping `Observe` the single + dispatch entry point and avoiding -Woverloaded-virtual noise. + + The downcast stays a `dynamic_cast` (so the dispatch semantics are identical to + the old hand-written observers), but it now lives in exactly one place rather + than being copy-pasted into every observer body. + + \tparam Derived The concrete observer (CRTP). + \tparam ObservedT The observed type (so this is still an Observer). + \tparam ConcreteEvents The exact event types to subscribe to, e.g. + `TrackingStarted`. `Derived` must provide an + `ObserveResult OnEvent(E const&)` overload for each one. + */ + template + class TypedObserver : public Observer + { BOOST_TYPE_INDEX_REGISTER_CLASS + public: + std::vector SubscribedEventTypes() const override + { + return { std::type_index(typeid(ConcreteEvents))... }; + } + + ObserveResult Observe(AnyEvent const& e) override + { + ObserveResult result = ObserveResult::KeepObserving; + bool matched = false; + // try each subscribed type; the first whose dynamic type matches wins. + (TryDispatch(e, result, matched), ...); + return result; + } + + virtual ~TypedObserver() = default; + + private: + template + void TryDispatch(AnyEvent const& e, ObserveResult& result, bool& matched) + { + if (matched) + return; + if (auto p = dynamic_cast(&e)) + { + matched = true; + result = static_cast(this)->OnEvent(*p); + } + } + }; + + } #endif diff --git a/core/include/bertini2/double_extensions.hpp b/core/include/bertini2/double_extensions.hpp index d054f8d7c..5c42e73f9 100644 --- a/core/include/bertini2/double_extensions.hpp +++ b/core/include/bertini2/double_extensions.hpp @@ -39,6 +39,10 @@ namespace bertini{ +// Forward declaration — full definition in random.hpp / random.cpp. +// Avoids pulling mpfr_complex.hpp into the Boost.Multiprecision include chain. +std::mt19937& ThreadEngine(); + using dbl = std::complex; using dbl_complex = std::complex; @@ -69,9 +73,8 @@ namespace bertini{ inline double RandReal() { - static std::default_random_engine generator; - static std::uniform_real_distribution distribution(-1.0,1.0); - return distribution(generator); + static thread_local std::uniform_real_distribution distribution(-1.0,1.0); + return distribution(ThreadEngine()); } namespace{ diff --git a/core/include/bertini2/eigen_extensions.hpp b/core/include/bertini2/eigen_extensions.hpp index 5fa8d8126..eb13084ae 100644 --- a/core/include/bertini2/eigen_extensions.hpp +++ b/core/include/bertini2/eigen_extensions.hpp @@ -75,21 +75,25 @@ namespace Eigen { return -highest(); } + // ThreadPrecision (thread-local), not DefaultPrecision (global): these + // tolerances are consumed inside tracking (LU solves, norms, convergence + // checks), which may run on std::thread workers whose precision is set + // thread-locally. On the main thread the two agree. inline static Real dummy_precision() { - using bertini::DefaultPrecision; - return pow( mpfr_real(10),-int(DefaultPrecision()-3)); + using bertini::ThreadPrecision; + return pow( mpfr_real(10),-int(ThreadPrecision()-3)); } inline static Real epsilon() { - using bertini::DefaultPrecision; - return pow(mpfr_real(10),-int(DefaultPrecision())); + using bertini::ThreadPrecision; + return pow(mpfr_real(10),-int(ThreadPrecision())); } static inline int digits10() { - return bertini::DefaultPrecision(); + return bertini::ThreadPrecision(); // return internal::default_digits10_impl::run(); } //http://www.manpagez.com/info/mpfr/mpfr-2.3.2/mpfr_31.php @@ -267,8 +271,8 @@ namespace Eigen { namespace bertini { - template using Vec = Eigen::Matrix; - template using Mat = Eigen::Matrix; + template using Vec = Eigen::Matrix; + template using Mat = Eigen::Matrix; /** @@ -422,7 +426,7 @@ namespace bertini { #endif // this loop won't test entry (0,0). it's tested separately after. - for (unsigned int ii = LU.rows()-1; ii > 0; ii--) + for (Eigen::Index ii = LU.rows()-1; ii > 0; ii--) { if (IsSmallValue(LU(ii,ii))) { @@ -516,7 +520,7 @@ namespace bertini { inline Vec RandomOfUnits(unsigned int size) { - return Vec(size).unaryExpr([](NumberType const& x) { return RandomUnit(); }); + return Vec(size).unaryExpr([](NumberType const&) { return RandomUnit(); }); } } diff --git a/core/include/bertini2/eigen_serialization_addon.hpp b/core/include/bertini2/eigen_serialization_addon.hpp index 0118f3257..70bdefaa0 100644 --- a/core/include/bertini2/eigen_serialization_addon.hpp +++ b/core/include/bertini2/eigen_serialization_addon.hpp @@ -27,7 +27,7 @@ friend class boost::serialization::access; template -void save(Archive & ar, const unsigned int version) const { +void save(Archive & ar, const unsigned int /*version*/) const { derived().eval(); const Eigen::Index rows = derived().rows(), cols = derived().cols(); ar & rows; @@ -38,7 +38,7 @@ void save(Archive & ar, const unsigned int version) const { } template -void load(Archive & ar, const unsigned int version) { +void load(Archive & ar, const unsigned int /*version*/) { Eigen::Index rows, cols; ar & rows; ar & cols; diff --git a/core/include/bertini2/endgames.hpp b/core/include/bertini2/endgames.hpp index 9d5177a8b..580732c98 100644 --- a/core/include/bertini2/endgames.hpp +++ b/core/include/bertini2/endgames.hpp @@ -40,3 +40,26 @@ #include "bertini2/endgames/observers.hpp" +// Explicit instantiation declarations — suppress re-instantiation of the +// endgame stack in every including TU. The definitions live in +// core/src/eti/endgames_eti.cpp; see ADR-0014. Adding a flavor or tracker? +// Extend both lists. +namespace bertini{ namespace endgame{ + +extern template class EndgameBase>, FixedPrecEndgame>; +extern template class EndgameBase>, FixedPrecEndgame>; +extern template class EndgameBase, AMPEndgame>; +extern template class EndgameBase>, FixedPrecEndgame>; +extern template class EndgameBase>, FixedPrecEndgame>; +extern template class EndgameBase, AMPEndgame>; + +extern template class PowerSeriesEndgame>; +extern template class PowerSeriesEndgame>; +extern template class PowerSeriesEndgame; +extern template class CauchyEndgame>; +extern template class CauchyEndgame>; +extern template class CauchyEndgame; + +}} // namespaces + + diff --git a/core/include/bertini2/endgames/amp_endgame.hpp b/core/include/bertini2/endgames/amp_endgame.hpp index c30629612..1b91b52d9 100644 --- a/core/include/bertini2/endgames/amp_endgame.hpp +++ b/core/include/bertini2/endgames/amp_endgame.hpp @@ -62,14 +62,14 @@ class AMPEndgame : public virtual EndgamePrecPolicyBase } static - void EnsureAtPrecision(double & obj, unsigned prec) + void EnsureAtPrecision(double & /*obj*/, unsigned prec) { if (prec!=DoublePrecision()) throw std::runtime_error("attempting to adjust precision of double to non-double precision"); } static - void EnsureAtPrecision(std::complex & obj, unsigned prec) + void EnsureAtPrecision(std::complex & /*obj*/, unsigned prec) { if (prec!=DoublePrecision()) throw std::runtime_error("attempting to adjust precision of std::complex to non-double precision"); @@ -97,7 +97,6 @@ class AMPEndgame : public virtual EndgamePrecPolicyBase using bertini::Precision; assert(Precision(current_time)==Precision(current_sample) && "precision of sample and time to be refined in AMP endgame must match"); - using RT = mpfr_float; using std::max; auto& TR = this->GetTracker(); TR.ChangePrecision(Precision(current_time)); @@ -110,13 +109,14 @@ class AMPEndgame : public virtual EndgamePrecPolicyBase if (refinement_success==SuccessCode::HigherPrecisionNecessary || refinement_success==SuccessCode::FailedToConverge) { - using bertini::Precision; - auto prev_precision = DefaultPrecision(); + // thread-local precision: endgames may run on std::thread workers, so + // neither read nor write the global default precision. + auto prev_precision = ThreadPrecision(); auto higher_precision = max(prev_precision,LowestMultiplePrecision())+ PrecisionIncrement(); - DefaultPrecision(higher_precision); + SetThreadPrecision(higher_precision); this->GetTracker().ChangePrecision(higher_precision); NotifyObservers(PrecisionChanged(*this, prev_precision, higher_precision)); @@ -129,7 +129,7 @@ class AMPEndgame : public virtual EndgamePrecPolicyBase auto time_higher_precision = current_time; Precision(time_higher_precision,higher_precision); - assert(time_higher_precision.precision()==DefaultPrecision()); + assert(time_higher_precision.precision()==ThreadPrecision()); refinement_success = this->GetTracker().Refine(result_higher_prec, next_sample_higher_prec, time_higher_precision, @@ -139,7 +139,7 @@ class AMPEndgame : public virtual EndgamePrecPolicyBase Precision(result, higher_precision); result = result_higher_prec; - assert(Precision(result)==DefaultPrecision()); + assert(Precision(result)==ThreadPrecision()); } return refinement_success; } diff --git a/core/include/bertini2/endgames/base_endgame.hpp b/core/include/bertini2/endgames/base_endgame.hpp index e8624ccba..62c5890c4 100644 --- a/core/include/bertini2/endgames/base_endgame.hpp +++ b/core/include/bertini2/endgames/base_endgame.hpp @@ -96,15 +96,15 @@ class EndgameBase : public: using TrackerType = typename PrecT::TrackerType; - using BaseComplexType = typename tracking::TrackerTraits::BaseComplexType; - using BaseRealType = typename tracking::TrackerTraits::BaseRealType; + using BaseComplexT = typename tracking::TrackerTraits::BaseComplexT; + using BaseRealT = typename tracking::TrackerTraits::BaseRealT; using EmitterType = FlavorT; protected: - using BCT = BaseComplexType; - using BRT = BaseRealType; + using BCT = BaseComplexT; + using BRT = BaseRealT; using Configured = detail::Configured< typename AlgoTraits::NeededConfigs >; @@ -121,11 +121,14 @@ class EndgameBase : // universal endgame state variables - mutable Vec final_approximation_; - mutable Vec previous_approximation_; - mutable unsigned int cycle_number_ = 0; + mutable Vec final_approximation_; + mutable Vec previous_approximation_; + mutable unsigned int cycle_number_ = 0; mutable NumErrorT approximate_error_; + BCT start_time_{}; // endgame boundary; set via SetBoundaryTime() + BCT target_time_{}; // final target (default 0); set via SetTargetTime() + @@ -150,27 +153,31 @@ class EndgameBase : public: - /** - \brief The main function for running an endgame, from time to time, from a given point to a possibly singular solution. - */ - SuccessCode Run(const BCT & start_time, const Vec & start_point, BCT const& target_time) - { - return this->AsFlavor().RunImpl(start_time, start_point, target_time); - } + void SetBoundaryTime(BCT const& t) { start_time_ = t; } + void SetTargetTime (BCT const& t) { target_time_ = t; } + BCT const& BoundaryTime() const { return start_time_; } + BCT const& TargetTime() const { return target_time_; } /** - \brief Run the endgame, shooting for default time of t=0. + \brief Run the endgame from the stored boundary time to the stored target time. + + Re-precisions the stored times to match start_point before dispatching to RunImpl, + so the caller never needs to worry about precision alignment. - \see Run + Call SetBoundaryTime() before invoking Run(). */ - SuccessCode Run(BCT const& start_time, Vec const& start_point) + SuccessCode Run(Vec const& start_point) { - return Run(start_time, start_point, static_cast(0)); + using bertini::Precision; + auto prec = Precision(start_point); + BCT t = start_time_; Precision(t, prec); + BCT t0 = target_time_; Precision(t0, prec); + return this->AsFlavor().RunImpl(t, start_point, t0); } - template - SuccessCode RefineAllSamples(SampCont & samples, TimeCont & times) + template + SuccessCode RefineAllSamples(SampCont & samples, TimeCont & times) { for (size_t ii=0; iiRefineSampleImpl(result, current_sample, current_time, tol, max_iterations); } - void ChangePrecision(unsigned p) - { - AsFlavor().ChangePrecision(p); - PrecT::ChangePrecision(p); - ChangePrecision(this->final_approximation_,p); - ChangePrecision(this->previous_approximation_,p); - } + // note: a ChangePrecision(unsigned) lived here until 2026-06-12; it called + // flavor-level ChangePrecision methods that have never existed, so it could + // not compile -- it just was never instantiated until explicit template + // instantiation (ADR-0014) forced every member. zero callers; deleted. + // precision changes go through the PrecT policy (see prec_base.hpp). /** @@ -228,7 +233,7 @@ class EndgameBase : } explicit EndgameBase(TrackerType const& tr, const ConfigsAsTuple& settings ) : - Configured( settings ), PrecT(tr), EndgamePrecPolicyBase(tr) + EndgamePrecPolicyBase(tr), Configured( settings ), PrecT(tr) {} @@ -258,7 +263,15 @@ class EndgameBase : \brief Setter for the final tolerance. */ inline - void SetFinalTolerance(BRT const& ft){this->template Get().final_tolerance = ft;} + void SetFinalTolerance(NumErrorT const& ft) + { + // pre-ETI this member was never instantiated, hiding two defects: Get<> + // returns const& (assignment through it cannot compile), and the old + // BRT parameter type didn't match final_tolerance's storage (NumErrorT) + auto settings = this->template Get(); + settings.final_tolerance = ft; + this->Set(settings); + } @@ -266,9 +279,9 @@ class EndgameBase : /** \brief Get the most-recent approximation */ - template + template inline - const Vec& FinalApproximation() const + const Vec& FinalApproximation() const { return final_approximation_; } @@ -276,9 +289,9 @@ class EndgameBase : /** \brief Get the second-most-recent approximation */ - template + template inline - const Vec& PreviousApproximation() const + const Vec& PreviousApproximation() const { return previous_approximation_; } @@ -319,8 +332,8 @@ class EndgameBase : start_time: is the time when we start the endgame process usually this is .1 x_endgame_start: is the space value at start_time - times: a deque of time values. These values will be templated to be CT - samples: a deque of sample values that are in correspondence with the values in times. These values will be vectors with entries of CT. + times: a deque of time values. These values will be templated to be ComplexT + samples: a deque of sample values that are in correspondence with the values in times. These values will be vectors with entries of ComplexT. ## Output @@ -340,12 +353,12 @@ class EndgameBase : \param times A deque that will hold all the time values of the samples we are going to use to start the endgame. \param samples a deque that will hold all the samples corresponding to the time values in times. - \tparam CT The complex number type. + \tparam ComplexT The complex number type. */ - template - SuccessCode ComputeInitialSamples(const CT & start_time,const CT & target_time, const Vec & x_endgame_start, TimeCont & times, SampCont & samples) // passed by reference to allow times to be filled as well. + template + SuccessCode ComputeInitialSamples(const ComplexT & start_time,const ComplexT & target_time, const Vec & x_endgame_start, TimeCont & times, SampCont & samples) // passed by reference to allow times to be filled as well. { - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; assert(this->template Get().num_sample_points>0 && "number of sample points must be positive"); if (tracking::TrackerTraits::IsAdaptivePrec) @@ -361,10 +374,10 @@ class EndgameBase : auto num_vars = this->GetSystem().NumVariables(); //start at 1, because the input point is the 0th element. - for(int ii=1; ii < this->template Get().num_sample_points; ++ii) - { - times.emplace_back((times[ii-1] + target_time) * RT(this->template Get().sample_factor)); // next time is a point between the previous time and target time. - samples.emplace_back(Vec(num_vars)); // sample_factor gives us some point between the two, usually the midpoint. + for(unsigned ii=1; ii < this->template Get().num_sample_points; ++ii) + { + times.emplace_back((times[ii-1] + target_time) * RealT(this->template Get().sample_factor)); // next time is a point between the previous time and target time. + samples.emplace_back(Vec(num_vars)); // sample_factor gives us some point between the two, usually the midpoint. auto tracking_success = this->GetTracker().TrackPath(samples[ii],times[ii-1],times[ii],samples[ii-1]); this->EnsureAtPrecision(times[ii],Precision(samples[ii])); diff --git a/core/include/bertini2/endgames/cauchy.hpp b/core/include/bertini2/endgames/cauchy.hpp index ed5293261..a79646b6a 100644 --- a/core/include/bertini2/endgames/cauchy.hpp +++ b/core/include/bertini2/endgames/cauchy.hpp @@ -61,8 +61,8 @@ Below we demonstrate a basic usage of the CauchyEndgame class to find the singul The pattern is as described above: create an instance of the class, feeding it the system to be used, and the endgame boundary time and other variable values at the endgame boundary. \code{.cpp} using namespace bertini::tracking; -using RealT = tracking::TrackerTraits::BaseRealType; // Real types -using ComplexT = tracking::TrackerTraits::BaseComplexType; Complex types +using RealT = tracking::TrackerTraits::BaseRealT; // Real types +using ComplexT = tracking::TrackerTraits::BaseComplexT; Complex types // 1. Define the polynomial system that we wish to solve. System target_sys; @@ -175,8 +175,8 @@ class CauchyEndgame : using FinalEGT = CauchyEndgame; using TrackerType = typename PrecT::TrackerType; - using BaseComplexType = typename tracking::TrackerTraits::BaseComplexType; - using BaseRealType = typename tracking::TrackerTraits::BaseRealType; + using BaseComplexT = typename tracking::TrackerTraits::BaseComplexT; + using BaseRealT = typename tracking::TrackerTraits::BaseRealT; using EmitterType = CauchyEndgame; @@ -190,8 +190,8 @@ class CauchyEndgame : using TupleOfTimes = typename BaseEGT::TupleOfTimes; using TupleOfSamps = typename BaseEGT::TupleOfSamps; - using BCT = BaseComplexType; - using BRT = BaseRealType; + using BCT = BaseComplexT; + using BRT = BaseRealT; using Configs = typename AlgoTraits::NeededConfigs; using ConfigsAsTuple = typename Configs::ToTuple; @@ -226,50 +226,50 @@ class CauchyEndgame : /** \brief Function that clears all samples and times from data members for the Cauchy endgame */ - template + template void ClearTimesAndSamples() { - std::get >(pseg_times_).clear(); - std::get >(cauchy_times_).clear(); - std::get >(pseg_samples_).clear(); - std::get >(cauchy_samples_).clear();} + std::get >(pseg_times_).clear(); + std::get >(cauchy_times_).clear(); + std::get >(pseg_samples_).clear(); + std::get >(cauchy_samples_).clear();} /** \brief Setter for the time values for the power series approximation of the Cauchy endgame. */ - template - void SetPSEGTimes(TimeCont pseg_times_to_set) - { std::get >(pseg_times_) = pseg_times_to_set;} + template + void SetPSEGTimes(TimeCont pseg_times_to_set) + { std::get >(pseg_times_) = pseg_times_to_set;} /** \brief Getter for the time values for the power series approximation of the Cauchy endgame. */ - template - TimeCont& GetPSEGTimes() {return std::get >(pseg_times_);} - template - const TimeCont& GetPSEGTimes() const {return std::get >(pseg_times_);} + template + TimeCont& GetPSEGTimes() {return std::get >(pseg_times_);} + template + const TimeCont& GetPSEGTimes() const {return std::get >(pseg_times_);} /** \brief Setter for the space values for the power series approximation of the Cauchy endgame. */ - template - void SetPSEGSamples(SampCont const& pseg_samples_to_set) { std::get >(pseg_samples_) = pseg_samples_to_set;} + template + void SetPSEGSamples(SampCont const& pseg_samples_to_set) { std::get >(pseg_samples_) = pseg_samples_to_set;} /** \brief Getter for the space values for the power series approximation of the Cauchy endgame. Available in const and non-const flavors */ - template - SampCont& GetPSEGSamples() {return std::get >(pseg_samples_);} - template - const SampCont& GetPSEGSamples() const {return std::get >(pseg_samples_);} + template + SampCont& GetPSEGSamples() {return std::get >(pseg_samples_);} + template + const SampCont& GetPSEGSamples() const {return std::get >(pseg_samples_);} /** \brief Setter for the space values for the Cauchy endgame. */ - template - void SetCauchySamples(SampCont const& cauchy_samples_to_set) + template + void SetCauchySamples(SampCont const& cauchy_samples_to_set) { - std::get >(cauchy_samples_) = cauchy_samples_to_set; + std::get >(cauchy_samples_) = cauchy_samples_to_set; } /** @@ -277,36 +277,36 @@ class CauchyEndgame : Available in const and non-const flavors */ - template - SampCont& GetCauchySamples() + template + SampCont& GetCauchySamples() { - return std::get >(cauchy_samples_); + return std::get >(cauchy_samples_); } - template - const SampCont& GetCauchySamples() const { return std::get >(cauchy_samples_); } + template + const SampCont& GetCauchySamples() const { return std::get >(cauchy_samples_); } /** \brief Setter for the time values for the Cauchy endgame. */ - template - void SetCauchyTimes(TimeCont const& cauchy_times_to_set) + template + void SetCauchyTimes(TimeCont const& cauchy_times_to_set) { - std::get >(cauchy_times_) = cauchy_times_to_set; + std::get >(cauchy_times_) = cauchy_times_to_set; } /** \brief Getter for the time values for the Cauchy endgame. */ - template - TimeCont& GetCauchyTimes() + template + TimeCont& GetCauchyTimes() { - return std::get >(cauchy_times_); + return std::get >(cauchy_times_); } - template - const TimeCont& GetCauchyTimes() const + template + const TimeCont& GetCauchyTimes() const { - return std::get >(cauchy_times_); + return std::get >(cauchy_times_); } @@ -335,7 +335,7 @@ class CauchyEndgame : explicit CauchyEndgame(TrackerType const& tr, const ConfigsAsTuple& settings ) - : BaseEGT(tr, settings), EndgamePrecPolicyBase(tr) + : EndgamePrecPolicyBase(tr), BaseEGT(tr, settings) { } template< typename... Ts > @@ -368,23 +368,23 @@ class CauchyEndgame : ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. Depeding on the number of samples points, we make a polgon around the origin with that many vertices. This function should be called the same number of times as paths converging to the solution we are approximating. */ - template - SuccessCode CircleTrack(CT const& target_time) + template + SuccessCode CircleTrack(ComplexT const& target_time) { using bertini::Precision; - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; using std::acos; ValidateConfigs(); - auto& circle_times = std::get >(cauchy_times_); - auto& circle_samples = std::get >(cauchy_samples_); + auto& circle_times = std::get >(cauchy_times_); + auto& circle_samples = std::get >(cauchy_samples_); - CT starting_time = circle_times.back(); // take a COPY here, so won't invalidate it later + ComplexT starting_time = circle_times.back(); // take a COPY here, so won't invalidate it later // the initial sample has already been added to the sample repo... so don't do that here, please @@ -392,8 +392,8 @@ class CauchyEndgame : for (unsigned ii = 0; ii < this->EndgameSettings().num_sample_points; ++ii) { - const Vec& current_sample = circle_samples.back(); - const CT& current_time = circle_times.back(); + const Vec& current_sample = circle_samples.back(); + const ComplexT& current_time = circle_times.back(); #ifndef BERTINI_DISABLE_PRECISION_CHECKS if (Precision(current_time)!=Precision(current_sample)){ @@ -412,14 +412,14 @@ class CauchyEndgame : //Generalized since we could have a nonzero target time. using std::arg; - RT radius = abs(starting_time - target_time), angle = arg(starting_time - target_time); // generalized for nonzero target_time. + RealT radius = abs(starting_time - target_time), angle = arg(starting_time - target_time); // generalized for nonzero target_time. - auto next_sample = Vec(num_vars); - CT next_time = (ii==this->EndgameSettings().num_sample_points-1) + auto next_sample = Vec(num_vars); + ComplexT next_time = (ii==this->EndgameSettings().num_sample_points-1) ? starting_time : - polar(radius, (ii+1)*2*acos(static_cast(-1)) / (this->EndgameSettings().num_sample_points) + angle) + target_time; + polar(radius, (ii+1)*2*acos(static_cast(-1)) / (this->EndgameSettings().num_sample_points) + angle) + target_time; // If we are tracking to a nonzero target time we need to shift our values to track to. This is a step that may not be needed if target_time = 0 ; @@ -455,18 +455,18 @@ class CauchyEndgame : }//end CircleTrack - template - void AddToCauchyData(CT const& time, Vec const& sample) + template + void AddToCauchyData(ComplexT const& time, Vec const& sample) { - std::get>(cauchy_times_).push_back(time); - std::get>(cauchy_samples_).push_back(sample); + std::get>(cauchy_times_).push_back(time); + std::get>(cauchy_samples_).push_back(sample); } - template - void AddToPSData(CT const& time, Vec const& sample) + template + void AddToPSData(ComplexT const& time, Vec const& sample) { - std::get>(pseg_times_).push_back(time); - std::get>(pseg_samples_).push_back(sample); + std::get>(pseg_times_).push_back(time); + std::get>(pseg_samples_).push_back(sample); } /** @@ -483,32 +483,34 @@ class CauchyEndgame : ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. Consult page 53 of \cite bertinibook, for the reasoning behind this heuristic. */ - template - auto ComputeCOverK() const -> typename Eigen::NumTraits::Real + template + auto ComputeCOverK() const -> typename Eigen::NumTraits::Real {//Obtain samples for computing C over K. - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; using std::abs; using std::log; - const auto& pseg_samples = std::get >(pseg_samples_); + const auto& pseg_samples = std::get >(pseg_samples_); assert(pseg_samples.size()>=3); - const Vec & sample0 = pseg_samples[0]; - const Vec & sample1 = pseg_samples[1]; - const Vec & sample2 = pseg_samples[2]; + const Vec & sample0 = pseg_samples[0]; + const Vec & sample1 = pseg_samples[1]; + const Vec & sample2 = pseg_samples[2]; - Vec rand_vector = Vec::Random(sample0.size()); //should be a row vector for ease in multiplying. + Vec rand_vector(sample0.size()); + for (int ii = 0; ii < (int)sample0.size(); ++ii) + rand_vector(ii) = RandomUnit(); // //DO NOT USE Eigen .dot() it will do conjugate transpose which is not what we want. // //Also, the .transpose*rand_vector returns an expression template that we do .norm of since abs is not available for that expression type. - RT estimate = abs(log(abs((((sample2 - sample1).transpose()*rand_vector).template lpNorm())/(((sample1 - sample0).transpose()*rand_vector).template lpNorm())))); - estimate = abs(log(RT(this->EndgameSettings().sample_factor)))/estimate; + RealT estimate = abs(log(abs((((sample2 - sample1).transpose()*rand_vector).template lpNorm())/(((sample1 - sample0).transpose()*rand_vector).template lpNorm())))); + estimate = abs(log(RealT(this->EndgameSettings().sample_factor)))/estimate; if (estimate < 1) - return RT(1); + return RealT(1); else return estimate; @@ -526,22 +528,22 @@ class CauchyEndgame : false: if our ratios are not withing tolerances set by the user or by default. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. */ - template - bool CheckForCOverKStabilization(TimeCont const& c_over_k_array) const + template + bool CheckForCOverKStabilization(TimeCont const& c_over_k_array) const { - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; using std::abs; assert(c_over_k_array.size()>=GetCauchySettings().num_needed_for_stabilization); for(unsigned ii = 1; ii < GetCauchySettings().num_needed_for_stabilization ; ++ii) { - RT a = abs(c_over_k_array[ii-1]); - RT b = abs(c_over_k_array[ii]); + RealT a = abs(c_over_k_array[ii-1]); + RealT b = abs(c_over_k_array[ii]); - typename Eigen::NumTraits::Real divide = a; + typename Eigen::NumTraits::Real divide = a; if(a < b) divide = a/b; @@ -561,8 +563,8 @@ class CauchyEndgame : Output: An mpfr_float representing a tolerance threshold for declaring a loop to be closed. Details: Used in Bertini 1 as a heuristic for computing separatedness of roots. Decided to not be used since assumptions for this tolerance are not usually met. - template - mpfr_float FindToleranceForClosedLoop(CT x_time, Vec x_sample) + template + mpfr_float FindToleranceForClosedLoop(ComplexT x_time, Vec x_sample) { auto degree_max = std::max(this->GetTracker().AMP_config_.degree_bound,mpfr_float("2.0")); auto K = this->GetTracker().AMP_config_.coefficient_bound; @@ -584,7 +586,7 @@ class CauchyEndgame : } M = degree_max * (degree_max - 1) * N; auto jacobian_at_current_time = this->GetSystem().Jacobian(x_sample,x_time); - auto minimum_singular_value = Eigen::JacobiSVD< Mat >(jacobian_at_current_time).singularValues()(this->GetSystem().NumVariables() - 1 ); + auto minimum_singular_value = Eigen::JacobiSVD< Mat >(jacobian_at_current_time).singularValues()(this->GetSystem().NumVariables() - 1 ); auto norm_of_sample = x_sample.norm(); L = pow(norm_of_sample,degree_max - 2); auto tol = K * L * M; @@ -618,14 +620,13 @@ class CauchyEndgame : false: if we have not closed the loop ##Details: - \tparam CT The complex number type + \tparam ComplexT The complex number type */ - template + template bool CheckClosedLoop() { - using RT = typename Eigen::NumTraits::Real; - auto& times = std::get >(cauchy_times_); - auto& samples = std::get >(cauchy_samples_); + auto& times = std::get >(cauchy_times_); + auto& samples = std::get >(cauchy_samples_); if((samples.front() - samples.back()).template lpNorm() < this->GetTracker().TrackingTolerance()) { @@ -667,25 +668,25 @@ class CauchyEndgame : ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. It is important to know if we are within the endgame operating zone. This function allows us to have a check that heuristcially will tell us if we are. */ - template - bool RatioEGOperatingZoneTest(CT const& target_time) const + template + bool RatioEGOperatingZoneTest(ComplexT const& target_time) const { - using RT = typename Eigen::NumTraits::Real; - RT min(1e300); - RT max(0); - auto& times = std::get >(cauchy_times_); - auto& samples = std::get >(cauchy_samples_); + using RealT = typename Eigen::NumTraits::Real; + RealT min(1e300); + RealT max(0); + auto& times = std::get >(cauchy_times_); + auto& samples = std::get >(cauchy_samples_); if(norm(times.front() - target_time) < GetCauchySettings().ratio_cutoff_time) { return true; } else { - RT norm; + RealT norm; for(unsigned int ii=0; ii < this->EndgameSettings().num_sample_points; ++ii) { norm = samples[ii].template lpNorm(); @@ -735,10 +736,10 @@ class CauchyEndgame : tracked to an appropriate time. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. */ - template - SuccessCode InitialCauchyLoops(CT const& target_time) + template + SuccessCode InitialCauchyLoops(ComplexT const& target_time) { using std::max; // auto fail_safe_max_cycle_number = max(GetCauchySettings().fail_safe_maximum_cycle_number,this->CycleNumber()); @@ -750,7 +751,7 @@ class CauchyEndgame : while (loop_hasnt_closed) { this->CycleNumber(0); - ClearAndSeedCauchyData(); + ClearAndSeedCauchyData(); // track around a circle once. we'll use it to measure whether we believe we are in the eg operating zone, based on the ratio of norms of sample points around the circle auto tracking_success = CircleTrack(target_time); @@ -759,11 +760,11 @@ class CauchyEndgame : return tracking_success; // find the ratio of the maximum and minimum coordinate wise for the loop. - if (RatioEGOperatingZoneTest(target_time)) + if (RatioEGOperatingZoneTest(target_time)) { // then we believe we are in the EG operating zone, since the path is relatively flat. i still disbelieve this is a good test (dab 20160310) while (true) { - if (CheckClosedLoop()) + if (CheckClosedLoop()) {//error is small enough, exit the loop with success. NotifyObservers(ClosedLoop(*this)); initial_cauchy_loop_success = SuccessCode::Success; @@ -786,7 +787,7 @@ class CauchyEndgame : }//end if (RatioEGOperatingZoneTest()) else { - auto advance_success = AdvanceTime(target_time); + auto advance_success = AdvanceTime(target_time); if (advance_success!=SuccessCode::Success) return advance_success; } @@ -797,11 +798,11 @@ class CauchyEndgame : - template - void RotateOntoPS(CT const& next_time, Vec const& next_sample) + template + void RotateOntoPS(ComplexT const& next_time, Vec const& next_sample) { - auto& ps_times = std::get >(pseg_times_); - auto& ps_samples = std::get >(pseg_samples_); + auto& ps_times = std::get >(pseg_times_); + auto& ps_samples = std::get >(pseg_samples_); ps_times.pop_front(); ps_samples.pop_front(); @@ -810,13 +811,13 @@ class CauchyEndgame : ps_samples.push_back(next_sample); } - template + template void ClearAndSeedCauchyData() { - auto& cau_times = std::get >(cauchy_times_); - auto& cau_samples = std::get >(cauchy_samples_); - auto& ps_times = std::get >(pseg_times_); - auto& ps_samples = std::get >(pseg_samples_); + auto& cau_times = std::get >(cauchy_times_); + auto& cau_samples = std::get >(cauchy_samples_); + auto& ps_times = std::get >(pseg_times_); + auto& ps_samples = std::get >(pseg_samples_); cau_times.clear(); cau_samples.clear(); @@ -840,7 +841,7 @@ class CauchyEndgame : ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. This function is in charge of finding the very first approximation of the origin. It does this by first computing some initial samples like what is done in the Power Series Endgame. We continue to track forward in this manner until we have stabilization of the cycle number being approximated. @@ -848,15 +849,15 @@ class CauchyEndgame : Once we have stabilization we then perform InitialCauchyLoops while getting the accurate cycle number, and check the norms of the samples and make sure we are ready to approximate. */ - template - SuccessCode InitialApproximation(CT const& start_time, Vec const& start_point, - CT const& target_time, Vec & approximation) + template + SuccessCode InitialApproximation(ComplexT const& start_time, Vec const& start_point, + ComplexT const& target_time, Vec & approximation) { auto init_success = GetIntoEGZone(start_time, start_point, target_time); if (init_success!= SuccessCode::Success) return init_success; - auto cauchy_loop_success = InitialCauchyLoops(target_time); + auto cauchy_loop_success = InitialCauchyLoops(target_time); if (cauchy_loop_success != SuccessCode::Success) return cauchy_loop_success; @@ -866,44 +867,44 @@ class CauchyEndgame : - template - SuccessCode GetIntoEGZone(CT const& start_time, Vec const& start_point, CT const& target_time) + template + SuccessCode GetIntoEGZone(ComplexT const& start_time, Vec const& start_point, ComplexT const& target_time) { - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; //initialize array holding c_over_k estimates - std::deque c_over_k; + std::deque c_over_k; - auto& ps_times = std::get >(pseg_times_); - auto& ps_samples = std::get >(pseg_samples_); + auto& ps_times = std::get >(pseg_times_); + auto& ps_samples = std::get >(pseg_samples_); //Compute initial samples for pseg auto initial_sample_success = this->ComputeInitialSamples(start_time, target_time, start_point, ps_times, ps_samples); if (initial_sample_success!=SuccessCode::Success) return initial_sample_success; - c_over_k.push_back(ComputeCOverK()); + c_over_k.push_back(ComputeCOverK()); //track until for more c_over_k estimates or until we reach a cutoff time. for (unsigned ii = 0; ii < GetCauchySettings().num_needed_for_stabilization; ++ii) { - auto advance_success = AdvanceTime(target_time); + auto advance_success = AdvanceTime(target_time); if (advance_success!=SuccessCode::Success) return advance_success; - c_over_k.push_back(ComputeCOverK()); + c_over_k.push_back(ComputeCOverK()); }//end while //have we stabilized yet? while(!CheckForCOverKStabilization(c_over_k) && abs(ps_times.back()-target_time) > GetCauchySettings().cycle_cutoff_time) { - auto advance_success = AdvanceTime(target_time); + auto advance_success = AdvanceTime(target_time); if (advance_success!=SuccessCode::Success) return advance_success; c_over_k.pop_front(); - c_over_k.push_back(ComputeCOverK()); + c_over_k.push_back(ComputeCOverK()); }//end while @@ -922,17 +923,16 @@ class CauchyEndgame : SuccessCode deeming if we were suceessful, or if we encountered an error. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. We can compute the Cauchy Integral Formula in this particular instance by computing the mean of the samples we have collected around the origin. /todo i believe this function works incorrectly when the target time is not 0. hence, the target time needs to be passed in. */ - template - SuccessCode ComputeCauchyApproximationOfXAtT0(Vec& result) + template + SuccessCode ComputeCauchyApproximationOfXAtT0(Vec& result) { - using RT = typename Eigen::NumTraits::Real; - auto& cau_times = std::get >(cauchy_times_); - auto& cau_samples = std::get >(cauchy_samples_); + auto& cau_times = std::get >(cauchy_times_); + auto& cau_samples = std::get >(cauchy_samples_); if (cau_samples.size() != this->CycleNumber() * this->EndgameSettings().num_sample_points+1) { @@ -951,11 +951,11 @@ class CauchyEndgame : auto total_num_pts = this->CycleNumber() * this->EndgameSettings().num_sample_points; - this->template RefineAllSamples(cau_samples, cau_times); + this->template RefineAllSamples(cau_samples, cau_times); Precision(result, Precision(cau_samples.back())); - result = Vec::Zero(this->GetSystem().NumVariables()); + result = Vec::Zero(this->GetSystem().NumVariables()); for(unsigned int ii = 0; ii < total_num_pts; ++ii) result += cau_samples[ii]; result /= this->CycleNumber() * this->EndgameSettings().num_sample_points; @@ -977,17 +977,17 @@ class CauchyEndgame : ##Details: the starting time and point for this routine are the most recent power series samples. - \tparam CT The complex number type. + \tparam ComplexT The complex number type. This function populates the deque cauchy_samples and cauchy_times. These are data members of the class and are not passed in. This function will continue to call CircleTrack until we have closed the loop. */ - template - SuccessCode ComputeCauchySamples(CT const& target_time) + template + SuccessCode ComputeCauchySamples(ComplexT const& target_time) { using bertini::Precision; - ClearAndSeedCauchyData(); + ClearAndSeedCauchyData(); this->CycleNumber(0); @@ -1001,7 +1001,7 @@ class CauchyEndgame : { return tracking_success; } - else if(CheckClosedLoop()) + else if(CheckClosedLoop()) { return SuccessCode::Success; } @@ -1018,25 +1018,25 @@ class CauchyEndgame : If the distance between next and target is too small, dies (returns not success). */ - template - SuccessCode AdvanceTime(CT const& target_time) + template + SuccessCode AdvanceTime(ComplexT const& target_time) { - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; - auto& ps_times = std::get >(pseg_times_); - auto& ps_samples = std::get >(pseg_samples_); + auto& ps_times = std::get >(pseg_times_); + auto& ps_samples = std::get >(pseg_samples_); auto& current_time = ps_times.back(); auto& current_sample = ps_samples.back(); //Generalized next_time in case if we are not trying to converge to the t = 0. - CT next_time = (target_time-current_time) * static_cast(this->EndgameSettings().sample_factor)+current_time; + ComplexT next_time = (target_time-current_time) * static_cast(this->EndgameSettings().sample_factor)+current_time; if (abs(next_time - target_time) < this->EndgameSettings().min_track_time)//we are too close to t = 0 but we do not have the correct tolerance - so we exit return SuccessCode::MinTrackTimeReached; // advance in time - Vec next_sample; + Vec next_sample; auto time_advance_success = this->GetTracker().TrackPath(next_sample,current_time, next_time, current_sample); if (time_advance_success != SuccessCode::Success) { @@ -1065,17 +1065,17 @@ class CauchyEndgame : SuccessCode: reporting if we were successful in the endgame or if we encountered an error ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. This function runs the entire Cauchy Endgame. We first take our endgame boundary time value and sample to find a first approximation of the origin. This is done by using the idea for the power series endgame. We check for stabilization of the cycle number, and check to see when the ratios of the maximum and minimum norm of samples collected by CircleTrack are withing a tolerance. When both of these conditions are met we do a Hermite interpolation. At this point we can start tracking in to the origin while using CircleTrack to compute samples and calculating their mean to get an approximation of the origin using the Cauchy Integral Formula. */ - template - SuccessCode RunImpl(CT const& start_time, Vec const& start_point, CT const& target_time) + template + SuccessCode RunImpl(ComplexT const& start_time, Vec const& start_point, ComplexT const& target_time) { - if (start_point.size()!=this->GetSystem().NumVariables()) + if (start_point.size()!=static_cast(this->GetSystem().NumVariables())) { std::stringstream err_msg; err_msg << "number of variables in start point for CauchyEG, " << start_point.size() << ", must match the number of variables in the system, " << this->GetSystem().NumVariables(); @@ -1089,14 +1089,14 @@ class CauchyEndgame : throw std::runtime_error(ss.str()); } - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; - Vec& latest_approx = this->final_approximation_; - Vec& prev_approx = this->previous_approximation_; + Vec& latest_approx = this->final_approximation_; + Vec& prev_approx = this->previous_approximation_; NumErrorT& approx_error = this->approximate_error_; - ClearTimesAndSamples(); //clear times and samples before we begin. + ClearTimesAndSamples(); //clear times and samples before we begin. this->CycleNumber(0); prev_approx = start_point; @@ -1104,21 +1104,24 @@ class CauchyEndgame : if (init_success!= SuccessCode::Success) return init_success; - auto cauchy_loop_success = InitialCauchyLoops(target_time); + auto cauchy_loop_success = InitialCauchyLoops(target_time); if (cauchy_loop_success != SuccessCode::Success) return cauchy_loop_success; - RT norm_of_dehom_prev, norm_of_dehom_latest; + // initialized to 0 so the security check below never reads indeterminate values. + // NOTE: the guard on the assignment (level <= 0) and the guard on the check in the loop (level != 0) + // appear inconsistent with each other and with the PowerSeries endgame, which uses (level <= 0) for both. + RealT norm_of_dehom_prev(0), norm_of_dehom_latest(0); if(this->SecuritySettings().level <= 0) - norm_of_dehom_prev = this->GetSystem().DehomogenizePoint(prev_approx).template lpNorm(); + norm_of_dehom_prev = this->GetSystem().InfinityNormOfDehomogenized(prev_approx); do { //Compute a cauchy approximation. Uses the previously computed samples, //either from InitialCauchyLoops, or ComputeCauchySamples - auto extrapolation_success = ComputeCauchyApproximationOfXAtT0(latest_approx); + auto extrapolation_success = ComputeCauchyApproximationOfXAtT0(latest_approx); if (extrapolation_success!=SuccessCode::Success) return extrapolation_success; @@ -1133,7 +1136,7 @@ class CauchyEndgame : if (this->SecuritySettings().level) {//we are too large, break out of loop to return error. - norm_of_dehom_latest = this->GetSystem().DehomogenizePoint(latest_approx).template lpNorm(); + norm_of_dehom_latest = this->GetSystem().InfinityNormOfDehomogenized(latest_approx); if (norm_of_dehom_prev > this->SecuritySettings().max_norm && norm_of_dehom_latest > this->SecuritySettings().max_norm ) @@ -1146,7 +1149,7 @@ class CauchyEndgame : prev_approx = latest_approx; norm_of_dehom_prev = norm_of_dehom_latest; - auto advance_success = AdvanceTime(target_time); + auto advance_success = AdvanceTime(target_time); if (advance_success != SuccessCode::Success) return advance_success; diff --git a/core/include/bertini2/endgames/config.hpp b/core/include/bertini2/endgames/config.hpp index ea9586d3d..4ffdb3bb4 100644 --- a/core/include/bertini2/endgames/config.hpp +++ b/core/include/bertini2/endgames/config.hpp @@ -33,6 +33,7 @@ #include "bertini2/detail/typelist.hpp" #include "bertini2/common/config.hpp" +#include "bertini2/mpfr_extensions.hpp" namespace bertini{ namespace endgame{ @@ -107,7 +108,7 @@ namespace bertini{ namespace endgame{ unsigned num_sample_points = 3; //NumSamplePoints default = 2 T min_track_time = T(1e-100); //nbrh radius in Bertini book. NbhdRadius - mpq_rational sample_factor = mpq_rational(1,2); //SampleFactor + mpq_rational sample_factor{1, 2}; //SampleFactor — mpq_rational: exact, no floating-point precision to go stale unsigned max_num_newton_iterations = 15; // the maximum number allowable iterations during endgames, for points used to approximate the final solution. diff --git a/core/include/bertini2/endgames/events.hpp b/core/include/bertini2/endgames/events.hpp index aa18d49c8..7e41efbf4 100644 --- a/core/include/bertini2/endgames/events.hpp +++ b/core/include/bertini2/endgames/events.hpp @@ -93,7 +93,7 @@ namespace bertini { { BOOST_TYPE_INDEX_REGISTER_CLASS public: - using CT = typename ObservedT::BaseComplexType; + using ComplexT = typename ObservedT::BaseComplexT; /** \brief The constructor for a CircleAdvanced Event. @@ -102,8 +102,8 @@ namespace bertini { \param next The precision after changing. */ CircleAdvanced(const ObservedT & obs, - Vec const& new_point, - CT const& new_time) : EndgameEvent(obs), + Vec const& new_point, + ComplexT const& new_time) : EndgameEvent(obs), new_point_(new_point), new_time_(new_time) {} @@ -117,8 +117,8 @@ namespace bertini { const auto& NewTime() const {return new_time_;} private: - const Vec& new_point_; - const CT& new_time_; + const Vec& new_point_; + const ComplexT& new_time_; }; diff --git a/core/include/bertini2/endgames/fixed_prec_endgame.hpp b/core/include/bertini2/endgames/fixed_prec_endgame.hpp index 3f5c62849..88be1a161 100644 --- a/core/include/bertini2/endgames/fixed_prec_endgame.hpp +++ b/core/include/bertini2/endgames/fixed_prec_endgame.hpp @@ -48,11 +48,11 @@ class FixedPrecEndgame : public virtual EndgamePrecPolicyBase { public: using TrackerType = TrackerT; - using BaseComplexType = typename tracking::TrackerTraits::BaseComplexType; - using BaseRealType = typename tracking::TrackerTraits::BaseRealType; + using BaseComplexT = typename tracking::TrackerTraits::BaseComplexT; + using BaseRealT = typename tracking::TrackerTraits::BaseRealT; - using BCT = BaseComplexType; - using BRT = BaseRealType; + using BCT = BaseComplexT; + using BRT = BaseRealT; template static @@ -76,15 +76,11 @@ class FixedPrecEndgame : public virtual EndgamePrecPolicyBase SuccessCode RefineSampleImpl(Vec & result, Vec const& current_sample, BCT const& current_time, double tol, unsigned max_iterations) const { - using RT = mpfr_float; - using std::max; - auto& TR = this->GetTracker(); - auto refinement_success = this->GetTracker().Refine(result,current_sample,current_time, tol, max_iterations); - return SuccessCode::Success; + return refinement_success; } explicit diff --git a/core/include/bertini2/endgames/interpolation.hpp b/core/include/bertini2/endgames/interpolation.hpp index 422f6721a..f26f3e66e 100644 --- a/core/include/bertini2/endgames/interpolation.hpp +++ b/core/include/bertini2/endgames/interpolation.hpp @@ -57,10 +57,10 @@ namespace bertini{ \param times A deque that will hold all the time values of the samples we are going to use to start the endgame. \param samples a deque that will hold all the samples corresponding to the time values in times. -\tparam CT The complex number type. +\tparam ComplexT The complex number type. */ -template - Vec HermiteInterpolateAndSolve(CT const& target_time, const unsigned int num_sample_points, const TimeCont & times, const SampCont & samples, const SampCont & derivatives, ContStart shift_from = ContStart::Back) +template + Vec HermiteInterpolateAndSolve(ComplexT const& target_time, const unsigned int num_sample_points, const TimeCont & times, const SampCont & samples, const SampCont & derivatives, ContStart shift_from = ContStart::Back) { assert((times.size() >= num_sample_points) && "must have sufficient number of sample times"); assert((samples.size() >= num_sample_points) && "must have sufficient number of sample points"); @@ -70,17 +70,17 @@ template unsigned num_t, num_s, num_d; if (shift_from == ContStart::Back) { - num_t = times.size()-1; - num_s = samples.size()-1; - num_d = derivatives.size()-1; + num_t = static_cast(times.size()-1); + num_s = static_cast(samples.size()-1); + num_d = static_cast(derivatives.size()-1); } else { num_t = num_s = num_d = num_sample_points-1; } - Mat< Vec > space_differences(2*num_sample_points,2*num_sample_points); - Vec time_differences(2*num_sample_points); + Mat< Vec > space_differences(2*num_sample_points,2*num_sample_points); + Vec time_differences(2*num_sample_points); for(unsigned int ii=0; ii //Start of Result from Hermite polynomial, this is using the diagonal of the //finite difference matrix. - Vec Result = space_differences(2*num_sample_points - 1,2*num_sample_points - 1); + Vec Result = space_differences(2*num_sample_points - 1,2*num_sample_points - 1); //This builds the hermite polynomial from the highest term down. diff --git a/core/include/bertini2/endgames/observers.hpp b/core/include/bertini2/endgames/observers.hpp index ff1398ea8..617f8ee0d 100644 --- a/core/include/bertini2/endgames/observers.hpp +++ b/core/include/bertini2/endgames/observers.hpp @@ -52,11 +52,11 @@ struct GoryDetailLogger : public Observer {BOOST_TYPE_INDEX_REGISTER_CLASS using EmitterT = EndgameT; -using BCT = typename EndgameT::BaseComplexType; +using BCT = typename EndgameT::BaseComplexT; virtual ~GoryDetailLogger() = default; -virtual void Observe(AnyEvent const& e) override +virtual ObserveResult Observe(AnyEvent const& e) override { if(auto p = dynamic_cast*>(&e)) { @@ -104,6 +104,8 @@ virtual void Observe(AnyEvent const& e) override { BOOST_LOG_TRIVIAL(severity_level::debug) << "unprogrammed response for event of type " << boost::typeindex::type_id_runtime(e).pretty_name(); } + + return ObserveResult::KeepObserving; } }; // gory detail diff --git a/core/include/bertini2/endgames/powerseries.hpp b/core/include/bertini2/endgames/powerseries.hpp index 7a3d5329d..13622304f 100644 --- a/core/include/bertini2/endgames/powerseries.hpp +++ b/core/include/bertini2/endgames/powerseries.hpp @@ -59,8 +59,8 @@ The pattern is as described above: create an instance of the class, feeding it t \code{.cpp} using namespace bertini::tracking; -using RealT = tracking::TrackerTraits::BaseRealType; // Real types -using ComplexT = tracking::TrackerTraits::BaseComplexType; Complex types +using RealT = tracking::TrackerTraits::BaseRealT; // Real types +using ComplexT = tracking::TrackerTraits::BaseComplexT; Complex types // 1. Define the polynomial system that we wish to solve. System target_sys; @@ -175,8 +175,8 @@ class PowerSeriesEndgame : using FinalEGT = PowerSeriesEndgame; using TrackerType = typename BaseEGT::TrackerType; - using BaseComplexType = typename BaseEGT::BaseComplexType; - using BaseRealType = typename BaseEGT::BaseRealType; + using BaseComplexT = typename BaseEGT::BaseComplexT; + using BaseRealT = typename BaseEGT::BaseRealT; using EmitterType = PowerSeriesEndgame; @@ -187,8 +187,8 @@ class PowerSeriesEndgame : using TupleOfTimes = typename BaseEGT::TupleOfTimes; using TupleOfSamps = typename BaseEGT::TupleOfSamps; - using BCT = BaseComplexType; - using BRT = BaseRealType; + using BCT = BaseComplexT; + using BRT = BaseRealT; using Configs = typename AlgoTraits::NeededConfigs; using ConfigsAsTuple = typename Configs::ToTuple; @@ -218,21 +218,25 @@ class PowerSeriesEndgame : */ mutable Vec rand_vector_; - template + template void AssertSizesTimeSpace() const { +#ifndef NDEBUG const auto num_sample_points = this->EndgameSettings().num_sample_points; - assert(std::get >(samples_).size()==std::get >(times_).size() && "must have same number of samples in times and spaces"); - assert(std::get >(samples_).size()>=num_sample_points && "must have sufficient number of samples"); + assert(std::get >(samples_).size()==std::get >(times_).size() && "must have same number of samples in times and spaces"); + assert(std::get >(samples_).size()>=num_sample_points && "must have sufficient number of samples"); +#endif } - template + template void AssertSizesTimeSpaceDeriv() const { +#ifndef NDEBUG const auto num_sample_points = this->EndgameSettings().num_sample_points; - assert(std::get >(samples_).size()==std::get >(times_).size() && "must have same number of samples in times and spaces"); - assert(std::get >(samples_).size()==std::get >(derivatives_).size() && "must have same number of samples in derivatives and spaces"); - assert(std::get >(samples_).size()>=num_sample_points && "must have sufficient number of samples"); + assert(std::get >(samples_).size()==std::get >(times_).size() && "must have same number of samples in times and spaces"); + assert(std::get >(samples_).size()==std::get >(derivatives_).size() && "must have same number of samples in derivatives and spaces"); + assert(std::get >(samples_).size()>=num_sample_points && "must have sufficient number of samples"); +#endif } public: @@ -243,24 +247,24 @@ class PowerSeriesEndgame : /** \brief Function that clears all samples and times from data members for the Power Series endgame */ - template + template void ClearTimesAndSamples() { - std::get >(times_).clear(); - std::get >(samples_).clear(); + std::get >(times_).clear(); + std::get >(samples_).clear(); } /** \brief Function to set the times used for the Power Series endgame. */ - template - void SetTimes(TimeCont const& times_to_set) { std::get >(times_) = times_to_set;} + template + void SetTimes(TimeCont const& times_to_set) { std::get >(times_) = times_to_set;} /** \brief Function to get the times used for the Power Series endgame. */ - template - const auto& GetTimes() const {return std::get >(times_);} + template + const auto& GetTimes() const {return std::get >(times_);} const BCT& LatestTimeImpl() const @@ -271,20 +275,25 @@ class PowerSeriesEndgame : /** \brief Function to set the space values used for the Power Series endgame. */ - template - void SetSamples(SampCont const& samples_to_set) { std::get >(samples_) = samples_to_set;} + template + void SetSamples(SampCont const& samples_to_set) { std::get >(samples_) = samples_to_set;} /** \brief Function to get the space values used for the Power Series endgame. */ - template - const auto& GetSamples() const {return std::get >(samples_);} + template + const auto& GetSamples() const {return std::get >(samples_);} /** \brief Function to set the times used for the Power Series endgame. */ - template - void SetRandVec(int size) {rand_vector_ = Vec::Random(size);} + template + void SetRandVec(int size) + { + rand_vector_.resize(size); + for (int ii = 0; ii < size; ++ii) + rand_vector_(ii) = RandomUnit(); + } @@ -292,7 +301,7 @@ class PowerSeriesEndgame : explicit PowerSeriesEndgame(TrackerType const& tr, const ConfigsAsTuple& settings ) - : BaseEGT(tr, settings), EndgamePrecPolicyBase(tr) + : EndgamePrecPolicyBase(tr), BaseEGT(tr, settings) {} template< typename... Ts > @@ -312,27 +321,27 @@ class PowerSeriesEndgame : upper_bound_on_cycle_number_: Used for an exhaustive search for the best cycle number for approimating the path to t = 0. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. */ - template + template unsigned ComputeBoundOnCycleNumber() { - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; using std::log; using std::abs; - const auto& samples = std::get >(samples_); + const auto& samples = std::get >(samples_); - AssertSizesTimeSpace(); + AssertSizesTimeSpace(); auto num_samples = samples.size(); - const Vec & sample0 = samples[num_samples-3]; - const Vec & sample1 = samples[num_samples-2]; - const Vec & sample2 = samples[num_samples-1]; // most recent sample. oldest samples at front of the container + const Vec & sample0 = samples[num_samples-3]; + const Vec & sample1 = samples[num_samples-2]; + const Vec & sample2 = samples[num_samples-1]; // most recent sample. oldest samples at front of the container // should this only be if the system is homogenized? - CT rand_sum1 = ((sample1 - sample0).transpose()*rand_vector_).sum(); - CT rand_sum2 = ((sample2 - sample1).transpose()*rand_vector_).sum(); + ComplexT rand_sum1 = ((sample1 - sample0).transpose()*rand_vector_).sum(); + ComplexT rand_sum2 = ((sample2 - sample1).transpose()*rand_vector_).sum(); if ( abs(rand_sum1)==0 || abs(rand_sum2)==0) // avoid division by 0 { @@ -340,7 +349,7 @@ class PowerSeriesEndgame : return upper_bound_on_cycle_number_; } - RT estimate = log(static_cast(this->EndgameSettings().sample_factor))/log(abs(rand_sum2/rand_sum1)); + RealT estimate = log(static_cast(this->EndgameSettings().sample_factor))/log(abs(rand_sum2/rand_sum1)); if (estimate < 1) // would be nan if sample points are same as each other upper_bound_on_cycle_number_ = 1; @@ -367,27 +376,26 @@ class PowerSeriesEndgame : cycle_number_: Used to create a hermite interpolation to t = 0. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. This is done by an exhaustive search from 1 to upper_bound_on_cycle_number. There is a conversion to the s-space from t-space in this function. As a by-product the derivatives at each of the samples is returned for further use. */ - template - unsigned ComputeCycleNumber(CT const& t0) + template + unsigned ComputeCycleNumber(ComplexT const& t0) { - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; - const auto& samples = std::get >(samples_); - const auto& times = std::get >(times_); - const auto& derivatives = std::get >(derivatives_); + const auto& samples = std::get >(samples_); + const auto& times = std::get >(times_); - AssertSizesTimeSpaceDeriv(); + AssertSizesTimeSpaceDeriv(); - const Vec &most_recent_sample = samples.back(); - const CT& most_recent_time = times.back(); + const Vec &most_recent_sample = samples.back(); + const ComplexT& most_recent_time = times.back(); //Compute upper bound for cycle number. - ComputeBoundOnCycleNumber(); + ComputeBoundOnCycleNumber(); unsigned num_pts; @@ -397,19 +405,18 @@ class PowerSeriesEndgame : num_pts = this->EndgameSettings().num_sample_points-1; - auto min_found_difference = Eigen::NumTraits::highest(); + auto min_found_difference = Eigen::NumTraits::highest(); - TimeCont s_times(num_pts); - SampCont s_derivatives(num_pts); + TimeCont s_times(num_pts); + SampCont s_derivatives(num_pts); - auto offset = samples.size() - num_pts - 1; // -1 here to shift away from the back of the container for(unsigned int candidate = 1; candidate <= upper_bound_on_cycle_number_; ++candidate) { using std::pow; std::tie(s_times, s_derivatives) = TransformToSPlane(candidate, t0, num_pts, ContStart::Front); - RT cand_power{1/static_cast(candidate)}; - RT curr_diff = (HermiteInterpolateAndSolve( + RealT cand_power{1/static_cast(candidate)}; + RealT curr_diff = (HermiteInterpolateAndSolve( pow((most_recent_time-t0)/(times[0]-t0),cand_power), // the target time num_pts,s_times,samples,s_derivatives, ContStart::Front) // the input data - @@ -438,14 +445,14 @@ class PowerSeriesEndgame : None: Derivatives are members of this class. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. */ - template + template void ComputeAllDerivatives() { - auto& samples = std::get >(samples_); - auto& times = std::get >(times_); - auto& derivatives = std::get >(derivatives_); + auto& samples = std::get >(samples_); + auto& times = std::get >(times_); + auto& derivatives = std::get >(derivatives_); assert((samples.size() == times.size()) && "must have same number of times and samples"); @@ -471,23 +478,23 @@ class PowerSeriesEndgame : this function also transforms them into the interval [0 1] */ - template - std::tuple, SampCont> TransformToSPlane(int cycle_num, CT const& t0, unsigned num_pts, ContStart shift_from) + template + std::tuple, SampCont> TransformToSPlane(int cycle_num, ComplexT const& t0, unsigned num_pts, ContStart shift_from) { if (cycle_num==0) throw std::runtime_error("cannot transform to s plane with cycle number 0"); - AssertSizesTimeSpaceDeriv(); + AssertSizesTimeSpaceDeriv(); - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; - const auto& times = std::get >(times_); - const auto& derivatives = std::get >(derivatives_); + const auto& times = std::get >(times_); + const auto& derivatives = std::get >(derivatives_); - RT c = static_cast(cycle_num); - RT one_over_c = 1/c; + RealT c = static_cast(cycle_num); + RealT one_over_c = 1/c; - unsigned offset_t, offset_d; + size_t offset_t, offset_d; if (shift_from == ContStart::Back) { offset_t = times.size()-num_pts; @@ -497,10 +504,10 @@ class PowerSeriesEndgame : offset_t = offset_d = 0; - TimeCont s_times(num_pts); - SampCont s_derivatives(num_pts); + TimeCont s_times(num_pts); + SampCont s_derivatives(num_pts); - CT time_shift = times[offset_t] - t0; + ComplexT time_shift = times[offset_t] - t0; for(unsigned ii = 0; ii < num_pts; ++ii){ s_times[ii] = pow((times[ii+offset_t]-t0)/time_shift, one_over_c); @@ -523,26 +530,26 @@ class PowerSeriesEndgame : SuccessCode: This reports back if we were successful in making an approximation. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. This function handles computing an approximation at the origin. We compute the cycle number best for the approximation, and convert derivatives and times to the s-plane where s = t^(1/c). We use the converted times and derivatives along with the samples to do a Hermite interpolation. */ - template - SuccessCode ComputeApproximationOfXAtT0(Vec& result, const CT & t0) + template + SuccessCode ComputeApproximationOfXAtT0(Vec& result, const ComplexT & t0) { - const auto c = ComputeCycleNumber(t0); + const auto c = ComputeCycleNumber(t0); auto num_pts = this->EndgameSettings().num_sample_points; - TimeCont s_times; - SampCont s_derivatives; + TimeCont s_times; + SampCont s_derivatives; std::tie(s_times, s_derivatives) = TransformToSPlane(c, t0, num_pts, ContStart::Back); // the data was transformed to be on the interval [0 1] so we can hard-code the time-to-solve as 0 here. Precision(result, Precision(s_derivatives.back())); - result = HermiteInterpolateAndSolve(CT(0), num_pts, s_times, std::get >(samples_), s_derivatives, ContStart::Back); + result = HermiteInterpolateAndSolve(ComplexT(0), num_pts, s_times, std::get >(samples_), s_derivatives, ContStart::Back); return SuccessCode::Success; }//end ComputeApproximationOfXAtT0 @@ -558,24 +565,23 @@ class PowerSeriesEndgame : SuccessCode: This reports back if we were successful in advancing time. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. This function computes the next time value for the power series endgame. After computing this time value, it will track to it and compute the derivative at this time value for further appoximations to be made during the endgame. */ - template - SuccessCode AdvanceTime(const CT & target_time) + template + SuccessCode AdvanceTime(const ComplexT & target_time) { - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; - auto& samples = std::get >(samples_); - auto& times = std::get >(times_); - auto& derivatives = std::get >(derivatives_); + auto& samples = std::get >(samples_); + auto& times = std::get >(times_); - AssertSizesTimeSpaceDeriv(); + AssertSizesTimeSpaceDeriv(); - Vec next_sample; - CT next_time = (times.back() + target_time) * static_cast(this->EndgameSettings().sample_factor); //setting up next time value using the midpoint formula, sample_factor will give us some + Vec next_sample; + ComplexT next_time = (times.back() + target_time) * static_cast(this->EndgameSettings().sample_factor); //setting up next time value using the midpoint formula, sample_factor will give us some if (abs(next_time - target_time) < this->EndgameSettings().min_track_time) // generalized for target_time not equal to 0. { @@ -634,36 +640,36 @@ class PowerSeriesEndgame : SuccessCode: This reports back if we were successful in advancing time. ##Details: - \tparam CT The complex number type. + \tparam ComplexT The complex number type. Tracking forward with the number of sample points, this function will make approximations using Hermite interpolation. This process will continue until two consecutive approximations are withing final tolerance of each other. */ - template - SuccessCode RunImpl(const CT & start_time, const Vec & start_point, CT const& target_time) + template + SuccessCode RunImpl(const ComplexT & start_time, const Vec & start_point, ComplexT const& target_time) { - if (start_point.size()!=this->GetSystem().NumVariables()) + if (start_point.size()!=static_cast(this->GetSystem().NumVariables())) { std::stringstream err_msg; err_msg << "number of variables in start point for PSEG, " << start_point.size() << ", must match the number of variables in the system, " << this->GetSystem().NumVariables(); throw std::runtime_error(err_msg.str()); } - DefaultPrecision(Precision(start_point)); + // thread-local only: the endgame may run on a std::thread worker. + SetThreadPrecision(Precision(start_point)); - using RT = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; //Set up for the endgame. - ClearTimesAndSamples(); + ClearTimesAndSamples(); // unpack some references for easy use - auto& samples = std::get >(samples_); - auto& times = std::get >(times_); - auto& derivatives = std::get >(derivatives_); - Vec& latest_approx = this->final_approximation_; - Vec& prev_approx = this->previous_approximation_; + auto& samples = std::get >(samples_); + auto& times = std::get >(times_); + Vec& latest_approx = this->final_approximation_; + Vec& prev_approx = this->previous_approximation_; // this is for estimating a ... norm? - SetRandVec(start_point.size()); + SetRandVec(static_cast(start_point.size())); auto initial_sample_success = this->ComputeInitialSamples(start_time, target_time, start_point, times, samples); @@ -674,8 +680,8 @@ class PowerSeriesEndgame : return initial_sample_success; } - this->template RefineAllSamples(samples, times); - ComputeAllDerivatives(); + this->template RefineAllSamples(samples, times); + ComputeAllDerivatives(); @@ -685,10 +691,10 @@ class PowerSeriesEndgame : if (extrapolation_code != SuccessCode::Success) return extrapolation_code; - RT norm_of_dehom_of_latest_approx; - RT norm_of_dehom_of_prev_approx; + RealT norm_of_dehom_of_latest_approx(0); // initialized to 0 so the security check never reads indeterminate values + RealT norm_of_dehom_of_prev_approx(0); if (this->SecuritySettings().level <= 0) - norm_of_dehom_of_prev_approx = this->GetSystem().DehomogenizePoint(prev_approx).template lpNorm(); + norm_of_dehom_of_prev_approx = this->GetSystem().InfinityNormOfDehomogenized(prev_approx); NumErrorT& approx_error = this->approximate_error_; @@ -696,7 +702,7 @@ class PowerSeriesEndgame : while (approx_error > this->FinalTolerance()) { - auto advance_code = AdvanceTime(target_time); + auto advance_code = AdvanceTime(target_time); if (advance_code!=SuccessCode::Success) { NotifyObservers(EndgameFailure(*this)); @@ -704,8 +710,8 @@ class PowerSeriesEndgame : } // this code is what bertini1 does... it refines all samples, like, all the time. - this->template RefineAllSamples(samples, times); - ComputeAllDerivatives(); + this->template RefineAllSamples(samples, times); + ComputeAllDerivatives(); extrapolation_code = ComputeApproximationOfXAtT0(latest_approx, target_time); if (extrapolation_code!=SuccessCode::Success) @@ -720,7 +726,7 @@ class PowerSeriesEndgame : if(this->SecuritySettings().level <= 0) { - norm_of_dehom_of_latest_approx = this->GetSystem().DehomogenizePoint(latest_approx).template lpNorm(); + norm_of_dehom_of_latest_approx = this->GetSystem().InfinityNormOfDehomogenized(latest_approx); if(norm_of_dehom_of_latest_approx > this->SecuritySettings().max_norm && norm_of_dehom_of_prev_approx > this->SecuritySettings().max_norm) { NotifyObservers(SecurityMaxNormReached(*this)); diff --git a/core/include/bertini2/endgames/prec_base.hpp b/core/include/bertini2/endgames/prec_base.hpp index d73efacdb..956e9c4bd 100644 --- a/core/include/bertini2/endgames/prec_base.hpp +++ b/core/include/bertini2/endgames/prec_base.hpp @@ -84,7 +84,8 @@ class EndgamePrecPolicyBase : public virtual Observable void ChangePrecision(unsigned p) { - tracker_.ChangePrecision(p); + // .get(): tracker_ is a reference_wrapper (never instantiated pre-ETI) + tracker_.get().ChangePrecision(p); } private: diff --git a/core/include/bertini2/function_tree.hpp b/core/include/bertini2/function_tree.hpp index bbf58ed36..ad05c4520 100644 --- a/core/include/bertini2/function_tree.hpp +++ b/core/include/bertini2/function_tree.hpp @@ -39,7 +39,6 @@ #include "bertini2/function_tree/operators/arithmetic.hpp" #include "bertini2/function_tree/operators/trig.hpp" -#include "bertini2/function_tree/symbols/linear_product.hpp" #include "bertini2/function_tree/symbols/symbol.hpp" @@ -47,11 +46,11 @@ #include "bertini2/function_tree/symbols/number.hpp" #include "bertini2/function_tree/symbols/special_number.hpp" -#include "bertini2/function_tree/roots/function.hpp" -#include "bertini2/function_tree/roots/jacobian.hpp" +#include "bertini2/function_tree/roots/named_expression.hpp" #include "bertini2/function_tree/simplify.hpp" +#include "bertini2/function_tree/gather.hpp" @@ -61,8 +60,6 @@ BOOST_CLASS_EXPORT_KEY(bertini::node::Node) BOOST_SERIALIZATION_ASSUME_ABSTRACT(bertini::node::Node) -BOOST_CLASS_EXPORT_KEY(bertini::node::Handle) - BOOST_CLASS_EXPORT_KEY(bertini::node::Symbol) BOOST_CLASS_EXPORT_KEY(bertini::node::NamedSymbol) BOOST_CLASS_EXPORT_KEY(bertini::node::Number) @@ -83,8 +80,7 @@ BOOST_CLASS_EXPORT_KEY(bertini::node::Rational) BOOST_CLASS_EXPORT_KEY(bertini::node::special_number::Pi) BOOST_CLASS_EXPORT_KEY(bertini::node::special_number::E) -BOOST_CLASS_EXPORT_KEY(bertini::node::Function) -BOOST_CLASS_EXPORT_KEY(bertini::node::Jacobian) +BOOST_CLASS_EXPORT_KEY(bertini::node::NamedExpression) BOOST_CLASS_EXPORT_KEY(bertini::node::SinOperator) BOOST_CLASS_EXPORT_KEY(bertini::node::ArcSinOperator) @@ -124,8 +120,7 @@ BOOST_CLASS_TRACKING(bertini::node::Rational, boost::serialization::track_always BOOST_CLASS_TRACKING(bertini::node::special_number::Pi, boost::serialization::track_always) BOOST_CLASS_TRACKING(bertini::node::special_number::E, boost::serialization::track_always) -BOOST_CLASS_TRACKING(bertini::node::Function, boost::serialization::track_always) -BOOST_CLASS_TRACKING(bertini::node::Jacobian, boost::serialization::track_always) +BOOST_CLASS_TRACKING(bertini::node::NamedExpression, boost::serialization::track_always) BOOST_CLASS_TRACKING(bertini::node::SinOperator, boost::serialization::track_always) BOOST_CLASS_TRACKING(bertini::node::ArcSinOperator, boost::serialization::track_always) diff --git a/core/include/bertini2/function_tree/canonical.hpp b/core/include/bertini2/function_tree/canonical.hpp new file mode 100644 index 000000000..8f1b6fff0 --- /dev/null +++ b/core/include/bertini2/function_tree/canonical.hpp @@ -0,0 +1,84 @@ +//This file is part of Bertini 2. +// +//canonical.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//canonical.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with canonical.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file canonical.hpp + +\brief Canonical ordering of commutative-operator operands (reorder-only). + +When canonicalization is enabled, the operands of a Sum / Mult are sorted into a +deterministic, content-based canonical order BEFORE the node is interned, so that +structurally-equal-up-to-reordering expressions (x+y and y+x) collapse to one shared node. + +The order is a pluggable monomial order -- Lex / RevLex / GrevLex -- on the operands' +exponent vectors (via Node::MultiDegree), built on the global variable order BY NAME +(node::GatherVariables already returns variables sorted alphabetically). Non-polynomial +operands sort after the polynomial ones, with a printed-form tie-break to stay total. +This sorts operands but never combines them (no like-term folding; a full polynomial normal +form is future work). + +Nothing here changes Node::Hash()/IsSame(): canonicalization is a normalization applied at +construction, and the existing order-sensitive predicates then see the normalized order. +*/ + +#pragma once + +#include +#include + +#include "bertini2/function_tree/node.hpp" + +namespace bertini { +namespace node { + +/// Pluggable monomial orders for canonicalization. The default is deferred; GrevLex (the +/// usual computational-algebra default, and graded => degree-descending, matching how +/// polynomials are conventionally written) is the placeholder. +enum class MonomialOrder { Lex, RevLex, GrevLex }; + +/// The monomial order currently used for canonicalization (session-global -- it must be +/// consistent session-wide, since it determines interned identity). +MonomialOrder CurrentMonomialOrder(); +void SetMonomialOrder(MonomialOrder order); + +/// Whether Sum/Mult construction canonicalizes operand order by default. (Per-expression +/// opt-out is a future refinement; for now this is the session switch.) +bool CanonicalizeByDefault(); +void SetCanonicalizeByDefault(bool on); + +/** +\brief Sort an n-ary operator's (operand, flag) pairs into canonical order, in place. + +\param operands the operator's operands (mutated into canonical order) +\param flags the parallel signs (Sum) or multiply/divide flags (Mult), reordered in tandem +\param multiplicands_first for a Mult, keep flag==true (multiplicands) ahead of flag==false + (divisors), so a divisor never ends up first (the eval path expects a leading + multiplicand). Pass false for a Sum (the +/- sign does not affect term order). + +No-op when canonicalization is disabled or there are fewer than two operands. Called from +the Sum/Mult constructors, so the node is canonical before Make() interns it. +*/ +void CanonicalizeNaryOperands(std::vector>& operands, + std::vector& flags, + bool multiplicands_first); + +} // namespace node +} // namespace bertini diff --git a/core/include/bertini2/function_tree/find.hpp b/core/include/bertini2/function_tree/find.hpp new file mode 100644 index 000000000..42d9f0755 --- /dev/null +++ b/core/include/bertini2/function_tree/find.hpp @@ -0,0 +1,60 @@ +//This file is part of Bertini 2. +// +//include/bertini2/function_tree/find.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//include/bertini2/function_tree/find.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with include/bertini2/function_tree/find.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire + +/** +\file include/bertini2/function_tree/find.hpp + +\brief Discover named nodes of a given kind in a function tree (sympy's `find`). + +`Find(root)` returns the distinct nodes of kind `T` appearing anywhere in the tree, +de-duplicated by identity and sorted by name. It is the general form of the old +`GatherVariables`: `Find` are the solve-variables, `Find` the +named subexpressions (`a = x^2+y^2`), `Find<...>` the core's named symbols. Descent uses +only public child accessors, so every operator subtype is handled via its base, and it +descends through Handle/NamedExpression into the wrapped expression. + +The definitions live in find.cpp with explicit instantiations (one per kind we discover), +to keep this traversal out of every translation unit. +*/ + +#pragma once + +#include +#include + +#include "bertini2/function_tree/forward_declares.hpp" + +namespace bertini { +namespace node { + + /// All distinct nodes of kind T in the subtree rooted at n, sorted by name. + template + std::vector> Find(std::shared_ptr const& n); + + /// The union of Find across several roots (one shared traversal), sorted by name. + template + std::vector> Find(std::vector> const& roots); + +} // namespace node +} // namespace bertini diff --git a/core/include/bertini2/function_tree/forward_declares.hpp b/core/include/bertini2/function_tree/forward_declares.hpp index c0d06f447..671452273 100644 --- a/core/include/bertini2/function_tree/forward_declares.hpp +++ b/core/include/bertini2/function_tree/forward_declares.hpp @@ -41,8 +41,7 @@ namespace bertini { class Integer; class Float; class Rational; - class Function; - class Jacobian; + class NamedExpression; class Differential; } @@ -62,9 +61,6 @@ namespace bertini { class NegateOperator; class SqrtOperator; - class LinearProduct; - class DiffLinear; - } namespace node{ @@ -94,8 +90,7 @@ namespace bertini { ar.template register_type(); ar.template register_type(); ar.template register_type(); - ar.template register_type(); - ar.template register_type(); + ar.template register_type(); ar.template register_type(); // ar.template register_type(); // abstract type // ar.template register_type(); // abstract type @@ -108,7 +103,6 @@ namespace bertini { ar.template register_type(); ar.template register_type(); ar.template register_type(); - ar.template register_type(); // ar.template register_type(); // abstract type ar.template register_type(); ar.template register_type(); diff --git a/core/include/bertini2/function_tree/gather.hpp b/core/include/bertini2/function_tree/gather.hpp new file mode 100644 index 000000000..379836184 --- /dev/null +++ b/core/include/bertini2/function_tree/gather.hpp @@ -0,0 +1,68 @@ +//This file is part of Bertini 2. +// +//include/bertini2/function_tree/gather.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//include/bertini2/function_tree/gather.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with include/bertini2/function_tree/gather.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire + + +/** +\file include/bertini2/function_tree/gather.hpp + +\brief Free functions for discovering the variables appearing in a function tree. +*/ + +#pragma once + +#include +#include + +#include "bertini2/function_tree/node.hpp" + +namespace bertini { +namespace node { + + /** + \brief Collect the distinct Variables appearing in a function-tree subtree. + + Traverses the tree non-intrusively, via the public child accessors of the + operator and root node types. The returned group is de-duplicated (by node + identity) and ordered alphabetically by Variable name. + + \param n The root of the subtree to scan. May be null, in which case an empty + group is returned. + \return A VariableGroup of the distinct variables found, sorted by name. + */ + VariableGroup GatherVariables(std::shared_ptr const& n); + + /** + \brief Collect the distinct Variables appearing across a collection of expressions. + + The union of the variables of each expression, de-duplicated (by node identity) + and ordered alphabetically by Variable name. This is what the + function-list System constructor uses to auto-build a single variable group. + + \param functions The expressions to scan. + \return A VariableGroup of the distinct variables found, sorted by name. + */ + VariableGroup GatherVariables(std::vector> const& functions); + +} // namespace node +} // namespace bertini diff --git a/core/include/bertini2/function_tree/node.hpp b/core/include/bertini2/function_tree/node.hpp index d6a3b3d2a..38aae70ee 100755 --- a/core/include/bertini2/function_tree/node.hpp +++ b/core/include/bertini2/function_tree/node.hpp @@ -47,6 +47,8 @@ #include #include #include +#include +#include #include @@ -87,48 +89,23 @@ enum class VariableGroupType namespace node{ -namespace detail{ - template - struct FreshEvalSelector - {}; +/** +\brief Operator precedence classes, used to decide parenthesization when printing. + +Higher binds tighter. A child is wrapped in parentheses only when its +precedence is too low for the position it is printed in; leaves and +self-delimiting nodes (function calls like sin(...), complex pairs) are +atoms and never wrapped. Negative literal constants report PrecNegate so +they parenthesize exactly where a Negate node would. +*/ +enum PrintPrecedence : unsigned { + PrecSum = 10, + PrecNegate = 15, + PrecMult = 20, + PrecPower = 30, + PrecAtom = 100 +}; - template<> - struct FreshEvalSelector - { - - template - static dbl Run(N const& n, std::shared_ptr const& diff_variable) - { - return n.FreshEval_d(diff_variable); - } - - - template - static void RunInPlace(dbl& evaluation_value, N const& n, std::shared_ptr const& diff_variable) - { - n.FreshEval_d(evaluation_value, diff_variable); - } - - }; - - template<> - struct FreshEvalSelector - { - template - static mpfr_complex Run(N const& n, std::shared_ptr const& diff_variable) - { - return n.FreshEval_mp(diff_variable); - } - - - template - static void RunInPlace(mpfr_complex& evaluation_value, N const& n, std::shared_ptr const& diff_variable) - { - n.FreshEval_mp(evaluation_value, diff_variable); - } - - }; -} /** An interface for all nodes in a function tree, and for a function object as well. Almost all methods that will be called on a node must be declared in this class. The main evaluation method is @@ -138,97 +115,109 @@ An interface for all nodes in a function tree, and for a function object as well \brief Abstract base class for the Bertini hybrid-precision (double-multiple) expression tree. */ -class Node : public virtual VisitableBase<>, public std::enable_shared_from_this +class Node : public VisitableBase<>, public std::enable_shared_from_this { - friend detail::FreshEvalSelector; - friend detail::FreshEvalSelector; public: - + virtual ~Node() = default; - + ///////// PUBLIC PURE METHODS ///////////////// - + /** - Tells code to run a fresh eval on node next time. + \brief Functionally simplify this tree, returning a NEW simplified tree. + + Non-mutating successor to the in-place EliminateZeros / EliminateOnes / ReduceDepth + machinery (ADR-0011, issue 251). The input tree is never modified; a fresh simplified + tree is returned, built through the SimplifiedSum / SimplifiedMult / SimplifiedNegate + factories so that literal zeros/ones vanish and exact constants fold. The default + (leaves, and any node with nothing to simplify) returns the node unchanged -- structural + sharing preserved. + + \return A simplified tree (possibly the same node, if nothing simplified). */ - virtual void Reset() const = 0; - - ///////// END PUBLIC PURE METHODS ///////////////// - - + virtual std::shared_ptr Simplified() const; - public: /** - Evaluate the node. If flag false, just return value, if flag true - run the specific FreshEval of the node, then set flag to false. + Virtual method for printing Nodes to arbitrary output streams. + */ + virtual void print(std::ostream& target) const = 0; - Template type is type of value you want returned. + /** + \brief The printing precedence of this node, deciding parenthesization. - \return The value of the node. - \tparam T The number type for return. Must be one of the types stored in the Node class, currently dbl and mpfr_complex. - */ - template - T Eval(std::shared_ptr const& diff_variable = nullptr) const + Defaults to PrecAtom: leaves and self-delimiting nodes are never wrapped. + Operator nodes override this; printing parents wrap a child only when its + precedence is too low for the position it occupies. + */ + virtual unsigned Precedence() const { - T result; - EvalInPlace(result, diff_variable); - return result; + return PrecAtom; } - /** - Evaluate the node in place. If flag false, just return value, if flag true - run the specific FreshEval of the node, then set flag to false. - - Template type is type of value you want returned. - - \return The value of the node. - \tparam T The number type for return. Must be one of the types stored in the Node class, currently dbl and mpfr_complex. - */ - template - void EvalInPlace(T& eval_value, std::shared_ptr const& diff_variable = nullptr) const; - - - ///////// PUBLIC PURE METHODS ///////////////// + \brief Is this node the literal constant 0? + Exact check on stored literal values (Integer/Float/Rational override); + false for everything else, including non-literal expressions that happen + to evaluate to zero. Used by differentiation to build already-simplified + trees without evaluating anything. + */ + virtual bool IsLiteralZero() const + { + return false; + } /** - \brief A transform function, which eliminates nodes from the tree - - A better implementation of this would use a generic Transform. If you know how to do this, please rewrite the Nodes so they can transform to our whim, and submit a PR. + \brief Is this node the literal constant 1? - \return The number of nodes eliminated - - \see EliminateZeros + \see IsLiteralZero */ - virtual unsigned EliminateOnes() = 0; - - /** - \brief A transform function, which eliminates nodes from the tree + virtual bool IsLiteralOne() const + { + return false; + } - A better implementation of this would use a generic Transform. If you know how to do this, please rewrite the Nodes so they can transform to our whim, and submit a PR. + /** + \brief Memoized structural hash of this node. - \return The number of nodes eliminated + Order-sensitive: equal structures hash equal, where children are folded in by their own + Hash() and operand order / signs / exponents participate. The default (leaves and any + node not overriding HashImpl) is node identity (the object's address), so distinct objects + hash distinctly; value nodes (Integer/Rational/Float) and operators override to be + structural. Memoized -- valid because nodes are immutable post-construction -- and + deliberately independent of the mutable working precision (stable across precision()). - \see EliminateOnes + This is the predicate layer for the hash-consing intern table; nothing wires it into + construction yet. */ - virtual unsigned EliminateZeros() = 0; - + std::size_t Hash() const; /** - \brief A mutating function which finds ways to reduce the depth of the tree - - \note The default implementation is empty, and does literally nothing. + \brief Opaque, lazily-populated cache of a compiled evaluator for this expression. + + The system layer (EvalExpression / f.eval) compiles a StraightLineProgram for ([this], + canonical-by-name variable order) on first use and stashes it here, so repeated evaluations + of the same (hash-consed, immutable) expression reuse it --- like the memoized Hash(). The + type is erased because the SLP lives in the system layer, above function_tree; the compiled + program holds no node pointers (its constants are value recipes, ADR-0027), so + there is no reference cycle. Transient: not serialized; a clone recompiles on demand. */ - virtual unsigned ReduceDepth(); + std::shared_ptr EvalProgram() const { return eval_program_; } + void SetEvalProgram(std::shared_ptr p) const { eval_program_ = p; } /** - Virtual method for printing Nodes to arbitrary output streams. + \brief Order-sensitive structural equality, shallow given interned children. + + Two nodes are the same iff they have the same dynamic type, the same operator payload + (signs / mult-or-div flags / integer exponent / literal value), and the same operands + **by pointer** (operands are not recursed -- in the hash-consed world children are already + canonical, so pointer-equality is structural equality). The default is node identity; + value/operator nodes override. Consistent with Hash(): IsSame(a,b) implies a.Hash()==b.Hash(). */ - virtual void print(std::ostream& target) const = 0; - + virtual bool IsSame(Node const& other) const; + /** \brief Compute the derivative with respect to a single variable. @@ -265,12 +254,20 @@ class Node : public virtual VisitableBase<>, public std::enable_shared_from_this virtual int Degree(VariableGroup const& vars) const = 0; /** - Homogenize a tree, inputting a variable group holding the non-homogeneous variables, and the new homogenizing variable. The homvar may be an element of the variable group, that's perfectly ok. - + Homogenize a tree, returning a NEW homogenized tree (functional / non-mutating). Input a + variable group holding the non-homogeneous variables, and the new homogenizing variable. + The homvar may be an element of the variable group, that's perfectly ok. + + The input tree is never modified; degree-deficient summands are padded with powers of homvar + in a freshly-built tree (so a throw on a non-polynomial term can't leave a half-homogenized + tree behind). The default (leaves, and anything with nothing to homogenize) returns the node + unchanged. + \param homvar The homogenizing variable, which is multiplied against terms with degree deficiency with repect to other terms. \param vars A group of variables, with respect to which you wish to homogenize. + \return A homogenized tree (possibly the same node, if nothing changed). */ - virtual void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) = 0; + virtual std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const; /** Check for homogeneity, absolutely with respect to all variables, including path variables and all other variable types, or with respect to a single varaible, if passed. @@ -285,16 +282,6 @@ class Node : public virtual VisitableBase<>, public std::enable_shared_from_this \return True if it is homogeneous, false if not. */ virtual bool IsHomogeneous(VariableGroup const& vars) const = 0; - - - /** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ - virtual void precision(unsigned int prec) const = 0; - - unsigned precision() const; ///////// PUBLIC PURE METHODS ///////////////// @@ -319,54 +306,25 @@ class Node : public virtual VisitableBase<>, public std::enable_shared_from_this protected: - //Stores the current value of the node in all required types - //We must hard code in all types that we want here. - //TODO: Initialize this to some default value, second = false - mutable std::tuple< std::pair, std::pair > current_value_; - - - - ///////// PRIVATE PURE METHODS ///////////////// - - /** - Overridden code for specific node types, for how to evaluate themselves. Called from the wrapper Eval<>() call from Node, if so required (by resetting, etc). + /// Memoized structural hash (computed on first Hash() call; nodes are immutable so it + /// never needs invalidating). Not serialized -- a clone recomputes it on demand. + mutable std::optional structural_hash_; - If we had the ability to use template virtual functions, we would have. However, this is impossible with current C++ without using experimental libraries, so we have two copies -- because there are two number types for Nodes, dbl and mpfr_complex. - */ - virtual dbl FreshEval_d(std::shared_ptr const&) const = 0; + /// Opaque compiled-evaluator cache (a system-layer StraightLineProgram for [this]); see + /// EvalProgram(). Lazily populated by EvalExpression; transient, not serialized. + mutable std::shared_ptr eval_program_; - /** - Overridden code for specific node types, for how to evaluate themselves. Called from the wrapper EvalInPlace<>() call from Node, if so required (by resetting, etc). - - If we had the ability to use template virtual functions, we would have. However, this is impossible with current C++ without using experimental libraries, so we have two copies -- because there are two number types for Nodes, dbl and mpfr_complex. - */ - virtual void FreshEval_d(dbl& evaluation_value, std::shared_ptr const&) const = 0; + /// Compute this node's structural hash. Default: identity (the object address), so + /// distinct nodes hash distinctly. Value/operator nodes override to be structural. + virtual std::size_t HashImpl() const; - - /** - Overridden code for specific node types, for how to evaluate themselves. Called from the wrapper Eval<>() call from Node, if so required (by resetting, etc). + /// Combine an extra value into a running hash (boost::hash_combine recipe). + static void HashCombine(std::size_t& seed, std::size_t value) + { + seed ^= value + 0x9e3779b97f4a7c15ULL + (seed << 6) + (seed >> 2); + } - If we had the ability to use template virtual functions, we would have. However, this is impossible with current C++ without using experimental libraries, so we have two copies -- because there are two number types for Nodes, dbl and mpfr_complex. - */ - virtual mpfr_complex FreshEval_mp(std::shared_ptr const&) const = 0; - - /** - Overridden code for specific node types, for how to evaluate themselves. Called from the wrapper Eval<>() call from Node, if so required (by resetting, etc). - - If we had the ability to use template virtual functions, we would have. However, this is impossible with current C++ without using experimental libraries, so we have two copies -- because there are two number types for Nodes, dbl and mpfr_complex. - */ - virtual void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const&) const = 0; - - - - ///////// END PRIVATE PURE METHODS ///////////////// - - - /** - Set the stored values for the Node to indicate a fresh eval on the next pass. This is so that Nodes which are referred to more than once, are only evaluated once. The first evaluation is fresh, and then the indicator for fresh/stored is set to stored. Subsequent evaluation calls simply return the stored number. - */ - void ResetStoredValues() const; Node(); @@ -376,7 +334,7 @@ class Node : public virtual VisitableBase<>, public std::enable_shared_from_this friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { register_derived_node_types(ar); } @@ -398,42 +356,21 @@ class Node : public virtual VisitableBase<>, public std::enable_shared_from_this } - - // inherit from this to get a nice method of producing shared pointers to specific type, solving the diamond problem - // - // T is a derived type - // - // I adapted from: - // https://stackoverflow.com/questions/16082785/use-of-enable-shared-from-this-with-multiple-inheritance - // - template - struct EnableSharedFromThisVirtual: public virtual Node - { - - public: - - std::shared_ptr shared_from_this() { - return std::dynamic_pointer_cast(Node::shared_from_this()); - } - - std::shared_ptr shared_from_this() const{ - return std::dynamic_pointer_cast(Node::shared_from_this()); - } - - - - template - std::shared_ptr downcast_shared_from_this(){ - return std::dynamic_pointer_cast(Node::shared_from_this()); - } - - template - std::shared_ptr downcast_shared_from_this() const{ - return std::dynamic_pointer_cast(Node::shared_from_this()); - } - - }; - + /** + \brief Hash-cons a freshly-built node: return an existing structurally-equal node if one is + live, otherwise register and return this one. + + The intern table is a process-global, weak (self-cleaning) map keyed by Node::Hash() and + disambiguated by Node::IsSame(). Every Make() routes its just-constructed node through here, + so structurally-equal subtrees collapse to a single shared object (hash-consing). On a hit + the just-built candidate is discarded. Nodes whose IsSame() is identity (e.g. Variable, + Function, Pi, E) never match, so they pass through unchanged -- no special-casing. + + Thread note: guarded by a mutex, contended only during single-threaded authoring; + deserialization (Clone) constructs nodes WITHOUT going through Make/Intern, so per-thread + tracking clones stay private and un-interned. + */ + std::shared_ptr Intern(std::shared_ptr const& candidate); } // re: namespace node diff --git a/core/include/bertini2/function_tree/operators/arithmetic.hpp b/core/include/bertini2/function_tree/operators/arithmetic.hpp index 68e877cd0..f9d4b9058 100644 --- a/core/include/bertini2/function_tree/operators/arithmetic.hpp +++ b/core/include/bertini2/function_tree/operators/arithmetic.hpp @@ -56,6 +56,7 @@ #include "bertini2/function_tree/symbols/differential.hpp" #include "bertini2/function_tree/forward_declares.hpp" +#include "bertini2/function_tree/canonical.hpp" #include @@ -73,26 +74,21 @@ namespace node{ \brief Represents summation and difference Operator. This class represents summation and difference operators. All children are terms and are stored - in a single vector, and a vector of bools is used to determine the sign of each term. FreshEval method - is defined for summation and difference. + in a single vector, and a vector of bools is used to determine the sign of each term. */ - class SumOperator : public virtual NaryOperator, public virtual EnableSharedFromThisVirtual + class SumOperator : public NaryOperator { public: BERTINI_DEFAULT_VISITABLE() virtual ~SumOperator() = default; - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; - unsigned ReduceDepth() override; - unsigned ReduceSubSums(); - unsigned ReduceSubMults(); + std::shared_ptr Simplified() const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new SumOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new SumOperator(ts...) ))); } private: @@ -105,13 +101,25 @@ namespace node{ { AddOperand(left); AddOperand(right); + CanonicalizeNaryOperands(operands_, signs_, false); } - - + + SumOperator(const std::shared_ptr & left, bool add_or_sub_left, const std::shared_ptr & right, bool add_or_sub_right) { AddOperand(left, add_or_sub_left); AddOperand(right, add_or_sub_right); + CanonicalizeNaryOperands(operands_, signs_, false); + } + + // Build a complete sum from a full (term, sign) list. The node is fully constructed + // before Make() interns it -- so callers never AddOperand AFTER Make (which, with interning, + // could mutate a shared interned node). + explicit SumOperator(std::vector, bool>> const& terms) + { + for (auto const& t : terms) + AddOperand(t.first, t.second); + CanonicalizeNaryOperands(operands_, signs_, false); } public: @@ -156,6 +164,11 @@ namespace node{ Method for printing to output stream */ void print(std::ostream & target) const override; + + unsigned Precedence() const override + { + return PrecSum; + } @@ -186,7 +199,9 @@ namespace node{ /** Homogenize a sum, with respect to a variable group, and using a homogenizing variable. */ - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; + std::size_t HashImpl() const override; + bool IsSame(Node const& other) const override; bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; @@ -201,30 +216,10 @@ namespace node{ protected: - /** - Specific implementation of FreshEval for add and subtract. - If child_sign_ = true, then add, else subtract - */ - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - /** - Specific implementation of FreshEval in place for add and subtract. - If child_sign_ = true, then add, else subtract - */ - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - /** - Specific implementation of FreshEval for add and subtract. - If child_sign_ = true, then add, else subtract - */ - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - /** - Specific implementation of FreshEval for add and subtract. - If child_sign_ = true, then add, else subtract - */ - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; private: // Stores the sign of the particular term. There is a one-one @@ -241,7 +236,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & signs_; } @@ -274,10 +269,9 @@ namespace node{ /** \brief The negation Operator. - This class represents the negation Operator. FreshEval method - is defined for negation and multiplies the value by -1. + This class represents the negation Operator. */ - class NegateOperator : public virtual UnaryOperator, public virtual EnableSharedFromThisVirtual + class NegateOperator : public UnaryOperator { public: BERTINI_DEFAULT_VISITABLE() @@ -285,7 +279,7 @@ namespace node{ template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new NegateOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new NegateOperator(ts...) ))); } private: @@ -295,13 +289,18 @@ namespace node{ public: - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; /** Print to an arbitrary ostream. */ void print(std::ostream & target) const override; + + unsigned Precedence() const override + { + return PrecNegate; + } /** @@ -326,12 +325,7 @@ namespace node{ protected: - // Specific implementation of FreshEval for negate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; private: @@ -341,7 +335,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -366,24 +360,20 @@ namespace node{ \brief Multiplication and division Operator. This class represents the Operator for multiplication and division. All children are factors and are stored - in a vector. FreshEval method is defined for multiplication. + in a vector. */ - class MultOperator : public virtual NaryOperator, public virtual EnableSharedFromThisVirtual + class MultOperator : public NaryOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; - unsigned ReduceDepth() override; - unsigned ReduceSubSums(); - unsigned ReduceSubMults(); + std::shared_ptr Simplified() const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new MultOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new MultOperator(ts...) ))); } private: @@ -401,15 +391,26 @@ namespace node{ { AddOperand(left); AddOperand(right); + CanonicalizeNaryOperands(operands_, mult_or_div_, true); } - - + + MultOperator(const std::shared_ptr & left, bool mult_or_div_left, const std::shared_ptr & right, bool mult_or_div_right) { AddOperand(left, mult_or_div_left); AddOperand(right, mult_or_div_right); + CanonicalizeNaryOperands(operands_, mult_or_div_, true); } - + + // Build a complete product from a full (factor, mult-or-div) list -- fully constructed + // before Make() interns it, so no post-Make AddOperand on a shared interned node. + explicit MultOperator(std::vector, bool>> const& factors) + { + for (auto const& f : factors) + AddOperand(f.first, f.second); + CanonicalizeNaryOperands(operands_, mult_or_div_, true); + } + public: virtual ~MultOperator() = default; @@ -438,6 +439,11 @@ namespace node{ overridden method for printing to an output stream */ void print(std::ostream & target) const override; + + unsigned Precedence() const override + { + return PrecMult; + } /** Differentiates using the product rule. If there is division, consider as ^(-1) and use chain rule. @@ -458,7 +464,9 @@ namespace node{ std::vector MultiDegree(VariableGroup const& vars) const override; - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; + std::size_t HashImpl() const override; + bool IsSame(Node const& other) const override; bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; @@ -477,13 +485,8 @@ namespace node{ protected: - // Specific implementation of FreshEval for mult and divide. // If child_mult_ = true, then multiply, else divide - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; @@ -507,7 +510,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & mult_or_div_; } @@ -535,19 +538,18 @@ namespace node{ \see IntegerPowerOperator */ - class PowerOperator : public virtual Operator, public virtual EnableSharedFromThisVirtual + class PowerOperator : public Operator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new PowerOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new PowerOperator(ts...) ))); } @@ -580,14 +582,18 @@ namespace node{ return exponent_; } - void Reset() const override; - - - + + + void print(std::ostream & target) const override; - - - + + unsigned Precedence() const override + { + return PrecPower; + } + + + /** Differentiates with the power rule. */ @@ -607,7 +613,9 @@ namespace node{ - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; + std::size_t HashImpl() const override; + bool IsSame(Node const& other) const override; bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; @@ -623,24 +631,12 @@ namespace node{ \param prec the number of digits to change precision to. */ - virtual void precision(unsigned int prec) const override - { - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); - - base_->precision(prec); - exponent_->precision(prec); - } protected: - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaulation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaulation_value, std::shared_ptr const& diff_variable) const override; private: @@ -657,7 +653,7 @@ namespace node{ template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & base_; ar & exponent_; @@ -683,21 +679,27 @@ namespace node{ This class represents the exponentiation operator. The base is stored in - operand_, and an extra variable(exponent_) stores the exponent. FreshEval is - defined as the exponention operation. + operand_, and an extra variable(exponent_) stores the exponent. */ - class IntegerPowerOperator : public virtual UnaryOperator, public virtual EnableSharedFromThisVirtual + class IntegerPowerOperator : public UnaryOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; + std::size_t HashImpl() const override; + bool IsSame(Node const& other) const override; /** polymorphic method for printing to an arbitrary stream. */ void print(std::ostream & target) const override; + + unsigned Precedence() const override + { + return PrecPower; + } /** @@ -750,14 +752,14 @@ namespace node{ template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new IntegerPowerOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new IntegerPowerOperator(ts...) ))); } private: /** Constructor, passing in the Node you want as the base, and the integer you want for the power. */ - IntegerPowerOperator(const std::shared_ptr & N, int p) : exponent_(p), UnaryOperator(N) + IntegerPowerOperator(const std::shared_ptr & N, int p) : UnaryOperator(N), exponent_(p) {} @@ -766,28 +768,10 @@ namespace node{ protected: - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override - { - return pow(operand_->Eval(diff_variable), exponent_); - } - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = pow(evaluation_value, exponent_); - } - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override - { - return pow(operand_->Eval(diff_variable),exponent_); - } - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = pow(evaluation_value, exponent_); - } private: @@ -799,7 +783,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & exponent_; } @@ -828,10 +812,9 @@ namespace node{ \brief Represents the square root Operator - This class represents the square root function. FreshEval method - is defined for square root and takes the square root of the child node. + This class represents the square root function. */ - class SqrtOperator : public virtual UnaryOperator, public virtual EnableSharedFromThisVirtual + class SqrtOperator : public UnaryOperator { public: BERTINI_DEFAULT_VISITABLE() @@ -839,7 +822,7 @@ namespace node{ template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new SqrtOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new SqrtOperator(ts...) ))); } private: @@ -848,8 +831,8 @@ namespace node{ public: - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; void print(std::ostream & target) const override; @@ -871,12 +854,7 @@ namespace node{ protected: - // Specific implementation of FreshEval for negate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; private: @@ -886,7 +864,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -904,21 +882,20 @@ namespace node{ /** \brief represents the exponential function - This class represents the exponential function. FreshEval method - is defined for exponential and takes the exponential of the child node. + This class represents the exponential function. */ - class ExpOperator : public virtual UnaryOperator, public virtual EnableSharedFromThisVirtual + class ExpOperator : public UnaryOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new ExpOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new ExpOperator(ts...) ))); } private: @@ -950,19 +927,14 @@ namespace node{ protected: - // Specific implementation of FreshEval for exponentiate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; private: ExpOperator() = default; friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -973,18 +945,18 @@ namespace node{ This class represents the natural logarithm function. */ - class LogOperator : public virtual UnaryOperator, public virtual EnableSharedFromThisVirtual + class LogOperator : public UnaryOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new LogOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new LogOperator(ts...) ))); } private: @@ -1016,19 +988,14 @@ namespace node{ protected: - // Specific implementation of FreshEval for exponentiate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; private: LogOperator() = default; friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -1082,6 +1049,43 @@ namespace node{ + /////////////////// + // + // SIMPLIFIED-CONSTRUCTION FACTORIES + // + ///////////////////// + + /** + \brief Negation that never builds junk: -0 stays 0. + + Builds a fresh node; never modifies the input. + */ + std::shared_ptr SimplifiedNegate(std::shared_ptr const& n); + + /** + \brief Build a sum from (term, add_or_sub) pairs, omitting literal zeros. + + Empty after pruning -> Integer 0; a single added term is returned unwrapped; + a single subtracted term is negated. Builds fresh nodes; the term nodes are + shared, never modified. Used by differentiation so derivative trees come out + already simplified. + */ + std::shared_ptr SimplifiedSum(std::vector, bool>> const& terms); + + /** + \brief Build a product from (factor, mult_or_div) pairs, simplified. + + A multiplied literal zero collapses the whole product to 0; literal ones are + dropped; literal Integer/Rational constants (real-valued) are folded together + exactly (no Float folding -- precision semantics stay untouched). A literal + zero DIVISOR is left in place, keeping the division by zero visible. A single + surviving multiplied factor is returned unwrapped. Builds fresh nodes; the + factor nodes are shared, never modified. + */ + std::shared_ptr SimplifiedMult(std::vector, bool>> const& factors); + + + /////////////////// // // SUM AND DIFFERENCE ARITHMETIC OPERATORS @@ -1185,22 +1189,22 @@ namespace node{ return SumOperator::Make(lhs,true,rhs,false); } - inline std::shared_ptr operator-(std::shared_ptr lhs, mpfr_float rhs) + inline std::shared_ptr operator-(std::shared_ptr lhs, mpfr_float const& rhs) { return SumOperator::Make(lhs, true, Float::Make(rhs), false); } - - inline std::shared_ptr operator-(mpfr_float lhs, std::shared_ptr rhs) + + inline std::shared_ptr operator-(mpfr_float const& lhs, std::shared_ptr rhs) { return SumOperator::Make(Float::Make(lhs), true, rhs, false); } - inline std::shared_ptr operator-(std::shared_ptr lhs, mpfr_complex rhs) + inline std::shared_ptr operator-(std::shared_ptr lhs, mpfr_complex const& rhs) { return SumOperator::Make(lhs, true, Float::Make(rhs), false); } - - inline std::shared_ptr operator-(mpfr_complex lhs, std::shared_ptr rhs) + + inline std::shared_ptr operator-(mpfr_complex const& lhs, std::shared_ptr rhs) { return SumOperator::Make(Float::Make(lhs), true, rhs, false); } @@ -1247,22 +1251,22 @@ namespace node{ } - inline std::shared_ptr operator*(std::shared_ptr lhs, mpfr_float rhs) + inline std::shared_ptr operator*(std::shared_ptr lhs, mpfr_float const& rhs) { return MultOperator::Make(lhs,Float::Make(rhs)); } - inline std::shared_ptr operator*(std::shared_ptr lhs, mpfr_complex rhs) + inline std::shared_ptr operator*(std::shared_ptr lhs, mpfr_complex const& rhs) { return MultOperator::Make(lhs,Float::Make(rhs)); } - inline std::shared_ptr operator*(mpfr_float lhs, std::shared_ptr rhs) + inline std::shared_ptr operator*(mpfr_float const& lhs, std::shared_ptr rhs) { return MultOperator::Make(Float::Make(lhs), rhs); } - inline std::shared_ptr operator*(mpfr_complex lhs, std::shared_ptr rhs) + inline std::shared_ptr operator*(mpfr_complex const& lhs, std::shared_ptr rhs) { return MultOperator::Make(Float::Make(lhs), rhs); } diff --git a/core/include/bertini2/function_tree/operators/operator.hpp b/core/include/bertini2/function_tree/operators/operator.hpp index e041e1035..f87c34462 100755 --- a/core/include/bertini2/function_tree/operators/operator.hpp +++ b/core/include/bertini2/function_tree/operators/operator.hpp @@ -54,7 +54,7 @@ namespace node{ This class is an interface for all operators in the Bertini2 function tree. */ - class Operator : public virtual Node + class Operator : public Node { public: @@ -66,7 +66,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -80,7 +80,7 @@ namespace node{ This class is an interface for all unary operators, such as negation. The sole child is stored in a shared_ptr. */ - class UnaryOperator : public virtual Operator + class UnaryOperator : public Operator { public: @@ -90,9 +90,12 @@ namespace node{ virtual ~UnaryOperator() = default; - - - void Reset() const override; + + // Structural hash/equality for every unary op (typeid distinguishes Sin/Cos/Exp/...); + // IntegerPowerOperator overrides to fold in its exponent. + std::size_t HashImpl() const override; + bool IsSame(Node const& other) const override; + void SetOperand(std::shared_ptr n); @@ -118,10 +121,6 @@ namespace node{ */ std::vector MultiDegree(VariableGroup const& vars) const override; - - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; - - bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; @@ -137,7 +136,6 @@ namespace node{ \param prec the number of digits to change precision to. */ - void precision(unsigned int prec) const override; @@ -149,7 +147,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & operand_; } @@ -165,14 +163,13 @@ namespace node{ Operands of the operator are stored in a vector and methods to add and access operands are available in this interface. */ - class NaryOperator : public virtual Operator + class NaryOperator : public Operator { public: virtual ~NaryOperator() = default; - void Reset() const override; // Add an operand onto the container for this operator virtual void AddOperand(std::shared_ptr n); @@ -192,7 +189,6 @@ namespace node{ \param prec the number of digits to change precision to. */ - void precision(unsigned int prec) const override; @@ -213,7 +209,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & operands_; } diff --git a/core/include/bertini2/function_tree/operators/trig.hpp b/core/include/bertini2/function_tree/operators/trig.hpp index 0df999923..5ced0f511 100644 --- a/core/include/bertini2/function_tree/operators/trig.hpp +++ b/core/include/bertini2/function_tree/operators/trig.hpp @@ -52,7 +52,7 @@ namespace node{ Abstract class for trigonometric Operator types. */ - class TrigOperator: public virtual UnaryOperator + class TrigOperator: public UnaryOperator { public: BERTINI_DEFAULT_VISITABLE() @@ -74,7 +74,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } @@ -88,25 +88,24 @@ namespace node{ /** \brief Provides the sine Operator. - This class represents the sine function. FreshEval method - is defined for sine and takes the sine of the child node. + This class represents the sine function. */ - class SinOperator : public virtual TrigOperator, public virtual EnableSharedFromThisVirtual + class SinOperator : public TrigOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new SinOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new SinOperator(ts...) ))); } private: - SinOperator(const std::shared_ptr & N) : TrigOperator(N), UnaryOperator(N) + SinOperator(const std::shared_ptr & N) : TrigOperator(N) {}; public: @@ -126,15 +125,10 @@ namespace node{ protected: - // Specific implementation of FreshEval for negate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; private: @@ -142,7 +136,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -151,25 +145,24 @@ namespace node{ /** \brief Provides the inverse sine Operator. - This class represents the inverse sine function. FreshEval method - is defined for arcsine and takes the sine of the child node. + This class represents the inverse sine function. */ - class ArcSinOperator : public virtual TrigOperator, public virtual EnableSharedFromThisVirtual + class ArcSinOperator : public TrigOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new ArcSinOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new ArcSinOperator(ts...) ))); } private: - ArcSinOperator(const std::shared_ptr & N) : TrigOperator(N), UnaryOperator(N) + ArcSinOperator(const std::shared_ptr & N) : TrigOperator(N) {}; public: @@ -187,15 +180,10 @@ namespace node{ protected: - // Specific implementation of FreshEval for negate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; private: ArcSinOperator() = default; @@ -203,7 +191,7 @@ namespace node{ template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -218,25 +206,24 @@ namespace node{ /** \brief Provides the cosine Operator. - This class represents the cosine function. FreshEval method - is defined for cosine and takes the cosine of the child node. + This class represents the cosine function. */ - class CosOperator : public virtual TrigOperator, public virtual EnableSharedFromThisVirtual + class CosOperator : public TrigOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new CosOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new CosOperator(ts...) ))); } private: - CosOperator(const std::shared_ptr & N) : TrigOperator(N), UnaryOperator(N) + CosOperator(const std::shared_ptr & N) : TrigOperator(N) {}; public: @@ -258,15 +245,10 @@ namespace node{ protected: - // Specific implementation of FreshEval for negate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; @@ -275,7 +257,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -284,25 +266,24 @@ namespace node{ /** \brief Provides the arc cosine Operator. - This class represents the inverse cosine function. FreshEval method - is defined for arccosine and takes the arccosine of the child node. + This class represents the inverse cosine function. */ - class ArcCosOperator : public virtual TrigOperator, public virtual EnableSharedFromThisVirtual + class ArcCosOperator : public TrigOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new ArcCosOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new ArcCosOperator(ts...) ))); } private: - ArcCosOperator(const std::shared_ptr & N) : TrigOperator(N), UnaryOperator(N) + ArcCosOperator(const std::shared_ptr & N) : TrigOperator(N) {}; public: @@ -323,15 +304,10 @@ namespace node{ protected: - // Specific implementation of FreshEval for negate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; private: @@ -339,7 +315,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -356,25 +332,24 @@ namespace node{ /** \brief Provides the tangent Operator. - This class represents the tangent function. FreshEval method - is defined for tangent and takes the tangent of the child node. + This class represents the tangent function. */ - class TanOperator : public virtual TrigOperator, public virtual EnableSharedFromThisVirtual + class TanOperator : public TrigOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new TanOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new TanOperator(ts...) ))); } private: - TanOperator(const std::shared_ptr & N) : TrigOperator(N), UnaryOperator(N) + TanOperator(const std::shared_ptr & N) : TrigOperator(N) {}; public: @@ -393,15 +368,10 @@ namespace node{ protected: - // Specific implementation of FreshEval for negate. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; private: @@ -410,7 +380,7 @@ namespace node{ template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -419,25 +389,24 @@ namespace node{ /** \brief Provides the inverse tangent Operator. - This class represents the inverse tangent function. FreshEval method - is defined for arctangent and takes the arc tangent of the child node. + This class represents the inverse tangent function. */ - class ArcTanOperator : public virtual TrigOperator, public virtual EnableSharedFromThisVirtual + class ArcTanOperator : public TrigOperator { public: BERTINI_DEFAULT_VISITABLE() - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; + std::shared_ptr Simplified() const override; + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new ArcTanOperator(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new ArcTanOperator(ts...) ))); } private: - ArcTanOperator(const std::shared_ptr & N) : TrigOperator(N), UnaryOperator(N) + ArcTanOperator(const std::shared_ptr & N) : TrigOperator(N) {}; public: @@ -456,22 +425,17 @@ namespace node{ protected: - // Specific implementation of FreshEval for arctangent. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; private: ArcTanOperator() = default; friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; diff --git a/core/include/bertini2/function_tree/roots/function.hpp b/core/include/bertini2/function_tree/roots/function.hpp deleted file mode 100755 index b423397fd..000000000 --- a/core/include/bertini2/function_tree/roots/function.hpp +++ /dev/null @@ -1,274 +0,0 @@ -//This file is part of Bertini 2. -// -//bertini2/function_tree/roots/function.hpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//bertini2/function_tree/roots/function.hpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with bertini2/function_tree/roots/function.hpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// James Collins -// West Texas A&M University -// Spring, Summer 2015 -// -// silviana amethyst, university of wisconsin-eau claire -// -// silviana amethyst -// UWEC -// Spring 2018 -// -// Created by Collins, James B. on 4/30/15. -// -// -// bertini2/function_tree/roots/function.hpp: Declares the class Function. - -/** -\file bertini2/function_tree/roots/function.hpp - -\brief Provides the Function Node type, a NamedSymbol. - -*/ - - -#ifndef BERTINI_FUNCTION_NODE_HPP -#define BERTINI_FUNCTION_NODE_HPP - - -#include "bertini2/function_tree/symbols/symbol.hpp" - - - -namespace bertini { -namespace node{ - - - - class Handle : public virtual NamedSymbol - { - BERTINI_DEFAULT_VISITABLE() - - - public: - - /** - overridden function for piping the tree to an output stream. - */ - void print(std::ostream & target) const override; - - - Handle(std::string const& new_name); - - - /** - Constructor defines entry node at construct time. - */ - Handle(const std::shared_ptr & entry, std::string const& name = "unnamed_function"); - - - protected: - - Handle() = default; - - public: - /** - Add a child onto the container for this operator - */ - void SetRoot(std::shared_ptr const& entry); - - - /** - throws a runtime error if the root node is nullptr - */ - void EnsureNotEmpty() const; - - - /** - The function which flips the fresh eval bit back to fresh. - */ - void Reset() const override; - - - - /** - Get the pointer to the entry node for this function. - */ - const std::shared_ptr& EntryNode() const; - - - /** - Calls Differentiate on the entry node and returns differentiated entry node. - */ - std::shared_ptr Differentiate(std::shared_ptr const& v = nullptr) const override; - - /** - Compute the degree of a node. For functions, the degree is the degree of the entry node. - */ - int Degree(std::shared_ptr const& v = nullptr) const override; - - - int Degree(VariableGroup const& vars) const override; - - - - /** - Compute the multidegree with respect to a variable group. This is for homogenization, and testing for homogeneity. - */ - std::vector MultiDegree(VariableGroup const& vars) const override; - - - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; - - bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; - - /** - Check for homogeneity, with respect to a variable group. - */ - bool IsHomogeneous(VariableGroup const& vars) const override; - - - /** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ - void precision(unsigned int prec) const override; - - - - protected: - - /** - Calls FreshEval on the entry node to the tree. - */ - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - - /** - Calls FreshEval in place on the entry node to the tree. - */ - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - - - /** - Calls FreshEval on the entry node to the tree. - */ - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - - /** - Calls FreshEval in place on the entry node to the tree. - */ - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; - - - std::shared_ptr entry_node_ = nullptr; - - private: - - - - friend class boost::serialization::access; - - template - void serialize(Archive& ar, const unsigned version) { - ar & boost::serialization::base_object(*this); - ar & entry_node_; - } - - }; // class Function - - - - -}} // namespaces - - - - - - - - - - - - - - -namespace bertini { -namespace node{ - - /** - \brief Formal entry point into an expression tree. - - This class defines a function. It stores the entry node for a particular functions tree. - */ - class Function : public virtual Handle, public virtual EnableSharedFromThisVirtual - { - public: - BERTINI_DEFAULT_VISITABLE() - - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Function(ts...) ); - } - - template - static - std::shared_ptr MakeInPlace(Function* ptr, Ts&& ...ts){ - return std::shared_ptr( new(ptr) Function(ts...) ); - } - - private: - - Function(std::string const& new_name); - - - /** - Constructor defines entry node at construct time. - */ - Function(const std::shared_ptr & entry, std::string const& name = "unnamed_function"); - - public: - - - - virtual ~Function() = default; - - - - protected: - - - Function() = default; - - - private: - friend class boost::serialization::access; - // template friend void boost::serialization::load_construct_data(Archive & ar, Function * t, const unsigned int file_version); - // template friend void boost::serialization::save_construct_data(Archive & ar, const Function * t, const unsigned int file_version); - - template - void serialize(Archive& ar, const unsigned version) { - ar & boost::serialization::base_object(*this); - } - }; - -} // re: namespace node -} // re: namespace bertini - - - -#endif diff --git a/core/include/bertini2/function_tree/roots/jacobian.hpp b/core/include/bertini2/function_tree/roots/jacobian.hpp deleted file mode 100644 index 33bdba044..000000000 --- a/core/include/bertini2/function_tree/roots/jacobian.hpp +++ /dev/null @@ -1,131 +0,0 @@ -//This file is part of Bertini 2. -// -//jacobian.hpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//jacobian.hpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with jacobian.hpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// James Collins -// West Texas A&M University -// Spring, Summer 2015 -// -// silviana amethyst, university of wisconsin-eau claire -// -// silviana amethyst -// UWEC -// Spring 2018 -// -// Created by Collins, James B. on 6/11/15. -// -// -// jacobian.hpp: Declares the class Jacobian. - -/** -\file jacobian.hpp - -\brief Provides the Jacobian node type. - -*/ - - -#ifndef BERTINI_JACOBIAN_NODE_HPP -#define BERTINI_JACOBIAN_NODE_HPP - - -#include "bertini2/function_tree/node.hpp" -#include "bertini2/function_tree/roots/function.hpp" -#include "bertini2/function_tree/symbols/variable.hpp" - - - -namespace bertini { -namespace node{ - - /** - \brief Defines the entry point into a Jacobian tree. - - This class defines a Jacobian tree. It stores the entry node for a particular functions tree. - */ - class Jacobian : public virtual Handle, public virtual EnableSharedFromThisVirtual - { - friend detail::FreshEvalSelector; - friend detail::FreshEvalSelector; - public: - BERTINI_DEFAULT_VISITABLE() - - - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Jacobian(ts...) ); - } - - private: - /** - */ - Jacobian(const std::shared_ptr & entry); - - public: - - /** - Jacobians must be evaluated with EvalJ, so that when current_diff_variable changes - the Jacobian is reevaluated. - */ - template - T Eval(std::shared_ptr const& diff_variable = nullptr) const = delete; - - - // Evaluate the node. If flag false, just return value, if flag true - // run the specific FreshEval of the node, then set flag to false. - template - T EvalJ(std::shared_ptr const& diff_variable) const; - - - // Evaluate the node. If flag false, just return value, if flag true - // run the specific FreshEval of the node, then set flag to false. - template - void EvalJInPlace(T& eval_value, std::shared_ptr const& diff_variable) const; - - - - virtual ~Jacobian() = default; - - /** - \brief Default construction of a Jacobian node is forbidden - */ - Jacobian() = default; - - private: - - mutable std::shared_ptr current_diff_variable_; - - - friend class boost::serialization::access; - - template - void serialize(Archive& ar, const unsigned version) { - ar & boost::serialization::base_object(*this); - } - }; - -} // re: namespace node -} // re: namespace bertini - - - -#endif diff --git a/core/include/bertini2/function_tree/roots/named_expression.hpp b/core/include/bertini2/function_tree/roots/named_expression.hpp new file mode 100644 index 000000000..283eabcde --- /dev/null +++ b/core/include/bertini2/function_tree/roots/named_expression.hpp @@ -0,0 +1,140 @@ +//This file is part of Bertini 2. +// +//bertini2/function_tree/roots/named_expression.hpp is free software: you can redistribute it +//and/or modify it under the terms of the GNU General Public License as published by the Free +//Software Foundation, either version 3 of the License, or (at your option) any later version. +// +//bertini2/function_tree/roots/named_expression.hpp is distributed in the hope that it will be +//useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +//FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License along with +//bertini2/function_tree/roots/named_expression.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire + +/** +\file bertini2/function_tree/roots/named_expression.hpp + +\brief A user-named expression node: prints as its name, evaluates to its expression. +*/ + +#ifndef BERTINI_NAMED_EXPRESSION_HPP +#define BERTINI_NAMED_EXPRESSION_HPP + +#include "bertini2/function_tree/symbols/symbol.hpp" + +namespace bertini { +namespace node{ + + /** + \brief A user-named expression --- the surviving, immutable entry point into a subtree. + + `Named(x^2+y^2, "a")` wraps an expression and gives it a name. It is **immutable**: the + expression is supplied at construction (there is no SetRoot). It is hash-consed by + (expression, name), so `Named(e,"a")` is a distinct node from the bare `e` and from + `Named(e,"b")`. It **prints as its name** --- the expansion is revealed elsewhere (the + System's Describe, which discovers named expressions). Everything else (eval, differentiate, + degree, homogenize, precision) forwards to the wrapped expression. + + This is the sole surviving "handle" node: it absorbed the old Handle base and replaced the + deleted Function class. Distinct from Variable (a leaf) and from the core's named symbols + (Pi, E): those are the other two named kinds. Named expressions are discoverable as their own + kind (see Find). + */ + class NamedExpression : public NamedSymbol + { + public: + BERTINI_DEFAULT_VISITABLE() + + template + static + std::shared_ptr Make(Ts&& ...ts){ + return std::static_pointer_cast(Intern(std::shared_ptr( new NamedExpression(ts...) ))); + } + + /// Prints as just the name (the expansion is shown by the System's Describe). + void print(std::ostream & target) const override + { + target << name(); + } + + // Hash-consed by (expression, name), so distinct names / expressions are distinct nodes. + std::size_t HashImpl() const override + { + std::size_t h = typeid(NamedExpression).hash_code(); + HashCombine(h, EntryNode()->Hash()); + HashCombine(h, std::hash{}(name())); + return h; + } + bool IsSame(Node const& other) const override + { + auto o = dynamic_cast(&other); + // entries are interned, so pointer identity is structural equality + return o && name() == o->name() && EntryNode().get() == o->EntryNode().get(); + } + + /// throws a runtime error if the entry node is nullptr + void EnsureNotEmpty() const; + + /// flips the fresh-eval bit back to fresh (downward through the wrapped expression) + + /// the wrapped (entry) expression this name stands for + const std::shared_ptr& EntryNode() const; + + /// differentiate the wrapped expression + std::shared_ptr Differentiate(std::shared_ptr const& v = nullptr) const override; + + /// the degree is the degree of the wrapped expression + int Degree(std::shared_ptr const& v = nullptr) const override; + int Degree(VariableGroup const& vars) const override; + + std::vector MultiDegree(VariableGroup const& vars) const override; + + std::shared_ptr Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const override; + + bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; + bool IsHomogeneous(VariableGroup const& vars) const override; + + /// change the precision of this variable-precision tree node + + virtual ~NamedExpression() = default; + + protected: + NamedExpression() = default; + + + std::shared_ptr entry_node_ = nullptr; + + private: + NamedExpression(std::shared_ptr const& entry, std::string const& name) + : NamedSymbol(name), entry_node_(entry) + {} + + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) { + ar & boost::serialization::base_object(*this); + ar & entry_node_; + } + }; + + /// Give an expression a name: `Named(x*x + y*y, "a")`. The result prints as `a` and evaluates + /// to the expression; it is hash-consed by (expression, name). + inline std::shared_ptr Named(std::shared_ptr const& expr, std::string const& name) + { + return NamedExpression::Make(expr, name); + } + +} // re: namespace node +} // re: namespace bertini + +#endif diff --git a/core/include/bertini2/function_tree/simplify.hpp b/core/include/bertini2/function_tree/simplify.hpp index d32cec1bd..cd20dfe69 100644 --- a/core/include/bertini2/function_tree/simplify.hpp +++ b/core/include/bertini2/function_tree/simplify.hpp @@ -40,7 +40,12 @@ General methods exploting polymorphism, for simplifying a Node, and all subnodes namespace bertini { -unsigned Simplify(std::shared_ptr const& n); +/** +\brief Functionally simplify a tree: returns a NEW simplified tree, leaving the input +untouched (non-mutating, copy-on-write; ADR-0011, issue #251). Thin wrapper over +node::Node::Simplified(). +*/ +std::shared_ptr Simplify(std::shared_ptr const& n); } // namespace bertini diff --git a/core/include/bertini2/function_tree/symbols/differential.hpp b/core/include/bertini2/function_tree/symbols/differential.hpp index 832db2627..167ce5837 100644 --- a/core/include/bertini2/function_tree/symbols/differential.hpp +++ b/core/include/bertini2/function_tree/symbols/differential.hpp @@ -58,7 +58,7 @@ namespace node{ This class represents differentials. These are produced in the course of differentiation of a non-constant expression tree. */ - class Differential : public virtual NamedSymbol, public virtual EnableSharedFromThisVirtual + class Differential : public NamedSymbol { public: BERTINI_DEFAULT_VISITABLE() @@ -67,7 +67,7 @@ namespace node{ template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Differential(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new Differential(ts...) ))); } @@ -79,7 +79,6 @@ namespace node{ public: - void Reset() const override; const std::shared_ptr& GetVariable() const; @@ -108,8 +107,6 @@ namespace node{ std::vector MultiDegree(VariableGroup const& vars) const override; - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; - bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; /** @@ -123,19 +120,14 @@ namespace node{ \param prec the number of digits to change precision to. */ - void precision(unsigned int prec) const override; protected: // This should never be called for a Differential. Only for Jacobians. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; @@ -147,7 +139,7 @@ namespace node{ friend class boost::serialization::access; template - void save(Archive & ar, const unsigned int version) const + void save(Archive & ar, const unsigned int /*version*/) const { ar & boost::serialization::base_object(*this); ar & differential_variable_; @@ -155,7 +147,7 @@ namespace node{ } template - void load(Archive & ar, const unsigned int version) + void load(Archive & ar, const unsigned int /*version*/) { ar & boost::serialization::base_object(*this); ar & std::const_pointer_cast(differential_variable_); diff --git a/core/include/bertini2/function_tree/symbols/linear_product.hpp b/core/include/bertini2/function_tree/symbols/linear_product.hpp deleted file mode 100644 index 33eec67e7..000000000 --- a/core/include/bertini2/function_tree/symbols/linear_product.hpp +++ /dev/null @@ -1,1126 +0,0 @@ -//This file is part of Bertini 2. -// -//variable.hpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//variable.hpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with variable.hpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// James Collins -// West Texas A&M University -// Spring, Summer 2017 -// -// -// silviana amethyst -// University of Wisconsin - Eau Claire -// -// Created by Collins, James B. on 3/6/2017. - - -/** - \file linear_product.hpp - - \brief Provides the LinearProduct and DiffLinear Node classes. - - */ -#ifndef BERTINI_FUNCTION_TREE_LINPRODUCT_HPP -#define BERTINI_FUNCTION_TREE_LINPRODUCT_HPP - -#include "bertini2/function_tree.hpp" - -#include "bertini2/eigen_extensions.hpp" - -template using Mat = bertini::Mat; - -namespace bertini { - namespace node{ - /** - \brief Represents a product of linears as leaves in the function tree. - - This class represents a product of linear factors of a fixed set of variables. This could - also be a single linear of a fixed set of variables. - - When differentiated, produces a differential referring to it. - */ - class LinearProduct : public virtual Symbol, public virtual EnableSharedFromThisVirtual - { - public: - BERTINI_DEFAULT_VISITABLE() - - virtual ~LinearProduct() = default; - - - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new LinearProduct(ts...) ); - } - - - private: - /** - /brief Constructor for a linear product node that generates random coefficients automatically. - - \param variables A deque of variable nodes that are used in each linear factor. This does not have to be an actual system variable group. - \param num_factors The number of linear factors in the product. - - */ - LinearProduct(VariableGroup variables, int num_factors, bool is_hom_vars = false) : - variables_(variables), num_factors_(num_factors), is_hom_vars_(is_hom_vars) - { - // num_variables_ = variables.size(); //Include homogenize variable - // - // // Resize coeffs matrix - Mat& coeffs_dbl_ref = std::get>(coeffs_); - // coeffs_dbl_ref.resize(num_factors_, num_variables_+1); - Mat& coeffs_mpfr_ref = std::get>(coeffs_); - // coeffs_mpfr_ref.resize(num_factors_, num_variables_+1); - // // Resize temporary variable holders and fill constant with 1 - // temp_var_d_.resize(num_variables_ + 1); - // temp_var_d_[num_variables_] = dbl(1); - // temp_var_mp_.resize(num_variables_ + 1); - // temp_var_mp_[num_variables_] = mpfr_complex(1); - - SetupVariables(num_factors, variables); - coeffs_rat_real_.resize(num_factors_, num_variables_+1); - coeffs_rat_imag_.resize(num_factors_, num_variables_+1); - - - for (int ii = 0; ii < num_factors_; ++ii) - { - for (int jj = 0; jj < num_variables_+1; ++jj) - { - // Generate random constants as mpq_rationals. Then downsample to mpfr_complex and dbl. - // TODO: RandomRat() does not generate random numbers. Same each run. - coeffs_rat_real_(ii,jj) = RandomRat(); - coeffs_rat_imag_(ii,jj) = RandomRat(); - coeffs_dbl_ref(ii,jj).real( static_cast(coeffs_rat_real_(ii,jj)) ); - coeffs_dbl_ref(ii,jj).imag( static_cast(coeffs_rat_imag_(ii,jj)) ); - coeffs_mpfr_ref(ii,jj).real( static_cast(coeffs_rat_real_(ii,jj)) ); - coeffs_mpfr_ref(ii,jj).imag( static_cast(coeffs_rat_imag_(ii,jj)) ); - } - } - - is_rational_coeffs_ = true; - - // If we are creating a linear product for variable group that is already homogenized, - // we do not use the constant(last) column in the coefficient matrix - if(is_hom_vars) - { - is_homogenized_ = true; - hom_variable_ = Integer::Make(0); - for(int ii = 0; ii < coeffs_mpfr_ref.rows(); ++ii) - { - coeffs_rat_real_(ii,num_variables_) = mpq_rational(0); - coeffs_rat_imag_(ii,num_variables_) = mpq_rational(0); - coeffs_dbl_ref(ii,num_variables_) = dbl(0); - coeffs_mpfr_ref(ii,num_variables_) = mpfr_complex(0); - } - } - - - } - - - - /** - \brief Constructor for a linear product node that passes in random coefficients. - - \param variables A deque of variable nodes that are used in each linear factor. This does not have to be an actual system variable group. - \param num_factors The number of linear factors in the product. - \param coeffs_real A matrix of dbl data types for all the coefficients in the linear factors. Matrix must be of size num_factors x (num_variables + 1) with constant coefficient in last column. - \param coeffs_mpfr A matrix of mpfr_complex data types for all the coefficients in the linear factors. Matrix must be of size num_factors x (num_variables + 1) with constant coefficient in last column. - - */ - LinearProduct(VariableGroup const& variables, Mat const& coeffs_mpfr, bool is_hom_vars = false) - : variables_(variables), num_factors_(coeffs_mpfr.rows()), is_hom_vars_(is_hom_vars) - { - if(variables.size()+1 != coeffs_mpfr.cols()) - throw std::runtime_error("attempting to construct a linear product manually, not enough columns for the number of variables in the variable group"); - - - // Resize coeffs matrix - Mat& coeffs_dbl_ref = std::get>(coeffs_); - Mat& coeffs_mpfr_ref = std::get>(coeffs_); - - SetupVariables(num_factors_, variables); - - coeffs_mpfr_ref = coeffs_mpfr; - - // If we are creating a linear product for variable group that is already homogenized, - // we do not use the constant(last) column in the coefficient matrix - if(is_hom_vars) - { - is_homogenized_ = true; - hom_variable_ = Integer::Make(0); - for(int ii = 0; ii < coeffs_mpfr_ref.rows(); ++ii) - { - coeffs_mpfr_ref(ii,num_variables_) = mpfr_complex(0); - } - } - - // Set the coefficient matrices with input matrices. - - for(int ii = 0; ii < num_factors_; ++ii) - { - for(int jj = 0; jj < coeffs_mpfr.cols(); ++jj) - { - coeffs_dbl_ref(ii,jj) = static_cast(coeffs_mpfr_ref(ii,jj)); - } - } - - - is_rational_coeffs_ = false; - - } - - - - public: - - - - - - /** - \brief Accept a variable and add it to the list of variables in linear factors. - - \param child Variable to be added. If node is not a Variable, then runtime error is thrown. - - */ - // void AddVariable(std::shared_ptr child) override - // { - // if(std::dynamic_pointer_cast< std::shared_ptr >(child) == nullptr) - // { - // throw std::runtime_error("Only Variable node types can be children of a LinearProduct node."); - // } - // } - - - - - - - /** - \brief Reset variable values in this node - */ - void Reset() const override - { - Node::ResetStoredValues(); - for(auto& v : variables_) - { - v->Reset(); - } - }; - - - /** - Method for printing to output stream - */ - void print(std::ostream & target) const override; - - - - - /** - Return SumOperator whose children are derivatives of children_ - */ - std::shared_ptr Differentiate(std::shared_ptr const& v = nullptr) const override ; - - - - - /** - \brief Computes the degree for a particular variable. If that variable is part of the linear product, the degree is equal to the number of factors. - - \param v The variable we are determining the degree with respect to. - \return Degree of polynomial with respect to variable v. - */ - int Degree(std::shared_ptr const& v = nullptr) const override; - - - - - /** - \brief Computer the degree for a particular group of variables. If one of the variables is a part of the linear product, the degree is equal to the number of factors. - - \param vars The group of variables we are determing the degree with respect to. - - \return Degree of polynomial with respect to variable group. - */ - - int Degree(VariableGroup const& vars) const override; - - - /** - \brief Compute the multidegree with respect to a variable group. This is for homogenization, and testing for homogeneity. - - \param vars The variable group computing degree with respect to. - \return Multidegree vector of polynomial with repect to variable group. - */ - std::vector MultiDegree(VariableGroup const& vars) const override; - - - - - - - - /** - \brief Homogenize a sum, with respect to a variable group, and using a homogenizing variable. - - \param vars Variable group to homogenize with respect to. - \param homvar Homogenization variable. - */ - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; - - - - /** - \brief Check for homogeneity, with respect to a variable. If vars is the same variable group as the one used to create the linear product, and it has been homogenized, then return true. - - \param vars Variable group to check if homogenous with respect to. - - \return boolean. - */ - - bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; - - /** - \brief Check for homogeneity, with respect to a variable group. If vars is the same variable group as the one used to create the linear product, and it has been homogenized, then return true. - - \param vars Variable group to check if homogenous with respect to. - - \return boolean. - */ - bool IsHomogeneous(VariableGroup const& vars) const override; - - - - /** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ - virtual void precision(unsigned int prec) const override - { - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); - - this->PrecisionChangeSpecific(prec); - - for (auto iter : variables_) - iter->precision(prec); - - }; - - - - unsigned EliminateZeros() override { return 0;}; - - unsigned EliminateOnes() override {return 0;}; - - - - - - - - - - - - - - - - - - /** - \brief Getter for the differential of all variables - - */ - void GetVariables(VariableGroup& vars) const - { - vars = variables_; - } - - - - - /** - \brief Getter for the homogenizing variable - */ - void GetHomVariable(std::shared_ptr& hom_var) const - { - hom_var = hom_variable_; - } - - - - /** - \brief Getter for rational coefficients - */ - void GetRatCoeffs(Mat& coeffs_real, Mat& coeffs_imag) const - { - coeffs_real = coeffs_rat_real_; - coeffs_imag = coeffs_rat_imag_; - } - - /** - \brief Getter for mpfr_complex coefficients - */ - void GetMPFRCoeffs(Mat& coeffs) const - { - auto& coeffs_mpfr_ref = std::get>(coeffs_); - coeffs = coeffs_mpfr_ref; - } - - /** - \brief Are there rational coefficients? - */ - bool IsRationalCoefficients() - { - return is_rational_coeffs_; - } - - - - /** - \brief Getter for the coefficients of a single linear factor. Constant coefficient is last element of the Vector. - - \param index Index of the linear factor. - - \return Vector containing coefficients. - */ - template - Vec GetCoeffs(size_t index) - { - Vec coeff_ret(num_variables_ + 1); - Mat& coeff_ref = std::get>(coeffs_); - - for(int jj = 0; jj < num_variables_ + 1; ++jj) - { - coeff_ret(jj) = coeff_ref(index,jj); - } - - return coeff_ret; - } - - - - - - - - - - - - - - - - - protected: - /** - \brief Evaluation of linear product node. Returns evaluation value. - - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override - { - dbl eval_value; - - this->FreshEval_d(eval_value, diff_variable); - return eval_value; - } - - /** - \brief Evaluation of linear product node IN PLACE. Returns evaluation value. - - \param evaluation_value The in place variable that stores the evaluation. - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override - { - evaluation_value = dbl(1); - const Mat& coeffs_ref = std::get< Mat >(coeffs_); - - - // Evaluate all afine variables and store - for (int jj = 0; jj < num_variables_; ++jj) - { - variables_[jj]->EvalInPlace(temp_var_d_[jj], diff_variable); - } - - hom_variable_->EvalInPlace(temp_var_d_[num_variables_], diff_variable); - - - - - - - // Evaluate the linear product - for (int ii = 0; ii < num_factors_; ++ii) - { - // Add all terms in one linear factor and store in temp_sum_d_ - temp_sum_d_ = dbl(0); - for (int jj = 0; jj < num_variables_ + 1; ++jj) - { - temp_sum_d_ += coeffs_ref(ii,jj)*temp_var_d_[jj]; - }// re: loop through variables - - - // Multiply factors together - evaluation_value *= temp_sum_d_; - }// re: loop through factors - } - - - /** - \brief Evaluation of linear product node. Returns evaluation value. - - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override - { - mpfr_complex eval_value; - - this->FreshEval_mp(eval_value, diff_variable); - return eval_value; - } - - - /** - \brief Evaluation of linear product node IN PLACE. Returns evaluation value. - - \param evaluation_value The in place variable that stores the evaluation. - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override - { - evaluation_value = mpfr_complex(1); - const Mat& coeffs_ref = std::get< Mat >(coeffs_); - - // Evaluate all afine variables and store - for (int jj = 0; jj < num_variables_; ++jj) - { - variables_[jj]->EvalInPlace(temp_var_mp_[jj], diff_variable); - } - hom_variable_->EvalInPlace(temp_var_mp_[num_variables_], diff_variable); - - - - - - for (int ii = 0; ii < num_factors_; ++ii) - { - // Add all terms in one linear factor and store in temp_sum_d_ - temp_sum_mp_ = mpfr_complex(0); - for (int jj = 0; jj < num_variables_ + 1; ++jj) - { - temp_sum_mp_ += coeffs_ref(ii,jj)*temp_var_mp_[jj]; - } - - - // Multiply factors together - evaluation_value *= temp_sum_mp_; - } - } - - - - - - - - - - - - - - - - - - - - - - private: - Mat coeffs_rat_real_; ///< Matrix of real rational coefficients that define the linear product. Each row corresponds to a factor in the product, columns correspond to the terms in each factor, with the final column being the constant coefficient. These rationals can then be downsampled for each data type. - - Mat coeffs_rat_imag_; ///< Same as coeffs_rat_real_ but for imaginary portion of the coefficients. - - std::tuple< Mat, Mat > coeffs_; ///< Matrix of coefficients that define the linear product. Each row corresponds to a factor in the product, columns correspond to the terms in each factor with the final column being the constant coefficient. This is a tuple with one matrix for each data type. - - VariableGroup variables_; ///< Variables to be used in each linear factor. Does not have to correspond directly to a variable group from the system. - - std::shared_ptr hom_variable_; ///< The homogenizing variable for this variable group. Initially this is set to an Integer = 1. When we homogenize, this is set to the variable. - - - size_t num_factors_; ///< The number of factors in the linear product. - size_t num_variables_; ///< The number of variables in each linear. - bool is_rational_coeffs_; ///< Do we have a rational coefficient to downsample from? - bool is_homogenized_ = false; ///< Have we homogenized the linear product? - bool is_hom_vars_ = false; ///< Is this linear for a homogeneous variable group? - - - mutable std::vector temp_var_d_; - mutable std::vector temp_var_mp_; - mutable mpfr_complex temp_sum_mp_; - mutable dbl temp_sum_d_; - - - - - - - - private: - - LinearProduct() = default; - - LinearProduct(VariableGroup const& variables, std::shared_ptr const& hom_var, - Mat const& coeffs_real, Mat const& coeffs_imag, bool is_hom_vars) : - variables_(variables), num_factors_(coeffs_real.rows()), hom_variable_(hom_var), is_hom_vars_(is_hom_vars) - { - num_variables_ = variables.size(); - - // Resize coeffs matrix - Mat& coeffs_dbl_ref = std::get>(coeffs_); - coeffs_dbl_ref.resize(num_factors_, num_variables_+1); - Mat& coeffs_mpfr_ref = std::get>(coeffs_); - coeffs_mpfr_ref.resize(num_factors_, num_variables_+1); - - // Resize temporary variable holders - temp_var_d_.resize(num_variables_ + 1); - temp_var_d_[num_variables_] = dbl(1); - temp_var_mp_.resize(num_variables_ + 1); - temp_var_mp_[num_variables_] = mpfr_complex(1); - - - coeffs_rat_real_ = coeffs_real; - coeffs_rat_imag_ = coeffs_imag; - - for (int ii = 0; ii < num_factors_; ++ii) - { - for (int jj = 0; jj < num_variables_+1; ++jj) - { - coeffs_dbl_ref(ii,jj).real( static_cast(coeffs_rat_real_(ii,jj)) ); - coeffs_dbl_ref(ii,jj).imag( static_cast(coeffs_rat_imag_(ii,jj)) ); - coeffs_mpfr_ref(ii,jj).real( static_cast(coeffs_rat_real_(ii,jj)) ); - coeffs_mpfr_ref(ii,jj).imag( static_cast(coeffs_rat_imag_(ii,jj)) ); - } - } - - is_rational_coeffs_ = true; - - if(is_hom_vars) - { - is_homogenized_ = true; - hom_variable_ = Integer::Make(0); - } - - } - - - - LinearProduct(VariableGroup const& variables, std::shared_ptr const& hom_var, Mat const& coeffs, bool is_hom_vars) : - variables_(variables), num_factors_(coeffs.rows()), hom_variable_(hom_var), is_hom_vars_(is_hom_vars) - { - num_variables_ = variables.size(); - - // Resize coeffs matrix - Mat& coeffs_dbl_ref = std::get>(coeffs_); - coeffs_dbl_ref.resize(num_factors_, num_variables_+1); - Mat& coeffs_mpfr_ref = std::get>(coeffs_); - coeffs_mpfr_ref.resize(num_factors_, num_variables_+1); - - // Resize temporary variable holders - temp_var_d_.resize(num_variables_ + 1); - temp_var_d_[num_variables_] = dbl(1); - temp_var_mp_.resize(num_variables_ + 1); - temp_var_mp_[num_variables_] = mpfr_complex(1); - - coeffs_mpfr_ref = coeffs; - - for (int ii = 0; ii < num_factors_; ++ii) - { - for (int jj = 0; jj < num_variables_+1; ++jj) - { - coeffs_dbl_ref(ii,jj) = static_cast(coeffs_mpfr_ref(ii,jj)); - } - } - - is_rational_coeffs_ = false; - - if(is_hom_vars) - { - is_homogenized_ = true; - hom_variable_ = Integer::Make(0); - } - - } - - - /** - \brief Break off a single linear factor in the product and return as a LinearProduct node. - - \param index Index of the linear factor, starting at 0 - \return LinearProduct node contain the single linear. - */ - - std::shared_ptr GetLinears(size_t index) const; - - - - /** - \brief Break off a set of linear factors in the product and return as a LinearProduct node. - - \param indices std::vector of indices into the factors_ vector - \return LinearProduct node contain the linears. - */ - - std::shared_ptr GetLinears(std::vector indices) const; - - - - void SetupVariables(size_t num_factors, VariableGroup const& variables) - { - num_variables_ = variables.size(); - - // Resize coeffs matrix - Mat& coeffs_dbl_ref = std::get>(coeffs_); - coeffs_dbl_ref.resize(num_factors_, num_variables_+1); - Mat& coeffs_mpfr_ref = std::get>(coeffs_); - coeffs_mpfr_ref.resize(num_factors_, num_variables_+1); - - // Resize temporary variable holders - temp_var_d_.resize(num_variables_ + 1); - temp_var_d_[num_variables_] = dbl(1); - temp_var_mp_.resize(num_variables_ + 1); - temp_var_mp_[num_variables_] = mpfr_complex(1); - - hom_variable_ = Integer::Make(1); - } - - - - - friend class boost::serialization::access; - - template - void serialize(Archive& ar, const unsigned version) { - ar & boost::serialization::base_object(*this); - } - - - void PrecisionChangeSpecific(unsigned prec) const - { - temp_sum_mp_.precision(prec); - for(auto& v : temp_var_mp_) - { - v.precision(prec); - } - - Mat coeffs_ref = std::get>(coeffs_); - for(int ii = 0; ii < coeffs_ref.rows(); ++ii) - { - for(int jj = 0; jj < coeffs_ref.cols(); ++jj) - { - coeffs_ref(ii,jj).precision(prec); - coeffs_ref(ii,jj).real( static_cast(coeffs_rat_real_(ii,jj)) ); - coeffs_ref(ii,jj).imag( static_cast(coeffs_rat_imag_(ii,jj)) ); - } - } - } - - }; - - - - - - - - - - - - - - - - - - /** - \brief Represents the differential of a single linear. - - This class represents a linear of differentials. This is the result of differentiating a single linear node. - - */ - class DiffLinear : public virtual Symbol, public virtual EnableSharedFromThisVirtual - { - public: - BERTINI_DEFAULT_VISITABLE() - - virtual ~DiffLinear() = default; - - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new DiffLinear(ts...) ); - } - - private: - - /** - \brief Create a linear differential node. Only a linear, not a product. - - \param linear The linear that we are differentiating. - - */ - DiffLinear(std::shared_ptr const& linear); - - public: - - - - - - - - - - - - - /** - \brief Reset variable values in this node - */ - void Reset() const override - { - Node::ResetStoredValues(); - }; - - - /** - Method for printing to output stream - */ - void print(std::ostream & target) const override; - - - - - /** - Return SumOperator whose children are derivatives of children_ - */ - std::shared_ptr Differentiate(std::shared_ptr const& v = nullptr) const override - { - return Integer::Make(0); - } - - - /** - \brief Computes the degree for a particular variable. If that variable is part of the linear product, the degree is equal to the number of factors. - - \param v The variable we are determining the degree with respect to. - \return Degree of polynomial with respect to variable v. - */ - int Degree(std::shared_ptr const& v = nullptr) const override - { - return 0; - }; - - - - - /** - \brief Computer the degree for a particular group of variables. If one of the variables is a part of the linear product, the degree is equal to the number of factors. - - \param vars The group of variables we are determing the degree with respect to. - - \return Degree of polynomial with respect to variable group. - */ - - int Degree(VariableGroup const& vars) const override - { - return 0; - }; - - /** - \brief Compute the multidegree with respect to a variable group. This is for homogenization, and testing for homogeneity. - - \param vars The variable group computing degree with respect to. - \return Multidegree vector of polynomial with repect to variable group. - */ - std::vector MultiDegree(VariableGroup const& vars) const override - { - return std::vector(vars.size(),0); - } - - - - - - /** - \brief Homogenize a sum, with respect to a variable group, and using a homogenizing variable. - - \param vars Variable group to homogenize with respect to. - \param homvar Homogenization variable. - */ - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override - { - }; - - - bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override - { - return true; - } - - /** - \brief Check for homogeneity, with respect to a variable group. If vars is the same variable group as the one used to create the linear product, and it has been homogenized, then return true. - - \param vars Variable group to check if homogenous with respect to. - - \return boolean. - */ - bool IsHomogeneous(VariableGroup const& vars) const override - { - return true; - }; - - - - /** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ - virtual void precision(unsigned int prec) const override - { - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); - - this->PrecisionChangeSpecific(prec); - - }; - - - - - - - - - - - - - - - - - - - - - - - - - - - protected: - /** - \brief Evaluation of linear product node. Returns evaluation value. - - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override - { - dbl eval_value; - - this->FreshEval_d(eval_value, diff_variable); - return eval_value; - } - - /** - \brief Evaluation of linear product node IN PLACE. Returns evaluation value. - - \param evaluation_value The in place variable that stores the evaluation. - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override - { - for(int ii = 0; ii < variables_.size(); ++ii) - { - if(diff_variable == variables_[ii]) - { - auto& coeff_ref = std::get>(coeffs_); - evaluation_value = coeff_ref(0,ii); - return; - } - } - - // If not one of the affine variables - if(diff_variable == hom_variable_) - { - auto& coeff_ref = std::get>(coeffs_); - evaluation_value = coeff_ref(0,variables_.size()-1); - return; - } - - - // If none of the variables - evaluation_value = dbl(0); - return; - } - - - /** - \brief Evaluation of linear product node. Returns evaluation value. - - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override - { - mpfr_complex eval_value; - - this->FreshEval_mp(eval_value, diff_variable); - return eval_value; - } - - - /** - \brief Evaluation of linear product node IN PLACE. Returns evaluation value. - - \param evaluation_value The in place variable that stores the evaluation. - \param diff_variable Variable that we are differentiating with respect to. Only for evaluating Jacobians. - */ - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override - { - for(int ii = 0; ii < variables_.size(); ++ii) - { - if(diff_variable == variables_[ii]) - { - auto& coeff_ref = std::get>(coeffs_); - evaluation_value = coeff_ref(0,ii); - return; - } - } - - // If not one of the affine variables - if(diff_variable == hom_variable_) - { - auto& coeff_ref = std::get>(coeffs_); - evaluation_value = coeff_ref(0,variables_.size()-1); - return; - } - - - // If none of the variables - evaluation_value = mpfr_complex(0); - return; - } - - - - - - - - - - - - - - - - - - - - - - private: - // std::vector< std::vector< std::tuple > > coeffs_; - Mat coeffs_rat_real_; ///< Matrix of real rational coefficients that define the linear product. Each row corresponds to a factor in the product, columns correspond to the terms in each factor, with the final column being the constant coefficient. These rationals can then be downsampled for each data type. - - Mat coeffs_rat_imag_; ///< Same as coeffs_rat_real_ but for imaginary portion of the coefficients. - - std::tuple< Mat, Mat > coeffs_; ///< Matrix of coefficients that define the linear product. Each row corresponds to a factor in the product, columns correspond to the terms in each factor with the final column being the constant coefficient. This is a tuple with one matrix for each data type. - - VariableGroup variables_; ///< Differentials of variables used in the linear. - - std::shared_ptr hom_variable_; ///< The homogenizing variable for this variable group. Initially this is set to an Integer = 0. When we homogenize, this is set to the variable. - - - size_t num_variables_; ///< The number of variables in each linear. - - - - - - - - - - private: - - DiffLinear() = default; - - - - friend class boost::serialization::access; - - template - void serialize(Archive& ar, const unsigned version) { - ar & boost::serialization::base_object(*this); - } - - - void PrecisionChangeSpecific(unsigned prec) const - { - Mat coeffs_ref = std::get>(coeffs_); - for(int ii = 0; ii < coeffs_ref.rows(); ++ii) - { - for(int jj = 0; jj < coeffs_ref.cols(); ++jj) - { - coeffs_ref(ii,jj).precision(prec); - } - } - } - - }; - - - - - - } // re: namespace node -} // re: namespace bertini - - - - -#endif diff --git a/core/include/bertini2/function_tree/symbols/number.hpp b/core/include/bertini2/function_tree/symbols/number.hpp index 95a8f62f0..9351fdb72 100755 --- a/core/include/bertini2/function_tree/symbols/number.hpp +++ b/core/include/bertini2/function_tree/symbols/number.hpp @@ -56,10 +56,9 @@ namespace node{ /** \brief Abstract Number type from which other Numbers derive. - This class represents constant leaves to a function tree. FreshEval simply returns - the value of the constant. + This class represents constant leaves to a function tree. */ - class Number : public virtual Symbol + class Number : public Symbol { public: @@ -67,7 +66,6 @@ namespace node{ - void Reset() const override; @@ -81,7 +79,7 @@ namespace node{ The degree of a number is always 0. It's a number. */ inline - int Degree(std::shared_ptr const& v = nullptr) const override + int Degree(std::shared_ptr const& /*v*/ = nullptr) const override { return 0; } @@ -92,7 +90,7 @@ namespace node{ The degree of a number is always 0. It's a number. */ inline - int Degree(VariableGroup const& vars) const override + int Degree(VariableGroup const& /*vars*/) const override { return 0; } @@ -109,22 +107,13 @@ namespace node{ return std::vector(vars.size(), 0); } - /** - \brief Homogenize this node. - - Homogenization of a number is a trivial operation. Don't do anything. - */ - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override - { - - } /** \brief Is this node homogeneous? Numbers are always homogeneous */ - bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override + bool IsHomogeneous(std::shared_ptr const& /*v*/ = nullptr) const override { return true; } @@ -132,19 +121,12 @@ namespace node{ /** Check for homogeneity, with respect to a variable group. */ - bool IsHomogeneous(VariableGroup const& vars) const override + bool IsHomogeneous(VariableGroup const& /*vars*/) const override { return true; } - /** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ - void precision(unsigned int prec) const override; - /** \brief Differentiate a number. */ @@ -156,7 +138,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } @@ -169,7 +151,7 @@ namespace node{ Signed real Integer storage in an expression tree. Consider using a Rational type. */ - class Integer : public virtual Number, public virtual EnableSharedFromThisVirtual + class Integer : public Number { public: BERTINI_DEFAULT_VISITABLE() @@ -179,16 +161,52 @@ namespace node{ Integer(Integer const&) = default; ~Integer() = default; - + void print(std::ostream & target) const override; - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Integer(ts...) ); + // negative literals print with a leading '-', so parenthesize like a Negate + unsigned Precedence() const override + { + return true_value_ < 0 ? PrecNegate : PrecAtom; + } + + /** + \brief Get the literal value this node represents. + */ + mpz_int const& GetValue() const + { + return true_value_; + } + + bool IsLiteralZero() const override + { + return true_value_ == 0; + } + + bool IsLiteralOne() const override + { + return true_value_ == 1; + } + + std::size_t HashImpl() const override + { + std::size_t h = typeid(Integer).hash_code(); + HashCombine(h, std::hash{}(true_value_.str())); + return h; + } + bool IsSame(Node const& other) const override + { + auto o = dynamic_cast(&other); + return o && true_value_ == o->true_value_; + } + + template + static + std::shared_ptr Make(Ts&& ...ts){ + return std::static_pointer_cast(Intern(std::shared_ptr( new Integer(ts...) ))); } private: @@ -209,15 +227,6 @@ namespace node{ - // Return value of constant - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - - - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; mpz_int true_value_; @@ -227,7 +236,7 @@ namespace node{ Integer() = default; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & true_value_; } @@ -246,7 +255,7 @@ namespace node{ Number type for storing floating point numbers within an expression tree. The number passed in at construct time is stored as the true value, and evaluation down or up samples from this 'true value'. Consider using a Rational or Integer if possible. */ - class Float : public virtual Number, public virtual EnableSharedFromThisVirtual + class Float : public Number { public: BERTINI_DEFAULT_VISITABLE() @@ -261,14 +270,53 @@ namespace node{ void print(std::ostream & target) const override; + // real-valued floats print bare (no complex pair); negative ones get + // a leading '-', so parenthesize like a Negate. pairs self-delimit. + unsigned Precedence() const override + { + if (highest_precision_value_.imag() == 0 && highest_precision_value_.real() < 0) + return PrecNegate; + return PrecAtom; + } - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Float(ts...) ); + /** + \brief Get the literal value this node represents, at its stored (highest) precision. + */ + mpfr_complex const& GetValue() const + { + return highest_precision_value_; } + bool IsLiteralZero() const override + { + return highest_precision_value_.real() == 0 && highest_precision_value_.imag() == 0; + } + bool IsLiteralOne() const override + { + return highest_precision_value_.real() == 1 && highest_precision_value_.imag() == 0; + } + + std::size_t HashImpl() const override + { + std::size_t h = typeid(Float).hash_code(); + HashCombine(h, std::hash{}(highest_precision_value_.real().str())); + HashCombine(h, std::hash{}(highest_precision_value_.imag().str())); + return h; + } + bool IsSame(Node const& other) const override + { + auto o = dynamic_cast(&other); + return o + && highest_precision_value_.real() == o->highest_precision_value_.real() + && highest_precision_value_.imag() == o->highest_precision_value_.imag(); + } + + template + static + std::shared_ptr Make(Ts&& ...ts){ + return std::static_pointer_cast(Intern(std::shared_ptr( new Float(ts...) ))); + } private: @@ -288,14 +336,6 @@ namespace node{ Float(std::string const& rval, std::string const& ival) : highest_precision_value_(rval,ival) {} - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - - - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; mpfr_complex highest_precision_value_; @@ -303,7 +343,7 @@ namespace node{ friend class boost::serialization::access; Float() = default; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & const_cast(highest_precision_value_); } @@ -319,7 +359,7 @@ namespace node{ The Rational number type for Bertini2 expression trees. The `true value' is stored using two mpq_rational numbers from the Boost.Multiprecision library, and the ratio is converted into a double or a mpfr_complex at evaluate time. */ - class Rational : public virtual Number, public virtual EnableSharedFromThisVirtual + class Rational : public Number { public: BERTINI_DEFAULT_VISITABLE() @@ -359,17 +399,87 @@ namespace node{ void print(std::ostream & target) const override; + // real-valued rationals print bare (no complex pair). the bare form is + // textually an expression: a leading '-' parenthesizes like a Negate, and + // 'p/q' contains a division, so it binds like a Mult (x/(1/3), not x/1/3). + // complex pairs self-delimit. + unsigned Precedence() const override + { + if (true_value_imag_ == 0) + { + if (true_value_real_ < 0) + return PrecNegate; + if (denominator(true_value_real_) != 1) + return PrecMult; + } + return PrecAtom; + } + /** + \brief Get the real part of the literal value this node represents. + */ + mpq_rational const& GetValueReal() const + { + return true_value_real_; + } + /** + \brief Get the imaginary part of the literal value this node represents. + */ + mpq_rational const& GetValueImag() const + { + return true_value_imag_; + } - - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Rational(ts...) ); + /** + \brief Get this exact constant as a number of type NumT, independent of the + evaluation engine. + + Unlike Eval, this does no caching and never touches the node's stored working + value --- it is a pure read of the literal. It matches the literal's conversion: + double truncation for dbl, and a value at the current thread precision for mpfr. + */ + template + NumT Value() const + { + if constexpr (std::is_same::value) + return dbl(double(true_value_real_), double(true_value_imag_)); + else + return NumT(boost::multiprecision::mpfr_float(true_value_real_, ThreadPrecision()), + boost::multiprecision::mpfr_float(true_value_imag_, ThreadPrecision())); + } + + bool IsLiteralZero() const override + { + return true_value_real_ == 0 && true_value_imag_ == 0; + } + + bool IsLiteralOne() const override + { + return true_value_real_ == 1 && true_value_imag_ == 0; + } + + std::size_t HashImpl() const override + { + std::size_t h = typeid(Rational).hash_code(); + HashCombine(h, std::hash{}(true_value_real_.str())); + HashCombine(h, std::hash{}(true_value_imag_.str())); + return h; + } + bool IsSame(Node const& other) const override + { + auto o = dynamic_cast(&other); + return o && true_value_real_ == o->true_value_real_ && true_value_imag_ == o->true_value_imag_; } + + template + static + std::shared_ptr Make(Ts&& ...ts){ + return std::static_pointer_cast(Intern(std::shared_ptr( new Rational(ts...) ))); + } + private: explicit @@ -396,15 +506,6 @@ namespace node{ {} - // Return value of constant - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - - - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; mpq_rational true_value_real_, true_value_imag_; @@ -412,7 +513,7 @@ namespace node{ friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & const_cast(true_value_real_); ar & const_cast(true_value_imag_); diff --git a/core/include/bertini2/function_tree/symbols/special_number.hpp b/core/include/bertini2/function_tree/symbols/special_number.hpp index 5598abc4e..04e25b8df 100644 --- a/core/include/bertini2/function_tree/symbols/special_number.hpp +++ b/core/include/bertini2/function_tree/symbols/special_number.hpp @@ -63,42 +63,43 @@ namespace node{ The number \f$\pi\f$. Gets its own class because it is such an important number. */ - class Pi : public virtual Number, public virtual NamedSymbol, public virtual EnableSharedFromThisVirtual + class Pi : public Number, public NameHolder { public: BERTINI_DEFAULT_VISITABLE() virtual ~Pi() = default; - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Pi(ts...) ); + template + static + std::shared_ptr Make(Ts&& ...ts){ + return std::static_pointer_cast(Intern(std::shared_ptr( new Pi(ts...) ))); + } + + void print(std::ostream & target) const override + { + target << name(); } private: - Pi() : NamedSymbol("pi") + Pi() : NameHolder("pi") {} // Return value of constant - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); - ar & boost::serialization::base_object(*this); + ar & boost::serialization::base_object(*this); } }; @@ -109,46 +110,47 @@ namespace node{ The number \f$e\f$. Gets its own class because it is such an important number. */ - class E : public virtual Number, public virtual NamedSymbol, public virtual EnableSharedFromThisVirtual + class E : public Number, public NameHolder { public: BERTINI_DEFAULT_VISITABLE() - + virtual ~E() = default; - template - static - std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new E(ts...) ); + template + static + std::shared_ptr Make(Ts&& ...ts){ + return std::static_pointer_cast(Intern(std::shared_ptr( new E(ts...) ))); + } + + void print(std::ostream & target) const override + { + target << name(); } private: - E() : NamedSymbol("e") + E() : NameHolder("e") {} // Return value of constant - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); - ar & boost::serialization::base_object(*this); + ar & boost::serialization::base_object(*this); } }; diff --git a/core/include/bertini2/function_tree/symbols/symbol.hpp b/core/include/bertini2/function_tree/symbols/symbol.hpp index 96cc06322..057eead4c 100755 --- a/core/include/bertini2/function_tree/symbols/symbol.hpp +++ b/core/include/bertini2/function_tree/symbols/symbol.hpp @@ -53,21 +53,18 @@ namespace node { This class is an interface for all non-operators. */ - class Symbol : public virtual Node + class Symbol : public Node { public: virtual ~Symbol() = default; - unsigned EliminateZeros() override; - unsigned EliminateOnes() override; - private: friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } }; @@ -79,12 +76,54 @@ namespace node { + /** + \brief Capability class for things which have a name. + + Deliberately NOT a Node: classes that need a name alongside a different + primary base (e.g. special_number::Pi, which is a Number) inherit this + without creating a diamond in the Node hierarchy. + */ + class NameHolder + { + public: + const std::string& name() const + { + return name_; + } + + void name(const std::string& new_name) + { + name_ = new_name; + } + + protected: + ~NameHolder() = default; // not polymorphic; never delete through NameHolder* + + NameHolder() = default; + + explicit NameHolder(std::string new_name) : name_(std::move(new_name)) + {} + + std::string name_; + + private: + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) { + ar & name_; + } + }; + + + + /** \brief Symbols which have names are named symbols. - + Symbols which have names are named symbols. */ - class NamedSymbol : public virtual Symbol + class NamedSymbol : public Symbol { public: @@ -120,7 +159,7 @@ namespace node { friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & name_; } diff --git a/core/include/bertini2/function_tree/symbols/variable.hpp b/core/include/bertini2/function_tree/symbols/variable.hpp index 166068653..c834dab64 100755 --- a/core/include/bertini2/function_tree/symbols/variable.hpp +++ b/core/include/bertini2/function_tree/symbols/variable.hpp @@ -50,12 +50,11 @@ namespace node{ /** \brief Represents variable leaves in the function tree. - This class represents variable leaves in the function tree. FreshEval returns - the current value of the variable. + This class represents variable leaves in the function tree. When differentiated, produces a differential referring to it. */ - class Variable : public virtual NamedSymbol, public virtual EnableSharedFromThisVirtual + class Variable : public NamedSymbol { public: BERTINI_DEFAULT_VISITABLE() @@ -64,7 +63,7 @@ namespace node{ template static std::shared_ptr Make(Ts&& ...ts){ - return std::shared_ptr( new Variable(ts...) ); + return std::static_pointer_cast(Intern(std::shared_ptr( new Variable(ts...) ))); } private: @@ -74,41 +73,31 @@ namespace node{ virtual ~Variable() = default; - + + // variables are canonical BY NAME -- Make("x") interns to a single shared x, + // so system1's x IS system2's x. (Value/eval-state lives on that shared node until C2 + // moves it into a per-thread eval context.) + std::size_t HashImpl() const override + { + std::size_t h = typeid(Variable).hash_code(); + HashCombine(h, std::hash{}(name())); + return h; + } + bool IsSame(Node const& other) const override + { + auto o = dynamic_cast(&other); + return o && name() == o->name(); + } explicit operator std::string(){return name();} - // This sets the value for the variable - template - void set_current_value(T const& val); - - /** - \brief Changes the value of the variable to be not-a-number. - */ - template - void SetToNan(); - - /** - \brief Changes the value of the variable to be a random complex number. - */ - template - void SetToRand(); - - /** - \brief Changes the value of the variable to be a random complex number, of magnitude 1. - */ - template - void SetToRandUnit(); - /** - Differentiates a variable. + Differentiates a variable. */ std::shared_ptr Differentiate(std::shared_ptr const& v = nullptr) const override; - - void Reset() const override; /** Compute the degree with respect to a single variable. @@ -123,8 +112,6 @@ namespace node{ std::vector MultiDegree(VariableGroup const& vars) const override; - void Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) override; - bool IsHomogeneous(std::shared_ptr const& v = nullptr) const override; /** @@ -132,33 +119,15 @@ namespace node{ */ bool IsHomogeneous(VariableGroup const& vars) const override; - - /** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ - void precision(unsigned int prec) const override; - protected: - - // Return current value of the variable. - dbl FreshEval_d(std::shared_ptr const& diff_variable) const override; - - void FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const override; - - - mpfr_complex FreshEval_mp(std::shared_ptr const& diff_variable) const override; - - void FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const override; Variable(); private: - + friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } diff --git a/core/include/bertini2/io/classic_writer.hpp b/core/include/bertini2/io/classic_writer.hpp new file mode 100644 index 000000000..073a79b7a --- /dev/null +++ b/core/include/bertini2/io/classic_writer.hpp @@ -0,0 +1,196 @@ +//This file is part of Bertini 2. +// +//Bertini 2 is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//Bertini 2 is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with Bertini 2. If not, see . +// +// Copyright(C) 2015 - 2026 by Bertini2 developers +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file classic_writer.hpp + +\brief Emit a System (and, with a config, a whole solve) as a Bertini 1 *classic* input file. + +The classic parser (io/parsing) reads Bertini 1 input files; this is its inverse. Emitting a +`CONFIG ... END;\nINPUT ... END;` file lets the *same* problem be run in Bertini 1 for +cross-validation -- e.g. to check whether a path-crossing or a root count reproduces across +implementations (modulo each one's random start system). + +The emitted text is round-trip-safe: parsing it back with bertini::System's classic constructor +yields an equivalent system. +*/ + +#pragma once + +#include "bertini2/system/system.hpp" +#include "bertini2/function_tree/find.hpp" +#include "bertini2/trackers/config.hpp" + +#include +#include +#include +#include +#include + +namespace bertini{ + namespace classic{ + + /** + \brief Emit the INPUT-section body of a system in classic syntax: the variable groups, any + named subexpressions (as `name = expr;` subfunctions, defined before the functions that use + them), and the functions (`function f0,f1,...;` then `f0 = ...;`). + + No `INPUT`/`END;` wrapper -- WriteClassicInput adds that. Functions are named `f0, f1, ...` + positionally (Bertini 2 functions carry no user name after parsing). + */ + inline void EmitSystem(std::ostream& out, System const& sys) + { + auto emit_groups = [&out](char const* keyword, auto const& groups){ + for (auto const& grp : groups) + { + out << keyword << " "; + for (size_t i = 0; i < grp.size(); ++i) + out << (i ? ", " : "") << *grp[i]; + out << ";\n"; + } + }; + emit_groups("variable_group", sys.VariableGroups()); + emit_groups("hom_variable_group", sys.HomVariableGroups()); + + auto functions = sys.NaturalFunctionsAsNodes(); + + // Named subexpressions are not stored separately; they are discovered inside the function + // trees (nested ones included). Emit each as `name = expr;` before the functions, the way + // the classic parser expects a subfunction to be defined ahead of its use. + { + std::vector> roots(functions.begin(), functions.end()); + for (auto const& ne : node::Find(roots)) + out << ne->name() << " = " << ne->EntryNode() << ";\n"; + } + + out << "function "; + for (size_t i = 0; i < functions.size(); ++i) + out << (i ? ", " : "") << "f" << i; + out << ";\n"; + for (size_t i = 0; i < functions.size(); ++i) + out << "f" << i << " = " << functions[i] << ";\n"; + } + + /// \brief The INPUT-section body as a string. \see EmitSystem. + inline std::string SystemToClassic(System const& sys) + { + std::ostringstream ss; + EmitSystem(ss, sys); + return ss.str(); + } + + + /** + \brief Tracking / precision settings to emit in the CONFIG section, in Bertini 1 terms. + + Defaults mirror Bertini 2's defaults for an adaptive zero-dim solve, so the emitted file runs + the *same* problem with the *same* knobs in Bertini 1 (the random start system aside). + */ + struct ClassicWriteOptions + { + int tracktype = 0; ///< 0 = zero-dimensional solve + int mptype = 2; ///< 0 double, 1 fixed-multiple, 2 adaptive + int odepredictor = 5; ///< 5 = RKF45 (the Bertini 2 default) + double tracktolbeforeeg = 1e-5; ///< Newton tolerance before the endgame + double tracktolduringeg = 1e-6; ///< Newton tolerance during the endgame + double finaltol = 1e-11; ///< final tracking tolerance + // step-size cadence -- governs how aggressively a path may take a big step (i.e. jump), + // so it must be controlled for a fair crossing-rate comparison against Bertini 1. + double maxstepsize = 0.1; ///< MaxStepSize (B2 SteppingConfig default 1/10) + double stepsuccessfactor = 2.0; ///< StepSuccessFactor (B2 default 2) + double stepfailfactor = 0.5; ///< StepFailFactor (B2 default 1/2) + unsigned stepsforincrease = 5; ///< StepsForIncrease (B2 default 5) + unsigned long maxnumbersteps = 100000;///< MaxNumberSteps (B2 default 1e5) + unsigned maxnewtonits = 2; ///< MaxNewtonIts (B2 default 2) + unsigned maxcrossedpathresolves = 2; ///< endgame-boundary crossed-path re-track attempts + }; + + /** + \brief Map a Bertini 2 `Predictor` to its Bertini 1 classic `odepredictor` integer. + + The inverse of the classic settings parser's predictor table (`settings_parsers/tracking.hpp`). + `HeunEuler` has no classic number; it is emitted as `1` (Heun), its nearest classic relative. + */ + inline int PredictorToClassic(tracking::Predictor p) + { + using P = tracking::Predictor; + switch (p) + { + case P::Constant: return -1; + case P::Euler: return 0; + case P::Heun: return 1; + case P::RK4: return 2; + case P::HeunEuler: return 1; + case P::RKNorsett34: return 4; + case P::RKF45: return 5; + case P::RKCashKarp45: return 6; + case P::RKDormandPrince56: return 7; + case P::RKVerner67: return 8; + } + return 5; // RKF45 -- the Bertini 2 default, a safe fallback + } + + /** + \brief Emit the CONFIG-section body (no CONFIG/END wrapper). The AMP coefficient/degree + bounds are derived from the system; the rest come from `opt`. + */ + inline void EmitConfig(std::ostream& out, System const& sys, ClassicWriteOptions const& opt) + { + auto num = [](double v){ std::ostringstream s; s << std::setprecision(15) << v; return s.str(); }; + out << "tracktype: " << opt.tracktype << ";\n"; + out << "mptype: " << opt.mptype << ";\n"; + out << "odepredictor: " << opt.odepredictor << ";\n"; + out << "tracktolbeforeeg: " << num(opt.tracktolbeforeeg) << ";\n"; + out << "tracktolduringeg: " << num(opt.tracktolduringeg) << ";\n"; + out << "finaltol: " << num(opt.finaltol) << ";\n"; + out << "maxstepsize: " << num(opt.maxstepsize) << ";\n"; + out << "stepsuccessfactor: " << num(opt.stepsuccessfactor) << ";\n"; + out << "stepfailfactor: " << num(opt.stepfailfactor) << ";\n"; + out << "stepsforincrease: " << opt.stepsforincrease << ";\n"; + out << "maxnumbersteps: " << opt.maxnumbersteps << ";\n"; + out << "maxnewtonits: " << opt.maxnewtonits << ";\n"; + out << "maxcrossedpathresolves: " << opt.maxcrossedpathresolves << ";\n"; + out << "coefficientbound: " << num(static_cast(sys.CoefficientBound())) << ";\n"; + out << "degreebound: " << sys.DegreeBound() << ";\n"; + } + + /// \brief Write a complete Bertini 1 classic input file: `CONFIG ... END;\nINPUT ... END;`. + inline void WriteClassicInput(std::ostream& out, System const& sys, + ClassicWriteOptions const& opt = ClassicWriteOptions{}) + { + out << "CONFIG\n"; + EmitConfig(out, sys, opt); + out << "END;\n\nINPUT\n"; + EmitSystem(out, sys); + out << "END;\n"; + } + + /// \brief The complete classic input file as a string. \see WriteClassicInput. + inline std::string SystemToClassicFile(System const& sys, + ClassicWriteOptions const& opt = ClassicWriteOptions{}) + { + std::ostringstream ss; + WriteClassicInput(ss, sys, opt); + return ss.str(); + } + + } // namespace classic +} // namespace bertini diff --git a/core/include/bertini2/io/file_utilities.hpp b/core/include/bertini2/io/file_utilities.hpp index 47a4e2b8e..4ff5fce1a 100644 --- a/core/include/bertini2/io/file_utilities.hpp +++ b/core/include/bertini2/io/file_utilities.hpp @@ -31,6 +31,7 @@ #pragma once #include #include +#include #include namespace bertini{ diff --git a/core/include/bertini2/io/generators.hpp b/core/include/bertini2/io/generators.hpp index 8b4147c04..9c80c2d4a 100644 --- a/core/include/bertini2/io/generators.hpp +++ b/core/include/bertini2/io/generators.hpp @@ -79,7 +79,7 @@ namespace bertini{ struct BertiniNumPolicy : public karma::real_policies { // we want the numbers always to be in scientific format - static int floatfield(Num n) { return std::ios_base::scientific; } + static int floatfield(Num /*n*/) { return std::ios_base::scientific; } static unsigned int precision(Num) { return std::numeric_limits::max_digits10; @@ -90,7 +90,7 @@ namespace bertini{ struct BertiniNumPolicy : public karma::real_policies { // we want the numbers always to be in scientific format - static int floatfield(mpfr_float n) { return std::ios_base::scientific; } + static int floatfield(mpfr_float /*n*/) { return std::ios_base::scientific; } static unsigned int precision(mpfr_float const& x) { return x.precision(); diff --git a/core/include/bertini2/io/parsing/classic_utilities.hpp b/core/include/bertini2/io/parsing/classic_utilities.hpp index db1553a65..83d88d43f 100644 --- a/core/include/bertini2/io/parsing/classic_utilities.hpp +++ b/core/include/bertini2/io/parsing/classic_utilities.hpp @@ -281,18 +281,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config/input split parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("SplitInputFileParser")) + ); } private: @@ -355,18 +347,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config/input split parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("CommentStripper")) + ); } private: diff --git a/core/include/bertini2/io/parsing/function_rules.hpp b/core/include/bertini2/io/parsing/function_rules.hpp index 6271e589b..c04404260 100644 --- a/core/include/bertini2/io/parsing/function_rules.hpp +++ b/core/include/bertini2/io/parsing/function_rules.hpp @@ -34,7 +34,7 @@ #include "bertini2/io/parsing/qi_files.hpp" #include "bertini2/function_tree/node.hpp" -#include "bertini2/function_tree/roots/function.hpp" +#include "bertini2/function_tree/roots/named_expression.hpp" #include "bertini2/function_tree/operators/arithmetic.hpp" #include "bertini2/function_tree/operators/trig.hpp" @@ -142,7 +142,6 @@ namespace bertini { struct FunctionParser : qi::grammar(), boost::spirit::ascii::space_type> { using Node = node::Node; - using Function = node::Function; using Float = node::Float; using Integer = node::Integer; using Rational = node::Rational; @@ -162,7 +161,9 @@ namespace bertini { using ::pow; root_rule_.name("function_"); - root_rule_ = expression_ [ _val = make_shared_()(_1)]; + // Return the bare parsed expression (no Function wrapper): the System parser names + // it (a NamedExpression for subfunctions) or stores it directly (top-level functions). + root_rule_ = expression_ [ _val = _1]; /////////////////// @@ -198,8 +199,11 @@ namespace bertini { exp_elem_ = (symbol_ >> !qi::alnum) [_val = _1] | ( '(' > expression_ [_val = _1] > ')' ) // using the > expectation here. - | (lit('-') > expression_ [_val = -_1]) - | (lit('+') > expression_ [_val = _1]) + // unary +/- bind a single factor_, NOT the whole expression_: "-y+x" is + // (-y)+x, and "-x^2" is -(x^2). (Binding expression_ here made a leading + // minus greedily negate everything after it.) + | (lit('-') > factor_ [_val = -_1]) + | (lit('+') > factor_ [_val = _1]) | (lit("sin") > '(' > expression_ [_val = sin_lazy(_1)] > ')' ) | (lit("cos") > '(' > expression_ [_val = cos_lazy(_1)] > ')' ) | (lit("tan") > '(' > expression_ [_val = tan_lazy(_1)] > ')' ) @@ -245,22 +249,10 @@ namespace bertini { - using qi::on_error; - using boost::phoenix::val; - using boost::phoenix::construct; - - - on_error - ( - root_rule_ - , std::cout - << val("Function parser error: expecting ") - << _4 - << val(" here: \"") - << construct(_3, _2) - << val("\"") - << std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("FunctionParser")) + ); diff --git a/core/include/bertini2/io/parsing/number_parsers.hpp b/core/include/bertini2/io/parsing/number_parsers.hpp index 28b87ac61..91239748e 100644 --- a/core/include/bertini2/io/parsing/number_parsers.hpp +++ b/core/include/bertini2/io/parsing/number_parsers.hpp @@ -60,25 +60,17 @@ namespace bertini{ auto asdf = max(prev_prec,LowestMultiplePrecision()); auto digits = max(P.size(),static_cast(asdf)); - B.precision(digits); - B = mpfr_float(P,digits); + B.precision(static_cast(digits)); + B = mpfr_float(P,static_cast(digits)); }, _val,_1 ) ]; - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("mpfr_float parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("MpfrFloatParser")) + ); } qi::rule root_rule_; @@ -103,7 +95,6 @@ namespace bertini{ [] (mpfr_complex & B, mpfr_float const& P, mpfr_float const& Q) { - auto prev_prec = DefaultPrecision(); auto digits = max(P.precision(),Q.precision()); B.precision(digits); diff --git a/core/include/bertini2/io/parsing/qi_files.hpp b/core/include/bertini2/io/parsing/qi_files.hpp index c9fa6f124..df77de6ca 100644 --- a/core/include/bertini2/io/parsing/qi_files.hpp +++ b/core/include/bertini2/io/parsing/qi_files.hpp @@ -51,3 +51,55 @@ #include #include + +#include +#include +#include +#include +#include + +namespace bertini { +namespace parsing { +namespace classic { + +inline std::string FormatParseError( + std::string::const_iterator begin, + std::string::const_iterator end, + std::string::const_iterator err_pos, + boost::spirit::info const& what, + std::string const& parser_name) +{ + int line = 1 + (int)std::count(begin, err_pos, '\n'); + auto rev = std::find(std::make_reverse_iterator(err_pos), + std::make_reverse_iterator(begin), '\n'); + int col = 1 + (int)std::distance(rev.base(), err_pos); + + std::string remaining(err_pos, end); + if (remaining.size() > 60) + remaining = remaining.substr(0, 60) + "..."; + if (remaining.empty()) + remaining = ""; + + std::ostringstream oss; + oss << "[" << parser_name << "] parse error at line " << line + << ", col " << col << ":\n" + << " expected: " << what << "\n" + << " found: \"" << remaining << "\"\n"; + return oss.str(); +} + +inline void ReportParseError( + std::string::const_iterator begin, + std::string::const_iterator end, + std::string::const_iterator err_pos, + boost::spirit::info const& what, + std::string const& parser_name) +{ + auto msg = FormatParseError(begin, end, err_pos, what, parser_name); + std::cerr << msg; + throw std::runtime_error(msg); +} + +} // namespace classic +} // namespace parsing +} // namespace bertini diff --git a/core/include/bertini2/io/parsing/settings_parsers.hpp b/core/include/bertini2/io/parsing/settings_parsers.hpp index 8ce779fdb..871b9e08f 100644 --- a/core/include/bertini2/io/parsing/settings_parsers.hpp +++ b/core/include/bertini2/io/parsing/settings_parsers.hpp @@ -35,6 +35,7 @@ #include "bertini2/io/parsing/settings_parsers/tracking.hpp" #include "bertini2/io/parsing/settings_parsers/endgames.hpp" #include "bertini2/io/parsing/settings_parsers/algorithm.hpp" +#include "bertini2/io/parsing/settings_parsers/random.hpp" @@ -50,7 +51,7 @@ namespace bertini { \param config_str The comment-stripped configuration string from a Bertini classic input file. \tparam ConfigT The config ConfigT type - \tparam RT Real number type + \tparam RealT Real number type \returns The config struct filled with data from the input file. */ @@ -64,7 +65,16 @@ namespace bertini { ConfigSettingParser parser; auto parse_success = phrase_parse(iter, end, parser,boost::spirit::ascii::space, settings); if (!parse_success || iter!=end) - throw std::runtime_error("failed to parse into config struct from file"); + { + std::string remaining(iter, end); + if (remaining.size() > 60) + remaining = remaining.substr(0, 60) + "..."; + if (remaining.empty()) + remaining = ""; + throw std::runtime_error( + std::string("[config] parser did not consume entire input; " + "unparsed remainder: \"") + remaining + "\""); + } return settings; } @@ -77,7 +87,7 @@ namespace bertini { A specialization for a typelist of configs appears below. - \tparam RT Real number type + \tparam RealT Real number type \tparam Ts Configuration structures to be filled by the parser */ template @@ -107,7 +117,7 @@ namespace bertini { \brief Specialization of ConfigParser for a single config struct \tparam ConfigT The config ConfigT type - \tparam RT Real number type + \tparam RealT Real number type */ template struct ConfigParser @@ -132,7 +142,7 @@ namespace bertini { \brief Specialization of ConfigParser for a typelist, returning a thing passed down from the base variadic case. \tparam ConfigT The config ConfigT type - \tparam RT Real number type + \tparam RealT Real number type */ template struct ConfigParser> diff --git a/core/include/bertini2/io/parsing/settings_parsers/algorithm.hpp b/core/include/bertini2/io/parsing/settings_parsers/algorithm.hpp index 6f0acac20..75c152543 100644 --- a/core/include/bertini2/io/parsing/settings_parsers/algorithm.hpp +++ b/core/include/bertini2/io/parsing/settings_parsers/algorithm.hpp @@ -134,18 +134,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::Tolerances")) + ); @@ -171,8 +163,8 @@ namespace bertini { - template - struct ConfigSettingParser, Skipper> : qi::grammar(), Skipper> + template + struct ConfigSettingParser : qi::grammar { ConfigSettingParser() : ConfigSettingParser::base_type(root_rule_, "algorithm::ZeroDimConfig") @@ -203,27 +195,27 @@ namespace bertini { root_rule_.name("config::ZeroDim"); - root_rule_ = ((init_prec_[phx::bind( [this](algorithm::ZeroDimConfig & S, int num) + root_rule_ = ((init_prec_[phx::bind( [this](algorithm::ZeroDimConfig & S, int num) { S.initial_ambient_precision = num; }, _val, _1 )] - ^ path_variable_name_[phx::bind( [this](algorithm::ZeroDimConfig & S, std::string omnom) + ^ path_variable_name_[phx::bind( [this](algorithm::ZeroDimConfig & S, std::string omnom) { S.path_variable_name = omnom; }, _val, _1 )] - ^ max_cross_resolve_[phx::bind( [this](algorithm::ZeroDimConfig & S, int num) + ^ max_cross_resolve_[phx::bind( [this](algorithm::ZeroDimConfig & S, int num) { S.max_num_crossed_path_resolve_attempts = num; }, _val, _1 )] - ^ start_time_[phx::bind( [this](algorithm::ZeroDimConfig & S, ComplexT num) + ^ start_time_[phx::bind( [this](algorithm::ZeroDimConfig & S, mpq_rational num) { S.start_time = num; }, _val, _1 )] - ^ endgame_boundary_[phx::bind( [this](algorithm::ZeroDimConfig & S, ComplexT num) + ^ endgame_boundary_[phx::bind( [this](algorithm::ZeroDimConfig & S, mpq_rational num) { S.endgame_boundary = num; }, _val, _1 )] - ^ target_time_[phx::bind( [this](algorithm::ZeroDimConfig & S, ComplexT num) + ^ target_time_[phx::bind( [this](algorithm::ZeroDimConfig & S, mpq_rational num) { S.target_time = num; }, _val, _1 )] @@ -239,9 +231,11 @@ namespace bertini { ; - auto str_to_ComplexT = [this](ComplexT & num, std::string str) + // The times are stored as exact, precision-free mpq_rational. Parse the number + // string to a multiprecision real, then to a rational (the homotopy times are real). + auto str_to_rational = [this](mpq_rational & num, std::string str) { - num = bertini::NumTraits::FromString(str); + num = mpq_rational(mpfr_float(str)); }; @@ -261,15 +255,15 @@ namespace bertini { start_time_.name("start_time_"); start_time_ = *(char_ - all_names_) >> (no_case[start_time_name] >> ':') - >> mpfr_rules.number_string_[phx::bind( str_to_ComplexT, _val, _1 )] >> ';'; + >> mpfr_rules.number_string_[phx::bind( str_to_rational, _val, _1 )] >> ';'; endgame_boundary_.name("endgame_boundary_"); endgame_boundary_ = *(char_ - all_names_) >> (no_case[endgame_boundary_name] >> ':') - >> mpfr_rules.number_string_[phx::bind( str_to_ComplexT, _val, _1 )] >> ';'; + >> mpfr_rules.number_string_[phx::bind( str_to_rational, _val, _1 )] >> ';'; target_time_.name("target_time_"); target_time_ = *(char_ - all_names_) >> (no_case[target_time_name] >> ':') - >> mpfr_rules.number_string_[phx::bind( str_to_ComplexT, _val, _1 )] >> ';'; + >> mpfr_rules.number_string_[phx::bind( str_to_rational, _val, _1 )] >> ';'; @@ -282,18 +276,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::ZeroDim")) + ); @@ -301,9 +287,9 @@ namespace bertini { private: - qi::rule(), ascii::space_type > root_rule_; + qi::rule root_rule_; - qi::rule start_time_, target_time_, endgame_boundary_; + qi::rule start_time_, target_time_, endgame_boundary_; qi::rule init_prec_, max_cross_resolve_; qi::rule valid_variable_name_, path_variable_name_; @@ -372,18 +358,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::MidPath")) + ); @@ -456,18 +434,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::AutoRetrack")) + ); @@ -562,18 +532,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::Sharpening")) + ); @@ -640,15 +602,15 @@ namespace bertini { }, _val, _1 )] ^ slice_before_[phx::bind( [this](algorithm::RegenerationConfig & S, T num) { - S.newton_before_endgame = num; + S.slice_newton_before_endgame = num; }, _val, _1 )] ^ slice_during_[phx::bind( [this](algorithm::RegenerationConfig & S, T num) { - S.newton_during_endgame = num; + S.slice_newton_during_endgame = num; }, _val, _1 )] ^ slice_final_[phx::bind( [this](algorithm::RegenerationConfig & S, T num) { - S.final_tolerance = num; + S.slice_final_tolerance = num; }, _val, _1 )] ) >> -no_setting_) | no_setting_; @@ -702,18 +664,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::Regeneration")) + ); @@ -760,6 +714,7 @@ namespace bertini { std::string real_thresh_name = "imagthreshold"; std::string endpoint_finite_name = "endpointfinitethreshold"; std::string same_point_name = "endpointsamethreshold"; + std::string cond_num_name = "condnumthreshold"; @@ -775,14 +730,19 @@ namespace bertini { }, _val, _1 )] ^ same_point_[phx::bind( [this](algorithm::PostProcessingConfig & S, T num) { - S.same_point_tolerance = num; + S.same_point_tolerance_multiplier = num; + }, _val, _1 )] + ^ cond_num_[phx::bind( [this](algorithm::PostProcessingConfig & S, T num) + { + S.condition_number_threshold = num; }, _val, _1 )] ) >> -no_setting_) | no_setting_; - + all_names_ = (no_case[real_thresh_name] >> ':') | (no_case[endpoint_finite_name] >> ':') | - (no_case[same_point_name] >> ':') + (no_case[same_point_name] >> ':') | + (no_case[cond_num_name] >> ':') ; @@ -803,6 +763,10 @@ namespace bertini { same_point_ = *(char_ - all_names_) >> (no_case[same_point_name] >> ':') >> mpfr_rules.number_string_[phx::bind( str_to_T, _val, _1 )] >> ';'; + cond_num_.name("cond_num_"); + cond_num_ = *(char_ - all_names_) >> (no_case[cond_num_name] >> ':') + >> mpfr_rules.number_string_[phx::bind( str_to_T, _val, _1 )] >> ';'; + no_setting_.name("no_setting_"); @@ -814,18 +778,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::PostProcessing")) + ); @@ -835,13 +791,13 @@ namespace bertini { private: qi::rule root_rule_; - qi::rule real_threshold_, endpoint_finite_, same_point_; + qi::rule real_threshold_, endpoint_finite_, same_point_, cond_num_; qi::rule no_decl_, no_setting_, all_names_; rules::LongNum mpfr_rules; - - - + + + }; //re: PostProcessing @@ -899,18 +855,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::classic::AlgoChoice")) + ); @@ -928,7 +876,62 @@ namespace bertini { + template + struct ConfigSettingParser : qi::grammar + { + ConfigSettingParser() : ConfigSettingParser::base_type(root_rule_, "config::classic::EndgameChoiceConfig") + { + namespace phx = boost::phoenix; + using qi::_1; + using qi::_2; + using qi::_3; + using qi::_4; + using qi::_val; + using qi::char_; + using boost::spirit::ascii::no_case; + + // Bertini1: endgamenum: 1=PSEG, 2=Cauchy (default) + endgamechoice_.add("1", algorithm::classic::EndgameChoice::PowerSeries); + endgamechoice_.add("2", algorithm::classic::EndgameChoice::Cauchy); + + std::string setting_name = "endgamenum"; + + // enum_rule_ parses the setting and returns the EndgameChoice enum value + enum_rule_.name("endgamechoice_"); + enum_rule_ = *(char_ - (no_case[setting_name] >> ':')) >> (no_case[setting_name] >> ':') >> endgamechoice_[_val = _1] >> ';'; + + root_rule_.name("config::classic::EndgameChoiceConfig"); + + root_rule_ = enum_rule_[phx::bind([](algorithm::classic::EndgameChoiceConfig& cfg, + algorithm::classic::EndgameChoice c){ + cfg.endgame = c; + }, _val, _1)] + >> -no_setting_ + | no_setting_; // no endgamenum → default (Cauchy) + + no_setting_.name("no_setting_"); + no_setting_ = *(char_ - no_case[setting_name]); + + no_decl_.name("no_decl_"); + no_decl_ = *(char_); + + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::classic::EndgameChoiceConfig")) + ); + } + + private: + qi::rule root_rule_; + qi::rule enum_rule_; + qi::rule no_decl_, no_setting_; + + qi::symbols endgamechoice_; + + }; //re: EndgameChoiceConfig + + } // re: namespace classic - + }// re: namespace parsing }// re: namespace bertini \ No newline at end of file diff --git a/core/include/bertini2/io/parsing/settings_parsers/endgames.hpp b/core/include/bertini2/io/parsing/settings_parsers/endgames.hpp index 21d594a95..00856d0f0 100644 --- a/core/include/bertini2/io/parsing/settings_parsers/endgames.hpp +++ b/core/include/bertini2/io/parsing/settings_parsers/endgames.hpp @@ -103,18 +103,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::Security")) + ); @@ -143,7 +135,8 @@ namespace bertini { struct ConfigSettingParser : qi::grammar { using T = double; - using R = mpq_rational; + using R = mpq_rational; // exact decimal-to-rational, no floating-point precision to go stale + ConfigSettingParser() : ConfigSettingParser::base_type(root_rule_, "EndgameConfig") { @@ -160,16 +153,16 @@ namespace bertini { using boost::spirit::lexeme; using boost::spirit::as_string; using boost::spirit::ascii::no_case; - - - + + + std::string samplefactor_name = "samplefactor"; std::string numpoints_name = "numsamplepoints"; std::string mintrack_name = "nbhdradius"; - - + + root_rule_.name("config::Endgame"); - + root_rule_ = ((sample_factor_[phx::bind( [this](bertini::endgame::EndgameConfig & S, R num) { S.sample_factor = num; @@ -182,18 +175,18 @@ namespace bertini { { S.num_sample_points = num; }, _val, _1 )]) - + >> -no_setting_) | no_setting_; - - + + all_names_ = (no_case[samplefactor_name] >> ':') | (no_case[numpoints_name] >> ':')| (no_case[mintrack_name] >> ':'); - + sample_factor_.name("sample_factor_"); sample_factor_ = *(char_ - all_names_) >> (no_case[samplefactor_name] >> ':') - >> mpfr_rules.rational[phx::bind( [this](R & num, std::string str) + >> mpfr_rules.rational[phx::bind( [](R & num, std::string const& str) { - num = bertini::NumTraits::FromString(str); + num = bertini::NumTraits::FromString(str); }, _val, _1 )] >> ';'; min_track_.name("min_track_"); @@ -217,18 +210,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::Endgame")) + ); @@ -304,18 +289,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("bertini::endgame::PowerSeriesConfig")) + ); @@ -399,18 +376,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::Cauchy")) + ); @@ -498,18 +467,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("bertini::endgame::TrackBackConfig")) + ); diff --git a/core/include/bertini2/io/parsing/settings_parsers/random.hpp b/core/include/bertini2/io/parsing/settings_parsers/random.hpp new file mode 100644 index 000000000..7fe2a53a8 --- /dev/null +++ b/core/include/bertini2/io/parsing/settings_parsers/random.hpp @@ -0,0 +1,78 @@ +//This file is part of Bertini 2. +// +//bertini2/io/parsing/settings_parsers/random.hpp is free software: you can redistribute it +//and/or modify it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//bertini2/io/parsing/settings_parsers/random.hpp is distributed in the hope that it will be +//useful, but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with bertini2/io/parsing/settings_parsers/random.hpp. If not, see +//. +// +// Copyright(C) Bertini2 Development Team + +/** + \file bertini2/io/parsing/settings_parsers/random.hpp + + \brief Parser specialisation for RandomConfig — handles `randomseed: ;`. + */ + +#pragma once + +#include "bertini2/io/parsing/settings_parsers/base.hpp" +#include "bertini2/nag_algorithms/common/config.hpp" + +namespace bertini { + namespace parsing { + namespace classic { + + template + struct ConfigSettingParser + : qi::grammar + { + ConfigSettingParser() : ConfigSettingParser::base_type(root_rule_, "config::Random") + { + namespace phx = boost::phoenix; + using qi::_1; + using qi::_val; + using qi::lit; + using qi::char_; + using qi::ulong_; + using boost::spirit::ascii::no_case; + + std::string seed_name = "randomseed"; + + root_rule_.name("config::Random"); + root_rule_ = (random_seed_[phx::bind( + [this](algorithm::RandomConfig& S, unsigned long v) { + S.random_seed = v; + }, _val, _1)] + >> -no_setting_) + | no_setting_; + + all_names_ = no_case[seed_name] >> ':'; + + random_seed_.name("random_seed_"); + random_seed_ = *(char_ - all_names_) + >> (no_case[seed_name] >> ':') + >> ulong_[_val = _1] + >> ';'; + + no_setting_.name("no_setting_"); + no_setting_ = *(char_ - all_names_); + } + + private: + qi::rule root_rule_; + qi::rule random_seed_; + qi::rule no_setting_, all_names_; + }; + + } // namespace classic + } // namespace parsing +} // namespace bertini diff --git a/core/include/bertini2/io/parsing/settings_parsers/tracking.hpp b/core/include/bertini2/io/parsing/settings_parsers/tracking.hpp index ae5e7615b..96120810a 100644 --- a/core/include/bertini2/io/parsing/settings_parsers/tracking.hpp +++ b/core/include/bertini2/io/parsing/settings_parsers/tracking.hpp @@ -85,7 +85,7 @@ namespace bertini { using boost::spirit::ascii::no_case; precisiontype_.add("0", PrecisionType::Fixed); - precisiontype_.add("1", PrecisionType::Adaptive); + precisiontype_.add("1", PrecisionType::FixedMultiple); precisiontype_.add("2", PrecisionType::Adaptive); @@ -107,18 +107,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::PrecisionType")) + ); @@ -195,18 +187,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("Predictor")) + ); @@ -247,7 +231,7 @@ namespace bertini { { private: - using T = double; + using T = mpq_rational; // exact rational: no MPFR precision state to go stale using R = mpq_rational; public: @@ -310,23 +294,23 @@ namespace bertini { max_step_size_.name("max_step_size_"); max_step_size_ = *(char_ - all_names_) >> (no_case[maxstep_name] >> ':') - >> mpfr_rules.rational[phx::bind( [this](T & num, std::string const& str) + >> mpfr_rules.rational[phx::bind( [](T & num, std::string const& str) { - num = bertini::NumTraits::FromString(str); + num = bertini::NumTraits::FromString(str); }, _val, _1 )] >> ';'; - + stepsize_success_.name("stepsize_success_"); stepsize_success_ = *(char_ - all_names_) >> (no_case[stepsuccess_name] >> ':') - >> mpfr_rules.rational[phx::bind( [this](R & num, std::string const& str) + >> mpfr_rules.rational[phx::bind( [](R & num, std::string const& str) { - num = bertini::NumTraits::FromString(str); + num = bertini::NumTraits::FromString(str); }, _val, _1 )] >> ';'; - + stepsize_fail_.name("stepsize_fail_"); stepsize_fail_ = *(char_ - all_names_) >> (no_case[stepfail_name] >> ':') - >> mpfr_rules.rational[phx::bind( [this](R & num, std::string const& str) + >> mpfr_rules.rational[phx::bind( [](R & num, std::string const& str) { - num = bertini::NumTraits::FromString(str); + num = bertini::NumTraits::FromString(str); }, _val, _1 )] >> ';'; steps_increase_.name("steps_increase_"); @@ -345,18 +329,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("SteppingConfig")) + ); @@ -434,18 +410,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("NewtonConfig")) + ); @@ -492,18 +460,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::FixedPrecision")) + ); } @@ -550,7 +510,6 @@ namespace bertini { std::string safety_one_name = "ampsafetydigits1"; std::string safety_two_name = "ampsafetydigits2"; std::string max_prec_name = "ampmaxprec"; - std::string consec_steps_prec_dec_name = "maxstepsprecisiondecrease"; std::string max_num_prec_decs_name = "maxnumprecdecreases"; root_rule_.name("config::AMP"); @@ -587,10 +546,6 @@ namespace bertini { { S.maximum_precision = num; }, _val, _1 )] - ^ consec_steps_prec_dec_[phx::bind( [this](AdaptiveMultiplePrecisionConfig & S, unsigned num) - { - S.consecutive_successful_steps_before_precision_decrease = num; - }, _val, _1 )] ^ max_num_prec_decs_[phx::bind( [this](AdaptiveMultiplePrecisionConfig & S, unsigned num) { S.max_num_precision_decreases = num; @@ -606,7 +561,6 @@ namespace bertini { (no_case[safety_one_name] >> ':') | (no_case[safety_two_name] >> ':') | (no_case[max_prec_name] >> ':') | - (no_case[consec_steps_prec_dec_name] >> ':') | (no_case[max_num_prec_decs_name] >> ':') ; @@ -647,9 +601,6 @@ namespace bertini { max_prec_.name("max_prec_"); max_prec_ = *(char_ - all_names_) >> (no_case[max_prec_name] >> ':') >> qi::uint_[_val=_1] >> ';'; - consec_steps_prec_dec_.name("consec_steps_prec_dec_"); - consec_steps_prec_dec_ = *(char_ - all_names_) >> (no_case[consec_steps_prec_dec_name] >> ':') >> qi::uint_[_val=_1] >> ';'; - max_num_prec_decs_.name("max_num_prec_decs_"); max_num_prec_decs_ = *(char_ - all_names_) >> (no_case[max_num_prec_decs_name] >> ':') >> qi::uint_[_val=_1] >> ';'; @@ -663,18 +614,10 @@ namespace bertini { - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("config parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("config::AMP")) + ); @@ -686,7 +629,7 @@ namespace bertini { qi::rule degree_bound_, coefficient_bound_, lin_solve_error_bnd_, jac_eval_err_bnd_, func_eval_err_bnd_; qi::rule safety_one_, safety_two_; - qi::rule max_prec_, consec_steps_prec_dec_, max_num_prec_decs_; + qi::rule max_prec_, max_num_prec_decs_; qi::rule no_decl_, no_setting_, all_names_; rules::LongNum mpfr_rules; diff --git a/core/include/bertini2/io/parsing/system_parsers.hpp b/core/include/bertini2/io/parsing/system_parsers.hpp index 4ae082d35..676571e03 100644 --- a/core/include/bertini2/io/parsing/system_parsers.hpp +++ b/core/include/bertini2/io/parsing/system_parsers.hpp @@ -61,7 +61,9 @@ namespace bertini { if (!r || first != last) // fail if we did not get a full match return false; - + + // every definition has now filled its function box: emit the bare entries. + S.EmitDeclaredFunctions(s); sys = s; return r; } @@ -84,9 +86,19 @@ namespace bertini { if (!s || iter!=end) { - throw std::runtime_error("unable to correctly parse string in construction of system"); + std::string remaining(iter, end); + if (remaining.size() > 60) + remaining = remaining.substr(0, 60) + "..."; + if (remaining.empty()) + remaining = ""; + throw std::runtime_error( + "[SystemParser] parser did not consume entire input; " + "unparsed remainder: \"" + remaining + "\""); } + // every definition has now filled its function box: emit the bare entries. + S.EmitDeclaredFunctions(sys); + using std::swap; swap(sys,*this); } diff --git a/core/include/bertini2/io/parsing/system_rules.hpp b/core/include/bertini2/io/parsing/system_rules.hpp index b36717519..44ae364e9 100644 --- a/core/include/bertini2/io/parsing/system_rules.hpp +++ b/core/include/bertini2/io/parsing/system_rules.hpp @@ -31,6 +31,8 @@ +#include + #include "bertini2/io/parsing/qi_files.hpp" #include "bertini2/system/system.hpp" #include "bertini2/io/parsing/function_rules.hpp" @@ -41,12 +43,10 @@ namespace bertini { namespace parsing { namespace classic { // a few local using statements to reduce typing etc. - using Function = node::Function; using Variable = node::Variable; using Node = node::Node; - - - using Fn = std::shared_ptr; + + using Var = std::shared_ptr; using Nd = std::shared_ptr; @@ -79,9 +79,9 @@ namespace bertini { { - SystemParser() : function_parser_(&encountered_symbols_), - /*initialize here with address of encountered_symbols*/ - SystemParser::base_type(root_rule_) + SystemParser() : SystemParser::base_type(root_rule_), + /*initialize function_parser_ here with address of encountered_symbols*/ + function_parser_(&encountered_symbols_) { namespace phx = boost::phoenix; using qi::_1; @@ -146,19 +146,23 @@ namespace bertini { | variables_ [phx::bind(&System::AddUngroupedVariables, _val, _1)] | - functions_ [phx::bind(&System::AddFunctions, _val, _1)] + functions_ [phx::bind([this](std::vector const& names){ this->CollectFunctionNames(names); }, _1)] | - constants_ [phx::bind(&System::AddConstants, _val, _1)] + constants_ [phx::bind([this](std::vector const& names){ this->CollectConstantNames(names); }, _1)] | - parameters_ [phx::bind(&System::AddParameters, _val, _1)] + parameters_ [phx::bind([this](std::vector const& names){ this->CollectParameterNames(names); }, _1)] | implicit_parameters_ [phx::bind(&System::AddImplicitParameters, _val, _1)] | path_variable_ [phx::bind(&System::AddPathVariable, _val, _1)] | - subfunction_ [phx::bind(&System::AddSubfunction, _val, _1)] - | + // definition_ before subfunction_: a declared name (function/constant/ + // parameter) lives only in encountered_functions_, not encountered_symbols_, + // so its definition line would otherwise be misread as a fresh subfunction. + // definition_ matches declared LHS names; subfunction_ catches the rest. definition_ + | + qi::omit[subfunction_] ) ; @@ -204,10 +208,10 @@ namespace bertini { new_function_.name("new_function_"); - new_function_ = unencountered_symbol_ [boost::phoenix::bind( [this](Fn & F, std::string str) + new_function_ = unencountered_symbol_ [boost::phoenix::bind( [this](std::string & name, std::string str) { - MakeAndAddFunction(F,str); - }, _val, _1 )];//[_val = make_shared_() (_1)] + MakeAndDeclareName(name,str); + }, _val, _1 )]; @@ -228,21 +232,22 @@ namespace bertini { definition_.name("definition_"); - definition_ = (encountered_functions_ > '=' > function_parser_ > ';') [phx::bind( [](const Fn & F, const Nd & N) + definition_ = (encountered_functions_ > '=' > function_parser_ > ';') [phx::bind( [this](std::string const& name, const Nd & expr) { - F->SetRoot(N); + this->DefineNamed(name, expr); },_1, _2)] ; using qi::_a; using qi::omit; subfunction_.name("subfunction"); - subfunction_ = new_function_ [_a = _1] > '=' > - function_parser_ [_val = _a, phx::bind( [](Fn & F, const Nd & N) - { - F->SetRoot(N); - },_a, _1)] - // omit close + subfunction_ = unencountered_symbol_ [_a = _1] > '=' > + function_parser_ [phx::bind( [this](Nd & result, std::string const& name, const Nd & expr) + { + auto ne = node::NamedExpression::Make(expr, name); + encountered_symbols_.add(name, ne); + result = ne; + }, _val, _a, _1)] > ';'; @@ -284,18 +289,10 @@ namespace bertini { // BOOST_SPIRIT_DEBUG_NODES( (unencountered_symbol_) (new_variable_) (genericvargp_)) - using phx::val; - using phx::construct; - using namespace qi::labels; - qi::on_error - ( root_rule_ , - std::cout<< - val("System parser could not complete parsing. Expecting ")<< - _4<< - val(" here: ")<< - construct(_3,_2)<< - std::endl - ); + qi::on_error( + root_rule_, + phx::bind(&ReportParseError, _1, _2, _3, _4, std::string("SystemParser")) + ); } @@ -318,11 +315,11 @@ namespace bertini { - qi::rule(), Skipper > functions_, constants_, parameters_; - qi::rule(), Skipper > genericfuncgp_; - qi::rule > subfunction_; - - qi::rule new_function_; + qi::rule(), Skipper > functions_, constants_, parameters_; + qi::rule(), Skipper > genericfuncgp_; + qi::rule > subfunction_; + + qi::rule new_function_; qi::rule unencountered_symbol_; @@ -336,7 +333,7 @@ namespace bertini { // symbol declarations qi::symbols encountered_symbols_; qi::symbols declarative_symbols_; - qi::symbols encountered_functions_; + qi::symbols encountered_functions_; qi::symbols special_numbers_; FunctionParser function_parser_; @@ -345,11 +342,16 @@ namespace bertini { To accompany the rule for making new functions when you encounter a new symbol. Simultaneously makes a new function, and adds it to the set of symbols. */ - void MakeAndAddFunction(Fn & F, std::string str) + void MakeAndDeclareName(std::string & name, std::string str) { - F = Function::Make(str); - encountered_symbols_.add(str, F); - encountered_functions_.add(str,F); + // A declare-then-define name (function / constant / parameter) is just a + // marker until its definition is parsed: record the name in + // encountered_functions_ so the definition_ rule can match its LHS. We do + // NOT add it to encountered_symbols_ yet -- the name binds to its + // NamedExpression only once defined (DefineNamed), at which point references + // to it resolve. (Define-before-use is assumed.) + name = str; + encountered_functions_.add(str, str); } /** @@ -361,12 +363,43 @@ namespace bertini { V = Variable::Make(str); encountered_symbols_.add(str, V); } - - - void SetRootNode(Fn & F, const Nd & N) + + // Bertini-1 input declares the declare-then-define kinds first ("function f, g;", + // "constant c;", "parameter p;"), then defines them (and any inline subfunctions) + // later. We record the declared names per kind, and bind each name to an immutable + // NamedExpression when its definition is parsed (DefineNamed). After the whole input + // is parsed we emit them to the System (EmitDeclaredFunctions): the System receives + // bare, fully-built expressions -- no Function declaration box is ever created. + public: + void CollectFunctionNames(std::vector const& names) + { for (auto const& n : names) declared_function_names_.push_back(n); } + void CollectConstantNames(std::vector const& names) + { for (auto const& n : names) declared_constant_names_.push_back(n); } + void CollectParameterNames(std::vector const& names) + { for (auto const& n : names) declared_parameter_names_.push_back(n); } + + // Bind a declared name to its (immutable) NamedExpression: makes references to the + // name resolve, and records the definition for emission to the System. + void DefineNamed(std::string const& name, Nd const& expr) { - F->SetRoot(N); + auto ne = node::NamedExpression::Make(expr, name); + encountered_symbols_.add(name, ne); + definitions_[name] = ne; } + + void EmitDeclaredFunctions(System& s) const + { + for (auto const& n : declared_function_names_) + s.AddFunction(definitions_.at(n)->EntryNode()); + for (auto const& n : declared_constant_names_) + s.AddConstant(definitions_.at(n)); + for (auto const& n : declared_parameter_names_) + s.AddParameter(definitions_.at(n)); + } + + private: + std::vector declared_function_names_, declared_constant_names_, declared_parameter_names_; + std::map> definitions_; }; } // re: namespace classic diff --git a/core/include/bertini2/io/splash.hpp b/core/include/bertini2/io/splash.hpp index 71a4a67f1..3dad5f17d 100644 --- a/core/include/bertini2/io/splash.hpp +++ b/core/include/bertini2/io/splash.hpp @@ -38,6 +38,10 @@ #include "boost/version.hpp" +#ifdef BERTINI2_HAVE_MPI +#include "bertini2/parallel/mpi_include.hpp" +#endif + #include #define BERTINI2_PACKAGE_URL "https://github.com/bertiniteam/b2" @@ -165,7 +169,20 @@ std::string MPFRVersion() return mpfr_get_version(); } -inline +inline +std::string MPIVersion() +{ +#ifdef BERTINI2_HAVE_MPI + char buf[MPI_MAX_LIBRARY_VERSION_STRING]; + int len = 0; + MPI_Get_library_version(buf, &len); + return std::string(buf, len); +#else + return "not available (serial build)"; +#endif +} + +inline std::string BoostHeaderVersion() { std::stringstream ss; @@ -184,7 +201,8 @@ std::string DependencyVersions() ss << "Compiled against Boost headers " << BoostHeaderVersion() << "\n"; ss << "Compiled against Eigen " << EigenHeaderVersion() << "\n"; ss << "Linked against GMP " << GMPVersion() << "\n"; - ss << "Linked against MPFR " << MPFRVersion() << "\n\n"; + ss << "Linked against MPFR " << MPFRVersion() << "\n"; + ss << "MPI: " << MPIVersion() << "\n\n"; return ss.str(); } diff --git a/core/include/bertini2/mpfr_complex.hpp b/core/include/bertini2/mpfr_complex.hpp index 25b7f41a8..d47481e0f 100644 --- a/core/include/bertini2/mpfr_complex.hpp +++ b/core/include/bertini2/mpfr_complex.hpp @@ -64,11 +64,12 @@ using bmp::backends::mpc_complex_backend; #endif inline auto DefaultPrecisionPolicy(){ - return bmp::variable_precision_options::preserve_source_precision; + return bmp::variable_precision_options::preserve_related_precision; } + // shamelessly adapted from the documentation for variable precision in Boost.Multiprecision. // see https://www.boost.org/doc/libs/1_82_0/libs/multiprecision/doc/html/boost_multiprecision/tut/variable.html struct scoped_mpfr_precision_options_this_thread @@ -144,8 +145,37 @@ using bmp::backends::mpc_complex_backend; // with the static default. See commit 3111255b for original context. mpfr_float::thread_default_precision(prec); mpfr_complex::thread_default_precision(prec); +#ifdef BMP_EXPRESSION_TEMPLATES + // With ET on, the default global policy for mpc_complex_backend is preserve_related_precision + // and for mpfr_float_backend it is preserve_target_precision. Both differ from what we want. + // Setting preserve_related_precision per-thread mirrors the mpc_complex_backend global default + // and ensures copy/construction semantics match the et_off behavior. Specifically: + // - mpc_complex copy ctor uses preserve_related_precision() (>= 3) to preserve source precision + // - assign_components_set_precision uses preserve_component_precision() (>= 2) to resize + // a complex from real components at higher-than-default precision + // - mpc_complex = mpfr_float uses preserve_component_precision() (>= 2) to resize + // preserve_related_precision satisfies all three thresholds. + // For mpfr_float, preserve_related_precision also enables source-precision-preserving copies. + mpfr_float::thread_default_variable_precision_options( + bmp::variable_precision_options::preserve_related_precision); + mpfr_complex::thread_default_variable_precision_options( + bmp::variable_precision_options::preserve_related_precision); +#endif + } + + // Sets thread-local precision only — does NOT write the global default_precision. + // Safe to call concurrently from multiple std::thread workers tracking at different + // precisions. Use instead of DefaultPrecision() inside per-thread tracking loops. + inline void SetThreadPrecision(unsigned prec) + { + mpfr_float::thread_default_precision(prec); + mpfr_complex::thread_default_precision(prec); } + inline unsigned ThreadPrecision() + { + return static_cast(mpfr_float::thread_default_precision()); + } } diff --git a/core/include/bertini2/mpfr_extensions.hpp b/core/include/bertini2/mpfr_extensions.hpp index 7a73dad5e..48a702ff6 100644 --- a/core/include/bertini2/mpfr_extensions.hpp +++ b/core/include/bertini2/mpfr_extensions.hpp @@ -82,6 +82,7 @@ namespace bertini{ { num.precision(prec); } + } diff --git a/core/include/bertini2/nag_algorithms/common/config.hpp b/core/include/bertini2/nag_algorithms/common/config.hpp index 499770046..202a95953 100644 --- a/core/include/bertini2/nag_algorithms/common/config.hpp +++ b/core/include/bertini2/nag_algorithms/common/config.hpp @@ -29,6 +29,8 @@ #include "bertini2/system/start_systems.hpp" #include "bertini2/nag_algorithms/common/policies.hpp" +#include + namespace bertini{ namespace algorithm{ @@ -37,6 +39,17 @@ using SolnCont = std::vector; namespace classic{ +enum class EndgameChoice +{ + PowerSeries = 1, + Cauchy = 2 +}; + +struct EndgameChoiceConfig +{ + EndgameChoice endgame = EndgameChoice::Cauchy; +}; + enum class AlgoChoice { EvalFunctions = -4, @@ -110,9 +123,14 @@ struct RegenerationConfig bool higher_dimension_check = true; ///< RegenHigherDimTest unsigned start_level = 0; - T newton_before_endgame; ///< The tolerance for tracking before reaching the endgame. SliceTolBeforeEG - T newton_during_endgame; ///< The tolerance for tracking during the endgame. SliceTolDuringEG - T final_tolerance; ///< The final tolerance to track to, using the endgame. SliceFinalTol + // These are the SLICE-moving tracking tolerances (Bertini 1's SliceTol* family) -- the tolerances + // for moving the linear slices during regeneration, kept separate from the main tracking + // tolerances in TolerancesConfig. The slice_ prefix makes every config field name unique across + // structs, which is what lets a field be set on an owner without naming its struct + // (owner.update(field=...) routes by field). + T slice_newton_before_endgame; ///< Slice-moving tracking tolerance before the endgame. SliceTolBeforeEG + T slice_newton_during_endgame; ///< Slice-moving tracking tolerance during the endgame. SliceTolDuringEG + T slice_final_tolerance; ///< Final tolerance to track the slice move to, using the endgame. SliceFinalTol }; @@ -120,22 +138,52 @@ struct RegenerationConfig struct PostProcessingConfig{ using T = NumErrorT; - T real_threshold = T(1)/T(100000000); ///< threshold on the imaginary part of a solution being 0. If the imag part exceeds this, the point is considered complex. Currently, this is the implemented available way in Bertini2 for determining this, but there are other methods. Smale's alpha theory provides ways to prove that a point is real. If this is something you need, please consider adding the method to the library, for all to use! Or, if this is technically beyond your C++ capabilities, add as an issue on the github page, and indicate it as a feature request. + T real_threshold = T(1)/T(100000000); ///< Bertini 1's `ImagThreshold`. Threshold on the imaginary part of a (dehomogenized) solution coordinate being 0: a point is real if the infinity norm of the imaginary parts is below this. If the imag part exceeds this, the point is considered complex. Currently, this is the implemented available way in Bertini2 for determining this, but there are other methods. Smale's alpha theory provides ways to prove that a point is real. If this is something you need, please consider adding the method to the library, for all to use! Or, if this is technically beyond your C++ capabilities, add as an issue on the github page, and indicate it as a feature request. B1 default 1e-8. - T endpoint_finite_threshold = T(1)/T(100000); ///< The threshold on norm of endpoints being considered infinite. There is another setting in Tolerances, `path_truncation_threshold`, which tells the path tracker to die if exceeded. Another related setting is in Security, `max_norm` -- the endgame dies if the norm of the computed approximation exceeds this twice. + T endpoint_finite_threshold = T(100000); ///< Bertini 1's `EndpointFiniteThreshold`. An endpoint is considered to be at infinity if the infinity norm of its *dehomogenized* coordinates is larger than this value. This uses the same dehomogenize-then-infinity-norm computation the endgame uses for its `Security::max_norm` divergence check (a separate, smaller threshold for bailing out *during* the endgame). There is also `path_truncation_threshold` in Tolerances, which tells the path tracker to die if exceeded. B1 default 1e5. - T same_point_tolerance {T(1)/T(10000000000)}; ///< The tolerance for whether two points are the same. This should be *lower* than the accuracy to which you request your solutions be computed. Perhaps by at least two orders of magnitude, but the default value is a factor of 10 less stringent. This also depends on the norm being used to tell whether two points are the same, and the norm used for the convergence condition to terminate tracking. + T same_point_tolerance_multiplier {T(10)}; ///< Bertini 1's `EndpointSameThreshold`. A multiplier (>= 1) on `final_tolerance`: two endpoints are considered the same point if the infinity norm of the difference of their *dehomogenized* coordinates is below `final_tolerance * same_point_tolerance_multiplier`. Keeping it a multiplier (rather than an absolute tolerance) means the same-point test always stays a fixed factor looser than the accuracy you tracked to, even if `final_tolerance` is changed. B1 default 10. + + T condition_number_threshold {T(100000000)}; ///< Bertini 1's `CondNumThreshold`. An endpoint is considered singular if it is the endpoint of multiple paths (multiplicity > 1), or if the approximation of the condition number (in the spectral norm, as estimated by the tracker) is larger than this value. B1 default 1e8. }; +/** +\brief The default ambient precision for a ZeroDimConfig, by complex type. + +Double-precision solves work at double; multiprecision solves work at the current default precision +-- which is the precision the MultiplePrecisionTracker is built at, so that in a fixed-multiple solve +the start points, the tracker, and the working ("thread") precision are all one and the same value. +(Defaulting to DoublePrecision() here left fixed-multiple solves with the tracker at DefaultPrecision() +but the ambient/thread precision at double -- a mismatch the tracker rejects at start.) +*/ template +inline unsigned DefaultInitialAmbientPrecision() +{ + if constexpr (std::is_same::value) + return DoublePrecision(); + else + return DefaultPrecision(); +} + +// Not templated on the complex type: the three times are stored as exact, precision-free +// mpq_rational (real) and converted to the tracking complex type at the current working precision at +// use -- the same pattern SteppingConfig uses for its step sizes. This keeps the config precision- +// agnostic (one struct, not a DoublePrec/Multiprec pair), and is in fact more correct under adaptive +// precision: a value like the endgame boundary 1/10 is materialized at full working precision rather +// than frozen at whatever precision the config happened to be constructed at. +// +// The times are real because a zero-dim solve tracks along the real t-axis (1 -> 0). Complex +// homotopy times remain available at the endgame level (set directly), just not through this config. struct ZeroDimConfig { - unsigned initial_ambient_precision = DoublePrecision(); + // Per-complex-type default; the ZeroDim algorithm overwrites this in DefaultSettingsSetup with + // DefaultInitialAmbientPrecision() (it knows its tracking type, this struct does not). + unsigned initial_ambient_precision = DefaultPrecision(); unsigned max_num_crossed_path_resolve_attempts = 2; ///< The maximum number of times to attempt to re-solve crossed paths at the endgame boundary. - ComplexT start_time = ComplexT(1); - ComplexT endgame_boundary = ComplexT(1)/ComplexT(10); - ComplexT target_time = ComplexT(0); + mpq_rational start_time{1}; ///< Homotopy start time (t=1). + mpq_rational endgame_boundary{1, 10}; ///< Time at which tracking hands off to the endgame (t=1/10). + mpq_rational target_time{0}; ///< Homotopy target time (t=0). std::string path_variable_name = "ZERO_DIM_PATH_VARIABLE"; }; @@ -145,6 +193,17 @@ struct MetaConfig classic::AlgoChoice tracktype = classic::AlgoChoice::ZeroDim; }; +/** +Global RNG seed for reproducible runs. random_seed == 0 (the default) draws from +std::random_device and reports the effective seed so the run can be reproduced. +Set via `randomseed: N;` in the classic Bertini input file or bertini.set_random_seed(N) +in Python. Must be applied before system construction (gamma, patch, TD-constants). +*/ +struct RandomConfig +{ + unsigned long random_seed = 0; +}; + // a forward declare template diff --git a/core/include/bertini2/nag_algorithms/common/policies.hpp b/core/include/bertini2/nag_algorithms/common/policies.hpp index f07e75747..865b63887 100644 --- a/core/include/bertini2/nag_algorithms/common/policies.hpp +++ b/core/include/bertini2/nag_algorithms/common/policies.hpp @@ -145,7 +145,12 @@ namespace bertini { using SystemT = SystemType; using StartSystemT = StartSystemType; - + + // This policy owns (deep-copies) its systems, so in a distributed solve rank 0's + // systems can be broadcast and installed authoritatively on every rank. RefToGiven, + // which only holds references to user-managed systems, sets this false. + static constexpr bool OwnsSystems = true; + private: StoredSystemT target_system_; StoredStartSystemT start_system_; @@ -197,10 +202,12 @@ namespace bertini { static void FormHomotopy(SystemType & homotopy, SystemType const& target, StartSystemType const& start, std::string const& path_variable_name) { - auto t = node::Variable::Make(path_variable_name); - - homotopy = (1-t)*target + node::Rational::Make(node::Rational::Rand())*t*start; - homotopy.AddPathVariable(t); + // MakeHomotopy builds H = (1-t)*target + gamma*t*start with a random gamma, + // choosing a blend block when the start system carries structured blocks (e.g. + // the MHom products-of-linears start) and node arithmetic otherwise. The same + // construction is exposed to Python as system.make_homotopy so a user-authored + // start system can be turned into a trackable homotopy. + homotopy = MakeHomotopy(target, start, path_variable_name); } /** @@ -274,6 +281,10 @@ namespace bertini { using SMP::StartSystem; using SMP::Homotopy; + // The user owns these systems (we only hold references); a distributed solve must not + // overwrite them, so it does not broadcast/install rank 0's systems here. See CloneGiven. + static constexpr bool OwnsSystems = false; + private: StoredSystemT target_system_; ///< The target system which we track to. @@ -312,9 +323,135 @@ namespace bertini { return std::ref(sys); } - void SystemSetup(std::string const& path_variable_name) const + void SystemSetup(std::string const& /*path_variable_name*/) const { } }; + + + + + /** + \brief A base class for system management for algorithms which operate on a single system. + + In contrast to SysMgmtPolicy, some algorithms -- notably the NumericalIrreducibleDecomposition algorithm -- do not form a homotopy from a start system, and so only need to manage a single target system. This base provides just the target-system get/set. + + \see SysMgmtPolicy + */ + template< typename D, typename SystemType, typename StoredSystemType> + struct SingleSysMgmtPolicy + { + using SystemT = SystemType; + using StoredSystemT = StoredSystemType; + +private: + const D& AsDerived() const + { + return static_cast(*this); + } + + D& AsDerived() + { + return static_cast(*this); + } + +public: + /** + A getter for the system to be operated on. + */ + const SystemT& TargetSystem() const + { + return AsDerived().target_system_; + } + + /** + A setter for the system to be operated on. + */ + void TargetSystem(StoredSystemT const& sys) + { + AsDerived().target_system_ = sys; + } + }; + + + + + /** + \brief A single-system management policy which clones the supplied target. + + The single-system analog of CloneGiven: makes a clone of the supplied system at construct time, so the algorithm may homogenize/patch it without disturbing the user's original. No start system or homotopy is formed. + + Reusable by any algorithm which operates on a single system with no homotopy-from-start-system (e.g. NumericalIrreducibleDecomposition). + + \see CloneGiven, SingleSysMgmtPolicy + */ + template + struct CloneTarget : public SingleSysMgmtPolicy, SystemType, SystemType> + { + using SMP = SingleSysMgmtPolicy, SystemType, SystemType>; + friend SMP; + + using StoredSystemT = typename SMP::StoredSystemT; + + using SMP::TargetSystem; + + using SystemT = SystemType; + +private: + StoredSystemT target_system_; + +public: + /** + Forward the system on to the stored (cloned) target. + */ + CloneTarget(SystemType const& target) : target_system_(AtConstruct(target)) + {} + + + static + StoredSystemT AtConstruct(SystemType const& sys) + { + return Clone(sys); + } + + + template + static + T AtSet(T const& sys) + { + return sys; + } + + + /** + Homogenize and patch the target system. + */ + static + void PrepareTarget(SystemType & target) + { + target.Homogenize(); // work over projective coordinates + target.AutoPatch(); // then patch if needed + } + + + /** + \brief Sets up the (single) system to be operated on. + + Homogenizes and patches the target. No start system or homotopy is formed. + */ + void SystemSetup() + { + PrepareTarget(TargetSystem()); + } + + + /** + A non-const getter for the system to be operated on. + */ + SystemT& TargetSystem() + { + return target_system_; + } + }; } // ns policy } // ns bertini \ No newline at end of file diff --git a/core/include/bertini2/nag_algorithms/events.hpp b/core/include/bertini2/nag_algorithms/events.hpp new file mode 100644 index 000000000..7c649ff60 --- /dev/null +++ b/core/include/bertini2/nag_algorithms/events.hpp @@ -0,0 +1,106 @@ +//This file is part of Bertini 2. +// +//nag_algorithms/events.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//nag_algorithms/events.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with nag_algorithms/events.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file nag_algorithms/events.hpp + +\brief Events emitted by the higher-level algorithms (e.g. ZeroDim) during a solve. + +These are emitted on a non-templated emitter base (e.g. AnyZeroDim) so that a single +observer type can watch every templated instantiation of the algorithm. An observer +recovers the concrete algorithm (and its full interface) from the event's Get(): in +C++ via dynamic_cast, in Python automatically via RTTI to the most-derived registered +type. +*/ + +#pragma once + +#include "bertini2/detail/events.hpp" + +#include + +namespace bertini { + + namespace algorithm { + + /** + \brief Generic event for a numerical algebraic geometry algorithm. + */ + ADD_BERTINI_EVENT_TYPE(AlgorithmEvent, ConstEvent); + + /** + \brief The algorithm has begun (e.g. the whole zero-dim solve is starting). + */ + ADD_BERTINI_EVENT_TYPE(AlgorithmStarted, AlgorithmEvent); + + /** + \brief The algorithm has finished. + */ + ADD_BERTINI_EVENT_TYPE(AlgorithmComplete, AlgorithmEvent); + + + /** + \brief The algorithm is beginning work on one solution path, carrying its index. + + Between this and the matching PathComplete, the algorithm runs everything for + that path -- the main homotopy track and any endgame sub-tracks -- so an + observer can group them together as one complete solution path. + */ + template + class PathStarted : public AlgorithmEvent + { BOOST_TYPE_INDEX_REGISTER_CLASS + public: + using HeldT = typename AlgorithmEvent::HeldT; + PathStarted(HeldT obs, std::size_t path_index) + : AlgorithmEvent(obs), path_index_(path_index) + {} + + std::size_t PathIndex() const { return path_index_; } + + virtual ~PathStarted() = default; + PathStarted() = delete; + private: + std::size_t path_index_; + }; + + /** + \brief The algorithm has finished work on one solution path, carrying its index. + */ + template + class PathComplete : public AlgorithmEvent + { BOOST_TYPE_INDEX_REGISTER_CLASS + public: + using HeldT = typename AlgorithmEvent::HeldT; + PathComplete(HeldT obs, std::size_t path_index) + : AlgorithmEvent(obs), path_index_(path_index) + {} + + std::size_t PathIndex() const { return path_index_; } + + virtual ~PathComplete() = default; + PathComplete() = delete; + private: + std::size_t path_index_; + }; + + } // namespace algorithm + +} // namespace bertini diff --git a/core/include/bertini2/nag_algorithms/midpath_check.hpp b/core/include/bertini2/nag_algorithms/midpath_check.hpp index 050634671..3c65c9324 100644 --- a/core/include/bertini2/nag_algorithms/midpath_check.hpp +++ b/core/include/bertini2/nag_algorithms/midpath_check.hpp @@ -42,7 +42,7 @@ namespace bertini{ namespace algorithm{ - template + template struct MidpathChecker : public detail::Configured { using MidPathConfT = MidPathConfig; @@ -134,21 +134,31 @@ namespace bertini{ template bool Check(BoundaryData const& boundary_data, StartSystemT const& start_system) { + // Each Check is independent: the algorithm calls this repeatedly (once before any + // resolve, then again after each MidpathResolve). Reset the accumulated state so a + // later call reports *this* call's crossings, not the union with prior calls. + // Without this, passed_ (only ever set false below) would stay false forever once + // any crossing was seen, and crossed_paths_ would grow without bound -- the resolve + // loop in EGBoundaryAction would then never see a clean pass and would re-track + // stale indices. + passed_ = true; + crossed_paths_.clear(); + for (PathIndT ii = 0; ii < boundary_data.size(); ++ii) { if ( boundary_data[ii].success_code != SuccessCode::Success) continue; - const Vec& solution_ii = boundary_data[ii].path_point; - const auto start_ii = start_system.template StartPoint(ii); + const Vec& solution_ii = boundary_data[ii].path_point; + const auto start_ii = start_system.template StartPoint(ii); for (PathIndT jj = ii+1; jj < boundary_data.size(); ++jj) { if ( boundary_data[jj].success_code != SuccessCode::Success) continue; - const Vec& solution_jj = boundary_data[jj].path_point; - const Vec diff_sol = solution_ii - solution_jj; + const Vec& solution_jj = boundary_data[jj].path_point; + const Vec diff_sol = solution_ii - solution_jj; if ((diff_sol.template lpNorm()/solution_ii.template lpNorm()) < SamePointTol()) { @@ -156,9 +166,17 @@ namespace bertini{ bool j_already_stored = false; // Check if start points are the same - const auto start_jj = start_system.template StartPoint(jj); + const auto start_jj = start_system.template StartPoint(jj); auto diff_start = start_ii - start_jj; - bool same_start = (diff_start.template lpNorm() > SamePointTol()); + // same_start is true when the two paths *began at the same start point* + // (their start points differ by less than the tolerance). That governs + // whether this coincidence is a genuine path crossing worth re-tracking: + // CrossedPath sets rerun_ = !same_start, so two *distinct* starts that + // have collided (same_start == false) get re-tracked, while two paths + // that legitimately share a start point do not. NOTE: this comparison + // was historically inverted (>), which flipped rerun and meant genuine + // crossings were never re-tracked. + bool same_start = (diff_start.template lpNorm() < SamePointTol()); // Check if path has already been stored in crossed_paths_ diff --git a/core/include/bertini2/nag_algorithms/numerical_irreducible_decomposition.hpp b/core/include/bertini2/nag_algorithms/numerical_irreducible_decomposition.hpp index 0e90b740b..fb8b0ab82 100644 --- a/core/include/bertini2/nag_algorithms/numerical_irreducible_decomposition.hpp +++ b/core/include/bertini2/nag_algorithms/numerical_irreducible_decomposition.hpp @@ -30,27 +30,227 @@ #pragma once +#include "bertini2/num_traits.hpp" + +#include "bertini2/detail/visitable.hpp" +#include "bertini2/tracking.hpp" + +#include "bertini2/detail/configured.hpp" +#include "bertini2/detail/observable.hpp" + +#include "bertini2/nag_algorithms/common/algorithm_base.hpp" +#include "bertini2/nag_algorithms/common/config.hpp" +#include "bertini2/nag_algorithms/common/policies.hpp" + #include "bertini2/nag_datatypes/numerical_irreducible_decomposition.hpp" +#include + namespace bertini { namespace algorithm { - template - struct NumericalIrreducibleDecomposition - { +/** +forward declare of the NumericalIrreducibleDecomposition algorithm. + +Unlike ZeroDim, NID has no start system -- the regenerative cascade is a +fundamentally different way of solving a polynomial system -- so it is managed by +a single-system management policy, and carries no StartSystem template parameter. +*/ +template< typename TrackerType, typename EndgameType, + typename SystemType = System, + template class SystemManagementP = policy::CloneTarget > +struct NumericalIrreducibleDecomposition; + + + +/** +specify the traits for the algorithm. this is why we need the forward declare. + +The NeededConfigs typelist is what drives the (reusable) Python config interface: +the ConfiguredVisitor reflects over exactly these types. +*/ +template< typename TrackerType, typename EndgameType, + typename SystemType, + template class SystemManagementP > +struct AlgoTraits< NumericalIrreducibleDecomposition > +{ + using BaseRealT = typename tracking::TrackerTraits::BaseRealT; + using BaseComplexT = typename tracking::TrackerTraits::BaseComplexT; + + using NeededConfigs = detail::TypeList< + RegenerationConfig, + TolerancesConfig, + SharpeningConfig, + PostProcessingConfig + >; +}; + + + +struct AnyNID : public virtual AnyAlgorithm +{ + virtual ~AnyNID() = default; +}; + + + +/** +\brief The Numerical Irreducible Decomposition algorithm. + +\note This is framework scaffolding. The regenerative cascade itself is not yet +implemented; the compute entry points (Run, Solve, RegenerativeCascade) currently +throw. The class is fully wired for configuration (via detail::Configured and the +NeededConfigs typelist) and observation, and exposes a Tracker and Endgame, so it +slots into the existing Python config interface with no extra plumbing. +*/ +template< typename TrackerType, typename EndgameType, + typename SystemType, + template class SystemManagementP > +struct NumericalIrreducibleDecomposition : + public virtual AnyNID, + public Observable, + public SystemManagementP, + public detail::Configured< + typename AlgoTraits< NumericalIrreducibleDecomposition >::NeededConfigs> +{ + // these usings are for getters in python + using TrackerT = TrackerType; + using EndgameT = EndgameType; + using SystemT = SystemType; + + +/// a bunch of using statements to reduce typing. + using BaseComplexT = typename tracking::TrackerTraits::BaseComplexT; + using BaseRealT = typename tracking::TrackerTraits::BaseRealT; + + using SystemManagementPolicy = SystemManagementP; + + using Config = detail::Configured< + typename AlgoTraits< NumericalIrreducibleDecomposition >::NeededConfigs>; + using Config::Get; + + + using Regeneration = RegenerationConfig; + using Tolerances = TolerancesConfig; + using Sharpening = SharpeningConfig; + using PostProcessing = PostProcessingConfig; + + using ResultT = nag_datatype::NumericalIrreducibleDecomposition; + + using SystemManagementPolicy::TargetSystem; + - static - nag_datatype::NumericalIrreducibleDecomposition RegenerativeCascade() - { +/// constructors + + /** + Construct a NumericalIrreducibleDecomposition algorithm object from the system to be decomposed. + */ + NumericalIrreducibleDecomposition(SystemType const& target) + : SystemManagementPolicy(target), tracker_(TargetSystem()), endgame_(tracker_) + { + DefaultSetup(); + } + + virtual ~NumericalIrreducibleDecomposition() = default; - } +/// the main functions + + /** + \brief Main Run() function provided for calling from the blackbox mode. + */ + void Run() override + { + Solve(); + } + /** + \brief Perform the numerical irreducible decomposition. - - }; + \note Not yet implemented -- this is framework scaffolding. + */ + void Solve() + { + throw std::runtime_error("NumericalIrreducibleDecomposition is not yet implemented"); } -} \ No newline at end of file + + /** + \brief Run the regenerative cascade. + + \note Not yet implemented -- this is framework scaffolding. + */ + ResultT RegenerativeCascade() + { + throw std::runtime_error("NumericalIrreducibleDecomposition::RegenerativeCascade is not yet implemented"); + } + + /** + \brief Get the most recently computed decomposition. + */ + const ResultT& GetDecomposition() const + { + return decomposition_; + } + + +/// tracker / endgame access + + const TrackerType& GetTracker() const + { + return tracker_; + } + + TrackerType& GetTracker() + { + return tracker_; + } + + const EndgameType& GetEndgame() const + { + return endgame_; + } + + EndgameType& GetEndgame() + { + return endgame_; + } + + +/// setup functions + + void DefaultSetup() + { + DefaultSettingsSetup(); + DefaultSystemSetup(); + } + + /** + Fills the configs from default values. + */ + void DefaultSettingsSetup() + { + this->template Set(Regeneration()); + this->template Set(Tolerances()); + this->template Set(Sharpening()); + this->template Set(PostProcessing()); + } + + void DefaultSystemSetup() + { + SystemManagementPolicy::SystemSetup(); + } + + +private: + TrackerType tracker_; + EndgameType endgame_; + ResultT decomposition_; +}; + + + } // ns algorithm + +} // ns bertini \ No newline at end of file diff --git a/core/include/bertini2/nag_algorithms/output.hpp b/core/include/bertini2/nag_algorithms/output.hpp index 246ec6e03..0446fe4e8 100644 --- a/core/include/bertini2/nag_algorithms/output.hpp +++ b/core/include/bertini2/nag_algorithms/output.hpp @@ -75,14 +75,14 @@ struct Classic > NumVariables(out, zd); Variables(out, zd,"\n\n"); - const auto& s = zd.FinalSolutions(); - const auto& m = zd.FinalSolutionMetadata(); + const auto& s = zd.SolutionsInternalCoords(); const auto n = s.size(); for (decltype(s.size()) ii{0}; ii> static void RawData(OutT & out, ZDT const& zd) { - const auto n = zd.FinalSolutions().size(); + const auto n = zd.SolutionsInternalCoords().size(); NumVariables(out, zd,"\n\n"); - for (decltype(zd.FinalSolutions().size()) ii{0}; ii> static void EndPoint(IndexT const& ind, OutT & out, ZDT const& zd, std::string const& additional = "") { - generators::Classic::generate(boost::spirit::ostream_iterator(out), zd.FinalSolutions()[ind]); + generators::Classic::generate(boost::spirit::ostream_iterator(out), zd.SolutionsInternalCoords()[ind]); out << additional; } @@ -167,9 +168,9 @@ struct Classic > static void EndPointDehom(IndexT const& ind, OutT & out, ZDT const& zd, std::string const& additional = "") { - DefaultPrecision(Precision(zd.FinalSolutions()[ind])); + DefaultPrecision(Precision(zd.SolutionsInternalCoords()[ind])); - generators::Classic::generate(boost::spirit::ostream_iterator(out), zd.TargetSystem().DehomogenizePoint(zd.FinalSolutions()[ind])); + generators::Classic::generate(boost::spirit::ostream_iterator(out), zd.SolutionsUserCoords()[ind]); out << additional; } @@ -201,7 +202,7 @@ struct Classic > static void EndPointMDRaw(IndexT const& ind, OutT & out, ZDT const& zd, std::string const& additional = "\n") { - const auto& pt = zd.FinalSolutions()[ind]; + const auto& pt = zd.SolutionsInternalCoords()[ind]; const auto& data = zd.FinalSolutionMetadata()[ind]; out << data.path_index << '\n' << Precision(pt) << '\n'; @@ -229,13 +230,11 @@ struct NonsingularSolutions static auto Extract(AlgoT const& alg) { - using BCT = typename AlgoTraits::BaseComplexType; - - const auto& sys = alg.TargetSystem(); + using BCT = typename AlgoTraits::BaseComplexT; SampCont solns; - const auto& s = alg.FinalSolutions(); + const auto& s = alg.SolutionsUserCoords(); const auto& m = alg.FinalSolutionMetadata(); const auto n = s.size(); for (decltype(s.size()) ii{0}; ii::BaseComplexType; - - const auto& sys = alg.TargetSystem(); + using BCT = typename AlgoTraits::BaseComplexT; SampCont solns; - const auto& s = alg.FinalSolutions(); - const auto& m = alg.FinalSolutionMetadata(); + const auto& s = alg.SolutionsUserCoords(); const auto n = s.size(); for (decltype(s.size()) ii{0}; ii +#include +#include +#include namespace bertini { @@ -69,13 +74,13 @@ template class SystemManagementP> struct AlgoTraits > { - using BaseRealType = typename tracking::TrackerTraits::BaseRealType; - using BaseComplexType = typename tracking::TrackerTraits::BaseComplexType; + using BaseRealT = typename tracking::TrackerTraits::BaseRealT; + using BaseComplexT = typename tracking::TrackerTraits::BaseComplexT; using NeededConfigs = detail::TypeList< TolerancesConfig, PostProcessingConfig, - ZeroDimConfig, + ZeroDimConfig, AutoRetrackConfig >; }; @@ -83,6 +88,9 @@ struct AlgoTraits +template struct SolutionMetaData { - using SolnIndT = typename SolnCont::size_type; + using SolnIndT = typename SolnCont::size_type; // only vaguely metadata. artifacts of randomness or ordering SolnIndT path_index; // path number of the solution @@ -117,7 +125,7 @@ struct SolutionMetaData ///// things computed across all of the solve bool precision_changed = false; - ComplexType time_of_first_prec_increase; // time value of the first increase in precision + ComplexT time_of_first_prec_increase; // time value of the first increase in precision decltype(DefaultPrecision()) max_precision_used = 0; ///// things computed in pre-endgame only @@ -127,7 +135,7 @@ struct SolutionMetaData ///// things computed in endgame only NumErrorT condition_number; // the latest estimate on the condition number NumErrorT newton_residual; // the latest newton residual - ComplexType final_time_used; // the final value of time tracked to + ComplexT final_time_used; // the final value of time tracked to NumErrorT accuracy_estimate; // accuracy estimate between extrapolations NumErrorT accuracy_estimate_user_coords; // accuracy estimate between extrapolations, in natural coordinates unsigned cycle_num; // cycle number used in extrapolations @@ -138,11 +146,12 @@ struct SolutionMetaData NumErrorT function_residual; // the latest function residual int multiplicity = 1; // multiplicity - bool is_real; // real flag: 0 - not real, 1 - real - bool is_finite; // finite flag: -1 - no finite/infinite distinction, 0 - infinite, 1 - finite - bool is_singular; // singular flag: 0 - non-sigular, 1 - singular + bool multiplicity_representative = true; // is this the chosen single representative of its multiplicity cluster? (the m copies of a multiplicity-m point share one point; exactly one is the representative, the rest are false) + bool is_real = false; // real flag: whether the (dehomogenized) endpoint is real + bool is_finite = false; // finite flag: whether the endpoint is finite (not at infinity) + bool is_singular = false; // singular flag: whether the endpoint is singular (multiple, or ill-conditioned) - bool operator==(const SolutionMetaData & other){ + bool operator==(const SolutionMetaData & other){ bool result = this->path_index == other.path_index && this->solution_index == other.solution_index @@ -159,6 +168,7 @@ struct SolutionMetaData && this->endgame_success == other.endgame_success && this->function_residual == other.function_residual && this->multiplicity == other.multiplicity + && this->multiplicity_representative == other.multiplicity_representative && this->is_real == other.is_real && this->is_finite == other.is_finite && this->is_singular == other.is_singular @@ -190,6 +200,7 @@ std::ostream& operator<<(std::ostream & out, const SolutionMetaData & meta out << "function_residual = " << meta.function_residual << std::endl; out << "multiplicity = " << meta.multiplicity << std::endl; + out << "multiplicity_representative = " << meta.multiplicity_representative << std::endl; out << "is_real = " << meta.is_real << std::endl; out << "is_finite = " << meta.is_finite << std::endl; out << "is_singular = " << meta.is_singular << std::endl; @@ -198,26 +209,34 @@ std::ostream& operator<<(std::ostream & out, const SolutionMetaData & meta } -template +template struct EGBoundaryMetaData { - using RealType = typename NumTraits::Real; + using RealT = typename NumTraits::Real; - Vec path_point; + Vec path_point; SuccessCode success_code = SuccessCode::NeverStarted; - RealType last_used_stepsize; + RealT last_used_stepsize; + // The precision the tracker was actually using when it reached the endgame boundary. + // Carried explicitly (rather than inferred from Precision(path_point)) because the + // path_point is always stored as the tracker's BaseComplexT (multiprecision for AMP); + // a path that tracked in double gets widened on output, so its mantissa precision no + // longer reflects the precision that was in use. The endgame should resume at this + // precision. See zero_dim_solve TrackSinglePathDuringEG. + unsigned precision = DoublePrecision(); EGBoundaryMetaData() = default; EGBoundaryMetaData(EGBoundaryMetaData const&) = default; - EGBoundaryMetaData(Vec const& pt, SuccessCode const& code, RealType const& ss) : - path_point(pt), success_code(code), last_used_stepsize(ss) + EGBoundaryMetaData(Vec const& pt, SuccessCode const& code, RealT const& ss, unsigned prec) : + path_point(pt), success_code(code), last_used_stepsize(ss), precision(prec) {} - - bool operator==(const EGBoundaryMetaData & other){ - bool result = + + bool operator==(const EGBoundaryMetaData & other){ + bool result = this->path_point == other.path_point && this->success_code == other.success_code && this->last_used_stepsize == other.last_used_stepsize + && this->precision == other.precision ; return result; @@ -230,6 +249,140 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me out << "path_point = " << meta.path_point << std::endl; out << "success_code = " << meta.success_code << std::endl; out << "last_used_stepsize = " << meta.last_used_stepsize << std::endl; + out << "precision = " << meta.precision << std::endl; + return out; +} + +/** +\brief Summary of what the midpath (path-crossing) check found at the endgame boundary, and what the +algorithm did about it. + +Two distinct paths landing on the same point at the endgame boundary is a probability-0 event: it +signals a path crossing (under-resolved tracking), not a benign coincidence. The zero-dim algorithm +detects these at the boundary using a relaxed same-point tolerance and re-tracks the offending paths +with tightened settings (see ZeroDim::EGBoundaryAction). This struct records the outcome so a caller +can tell whether a solve hit crossings and whether they were resolved. + +\see ZeroDim::EndgameBoundaryMetadata +*/ +struct MidpathCheckReport +{ + bool passed = true; ///< Did the final midpath check pass (no crossings remained)? + unsigned num_crossings_detected = 0; ///< Number of crossed paths found on the *first* check, before any re-tracking. + unsigned num_resolve_attempts = 0; ///< How many re-track attempts were actually performed. + std::vector crossed_path_indices; ///< Indices of the paths flagged as crossed on the first check. +}; + +inline +std::ostream& operator<<(std::ostream & out, const MidpathCheckReport & r){ + out << "passed = " << std::boolalpha << r.passed << std::endl; + out << "num_crossings_detected = " << r.num_crossings_detected << std::endl; + out << "num_resolve_attempts = " << r.num_resolve_attempts << std::endl; + out << "crossed_path_indices = ["; + for (size_t i = 0; i < r.crossed_path_indices.size(); ++i) + out << (i ? ", " : "") << r.crossed_path_indices[i]; + out << "]" << std::endl; + return out; +} + +/** +\brief A concise, human-readable summary of a zero-dimensional solve: how every path ended up, and +-- crucially -- whether any path was lost. + +A raw solution count can lie: if a near-singular path fails to track, a genuine root is silently +missing and the count comes back short. This report buckets every endpoint into a finite solution, +a clean divergence, or a genuine FAILURE (the tracker gave up), records the failures by their named +SuccessCode, and exposes all_paths_resolved as the one-line health check. + +\see ZeroDim::Report, SummarizeSolve +*/ +struct SolveReport +{ + unsigned long long num_paths_tracked = 0; ///< total paths tracked (the Bezout / start count) + unsigned long long num_finite_solutions = 0; ///< DISTINCT finite solutions (round of sum 1/multiplicity) + unsigned long long num_finite_endpoints = 0; ///< raw count of finite, successful endpoints + unsigned long long num_diverged = 0; ///< paths ending at infinity (Success & !finite, or GoingToInfinity) + unsigned long long num_failed = 0; ///< paths the tracker could not resolve (no solution, no clean divergence) + unsigned long long num_singular = 0; ///< finite solutions flagged singular (multiple / ill-conditioned) + unsigned long long num_real = 0; ///< finite solutions flagged real + std::map failures_by_reason; ///< histogram of the failed paths' SuccessCodes + double max_condition_number = 0; ///< largest condition number among finite solutions + unsigned max_precision_used = 0; ///< highest working precision any path needed (digits) + MidpathCheckReport midpath; ///< the path-crossing check outcome + bool all_paths_resolved = false; ///< num_failed==0 AND no unresolved crossings: the solve is trustworthy +}; + +/** +\brief Build a SolveReport from the per-endpoint final metadata and the midpath-crossing report. +Pure aggregation -- one pass, no solve state -- so it is unit-testable on synthetic metadata. +*/ +template +SolveReport SummarizeSolve(std::vector> const& metadata, + MidpathCheckReport const& midpath) +{ + SolveReport r; + r.num_paths_tracked = metadata.size(); + r.midpath = midpath; + double finite_distinct = 0; + for (auto const& m : metadata) + { + if (m.max_precision_used > r.max_precision_used) + r.max_precision_used = m.max_precision_used; + + bool diverged = (m.endgame_success == SuccessCode::GoingToInfinity); + if (m.endgame_success == SuccessCode::Success) + { + if (m.is_finite) + { + ++r.num_finite_endpoints; + finite_distinct += 1.0 / m.multiplicity; + if (m.is_singular) ++r.num_singular; + if (m.is_real) ++r.num_real; + if (static_cast(m.condition_number) > r.max_condition_number) + r.max_condition_number = static_cast(m.condition_number); + } + else + diverged = true; + } + + if (diverged) + ++r.num_diverged; + else if (m.endgame_success != SuccessCode::Success) // neither a solution nor a clean divergence + { + ++r.num_failed; + ++r.failures_by_reason[m.endgame_success]; + } + } + r.num_finite_solutions = static_cast(finite_distinct + 0.5); + r.all_paths_resolved = (r.num_failed == 0) && midpath.passed; + return r; +} + +inline +std::ostream& operator<<(std::ostream & out, const SolveReport & r) +{ + out << "zero-dim solve -- " << r.num_paths_tracked << " paths tracked\n"; + out << " finite solutions " << r.num_finite_solutions << " (distinct)\n"; + out << " diverged " << r.num_diverged << "\n"; + out << " FAILED " << r.num_failed; + if (r.num_failed) + { + out << " "; + bool first = true; + for (auto const& kv : r.failures_by_reason) + { + out << (first ? "" : ", ") << kv.second << " x " << kv.first; + first = false; + } + } + out << "\n ----\n"; + out << " singular solutions " << r.num_singular << "\n"; + out << " real solutions " << r.num_real << "\n"; + out << " path crossings " << r.midpath.num_crossings_detected + << (r.midpath.passed ? " (resolved)" : " (UNRESOLVED)") << "\n"; + out << " max condition num " << r.max_condition_number << "\n"; + out << " max precision used " << r.max_precision_used << " digits\n"; + out << " all paths resolved? " << (r.all_paths_resolved ? "yes" : "NO") << "\n"; return out; } @@ -252,16 +405,24 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me using SystemT = SystemType; using StartSystemT = StartSystemType; + // This algorithm emits its lifecycle events on the AnyZeroDim base, so a + // single observer type can watch any templated ZeroDim. Accept observers + // declared for AnyZeroDim (in addition to the exact concrete type). + bool ObservableIsA(std::type_index t) const override + { + return Observable::ObservableIsA(t) || t == std::type_index(typeid(AnyZeroDim)); + } + /// a bunch of using statements to reduce typing. - using BaseComplexType = typename tracking::TrackerTraits::BaseComplexType; - using BaseRealType = typename tracking::TrackerTraits::BaseRealType; + using BaseComplexT = typename tracking::TrackerTraits::BaseComplexT; + using BaseRealT = typename tracking::TrackerTraits::BaseRealT; using PrecisionConfig = typename tracking::TrackerTraits::PrecisionConfig; - using SolnIndT = typename SolnCont::size_type; + using SolnIndT = typename SolnCont::size_type; using SystemManagementPolicy = SystemManagementP; @@ -276,17 +437,17 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me using Tolerances = TolerancesConfig; using PostProcessing = PostProcessingConfig; - using ZeroDimConf = ZeroDimConfig; + using ZeroDimConf = ZeroDimConfig; using AutoRetrack = AutoRetrackConfig; - using EGBoundaryMetaDataT = EGBoundaryMetaData; - using SolutionMetaDataT = SolutionMetaData; + using EGBoundaryMetaDataT = EGBoundaryMetaData; + using SolutionMetaDataT = SolutionMetaData; // a few more using statements - using MidpathType = MidpathChecker>; + using MidpathType = MidpathChecker>; using SystemManagementPolicy::TargetSystem; using SystemManagementPolicy::StartSystem; @@ -315,7 +476,212 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me \brief Main Run() function provided for calling from the blackbox mode */ void Run() override - {} + { + // TODO(Phase 2 Python): AnyZeroDim lacks GetSolutions() -- solutions are only + // accessible via the concrete type's member arrays or WriteMainData(). + // Resolve when designing Python bindings; the right approach depends on whether + // Python holds AnyZeroDim or the concrete ZeroDim<...> type directly. +#ifdef BERTINI2_HAVE_MPI + if (parallel::Size() > 1) + { + RunParallel(parallel::WorldComm()); + return; + } +#endif + Solve(); + } + +#ifdef BERTINI2_HAVE_MPI + /** + \brief Manager-worker parallel solve using plain C MPI. + + All ranks must have already constructed an identical ZeroDim from the same input. + Rank 0 acts as manager; ranks 1..P-1 are workers. + + Phase 1: workers track paths to the endgame boundary. + Manager gathers results, runs EGBoundaryAction (midpath check). + Phase 2: manager distributes successful paths (with boundary data) to workers. + Workers run the endgame, send final solutions back. + Manager runs PostEGAction (multiplicities, same-point detection). + */ + void RunParallel(MPI_Comm comm) + { + using Result = parallel::FullPathResult; + using Task = parallel::StartPointTask; + + mpfr_free_cache(); // reproducibility: clear this rank's mpfr constant cache (see Solve()) + + solutions_user_coords_fresh_ = false; + + // Each rank built its homotopy independently in the constructor -- with its OWN + // random patch, start-system coefficients, and gamma -- so a given path index would + // mean a different path on each worker, and the manager would gather a scrambled, + // mostly-wrong solution set. Make rank 0 the single authoritative source: broadcast + // its actual target system, start system, and homotopy to every rank. These are all + // exact (rational coefficients + integer/rational patch), so serialization carries + // them bit-for-bit -- there is no per-rank re-derivation that could drift, and the + // distributed homotopy is identical to rank 0's serial one. We also broadcast rank + // 0's RNG seed so any randomness drawn later (e.g. in the endgame) is consistent; the + // per-path tracking RNG is already deterministic in the path index (ReseedThisThread). + { + unsigned long seed = parallel::IsManager() ? GetGlobalSeed() : 0ul; + MPI_Bcast(&seed, 1, MPI_UNSIGNED_LONG, 0, comm); + SetGlobalSeed(seed); + + // When this policy owns its systems (CloneGiven), install rank 0's authoritative + // systems on every rank. For RefToGiven the user manages the systems and is + // responsible for their consistency across ranks, so we leave them untouched + // (matching the old no-op SystemSetup for that policy). + if constexpr (SystemManagementPolicy::OwnsSystems) + { + parallel::mpi_broadcast_serialized(comm, TargetSystem(), 0); + parallel::mpi_broadcast_serialized(comm, StartSystem(), 0); + parallel::mpi_broadcast_serialized(comm, Homotopy(), 0); + } + + num_start_points_ = StartSystem().NumStartPoints(); + GetTracker().SetSystem(Homotopy()); + } + + PreSolveChecks(); + PreSolveSetup(); + + const int n_threads = parallel::WorkerThreadCount(); + + // One round = dispatch a set of path indices, each executed as a WHOLE path + // (pre-endgame + endgame) by a worker. Round 0 is every path; later rounds re-run + // only the crossed paths with escalated settings -- the re-track is path-independent, + // so it rides the same demand-driven worker pool (no idle-manager bottleneck). + if (parallel::IsManager()) + { + // Rank 0 computes every start point once, authoritatively, and ships each to the + // worker that tracks it. Cached so re-track rounds reuse the identical points. + std::vector> start_points(num_start_points_); + for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) + start_points[ii] = ComputeStartPoint(static_cast(ii)); + + auto run_round = [&](std::queue& q) + { + parallel::RunManagerLoop(comm, q, + [this](Result const& r){ StoreFullPathResult(r); }); + }; + + std::queue queue; + for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) + queue.push(Task{ static_cast(ii), start_points[ii] }); + run_round(queue); + + // Midpath/crossing check on the collected boundary points, then bounded parallel + // resolve rounds. A continue/stop flag is broadcast to the workers each round so + // they know whether to run again (and escalate their settings first). + auto passed = midpath_.Check(solutions_at_endgame_boundary_, StartSystem()); + midpath_report_ = MidpathCheckReport{}; + midpath_report_.passed = passed; + for (auto const& v : midpath_.GetCrossedPaths()) + midpath_report_.crossed_path_indices.push_back(v.index()); + midpath_report_.num_crossings_detected = + static_cast(midpath_report_.crossed_path_indices.size()); + + const auto max_attempts = this->template Get().max_num_crossed_path_resolve_attempts; + unsigned num_resolve_attempts = 0; + while (true) + { + int keep_going = (!passed && num_resolve_attempts < max_attempts) ? 1 : 0; + MPI_Bcast(&keep_going, 1, MPI_INT, 0, comm); + if (!keep_going) + break; + + std::queue redo; + for (auto const& v : midpath_.GetCrossedPaths()) + if (v.rerun()) + { + auto idx = static_cast(v.index()); + redo.push(Task{ idx, start_points[idx] }); + } + run_round(redo); + + passed = midpath_.Check(solutions_at_endgame_boundary_, StartSystem()); + ++num_resolve_attempts; + } + midpath_report_.num_resolve_attempts = num_resolve_attempts; + midpath_report_.passed = passed; + if (!passed) + std::cerr << "warning: " << midpath_report_.num_crossings_detected + << " path crossing(s) detected at the endgame boundary remained unresolved " + << "after " << num_resolve_attempts << " re-track attempt(s); " + << "the affected solutions may be wrong. Consider a higher-order predictor " + << "or a tighter tracking tolerance. See EndgameBoundaryMetadata()." << std::endl; + + PostEGAction(); + } + else // worker + { + auto run_worker_round = [&]() + { + if (n_threads <= 1) + { + // Single-threaded worker: execute whole paths on the rank's member + // tracker/endgame directly, from rank 0's authoritative start point. + parallel::RunWorkerLoop(comm, + [this](Task const& task){ ExecuteOnePath(MemberDuringEGContext(), static_cast(task.path_index), task.start_point); }, + [this](Task const& task) -> Result { return PackFullPathResult(static_cast(task.path_index)); }); + } + else + { + // Threaded worker: each thread owns a self-contained PathThreadState clone + // so concurrent whole-path execution shares no mutable state. Cloned per + // round so it inherits any escalation (predictor bump) applied to the rank's + // member tracker between rounds. + auto state_factory = [this]() -> std::unique_ptr + { + // Clone(), not copy: System copies are SHALLOW (shared expression-tree + // nodes whose value caches mutate on Eval); Clone() deep-copies the tree. + // Trackers/Endgames have no default ctor, so aggregate-init via new. + std::unique_ptr s( + new PathThreadState{ Clone(GetTracker().GetSystem()), Clone(TargetSystem()), + GetTracker(), GetEndgame(), {}, {} }); + s->tracker.SetSystem(s->sys); + s->endgame.SetTracker(s->tracker); + SetThreadPrecision(this->template Get().initial_ambient_precision); + return s; + }; + + auto track_fn = [this](std::unique_ptr& state, Task const& task) -> Result + { + auto idx = static_cast(task.path_index); + ExecuteOnePath(state->Context(), idx, task.start_point); + return PackFullPathResult(idx); + }; + + parallel::RunWorkerLoopThreaded( + comm, state_factory, track_fn, n_threads); + } + }; // run_worker_round + + while (true) + { + run_worker_round(); + + int keep_going = 0; + MPI_Bcast(&keep_going, 1, MPI_INT, 0, comm); + if (!keep_going) + break; + + // Match the escalation the serial RunMidpathResolution applies between + // re-track passes: tighten midpath_retrack_tolerance_ (read by ExecuteOnePath) + // and bump a low-order predictor on this rank's member tracker, so the next + // round's thread clones inherit it. + EscalateRetrackSettings(); + } + } + } +#endif // BERTINI2_HAVE_MPI + + // Definitions are out-of-line at the bottom of this file, after + // output.hpp is included (avoiding a circular-include chicken-and-egg). + void WriteMainData(std::ostream& out) const override; + void WriteRawData(std::ostream& out) const override; + void ApplyParsedConfigs(std::string const& config_str) override; virtual ~ZeroDim() = default; /// setup functions @@ -329,7 +695,16 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me if (TargetSystem().HavePathVariable()) throw std::runtime_error("unable to perform zero dim solve on target system -- has path variable, use user homotopy instead."); - if (TargetSystem().NumVariables() > TargetSystem().NumTotalFunctions()) + // A square zero-dim system needs one equation per dimension. Each projective + // (homogeneous) variable group of size k spans P^{k-1}: its k coordinates carry + // only k-1 dimensions because scale is free, so it needs one fewer equation than + // it has variables. Subtract that free scale per projective group before + // comparing -- otherwise a genuinely square multiprojective system (e.g. the + // eigenvalue problem (A - lam I)x = 0 with x projective, lam affine) is wrongly + // rejected. Affine-only systems have no hom variable groups, so this is a no-op + // for them. (Patches, which would also enter NumTotalFunctions, are added later + // during system preparation; this check runs on the as-supplied system.) + if (TargetSystem().NumVariables() - TargetSystem().NumHomVariableGroups() > TargetSystem().NumTotalFunctions()) throw std::runtime_error("unable to perform zero dim solve on target system -- underconstrained, so has no zero dimensional solutions."); if (!TargetSystem().IsPolynomial()) @@ -371,7 +746,11 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me this->template Set(Tolerances()); this->template Set(PostProcessing()); - this->template Set(ZeroDimConf()); + // ZeroDimConfig is not templated on the complex type, so it cannot pick its own + // per-type ambient-precision default. Set it here, where the tracking type is known. + ZeroDimConf zdc; + zdc.initial_ambient_precision = DefaultInitialAmbientPrecision(); + this->template Set(zdc); this->template Set(AutoRetrack()); } @@ -385,6 +764,23 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me return midpath_retrack_tolerance_; } + /** + \brief Set the precision at which start points are computed. + + Defaults to the initial ambient precision. Set this to carry a higher precision forward + (e.g. when one solve's output seeds the next). Once set, PreSolveSetup will not override it. + */ + void SetStartPointPrecision(unsigned p) + { + start_point_precision_ = p; + start_point_precision_set_by_user_ = true; + } + + unsigned StartPointPrecision() const + { + return start_point_precision_; + } + /** call this after setting up the tolerances, etc. @@ -518,30 +914,162 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me */ void Solve() { + // Reproducibility: clear this thread's mpfr constant cache (pi, etc.) at the solve + // boundary. mpfr caches transcendental constants at the precision last requested; the + // Cauchy endgame needs pi (roots of unity), so a high-precision solve leaves pi cached + // at high precision. A subsequent lower-precision solve would otherwise reuse that + // cached value rounded down -- differing by ~1 ULP from a freshly-computed + // low-precision pi -- making a solve's behaviour depend on what ran before it in the + // process (non-reproducible). Clearing here makes each solve independent of history. + // Cheap (one constant recompute per solve); thread-local (each worker thread clears its + // own when it starts a solve). + mpfr_free_cache(); + + solutions_user_coords_fresh_ = false; + PreSolveChecks(); PreSolveSetup(); - TrackBeforeEG(); + this->NotifyObservers(AlgorithmStarted(*this)); - EGBoundaryAction(); + // Speculative-full-path model: carry every path all the way through (pre-endgame + + // endgame) as one unit, then detect crossings from the collected boundary points and + // re-run any crossed path in full. This is the same shape the distributed solve uses + // (one worker per whole path), so serial and distributed share the per-path primitive + // -- including computing the start point via the same ComputeStartPoint. + for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) + { + auto idx = static_cast(ii); + ExecuteOnePath(MemberDuringEGContext(), idx, ComputeStartPoint(idx)); + } - TrackDuringEG(); + RunMidpathResolution([this](SolnIndT idx){ ExecuteOnePath(MemberDuringEGContext(), idx, ComputeStartPoint(idx)); }); PostEGAction(); + + this->NotifyObservers(AlgorithmComplete(*this)); } /** - \brief Get the final computed solutions + \brief Get the computed solutions, in the coordinates of the user's + original variables. + + The stored internal solutions are dehomogenized through the target + system, once, lazily, into a cache; repeated calls return the cached + container by const reference. Assumes post-solve serial access, like + the other accessors here. + + \see SolutionsInternalCoords */ - const auto& FinalSolutions() const + const auto& SolutionsUserCoords() const + { + if (!solutions_user_coords_fresh_) + { + solutions_user_coords_.clear(); + solutions_user_coords_.reserve(solutions_post_endgame_.size()); + for (const auto& s : solutions_post_endgame_) + { + // A path that failed before or during the endgame leaves its endpoint + // slot default-constructed (zero coordinates); keep index alignment with + // FinalSolutionMetadata (output filters on it) by emitting an empty + // placeholder rather than trying to dehomogenize an unset point. + if (s.size() == 0) + solutions_user_coords_.emplace_back(); + else + solutions_user_coords_.push_back(this->TargetSystem().DehomogenizePoint(s)); + } + solutions_user_coords_fresh_ = true; + } + return solutions_user_coords_; + } + + /** + \brief Get the computed solutions in the solver's internal coordinates: + homogenized, and lying on the target system's patch. + + These are the coordinates for continuing work -- start points for further + tracking re-using the target system's patch, refinement, etc. Take a + user-coordinates point back to this representation with + System::HomogenizePoint on the target system. + + \see SolutionsUserCoords + */ + const auto& SolutionsInternalCoords() const { return solutions_post_endgame_; } + /** + \brief Select the solution points whose parallel metadata satisfies a predicate. + + The backing for the FiniteSolutions / RealSolutions / Singular / Nonsingular convenience + accessors. Returns points in user coordinates by default (pass user_coords=false for the + solver's internal coordinates), mirroring SolutionsUserCoords / SolutionsInternalCoords. + */ + template + SolnCont> SolutionsWhere(Pred pred, bool user_coords = true) const + { + auto const& sols = user_coords ? SolutionsUserCoords() : SolutionsInternalCoords(); + auto const& md = FinalSolutionMetadata(); + SolnCont> out; + for (size_t i = 0; i < md.size() && i < sols.size(); ++i) + if (pred(md[i])) + out.push_back(sols[i]); + return out; + } + + /** + \brief The finite solutions: successful endpoints the library calls FINITE (is_finite + applies the configured endpoint_finite_threshold). Includes singular, nonsingular, and + real solutions alike. \see RealSolutions, SingularSolutions, NonsingularSolutions + */ + SolnCont> FiniteSolutions(bool user_coords = true) const + { + return SolutionsWhere([](auto const& m){ + return m.endgame_success == SuccessCode::Success && m.is_finite; }, user_coords); + } + + /// \brief The real finite solutions (is_real applies the configured tolerance). + SolnCont> RealSolutions(bool user_coords = true) const + { + return SolutionsWhere([](auto const& m){ + return m.endgame_success == SuccessCode::Success && m.is_finite && m.is_real; }, user_coords); + } + + /// \brief The nonsingular finite solutions (simple, well-conditioned roots). + SolnCont> NonsingularSolutions(bool user_coords = true) const + { + return SolutionsWhere([](auto const& m){ + return m.endgame_success == SuccessCode::Success && m.is_finite && !m.is_singular; }, user_coords); + } + + /// \brief The singular finite solutions (multiple or ill-conditioned roots). + SolnCont> SingularSolutions(bool user_coords = true) const + { + return SolutionsWhere([](auto const& m){ + return m.endgame_success == SuccessCode::Success && m.is_finite && m.is_singular; }, user_coords); + } + + /** + \brief The solutions at infinity: endpoints not classified finite (is_finite is false) -- + the complement of FiniteSolutions within the full solution list. These are the paths the + endgame resolved as diverging (its GoingToInfinity / SecurityMaxNormReached verdict, or a + successful endpoint whose dehomogenized infinity norm exceeds endpoint_finite_threshold). + + \note A path that FAILED before the endgame also leaves is_finite at its default (false) and + so appears here; its stored point is not a meaningful solution at infinity. Consult the + endgame_success in FinalSolutionMetadata (or Report()) to distinguish a true divergence from a + tracking failure. \see FiniteSolutions + */ + SolnCont> InfiniteSolutions(bool user_coords = true) const + { + return SolutionsWhere([](auto const& m){ return !m.is_finite; }, user_coords); + } + /** \brief Get the metadat associated with the final computed solutions */ @@ -553,11 +1081,34 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me /** \brief Get the solutions as computed at the endgame boundary */ - const auto& EndgameBoundaryData() const + const auto& EndgameBoundarySolutions() const { return solutions_at_endgame_boundary_; } + /** + \brief Get the report from the midpath (path-crossing) check performed at the endgame + boundary: how many crossings were detected, which paths, how many re-track attempts were + made, and whether the check ultimately passed. + + \see MidpathCheckReport + */ + const MidpathCheckReport& EndgameBoundaryMetadata() const + { + return midpath_report_; + } + + /** + \brief A concise summary of the solve: how every path ended, by category, and whether any + path was lost. Computed on demand from the final solution metadata and the midpath report. + + \see SolveReport + */ + SolveReport Report() const + { + return SummarizeSolve(FinalSolutionMetadata(), EndgameBoundaryMetadata()); + } + private: /** @@ -572,6 +1123,28 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me void PreSolveSetup() { + // Fixed-multiple precision: FixedPrecisionConfig.precision (on the tracker) is the + // authoritative precision for the whole solve. Lift the tracker, the ambient/thread + // precision, the start-point precision (via initial_ambient_precision), and the systems + // all to that one value -- TrackerLoopInitialization requires them to agree. (Double is + // fixed at 16 and adaptive manages its own precision, so neither enters here.) + if constexpr (!tracking::TrackerTraits::IsAdaptivePrec) + { + auto const& fp = GetTracker().template Get(); + // Double validates (its PrecisionSetup throws on any precision other than 16); + // fixed-multiple adopts the value into precision_. + GetTracker().PrecisionSetup(fp); + if constexpr (!std::is_same::value) + if (fp.precision != 0) + { + ZeroDimConf zdc = this->template Get(); + zdc.initial_ambient_precision = fp.precision; // -> thread + start-point precision + this->template Set(zdc); + TargetSystem().precision(fp.precision); + Homotopy().precision(fp.precision); + } + } + auto num_as_size_t = static_cast(num_start_points_); solution_final_metadata_.resize(num_as_size_t); @@ -579,208 +1152,369 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me solutions_post_endgame_.resize(num_as_size_t); SetMidpathRetrackTol(this->template Get().newton_before_endgame); + + // Default the start-point precision to the initial ambient precision. A caller can + // override it via SetStartPointPrecision (e.g. to carry a higher precision forward + // from a previous solve whose output feeds this one). + if (!start_point_precision_set_by_user_) + start_point_precision_ = this->template Get().initial_ambient_precision; } /** - \brief Track from the start point in time, from each start point of the start system, to the endgame boundary. - - Results are accumulated into an internally stored variable, solutions_at_endgame_boundary_. - - The point at the endgame boundary, as well as the success flag, and the stepsize, are all stored. + Reference-bundle over the tracker/endgame/systems/observers a single path needs. + Lets one set of per-path execution bodies (ExecuteBeforeEG / ExecuteDuringEG) serve + both the serial flow (members) and the distributed worker (thread-owned clones), so + there is exactly ONE per-path code path and serial/distributed cannot diverge. */ - void TrackBeforeEG() + struct BeforeEGContext { - DefaultPrecision(this->template Get().initial_ambient_precision); - - GetTracker().SetTrackingTolerance(this->template Get().newton_before_endgame); - - auto t_start = this->template Get().start_time; - auto t_endgame_boundary = this->template Get().endgame_boundary; - - for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) - { - TrackSinglePathBeforeEG(static_cast(ii)); - } + TrackerType& tracker; + tracking::FirstPrecisionRecorder& first_prec_rec; + tracking::MinMaxPrecisionRecorder& min_max_prec; + }; + struct DuringEGContext + { + // const ref: the residual/dehomogenize/precision calls are const-callable, and the + // RefToGiven policy hands back a const target system. A mutable thread-owned clone + // binds here too. + SystemType const& target_sys; + TrackerType& tracker; + EndgameType& endgame; + tracking::FirstPrecisionRecorder& first_prec_rec; + tracking::MinMaxPrecisionRecorder& min_max_prec; + }; + + BeforeEGContext MemberBeforeEGContext() + { + return BeforeEGContext{ GetTracker(), first_prec_rec_, min_max_prec_ }; + } + DuringEGContext MemberDuringEGContext() + { + return DuringEGContext{ TargetSystem(), GetTracker(), GetEndgame(), first_prec_rec_, min_max_prec_ }; } - - /** - /brief Track a single path before we reach the endgame boundary. + \brief Compute the start point for one path, authoritatively. + + The single source of a start point. The serial flow calls this directly; in a distributed + solve rank 0 calls it and ships the result to the worker (the worker never derives its own), + so start points are identical across serial and distributed runs. Computed at + start_point_precision_ (defaults to the initial ambient precision; settable so a chained + solve can carry precision forward). StartPoint() mutates the shared start system's + expression-tree value caches, so generation is serialized. */ - void TrackSinglePathBeforeEG(SolnIndT soln_ind) + Vec ComputeStartPoint(SolnIndT soln_ind) { - // if you can think of a way to replace this `if` with something meta, please do so. - if (tracking::TrackerTraits::IsAdaptivePrec) - { - GetTracker().AddObserver(first_prec_rec_); - GetTracker().AddObserver(min_max_prec_); - } - - auto& smd = solution_final_metadata_[soln_ind]; - - smd.path_index = soln_ind; - smd.solution_index = soln_ind; - - DefaultPrecision(this->template Get().initial_ambient_precision); - auto t_start = this->template Get().start_time; - auto t_endgame_boundary = this->template Get().endgame_boundary; - auto start_point = StartSystem().template StartPoint(soln_ind); + SetThreadPrecision(start_point_precision_); + static std::mutex start_point_mutex; + std::lock_guard lock(start_point_mutex); + return StartSystem().template StartPoint(soln_ind); + } - Vec result; - auto tracking_success = GetTracker().TrackPath(result, t_start, t_endgame_boundary, start_point); + /** + \brief Track one path from the start time to the endgame boundary, against `ctx`. - solutions_at_endgame_boundary_[soln_ind] = EGBoundaryMetaDataT({ result, tracking_success, GetTracker().CurrentStepsize() }); + The single before-endgame body, shared by the serial flow and the distributed worker. + Uses SetThreadPrecision (thread-local) throughout, so it is correct on the main thread + and concurrently on worker threads alike. Writes the boundary point + the pre-endgame + portion of the metadata. + */ + void ExecuteBeforeEG(BeforeEGContext ctx, SolnIndT soln_ind, Vec const& start_point) + { + ReseedThisThread(static_cast(soln_ind)); - smd.pre_endgame_success = tracking_success; + if (tracking::TrackerTraits::IsAdaptivePrec) + { + ctx.tracker.AddObserver(ctx.first_prec_rec); + ctx.tracker.AddObserver(ctx.min_max_prec); + } - // if you can think of a way to replace this `if` with something meta, please do so. - if (tracking::TrackerTraits::IsAdaptivePrec) + auto& smd = solution_final_metadata_[soln_ind]; + smd.path_index = soln_ind; + smd.solution_index = soln_ind; + + auto initial_prec = this->template Get().initial_ambient_precision; + // SetThreadPrecision: writes thread-local only, safe from concurrent threads. + SetThreadPrecision(initial_prec); + + // Draw the condition-number probe direction ONCE here, for the whole track of this + // point (pre-endgame AND endgame). We have just reseeded this thread's RNG to a value + // determined solely by the path index, so the probe is deterministic per path and + // identical whether the path is tracked in a serial loop or on a distributed worker -- + // regardless of how the paths were split across ranks. It is NOT refreshed for the + // endgame (which would churn the RNG across its sample-circle sub-tracks), so the same + // direction persists start to finish. + ctx.tracker.RefreshConditionDirection(); + + // The times are stored precision-free (mpq_rational); materialize them as the tracking + // complex type at the current working precision. + BaseComplexT t_start ( BaseRealT(this->template Get().start_time) ); + BaseComplexT t_endgame_boundary( BaseRealT(this->template Get().endgame_boundary) ); + + // The start point is supplied by the caller (computed once, authoritatively, via + // ComputeStartPoint) rather than regenerated here. In a distributed solve rank 0 + // computes it and sends it with the task, so a worker never derives its own -- that, + // plus authoritative pi (issue #156), is what makes the start point identical across + // serial and distributed runs. + + // Reset to the configured initial step size for this fresh path. In the + // speculative-full-path model a path's endgame runs immediately before the next + // path's pre-endgame tracking on the same tracker, and the endgame leaves + // ReinitializeInitialStepSize(false) with a tiny step; without restoring it here the + // next path would crawl from the start time with that tiny step (effectively a hang). + ctx.tracker.ReinitializeInitialStepSize(true); + + // Begin tracking at the intended ambient precision rather than the start point's + // incidental precision. Total-degree start points are generated at + // LowestMultiplePrecision (the generator's arithmetic widens past the requested + // digits, see issue #308), so without this the AMP tracker would start every + // well-conditioned path in multiprecision and never drop to double. + if constexpr (tracking::TrackerTraits::IsAdaptivePrec) + ctx.tracker.SetStartPrecision(initial_prec); + + Vec result; + auto tracking_success = ctx.tracker.TrackPath(result, t_start, t_endgame_boundary, start_point); + + solutions_at_endgame_boundary_[soln_ind] = + EGBoundaryMetaDataT({ result, tracking_success, ctx.tracker.CurrentStepsize(), ctx.tracker.CurrentPrecision() }); + + // Clear the start-precision override so it does not leak into the endgame, which + // shares this tracker instance for its sample circles. + if constexpr (tracking::TrackerTraits::IsAdaptivePrec) + ctx.tracker.SetStartPrecision(std::nullopt); + + smd.pre_endgame_success = tracking_success; + + if (tracking::TrackerTraits::IsAdaptivePrec) + { + if (ctx.first_prec_rec.DidPrecisionIncrease()) { - if (first_prec_rec_.DidPrecisionIncrease()) - { - smd.precision_changed = true; - smd.time_of_first_prec_increase = first_prec_rec_.TimeOfIncrease(); - } - else - GetTracker().RemoveObserver(first_prec_rec_); - GetTracker().RemoveObserver(min_max_prec_); - using std::max; - smd.max_precision_used = - max(smd.max_precision_used, min_max_prec_.MaxPrecision()); + smd.precision_changed = true; + smd.time_of_first_prec_increase = ctx.first_prec_rec.TimeOfIncrease(); } - - + else + ctx.tracker.RemoveObserver(ctx.first_prec_rec); + ctx.tracker.RemoveObserver(ctx.min_max_prec); + using std::max; + smd.max_precision_used = + max(smd.max_precision_used, ctx.min_max_prec.MaxPrecision()); + } } - void EGBoundaryAction() +#ifdef BERTINI2_HAVE_MPI + /** + Self-contained per-thread state for executing one WHOLE path on a threaded MPI worker. + Each std::thread owns one: a homotopy copy (tracked by `tracker`), a target-system copy + (residual evaluation mutates System precision state), a Tracker and Endgame, and its own + precision observers so observer-derived metadata matches serial runs. Build a + DuringEGContext from it to drive ExecuteOnePath. + */ + struct PathThreadState { - auto midcheckpassed = midpath_.Check(solutions_at_endgame_boundary_, StartSystem()); - - unsigned num_resolve_attempts = 0; - while (!midcheckpassed && num_resolve_attempts < this->template Get().max_num_crossed_path_resolve_attempts) + System sys; // homotopy, tracked by `tracker` + System target_sys; // for function residuals / dehomogenization + TrackerType tracker; + EndgameType endgame; + tracking::FirstPrecisionRecorder first_prec_rec; + tracking::MinMaxPrecisionRecorder min_max_prec; + + DuringEGContext Context() { - MidpathResolve(); - midcheckpassed = midpath_.Check(solutions_at_endgame_boundary_, StartSystem()); - num_resolve_attempts++; + return DuringEGContext{ target_sys, tracker, endgame, first_prec_rec, min_max_prec }; } + }; +#endif // BERTINI2_HAVE_MPI + + /** + \brief Apply one step of escalation to the tracker before re-tracking crossed paths. + + Two remedies are applied: (1) tighten the tracking tolerance, and (2) raise the ODE + predictor to the default (RKF45) if a low-order predictor is in use -- a too-low-order + predictor (notably Euler) is the most common cause of spurious boundary crossings, and + no amount of tolerance-tightening on a first-order predictor is as effective as moving to + a higher-order one. The predictor bump is idempotent (once at RKF45's order it is a + no-op). + + \note Future work: this single step wants to become a *configurable, ordered remedy list* + (tighten tolerance, more Newton iterations, bump predictor, shrink min step size, ...), + applied in sequence until exhausted. That strategy abstraction is intentionally deferred; + for now the escalation is a fixed, minimal two-remedy step. Termination is guaranteed + regardless, because EGBoundaryAction bounds the number of attempts. + */ + void EscalateRetrackSettings() + { + midpath_retrack_tolerance_ *= this->template Get().midpath_decrease_tolerance_factor; + GetTracker().SetTrackingTolerance(midpath_retrack_tolerance_); + + const auto default_predictor = tracking::predict::DefaultPredictor(); + if (tracking::predict::Order(GetTracker().GetPredictor()) + < tracking::predict::Order(default_predictor)) + GetTracker().SetPredictor(default_predictor); } - void MidpathResolve() + + /** + \brief Execute one whole path: start -> endgame boundary -> target, against `ctx`. + + The unit of work in the speculative-full-path model: a single worker/thread carries a + path through both the pre-endgame tracking and the endgame, so the boundary state never + leaves the executing context (no serialize-the-handoff, hence no precision-transfer gap). + The pre-endgame tracking tolerance is `midpath_retrack_tolerance_` (== newton_before_endgame + on the first pass, tightened on re-track passes by EscalateRetrackSettings); the endgame + uses newton_during_endgame. + */ + void ExecuteOnePath(DuringEGContext ctx, SolnIndT soln_ind, Vec const& start_point) { - ShrinkMidpathTolerance(); + this->NotifyObservers(PathStarted(*this, static_cast(soln_ind))); + + ctx.tracker.SetTrackingTolerance(midpath_retrack_tolerance_); + ExecuteBeforeEG(BeforeEGContext{ ctx.tracker, ctx.first_prec_rec, ctx.min_max_prec }, soln_ind, start_point); - for(auto const& v : midpath_.GetCrossedPaths()) + if (solution_final_metadata_[soln_ind].pre_endgame_success != SuccessCode::Success) { - if(v.rerun()) - { - unsigned long long index = v.index(); - auto soln_ind = static_cast(index); - TrackSinglePathBeforeEG(soln_ind); - } + this->NotifyObservers(PathComplete(*this, static_cast(soln_ind))); + return; } - } - + ctx.tracker.SetTrackingTolerance(this->template Get().newton_during_endgame); + ExecuteDuringEG(ctx, soln_ind); - void ShrinkMidpathTolerance() - { - midpath_retrack_tolerance_ *= this->template Get().midpath_decrease_tolerance_factor; - GetTracker().SetTrackingTolerance(midpath_retrack_tolerance_); + this->NotifyObservers(PathComplete(*this, static_cast(soln_ind))); } + /** + \brief Detect path crossings from the collected boundary points and, for each crossed + path, re-run it via `redo_one` (a full-path redo) with escalated settings. - void TrackDuringEG() + Shared by the serial flow and the distributed manager. Populates midpath_report_. + Bounded by max_num_crossed_path_resolve_attempts (0 = detect-and-report only), so it always + terminates. Unresolved crossings stay tracked but observable (report.passed == false + + warning). + */ + template + void RunMidpathResolution(RedoOne&& redo_one) { + auto passed = midpath_.Check(solutions_at_endgame_boundary_, StartSystem()); - GetTracker().SetTrackingTolerance(this->template Get().newton_during_endgame); + midpath_report_ = MidpathCheckReport{}; + midpath_report_.passed = passed; + for (auto const& v : midpath_.GetCrossedPaths()) + midpath_report_.crossed_path_indices.push_back(v.index()); + midpath_report_.num_crossings_detected = + static_cast(midpath_report_.crossed_path_indices.size()); - for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) + const auto max_attempts = this->template Get().max_num_crossed_path_resolve_attempts; + unsigned num_resolve_attempts = 0; + while (!passed && num_resolve_attempts < max_attempts) { - auto soln_ind = static_cast(ii); + EscalateRetrackSettings(); + for (auto const& v : midpath_.GetCrossedPaths()) + if (v.rerun()) + redo_one(static_cast(v.index())); + passed = midpath_.Check(solutions_at_endgame_boundary_, StartSystem()); + ++num_resolve_attempts; + } - if (solution_final_metadata_[soln_ind].pre_endgame_success != SuccessCode::Success) - continue; + midpath_report_.num_resolve_attempts = num_resolve_attempts; + midpath_report_.passed = passed; - TrackSinglePathDuringEG(soln_ind); - } + if (!passed) + std::cerr << "warning: " << midpath_report_.num_crossings_detected + << " path crossing(s) detected at the endgame boundary remained unresolved " + << "after " << num_resolve_attempts << " re-track attempt(s); " + << "the affected solutions may be wrong. Consider a higher-order predictor " + << "or a tighter tracking tolerance. See EndgameBoundaryMetadata()." << std::endl; } - void TrackSinglePathDuringEG(SolnIndT soln_ind) + /** + \brief Run the endgame on one path (boundary→target), against `ctx`. + + The single during-endgame body, shared by the serial flow and the distributed worker. + Resumes at the precision the path was using at the boundary + (`solutions_at_endgame_boundary_[idx].precision`) and writes the final solution + the + endgame portion of the metadata. + */ + void ExecuteDuringEG(DuringEGContext ctx, SolnIndT soln_ind) { + ReseedThisThread(static_cast(soln_ind) + static_cast(num_start_points_)); - auto& smd = solution_final_metadata_[soln_ind]; - // if you can think of a way to replace this `if` with something meta, please do so. - if (tracking::TrackerTraits::IsAdaptivePrec) - { - if (!smd.precision_changed) - GetTracker().AddObserver(first_prec_rec_); - GetTracker().AddObserver(min_max_prec_); - } + auto& smd = solution_final_metadata_[soln_ind]; + if (tracking::TrackerTraits::IsAdaptivePrec) + { + if (!smd.precision_changed) + ctx.tracker.AddObserver(ctx.first_prec_rec); + ctx.tracker.AddObserver(ctx.min_max_prec); + } const auto& bdry_point = solutions_at_endgame_boundary_[soln_ind].path_point; + ctx.tracker.SetStepSize(solutions_at_endgame_boundary_[soln_ind].last_used_stepsize); + ctx.tracker.ReinitializeInitialStepSize(false); - GetTracker().SetStepSize(solutions_at_endgame_boundary_[soln_ind].last_used_stepsize); - GetTracker().ReinitializeInitialStepSize(false); + // Resume the endgame at the precision the path was actually using at the boundary, + // rather than inferring it from the (always-multiprecision) point's mantissa, which + // is unreliable for paths that tracked in double. + auto start_prec = solutions_at_endgame_boundary_[soln_ind].precision; + SetThreadPrecision(start_prec); - auto start_prec = Precision(bdry_point); + ctx.endgame.SetBoundaryTime(BaseComplexT(BaseRealT(this->template Get().endgame_boundary))); + ctx.endgame.SetTargetTime (BaseComplexT(BaseRealT(this->template Get().target_time))); - DefaultPrecision(start_prec); + auto eg_success = ctx.endgame.Run(bdry_point); - BaseComplexType t_end = this->template Get().target_time; + solutions_post_endgame_[soln_ind] = ctx.endgame.template FinalApproximation(); - BaseComplexType t_endgame_boundary = this->template Get().endgame_boundary; - Precision(t_endgame_boundary,start_prec); + smd.endgame_success = eg_success; - auto eg_success = GetEndgame().Run(t_endgame_boundary, bdry_point, t_end); - - solutions_post_endgame_[soln_ind] = GetEndgame().template FinalApproximation(); - - - // finally, store the metadata as necessary - smd.endgame_success = eg_success; - // if you can think of a way to replace this `if` with something meta, please do so. + // an unsuccessful endgame has no final approximation, so the final-point-dependent + // metadata cannot be computed. + if (eg_success != SuccessCode::Success) + { if (tracking::TrackerTraits::IsAdaptivePrec) { - if (!smd.precision_changed) - { - if (first_prec_rec_.DidPrecisionIncrease()) - { - smd.precision_changed = true; - smd.time_of_first_prec_increase = first_prec_rec_.TimeOfIncrease(); - } - } - GetTracker().RemoveObserver(first_prec_rec_); - GetTracker().RemoveObserver(min_max_prec_); - using std::max; - smd.max_precision_used = - max(smd.max_precision_used, min_max_prec_.MaxPrecision()); + ctx.tracker.RemoveObserver(ctx.first_prec_rec); + ctx.tracker.RemoveObserver(ctx.min_max_prec); } - if (tracking::TrackerTraits::IsAdaptivePrec) + return; + } + if (tracking::TrackerTraits::IsAdaptivePrec) + { + if (!smd.precision_changed) { - assert(Precision(solutions_post_endgame_[soln_ind])==Precision(GetEndgame().template FinalApproximation())); - DefaultPrecision(Precision(solutions_post_endgame_[soln_ind])); - TargetSystem().precision(Precision(solutions_post_endgame_[soln_ind])); + if (ctx.first_prec_rec.DidPrecisionIncrease()) + { + smd.precision_changed = true; + smd.time_of_first_prec_increase = ctx.first_prec_rec.TimeOfIncrease(); + } + ctx.tracker.RemoveObserver(ctx.first_prec_rec); } - smd.function_residual = static_cast(TargetSystem().Eval(solutions_post_endgame_[soln_ind]).template lpNorm()); - smd.final_time_used = GetEndgame().LatestTime(); - smd.condition_number = GetTracker().LatestConditionNumber(); - smd.newton_residual = GetTracker().LatestNormOfStep(); - - smd.accuracy_estimate = GetEndgame().ApproximateError(); - smd.accuracy_estimate_user_coords = - static_cast( (TargetSystem().DehomogenizePoint(solutions_post_endgame_[soln_ind]) - - TargetSystem().DehomogenizePoint(GetEndgame().template PreviousApproximation())).template lpNorm() ); - smd.cycle_num = GetEndgame().CycleNumber(); - // end metadata gathering + ctx.tracker.RemoveObserver(ctx.min_max_prec); + using std::max; + smd.max_precision_used = + max(smd.max_precision_used, ctx.min_max_prec.MaxPrecision()); + } + if (tracking::TrackerTraits::IsAdaptivePrec) + { + assert(Precision(solutions_post_endgame_[soln_ind])==Precision(ctx.endgame.template FinalApproximation())); + SetThreadPrecision(Precision(solutions_post_endgame_[soln_ind])); + ctx.target_sys.precision(Precision(solutions_post_endgame_[soln_ind])); + } + smd.function_residual = static_cast(ctx.target_sys.Eval(solutions_post_endgame_[soln_ind]).template lpNorm()); + smd.final_time_used = ctx.endgame.LatestTime(); + smd.condition_number = ctx.tracker.LatestConditionNumber(); + smd.newton_residual = ctx.tracker.LatestNormOfStep(); + + smd.accuracy_estimate = ctx.endgame.ApproximateError(); + smd.accuracy_estimate_user_coords = + static_cast( (ctx.target_sys.DehomogenizePoint(solutions_post_endgame_[soln_ind]) - + ctx.target_sys.DehomogenizePoint(ctx.endgame.template PreviousApproximation())).template lpNorm() ); + smd.cycle_num = ctx.endgame.CycleNumber(); } - void PostEGAction() { ComputePostTrackMetadata(); @@ -795,34 +1529,184 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me */ void ComputePostTrackMetadata() { + ClassifyFiniteAndReal(); ComputeMultiplicities(); + ClassifySingular(); } - void ComputeMultiplicities() + /** + \brief Classify each successful endpoint as finite/infinite, and (when finite) real. + + Uses the same dehomogenize-then-infinity-norm measurement the endgames use for their + `Security::max_norm` divergence test (`System::InfinityNormOfDehomogenized`), compared + against `endpoint_finite_threshold` -- so the metadata can never contradict the + endgame's own verdict. Endpoints the endgame already flagged as diverging + (`GoingToInfinity` / `SecurityMaxNormReached`) are taken as infinite without + recomputation. A finite endpoint is real if the infinity norm of the imaginary parts + of its dehomogenized coordinates is below `real_threshold`. + */ + void ClassifyFiniteAndReal() { - std::vector> multiplicity_indices(num_start_points_); + using std::abs; using std::imag; + const auto& post = this->template Get(); for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) { - if (solution_final_metadata_[ii].endgame_success!=SuccessCode::Success) + auto& smd = solution_final_metadata_[ii]; + + if (smd.endgame_success==SuccessCode::GoingToInfinity || + smd.endgame_success==SuccessCode::SecurityMaxNormReached) + { + smd.is_finite = false; // the endgame already decided this path diverges continue; + } + if (smd.endgame_success!=SuccessCode::Success) + continue; // failed otherwise: leave defaults (not finite/real/singular) - for (decltype(num_start_points_) jj{ii+1}; jj < num_start_points_; ++jj) + auto user_pt = this->TargetSystem().DehomogenizePoint(solutions_post_endgame_[ii]); + + smd.is_finite = + static_cast(user_pt.template lpNorm()) <= post.endpoint_finite_threshold; + + if (smd.is_finite) { - if (solution_final_metadata_[jj].endgame_success!=SuccessCode::Success) - continue; + NumErrorT max_imag{0}; + for (Eigen::Index k{0}; k < user_pt.size(); ++k) + { + NumErrorT a = static_cast(abs(imag(user_pt(k)))); + if (a > max_imag) max_imag = a; + } + smd.is_real = max_imag < post.real_threshold; + } + } + } - if ( (solutions_post_endgame_[ii] - solutions_post_endgame_[jj]).norm() < this->template Get().same_point_tolerance) + /** + \brief Cluster identical endpoints to compute multiplicities. + + Two endpoints are the same point when the infinity norm of the difference of their + *dehomogenized* coordinates is below `final_tolerance * same_point_tolerance_multiplier`. + Comparing dehomogenized (user) coordinates -- not the internal homogenized, on-patch + coordinates, which carry the homogenizing variable and patch scaling -- is essential; + the infinity norm matches the convergence norm the endgames use. Only finite, + successful endpoints are clustered (an at-infinity endpoint has no meaningful + dehomogenized coordinates to compare). + */ + void ComputeMultiplicities() + { + const NumErrorT same_tol = + this->template Get().final_tolerance * + this->template Get().same_point_tolerance_multiplier; + + std::vector> user_pts(num_start_points_); + std::vector eligible(num_start_points_, 0); + for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) + { + auto const& smd = solution_final_metadata_[ii]; + if (smd.endgame_success==SuccessCode::Success && smd.is_finite) + { + user_pts[ii] = this->TargetSystem().DehomogenizePoint(solutions_post_endgame_[ii]); + eligible[ii] = 1; + } + } + + for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) + { + if (!eligible[ii]) continue; + for (decltype(num_start_points_) jj{ii+1}; jj < num_start_points_; ++jj) + { + if (!eligible[jj]) continue; + if ( static_cast((user_pts[ii] - user_pts[jj]).template lpNorm()) < same_tol ) { - multiplicity_indices[ii].push_back(jj); - multiplicity_indices[jj].push_back(ii); ++solution_final_metadata_[ii].multiplicity; ++solution_final_metadata_[jj].multiplicity; + // jj coincides with the earlier ii, so it is a duplicate, not the + // representative; the lowest-index member of a cluster is never marked + // here, so it remains the single representative. + solution_final_metadata_[jj].multiplicity_representative = false; } } } } + /** + \brief Classify each successful endpoint as singular or not. + + Matching Bertini 1: an endpoint is singular if it is the endpoint of multiple paths + (multiplicity > 1), or if the approximation of its condition number (spectral norm, as + estimated by the tracker) exceeds `condition_number_threshold`. + */ + void ClassifySingular() + { + const NumErrorT cond_threshold = this->template Get().condition_number_threshold; + for (decltype(num_start_points_) ii{0}; ii < num_start_points_; ++ii) + { + auto& smd = solution_final_metadata_[ii]; + if (smd.endgame_success!=SuccessCode::Success) + continue; + smd.is_singular = (smd.multiplicity > 1) || (smd.condition_number > cond_threshold); + } + } + + + + /////// + // MPI pack/store helpers (only compiled when BERTINI2_HAVE_MPI is defined) + /////// + +#ifdef BERTINI2_HAVE_MPI + // Pack everything a worker computed for one whole path into the single result message. + parallel::FullPathResult PackFullPathResult(SolnIndT idx) const + { + parallel::FullPathResult r; + r.path_index = idx; + r.pre_endgame_success = solutions_at_endgame_boundary_[idx].success_code; + r.boundary_point = solutions_at_endgame_boundary_[idx].path_point; + r.boundary_stepsize = solutions_at_endgame_boundary_[idx].last_used_stepsize; + r.boundary_precision = solutions_at_endgame_boundary_[idx].precision; + + auto const& smd = solution_final_metadata_[idx]; + r.endgame_success = smd.endgame_success; + r.final_solution = solutions_post_endgame_[idx]; + r.function_residual = smd.function_residual; + r.condition_number = smd.condition_number; + r.newton_residual = smd.newton_residual; + r.final_time_used = smd.final_time_used; + r.accuracy_estimate = smd.accuracy_estimate; + r.accuracy_estimate_user_coords = smd.accuracy_estimate_user_coords; + r.cycle_num = smd.cycle_num; + r.precision_changed = smd.precision_changed; + r.time_of_first_prec_increase = smd.time_of_first_prec_increase; + r.max_precision_used = smd.max_precision_used; + return r; + } + + // Install a whole-path result on the manager. Overwrites cleanly, so re-dispatching a + // crossed path in a later resolve round simply replaces its earlier (crossed) result. + void StoreFullPathResult(parallel::FullPathResult const& r) + { + auto idx = static_cast(r.path_index); + solutions_at_endgame_boundary_[idx] = + EGBoundaryMetaDataT{ r.boundary_point, r.pre_endgame_success, r.boundary_stepsize, r.boundary_precision }; + solutions_post_endgame_[idx] = r.final_solution; + + auto& smd = solution_final_metadata_[idx]; + smd.path_index = idx; + smd.solution_index = idx; + smd.pre_endgame_success = r.pre_endgame_success; + smd.endgame_success = r.endgame_success; + smd.function_residual = r.function_residual; + smd.condition_number = r.condition_number; + smd.newton_residual = r.newton_residual; + smd.final_time_used = r.final_time_used; + smd.accuracy_estimate = r.accuracy_estimate; + smd.accuracy_estimate_user_coords = r.accuracy_estimate_user_coords; + smd.cycle_num = r.cycle_num; + smd.precision_changed = r.precision_changed; + smd.time_of_first_prec_increase = r.time_of_first_prec_increase; + smd.max_precision_used = r.max_precision_used; + } +#endif // BERTINI2_HAVE_MPI /////// @@ -831,6 +1715,9 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me unsigned long long num_start_points_; NumErrorT midpath_retrack_tolerance_; + MidpathCheckReport midpath_report_; ///< populated by EGBoundaryAction; exposed via EndgameBoundaryMetadata() + unsigned start_point_precision_ = DoublePrecision(); ///< precision at which start points are computed; defaults to initial ambient precision (see PreSolveSetup) + bool start_point_precision_set_by_user_ = false; /// observers used during tracking @@ -847,8 +1734,10 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me /// computed data - SolnCont< EGBoundaryMetaDataT > solutions_at_endgame_boundary_; // the BaseRealType is the last used stepsize - SolnCont > solutions_post_endgame_; + SolnCont< EGBoundaryMetaDataT > solutions_at_endgame_boundary_; // the BaseRealT is the last used stepsize + SolnCont > solutions_post_endgame_; + mutable SolnCont > solutions_user_coords_; ///< lazy cache for SolutionsUserCoords; serial post-solve access assumed + mutable bool solutions_user_coords_fresh_ = false; SolnCont solution_final_metadata_; @@ -857,3 +1746,136 @@ std::ostream& operator<<(std::ostream & out, const EGBoundaryMetaData & me } // ns algo } // ns bertini + +// Include output formatters after ZeroDim is fully defined. +// output.hpp includes zero_dim_solve.hpp, so #pragma once prevents re-inclusion +// and the circular dependency is resolved. Any TU that gets zero_dim_solve.hpp +// therefore also gets output.hpp, making WriteMainData/WriteRawData instantiable. +#include "bertini2/nag_algorithms/output.hpp" + +namespace bertini { +namespace algorithm { + +template class SystemManagementP> +inline void +ZeroDim::WriteMainData(std::ostream& out) const +{ + output::Classic::MainData(out, *this); +} + +template class SystemManagementP> +inline void +ZeroDim::WriteRawData(std::ostream& out) const +{ + output::Classic::RawData(out, *this); +} + +} // ns algorithm +} // ns bertini + +// Include config parsers after ZeroDim is fully defined, then provide the +// out-of-line definition of ApplyParsedConfigs. Parsing headers do not include +// zero_dim_solve.hpp, so there is no circular dependency here. +#include "bertini2/io/parsing/settings_parsers.hpp" + +namespace bertini { +namespace algorithm { + +// Injects every element of a std::tuple into `target` via Set. +// Works for any Configured<>-derived target whose typelist contains all Ts. +template +void InjectParsedTuple(Target& target, std::tuple const& t) { + (target.template Set(std::get(t)), ...); +} + +template class SystemManagementP> +void +ZeroDim + ::ApplyParsedConfigs(std::string const& config_str) +{ + using namespace parsing::classic; + + // 1. ZeroDim-owned configs (Tolerances, PostProcessing, ZeroDimConf, AutoRetrack) + using ZDConfs = typename Config::UsedConfigs; + auto zd = ConfigParser::Parse(config_str); + InjectParsedTuple(*this, zd); + // The path variable name comes from ZeroDimConf, so the homotopy must be + // re-formed if the parsed name differs from the one used at construction. + // Only re-run setup in that case: re-running unconditionally re-randomizes + // gamma and the start system for no reason, and (before Homogenize() was made + // idempotent) re-corrupted the prepared target system. + if (!Homotopy().HavePathVariable() + || Homotopy().GetPathVariable()->name() != this->template Get().path_variable_name) + { + DefaultSystemSetup(); + } + + // 2. Tracker — uses positional Setup() rather than Set, so handle explicitly. + using TkConfs = detail::TypeList< + tracking::SteppingConfig, + tracking::NewtonConfig, + tracking::Predictor>; + auto tk = ConfigParser::Parse(config_str); + tracker_.Setup( + std::get(tk), + this->template Get().newton_before_endgame, + this->template Get().path_truncation_threshold, + std::get(tk), + std::get(tk)); + tracker_.PrecisionSetup(PrecisionConfig(Homotopy())); + endgame_.SetTracker(tracker_); // keep endgame's tracker ref consistent after reconfigure + + // 3. Endgame-owned configs — endgame_ is the concrete EndgameType deriving from + // Configured, so Set is available directly. + // AlgoTraits::NeededConfigs is public (unlike EndgameType::Configs). + using EGConfs = typename endgame::AlgoTraits::NeededConfigs; + auto eg = ConfigParser::Parse(config_str); + InjectParsedTuple(endgame_, eg); + + // 4. Midpath config + SetMidpath(ConfigParser::Parse(config_str)); +} + +} // ns algorithm +} // ns bertini + + +// Explicit instantiation declarations — suppress re-instantiation of the six +// production ZeroDim types in every including TU. Definitions live in +// core/src/eti/zero_dim_eti.cpp; see ADR-0014. Other combos (different start +// systems, RefToGiven policy) simply instantiate implicitly as before. +#include "bertini2/endgames.hpp" +#include "bertini2/system/start_systems.hpp" + +namespace bertini{ namespace algorithm{ + +extern template struct ZeroDim::PSEG, System, start_system::TotalDegree>; +extern template struct ZeroDim::Cauchy, System, start_system::TotalDegree>; +extern template struct ZeroDim::PSEG, System, start_system::TotalDegree>; +extern template struct ZeroDim::Cauchy, System, start_system::TotalDegree>; +extern template struct ZeroDim::PSEG, System, start_system::TotalDegree>; +extern template struct ZeroDim::Cauchy, System, start_system::TotalDegree>; + +// the blackbox switch ladder additionally reaches MHomogeneous (CloneGiven) and +// User (RefToGiven) starts; definitions in core/src/eti/zero_dim_blackbox_eti.cpp +extern template struct ZeroDim::PSEG, System, start_system::MHomogeneous>; +extern template struct ZeroDim::Cauchy, System, start_system::MHomogeneous>; +extern template struct ZeroDim::PSEG, System, start_system::MHomogeneous>; +extern template struct ZeroDim::Cauchy, System, start_system::MHomogeneous>; +extern template struct ZeroDim::PSEG, System, start_system::MHomogeneous>; +extern template struct ZeroDim::Cauchy, System, start_system::MHomogeneous>; + +extern template struct ZeroDim::PSEG, System, start_system::User, policy::RefToGiven>; +extern template struct ZeroDim::Cauchy, System, start_system::User, policy::RefToGiven>; +extern template struct ZeroDim::PSEG, System, start_system::User, policy::RefToGiven>; +extern template struct ZeroDim::Cauchy, System, start_system::User, policy::RefToGiven>; +extern template struct ZeroDim::PSEG, System, start_system::User, policy::RefToGiven>; +extern template struct ZeroDim::Cauchy, System, start_system::User, policy::RefToGiven>; + +}} // namespaces diff --git a/core/include/bertini2/nag_datatypes/numerical_irreducible_decomposition.hpp b/core/include/bertini2/nag_datatypes/numerical_irreducible_decomposition.hpp index 80585bb5c..33d597a74 100644 --- a/core/include/bertini2/nag_datatypes/numerical_irreducible_decomposition.hpp +++ b/core/include/bertini2/nag_datatypes/numerical_irreducible_decomposition.hpp @@ -31,6 +31,9 @@ #include "bertini2/nag_datatypes/witness_set.hpp" +#include +#include + namespace bertini { namespace nag_datatype { @@ -41,22 +44,51 @@ namespace bertini { */ template class ObjManagementP = policy::Copy > - class NumericalIrreducibleDecomposition + class NumericalIrreducibleDecomposition { + public: using WS = WitnessSet; using WSCont = std::vector; + private: WSCont finished_witness_sets_; public: + /** + \brief The distinct codimensions which contain at least one component. + */ std::vector NonEmptyCodimensions() const - {} + { + std::vector codims; + for (const auto& w : finished_witness_sets_) + if (std::find(codims.begin(), codims.end(), w.Dimension()) == codims.end()) + codims.push_back(w.Dimension()); + + return codims; + } const WSCont& GetWitnessSets() const { return finished_witness_sets_; } + /** + \brief The number of witness sets stored. Convenience accessor for languages + (e.g. Python) which do not have the std::vector container bound. + */ + typename WSCont::size_type NumWitnessSets() const + { + return finished_witness_sets_.size(); + } + + /** + \brief Get (a const reference to) the i-th witness set. + */ + const WS& GetWitnessSet(typename WSCont::size_type i) const + { + return finished_witness_sets_.at(i); + } + WSCont WitnessSetsOfDim(int dim) { WSCont w_correct_dim; @@ -65,7 +97,7 @@ namespace bertini { w_correct_dim.push_back(w); return w_correct_dim; - } + } }; diff --git a/core/include/bertini2/num_traits.hpp b/core/include/bertini2/num_traits.hpp index f6d00101c..db901f6c8 100644 --- a/core/include/bertini2/num_traits.hpp +++ b/core/include/bertini2/num_traits.hpp @@ -66,7 +66,7 @@ namespace bertini inline static unsigned TolToDigits(double tol) { - return ceil(-log10(tol)); + return static_cast(ceil(-log10(tol))); } inline static @@ -127,19 +127,19 @@ namespace bertini return 10; } - inline + constexpr unsigned DoublePrecision() { return 16; } - inline + constexpr unsigned LowestMultiplePrecision() { return 20; } - - inline + + constexpr unsigned MaxPrecisionAllowed() { return 1000; @@ -200,18 +200,16 @@ namespace bertini { using std::abs; using std::sqrt; - static std::default_random_engine generator; - static std::uniform_real_distribution distribution(-1.0,1.0); - dbl_complex returnme(distribution(generator), distribution(generator)); + static thread_local std::uniform_real_distribution distribution(-1.0,1.0); + dbl_complex returnme(distribution(ThreadEngine()), distribution(ThreadEngine())); return returnme / sqrt( abs(returnme)); } template <> inline dbl_complex RandomUnit() { - static std::default_random_engine generator; - static std::uniform_real_distribution distribution(-1.0,1.0); - dbl_complex returnme(distribution(generator), distribution(generator)); + static thread_local std::uniform_real_distribution distribution(-1.0,1.0); + dbl_complex returnme(distribution(ThreadEngine()), distribution(ThreadEngine())); return returnme / abs(returnme); } @@ -242,12 +240,12 @@ namespace bertini { { inline static unsigned NumDigits() { - return DefaultPrecision(); + return ThreadPrecision(); } inline static unsigned NumFuzzyDigits() { - return DefaultPrecision()-3; + return ThreadPrecision()-3; } inline @@ -279,7 +277,7 @@ namespace bertini { { inline static unsigned NumDigits() { - return DefaultPrecision(); + return ThreadPrecision(); } inline static @@ -304,14 +302,62 @@ namespace bertini { using Complex = mpfr_complex; }; - template <> struct NumTraits + template <> struct NumTraits { - inline static - mpq_rational FromString(std::string const& s) + /** + \brief Parse a decimal string to an exact mpq_rational. + + Handles optional sign, decimal point, and scientific notation (e/E). + Unlike the mpq_rational(string) constructor (which expects "p/q" or integer + format), this accepts decimal strings like "0.647" → 647/1000 exactly. + + GMP treats a leading '0' as an octal prefix when base=0, so leading zeros + are stripped before mpz_int construction. "0.8" → digits "08" → "8". + */ + inline static + mpq_rational FromString(std::string const& str) { - return mpq_rational(s); + std::string s = str; + bool negative = false; + if (!s.empty() && s[0] == '-') { negative = true; s = s.substr(1); } + else if (!s.empty() && s[0] == '+') { s = s.substr(1); } + + int exp_shift = 0; + auto e_pos = s.find_first_of("eE"); + if (e_pos != std::string::npos) { + exp_shift = std::stoi(s.substr(e_pos + 1)); + s = s.substr(0, e_pos); + } + + auto dot_pos = s.find('.'); + int decimal_places = 0; + if (dot_pos != std::string::npos) { + decimal_places = static_cast(s.size()) - static_cast(dot_pos) - 1; + s.erase(dot_pos, 1); + } + + if (s.empty() || s.find_first_not_of('0') == std::string::npos) { + s = "0"; + } else { + s = s.substr(s.find_first_not_of('0')); + } + mpz_int numer(s); + if (negative) numer = -numer; + + int net_exp = decimal_places - exp_shift; + if (net_exp > 0) { + mpz_int denom = 1; + for (int i = 0; i < net_exp; ++i) denom *= 10; + return mpq_rational(numer, denom); + } else if (net_exp < 0) { + mpz_int mult = 1; + for (int i = 0; i < -net_exp; ++i) mult *= 10; + return mpq_rational(numer * mult, 1); + } else { + return mpq_rational(numer, 1); + } } - }; + }; } diff --git a/core/include/bertini2/parallel.hpp b/core/include/bertini2/parallel.hpp index 2e9a6e95b..7c7242875 100644 --- a/core/include/bertini2/parallel.hpp +++ b/core/include/bertini2/parallel.hpp @@ -27,3 +27,9 @@ */ #include "bertini2/parallel/initialize_finalize.hpp" +#include "bertini2/parallel/path_result.hpp" + +#ifdef BERTINI2_HAVE_MPI +#include "bertini2/parallel/manager.hpp" +#include "bertini2/parallel/worker.hpp" +#endif diff --git a/core/include/bertini2/parallel/initialize_finalize.hpp b/core/include/bertini2/parallel/initialize_finalize.hpp index 9dc98691e..e7ae80dd9 100644 --- a/core/include/bertini2/parallel/initialize_finalize.hpp +++ b/core/include/bertini2/parallel/initialize_finalize.hpp @@ -34,12 +34,27 @@ #include "bertini2/logging.hpp" #include "bertini2/io/splash.hpp" +#ifdef BERTINI2_HAVE_MPI +#include "bertini2/parallel/mpi_include.hpp" +#endif + namespace bertini{ namespace parallel{ void Finalize(); - void Initialize(); + void Initialize(); + +#ifdef BERTINI2_HAVE_MPI + MPI_Comm WorldComm(); +#endif + + // These work in both serial and parallel builds. + // In serial builds (no MPI) they return constants matching a single-rank world. + int Rank(); + int Size(); + bool IsManager(); // true iff Rank() == 0 + bool IsWorker(); // true iff Rank() != 0 } diff --git a/core/include/bertini2/parallel/manager.hpp b/core/include/bertini2/parallel/manager.hpp new file mode 100644 index 000000000..f096e8cb9 --- /dev/null +++ b/core/include/bertini2/parallel/manager.hpp @@ -0,0 +1,142 @@ +//This file is part of Bertini 2. +// +//bertini2/parallel/manager.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//bertini2/parallel/manager.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with bertini2/parallel/manager.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file bertini2/parallel/manager.hpp + +\brief Generic manager (rank 0) loop for dynamic-assignment manager-worker MPI parallelism. + +RunManagerLoop uses credit-based dispatch: each worker announces +its capacity (number of tracking threads) at loop start via TAG_CAPACITY; the +manager keeps up to that many tasks outstanding per rank, dispatching a new task +each time a result arrives. Workers are told to stop by receiving a sentinel task, +sent only once that rank has zero tasks outstanding. + +Requirements: + - TaskT must be Boost.MPI-serializable + - ResultT must be Boost.MPI-serializable + - sentinel must be a TaskT value that workers can detect via is_sentinel() + (or be a dedicated out-of-band value) + +For Phase 1 (before-EG), TaskT = std::size_t (path index) with sentinel = +std::numeric_limits::max(). + +For Phase 2 (during-EG), TaskT = Phase2Task with sentinel constructed +via Phase2Task::sentinel(). +*/ + +#pragma once + +#ifdef BERTINI2_HAVE_MPI + +#include "bertini2/parallel/path_result.hpp" +#include "bertini2/parallel/mpi_utils.hpp" + +#include "bertini2/parallel/mpi_include.hpp" + +#include +#include +#include +#include + +namespace bertini { +namespace parallel { + + +/** +\brief Run the manager side of a single tracking phase. + +\param comm MPI communicator (WorldComm()). +\param tasks Queue of TaskT items to distribute; consumed by this call. +\param store_fn Called on rank 0 with each received ResultT. + +All worker ranks must simultaneously be running RunWorkerLoop(). +*/ +template +void RunManagerLoop( + MPI_Comm comm, + std::queue& tasks, + std::function store_fn) +{ + int world_size = 0; + MPI_Comm_size(comm, &world_size); + const int num_workers = world_size - 1; + if (num_workers <= 0) + return; + + TaskT sentinel_value = detail::make_sentinel(TaskT{}); + + // Each worker announces how many tasks it can have in flight at once + // (1 for a serial worker, n_threads for a threaded one). + std::map capacity; // rank -> max tasks in flight + for (int ii = 0; ii < num_workers; ++ii) + { + int cap = 0; + MPI_Status status; + MPI_Recv(&cap, 1, MPI_INT, MPI_ANY_SOURCE, TAG_CAPACITY, comm, &status); + capacity[status.MPI_SOURCE] = (cap >= 1) ? cap : 1; + } + + std::map outstanding; // rank -> tasks dispatched but not yet returned + int active_workers = num_workers; + + // Seed each worker up to its capacity. A rank seeded with zero tasks + // (queue exhausted) is immediately sent the sentinel. + for (auto const& [rank, cap] : capacity) + { + outstanding[rank] = 0; + for (int ii = 0; ii < cap && !tasks.empty(); ++ii) + { + TaskT task = tasks.front(); tasks.pop(); + mpi_send_serialized(comm, rank, TAG_WORK_ITEM, task); + ++outstanding[rank]; + } + if (outstanding[rank] == 0) + { + mpi_send_serialized(comm, rank, TAG_WORK_ITEM, sentinel_value); + --active_workers; + } + } + + // Drain: refill a rank as each result arrives; retire it with a sentinel + // once the queue is empty and its last outstanding task has come back. + while (active_workers > 0) + { + ResultT result; + int worker = mpi_recv_serialized_any(comm, TAG_RESULT, result); + + store_fn(result); + --outstanding[worker]; + + if (!tasks.empty()) + { + TaskT task = tasks.front(); tasks.pop(); + mpi_send_serialized(comm, worker, TAG_WORK_ITEM, task); + ++outstanding[worker]; + } + else if (outstanding[worker] == 0) + { + mpi_send_serialized(comm, worker, TAG_WORK_ITEM, sentinel_value); + --active_workers; + } + } +} + +} // namespace parallel +} // namespace bertini + +#endif // BERTINI2_HAVE_MPI diff --git a/core/include/bertini2/parallel/mpi_include.hpp b/core/include/bertini2/parallel/mpi_include.hpp new file mode 100644 index 000000000..d70ed00cc --- /dev/null +++ b/core/include/bertini2/parallel/mpi_include.hpp @@ -0,0 +1,43 @@ +//This file is part of Bertini 2. +// +//bertini2/parallel/mpi_include.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//bertini2/parallel/mpi_include.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with bertini2/parallel/mpi_include.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file bertini2/parallel/mpi_include.hpp + +\brief Single include point for the MPI C API. + +Bertini 2 uses only the MPI **C** API. Including `` directly from C++ otherwise drags in the +deprecated MPI **C++** bindings (the `MPI::` namespace, `mpicxx.h`). MPICH compiles those in by +default, and they fail to build under modern C++ (notably clang) -- so the project built against +OpenMPI but not Homebrew MPICH on macOS. + +Defining `MPICH_SKIP_MPICXX` / `OMPI_SKIP_MPICXX` before `` pulls in only the C API. These are +also defined on the compile command line by CMake (`ENABLE_MPI` block); having them here too is belt +and suspenders, so the guard holds even if the build-system definition fails to reach a translation +unit. Always include this header instead of `` directly. +*/ + +#pragma once + +#ifndef MPICH_SKIP_MPICXX +#define MPICH_SKIP_MPICXX +#endif +#ifndef OMPI_SKIP_MPICXX +#define OMPI_SKIP_MPICXX +#endif + +#include diff --git a/core/include/bertini2/parallel/mpi_utils.hpp b/core/include/bertini2/parallel/mpi_utils.hpp new file mode 100644 index 000000000..6e0c13da8 --- /dev/null +++ b/core/include/bertini2/parallel/mpi_utils.hpp @@ -0,0 +1,169 @@ +//This file is part of Bertini 2. +// +//bertini2/parallel/mpi_utils.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//bertini2/parallel/mpi_utils.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with bertini2/parallel/mpi_utils.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file bertini2/parallel/mpi_utils.hpp + +\brief Serialization-aware MPI send/recv helpers using plain C MPI + Boost.Serialization. + +Boost.MPI's send/recv automatically invoked Boost.Serialization for complex types. +These helpers replicate that behavior using the C MPI API directly, removing the +Boost::mpi component dependency while keeping all existing serialize() methods. + +Pattern: serialize to Boost binary_oarchive → single MPI_Send(MPI_BYTE). + MPI_Probe + MPI_Get_count → MPI_Recv(MPI_BYTE) → deserialize. + +The probe-before-receive approach avoids the two-message (size + data) race condition +that would arise with MPI_ANY_SOURCE. +*/ + +#pragma once + +#ifdef BERTINI2_HAVE_MPI + +#include "bertini2/parallel/mpi_include.hpp" + +#include +#include + +#include +#include + +namespace bertini { +namespace parallel { + + +/** +\brief Serialize \p obj via Boost binary archive and send as a single MPI_BYTE message. +*/ +template +void mpi_send_serialized(MPI_Comm comm, int dest, int tag, T const& obj) +{ + std::ostringstream oss; + { + boost::archive::binary_oarchive oa(oss); + oa << obj; + } + std::string const buf = oss.str(); + MPI_Send(buf.data(), static_cast(buf.size()), MPI_BYTE, dest, tag, comm); +} + + +/** +\brief Receive a serialized message from any source, deserialize into \p obj. + +Uses MPI_Probe to determine the source and byte count before receiving, so +this is safe to call with MPI_ANY_SOURCE even when the message is a single send. + +\return The rank of the sending process. +*/ +template +int mpi_recv_serialized_any(MPI_Comm comm, int tag, T& obj) +{ + MPI_Status probe_status; + MPI_Probe(MPI_ANY_SOURCE, tag, comm, &probe_status); + + int source = probe_status.MPI_SOURCE; + int count = 0; + MPI_Get_count(&probe_status, MPI_BYTE, &count); + + std::string buf(static_cast(count), '\0'); + MPI_Status recv_status; + MPI_Recv(buf.data(), count, MPI_BYTE, source, tag, comm, &recv_status); + + std::istringstream iss(buf); + boost::archive::binary_iarchive ia(iss); + ia >> obj; + + return source; +} + + +/** +\brief Receive a serialized message from a specific source, deserialize into \p obj. +*/ +template +void mpi_recv_serialized(MPI_Comm comm, int source, int tag, T& obj) +{ + MPI_Status probe_status; + MPI_Probe(source, tag, comm, &probe_status); + + int count = 0; + MPI_Get_count(&probe_status, MPI_BYTE, &count); + + std::string buf(static_cast(count), '\0'); + MPI_Status recv_status; + MPI_Recv(buf.data(), count, MPI_BYTE, source, tag, comm, &recv_status); + + std::istringstream iss(buf); + boost::archive::binary_iarchive ia(iss); + ia >> obj; +} + + +/** +\brief Broadcast a std::string from \p root to all ranks in \p comm. + +Sends length then content using plain MPI — no Boost involved. +On non-root ranks, \p s is overwritten with the broadcast value. +*/ +inline void mpi_broadcast_string(MPI_Comm comm, std::string& s, int root) +{ + int len = static_cast(s.size()); + MPI_Bcast(&len, 1, MPI_INT, root, comm); + s.resize(static_cast(len)); + MPI_Bcast(s.data(), len, MPI_CHAR, root, comm); +} + + +/** +\brief Broadcast a Boost-serializable object from \p root to all ranks in \p comm. + +\p root serializes \p obj and broadcasts the bytes; every other rank deserializes into \p obj +(overwriting it). Used to make \p root the single authoritative source of an object (e.g. the +homotopy / start system) rather than having each rank re-derive its own copy. +*/ +template +void mpi_broadcast_serialized(MPI_Comm comm, T& obj, int root) +{ + int rank = 0; + MPI_Comm_rank(comm, &rank); + + std::string s; + if (rank == root) + { + std::ostringstream oss; + boost::archive::binary_oarchive oa(oss); + oa << obj; + s = oss.str(); + } + + mpi_broadcast_string(comm, s, root); + + if (rank != root) + { + std::istringstream iss(s); + boost::archive::binary_iarchive ia(iss); + ia >> obj; + } +} + + +} // namespace parallel +} // namespace bertini + +#endif // BERTINI2_HAVE_MPI diff --git a/core/include/bertini2/parallel/path_result.hpp b/core/include/bertini2/parallel/path_result.hpp new file mode 100644 index 000000000..5b785e11a --- /dev/null +++ b/core/include/bertini2/parallel/path_result.hpp @@ -0,0 +1,178 @@ +//This file is part of Bertini 2. +// +//bertini2/parallel/path_result.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//bertini2/parallel/path_result.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with bertini2/parallel/path_result.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file bertini2/parallel/path_result.hpp + +\brief Result struct used in the manager-worker MPI protocol for ZeroDim. + +In the unified speculative-full-path model a worker executes one WHOLE path (pre-endgame + +endgame), so the protocol is a single task/result pair: + task = SolnIndT path index + result = FullPathResult (boundary data + final solution + endgame metadata) + +FullPathResult carries Boost.Serialization support so Boost.MPI can transmit it via the existing +serialization for Eigen vectors and arbitrary-precision types. +*/ + +#pragma once + +#include +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/eigen_extensions.hpp" // defines Vec and sets up Eigen serialization plugin +#include "bertini2/mpfr_extensions.hpp" // Boost.Serialization for mpfr_float, mpc_complex +#include "bertini2/common/config.hpp" // SuccessCode (needs Vec and first) + +#include + +namespace bertini { +namespace parallel { + +constexpr int TAG_WORK_ITEM = 1; +constexpr int TAG_RESULT = 2; +constexpr int TAG_CAPACITY = 3; // worker -> manager: int, max tasks in flight on that rank + + + +/** +\brief Work item for the speculative-full-path model: a path index plus its start point. + +Rank 0 computes start points authoritatively and ships them to workers, so a worker never derives +its own (that, with authoritative pi, is what keeps start points -- and hence whole tracks -- +identical across serial and distributed runs). +*/ +template +struct StartPointTask +{ + using SolnIndT = std::size_t; + + SolnIndT path_index = std::numeric_limits::max(); // max = sentinel + Vec start_point; + + bool is_sentinel() const + { + return path_index == std::numeric_limits::max(); + } + + static StartPointTask sentinel() + { + return StartPointTask{}; // default path_index == max + } + + template + void serialize(Archive& ar, unsigned const) + { + ar & path_index; + ar & start_point; + } +}; + + +/** +\brief Result of executing one WHOLE path: start -> endgame boundary -> target. + +The single result type for the speculative-full-path model. A worker carries a path through both +the pre-endgame tracking and the endgame, so this bundles the boundary data (needed for the manager's +midpath/crossing check) together with the final solution and all endgame metadata. The work item +that produces it is a StartPointTask (path index + rank 0's start point). +*/ +template +struct FullPathResult +{ + using SolnIndT = std::size_t; + using RealT = typename NumTraits::Real; + + SolnIndT path_index = 0; + + // boundary (pre-endgame) data + SuccessCode pre_endgame_success = SuccessCode::NeverStarted; + Vec boundary_point; + RealT boundary_stepsize = RealT(0); + unsigned boundary_precision = DoublePrecision(); + + // endgame data + SuccessCode endgame_success = SuccessCode::NeverStarted; + Vec final_solution; + double function_residual = 0; + double condition_number = 0; + double newton_residual = 0; + ComplexT final_time_used; + double accuracy_estimate = 0; + double accuracy_estimate_user_coords = 0; + unsigned cycle_num = 0; + + // precision metadata (spans the whole path) + bool precision_changed = false; + ComplexT time_of_first_prec_increase; + unsigned max_precision_used = 0; + + template + void serialize(Archive& ar, unsigned const) + { + ar & path_index; + ar & pre_endgame_success; + ar & boundary_point; + ar & boundary_stepsize; + ar & boundary_precision; + ar & endgame_success; + ar & final_solution; + ar & function_residual; + ar & condition_number; + ar & newton_residual; + ar & final_time_used; + ar & accuracy_estimate; + ar & accuracy_estimate_user_coords; + ar & cycle_num; + ar & precision_changed; + ar & time_of_first_prec_increase; + ar & max_precision_used; + } +}; + +namespace detail { + +// Sentinel detection and factory for the work-item type. The work item is a StartPointTask whose +// max path_index marks "no more work". (The plain-size_t overloads remain for any other caller.) + +inline bool is_sentinel(std::size_t v) +{ + return v == std::numeric_limits::max(); +} + +inline std::size_t make_sentinel(std::size_t) +{ + return std::numeric_limits::max(); +} + +template +bool is_sentinel(StartPointTask const& t) +{ + return t.is_sentinel(); +} + +template +StartPointTask make_sentinel(StartPointTask const&) +{ + return StartPointTask::sentinel(); +} + +} // namespace detail + +} // namespace parallel +} // namespace bertini diff --git a/core/include/bertini2/parallel/thread_pool.hpp b/core/include/bertini2/parallel/thread_pool.hpp new file mode 100644 index 000000000..d3e8bd8f9 --- /dev/null +++ b/core/include/bertini2/parallel/thread_pool.hpp @@ -0,0 +1,198 @@ +//This file is part of Bertini 2. +// +//bertini2/parallel/thread_pool.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//bertini2/parallel/thread_pool.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with bertini2/parallel/thread_pool.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file bertini2/parallel/thread_pool.hpp + +\brief Thread-safe queue and worker thread pool for MPI+thread hybrid parallelism. + +ThreadSafeQueue: blocking FIFO backed by std::deque + std::mutex + std::condition_variable. + +WorkerThreadPool: spawns N tracking threads. Each +thread calls StateFactory() once at startup to build its own self-contained tracking state +(typically a struct holding a System copy and a Tracker copy that references that System). +The MPI communication thread submits tasks via submit() and collects results via collect(). +All MPI calls remain on the main thread (compatible with MPI_THREAD_FUNNELED). + +The StateFactory-based design avoids the "tracker holds a reference to an external System" +problem: the factory returns a heap-owned state struct (unique_ptr), so the System copy and +the Tracker copy that references it live together — at a stable address — for the lifetime +of the thread. + +Example: + struct TrackState { + System sys; + AMPTracker tracker; + }; + auto factory = [&]() { + auto s = std::unique_ptr(new TrackState{ base_system, base_tracker }); + s->tracker.SetSystem(s->sys); // repoint the copy at its own System + SetThreadPrecision(DefaultPrecision()); + return s; + }; + auto track_fn = [&](std::unique_ptr& state, SolnIndT const& idx) -> BeforeResult { + // ... track using state->tracker ... + }; + WorkerThreadPool + pool(n_threads, factory, track_fn); +*/ + +#pragma once + +#ifdef BERTINI2_HAVE_MPI + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace bertini { +namespace parallel { + + +template +class ThreadSafeQueue +{ + std::deque queue_; + std::mutex mutex_; + std::condition_variable cv_; + +public: + void push(T item) + { + { + std::lock_guard lock(mutex_); + queue_.push_back(std::move(item)); + } + cv_.notify_one(); + } + + T pop() + { + std::unique_lock lock(mutex_); + cv_.wait(lock, [this]{ return !queue_.empty(); }); + T item = std::move(queue_.front()); + queue_.pop_front(); + return item; + } + + std::optional try_pop() + { + std::lock_guard lock(mutex_); + if (queue_.empty()) + return std::nullopt; + T item = std::move(queue_.front()); + queue_.pop_front(); + return item; + } +}; + + +struct PoolShutdownSentinel {}; + + +/** +\brief Thread pool for parallel path tracking within a single MPI worker rank. + +\tparam TaskT Task type received from the MPI manager. +\tparam ResultT Result type sent back after tracking. +\tparam StateFactory Callable () -> StateT: constructs a per-thread state struct + (e.g. containing a System copy + Tracker copy) once at thread startup. +\tparam TrackFn Callable (StateT&, TaskT const&) -> ResultT: tracks one path using + the thread-local state. + +The StateFactory approach sidesteps the "tracker holds a reference to an external System" +issue: each thread owns its state struct, so System and Tracker lifetimes are co-managed. +*/ +template +class WorkerThreadPool +{ + using StateT = std::invoke_result_t; + using WorkItem = std::variant; + + ThreadSafeQueue task_queue_; + ThreadSafeQueue result_queue_; + std::vector threads_; + int n_threads_; + +public: + WorkerThreadPool(int n_threads, StateFactory state_factory, TrackFn track_fn) + : n_threads_(n_threads) + { + threads_.reserve(n_threads); + for (int i = 0; i < n_threads; ++i) + { + threads_.emplace_back([this, state_factory, track_fn]() mutable + { + // Build thread-local state once (System copy + Tracker copy). + StateT state = state_factory(); + + while (true) + { + WorkItem item = task_queue_.pop(); + + if (std::holds_alternative(item)) + break; + + TaskT const& task = std::get(item); + ResultT result = track_fn(state, task); + result_queue_.push(std::move(result)); + } + }); + } + } + + void submit(TaskT task) + { + task_queue_.push(WorkItem{std::move(task)}); + } + + // Block until one result is available, then return it. + ResultT collect() + { + return result_queue_.pop(); + } + + // Non-blocking: return a result if one is ready, otherwise std::nullopt. + std::optional try_collect() + { + return result_queue_.try_pop(); + } + + // Signal all threads to exit and join. Call only after all submitted tasks + // have been collected (result_queue_ empty). + void shutdown() + { + for (int i = 0; i < n_threads_; ++i) + task_queue_.push(WorkItem{PoolShutdownSentinel{}}); + for (auto& t : threads_) + t.join(); + threads_.clear(); + } +}; + + + +} // namespace parallel +} // namespace bertini + +#endif // BERTINI2_HAVE_MPI diff --git a/core/include/bertini2/parallel/worker.hpp b/core/include/bertini2/parallel/worker.hpp new file mode 100644 index 000000000..18505735c --- /dev/null +++ b/core/include/bertini2/parallel/worker.hpp @@ -0,0 +1,197 @@ +//This file is part of Bertini 2. +// +//bertini2/parallel/worker.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//bertini2/parallel/worker.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with bertini2/parallel/worker.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file bertini2/parallel/worker.hpp + +\brief Worker-side loops for dynamic-assignment manager-worker MPI parallelism. + +RunWorkerLoop: single-threaded worker. Receives tasks from rank 0, +calls track_fn, sends results back. Exits on sentinel. + +RunWorkerLoopThreaded: multi-threaded worker. Spawns a WorkerThreadPool +of n_threads tracking threads. The MPI communication thread (main) receives tasks one at +a time (manager protocol unchanged) and submits them to the pool. Results are collected +from the pool and sent back as they complete. + +Thread safety: all MPI calls remain on the main thread (MPI_THREAD_FUNNELED). +Each tracking thread owns its state (System copy + Tracker copy) built by state_factory. +*/ + +#pragma once + +#ifdef BERTINI2_HAVE_MPI + +#include "bertini2/parallel/path_result.hpp" +#include "bertini2/parallel/mpi_utils.hpp" +#include "bertini2/parallel/thread_pool.hpp" + +#include "bertini2/parallel/mpi_include.hpp" + +#include +#include // std::getenv +#include +#include +#include +#include + +namespace bertini { +namespace parallel { + + +/** +\brief Run the worker side of a single tracking phase (single-threaded). + +\param comm MPI communicator. +\param track_fn Called with each TaskT; performs tracking (stores results locally). +\param pack_fn Called with each TaskT after track_fn; returns a ResultT to send. +*/ +template +void RunWorkerLoop( + MPI_Comm comm, + std::function track_fn, + std::function pack_fn) +{ + // Announce capacity: a serial worker can have exactly one task in flight. + int capacity = 1; + MPI_Send(&capacity, 1, MPI_INT, 0, TAG_CAPACITY, comm); + + while (true) + { + TaskT task; + mpi_recv_serialized(comm, 0, TAG_WORK_ITEM, task); + + if (detail::is_sentinel(task)) + break; + + track_fn(task); + + ResultT result = pack_fn(task); + mpi_send_serialized(comm, 0, TAG_RESULT, result); + } +} + + +/** +\brief Run the worker side of a single tracking phase with a per-thread tracker pool. + +Spawns n_threads tracking threads, announces a capacity of n_threads to the manager +(which keeps that many tasks outstanding on this rank — see RunManagerLoop), then +runs an event loop on the main (MPI) thread: + + - forward any completed results from the pool to the manager, + - MPI_Iprobe for an incoming task; if present, receive and submit it to the pool, + - sleep briefly when there was nothing to do. + +The event loop never blocks in MPI_Recv while results are pending — a blocking +receive here would deadlock: the manager only sends the next task after receiving +a result, and the result would be stuck in the local queue. + +The manager sends the sentinel only when this rank has zero tasks outstanding, so +after the sentinel arrives there is nothing left to drain (the loop condition +covers the defensive case regardless). + +All MPI calls occur on this (main) thread — compatible with MPI_THREAD_FUNNELED. + +\param comm MPI communicator. +\param state_factory Callable () -> StateT: called once per thread at startup to build + a self-contained tracking state (e.g. {System copy, Tracker copy}). +\param track_fn Callable (StateT&, TaskT const&) -> ResultT: tracks one path. +\param n_threads Number of tracking threads. Must be >= 1. +*/ +template +void RunWorkerLoopThreaded( + MPI_Comm comm, + StateFactory state_factory, + TrackFn track_fn, + int n_threads) +{ + int capacity = n_threads; + MPI_Send(&capacity, 1, MPI_INT, 0, TAG_CAPACITY, comm); + + WorkerThreadPool pool(n_threads, state_factory, track_fn); + + int in_flight = 0; // tasks submitted to the pool but not yet collected + bool got_sentinel = false; + + while (!got_sentinel || in_flight > 0) + { + bool did_work = false; + + // Forward all completed results to the manager. + while (auto opt = pool.try_collect()) + { + mpi_send_serialized(comm, 0, TAG_RESULT, *opt); + --in_flight; + did_work = true; + } + + // Non-blocking check for an incoming task (or the sentinel). + if (!got_sentinel) + { + int flag = 0; + MPI_Status status; + MPI_Iprobe(0, TAG_WORK_ITEM, comm, &flag, &status); + if (flag) + { + TaskT task; + mpi_recv_serialized(comm, 0, TAG_WORK_ITEM, task); + + if (detail::is_sentinel(task)) + got_sentinel = true; + else + { + pool.submit(std::move(task)); + ++in_flight; + } + did_work = true; + } + } + + // Idle: nothing arrived and nothing finished. Tracking a path takes + // milliseconds to seconds, so a short sleep costs nothing measurable. + if (!did_work) + std::this_thread::sleep_for(std::chrono::microseconds(200)); + } + + pool.shutdown(); +} + + +/** +\brief Read the thread count for a worker rank from OMP_NUM_THREADS. + +Returns the value of OMP_NUM_THREADS if set and >= 1, otherwise 1 (serial). +HPC schedulers (SLURM) set OMP_NUM_THREADS automatically from --cpus-per-task. +*/ +inline int WorkerThreadCount() +{ + const char* env = std::getenv("OMP_NUM_THREADS"); + if (env) + { + int n = std::atoi(env); + if (n >= 1) + return n; + } + return 1; +} + + +} // namespace parallel +} // namespace bertini + +#endif // BERTINI2_HAVE_MPI diff --git a/core/include/bertini2/pool/pool.hpp b/core/include/bertini2/pool/pool.hpp index 5d0c13fd0..6d807063c 100644 --- a/core/include/bertini2/pool/pool.hpp +++ b/core/include/bertini2/pool/pool.hpp @@ -92,9 +92,6 @@ namespace bertini { static HeldType Make(Ts&& ...ts ) { - ObjT* obj = new ObjT(ts...); - - // held_data_.push_back(PointerPolicy::DefaultConstructed()); auto thing = std::shared_ptr(new ObjT(ts...)); held_data_.push_back(thing); return held_data_.back(); diff --git a/core/include/bertini2/random.hpp b/core/include/bertini2/random.hpp index 9c414a970..886a3cfe2 100644 --- a/core/include/bertini2/random.hpp +++ b/core/include/bertini2/random.hpp @@ -36,10 +36,52 @@ #include "bertini2/mpfr_complex.hpp" #include +#include +#include namespace bertini { + + /** + Returns this thread's canonical mt19937 engine. Every random draw of every + type routes through here — integer/rational (RandomInt/RandomRat), double + (rand_complex/RandReal), and multiprecision (RandomMp and everything built on + it) — so SetGlobalSeed()/ReseedThisThread() control them all uniformly. + */ + std::mt19937& ThreadEngine(); + + /** + Set the global RNG seed. seed == 0 draws from std::random_device and stores + the effective (non-zero) seed so it can be retrieved and reproduced later. + Must be called before system construction (gamma, patch, TD-constants) to make + those setup draws deterministic. + */ + void SetGlobalSeed(unsigned long seed); + + /** + Returns the effective global seed. If SetGlobalSeed has never been called, draws + from entropy on first call and caches the result. + */ + unsigned long GetGlobalSeed(); + + /** + Reseed this thread's engine deterministically from the global seed mixed with + stream_key. Call at the top of each TrackSinglePath* with soln_ind as the key + so per-step random draws (condition-number probe, PSEG rand-vector) are + path-indexed and mode-independent. + */ + void ReseedThisThread(uint64_t stream_key); + + /** + Derive a distinct, deterministic child seed for a worker rank from the global (master) seed. + The MPI manager computes one per worker and hands it over; the worker calls SetGlobalSeed(child), + giving every process its own non-overlapping deterministic stream -- all reproducible from the one + user seed, and no two processes ever generate the same random value. + */ + unsigned long DerivedWorkerSeed(uint64_t worker_index); + + /** Generate a random integer number between -10^digits and 10^digits */ @@ -48,22 +90,20 @@ namespace bertini mpz_int RandomInt() { using namespace boost::random; - static mt19937 mt; - static uniform_int_distribution ui(-(mpz_int(1) << digits*1000L/301L), mpz_int(1) << digits*1000L/301L); - return ui(mt); + static thread_local uniform_int_distribution ui(-(mpz_int(1) << digits*1000L/301L), mpz_int(1) << digits*1000L/301L); + return ui(ThreadEngine()); } - - + + /** Generate a random rational number with numerator and denomenator between -10^digits and 10^digits */ template mpq_rational RandomRat() { - using namespace boost::random; - static mt19937 mt; - static uniform_int_distribution ui(-(mpz_int(1) << digits*1000L/301L), mpz_int(1) << digits*1000L/301L); - return mpq_rational(ui(mt),ui(mt)); + using namespace boost::random; + static thread_local uniform_int_distribution ui(-(mpz_int(1) << digits*1000L/301L), mpz_int(1) << digits*1000L/301L); + return mpq_rational(ui(ThreadEngine()), ui(ThreadEngine())); } @@ -74,15 +114,19 @@ namespace bertini */ template mpfr_float RandomMp() - { + { using namespace boost::multiprecision; using namespace boost::random; - static uniform_real_distribution, et_on> > distribution(0,1); - static independent_bits_engine bit_generator; + static thread_local uniform_real_distribution, et_on> > distribution(0,1); - mpfr_float a{distribution(bit_generator)}; + // Draw from the single per-thread engine shared by every random type + // (RandomInt/RandomRat, the double-typed draws, and now the multiprecision + // ones), so SetGlobalSeed()/ReseedThisThread() control them all uniformly. + // The distribution fills the full mp mantissa from the 32-bit engine via + // generate_canonical. + mpfr_float a{distribution(ThreadEngine())}; return a; } @@ -167,11 +211,11 @@ using bertini::RandomMp; inline void RandomRealAssign(complex & a, unsigned num_digits) { - auto cached = DefaultPrecision(); - DefaultPrecision(num_digits); + auto cached = ThreadPrecision(); + SetThreadPrecision(num_digits); complex temp(RandomMp(mpfr_float(-1),mpfr_float(1),num_digits)); // ,0 a.swap(temp); - DefaultPrecision(cached); + SetThreadPrecision(cached); } /** @@ -187,10 +231,10 @@ using bertini::RandomMp; */ inline complex RandomReal(unsigned num_digits) { - auto cached = DefaultPrecision(); - DefaultPrecision(num_digits); + auto cached = ThreadPrecision(); + SetThreadPrecision(num_digits); auto result = complex(RandomMp(mpfr_float(-1),mpfr_float(1),num_digits));// ,0 - DefaultPrecision(cached); + SetThreadPrecision(cached); return result; } @@ -215,7 +259,7 @@ using bertini::RandomMp; inline complex rand_unit() { complex returnme( RandomMp(mpfr_float(-1),mpfr_float(1)), RandomMp(mpfr_float(-1),mpfr_float(1)) ); - return returnme / sqrt( abs(returnme)); + return returnme / abs(returnme); // normalize to modulus 1 (NOT sqrt(abs), which left modulus sqrt|z|) } inline complex RandomUnit() @@ -226,12 +270,12 @@ using bertini::RandomMp; inline void rand_assign(complex & a, unsigned num_digits) { - auto cached = DefaultPrecision(); - DefaultPrecision(num_digits); + auto cached = ThreadPrecision(); + SetThreadPrecision(num_digits); mpfr_complex temp( RandomMp(num_digits), RandomMp(num_digits) ); a = std::move(temp); - DefaultPrecision(cached); + SetThreadPrecision(cached); } inline @@ -251,13 +295,13 @@ using bertini::RandomMp; inline void RandomUnitAssign(complex & a, unsigned num_digits) { - auto cached = DefaultPrecision(); - DefaultPrecision(num_digits); + auto cached = ThreadPrecision(); + SetThreadPrecision(num_digits); a.precision(num_digits); complex temp(RandomMp(num_digits),RandomMp(num_digits)); - a = std::move(temp/sqrt(abs(temp))); - DefaultPrecision(cached); + a = std::move(temp/abs(temp)); // normalize to modulus 1 (NOT sqrt(abs)) + SetThreadPrecision(cached); } inline diff --git a/core/include/bertini2/system/blocks/blend_block.hpp b/core/include/bertini2/system/blocks/blend_block.hpp new file mode 100644 index 000000000..069d78111 --- /dev/null +++ b/core/include/bertini2/system/blocks/blend_block.hpp @@ -0,0 +1,381 @@ +//This file is part of Bertini 2. +// +//blend_block.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//blend_block.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with blend_block.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file bertini2/system/blocks/blend_block.hpp + +\brief A System evaluation block that linearly combines whole Systems with t-coefficients. + +This is how a coupled / start-target homotopy is represented inside the block-composed +System: a blend + + H(x,t) = sum_i c_i(t) * f_i(x) + +of operand Systems f_i (e.g. an SLP polynomial target and a products-of-linears MHom +start) weighted by coefficient functions c_i(t) that are ordinary function-tree nodes in +the path variable t -- so (1-t), gamma*t, and later t^lifting all work, and the +time-derivative is exact via Node::Differentiate(t): + + dH/dt = sum_i c_i'(t) * f_i(x) + +(the operand Systems are autonomous in t, so df_i/dt = 0). The Jacobian is +sum_i c_i(t) * (df_i/dx). + +Only the operands' NATURAL functions are blended; the homotopy System that owns this +block carries the (shared) patch, appended once after the block -- so patches are not +double-counted or scaled. + +Templated on the system type so that `System` can remain a forward declaration in the +Block variant (a System contains a Block which contains a BlendBlock which holds +shared_ptr -- the template parameter makes `System` a dependent name, so +its member functions are only required complete at instantiation, breaking the cycle). +Operands are held by shared_ptr, never by value, for the same reason. + +\note Evaluating a coefficient node sets the shared path-variable node's value (mutable +node state); per-thread tracking must Clone the homotopy, as it already must for Systems. +The operand Systems are shared_ptr; deep-cloning them per thread is a +follow-up (block-operand Clone semantics). +*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/eigen_extensions.hpp" +#include "bertini2/function_tree.hpp" +#include "bertini2/system/blocks/describe.hpp" + +namespace bertini { +namespace blocks { + +template +class BlendBlock +{ +public: + using Nd = std::shared_ptr; + using Var = std::shared_ptr; + using OperandPtr = std::shared_ptr; + + BlendBlock() : precision_(DefaultPrecision()) {} + + /** + \param path_variable The shared path variable t the coefficients are functions of. + \param coefficients One coefficient node c_i(t) per operand. + \param operands The Systems being blended; all must share NumNaturalFunctions(). + */ + BlendBlock(Var path_variable, std::vector coefficients, std::vector operands) + : path_variable_(std::move(path_variable)), + coefficients_(std::move(coefficients)), + operands_(std::move(operands)), + precision_(DefaultPrecision()) + { + assert(!operands_.empty() && "BlendBlock needs at least one operand"); + assert(coefficients_.size() == operands_.size() && "one coefficient per operand"); + derivative_coefficients_.reserve(coefficients_.size()); + for (auto const& c : coefficients_) + derivative_coefficients_.push_back(c->Differentiate(path_variable_)); + } + + // Memory-isolating copy (ADR-0027): each operand System is deep-copied via the + // System copy constructor, which shares its immutable node DAG and compiled SLP Program but + // gives it its own per-thread evaluation Memory. The coefficient-system cache is reset (rebuilt + // lazily per copy). The coefficient nodes / path variable are shared --- they are never written + // during evaluation. This lets path tracking Clone a System into per-thread copies that share + // no mutable state. + BlendBlock(BlendBlock const& other) + : path_variable_(other.path_variable_), + coefficients_(other.coefficients_), + derivative_coefficients_(other.derivative_coefficients_), + precision_(other.precision_) + { + operands_.reserve(other.operands_.size()); + for (auto const& op : other.operands_) + operands_.push_back(std::make_shared(*op)); + // coefficient_system_ deliberately left null: rebuilt lazily per copy + } + + BlendBlock& operator=(BlendBlock const& other) + { + if (this != &other) + { + path_variable_ = other.path_variable_; + coefficients_ = other.coefficients_; + derivative_coefficients_ = other.derivative_coefficients_; + precision_ = other.precision_; + operands_.clear(); + operands_.reserve(other.operands_.size()); + for (auto const& op : other.operands_) + operands_.push_back(std::make_shared(*op)); + coefficient_system_.reset(); + } + return *this; + } + + BlendBlock(BlendBlock&&) = default; + BlendBlock& operator=(BlendBlock&&) = default; + + /// The number of (natural) functions the blend contributes; the owning System adds any patch. + size_t NumFunctions() const + { + return operands_.empty() ? 0 : operands_.front()->NumNaturalFunctions(); + } + + /// Accessors for the function-tree expansion (System::ExpandToFunctionTree): the blend is + /// H = sum_i coefficients_[i](t) * operands_[i], so the expansion needs the coefficient nodes, + /// the operand systems (each itself expanded), and the shared path variable. + std::vector const& Coefficients() const { return coefficients_; } + std::vector const& Operands() const { return operands_; } + Var const& PathVariable() const { return path_variable_; } + + /// Human-facing description: terse shows the blend `f_a..f_b = c_0(t)*A + c_1(t)*B`, with the + /// coefficient nodes shown symbolically and the operands labelled A, B, ...; verbose lists each + /// operand's functions indented. + void Describe(std::ostream& out, size_t& row, VariableGroup const& /*vars*/, bool verbose) const + { + const size_t k = NumFunctions(); + out << " "; + describe_detail::PrintRowLabel(out, row, k); + out << " = "; + for (size_t i = 0; i < coefficients_.size(); ++i) + out << (i ? " + " : "") << "(" << coefficients_[i] << ")*" + << static_cast('A' + (i < 26 ? i : 25)); + out << " (blend of " << operands_.size() << " systems)\n"; + row += k; + if (verbose) + for (size_t i = 0; i < operands_.size(); ++i) + { + const char label = static_cast('A' + (i < 26 ? i : 25)); + out << " " << label << ":\n"; + auto f = operands_[i]->NaturalFunctionsAsNodes(); + for (size_t j = 0; j < f.size(); ++j) + out << " " << label << "_" << j << " = " << f[j] << "\n"; + } + } + + /// The blend c_0(t)f_0 + c_1(t)f_1 + ... has, per function, the max degree of its operands + /// in the space variables (the t-coefficients are constant in space). + std::vector Degrees() const + { + std::vector d(NumFunctions(), 0); + for (auto const& op : operands_) + { + auto od = op->Degrees(); + for (size_t i = 0; i < d.size() && i < od.size(); ++i) d[i] = std::max(d[i], od[i]); + } + return d; + } + std::vector Degrees(VariableGroup const& vars) const + { + std::vector d(NumFunctions(), 0); + for (auto const& op : operands_) + { + auto od = op->Degrees(vars); + for (size_t i = 0; i < d.size() && i < od.size(); ++i) d[i] = std::max(d[i], od[i]); + } + return d; + } + + // A blend is the coupling homotopy, built from already-prepared (homogenized) operands; its + // operands are shared_ptr and cannot be mutated, so Homogenize is a no-op. It + // is homogeneous/polynomial iff all its operands are. + void Homogenize(VariableGroup const&, std::shared_ptr const&) {} + bool IsHomogeneous(VariableGroup const&) const + { + for (auto const& op : operands_) if (!op->IsHomogeneous()) return false; + return true; + } + bool IsPolynomial(VariableGroup const&) const + { + for (auto const& op : operands_) if (!op->IsPolynomial()) return false; + return true; + } + + bool DependsOnPathVariable() const { return true; } + + bool HasConstantJacobian() const { return false; } + + /// Analytic block: nothing symbolic to differentiate. + void Differentiate() const {} + + unsigned Precision() const { return precision_; } + + void Precision(unsigned new_precision) const + { + for (auto const& op : operands_) + op->precision(new_precision); + // The coefficients are evaluated through the coefficient sub-system's SLP (which carries + // its own precision; see EvalCoefficients), so the coefficient nodes and the shared path + // variable are no longer evaluated during tracking. Their precision is vestigial and + // left untouched, keeping the shared node DAG read-only across threads (ADR-0027). + if (coefficient_system_) + coefficient_system_->precision(new_precision); + precision_ = new_precision; + } + + /// H(x,t) = sum_i c_i(t) * f_i(x) (natural functions only) + template + void EvalInPlace(Eigen::Ref> result, Vec const& vars, T const& path_value) const + { + SyncPrecision(vars); + result.setZero(); + const Eigen::Index k = static_cast(NumFunctions()); + const Vec coeffs = EvalCoefficients(path_value); + for (size_t i = 0; i < operands_.size(); ++i) + { + const Vec fi = operands_[i]->template Eval(vars); + result += coeffs(static_cast(i)) * fi.head(k); + } + } + + /// dH/dx = sum_i c_i(t) * (df_i/dx) (natural rows only) + template + void JacobianInPlace(Eigen::Ref> J, Vec const& vars, T const& path_value) const + { + SyncPrecision(vars); + J.setZero(); + const Eigen::Index k = static_cast(NumFunctions()); + const Vec coeffs = EvalCoefficients(path_value); + for (size_t i = 0; i < operands_.size(); ++i) + { + const Mat Ji = operands_[i]->template Jacobian(vars); + J += coeffs(static_cast(i)) * Ji.topRows(k); + } + } + + /// dH/dt = sum_i c_i'(t) * f_i(x) (operands are autonomous in t) + template + void TimeDerivInPlace(Eigen::Ref> result, Vec const& vars, T const& path_value) const + { + SyncPrecision(vars); + result.setZero(); + const Eigen::Index k = static_cast(NumFunctions()); + const Vec coeffs = EvalCoefficients(path_value); + const size_t n = operands_.size(); + for (size_t i = 0; i < n; ++i) + { + const Vec fi = operands_[i]->template Eval(vars); + result += coeffs(static_cast(n + i)) * fi.head(k); + } + } + +private: + /// Bring the operand systems to the precision of the evaluation point, so their + /// SetVariables precision checks pass as the adaptive tracker changes precision. + /// (No-op for double.) + template + void SyncPrecision(Vec const& vars) const + { + if constexpr (!std::is_same::value) + { + if (vars.size() > 0) + { + const unsigned p = bertini::Precision(vars(0)); + if (p != precision_) + Precision(p); + } + } + } + + /// Build (once) a small System whose functions are the coefficients c_i(t) followed by + /// the derivative coefficients c_i'(t), with the path variable t as their sole variable. + /// Evaluating it through its SLP yields every c_i(t) and c_i'(t) in one run --- so the + /// blend carries no node-level tree evaluation, and the coefficients' program is compiled + /// once and reused across tracker steps rather than rebuilt per evaluation. + SystemT& EnsureCoefficientSystem() const + { + if (!coefficient_system_) + { + auto sys = std::make_shared(); + for (auto const& c : coefficients_) + sys->AddFunction(c); + for (auto const& c : derivative_coefficients_) + sys->AddFunction(c); + sys->AddVariableGroup(VariableGroup{path_variable_}); + sys->precision(precision_); + coefficient_system_ = sys; + } + return *coefficient_system_; + } + + /// Evaluate [c_0(t) .. c_{n-1}(t), c_0'(t) .. c_{n-1}'(t)] at the given path-variable value. + template + Vec EvalCoefficients(T const& path_value) const + { + auto& cs = EnsureCoefficientSystem(); + if constexpr (!std::is_same::value) + cs.precision(bertini::Precision(path_value)); + Vec t_point(1); + t_point(0) = path_value; + return cs.template Eval(t_point); + } + + Var path_variable_; + std::vector coefficients_; + std::vector derivative_coefficients_; + std::vector operands_; + mutable unsigned precision_; + + /// Cached program for the coefficients (lazily built, see EnsureCoefficientSystem). Not + /// serialized: it is a pure cache, rebuilt on first evaluation after a load. + mutable std::shared_ptr coefficient_system_; + + friend class boost::serialization::access; + + // Operands are shared_ptr; serialize them as non-const pointers so we + // never ask boost to deserialize into a const object (const-ness is a local concern). + template + void save(Archive& ar, const unsigned /*version*/) const + { + ar & precision_; + ar & path_variable_; + ar & coefficients_; + ar & derivative_coefficients_; + std::vector> ops; + ops.reserve(operands_.size()); + for (auto const& o : operands_) + ops.push_back(std::const_pointer_cast(o)); + ar & ops; + } + + template + void load(Archive& ar, const unsigned /*version*/) + { + ar & precision_; + ar & path_variable_; + ar & coefficients_; + ar & derivative_coefficients_; + std::vector> ops; + ar & ops; + operands_.assign(ops.begin(), ops.end()); + } + + BOOST_SERIALIZATION_SPLIT_MEMBER() +}; + +} // namespace blocks +} // namespace bertini diff --git a/core/include/bertini2/system/blocks/block.hpp b/core/include/bertini2/system/blocks/block.hpp new file mode 100644 index 000000000..fe4b80148 --- /dev/null +++ b/core/include/bertini2/system/blocks/block.hpp @@ -0,0 +1,136 @@ +//This file is part of Bertini 2. +// +//block.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//block.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with block.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file bertini2/system/blocks/block.hpp + +\brief The evaluation-block contract for the block-composed System. + +A System is composed of evaluation blocks held in a std::variant and dispatched by +std::visit -- closed set, no type erasure. Each block contributes a contiguous range +of rows to the system's function vector / Jacobian. Every block must provide, for each +numeric type T in {dbl, mpfr_complex}: + + template void EvalInPlace(Eigen::Ref> seg, Vec const& vars, T const& t) const; + template void JacobianInPlace(Eigen::Ref> blk, Vec const& vars, T const& t) const; + template void TimeDerivInPlace(Eigen::Ref> seg, Vec const& vars, T const& t) const; + +plus the non-templated metadata / precision surface: + + size_t NumFunctions() const; // rows this block contributes + bool DependsOnPathVariable() const; // false => TimeDerivInPlace writes zeros, and dH/dt can skip it + unsigned Precision() const; + void Precision(unsigned) const; + +and the structural surface the owning System routes through every block (degrees + +homogenization), so a block can be homogenized and degree-counted uniformly: + + std::vector Degrees() const; + std::vector Degrees(VariableGroup const&) const; + bool IsHomogeneous(VariableGroup const&) const; + bool IsPolynomial(VariableGroup const&) const; + void Homogenize(VariableGroup const&, std::shared_ptr const&); // non-const + +`seg`/`blk` are caller-provided sub-ranges of the system's storage (the block writes its +own rows; the caller owns segmentation, the patch, etc.). `vars` is the full system +variable vector; `t` is the path-variable value (ignored by blocks that don't depend on +it). The contract is value-in / value-out so blocks are self-contained and testable +without a System. + +We are on C++17, so the contract is enforced structurally (std::visit + the is_block +detection trait below + static_assert), not via a C++20 concept. +*/ + +#pragma once + +#include +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/eigen_extensions.hpp" + +#include "bertini2/system/blocks/polynomial_block.hpp" +#include "bertini2/system/blocks/products_of_linears_block.hpp" +#include "bertini2/system/blocks/linear_forms_block.hpp" +#include "bertini2/system/blocks/blend_block.hpp" +#include "bertini2/system/blocks/randomization_block.hpp" + +namespace bertini { +namespace blocks { + +/** +\brief Detection trait: does B satisfy the evaluation-block contract? + +Checks the dbl instantiation of the templated eval entry points plus the metadata +methods. Adding a non-conforming type to the block variant then fails a one-line +static_assert instead of producing a deep template error at the visit site. +*/ +template +struct is_block : std::false_type {}; + +template +struct is_block().NumFunctions()), + decltype(std::declval().DependsOnPathVariable()), + decltype(std::declval().Precision()), + decltype(std::declval().Precision(0u)), // the precision setter + decltype(std::declval().Degrees()), + decltype(std::declval().Degrees(std::declval())), + decltype(std::declval().IsHomogeneous(std::declval())), + decltype(std::declval().IsPolynomial(std::declval())), + // Homogenize mutates (e.g. LinearFormsBlock folds its constant column onto the hom var), so + // it is detected on a non-const B&. + decltype(std::declval().Homogenize( + std::declval(), std::declval const&>())), + decltype(std::declval().template EvalInPlace( + std::declval>>(), std::declval const&>(), std::declval())), + decltype(std::declval().template JacobianInPlace( + std::declval>>(), std::declval const&>(), std::declval())), + decltype(std::declval().template TimeDerivInPlace( + std::declval>>(), std::declval const&>(), std::declval())) +>> : std::true_type {}; + +template +inline constexpr bool is_block_v = is_block::value; + +// As blocks are added, extend this static_assert list (and, later, the Block variant). +static_assert(is_block_v, + "PolynomialBlock must satisfy the evaluation-block contract"); +static_assert(is_block_v, + "ProductsOfLinearsBlock must satisfy the evaluation-block contract"); +static_assert(is_block_v, + "LinearFormsBlock must satisfy the evaluation-block contract"); + +} // namespace blocks + +// Forward declaration: BlendBlock and RandomizationBlock hold their operands by +// shared_ptr, and a System contains Blocks -- the template parameter keeps System a +// dependent name so the recursive type closes without System being complete here. (For the +// same reason these two block types are absent from the is_block_v static_assert list above: +// detecting the contract instantiates their templated eval entry points, which need System +// complete; that check happens structurally at the std::visit site instead.) +class System; + +/// The closed set of evaluation blocks a System can be composed of. Grows as block +/// types are added (eventually a PolynomialBlock so the polynomial part is uniform too). +using Block = std::variant, blocks::RandomizationBlock>; + +} // namespace bertini diff --git a/core/include/bertini2/system/blocks/describe.hpp b/core/include/bertini2/system/blocks/describe.hpp new file mode 100644 index 000000000..b027acc39 --- /dev/null +++ b/core/include/bertini2/system/blocks/describe.hpp @@ -0,0 +1,96 @@ +//This file is part of Bertini 2. +// +//describe.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//describe.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with describe.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file bertini2/system/blocks/describe.hpp + +\brief Small shared helpers for the per-block `Describe` methods -- the human-facing printing of a +block-composed System. Each block prints the rows it owns, labelled by a running global index +`f_k`. Two levels: **terse** (default) uses placeholder symbols for matrices/coefficients so only +the structure shows; **verbose** reveals the actual numbers. This output is for reading, not for +re-parsing. +*/ + +#pragma once + +#include +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/function_tree.hpp" + +namespace bertini { +namespace blocks { +namespace describe_detail { + +/// Print one (mpfr_complex) coefficient compactly: a real prints as its real part, a pure imaginary +/// as `b*i`, otherwise as `(a+b*i)`. +inline void PrintCoeff(std::ostream& out, mpfr_complex const& c) +{ + const bool re0 = (c.real() == 0), im0 = (c.imag() == 0); + if (im0) out << c.real(); + else if (re0) out << c.imag() << "*i"; + else out << "(" << c.real() << "+" << c.imag() << "*i)"; +} + +/// `f_k` for a single row, or `f_a..f_b` for a contiguous range of `n` rows starting at `row`. +inline void PrintRowLabel(std::ostream& out, size_t row, size_t n) +{ + out << "f_" << row; + if (n > 1) + out << "..f_" << (row + n - 1); +} + +/// The augmenting variable list `[x, y, 1]` (affine) or `[h, x, y]` (homogeneous: no trailing 1). +inline void PrintAugmentedVars(std::ostream& out, VariableGroup const& vars, size_t num_vars, bool homogeneous) +{ + out << "["; + for (size_t c = 0; c < num_vars && c < vars.size(); ++c) + out << (c ? ", " : "") << *vars[c]; + if (!homogeneous) + out << ", 1"; + out << "]"; +} + +/// One affine linear form, row r of an augmented coefficient matrix M (num_vars+1 cols affine, or +/// num_vars cols homogeneous). Verbose prints the actual sum `2*x + 1*y - 1`; otherwise nothing +/// (the caller prints the placeholder `c.[...]`). +inline void PrintLinearFormVerbose(std::ostream& out, Mat const& M, Eigen::Index r, + VariableGroup const& vars, size_t num_vars, bool homogeneous) +{ + bool first = true; + const size_t ncol = homogeneous ? num_vars : num_vars + 1; + for (size_t c = 0; c < ncol; ++c) + { + mpfr_complex const& coeff = M(r, static_cast(c)); + if (coeff.real() == 0 && coeff.imag() == 0) + continue; + if (!first) out << " + "; + first = false; + out << "("; + PrintCoeff(out, coeff); + out << ")"; + if (c < num_vars && c < vars.size()) // a variable column (the last affine column is the constant) + out << "*" << *vars[c]; + } + if (first) + out << "0"; +} + +} // namespace describe_detail +} // namespace blocks +} // namespace bertini diff --git a/core/include/bertini2/system/blocks/linear_forms_block.hpp b/core/include/bertini2/system/blocks/linear_forms_block.hpp new file mode 100644 index 000000000..46a979502 --- /dev/null +++ b/core/include/bertini2/system/blocks/linear_forms_block.hpp @@ -0,0 +1,279 @@ +//This file is part of Bertini 2. +// +//linear_forms_block.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//linear_forms_block.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with linear_forms_block.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file bertini2/system/blocks/linear_forms_block.hpp + +\brief A System evaluation block whose functions are linear forms: f(x) = M x + b. + +This is the linear-algebra workhorse for the block-composed System: a stack of affine +linear forms evaluated as one matrix-vector product, rather than as expanded scalar +function-tree expressions. It is the natural home for linear slices, randomization / +linear projections, and the linear conditions emitted by the bertini.linalg layer when +the coefficients are concrete (multiprecision) numbers rather than symbolic. + +Each function is + + f_i(x) = ( c_{i} . [x ; 1] ) + +i.e. a single linear form whose last coefficient (the column past the variables) carries +the constant term, so x is evaluated augmented with a trailing 1 -- exactly the augmented +convention of ProductsOfLinearsBlock, but with one factor per function instead of a +product. Evaluation is a single matrix-vector multiply; the Jacobian is the constant +coefficient matrix (its variable columns). + +Mirrors the multiprecision-coefficient pattern of ProductsOfLinearsBlock / bertini:: +LinearSlice: an mpfr master plus per-type working copies, with Precision() recasting the +mpfr copy. +*/ + +#pragma once + +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/eigen_extensions.hpp" +#include "bertini2/system/blocks/describe.hpp" + +namespace bertini { +namespace blocks { + +/** +\brief A block of system functions, each an affine linear form f_i(x) = c_i . [x ; 1]. + +Value-in / value-out: evaluation takes the variable vector and writes into a caller- +provided segment, so the block is self-contained and unit-testable without a System. +*/ +class LinearFormsBlock +{ +public: + LinearFormsBlock() : num_vars_(0), precision_(DefaultPrecision()) {} + + /** + \param num_vars The number of variables (n). The coefficient matrix has n+1 columns + (the trailing column multiplies the augmenting 1 -- the constant term of each form). + \param coefficients The augmented coefficient matrix: one row per function, shape + (number-of-functions) x (num_vars + 1). + */ + LinearFormsBlock(size_t num_vars, Mat coefficients) + : num_vars_(num_vars), coefficients_highest_precision_(std::move(coefficients)), + precision_(DefaultPrecision()) + { + assert(static_cast(coefficients_highest_precision_.cols()) == num_vars_ + 1 && + "a linear-forms coefficient matrix must have num_vars+1 columns"); + BuildWorking(); + } + + /// Number of functions (rows the block contributes to the system). + size_t NumFunctions() const { return static_cast(coefficients_highest_precision_.rows()); } + + /// Linear forms are degree 1. + std::vector Degrees() const { return std::vector(NumFunctions(), 1); } + std::vector Degrees(VariableGroup const&) const { return Degrees(); } + + /// Linear forms are polynomial. + bool IsPolynomial(VariableGroup const&) const { return true; } + /// An affine form a.x + b is inhomogeneous (it carries a constant); once homogenized the + /// constant has become a homogenizing-variable coefficient, so it is degree-1 homogeneous. + bool IsHomogeneous(VariableGroup const&) const { return homogeneous_; } + + /// Homogenize: fold the constant column onto the homogenizing variable. An augmented form + /// a.x + b (last column = constant) becomes the homogeneous a.x + b*h, where the + /// homogenizing variable h is prepended to the variable ordering -- so its column is the + /// old constant column. The block then evaluates M*[vars] directly (no augmenting 1). + /// Currently supports a single affine variable group (the dominant bertini.linalg case); + /// a second call (a second affine group) throws. + void Homogenize(VariableGroup const& /*group*/, std::shared_ptr const& /*hom_var*/) + { + if (homogeneous_) + throw std::runtime_error("LinearFormsBlock::Homogenize: block is already homogenized " + "(multiple affine variable groups are not yet supported for linear-forms blocks)"); + const auto& M = coefficients_highest_precision_; + Mat Mh(M.rows(), M.cols()); // same #cols: n+1 + Mh.col(0) = M.col(static_cast(num_vars_)); // constant -> h column (front) + Mh.rightCols(static_cast(num_vars_)) = + M.leftCols(static_cast(num_vars_)); // original variable columns + coefficients_highest_precision_ = Mh; + num_vars_ += 1; // h is now a real variable; all n+1 columns are variable columns + homogeneous_ = true; + BuildWorking(); + } + + /// Number of variables the block expects in the input vector. + size_t NumVariables() const { return num_vars_; } + + /// The master coefficient matrix (one row per form). Affine: num_vars+1 columns, the last being + /// the constant term. Homogeneous (post-Homogenize): num_vars columns, all variable columns. + /// Exposed for the function-tree expansion (System::NaturalFunctionsAsNodes). + Mat const& Coefficients() const { return coefficients_highest_precision_; } + /// Whether Homogenize has folded the constant column onto a homogenizing variable. + bool IsHomogenized() const { return homogeneous_; } + + /// Human-facing description: terse shows the placeholder `c.[x, y, 1]` (a linear form whose + /// coefficients are hidden); verbose shows the actual affine combination `2*x + 1*y - 1`. + void Describe(std::ostream& out, size_t& row, VariableGroup const& vars, bool verbose) const + { + for (Eigen::Index r = 0; r < coefficients_highest_precision_.rows(); ++r) + { + out << " f_" << row++ << " = "; + if (verbose) + describe_detail::PrintLinearFormVerbose(out, coefficients_highest_precision_, r, vars, num_vars_, homogeneous_); + else + { + out << "c."; + describe_detail::PrintAugmentedVars(out, vars, num_vars_, homogeneous_); + } + out << "\n"; + } + } + + /// Linear forms do not depend on the path variable. + bool DependsOnPathVariable() const { return false; } + + /// The Jacobian is the (constant) coefficient matrix, independent of x and t. + bool HasConstantJacobian() const { return true; } + + /// Analytic block: nothing symbolic to differentiate. + void Differentiate() const {} + + unsigned Precision() const { return precision_; } + + /// Set the working precision; recasts the mpfr working coefficients from the master. + void Precision(unsigned new_precision) const + { + if (new_precision > DoublePrecision()) + { + auto& wm = std::get>(coefficients_working_); + for (Eigen::Index r = 0; r < wm.rows(); ++r) + for (Eigen::Index c = 0; c < wm.cols(); ++c) + { + wm(r, c).precision(new_precision); + if (new_precision > precision_) + wm(r, c) = coefficients_highest_precision_(r, c); + } + } + precision_ = new_precision; + } + + /** + \brief Evaluate the block's function values into a caller-provided segment. + + \param result Length-NumFunctions() segment to write into. + \param vars Length-NumVariables() current variable values. + \param path_value The path-variable value (ignored: linear forms are autonomous). + */ + template + void EvalInPlace(Eigen::Ref> result, Vec const& vars, T const& /*path_value*/) const + { + // affine: f(x) = W * [x ; 1] (the trailing 1 carries each row's constant in the last + // column). homogeneous (post-Homogenize): every column is a variable column, so it is + // just W * vars (the old constant is now the homogenizing variable's coefficient). + if (homogeneous_) + result.noalias() = Working() * vars; + else + result.noalias() = Working() * Augment(vars); + } + + /** + \brief Evaluate the block's Jacobian (d f_i / d x_j) into a caller-provided block. + + The Jacobian of f(x) = M x + b is simply M (its variable columns) -- constant in x. + + \param J A NumFunctions() x NumVariables() block to write into. + \param vars Length-NumVariables() current variable values (unused: constant Jacobian). + \param path_value The path-variable value (ignored: linear forms are autonomous). + */ + template + void JacobianInPlace(Eigen::Ref> J, Vec const& /*vars*/, T const& /*path_value*/) const + { + // homogeneous: every column is d f / d x. affine: drop the trailing constant column. + if (homogeneous_) + J = Working(); + else + J = Working().leftCols(static_cast(num_vars_)); + } + + /** + \brief Time-derivative into a caller-provided segment. Linear forms are autonomous + (no path-variable dependence), so this is identically zero. + */ + template + void TimeDerivInPlace(Eigen::Ref> result, Vec const& /*vars*/, T const& /*path_value*/) const + { + result.setZero(); + } + +private: + template + const Mat& Working() const + { + return std::get>(coefficients_working_); + } + + template + Vec Augment(Vec const& vars) const + { + Vec aug(static_cast(num_vars_ + 1)); + aug.head(static_cast(num_vars_)) = vars; + T one(1); + if constexpr (!std::is_same::value) + one.precision(precision_); + aug(static_cast(num_vars_)) = one; + return aug; + } + + void BuildWorking() const + { + const auto& M = coefficients_highest_precision_; + auto& wd = std::get>(coefficients_working_); + auto& wm = std::get>(coefficients_working_); + wd.resize(M.rows(), M.cols()); + wm.resize(M.rows(), M.cols()); + for (Eigen::Index r = 0; r < M.rows(); ++r) + for (Eigen::Index c = 0; c < M.cols(); ++c) + { + wd(r, c) = dbl(M(r, c)); + wm(r, c) = M(r, c); + } + } + + size_t num_vars_; + bool homogeneous_ = false; ///< false: augmented affine (M*[x;1]); true: post-Homogenize (M*x) + Mat coefficients_highest_precision_; ///< master: rows = functions, cols = num_vars (homogeneous) or num_vars+1 (affine) + mutable std::tuple, Mat> coefficients_working_; + mutable unsigned precision_; + + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) + { + ar & num_vars_; + ar & homogeneous_; + ar & precision_; + ar & coefficients_highest_precision_; + ar & std::get<0>(coefficients_working_); + ar & std::get<1>(coefficients_working_); + } +}; + +} // namespace blocks +} // namespace bertini diff --git a/core/include/bertini2/system/blocks/polynomial_block.hpp b/core/include/bertini2/system/blocks/polynomial_block.hpp new file mode 100644 index 000000000..0f5c3b8e2 --- /dev/null +++ b/core/include/bertini2/system/blocks/polynomial_block.hpp @@ -0,0 +1,281 @@ +//This file is part of Bertini 2. +// +//polynomial_block.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//polynomial_block.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with polynomial_block.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file bertini2/system/blocks/polynomial_block.hpp + +\brief The evaluation block holding the classic polynomial path: function-tree functions +(and their derivatives) and/or the compiled straight-line program. + +This is the fold of System's historical `functions_` / `slp_` / `eval_method_` machinery +into a first-class block, so a System becomes a uniform loop over blocks ("everything is a +block"). It owns the function trees, their differentiation, and the SLP, and it carries +the system's variable ordering + path variable (the function-tree Jacobian/time-derivative +evaluate node trees through them). It is value-in: each Eval/Jacobian/TimeDeriv sets the +variables (and path value) from its arguments, then evaluates. + +The SLP it can compile itself: SLPCompiler reads exactly the variable ordering, path +variable, functions, and derivatives this block owns (see SLPCompiler::Compile). +*/ + +#pragma once + +#include +#include + +#include +#include +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/eigen_extensions.hpp" +#include "bertini2/function_tree.hpp" +#include "bertini2/system/straight_line_program.hpp" + +namespace bertini { +namespace blocks { + +/** +\brief The polynomial evaluation block: function trees + derivatives + SLP. +*/ +class PolynomialBlock +{ +public: + using NE = std::shared_ptr; + using Nd = std::shared_ptr; + using Var = std::shared_ptr; + + PolynomialBlock() : precision_(DefaultPrecision()) {} + + // ---- construction (System forwards AddFunction / AddSubFunction here) ---- + void AddFunction(Nd const& f) { functions_.push_back(f); Invalidate(); } + void AddConstant(NE const& f) { constant_subfunctions_.push_back(f); Invalidate(); } + + /// The variable ordering + path variable the function trees are evaluated against; the + /// owning System keeps these in sync (they change as variable groups are added / the + /// system is homogenized). + void SetVariableOrdering(VariableGroup const& vars) const { variables_ = vars; Invalidate(); } + void SetPathVariable(Var const& t) const { path_variable_ = t; Invalidate(); } + void ClearPathVariable() const { path_variable_.reset(); Invalidate(); } + + // Mutable access for the owning System's construction-time manipulations (Homogenize walks + // the trees in place; Reorder/Simplify reassign entries). The System keeps the variable + // groups / ordering; the block keeps the functions and their derivatives. + std::vector& Functions() { return functions_; } + std::vector const& Functions() const { return functions_; } + std::vector const& ConstantSubfunctions() const { return constant_subfunctions_; } + size_t NumConstants() const { return constant_subfunctions_.size(); } + + bool IsDifferentiated() const { return is_differentiated_; } + void Invalidate() const { is_differentiated_ = false; } + + /// Per-function degrees (total, and with respect to a variable group). + std::vector Degrees() const + { + std::vector d; d.reserve(functions_.size()); + for (auto const& f : functions_) d.push_back(f->Degree()); + return d; + } + std::vector Degrees(VariableGroup const& vars) const + { + std::vector d; d.reserve(functions_.size()); + for (auto const& f : functions_) d.push_back(f->Degree(vars)); + return d; + } + + /// Homogenize each function w.r.t. the group + its homogenizing var, functionally: + /// each function is rebound to a freshly homogenized copy, so any external holder of the + /// original function node never observes it change (shared variables are preserved). + void Homogenize(VariableGroup const& group, Var const& hom_var) + { + for (auto& f : functions_) + f = f->Homogenized(group, hom_var); + Invalidate(); + } + bool IsHomogeneous(VariableGroup const& vars) const + { + for (auto const& f : functions_) if (!f->IsHomogeneous(vars)) return false; + return true; + } + bool IsPolynomial(VariableGroup const& vars) const + { + for (auto const& f : functions_) if (!f->IsPolynomial(vars)) return false; + return true; + } + + // ---- block contract: metadata ---- + size_t NumFunctions() const { return functions_.size(); } + bool DependsOnPathVariable() const { return static_cast(path_variable_); } + + /// Human-facing description: one line per function, `f_k = `. Polynomials are the + /// content, so terse and verbose are the same (the expression is shown either way). + void Describe(std::ostream& out, size_t& row, VariableGroup const& /*vars*/, bool /*verbose*/) const + { + for (auto const& f : functions_) + out << " f_" << row++ << " = " << f << "\n"; + } + + unsigned Precision() const { return precision_; } + void Precision(unsigned new_precision) const + { + // The SLP (its per-thread Memory) is the sole evaluator and carries its own precision; the + // function / derivative / variable nodes are no longer evaluated during tracking, so their + // precision is vestigial and left untouched (keeps the shared node DAG read-only). + slp_.precision(new_precision); + precision_ = new_precision; + } + + // ---- block contract: evaluation (value-in) ---- + // The compiled SLP is the sole evaluator; the function/derivative trees survive only as the + // thing the SLP is compiled from (and that Simplify/Differentiate operate on). + template + void EvalInPlace(Eigen::Ref> result, Vec const& vars, T const& path_value) const + { + EnsureDifferentiated(); + SetValues(vars, path_value); + slp_.template GetFuncValsInPlace(result); + } + + template + void JacobianInPlace(Eigen::Ref> J, Vec const& vars, T const& path_value) const + { + EnsureDifferentiated(); + SetValues(vars, path_value); + slp_.template GetJacobianInPlace(J); + } + + template + void TimeDerivInPlace(Eigen::Ref> result, Vec const& vars, T const& path_value) const + { + if (!path_variable_) { result.setZero(); return; } + EnsureDifferentiated(); + SetValues(vars, path_value); + slp_.template GetTimeDerivInPlace(result); + } + + // ---- accessors the SLP compiler reads (mirror the System names) ---- + VariableGroup const& VariableOrdering() const { return variables_; } + bool HavePathVariable() const { return static_cast(path_variable_); } + Var GetPathVariable() const { return path_variable_; } + size_t NumNaturalFunctions() const { return functions_.size(); } + std::vector const& GetNaturalFunctions() const { return functions_; } + // The SLP is built from the explicit per-variable derivative trees (space/time derivatives). + std::vector const& GetSpaceDerivatives() const + { + if (space_derivatives_.empty()) + DifferentiateUsingDerivatives(); + return space_derivatives_; + } + std::vector const& GetTimeDerivatives() const + { + if (path_variable_ && time_derivatives_.empty()) + DifferentiateUsingDerivatives(); + return time_derivatives_; + } + + /// Build the symbolic derivatives (and, for SLP eval, compile the SLP from this block). + /// Derivatives are NOT simplified here; explicit simplification is System::Simplify + /// (SimplifyDerivatives), an opt-in the user invokes. + void Differentiate() const + { + if (is_differentiated_) return; + DifferentiateUsingDerivatives(); + is_differentiated_ = true; // set before Compile, which reads the deriv state + slp_ = SLPCompiler().Compile(*this); + } + + /// Simplify the function trees (and invalidate the derivatives, which must be rebuilt). + void SimplifyFunctions() const + { + // Intentionally inert on the top-level functions, preserving long-standing behavior: + // when functions were Function-wrapped, Handle::Simplified() returned self (an opaque + // boundary), so top-level simplification never happened. Now that functions are bare + // roots, actually simplifying them here would be a behavior change, deferred to its own step. + Invalidate(); + } + + /// Simplify the derivative trees. Simplification is functional and purely structural + /// (Node::Simplified, ADR-0011), so it needs no point and no node evaluation. Requires the + /// derivatives to already exist. + void SimplifyDerivatives() const + { + using bertini::Simplify; + for (auto& n : space_derivatives_) n = Simplify(n); + for (auto& n : time_derivatives_) n = Simplify(n); + } + +private: + void EnsureDifferentiated() const { if (!is_differentiated_) Differentiate(); } + + template + void SetValues(Vec const& vars, T const& path_value) const + { + slp_.SetVariableValues(vars); + if (path_variable_) slp_.template SetPathVariable(path_value); + } + + void DifferentiateUsingDerivatives() const + { + const size_t n = functions_.size(); + space_derivatives_.resize(n * variables_.size()); + for (size_t jj = 0; jj < variables_.size(); ++jj) + for (size_t ii = 0; ii < n; ++ii) + space_derivatives_[ii + jj * n] = functions_[ii]->Differentiate(variables_[jj]); + if (path_variable_) + { + time_derivatives_.resize(n); + for (size_t ii = 0; ii < n; ++ii) + time_derivatives_[ii] = functions_[ii]->Differentiate(path_variable_); + } + } + + mutable std::vector functions_; + mutable std::vector constant_subfunctions_; + + mutable std::vector space_derivatives_; + mutable std::vector time_derivatives_; + mutable StraightLineProgram slp_; + + mutable VariableGroup variables_; ///< the system's variable ordering (kept in sync by the owning System) + mutable Var path_variable_; ///< the path variable, or null + + mutable bool is_differentiated_ = false; + mutable unsigned precision_; + + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) + { + ar & constant_subfunctions_; + ar & functions_; + ar & is_differentiated_; + ar & space_derivatives_; + ar & time_derivatives_; + ar & slp_; + ar & variables_; + ar & path_variable_; + ar & precision_; + } +}; + +} // namespace blocks +} // namespace bertini diff --git a/core/include/bertini2/system/blocks/products_of_linears_block.hpp b/core/include/bertini2/system/blocks/products_of_linears_block.hpp new file mode 100644 index 000000000..edf68031b --- /dev/null +++ b/core/include/bertini2/system/blocks/products_of_linears_block.hpp @@ -0,0 +1,320 @@ +//This file is part of Bertini 2. +// +//products_of_linears_block.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//products_of_linears_block.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with products_of_linears_block.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file bertini2/system/blocks/products_of_linears_block.hpp + +\brief A System evaluation block whose functions are each a product of linear forms. + +This is the non-node representation of products of linears used by the m-homogeneous +start system and by regeneration. Each function is + + f_i(x) = prod_r ( c_{i,r} . [x ; 1] ) + +i.e. a product over factors r of a linear form in the variables (the last column of +each coefficient row is the constant / homogenizing-variable coefficient, so x is +evaluated augmented with a trailing 1). Evaluation is matrix-multiplies followed by +row-wise products; the Jacobian is the product rule, done with prefix/suffix products. +Nothing here is a function-tree node, so it never touches the SLP compiler. + +Mirrors the multiprecision-coefficient pattern of bertini::LinearSlice (system/slice.hpp): +an mpfr master plus per-type working copies, with Precision() recasting the mpfr copy. +*/ + +#pragma once + +#include +#include + +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/eigen_extensions.hpp" +#include "bertini2/system/blocks/describe.hpp" + +namespace bertini { +namespace blocks { + +/** +\brief A block of system functions, each a product of linear forms. + +Value-in / value-out: evaluation takes the variable vector and writes into a caller- +provided segment, so the block is self-contained and unit-testable without a System. +*/ +class ProductsOfLinearsBlock +{ +public: + ProductsOfLinearsBlock() : num_vars_(0), precision_(DefaultPrecision()) {} + + /** + \param num_vars The number of variables (n). Each coefficient row has n+1 entries + (the trailing entry multiplies the augmenting 1 -- the constant / homogenizing term). + \param factors One augmented coefficient matrix per function; matrix i has shape + (number-of-factors_i) x (num_vars + 1). + */ + ProductsOfLinearsBlock(size_t num_vars, std::vector> factors) + : num_vars_(num_vars), factors_highest_precision_(std::move(factors)), precision_(DefaultPrecision()) + { +#ifndef NDEBUG + for (auto const& M : factors_highest_precision_) + assert(static_cast(M.cols()) == num_vars_ + 1 && + "every products-of-linears coefficient matrix must have num_vars+1 columns"); +#endif + BuildWorking(); + } + + /// Number of functions (rows the block contributes to the system). + size_t NumFunctions() const { return factors_highest_precision_.size(); } + + /// Each function is a product of its linear factors, so its degree is the factor count. + std::vector Degrees() const + { + std::vector d; d.reserve(factors_highest_precision_.size()); + for (auto const& M : factors_highest_precision_) d.push_back(static_cast(M.rows())); + return d; + } + std::vector Degrees(VariableGroup const&) const { return Degrees(); } + + // The products-of-linears block is the m-homogeneous start system, constructed already + // homogenized (each factor carries its group's homogenizing variable). So Homogenize is a + // no-op and it reports homogeneous + polynomial. + void Homogenize(VariableGroup const&, std::shared_ptr const&) {} + bool IsHomogeneous(VariableGroup const&) const { return true; } + bool IsPolynomial(VariableGroup const&) const { return true; } + + /// Number of variables the block expects in the input vector. + size_t NumVariables() const { return num_vars_; } + + /// The master (highest-precision) coefficient matrices, one per function; matrix i is + /// (#factors_i) x (num_vars+1), the last column being the constant/augmenting term. Exposed + /// so the function-tree expansion (System::ExpandToFunctionTree) can rebuild f_i = prod_r L_r. + std::vector> const& Factors() const { return factors_highest_precision_; } + + /// Human-facing description: terse shows `prod of k linear forms`; verbose shows the actual + /// product of affine factors `(x - 1) * (x + 1)`. + void Describe(std::ostream& out, size_t& row, VariableGroup const& vars, bool verbose) const + { + for (auto const& M : factors_highest_precision_) + { + out << " f_" << row++ << " = "; + const Eigen::Index k = M.rows(); + if (!verbose) + { + out << "prod of " << k << " linear form" << (k == 1 ? "" : "s"); + } + else if (k == 0) + { + out << "1"; + } + else + { + for (Eigen::Index r = 0; r < k; ++r) + { + out << (r ? " * " : "") << "("; + describe_detail::PrintLinearFormVerbose(out, M, r, vars, num_vars_, false); + out << ")"; + } + } + out << "\n"; + } + } + + /// Products of linears do not depend on the path variable. + bool DependsOnPathVariable() const { return false; } + + /// The Jacobian is not constant (it depends on x), unlike a slice. + bool HasConstantJacobian() const { return false; } + + /// Analytic block: nothing symbolic to differentiate. + void Differentiate() const {} + + unsigned Precision() const { return precision_; } + + /// Set the working precision; recasts the mpfr working coefficients from the master. + void Precision(unsigned new_precision) const + { + if (new_precision > DoublePrecision()) + { + auto& wm = std::get>>(factors_working_); + for (size_t i = 0; i < wm.size(); ++i) + for (Eigen::Index r = 0; r < wm[i].rows(); ++r) + for (Eigen::Index c = 0; c < wm[i].cols(); ++c) + { + wm[i](r, c).precision(new_precision); + if (new_precision > precision_) + wm[i](r, c) = factors_highest_precision_[i](r, c); + } + } + precision_ = new_precision; + } + + /** + \brief Evaluate the block's function values into a caller-provided segment. + + \param result Length-NumFunctions() segment to write into. + \param vars Length-NumVariables() current variable values. + \param path_value The path-variable value (ignored: products of linears are autonomous). + */ + template + void EvalInPlace(Eigen::Ref> result, Vec const& vars, T const& /*path_value*/) const + { + const auto& W = Working(); + // aug = [vars ; 1]. The trailing 1 lets each coefficient row carry its constant + // term in its last column, so a linear factor is just the dot product (row . aug). + const Vec aug = Augment(vars); + + // Function i is a product of linear factors. W[i] is its k x (n+1) coefficient + // matrix, one row per factor, so W[i] * aug evaluates all k factors at once and the + // function value is their product. + for (size_t i = 0; i < W.size(); ++i) + { + const Vec fvals = W[i] * aug; // the k factor values L_0 .. L_{k-1} + T val(1); + for (Eigen::Index r = 0; r < fvals.size(); ++r) + val *= fvals(r); // f_i = prod_r L_r + result(static_cast(i)) = val; + } + } + + /** + \brief Evaluate the block's Jacobian (d f_i / d x_j) into a caller-provided block. + + \param J A NumFunctions() x NumVariables() block to write into. + \param vars Length-NumVariables() current variable values. + \param path_value The path-variable value (ignored: products of linears are autonomous). + */ + template + void JacobianInPlace(Eigen::Ref> J, Vec const& vars, T const& /*path_value*/) const + { + const auto& W = Working(); + const Vec aug = Augment(vars); // [vars ; 1]; see EvalInPlace + + // Function i is f_i = prod_r L_r, where L_r = (row r of M) . aug is the r-th linear + // factor's value. By the product rule, the partial derivative w.r.t. variable c is + // + // d f_i / d x_c = sum_r (d L_r / d x_c) * prod_{s != r} L_s + // = sum_r M(r,c) * weight_r, + // + // since d L_r / d x_c is just the coefficient M(r,c) of x_c in factor r, and + // weight_r := prod_{s != r} L_s is the product of every factor value except r's. + for (size_t i = 0; i < W.size(); ++i) + { + const Mat& M = W[i]; // k x (n+1): rows are factors, last col is constant + const Eigen::Index k = M.rows(); + + if (k == 0) // a 0-factor product is the constant 1; derivative 0 + { + J.row(static_cast(i)).setZero(); + continue; + } + + const Vec fvals = M * aug; // the factor values L_0 .. L_{k-1} + + // weight_r = prod_{s != r} L_s, computed in O(k) and WITHOUT division -- so it + // stays correct even when some factor value L_s is zero (dividing the total + // product by L_r would not). We split the "all but r" product into the factors + // before r and the factors after r, each built by a running accumulator: + // forward pass: weight_r <- prod_{s < r} L_s (the prefix product) + // backward pass: weight_r <- weight_r * prod_{s > r} L_s (times the suffix product) + Vec weight(k); + { + T acc(1); + for (Eigen::Index r = 0; r < k; ++r) { weight(r) = acc; acc *= fvals(r); } // prefix + acc = T(1); + for (Eigen::Index r = k - 1; r >= 0; --r) { weight(r) *= acc; acc *= fvals(r); } // suffix + } + + // Row i of the Jacobian is (sum_r M(r,c) * weight_r) over the variable columns + // c only; M.leftCols(num_vars_) drops the trailing constant column (not a variable). + J.row(static_cast(i)) = weight.transpose() * M.leftCols(static_cast(num_vars_)); + } + } + + /** + \brief Time-derivative into a caller-provided segment. Products of linears are + autonomous (no path-variable dependence), so this is identically zero. + */ + template + void TimeDerivInPlace(Eigen::Ref> result, Vec const& /*vars*/, T const& /*path_value*/) const + { + result.setZero(); + } + +private: + template + const std::vector>& Working() const + { + return std::get>>(factors_working_); + } + + template + Vec Augment(Vec const& vars) const + { + Vec aug(static_cast(num_vars_ + 1)); + aug.head(static_cast(num_vars_)) = vars; + T one(1); + if constexpr (!std::is_same::value) + one.precision(precision_); + aug(static_cast(num_vars_)) = one; + return aug; + } + + void BuildWorking() const + { + auto& wd = std::get>>(factors_working_); + auto& wm = std::get>>(factors_working_); + const size_t n = factors_highest_precision_.size(); + wd.resize(n); + wm.resize(n); + for (size_t i = 0; i < n; ++i) + { + const auto& M = factors_highest_precision_[i]; + wd[i].resize(M.rows(), M.cols()); + wm[i].resize(M.rows(), M.cols()); + for (Eigen::Index r = 0; r < M.rows(); ++r) + for (Eigen::Index c = 0; c < M.cols(); ++c) + { + wd[i](r, c) = dbl(M(r, c)); + wm[i](r, c) = M(r, c); + } + } + } + + size_t num_vars_; + std::vector> factors_highest_precision_; ///< master coefficients, one matrix per function + mutable std::tuple>, std::vector>> factors_working_; + mutable unsigned precision_; + + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) + { + ar & num_vars_; + ar & precision_; + ar & factors_highest_precision_; + ar & std::get<0>(factors_working_); + ar & std::get<1>(factors_working_); + } +}; + +} // namespace blocks +} // namespace bertini diff --git a/core/include/bertini2/system/blocks/randomization_block.hpp b/core/include/bertini2/system/blocks/randomization_block.hpp new file mode 100644 index 000000000..62f3d34ee --- /dev/null +++ b/core/include/bertini2/system/blocks/randomization_block.hpp @@ -0,0 +1,536 @@ +//This file is part of Bertini 2. +// +//randomization_block.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//randomization_block.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with randomization_block.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file bertini2/system/blocks/randomization_block.hpp + +\brief A System evaluation block that randomizes an overdetermined operand system down to a +square one, so its isolated solutions can be found by homotopy continuation. + +An overdetermined system F = (f_1, ..., f_N) of N functions in n variables (N > n) cannot be +fed to a total-degree start system (which requires squareness). Randomization replaces F with +n generic combinations whose isolated-solution set still contains that of F; the square result +is solved and the extraneous solutions discarded. + +This block holds the overdetermined system as a `shared_ptr` operand (mirroring +`BlendBlock`) and a constant coefficient matrix, and emits the n randomized rows + + g_i(x) = sum_j c_ij * f_j(x) * prod_g h_g^{(D_{i,g} - d_{j,g})} + +where f_j is the operand's j-th natural function (homogenized to its own multidegree d_j), +D_i is row i's target multidegree, h_g is the homogenizing variable of variable group g, and +c_ij = 0 whenever any deficit D_{i,g} - d_{j,g} is negative. The h-power factors are what make +a constant-coefficient combination of functions of *differing* degree homogenize correctly: +before homogenization every h_g = 1 and the formula collapses to g_i = sum_j c_ij f_j. + +Construction (degrees, sorting, the [I|C] / common-target-multidegree matrix) is done by +`System::Randomize`, which hands this block the finished coefficient matrix and the per-row / +per-function multidegrees. The block is multidegree-native so single-affine-group and +multihomogeneous randomization share one evaluator. + +Templated on the system type so `SystemT` stays a dependent name in the `Block` variant (a +System contains a Block which contains a RandomizationBlock holding a shared_ptr +-- the template parameter breaks the recursive type), exactly as for `BlendBlock`. + +\note Like BlendBlock, the operand is held by shared_ptr and its mutable per-evaluation state +(variable values) is shared across copies; per-thread tracking must Clone (the deep-clone of a +block operand is a shared follow-up with BlendBlock). +*/ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +#include "bertini2/num_traits.hpp" +#include "bertini2/eigen_extensions.hpp" +#include "bertini2/function_tree.hpp" +#include "bertini2/system/blocks/describe.hpp" + +namespace bertini { +namespace blocks { + +template +class RandomizationBlock +{ +public: + using Var = std::shared_ptr; + using OperandPtr = std::shared_ptr; + + RandomizationBlock() : num_groups_(0), precision_(DefaultPrecision()) {} + + /** + \param operand The overdetermined system being randomized (N natural functions). + \param coefficients The n x N constant matrix; row i, column j is c_ij. + \param target_multidegrees n rows, each a length-num_groups vector D_i (row i's target degree + per variable group). + \param operand_multidegrees N rows, each a length-num_groups vector d_j (operand function j's + degree per variable group), measured on the affine operand. + \param num_groups The number of (affine) variable groups G. + */ + RandomizationBlock(OperandPtr operand, + Mat coefficients, + std::vector> target_multidegrees, + std::vector> operand_multidegrees, + size_t num_groups) + : operand_(std::move(operand)), + coefficients_highest_precision_(std::move(coefficients)), + target_multidegrees_(std::move(target_multidegrees)), + operand_multidegrees_(std::move(operand_multidegrees)), + num_groups_(num_groups), + precision_(DefaultPrecision()) + { + BuildWorking(); + } + + // Memory-isolating copy (ADR-0027): the operand System is deep-copied via the + // System copy constructor, which shares its immutable node DAG and compiled SLP Program but + // gives it its own per-thread evaluation Memory. Everything else (the coefficient matrices, + // multidegrees, working buffers) is value state, copied per copy. This lets path tracking + // Clone a System into per-thread copies that share no mutable state. + RandomizationBlock(RandomizationBlock const& other) + : operand_(other.operand_ ? std::make_shared(*other.operand_) : nullptr), + coefficients_highest_precision_(other.coefficients_highest_precision_), + coefficients_working_(other.coefficients_working_), + target_multidegrees_(other.target_multidegrees_), + operand_multidegrees_(other.operand_multidegrees_), + num_groups_(other.num_groups_), + homogenized_(other.homogenized_), + hom_vars_(other.hom_vars_), + hom_var_index_(other.hom_var_index_), + precision_(other.precision_) + {} + + RandomizationBlock& operator=(RandomizationBlock const& other) + { + if (this != &other) + { + operand_ = other.operand_ ? std::make_shared(*other.operand_) : nullptr; + coefficients_highest_precision_ = other.coefficients_highest_precision_; + coefficients_working_ = other.coefficients_working_; + target_multidegrees_ = other.target_multidegrees_; + operand_multidegrees_ = other.operand_multidegrees_; + num_groups_ = other.num_groups_; + homogenized_ = other.homogenized_; + hom_vars_ = other.hom_vars_; + hom_var_index_ = other.hom_var_index_; + precision_ = other.precision_; + } + return *this; + } + + RandomizationBlock(RandomizationBlock&&) = default; + RandomizationBlock& operator=(RandomizationBlock&&) = default; + + /// Number of randomized functions (rows this block contributes) = n. + size_t NumFunctions() const { return static_cast(coefficients_highest_precision_.rows()); } + + /// The randomized functions' degrees: the (total) target degree of each row, i.e. the sum of + /// its target multidegree over the variable groups. This is the degree-reporting hook -- the + /// path count of the squared system is the product of these. + std::vector Degrees() const + { + std::vector d(NumFunctions(), 0); + for (size_t i = 0; i < target_multidegrees_.size(); ++i) + for (int e : target_multidegrees_[i]) + d[i] += e; + return d; + } + /// Per-group target degree. We match the group against the operand's variable groups by + /// identity so the correct column of the target multidegree is returned; an unrecognized group + /// reports the total (defensive -- the owning System asks per its own groups). + std::vector Degrees(VariableGroup const& vars) const + { + const int g = GroupIndex(vars); + if (g < 0) + return Degrees(); + std::vector d(NumFunctions(), 0); + for (size_t i = 0; i < target_multidegrees_.size(); ++i) + d[i] = target_multidegrees_[i][static_cast(g)]; + return d; + } + + /// The randomized system is polynomial / homogeneous exactly when the operand is (System's + /// IsPolynomial/IsHomogeneous are whole-system, no-argument checks -- as BlendBlock delegates). + bool IsPolynomial(VariableGroup const& /*vars*/) const { return operand_->IsPolynomial(); } + bool IsHomogeneous(VariableGroup const& /*vars*/) const { return homogenized_ ? operand_->IsHomogeneous() : false; } + + /** + \brief Homogenize: thread the owning system's homogenizing variable for this group into the + operand, so the operand's functions and this block's h-power deficit factors share one set of + homogenizing variables. + + The owning System calls this once per affine variable group, in group order. We accumulate the + supplied hom vars; once we have one per group we homogenize the operand with exactly those + variables (System::Homogenize(VariableGroup const&) reuses them rather than minting fresh ones) + and record each hom var's position in the operand's variable ordering for the h-power factors. + */ + void Homogenize(VariableGroup const& /*group*/, Var const& hom_var) + { + if (homogenized_) + return; + hom_vars_.push_back(hom_var); + if (hom_vars_.size() == num_groups_) + { + operand_->Homogenize(VariableGroup(hom_vars_)); + homogenized_ = true; + // the hom var of group g now sits at a fixed position of the (shared) variable ordering; + // the block reads h_g from there during evaluation. + hom_var_index_.assign(num_groups_, -1); + auto const& ordering = operand_->Variables(); + for (size_t g = 0; g < num_groups_; ++g) + for (size_t k = 0; k < ordering.size(); ++k) + if (ordering[k] == hom_vars_[g]) { hom_var_index_[g] = static_cast(k); break; } + } + } + + /// The randomization itself adds no path-variable dependence; it inherits the operand's. + bool DependsOnPathVariable() const { return operand_->HavePathVariable(); } + + /// The randomized Jacobian varies in x (operand Jacobian times constants, plus h-power terms). + bool HasConstantJacobian() const { return false; } + + /// Analytic block: nothing symbolic to differentiate (the operand differentiates itself). + void Differentiate() const {} + + /// The overdetermined operand, for the function-tree expansion oracle and the matrix getter. + OperandPtr const& Operand() const { return operand_; } + /// The randomization matrix R (master, highest precision); n x N, row i column j is c_ij. + Mat const& RandomizationMatrix() const { return coefficients_highest_precision_; } + + // Accessors for the function-tree expansion oracle (System::NaturalFunctionsAsNodes), which + // rebuilds g_i = sum_j c_ij * node(f_j) * prod_g h_g^{(D_{i,g}-d_{j,g})}. + bool IsHomogenized() const { return homogenized_; } + std::vector const& HomVars() const { return hom_vars_; } + std::vector> const& TargetMultidegrees() const { return target_multidegrees_; } + std::vector> const& OperandMultidegrees() const { return operand_multidegrees_; } + + /// Human-facing description: terse shows `f_a..f_b = R . g (R: nxN)` then the underlying + /// functions `g_j` indented (they are the interesting part); verbose additionally prints R's + /// entries. + void Describe(std::ostream& out, size_t& row, VariableGroup const& /*vars*/, bool verbose) const + { + const size_t n = NumFunctions(); + const size_t N = static_cast(coefficients_highest_precision_.cols()); + out << " "; + describe_detail::PrintRowLabel(out, row, n); + out << " = R . g (R: " << n << "x" << N << " randomization matrix)\n"; + row += n; + auto g = operand_->NaturalFunctionsAsNodes(); + for (size_t j = 0; j < g.size(); ++j) + out << " g_" << j << " = " << g[j] << "\n"; + if (verbose) + { + out << " R =\n"; + for (Eigen::Index i = 0; i < coefficients_highest_precision_.rows(); ++i) + { + out << " [ "; + for (Eigen::Index j = 0; j < coefficients_highest_precision_.cols(); ++j) + { + if (j) out << ", "; + describe_detail::PrintCoeff(out, coefficients_highest_precision_(i, j)); + } + out << " ]\n"; + } + } + } + + unsigned Precision() const { return precision_; } + + /// Set the working precision: recast the mpfr working coefficients from the master and bring + /// the operand to the same precision (so a high-precision coefficient is not multiplied against + /// a low-precision operand value). + void Precision(unsigned new_precision) const + { + if (new_precision > DoublePrecision()) + { + auto& wm = std::get>(coefficients_working_); + for (Eigen::Index r = 0; r < wm.rows(); ++r) + for (Eigen::Index c = 0; c < wm.cols(); ++c) + { + wm(r, c).precision(new_precision); + if (new_precision > precision_) + wm(r, c) = coefficients_highest_precision_(r, c); + } + } + operand_->precision(new_precision); + precision_ = new_precision; + } + + /** + \brief Evaluate the randomized function values into a caller-provided segment. + + g_i = sum_j c_ij * f_j * prod_g h_g^{(D_{i,g} - d_{j,g})}. + */ + template + void EvalInPlace(Eigen::Ref> result, Vec const& vars, T const& /*path_value*/) const + { + SyncPrecision(vars); + const size_t N = static_cast(coefficients_highest_precision_.cols()); + const Vec f = operand_->template Eval(vars).head(static_cast(N)); + const auto& C = Working(); + + result.setZero(); + for (size_t i = 0; i < NumFunctions(); ++i) + { + T acc = Zero(); + for (size_t j = 0; j < N; ++j) + { + const T& c = C(static_cast(i), static_cast(j)); + if (IsZero(c)) + continue; + acc += c * f(static_cast(j)) * HPower(i, j, vars); + } + result(static_cast(i)) = acc; + } + } + + /** + \brief Evaluate the randomized Jacobian into a caller-provided block. + + By the product rule on c_ij * f_j * W_ij(x) (W_ij = prod_g h_g^{e_{ij,g}}): + d g_i / d x_l = sum_j c_ij [ (d f_j / d x_l) W_ij + f_j (d W_ij / d x_l) ], + and dW_ij/dx_l is nonzero only when x_l is a homogenizing variable h_{g'} appearing in W_ij. + */ + template + void JacobianInPlace(Eigen::Ref> J, Vec const& vars, T const& /*path_value*/) const + { + SyncPrecision(vars); + const size_t N = static_cast(coefficients_highest_precision_.cols()); + const Vec f = operand_->template Eval(vars).head(static_cast(N)); + const Mat Jf = operand_->template Jacobian(vars).topRows(static_cast(N)); + const auto& C = Working(); + + J.setZero(); + for (size_t i = 0; i < NumFunctions(); ++i) + { + for (size_t j = 0; j < N; ++j) + { + const T& c = C(static_cast(i), static_cast(j)); + if (IsZero(c)) + continue; + + const T W = HPower(i, j, vars); // W_ij = prod_g h_g^{e_g} + // operand-Jacobian term: c_ij * W_ij * (d f_j / d x), across all columns. + J.row(static_cast(i)) += (c * W) * Jf.row(static_cast(j)); + + // h-power terms: only the homogenizing-variable columns pick up f_j * dW/dh_{g'}. + if (homogenized_) + { + for (size_t g = 0; g < num_groups_; ++g) + { + const int e = Deficit(i, j, g); + if (e <= 0 || hom_var_index_[g] < 0) + continue; // e<0 means c_ij==0 (already skipped); e==0 -> no dependence + // dW/dh_g = e * h_g^{e-1} * prod_{g2 != g} h_{g2}^{e_{g2}} + const T dW = MixedPartial(i, j, g, vars); + J(static_cast(i), hom_var_index_[g]) += c * f(static_cast(j)) * dW; + } + } + } + } + } + + /// Time-derivative. The randomization is autonomous in the path variable; if the operand + /// depends on t (unusual for a randomized target) its rows would contribute, but the common + /// case is an autonomous operand, so this is zero. + template + void TimeDerivInPlace(Eigen::Ref> result, Vec const& /*vars*/, T const& /*path_value*/) const + { + result.setZero(); + } + +private: + template + const Mat& Working() const { return std::get>(coefficients_working_); } + + /// A precision-correct zero / one for the accumulators. + template + T Zero() const + { + T z(0); + if constexpr (!std::is_same::value) z.precision(precision_); + return z; + } + + static bool IsZero(dbl const& c) { return c == dbl(0); } + static bool IsZero(mpfr_complex const& c) { return c.real() == 0 && c.imag() == 0; } + + /// The (signed) degree deficit of operand function j against row i's target, in group g. + int Deficit(size_t i, size_t j, size_t g) const + { + return target_multidegrees_[i][g] - operand_multidegrees_[j][g]; + } + + /// W_ij = prod_g h_g^{(D_{i,g} - d_{j,g})}. Before homogenization every h_g = 1, so 1. + template + T HPower(size_t i, size_t j, Vec const& vars) const + { + T w(1); + if constexpr (!std::is_same::value) w.precision(precision_); + if (!homogenized_) + return w; + for (size_t g = 0; g < num_groups_; ++g) + { + const int e = Deficit(i, j, g); + if (e == 0 || hom_var_index_[g] < 0) + continue; + w *= IntPow(vars(hom_var_index_[g]), e); + } + return w; + } + + /// d W_ij / d h_{g} = e_g * h_g^{e_g - 1} * prod_{g2 != g} h_{g2}^{e_{g2}} (e_g > 0 assumed). + template + T MixedPartial(size_t i, size_t j, size_t g, Vec const& vars) const + { + const int eg = Deficit(i, j, g); + T d(eg); + if constexpr (!std::is_same::value) d.precision(precision_); + d *= IntPow(vars(hom_var_index_[g]), eg - 1); + for (size_t g2 = 0; g2 < num_groups_; ++g2) + { + if (g2 == g) + continue; + const int e2 = Deficit(i, j, g2); + if (e2 == 0 || hom_var_index_[g2] < 0) + continue; + d *= IntPow(vars(hom_var_index_[g2]), e2); + } + return d; + } + + /// base^e for integer e >= 0, by repeated multiplication (exact, no branch-cut surprises). + template + static T IntPow(T const& base, int e) + { + T result(1); + if constexpr (!std::is_same::value) result.precision(bertini::Precision(base)); + for (int k = 0; k < e; ++k) + result *= base; + return result; + } + + /// Match a variable group against the operand's affine variable groups by identity; -1 if none. + int GroupIndex(VariableGroup const& vars) const + { + auto const& groups = operand_->VariableGroups(); + for (size_t g = 0; g < groups.size(); ++g) + if (SameGroup(groups[g], vars)) + return static_cast(g); + return -1; + } + static bool SameGroup(VariableGroup const& a, VariableGroup const& b) + { + if (a.size() != b.size()) + return false; + for (size_t k = 0; k < a.size(); ++k) + if (a[k] != b[k]) + return false; + return true; + } + + template + void SyncPrecision(Vec const& vars) const + { + if constexpr (!std::is_same::value) + { + if (vars.size() > 0) + { + const unsigned p = bertini::Precision(vars(0)); + if (p != precision_) + Precision(p); + } + } + } + + void BuildWorking() const + { + const auto& M = coefficients_highest_precision_; + auto& wd = std::get>(coefficients_working_); + auto& wm = std::get>(coefficients_working_); + wd.resize(M.rows(), M.cols()); + wm.resize(M.rows(), M.cols()); + for (Eigen::Index r = 0; r < M.rows(); ++r) + for (Eigen::Index c = 0; c < M.cols(); ++c) + { + wd(r, c) = dbl(M(r, c)); + wm(r, c) = M(r, c); + } + } + + OperandPtr operand_; + Mat coefficients_highest_precision_; ///< master n x N randomization matrix + mutable std::tuple, Mat> coefficients_working_; + std::vector> target_multidegrees_; ///< n x G + std::vector> operand_multidegrees_; ///< N x G + size_t num_groups_; ///< G (affine variable groups) + bool homogenized_ = false; + std::vector hom_vars_; ///< the (shared) homogenizing variable per group + std::vector hom_var_index_; ///< its position in the variable ordering + mutable unsigned precision_; + + friend class boost::serialization::access; + + // Operand held as shared_ptr (non-const, since Homogenize mutates the twin); split + // like BlendBlock so serialization never deserializes into a const object. + template + void save(Archive& ar, const unsigned /*version*/) const + { + ar & operand_; + ar & coefficients_highest_precision_; + ar & std::get<0>(coefficients_working_); + ar & std::get<1>(coefficients_working_); + ar & target_multidegrees_; + ar & operand_multidegrees_; + ar & num_groups_; + ar & homogenized_; + ar & hom_vars_; + ar & hom_var_index_; + ar & precision_; + } + + template + void load(Archive& ar, const unsigned /*version*/) + { + ar & operand_; + ar & coefficients_highest_precision_; + ar & std::get<0>(coefficients_working_); + ar & std::get<1>(coefficients_working_); + ar & target_multidegrees_; + ar & operand_multidegrees_; + ar & num_groups_; + ar & homogenized_; + ar & hom_vars_; + ar & hom_var_index_; + ar & precision_; + } + + BOOST_SERIALIZATION_SPLIT_MEMBER() +}; + +} // namespace blocks +} // namespace bertini diff --git a/core/include/bertini2/system/eval_expression.hpp b/core/include/bertini2/system/eval_expression.hpp new file mode 100644 index 000000000..9e5c85945 --- /dev/null +++ b/core/include/bertini2/system/eval_expression.hpp @@ -0,0 +1,122 @@ +//This file is part of Bertini 2. +// +//include/bertini2/system/eval_expression.hpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//include/bertini2/system/eval_expression.hpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with include/bertini2/system/eval_expression.hpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire + + +/** +\file include/bertini2/system/eval_expression.hpp + +\brief Evaluate a bare expression tree at a point, without the caller owning a System. + +The work is delegated to the single evaluation engine (the compiled SLP). The expression's +variables are discovered and ordered by name; a one-function program is compiled for the +expression and **memoized on the (immutable, hash-consed) node**, so repeated evaluation of the +same expression reuses it rather than recompiling. Each call runs the shared program against +its own working memory at the requested point. +*/ + +#pragma once + +#include +#include +#include +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/function_tree/gather.hpp" + +namespace bertini { + + /** + \brief Evaluate an expression tree at a point supplied as a variable-name -> value map. + + The expression's variables are discovered with `node::GatherVariables` (distinct, sorted + by name --- the session-global canonical order), a one-function program is compiled for the + expression (memoized on the node) and run at the point. Variables are matched to values by + name, which is unambiguous because variables are canonical by name across the whole session. + + Every variable appearing in the expression must have a supplied value, and every supplied + name must appear in the expression; either kind of mismatch throws. The latter is a + guard against typos --- a value silently going nowhere is almost always a mistake. + + \tparam T The evaluation number type (`dbl` or `mpfr_complex`). + \param expr The expression to evaluate. + \param variable_values A map from variable name to the value to substitute. + \return The value of the expression at the given point. + */ + template + T EvalExpression(std::shared_ptr const& expr, + std::map const& variable_values) + { + auto vars = node::GatherVariables(expr); // distinct, sorted by name + + // Guard against typos: every supplied name must be a variable of the expression. + for (auto const& named_value : variable_values) + { + bool appears = false; + for (auto const& v : vars) + if (v->name() == named_value.first) { appears = true; break; } + if (!appears) + throw std::runtime_error("value supplied for '" + named_value.first + + "', which is not a variable of the expression being evaluated"); + } + + // Bind values into the System's variable order (which is the name order of `vars`). + Vec point(static_cast(vars.size())); + for (size_t i = 0; i < vars.size(); ++i) + { + auto it = variable_values.find(vars[i]->name()); + if (it == variable_values.end()) + throw std::runtime_error("no value supplied for variable '" + vars[i]->name() + + "' when evaluating an expression"); + point(static_cast(i)) = it->second; + } + + // Compile the expression's evaluator once and memoize it on the (immutable, hash-consed) + // node, so repeated evaluation of the same expression reuses the compiled program rather + // than rebuilding a throwaway System and recompiling an SLP every call (ADR-0027). + // The compiled program holds no node pointers (its constants are value recipes), so caching + // it on the node creates no reference cycle. + auto cached = std::static_pointer_cast(expr->EvalProgram()); + if (!cached) + { + System sys; + sys.AddFunction(expr); + sys.AddVariableGroup(vars); + cached = std::make_shared(sys); + expr->SetEvalProgram(cached); + } + + // Per-call working copy: it shares the immutable compiled Program but gets its own + // evaluation Memory, so concurrent evaluations of the same cached expression do not race. + StraightLineProgram slp = *cached; + + if constexpr (!std::is_same::value) + if (vars.size() > 0) + slp.precision(Precision(point)); + + slp.Eval(point); + return slp.template GetFuncVals()(0); + } + +} // namespace bertini diff --git a/core/include/bertini2/system/patch.hpp b/core/include/bertini2/system/patch.hpp index 8b4ef7693..66d42d30b 100644 --- a/core/include/bertini2/system/patch.hpp +++ b/core/include/bertini2/system/patch.hpp @@ -134,6 +134,12 @@ namespace bertini { } } + /** + Copy assignment. Explicitly defaulted (memberwise), matching the previously-implicit behavior. + Note this differs from the custom copy constructor, which re-downsamples at current default precision. + */ + Patch& operator=(Patch const& other) = default; + /** \brief Constructor making a random complex patch on a space whose structure is described by the input argument. @@ -144,7 +150,7 @@ namespace bertini { \param sizes The sizes of the variable groups, including homogenizing variables if present. */ - Patch(std::vector const& sizes) : variable_group_sizes_(sizes), coefficients_highest_precision_(sizes.size()), precision_(DefaultPrecision()) + Patch(std::vector const& sizes) : coefficients_highest_precision_(sizes.size()), variable_group_sizes_(sizes), precision_(DefaultPrecision()) { using bertini::Precision; using bertini::multiprecision::RandomComplex; @@ -313,7 +319,7 @@ namespace bertini { // unpack from the tuple of working coefficients const std::vector >& coefficients = std::get > >(coefficients_working_); - unsigned offset(function_values.size() - NumVariableGroups()); // by precondition this number is at least 0. the precondition is ensured by the public wrapper + unsigned offset(static_cast(function_values.size() - NumVariableGroups())); // by precondition this number is at least 0. the precondition is ensured by the public wrapper unsigned counter(0); for (unsigned ii = 0; ii < NumVariableGroups(); ++ii) { @@ -355,6 +361,7 @@ namespace bertini { void JacobianInPlace(Eigen::MatrixBase & jacobian, Vec const& x) const { static_assert(std::is_same::value,"scalar types must match"); + (void)x; // only used in asserts; the jacobian of a patch is constant #ifndef BERTINI_DISABLE_ASSERTS @@ -368,7 +375,17 @@ namespace bertini { const std::vector >& coefficients = std::get > >(coefficients_working_); - unsigned offset(jacobian.rows() - NumVariableGroups()); // by precondition this number is at least 0. the precondition is ensured by the public wrapper + unsigned offset(static_cast(jacobian.rows() - NumVariableGroups())); // by precondition this number is at least 0. the precondition is ensured by the public wrapper + + // A patch row is sparse -- one block of coefficients per variable group, zero elsewhere. + // Zero the rows the patch owns before writing those coefficients, so the patch FULLY + // defines its own rows and the caller need not pre-zero the matrix. The block-composed + // Jacobian path allocates J uninitialized and assigns only the block (function) rows; + // without this the patch rows' off-coefficient entries are read uninitialized -- benign + // (zeroed pages) on Linux/macOS, but garbage on Windows, where a degree-2 homotopy's + // Jacobian "evaluated" to ~1e252 and wrecked the AMP condition-number estimate. + jacobian.block(offset, 0, NumVariableGroups(), jacobian.cols()).setZero(); + unsigned counter(0); for (unsigned ii = 0; ii < NumVariableGroups(); ++ii) for (unsigned jj=0; jj(variable_group_sizes_.size()); } @@ -523,7 +540,7 @@ namespace bertini { friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & precision_; ar & coefficients_highest_precision_; diff --git a/core/include/bertini2/system/slice.hpp b/core/include/bertini2/system/slice.hpp index 303c162a5..c0df532be 100644 --- a/core/include/bertini2/system/slice.hpp +++ b/core/include/bertini2/system/slice.hpp @@ -195,7 +195,7 @@ namespace bertini { */ unsigned NumVariables() const { - return sliced_vars_.size(); + return static_cast(sliced_vars_.size()); } @@ -206,12 +206,12 @@ namespace bertini { Make an empty linear slice. */ LinearSlice() : + coefficients_highest_precision_(0, 0), + constants_highest_precision_(static_cast(0)), sliced_vars_(), - precision_(DefaultPrecision()), num_dims_sliced_(0), - coefficients_highest_precision_(0, 0), - is_homogeneous_(false), - constants_highest_precision_(static_cast(0)) + precision_(DefaultPrecision()), + is_homogeneous_(false) { std::get > (coefficients_working_).resize(Dimension(), NumVariables()); std::get >(coefficients_working_).resize(Dimension(), NumVariables()); @@ -223,7 +223,7 @@ namespace bertini { /** \brief the constructor for linear slices. */ - LinearSlice(VariableGroup const& v, unsigned dim, bool homogeneous) : sliced_vars_(v), precision_(DefaultPrecision()), num_dims_sliced_(dim), coefficients_highest_precision_(dim, v.size()), is_homogeneous_(homogeneous), constants_highest_precision_(dim) + LinearSlice(VariableGroup const& v, unsigned dim, bool homogeneous) : coefficients_highest_precision_(dim, v.size()), constants_highest_precision_(dim), sliced_vars_(v), num_dims_sliced_(dim), precision_(DefaultPrecision()), is_homogeneous_(homogeneous) { std::get > (coefficients_working_).resize(Dimension(), NumVariables()); std::get >(coefficients_working_).resize(Dimension(), NumVariables()); @@ -318,7 +318,7 @@ namespace bertini { friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & precision_; ar & coefficients_highest_precision_; diff --git a/core/include/bertini2/system/start/mhom.hpp b/core/include/bertini2/system/start/mhom.hpp index 39d0c5bba..72b4d682a 100644 --- a/core/include/bertini2/system/start/mhom.hpp +++ b/core/include/bertini2/system/start/mhom.hpp @@ -124,15 +124,22 @@ namespace bertini std::vector degrees_; ///< stores the degrees of the functions. std::vector< VariableGroup > var_groups_; - Mat> linprod_matrix_; ///< All the linear products for each entry in the degree matrix. + /// Random linear-factor coefficients, one matrix per (function, variable group). + /// Entry (i,j) is a (degree_matrix_(i,j)) x (group_j_size + 1) matrix: each row is + /// a linear factor over group j's variables, the trailing column being the factor's + /// constant (0 for projective groups -- their factors are homogeneous). This is the + /// data the retired node::LinearProduct used to hold; the products-of-linears block + /// and the start-point solve read it directly. + Mat> linear_coeffs_; std::vector< std::vector > variable_cols_; ///< The columns associated with each variable. The first index is the variable group, the second index is the particular variable in the group. + size_t num_hom_groups_ = 0; ///< how many of the leading entries of var_groups_ are projective (homogeneous) groups; the rest are affine. A projective group of size k spans P^{k-1}: dimension k-1, so it takes k-1 functions and its start-point component is a homogeneous vector. mutable Vec temp_v_mp_; friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & degrees_; diff --git a/core/include/bertini2/system/start/total_degree.hpp b/core/include/bertini2/system/start/total_degree.hpp index ed2ed8d44..7b9498cec 100644 --- a/core/include/bertini2/system/start/total_degree.hpp +++ b/core/include/bertini2/system/start/total_degree.hpp @@ -71,7 +71,8 @@ namespace bertini template NumT RandomValue(size_t index) const { - return random_values_[index]->Eval(); + // A direct read of the literal constant --- no node-level evaluation. + return random_values_[index]->Value(); } @@ -134,7 +135,7 @@ namespace bertini friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); ar & random_values_; ar & degrees_; diff --git a/core/include/bertini2/system/start/user.hpp b/core/include/bertini2/system/start/user.hpp index 042bc5c5f..defdf3a9a 100644 --- a/core/include/bertini2/system/start/user.hpp +++ b/core/include/bertini2/system/start/user.hpp @@ -105,7 +105,7 @@ namespace bertini template friend void boost::serialization::save_construct_data(Archive & ar, const User * t, const unsigned int file_version); template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } @@ -119,7 +119,7 @@ namespace bertini namespace boost { namespace serialization { template inline void save_construct_data( - Archive & ar, const bertini::start_system::User * t, const unsigned int file_version + Archive & ar, const bertini::start_system::User * t, const unsigned int /*file_version*/ ){ // save data required to construct instance ar << t->user_system_; @@ -132,7 +132,7 @@ inline void save_construct_data( template inline void load_construct_data( - Archive & ar, bertini::start_system::User * t, const unsigned int file_version + Archive & ar, bertini::start_system::User * t, const unsigned int /*file_version*/ ){ // retrieve data from archive required to construct new instance bertini::System sys; diff --git a/core/include/bertini2/system/start/utility.hpp b/core/include/bertini2/system/start/utility.hpp index 250f988ec..33f2786cc 100644 --- a/core/include/bertini2/system/start/utility.hpp +++ b/core/include/bertini2/system/start/utility.hpp @@ -61,7 +61,7 @@ namespace bertini{ std::vector< T > subscripts(dimensions.size());//for forming a subscript from an index std::vector k(dimensions.size(),1); - for (int ii = 0; ii < dimensions.size()-1; ++ii) + for (size_t ii = 0; ii+1 < dimensions.size(); ++ii) k[ii+1] = k[ii]*dimensions[ii]; @@ -69,7 +69,7 @@ namespace bertini{ throw std::out_of_range("in IndexToSubscript, index exceeds max based on dimension sizes"); - for (int ii = dimensions.size()-1; ii >= 0; --ii) + for (int ii = static_cast(dimensions.size())-1; ii >= 0; --ii) { T I = index%k[ii]; T J = (index - I) / k[ii]; diff --git a/core/include/bertini2/system/start_base.hpp b/core/include/bertini2/system/start_base.hpp index 9dc3d919b..b0cfa8ae2 100644 --- a/core/include/bertini2/system/start_base.hpp +++ b/core/include/bertini2/system/start_base.hpp @@ -68,7 +68,7 @@ namespace bertini friend class boost::serialization::access; template - void serialize(Archive& ar, const unsigned version) { + void serialize(Archive& ar, const unsigned /*version*/) { ar & boost::serialization::base_object(*this); } diff --git a/core/include/bertini2/system/straight_line_program.hpp b/core/include/bertini2/system/straight_line_program.hpp index e3967b4ed..896098a3e 100644 --- a/core/include/bertini2/system/straight_line_program.hpp +++ b/core/include/bertini2/system/straight_line_program.hpp @@ -28,6 +28,17 @@ \file bertini2/system/straight_line_program.hpp \brief Provides the bertini::StraightLineProgram class. + +The straight-line program is split (ADR-0027) into two collaborators: + + * SLPProgram -- the immutable compiled program: the instruction tape, the constant recipe, + and the memory layout. Once compiled it never changes, so it is shareable read-only. + + * SLPMemory -- the per-thread mutable working state: the register file, the working precision, + and the freshness / frozen-prologue flags. Cheap to allocate; never shared across threads. + +`StraightLineProgram` is the facade owning one (shared) program and its (own) memory, presenting +the historical API unchanged. */ #ifndef BERTINI_SLP_HPP @@ -38,6 +49,8 @@ #include #include #include +#include +#include #include "bertini2/mpfr_complex.hpp" #include "bertini2/mpfr_extensions.hpp" @@ -46,6 +59,7 @@ #include "bertini2/detail/visitor.hpp" #include +#include // code copied from Bertini1's file include/bertini.h @@ -138,6 +152,7 @@ namespace bertini { class SLPCompiler; class System; // a forward declaration, solving the circular inclusion problem + class StraightLineProgram; enum Operation { // we'll start with the binary ones @@ -176,110 +191,304 @@ namespace bertini { std::string OpcodeToString(Operation op); + /** - \class StraightLineProgram + \struct SLPOutputLocations - An implementation of straight-line programs, implemented with strong inspiration from Bertini1's implementation. + A struct encapsulating the starting locations of outputs in the SLP's memory layout. + */ + struct SLPOutputLocations{ + size_t Functions{0}; + size_t Jacobian{0}; + size_t TimeDeriv{0}; - One constructs a SLP from a system, like + friend class boost::serialization::access; - ``` - System my_system(); - StraightLineProgram slp(my_system); - ``` + template + void serialize(Archive& ar, const unsigned /*version*/) { + ar & Functions; + ar & Jacobian; + ar & TimeDeriv; + } + }; - Maybe you don't need to know this, but in construction the SLP uses a helper class, the SLPCompiler + /** + \struct SLPInputLocations - Patches are just functions in this framework. The variables appear at the front of the memory, then functions, then derivatives. This should make copying data out easy, because it's all in one place. + A struct encapsulating the starting locations of inputs in the SLP's memory layout. + */ + struct SLPInputLocations{ + size_t Variables{0}; + size_t Time{0}; - In contrast to Bertini1 SLP's, we don't put all the numbers at the front -- they just get scattered through the SLP's memory. + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) { + ar & Variables; + ar & Time; + } + }; + + /** + \struct SLPNumberOf + + A struct encapsulating the numbers of things appearing in the SLP. */ - class StraightLineProgram{ + struct SLPNumberOf{ + size_t Functions{0}; + size_t Variables{0}; + size_t Jacobian{0}; + size_t TimeDeriv{0}; + + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) { + ar & Functions; + ar & Variables; + ar & Jacobian; + ar & TimeDeriv; + } + }; + + + + /** + \struct ConstantRecipe + + An exact, node-independent description of a constant baked into the program: enough to + (re)produce the constant's value at any working precision, without evaluating a function-tree + node (ADR-0027; node evaluation is being retired). Integers and rationals are stored exactly + (so they downsample to any precision without loss --- sidestepping any maximum-precision + setting); Pi/E are recomputed at the working precision; a Float literal carries its + authored-precision value (its inherent ceiling). + */ + struct ConstantRecipe{ + enum class Kind : int { Integer, Rational, Float, Pi, E }; + + Kind kind = Kind::Integer; + mpz_int int_value; //< Kind::Integer (exact) + mpq_rational rat_real, rat_imag; //< Kind::Rational (exact) + mpfr_complex float_value; //< Kind::Float (authored-precision literal; also a fixed variable's value) + size_t slot = 0; //< where this constant lives in the register file + + /// Produce the constant's value at the ambient working precision (ThreadPrecision), matching + /// the corresponding number node's FreshEval exactly. Definition + instantiations in the cpp. + template NumT Produce() const; + + friend class boost::serialization::access; + template + void serialize(Archive& ar, const unsigned /*version*/) { + int k = static_cast(kind); + ar & k; + kind = static_cast(k); + ar & int_value; + ar & rat_real; + ar & rat_imag; + ar & float_value; + ar & slot; + } + }; + + + + /** + \class SLPMemory + + The per-thread mutable working state of a straight-line program evaluation: the register + file (one bank per number type), the working precision, and the freshness / frozen-prologue + flags. Cheap to allocate; never shared across threads (ADR-0027). + */ + class SLPMemory{ + public: + template + std::vector& Get() { return std::get>(registers_); } + + template + std::vector const& Get() const { return std::get>(registers_); } + + //< The register file. Numbers and variables, plus temp results and output locations. It's + // all one block per number type. That's why it's called a SLP! + mutable std::tuple< std::vector, std::vector > registers_; + + mutable unsigned precision_ = 16; //< The current working number of digits + mutable bool is_evaluated_ = false; + + // Whether the frozen prologue's results in memory are valid. Tracked per number type: the + // double constants never change once computed; the mpfr constants are valid only while the + // working precision is unchanged. Transient (recomputed on first eval; not serialized). + mutable bool frozen_valid_dbl_ = false; + mutable unsigned frozen_valid_mp_precision_ = 0; + + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) { + ar & std::get>(registers_); + ar & std::get>(registers_); + ar & precision_; + ar & is_evaluated_; + // frozen_valid_* are transient (recomputed on first eval); not serialized. + } + }; + + + + /** + \class SLPProgram + + The immutable compiled straight-line program (ADR-0027): the instruction tape, the constant + recipe (true values of numbers + integer bank), and the memory layout (numbers of / locations + of things). Built once by the SLPCompiler; thereafter read-only, so it is shareable across + threads. Evaluation runs the tape against a per-thread SLPMemory. + */ + class SLPProgram{ friend SLPCompiler; + friend class StraightLineProgram; + friend std::ostream& operator <<(std::ostream& out, const StraightLineProgram & s); private: using Nd = std::shared_ptr; public: + using IntT = int; // this needs to co-vary on the stored type inside the node. node should stop using mpz, it's slow. + + SLPProgram() = default; + + bool HavePathVariable() const { return has_path_variable_; } + inline unsigned NumFunctions() const{ return static_cast(number_of_.Functions);} + inline unsigned NumVariables() const{ return static_cast(number_of_.Variables);} + inline size_t NumSlots() const { return num_slots_; } + inline size_t FirstLiveInstructionOffset() const { return first_live_instruction_; } /** - \struct OutputLocations + \brief loops through the instructions in the tape and evaluates each operation against the + given memory. - A struct encapsulating the starting locations of things in the SLP - */ - struct OutputLocations{ - size_t Functions{0}; - size_t Jacobian{0}; - size_t TimeDeriv{0}; - - friend class boost::serialization::access; - - template - void serialize(Archive& ar, const unsigned version) { - ar & Functions; - ar & Jacobian; - ar & TimeDeriv; - } + \tparam NumT numeric type + + uses a switch to find different operations from memory to make sure its performing the correct evaluations - }; + todo: implement a compile-time version of this using Boost.Hana + */ + template + void Eval(SLPMemory& memory) const; // this definition is in cpp, along with the lines that instantiate the needed versions. + private: /** - \struct InputLocations + \brief Add an instruction to the tape. This one's for binary operations - A struct encapsulating the starting locations of things in the SLP + \param binary_op The opcode, from the enum. + \param in_loc1 The location of the first operand + \param in_loc2 The locatiion in memory of the second operand + \param out_loc Where in memory to put the result of the operation. */ - struct InputLocations{ - size_t Variables{0}; - size_t Time{0}; + void AddInstruction(Operation binary_op, size_t in_loc1, size_t in_loc2, size_t out_loc); - friend class boost::serialization::access; + /** + \brief Add an instruction to the tape. This one's for unary operations - template - void serialize(Archive& ar, const unsigned version) { - ar & Variables; - ar & Time; - } - }; + \param unary_op The opcode, from the enum. + \param in_loc The location of the one and only operand + \param out_loc Where in memory to put the result of the operation. + */ + void AddInstruction(Operation unary_op, size_t in_loc, size_t out_loc); /** - \struct NumberOf - - A struct encapsulating the numbers of things appearing in the SLP + \brief Register an exact constant recipe and the memory location it downsamples into. */ - struct NumberOf{ - size_t Functions{0}; - size_t Variables{0}; - size_t Jacobian{0}; - size_t TimeDeriv{0}; - - friend class boost::serialization::access; - - template - void serialize(Archive& ar, const unsigned version) { - ar & Functions; - ar & Variables; - ar & Jacobian; - ar & TimeDeriv; - } - }; + void AddConstant(ConstantRecipe recipe); + + // Reorder `instructions_` into [frozen | live] and set `first_live_instruction_`. Called once + // at the end of compilation, after `num_slots_` is set. + void PartitionInstructions(); + + + bool has_path_variable_ = false; //< Does this SLP have a path variable? + + SLPNumberOf number_of_; //< Quantities of things + SLPOutputLocations output_locations_; //< Where to find outputs, like functions and derivatives + SLPInputLocations input_locations_; //< Where to find inputs, like variables and time + + std::vector integers_; + + std::vector instructions_; //< The instructions. The opcodes are stored as size_t's, as well as the locations of operands and results. + std::vector constant_recipes_; //< the exact constants, each carrying the slot to downsample into. + + // Freeze-set tape partition (ADR-0027). After compilation the instructions are stably + // reordered so every "frozen" instruction (one whose result depends only on frozen input + // slots --- the literal numbers, Pi/E; i.e. the freeze set is currently the constants) + // precedes every "live" instruction. `first_live_instruction_` is the word offset where the + // live segment begins. The frozen prologue depends only on precision, so a point-only change + // re-runs from `first_live_instruction_` and reuses the frozen slots already in memory; the + // whole tape runs only when the frozen values are not yet valid for the working precision. + size_t first_live_instruction_ = 0; + + size_t num_slots_ = 0; //< Total number of memory slots the program needs (per number bank). + + + friend class boost::serialization::access; + + template + void serialize(Archive& ar, const unsigned /*version*/) { + ar & has_path_variable_; + ar & number_of_; + ar & output_locations_; + ar & input_locations_; + ar & integers_; + ar & instructions_; + ar & constant_recipes_; + ar & first_live_instruction_; + ar & num_slots_; + } + }; + + + + /** + \class StraightLineProgram + + An implementation of straight-line programs, implemented with strong inspiration from Bertini1's implementation. + + One constructs a SLP from a system, like + + ``` + System my_system(); + StraightLineProgram slp(my_system); + ``` + + Maybe you don't need to know this, but in construction the SLP uses a helper class, the SLPCompiler + + Patches are just functions in this framework. The variables appear at the front of the memory, then functions, then derivatives. This should make copying data out easy, because it's all in one place. + + In contrast to Bertini1 SLP's, we don't put all the numbers at the front -- they just get scattered through the SLP's memory. + + The class is a thin facade (ADR-0027) over an immutable, shareable SLPProgram and a per-thread + SLPMemory. + */ + class StraightLineProgram{ + friend SLPCompiler; + friend std::ostream& operator <<(std::ostream& out, const StraightLineProgram & s); + + private: + using Nd = std::shared_ptr; + + public: /** The constructor -- how to make a SLP from a System. */ StraightLineProgram(System const & sys); - StraightLineProgram() = default; + StraightLineProgram() : program_(std::make_shared()) {} template void Eval(Eigen::MatrixBase const& variable_values) const { - using NumT = typename Derived::Scalar; SetVariableValues(variable_values); - - Eval(); - - + program_->Eval(memory_); } /** @@ -303,28 +512,15 @@ namespace bertini { // 1. copy variable values into memory locations they're supposed to go in SetVariableValues(variable_values); SetPathVariable(time); - Eval(); + program_->Eval(memory_); } - /** - \brief loops through the instructions in memory and evaluates each operation - - \tparam NumT numeric type - - uses a switch to find different operations from memory to make sure its performing the correct evaluations - - todo: implement a compile-time version of this using Boost.Hana - */ - template - void Eval() const; // this definition is in cpp, along with the lines that instantiate the needed versions. - - // a placeholder function that needs to be written. now just calls eval, since the eval functionality is both functions and jacobian wrapped together -- we don't keep arrays of their locations separately yet, so that would be the starting point. template void EvalFunctions() const{ - this->Eval(); + program_->Eval(memory_); } @@ -332,14 +528,14 @@ namespace bertini { // a placeholder function that needs to be written. now just calls eval, since the eval functionality is both functions and jacobian wrapped together -- we don't keep arrays of their locations separately yet, so that would be the starting point. template void EvalJacobian() const{ - this->Eval(); + program_->Eval(memory_); } // a placeholder function that needs to be written. now just calls eval, since the eval functionality is both functions and jacobian wrapped together -- we don't keep arrays of their locations separately yet, so that would be the starting point. template void EvalTimeDeriv() const{ - this->Eval(); + program_->Eval(memory_); } @@ -354,15 +550,15 @@ namespace bertini { */ template - void GetFuncValsInPlace(Vec & result) const{ - if (!is_evaluated_) - this->EvalFunctions(); + void GetFuncValsInPlace(Eigen::Ref> result) const{ + if (!memory_.is_evaluated_) + program_->Eval(memory_); - auto& memory = std::get>(memory_); + auto& memory = memory_.Get(); // copy content - for (int ii = 0; ii < number_of_.Functions; ++ii) { - result(ii) = memory[ii + output_locations_.Functions]; + for (size_t ii = 0; ii < program_->number_of_.Functions; ++ii) { + result(ii) = memory[ii + program_->output_locations_.Functions]; } } @@ -378,16 +574,16 @@ namespace bertini { */ template - void GetJacobianInPlace(Mat & result) const{ - if (!is_evaluated_) - this->EvalJacobian(); + void GetJacobianInPlace(Eigen::Ref> result) const{ + if (!memory_.is_evaluated_) + program_->Eval(memory_); - auto& memory = std::get>(memory_); + auto& memory = memory_.Get(); // copy content - for (int jj =0; jj < number_of_.Variables; ++jj) { - for (int ii = 0; ii < number_of_.Functions; ++ii) { - result(ii, jj) = memory[ii+jj*number_of_.Functions + output_locations_.Jacobian]; + for (size_t jj =0; jj < program_->number_of_.Variables; ++jj) { + for (size_t ii = 0; ii < program_->number_of_.Functions; ++ii) { + result(ii, jj) = memory[ii+jj*program_->number_of_.Functions + program_->output_locations_.Jacobian]; } } } @@ -404,15 +600,15 @@ namespace bertini { */ template - void GetTimeDerivInPlace(Vec & result) const{ - if (!is_evaluated_) - this->EvalTimeDeriv(); + void GetTimeDerivInPlace(Eigen::Ref> result) const{ + if (!memory_.is_evaluated_) + program_->Eval(memory_); - auto& memory = std::get>(memory_); + auto& memory = memory_.Get(); // 1. make container, size correctly. // 2. copy content - for (int ii = 0; ii < number_of_.Functions; ++ii) { - result(ii) = memory[ii + output_locations_.TimeDeriv]; + for (size_t ii = 0; ii < program_->number_of_.Functions; ++ii) { + result(ii) = memory[ii + program_->output_locations_.TimeDeriv]; } } @@ -425,7 +621,7 @@ namespace bertini { template Vec GetFuncVals() const{ Vec return_me(this->NumFunctions()); - GetFuncValsInPlace(return_me); + GetFuncValsInPlace(return_me); return return_me; } /** @@ -437,7 +633,7 @@ namespace bertini { template Mat GetJacobian() const{ Mat return_me(this->NumFunctions(), this->NumVariables()); - GetJacobianInPlace(return_me); + GetJacobianInPlace(return_me); return return_me; } /** @@ -449,14 +645,24 @@ namespace bertini { template Vec GetTimeDeriv() const{ Vec return_me(this->NumFunctions()); - GetTimeDerivInPlace(return_me); + GetTimeDerivInPlace(return_me); return return_me; } - inline unsigned NumFunctions() const{ return number_of_.Functions;} + inline unsigned NumFunctions() const{ return program_->NumFunctions();} - inline unsigned NumVariables() const{ return number_of_.Variables;} + inline unsigned NumVariables() const{ return program_->NumVariables();} + + /// Number of memory slots: one per distinct value the program holds (inputs, constants, + /// and one per compiled subexpression). Shared subexpressions get a single slot, so this + /// is a measure of the compiled (CSE'd) size of the program. + inline size_t NumMemorySlots() const{ return program_->NumSlots(); } + + /// Word offset into the instruction tape where the live segment begins (== total word length + /// of the frozen, constants-only prologue). Zero means the program has no frozen prologue. + /// Exposed for testing the freeze-set tape partition (ADR-0027). + inline size_t FirstLiveInstructionOffset() const { return program_->FirstLiveInstructionOffset(); } /** @@ -467,7 +673,7 @@ namespace bertini { inline unsigned precision() const { - return precision_; + return memory_.precision_; } /** @@ -485,7 +691,7 @@ namespace bertini { \return Well, does it? */ bool HavePathVariable() const { - return this->has_path_variable_; + return program_->has_path_variable_; } /** @@ -505,23 +711,24 @@ namespace bertini { void SetVariableValues(Eigen::MatrixBase const& variable_values) const{ using NumT = typename Derived::Scalar; -#if !defined(BERTINI_DISABLE_PRECISION_CHECKS) +#if !defined(BERTINI_DISABLE_PRECISION_CHECKS) // && _WIN32 - if (!std::is_same::value && Precision(variable_values)!=this->precision_){ + // An empty variable vector (a constant program with no variables) has no + // precision to read or check. + if (!std::is_same::value && variable_values.size() > 0 && Precision(variable_values)!=memory_.precision_){ std::stringstream err_msg; - err_msg << "variable_values and SLP must be of same precision. respective precisions: " << Precision(variable_values) << " " << this->precision_ << std::endl; + err_msg << "variable_values and SLP must be of same precision. respective precisions: " << Precision(variable_values) << " " << memory_.precision_ << std::endl; throw std::runtime_error(err_msg.str()); } #endif - using NumT = typename Derived::Scalar; - auto& memory = std::get>(memory_); // unpack for local reference + auto& memory = memory_.Get(); // unpack for local reference - for (int ii = 0; ii < number_of_.Variables; ++ii) { + for (size_t ii = 0; ii < program_->number_of_.Variables; ++ii) { //assign to memory - memory[ii + input_locations_.Variables] = variable_values(ii); + memory[ii + program_->input_locations_.Variables] = variable_values(ii); } - is_evaluated_ = false; + memory_.is_evaluated_ = false; } /** @@ -535,11 +742,11 @@ namespace bertini { template void SetPathVariable(ComplexT const& time) const{ -#if !defined(BERTINI_DISABLE_PRECISION_CHECKS) +#if !defined(BERTINI_DISABLE_PRECISION_CHECKS) // && _WIN32 - if (Precision(time)!= DoublePrecision() && Precision(time)!=this->precision_){ + if (Precision(time)!= DoublePrecision() && Precision(time)!=memory_.precision_){ std::stringstream err_msg; - err_msg << "time value and SLP must be of same precision. respective precisions: " << Precision(time) << " " << this->precision_ << std::endl; + err_msg << "time value and SLP must be of same precision. respective precisions: " << Precision(time) << " " << memory_.precision_ << std::endl; throw std::runtime_error(err_msg.str()); } #endif @@ -548,10 +755,10 @@ namespace bertini { throw std::runtime_error("calling Eval with path variable, but this StraightLineProgram doesn't have one."); // then actually copy the path variable into where it goes in memory - auto& memory = std::get>(memory_); // unpack for local reference + auto& memory = memory_.Get(); // unpack for local reference - memory[input_locations_.Time] = time; - is_evaluated_ = false; + memory[program_->input_locations_.Time] = time; + memory_.is_evaluated_ = false; } @@ -563,80 +770,41 @@ namespace bertini { private: - /** - \brief Add an instruction to memory. This one's for binary operations - - \param binary_op The opcode, from the enum. - \param in_loc1 The location of the first operand - \param in_loc2 The locatiion in memory of the second operand - \param out_loc Where in memory to put the result of the operation. - */ - void AddInstruction(Operation binary_op, size_t in_loc1, size_t in_loc2, size_t out_loc); - - /** - \brief Add an instruction to memory. This one's for unary operations - - \param unary_op The opcode, from the enum. - \param in_loc The location of the one and only operand - \param out_loc Where in memory to put the result of the operation. - */ - void AddInstruction(Operation unary_op, size_t in_loc, size_t out_loc); - - - /** - \brief Add a number to the memory at location, and memoize it for precision changing later. - */ - void AddNumber(Nd const num, size_t loc); - - template - auto& GetMemory() const{ - return std::get>(this->memory_); - } - + // Size the register file to the program's slot count and copy the constant values in. Called + // by the compiler once the program is built and memory_.precision_ is set. + void SetupMemory(); template void CopyNumbersIntoMemory() const; - mutable unsigned precision_ = 16; //< The current working number of digits - bool has_path_variable_ = false; //< Does this SLP have a path variable? - - NumberOf number_of_; //< Quantities of things - OutputLocations output_locations_; //< Where to find outputs, like functions and derivatives - InputLocations input_locations_; //< Where to find inputs, like variables and time - - mutable std::tuple< std::vector, std::vector > memory_; //< The memory of the object. Numbers and variables, plus temp results and output locations. It's all one block. That's why it's called a SLP! - std::vector integers_; - - std::vector instructions_; //< The instructions. The opcodes are stored as size_t's, as well as the locations of operands and results. - std::vector< std::pair > true_values_of_numbers_; //< the size_t is where in memory to downsample to. - - mutable bool is_evaluated_ = false; + std::shared_ptr program_; //< The immutable compiled program (shareable). + mutable SLPMemory memory_; //< The per-thread mutable working state. friend class boost::serialization::access; + // The program is serialized by value through the (owning, this-stage) shared_ptr, sidestepping + // boost's shared_ptr handling. Clone (system.cpp) recompiles the SLP after a round + // trip anyway; node_serialization round-trips it faithfully. template - void serialize(Archive& ar, const unsigned version) { - - ar & precision_; - ar & has_path_variable_; - - ar & number_of_; - ar & output_locations_; - ar & input_locations_; - - ar & std::get>(memory_); - ar & std::get>(memory_); - ar & integers_; - - ar & instructions_; - ar & true_values_of_numbers_; + void save(Archive& ar, const unsigned /*version*/) const { + SLPProgram const& prog = *program_; + ar & prog; + ar & memory_; + } - ar & is_evaluated_; + template + void load(Archive& ar, const unsigned /*version*/) { + auto prog = std::make_shared(); + ar & *prog; + program_ = prog; + ar & memory_; } + BOOST_SERIALIZATION_SPLIT_MEMBER() + }; @@ -650,8 +818,7 @@ namespace bertini { public Visitor, public Visitor, public Visitor, - public Visitor, - public Visitor, + public Visitor, public Visitor, // arithmetic @@ -691,7 +858,11 @@ namespace bertini { public: - SLP Compile(System const& sys); + // Compile from any source exposing the variable-ordering / functions / derivatives / + // path-variable accessors -- both System and blocks::PolynomialBlock qualify. + // Definition + explicit instantiations live in straight_line_program.cpp. + template + SLP Compile(SourceT const& source); // IF YOU ADD A THING HERE, YOU MUST ADD IT ABOVE AND IN THE CPP SOURCE @@ -701,8 +872,7 @@ namespace bertini { virtual void Visit(node::Integer const& n); virtual void Visit(node::Float const& n); virtual void Visit(node::Rational const& n); - virtual void Visit(node::Function const& n); - virtual void Visit(node::Jacobian const& n); + virtual void Visit(node::NamedExpression const& n); virtual void Visit(node::Differential const& n); // arithmetic @@ -731,14 +901,12 @@ namespace bertini { /** - \brief Provides a uniform interface for dealing with all numeric node types. + \brief Bake an exact constant into the program at the next available slot, and register + the node pointer so repeated references (CSE) share that slot. The recipe is built from + the concrete number node by the Visit methods (see RecipeFor in the cpp), reading the + node's true value directly --- no function-tree evaluation (ADR-0027). */ - template - void DealWithNumber(NodeT const& n){ - auto nd=n.shared_from_this(); // make a shared pointer to the node, so that it survives, and we get polymorphism - this->slp_under_construction_.AddNumber(nd, next_available_complex_); // register the number with the SLP - this->locations_encountered_nodes_[nd] = next_available_complex_++; // add to found symbols in the compiler, increment counter. - } + void RegisterConstant(Nd const& nd, ConstantRecipe recipe); /** \brief Reset the compiler to compile another SLP from another system. @@ -752,9 +920,8 @@ namespace bertini { std::map locations_encountered_nodes_; //< A registry of pointers-to-nodes and location in memory on where to find *their results* std::map locations_integers_; - std::map locations_top_level_functions_and_derivatives_; - SLP slp_under_construction_; //< the under-construction SLP. will be returned at end of `compile` + SLPProgram program_under_construction_; //< the under-construction program. wrapped into an SLP and returned at end of `Compile` }; @@ -764,9 +931,4 @@ namespace bertini { - - - - - #endif // for the ifndef include guards diff --git a/core/include/bertini2/system/system.hpp b/core/include/bertini2/system/system.hpp index d13364ad6..7ceefd803 100644 --- a/core/include/bertini2/system/system.hpp +++ b/core/include/bertini2/system/system.hpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include "bertini2/mpfr_complex.hpp" @@ -52,6 +53,7 @@ #include "bertini2/system/patch.hpp" #include "bertini2/system/straight_line_program.hpp" +#include "bertini2/system/blocks/block.hpp" #include #include @@ -61,33 +63,7 @@ namespace bertini { - - enum class EvalMethod - { - FunctionTree, // using virtual methods and recursion - SLP // using straight line programs - // now! 20230714, Eindhoven, Netherlands - }; - - enum class DerivMethod - { - JacobianNode, // using Jacobian nodes, which are either 1 or 0 when evaluated based on the variable of differentiation - Derivatives // classic differentiation, using more space in memory but not requiring a variable of differentation when evaluatiing - }; - - - /** - \brief Gets the default evaluation method for Jacobians. One might be faster... - */ - EvalMethod DefaultEvalMethod(); - - DerivMethod DefaultDerivMethod(); - - - /** - \brief Get the default value for whether a system should autosimplify. - */ - bool DefaultAutoSimplify(); + // (included above) so the evaluation blocks can see them. /** \brief The fundamental polynomial system class for Bertini2. @@ -100,18 +76,31 @@ namespace bertini { public: // a few local using statements to reduce typing etc. - using Fn = std::shared_ptr; + using NE = std::shared_ptr; using Var = std::shared_ptr; using Nd = std::shared_ptr; - using Jac = std::shared_ptr; - + /** \brief The default constructor for a system. */ - System() : is_differentiated_(false), have_path_variable_(false), have_ordering_(false), precision_(DefaultPrecision()), is_patched_(false) + System() : have_path_variable_(false), is_patched_(false), is_differentiated_(false), have_ordering_(false), precision_(DefaultPrecision()) {} - /** + /** + \brief Construct a system from a list of functions. + + The functions are added to the system, the variables appearing in them are + automatically discovered (see node::GatherVariables), and those variables are + placed into a single affine variable group (ordered alphabetically by name). + This is a convenience for programmatically building a system without having to + assemble the variable group by hand. + + \param functions The functions which define the system. + */ + explicit + System(std::vector const& functions); + + /** \brief The copy operator, creates a system from a string using the Bertini parser for Bertini classic syntax. */ explicit @@ -166,101 +155,6 @@ namespace bertini { void Differentiate() const; - - /** - \brief Force re-evaluation of the system next eval of functions. If something has changed in the system, call this. - */ - void ResetFunctions() const - { - // TODO: it has the unfortunate side effect of resetting constant functions, too. - switch (eval_method_){ - case EvalMethod::FunctionTree: - for (const auto& iter : functions_) - iter->Reset(); - break; - case EvalMethod::SLP: - // nothing - break; - } - - } - - /** - \brief Force re-evaluation of the system next eval of Jacobians. If something has changed in the system, call this. - */ - void ResetJacobian() const - { - switch (eval_method_) - { - case EvalMethod::FunctionTree:{ - - switch (deriv_method_){ - case DerivMethod::JacobianNode: - { - for (const auto& iter : jacobian_) - iter->Reset(); - break; - } - case DerivMethod::Derivatives: - { - for (const auto& iter : space_derivatives_) - iter->Reset(); - break; - } - } - - break; - } - case EvalMethod::SLP: - { - // nothing to do, it's not a resetting kind of thing. - break; - } - - } - } - - void ResetTimeDerivatives() const - { - switch (eval_method_) - { - case EvalMethod::FunctionTree:{ - - switch (deriv_method_){ - case DerivMethod::JacobianNode: - { - for (const auto& iter : jacobian_) - iter->Reset(); - break; - } - case DerivMethod::Derivatives: - { - for (const auto& iter : time_derivatives_) - iter->Reset(); - break; - } - } - - break; - } - case EvalMethod::SLP: - { - // nothing to do, it's not a resetting kind of thing. - break; - } - - } - } - - /** - \brief A complete reset of the system, so that all of functions, space derivatives, and time derivatives will all be re-evaluated. - */ - void Reset() const - { - ResetFunctions(); - ResetJacobian(); - ResetTimeDerivatives(); - } /** \brief Evaluate the system using the previously set variable (and time) values, in place. @@ -272,36 +166,21 @@ namespace bertini { void EvalInPlace(Vec & function_values) const { - if (function_values.size() != NumTotalFunctions()) + if (function_values.size() != static_cast(NumTotalFunctions())) { std::stringstream ss; ss << "trying to evaluate system in-place, but number length of vector into which to write the values (" << function_values.size() << ") doesn't match number of system user-defined functions plus patches ( " << NumNaturalFunctions() << "+" << NumPatches() << ") = " << NumTotalFunctions() << "). Use System.NumTotalFunctions() to make the container for in-place evaluation"; throw std::runtime_error(ss.str()); } - switch (eval_method_){ - case EvalMethod::FunctionTree: - { - unsigned counter(0); - for (auto iter=functions_.begin(); iter!=functions_.end(); iter++, counter++) { - (*iter)->EvalInPlace(function_values(counter)); - } - break; - } - - case EvalMethod::SLP: - { - slp_.GetFuncValsInPlace(function_values); - } - break; - } - + if (!is_differentiated_) + Differentiate(); // syncs + (for the polynomial block) compiles the SLP + EvalBlocksInPlace(function_values); if (IsPatched()) patch_.EvalInPlace(function_values, - std::get >(current_variable_values_)); // does a patch not have a caching mechanism? - // .segment(NumNaturalFunctions(),NumTotalVariableGroups()) - + std::get >(current_variable_values_)); + CoerceBlockOutputPrecision(function_values); } @@ -342,7 +221,7 @@ namespace bertini { { static_assert(std::is_same::value,"scalar types must match"); - if (variable_values.size()!=NumVariables()) + if (variable_values.size()!=static_cast(NumVariables())) { std::stringstream ss; ss << "trying to evaluate system, but number of input variables (" << variable_values.size() << ") doesn't match number of system variables (" << NumVariables() << ")."; @@ -352,7 +231,6 @@ namespace bertini { throw std::runtime_error("not using a time value for evaluation of system, but path variable IS defined."); SetVariables(variable_values.eval()); - ResetFunctions(); EvalInPlace(function_values); } @@ -408,15 +286,14 @@ namespace bertini { { static_assert(std::is_same::value, "scalar types must be the same"); - if (variable_values.size()!=NumVariables()) + if (variable_values.size()!=static_cast(NumVariables())) throw std::runtime_error("trying to evaluate system, but number of variables doesn't match."); if (!have_path_variable_) throw std::runtime_error("trying to use a time value for evaluation of system, but no path variable defined."); - SetVariables(variable_values.eval());//TODO: remove this eval + SetVariables(variable_values.eval()); SetPathVariable(path_variable_value); - ResetFunctions(); // todo, elimiante this. i feel like setting the variables or path variable should be enough to set the flag/ take the action EvalInPlace(function_values); } @@ -473,48 +350,18 @@ namespace bertini { { - if(J.rows() != NumTotalFunctions() || J.cols() != NumVariables()) + if(J.rows() != static_cast(NumTotalFunctions()) || J.cols() != static_cast(NumVariables())) { throw std::runtime_error("trying to evaluate jacobian of system in place, but input J doesn't have right number of columns or rows"); } - const auto& vars = Variables(); - if (!is_differentiated_) Differentiate(); - switch (eval_method_) - { - case EvalMethod::FunctionTree: - { - switch (deriv_method_){ - case DerivMethod::JacobianNode:{ - for (int ii = 0; ii < NumNaturalFunctions(); ++ii) - for (int jj = 0; jj < NumVariables(); ++jj) - jacobian_[ii]->EvalJInPlace(J(ii,jj),vars[jj]); - break; - } - case DerivMethod::Derivatives: - { - for (int jj = 0; jj < NumVariables(); ++jj) - for (int ii = 0; ii < NumNaturalFunctions(); ++ii) - space_derivatives_[ii+jj*NumNaturalFunctions()]->EvalInPlace(J(ii,jj)); - break; - } - } - break; - } // function tree branch - - case EvalMethod::SLP: - { - this->slp_.GetJacobianInPlace(J); // the variable values should have been copied into place elsewhere. that's not this function's responsibility. - break; - } - } - + JacobianBlocksInPlace(J); if (IsPatched()) - patch_.JacobianInPlace(J,std::get >(current_variable_values_)); - + patch_.JacobianInPlace(J, std::get >(current_variable_values_)); + CoerceBlockOutputPrecision(J); } @@ -554,7 +401,7 @@ namespace bertini { void JacobianInPlace(Mat & J, const Vec & variable_values) const { - if (variable_values.size()!=NumVariables()) + if (variable_values.size()!=static_cast(NumVariables())) throw std::runtime_error("trying to evaluate jacobian, but number of variables doesn't match."); if (HavePathVariable()) @@ -581,7 +428,7 @@ namespace bertini { template Mat Jacobian(const Vec & variable_values) const { - if (variable_values.size()!=NumVariables()) + if (variable_values.size()!=static_cast(NumVariables())) throw std::runtime_error("trying to evaluate jacobian, but number of variables doesn't match."); if (HavePathVariable()) @@ -611,15 +458,14 @@ namespace bertini { { static_assert(std::is_same::value, "scalar types must be the same"); - if (variable_values.size()!=NumVariables()) + if (variable_values.size()!=static_cast(NumVariables())) throw std::runtime_error("trying to evaluate jacobian, but number of variables doesn't match."); if (!HavePathVariable()) throw std::runtime_error("trying to use a time value for computation of jacobian, but no path variable defined."); - SetVariables(variable_values.eval()); // TODO: remove this eval + SetVariables(variable_values.eval()); SetPathVariable(path_variable_value); - ResetJacobian(); JacobianInPlace(J); } @@ -658,7 +504,7 @@ namespace bertini { template Mat Jacobian(const Vec & variable_values, const T & path_variable_value) const { - if (variable_values.size()!=NumVariables()) + if (variable_values.size()!=static_cast(NumVariables())) throw std::runtime_error("trying to evaluate jacobian, but number of variables doesn't match."); if (!HavePathVariable()) @@ -685,9 +531,8 @@ namespace bertini { { static_assert(std::is_same::value, "scalar types must be the same"); - SetVariables(variable_values.eval()); //TODO: remove this eval() + SetVariables(variable_values.eval()); SetPathVariable(path_variable_value); - ResetTimeDerivatives(); TimeDerivativeInPlace(ds_dt); } @@ -727,8 +572,7 @@ namespace bertini { { static_assert(std::is_same::value, "scalar types must be the same"); - SetVariables(variable_values.eval()); //TODO: remove this eval() - ResetTimeDerivatives(); + SetVariables(variable_values.eval()); TimeDerivativeInPlace(ds_dt); } @@ -760,7 +604,7 @@ namespace bertini { void TimeDerivativeInPlace(Vec & ds_dt) const { - if(ds_dt.size() < NumNaturalFunctions()) + if(ds_dt.size() < static_cast(NumNaturalFunctions())) { std::stringstream ss; ss << "trying to evaluate system in place, but number of input functions (" << ds_dt.size() << ") doesn't match number of system functions (" << NumNaturalFunctions() << ")."; @@ -770,42 +614,15 @@ namespace bertini { if (!HavePathVariable()) throw std::runtime_error("computing time derivative of system with no path variable defined"); - if (!is_differentiated_) Differentiate(); - switch (eval_method_) - { - case EvalMethod::FunctionTree:{ - switch (deriv_method_){ - case DerivMethod::JacobianNode: - { - for (int ii = 0; ii < NumNaturalFunctions(); ++ii) - jacobian_[ii]->EvalJInPlace(ds_dt(ii), path_variable_); - break; - } - case DerivMethod::Derivatives: - { - for (int ii = 0; ii < NumNaturalFunctions(); ++ii) - time_derivatives_[ii]->EvalInPlace(ds_dt(ii)); - break; - } - } - break; - } // function tree branch - - case EvalMethod::SLP: - { - this->slp_.GetTimeDerivInPlace(ds_dt); // the variable values should have been copied into place elsewhere. that's not this function's responsibility. - break; - } - } - + TimeDerivBlocksInPlace(ds_dt); // the patch doesn't move with time. derivatives 0. if (IsPatched()) - for (int ii = 0; ii < NumTotalVariableGroups(); ++ii) - ds_dt(ii+NumNaturalFunctions()) = T(0); - + for (size_t ii = 0; ii < NumTotalVariableGroups(); ++ii) + ds_dt(ii + NumNaturalFunctions()) = T(0); + CoerceBlockOutputPrecision(ds_dt); } @@ -839,6 +656,18 @@ namespace bertini { */ void Homogenize(); + /** + Homogenize the system reusing externally-supplied homogenizing variables -- one per affine + variable group, in group order -- instead of minting fresh ones. Used by + `RandomizationBlock` so a wrapped operand system shares the owning system's homogenizing + variables (the h-power deficit factors and the operand's functions must live in the same + homogeneous coordinates). Otherwise behaves exactly like `Homogenize()` on a fresh system. + + \throws std::runtime_error if the system is non-polynomial, already homogenized, or the + number of supplied homogenizing variables does not equal the number of affine variable groups. + */ + void Homogenize(VariableGroup const& provided_hom_vars); + /** Checks whether a system is homogeneous, overall. This means with respect to each variable group (including homogenizing variable if defined), homogeneous variable group, and ungrouped variables, if defined. @@ -896,6 +725,12 @@ namespace bertini { */ size_t NumHomVariables() const; + /** + Get the homogenizing variables, one per affine variable group (in group order), or + an empty container if the system is not homogenized. + */ + VariableGroup const& HomogenizingVariables() const { return homogenizing_variables_; } + /** Get the total number of variable groups in the system, including both affine and homogenous. Ignores the ungrouped variables, because they are not in any group. */ @@ -965,42 +800,26 @@ namespace bertini { template void SetVariables(const Vec & new_values) const { - if (new_values.size()!= NumVariables()) + if (new_values.size()!= static_cast(NumVariables())) throw std::runtime_error("variable vector of different length from system-owned variables in SetVariables"); - const auto& vars = Variables(); - #ifndef BERTINI_DISABLE_PRECISION_CHECKS - if (!std::is_same::value && (Precision(new_values) != this->precision())) - throw std::runtime_error("precision of input point in SetVariables (" + std::to_string(Precision(new_values)) + ") must match the precision of the system (" + std::to_string(this->precision()) + ")."); - - if (!std::is_same::value && (vars[0]->node::NamedSymbol::precision() != this->precision()) ) - throw std::runtime_error("internally, precision of variables (" + std::to_string(vars[0]->node::NamedSymbol::precision()) + ") in SetVariables must match the precision of the system (" + std::to_string(this->precision()) + ")."); - #endif - - if (!is_differentiated_) - Differentiate(); - - - switch (eval_method_){ - case EvalMethod::FunctionTree:{ - auto counter = 0; - - for (auto iter=vars.begin(); iter!=vars.end(); iter++, counter++) { - (*iter)->set_current_value(new_values(counter)); + // A system with no variables (a constant) has an empty point: there is no + // precision to read from it, so skip the check. + if (new_values.size() > 0) + { + if constexpr (!std::is_same::value) { + if (Precision(new_values) != this->precision()) + throw std::runtime_error("precision of input point in SetVariables (" + std::to_string(Precision(new_values)) + ") must match the precision of the system (" + std::to_string(this->precision()) + ")."); } - - std::get >(current_variable_values_) = new_values; - break; } - case EvalMethod::SLP:{ - std::get >(current_variable_values_) = new_values; // if this isn't here, then patch evaluation breaks. - slp_.SetVariableValues(new_values); - break; - } - } // switch + #endif - + // Blocks are value-in: the polynomial block feeds this stored vector into its SLP, the + // structured blocks compute on it directly, and the patch reads it too (see + // EvalBlocksInPlace). The shared Variable nodes are no longer written during evaluation + // (ADR-0027), so the node DAG stays read-only across threads. + std::get >(current_variable_values_) = new_values; } @@ -1019,55 +838,29 @@ namespace bertini { if (!have_path_variable_) throw std::runtime_error("trying to set the value of the path variable, but one is not defined for this system"); - if (!is_differentiated_) - Differentiate(); - - switch (eval_method_){ - case EvalMethod::FunctionTree:{ - path_variable_->set_current_value(new_value); - break; - } - case EvalMethod::SLP:{ - path_variable_->set_current_value(new_value); - slp_.SetPathVariable(new_value); - } - } + // Store the path value in the System's own per-thread buffer, NOT the shared node. + // Blocks are value-in and receive the path value as an argument (see EvalBlocksInPlace / + // CurrentPathValue), so the path-variable node is never read during evaluation (ADR-0027). + std::get(current_path_value_) = new_value; } + // Stage the system's current point (variables, and optionally the path value) for a + // subsequent Eval/Jacobian. (Formerly also reset the function-tree node caches; node-level + // evaluation is gone, so there is nothing to reset -- the SLP carries its own state.) template void SetAndReset(Vec const& new_space, T const& new_time) const { SetVariables(new_space); SetPathVariable(new_time); - - Reset(); } template void SetAndReset(Vec const& new_space) const { SetVariables(new_space); - - Reset(); } - /** - For a system with implicitly defined parameters, set their values. The values are determined externally to the system, and are tracked along with the variables. - \tparam T the number-type for return. Probably dbl=std::complex, or mpfr_complex=bertini::mpfr_complex. - \param new_values The new updated values for the implicit parameters. - */ - template - void SetImplicitParameters(Vec new_values) const - { - if (new_values.size()!= implicit_parameters_.size()) - throw std::runtime_error("trying to set implicit parameter values, but there is a size mismatch"); - - size_t counter = 0; - for (auto iter=implicit_parameters_.begin(); iter!=implicit_parameters_.end(); iter++, counter++) - (*iter)->set_current_value(new_values(counter)); - - } @@ -1091,9 +884,22 @@ namespace bertini { void AddVariableGroup(VariableGroup const& v); + /** + \brief Replace the entire variable-group structure of the system. + + Clears all existing affine, homogeneous, and ungrouped variables (and any + homogenizing variables introduced by a prior Homogenize), then installs the + supplied groups as the system's affine variable groups, in order. The path + variable, if any, is preserved. + + \param groups The affine variable groups to install. + */ + void SetVariableGroups(std::vector const& groups); + + /** Add a homogeneous (projective) variable group to the system. The system must be homogeneous with respect to this group, though this is not verified at the time of this call. - + \param v The variable group to add. */ void AddHomVariableGroup(VariableGroup const& v); @@ -1139,14 +945,7 @@ namespace bertini { \param F The parameter to add. */ - void AddParameter(Fn const& F); - - /** - Add some explicit parameters to the system. Explicit parameters should depend only on the path variable, though this is not checked in this function. - - \param F The parameters to add. - */ - void AddParameters(std::vector const& F); + void AddParameter(NE const& F); @@ -1157,7 +956,6 @@ namespace bertini { \param F The subfunction to add. */ - void AddSubfunction(Fn const& F); /** Add some subfunctions to the system. @@ -1166,24 +964,15 @@ namespace bertini { \param F The subfunctions to add. */ - void AddSubfunctions(std::vector const& F); - - - /** - Add a function to the system. - \param F The function to add. - */ - void AddFunction(Fn const& F); /** - Add a function to the system. + Add a function to the system, as a bare expression. \param F The function to add. - \param name The name of the function */ - void AddFunction(Nd const& F, std::string const& name = "unnamed_function"); + void AddFunction(Nd const& F); /** @@ -1191,28 +980,125 @@ namespace bertini { \param F The functions to add. */ - void AddFunctions(std::vector const& F); + void AddFunctions(std::vector const& F); + /** + \brief Append an evaluation block (products-of-linears, blend, ...). + A block-composed system evaluates its blocks (in the order added) instead of the + function-tree / SLP functions; blocks contribute the leading "natural" rows, with + any patch appended after, exactly as for a classic system. + */ + void AddBlock(Block b) { blocks_.push_back(std::move(b)); } + /// Read-only access to the system's evaluation blocks (in evaluation order). For + /// introspection / testing -- e.g. reading a RandomizationBlock's matrix; the variant + /// itself stays out of the Python surface. + std::vector const& Blocks() const { return blocks_; } + /// Remove the polynomial block (the System's natural functions), leaving any structured + /// blocks and the variable structure / patch intact. Used to turn a copy of a System + /// into a homotopy shell whose rows come from a blend block rather than its own + /// functions (FormHomotopy): `h = target; h.ClearFunctions(); h.AddBlock(blend);`. + void ClearFunctions() + { + for (auto it = blocks_.begin(); it != blocks_.end(); ) + { + if (std::holds_alternative(*it)) + it = blocks_.erase(it); + else + ++it; + } + InvalidateDifferentiation(); + } + /// \brief Whether this system is evaluated from blocks rather than the function tree. + bool HasBlocks() const { return !blocks_.empty(); } + + /// Does the system have any non-polynomial (structured) block -- products-of-linears, + /// linear-forms, blend? Every system has a PolynomialBlock for its functions after the + /// fold, so HasBlocks() is no longer the right test for "needs whole-System blending"; + /// this is. (e.g. an MHom start system has a products block; a total-degree start does not.) + bool HasStructuredBlocks() const + { + for (auto const& b : blocks_) + if (!std::holds_alternative(b)) + return true; + return false; + } + + /// \brief Remove all evaluation blocks (the system reverts to its function-tree + /// functions). Mainly for testing the block path against the function-tree path. + void ClearBlocks() { blocks_.clear(); } + + /// \brief Build an equivalent **pure function-tree** System: every block's functions + /// expressed as function-tree nodes, gathered into a single PolynomialBlock, with the + /// same variables, path variable, and patch (the patch is reused, not re-expressed). + /// + /// This is a verification / interop oracle — the block path exists for performance and + /// precision control, so this is NOT a replacement for block evaluation. It lets the + /// block-composed evaluation be cross-checked against the function-tree path + /// (eval / Jacobian must agree). Scoped to the current block types; a block that cannot + /// be expanded throws. + System ExpandToFunctionTree() const; + + /// \brief The system's natural (pre-patch) functions as function-tree expression nodes, + /// expanding any structured block. Used by ExpandToFunctionTree and, recursively, by + /// BlendBlock expansion (a blend is sum_i c_i(t) * operand_i, each operand expanded). + std::vector NaturalFunctionsAsNodes() const; /** - Add a constant function to the system. Constants must not depend on anything which can vary -- they're constant! + \brief Randomize an overdetermined system down to a square one, returning a NEW system. - \param C The constant to add. - */ - void AddConstant(Fn const& C); + An overdetermined system (N natural functions, n variables, N > n) is replaced by n generic + combinations whose isolated solutions still contain this system's -- the standard squaring-up + so the isolated solutions can be found by homotopy continuation (solve the square result, + then discard the extraneous solutions by re-evaluating this system). + The returned system carries a single `RandomizationBlock` wrapping a copy of this system; the + combination is `g_i = sum_j R_ij f_j` (the block applies the homogenizing-variable powers + needed when the functions differ in degree). **This system is not mutated** -- any internal + degree sorting happens on the copy. + + Auto form: for a single affine variable group the functions are sorted by descending degree + and `R = [I | C]` (C random), giving the optimal total-degree path count (the product of the + n largest degrees); for several variable groups a dense random `R` is used with a common + target multidegree. + + \throws std::runtime_error if the system is underdetermined (fewer functions than variables). + */ + System Randomize() const; /** - Add some constant functions to the system. Constants must not depend on anything which can vary -- they're constant! + \brief Randomize using a caller-supplied (exact) coefficient matrix R, leaving the functions + in their current order. R has one row per desired randomized function and one column per + natural function of this system. Returns a NEW system; this one is not mutated. + + \throws std::runtime_error if R's column count does not equal this system's natural-function count. + */ + System Randomize(Mat const& R) const; + + /** + \brief The randomization matrix R of a system produced by Randomize() (its first + RandomizationBlock's n x N coefficient matrix). + + \throws std::runtime_error if this system carries no randomization block. + */ + Mat RandomizationMatrix() const; + + - \param C The constants to add. + + + + + /** + Add a constant function to the system. Constants must not depend on anything which can vary -- they're constant! + + \param C The constant to add. */ - void AddConstants(std::vector const& C); + void AddConstant(NE const& C); @@ -1232,12 +1118,7 @@ namespace bertini { */ bool HavePathVariable() const; - auto& GetPathVariable() const{ - if (this->HavePathVariable()) - return this->path_variable_; - else - throw std::runtime_error("trying to get path variable for a system which doesn't have a path variable defined"); - } + const Var& GetPathVariable() const; /** Order the variables, by the order in which the groups were added. @@ -1288,7 +1169,7 @@ namespace bertini { Vec DehomogenizePoint(Vec const& x) const { - if (x.size()!=NumVariables()){ + if (x.size()!=static_cast(NumVariables())){ std::stringstream message; message << "dehomogenizing point with incorrect number of coordinates. input has "; message << x.size(); @@ -1304,6 +1185,68 @@ namespace bertini { } + /** + \brief The infinity norm of a point after dehomogenization. + + This is the single canonical "how big is this point, in user coordinates" measurement. + An endpoint going to infinity has its dehomogenized coordinates blow up, so this is what + the endgames test against `Security::max_norm` to detect divergence, and what the + zero-dim solver tests against `endpoint_finite_threshold` to classify finite/infinite + endpoints. Routing both through here keeps those decisions consistent: never compare the + raw internal (homogenized, on-patch) coordinates, which carry the homogenizing variable + and patch scaling. + + \tparam T the number-type of the point. Returns the associated real magnitude type. + */ + template + auto InfinityNormOfDehomogenized(Vec const& x) const + { + return DehomogenizePoint(x).template lpNorm(); + } + + + /** + \brief Take a point in user (dehomogenized) coordinates into this system's internal coordinates. + + Two steps: (1) insert the homogenizing coordinate, with value 1, for each affine + variable group, per the FIFO variable ordering; (2) if the system is patched, + rescale the result onto the patch. The result is the projectively-identical + point expressed in the coordinates the solver works in -- suitable for + comparison with internal-coordinate solutions, or as a start point for further + tracking re-using this system's patch. + + This is the inverse of DehomogenizePoint: DehomogenizePoint(HomogenizePoint(p)) == p. + On an unhomogenized, unpatched system this is the identity. + + \tparam T the number-type. Probably dbl=std::complex, or mpfr_complex=bertini::mpfr_complex. + + \throws std::runtime_error, if there is a mismatch between the number of variables in the input point, and the number of natural variables of the system. + */ + template + Vec HomogenizePoint(Vec const& x) const + { + + if (x.size()!=static_cast(NumNaturalVariables())){ + std::stringstream message; + message << "homogenizing point with incorrect number of coordinates. input has "; + message << x.size(); + message << " but system expects "; + message << NumNaturalVariables(); + throw std::runtime_error(message.str()); + } + + if (!have_ordering_) + ConstructOrdering(); + + auto x_homogenized = HomogenizePointFIFO(x); + + if (IsPatched()) + RescalePointToFitPatchInPlace(x_homogenized); + + return x_homogenized; + } + + /** @@ -1313,16 +1256,18 @@ namespace bertini { */ auto Function(unsigned index) const { - return functions_[index]; + return PolyBlockPtr()->Functions()[index]; } - + /** - \brief Get the functions. + \brief Get the functions. */ - auto GetNaturalFunctions() const + std::vector GetNaturalFunctions() const { - return functions_; + if (auto* p = PolyBlockPtr()) + return p->Functions(); + return {}; } @@ -1480,6 +1425,16 @@ namespace bertini { { patch_.RescalePointToFitInPlace(x); } + /** + \brief Human-facing description of the system, block by block. + + Each block describes the rows it owns (labelled `f_k` in function-vector order). `verbose == + false` (the default, used by `operator<<` / Python `str`) shows placeholder symbols for the + structured blocks' matrices/coefficients; `verbose == true` reveals the actual numbers and the + underlying functions of randomization / blend blocks. This is for reading, not re-parsing. + */ + void Describe(std::ostream& out, bool verbose = false) const; + /** \brief Overloaded operator for printing to an arbirtary out stream. */ @@ -1492,6 +1447,10 @@ namespace bertini { void ClearVariables(); + + + + /** \brief Copy the entire structure of variables from within one system to another. @@ -1503,59 +1462,11 @@ namespace bertini { */ void CopyVariableStructure(System const& other); - /** - \brief One of a family of functions indicating whether we can assume the system will always have uniform precision. - - \see PleaseAssumeUniformPrecision AssumeUniformPrecision IsAssumingUniformPrecision - */ - void DontAssumeUniformPrecision() - { - AssumeUniformPrecision(false); - } - - void PleaseAssumeUniformPrecision() - { - AssumeUniformPrecision(true); - } - - void AssumeUniformPrecision(bool val) - { - assume_uniform_precision_ = false; - } - - /** - \brief yon getter for the obvious thing it gets - */ - auto IsAssumingUniformPrecision() const - { - return assume_uniform_precision_; - } - - inline - void PleaseAutoSimplify() - { - SetAutoSimplify(true); - } - - inline - void DontAutoSimplify() - { - SetAutoSimplify(false); - } - - void SetAutoSimplify(bool val) - { - auto_simplify_ = val; - } - - - /** - \brief Query the state of autosimplification - */ - auto IsAutoSimplifying() const - { - return auto_simplify_; - } + // The Please/Dont AssumeUniformPrecision family was removed: the setter had + // ignored its argument (always storing false) for ages, so the early-out in + // System::precision() it was meant to enable was dead code, and skipping the + // propagation is unsound anyway (e.g. the SLP can be at a different precision + // than precision_ claims). precision() now always propagates. /** \brief Simplify the functions contained in the system. @@ -1579,42 +1490,9 @@ namespace bertini { void Simplify(); - /** - \brief Set method being used for evaluation - * */ - void SetEvalMethod(EvalMethod method) - { - eval_method_ = method; - } - - /** - \brief Query the current method used for evaluation - * */ - EvalMethod GetEvalMethod() const - { - return eval_method_; - } - - /** - \brief Set method being used for differentiation - * */ - void SetDerivMethod(DerivMethod method) - { - deriv_method_ = method; - } - - /** - \brief Query the current method used for differentiation - * */ - DerivMethod GetDerivMethod() const - { - return deriv_method_; - } - - /** \brief Add two systems together. @@ -1655,6 +1533,11 @@ namespace bertini { friend const System operator*(Nd const& N, System const& s); private: + /// Shared back end of the Randomize overloads: given the overdetermined operand (already a + /// copy, sorted or not) and a finished coefficient matrix, compute the per-row target + /// multidegrees, build the RandomizationBlock, and return a new system carrying it. + System AssembleRandomized(std::shared_ptr operand, Mat coefficients) const; + /** \brief Get the sizes according to the FIFO ordering. */ @@ -1715,6 +1598,7 @@ namespace bertini { { for (unsigned ii = 0; ii < hom_variable_groups_[hom_group_counter].size(); ++ii) x_dehomogenized(dehom_index++) = x(hom_index++); + hom_group_counter++; // was missing; mattered only for multiple hom groups of differing sizes break; } case VariableGroupType::Ungrouped: @@ -1733,14 +1617,201 @@ namespace bertini { return x_dehomogenized; } - void DifferentiateUsingDerivatives() const; - void DifferentiateUsingJacobianNode() const; + + /** + \brief FIFO-ordering implementation of HomogenizePoint's first step: insert + the homogenizing coordinate, with value 1, at each affine group's slot. + Homogeneous groups and ungrouped variables pass through. Patch rescaling is + the caller's job. + */ + template + Vec HomogenizePointFIFO(Vec const& x) const + { + #ifndef BERTINI_DISABLE_ASSERTS + assert(homogenizing_variables_.size()==0 || homogenizing_variables_.size()==NumVariableGroups() && "must have either 0 homogenizing variables, or the number of homogenizing variables must match the number of affine variable groups."); + #endif + + bool is_homogenized = homogenizing_variables_.size()!=0; + if (!is_homogenized) + return x; + + Vec x_homogenized(NumVariables()); + + unsigned affine_group_counter = 0; + unsigned hom_group_counter = 0; + + unsigned dehom_index = 0; // index into x, the user-coordinates point + unsigned hom_index = 0; // index into x_homogenized, the point we are computing + + for (auto& iter : time_order_of_variable_groups_) + { + switch (iter){ + case VariableGroupType::Affine: + { + x_homogenized(hom_index++) = T(1); + for (unsigned ii = 0; ii < variable_groups_[affine_group_counter].size(); ++ii) + x_homogenized(hom_index++) = x(dehom_index++); + affine_group_counter++; + break; + } + case VariableGroupType::Homogeneous: + { + for (unsigned ii = 0; ii < hom_variable_groups_[hom_group_counter].size(); ++ii) + x_homogenized(hom_index++) = x(dehom_index++); + hom_group_counter++; + break; + } + case VariableGroupType::Ungrouped: + { + x_homogenized(hom_index++) = x(dehom_index++); + break; + } + default: + { + throw std::runtime_error("unacceptable VariableGroupType in HomogenizePointFIFO"); + } + } + } + + return x_homogenized; + } + + // --- the System's polynomial block (its functions live here after the fold) --- + + /// Get the System's PolynomialBlock, creating an (empty) one in blocks_ if none exists. + blocks::PolynomialBlock& PolyBlock() + { + for (auto& b : blocks_) + if (auto* p = std::get_if(&b)) + return *p; + blocks_.emplace_back(blocks::PolynomialBlock{}); + return std::get(blocks_.back()); + } + + /// Find the System's PolynomialBlock, or nullptr if it has none (e.g. a pure + /// structured system, or a freshly-constructed one with no functions yet). + blocks::PolynomialBlock const* PolyBlockPtr() const + { + for (auto const& b : blocks_) + if (auto* p = std::get_if(&b)) + return p; + return nullptr; + } + + /// The polynomial block's functions (an empty list if there is no polynomial block). + std::vector const& PolyFunctions() const + { + static const std::vector none; + auto* p = PolyBlockPtr(); + return p ? p->Functions() : none; + } + + /// Mark the blocks as needing (re)differentiation after a structural change. + void InvalidateDifferentiation() const + { + is_differentiated_ = false; + if (auto* p = PolyBlockPtr()) + p->Invalidate(); + } + + /// Push the System-owned context (variable ordering, path variable, auto-simplify) into + /// the PolynomialBlock before it differentiates/evaluates. The block's setters are + /// idempotent (they only invalidate on real change), so this is safe to call repeatedly. + void SyncPolyBlock() const + { + if (auto* p = PolyBlockPtr()) + { + p->SetVariableOrdering(Variables()); + if (have_path_variable_) p->SetPathVariable(path_variable_); + else p->ClearPathVariable(); + } + } /** Puts together the ordering of variables, and stores it internally. */ void ConstructOrdering() const; + /// \brief The current path-variable value as type T (zero if no path variable). + template + T CurrentPathValue() const + { + if (have_path_variable_) + return std::get(current_path_value_); + return T(0); + } + + /// \brief Force a block-path evaluation result to the system's working precision. + /// + /// A block evaluates correctly at its working precision, but the *result* container + /// (function values / Jacobian / time derivative) is allocated by the caller, often at + /// whatever the ambient DefaultPrecision happens to be, and Eigen's coefficient-wise + /// assignment into it preserves the destination entry's precision. So writing a + /// 20-digit block value into a result entry that was allocated at, say, + /// MaxPrecisionAllowed leaves a 20-digit value carried at 1000-digit precision. The + /// adaptive tracker then propagates that over-precise value as the path point and the + /// next System::SetVariables throws (point precision != system precision). Coercing the + /// whole result to precision_ here makes a block-composed System honor the contract that + /// its evaluations come out at its working precision, exactly as the SLP path does. + /// No-op for double (which carries no precision). + template + void CoerceBlockOutputPrecision(Eigen::MatrixBase& result) const + { + using Scalar = typename Derived::Scalar; + if constexpr (!std::is_same::value) + { + using bertini::Precision; + Precision(result, precision_); + } + } + + /// \brief Evaluate the blocks' function values into the leading (natural) rows. + template + void EvalBlocksInPlace(Vec& function_values) const + { + const auto& vars = std::get >(current_variable_values_); + const T t = CurrentPathValue(); + Eigen::Index row = 0; + for (auto const& blk : blocks_) + std::visit([&](auto const& b){ + const Eigen::Index n = static_cast(b.NumFunctions()); + b.template EvalInPlace(function_values.segment(row, n), vars, t); + row += n; + }, blk); + } + + /// \brief Evaluate the blocks' Jacobian into the leading rows of J. + template + void JacobianBlocksInPlace(Mat& J) const + { + const auto& vars = std::get >(current_variable_values_); + const T t = CurrentPathValue(); + Eigen::Index row = 0; + for (auto const& blk : blocks_) + std::visit([&](auto const& b){ + const Eigen::Index n = static_cast(b.NumFunctions()); + Mat jb(n, J.cols()); // blocks write into a contiguous target + b.template JacobianInPlace(jb, vars, t); + J.block(row, 0, n, J.cols()) = jb; + row += n; + }, blk); + } + + /// \brief Evaluate the blocks' time-derivative into the leading rows. + template + void TimeDerivBlocksInPlace(Vec& ds_dt) const + { + const auto& vars = std::get >(current_variable_values_); + const T t = CurrentPathValue(); + Eigen::Index row = 0; + for (auto const& blk : blocks_) + std::visit([&](auto const& b){ + const Eigen::Index n = static_cast(b.NumFunctions()); + b.template TimeDerivInPlace(ds_dt.segment(row, n), vars, t); + row += n; + }, blk); + } + VariableGroup ungrouped_variables_; ///< ungrouped variable nodes. Not in an affine variable group, not in a projective group. Just hanging out, being a variable. std::vector< VariableGroup > variable_groups_; ///< Affine variable groups. When system is homogenized, will have a corresponding homogenizing variable. @@ -1753,40 +1824,30 @@ namespace bertini { Var path_variable_; ///< the single path variable for this system. Sometimes called time. VariableGroup implicit_parameters_; ///< Implicit parameters. These don't depend on anything, and will be moved from one parameter point to another by the tracker. They should be algebraically constrained by some equations. - std::vector< Fn > explicit_parameters_; ///< Explicit parameters. These should be functions of the path variable only, NOT of other variables. + std::vector< NE > explicit_parameters_; ///< Explicit parameters. These should be functions of the path variable only, NOT of other variables. + + // The polynomial path -- functions_, subfunctions_, constant_subfunctions_, their + // derivatives, the SLP, and eval_method_ -- has been folded into a + // blocks::PolynomialBlock held in blocks_ (see PolyBlock()/PolyBlockPtr()). The System + // is now a thin orchestrator over blocks + variable groups + patch. - std::vector< Fn > constant_subfunctions_; ///< degree-0 functions, depending on neither variables nor the path variable. - std::vector< Fn > subfunctions_; ///< Any declared subfunctions for the system. Can use these to ensure that complicated repeated structures are only created and evaluated once. - std::vector< Fn > functions_; ///< The system's functions. - class Patch patch_; ///< Patch on the variable groups. Assumed to be in the same order as the time_order_of_variable_groups_ if the system uses FIFO ordering, or in same order as the AffHomUng variable groups if that is set. bool is_patched_ = false; ///< Indicator of whether the system has been patched. - mutable std::vector< Jac > jacobian_; ///< The generated functions from differentiation. Created when first call for a Jacobian matrix evaluation. - - mutable std::vector< Nd > space_derivatives_; ///< The generated functions from differentiation with respect to space. in column-major order to be consistent with Eigen default order. Created when first call for a Jacobian matrix evaluation. + mutable bool is_differentiated_ = false; ///< orchestrator flag: have the blocks been differentiated + synced since the last structural change. - mutable std::vector< Nd > time_derivatives_; ///< The generated functions from differentiation with respect to time. in column-major order to be consistent with Eigen default order. Created when first call for a Jacobian matrix evaluation. - - mutable bool is_differentiated_ = false; ///< indicator for whether the jacobian tree has been populated. - - mutable StraightLineProgram slp_; ///< The straight line program. Is mutable since it's a has-a, not is-a relationship. + std::vector blocks_; ///< Evaluation blocks. Every System has one (a PolynomialBlock for its functions); structured systems (MHom, linear forms) add more. std::vector< VariableGroupType > time_order_of_variable_groups_; mutable std::tuple< Vec, Vec > current_variable_values_; + mutable std::tuple< dbl, mpfr_complex > current_path_value_{}; ///< per-thread path value (node-free; read by CurrentPathValue, written by SetPathVariable) mutable VariableGroup variable_ordering_; ///< The assembled ordering of the variables in the system. mutable bool have_ordering_ = false; - mutable unsigned precision_; ///< the current working precision of the system - - bool assume_uniform_precision_ = false; ///< a bit, setting whether we can assume the system is in uniform precision. if you are doing things that will allow pieces of the system to drift in terms of precision, then you should not assume this. \see AssumeUniformPrecision - - EvalMethod eval_method_ = DefaultEvalMethod(); ///< an enum class value, indicating which method of evaluation should be used. - DerivMethod deriv_method_ = DefaultDerivMethod(); ///< an enum class value, indicating which method of evaluation should be used. + mutable unsigned precision_; ///< the current working precision of the system - bool auto_simplify_ = DefaultAutoSimplify(); @@ -1797,7 +1858,7 @@ namespace bertini { /*definition of serialize function*/ template - void serialize(Archive & ar, const unsigned int version){ + void serialize(Archive & ar, const unsigned int /*version*/){ ar & ungrouped_variables_; ar & variable_groups_; ar & hom_variable_groups_; @@ -1810,38 +1871,18 @@ namespace bertini { ar & implicit_parameters_; ar & explicit_parameters_; - ar & constant_subfunctions_; - ar & subfunctions_; - ar & functions_; - - ar & patch_; ar & is_patched_; + // The polynomial path (functions / subfunctions / derivatives / SLP / eval+deriv + // methods) now lives inside the PolynomialBlock, which is archived as part of blocks_. + ar & blocks_; - ar & assume_uniform_precision_; - - ar & eval_method_; - ar & deriv_method_; - - ar & auto_simplify_; // now for the cached / mutable things ar & precision_; - - // if (Archive::is_loading::value == true){ - // is_differentiated_ = false;} - // // else - // // { - ar & is_differentiated_; - ar & jacobian_; - ar & space_derivatives_; - ar & time_derivatives_; - // } - - - ar & slp_; // does this need to be re-constructed after de-serialization? + ar & is_differentiated_; ar & time_order_of_variable_groups_; @@ -1886,6 +1927,50 @@ namespace bertini { + /** + \brief Form the gamma-trick straight-line homotopy H = (1-t)*target + gamma*t*start. + + The path variable `t` (named `path_variable_name`) is added to the returned homotopy, tracked + from t=1 (where H is gamma*start, so its roots are start's solutions) down to t=0 (where H is + target). When `start` carries a structured evaluation block (e.g. a products-of-linears start + system) it cannot be fused by node arithmetic, so the two systems are combined with a + BlendBlock that evaluates whole Systems; otherwise the node-arithmetic combination is used. + This is the same construction the zero-dim solver's CloneGiven policy uses internally; it is + exposed so a user-authored start system can be turned into a trackable homotopy for the + user-homotopy solve path. + + \param gamma The gamma coefficient (a node). If null, a random rational gamma is generated. + */ + System MakeHomotopy(System const& target, System const& start, + std::string const& path_variable_name = "t", + std::shared_ptr const& gamma = nullptr); + + /** + \brief Form a homotopy that moves ONLY some rows, leaving the rest fixed and evaluated once. + + The "regeneration" homotopy: `fixed` holds the equations that do not move (the polynomial system + and any static linear slices); `start_moving` and `end_moving` are systems holding just the rows + that move, agreeing in function count and variable structure. The result is + + H = [ fixed's blocks (unchanged) ; (1-t)*end_moving + gamma*t*start_moving ] + + with the moving rows produced by a single `BlendBlock` over `end_moving`/`start_moving` and the + fixed rows kept as their own sibling blocks. Because the fixed blocks are autonomous, they are + evaluated exactly once per point and contribute zero to `dH/dt` -- the fixed system is never + re-evaluated or scaled by the path coefficient as the moving rows slide. At t=1 the moving rows + are `gamma*start_moving` (so the start points are roots of `fixed` together with `start_moving`), + at t=0 they are `end_moving`. + + The fixed rows come first, then the moving rows; build the matching target (for the user-homotopy + pipeline) as `fixed` concatenated with `end_moving`, and the start points as the roots of `fixed` + together with `start_moving`. + + \param gamma The gamma coefficient (a node). If null, a random rational gamma is generated. + */ + System MakeMovingHomotopy(System const& fixed, System const& start_moving, System const& end_moving, + std::string const& path_variable_name = "t", + std::shared_ptr const& gamma = nullptr); + /** \brief Do a deep clone of the system. This includes the entire structure, variables, etc. everything. */ @@ -1895,6 +1980,48 @@ namespace bertini { \brief Free form function for simplifying systems. */ void Simplify(System & sys); + + // Explicit instantiation declarations for the two concrete numeric types. + // Definitions live in core/src/system/system.cpp. + // Suppresses re-instantiation of the heavy Eval/Jacobian/Set template bodies + // (with their eval_method_ switch trees) in every including TU. + + extern template void System::EvalInPlace(Vec&) const; + extern template void System::EvalInPlace(Vec&) const; + + extern template Vec System::Eval() const; + extern template Vec System::Eval() const; + + extern template void System::JacobianInPlace(Mat&) const; + extern template void System::JacobianInPlace(Mat&) const; + + extern template Mat System::Jacobian() const; + extern template Mat System::Jacobian() const; + + extern template Mat System::Jacobian(const Vec&) const; + extern template Mat System::Jacobian(const Vec&) const; + + extern template void System::JacobianInPlace(Mat&, const Vec&) const; + extern template void System::JacobianInPlace(Mat&, const Vec&) const; + + extern template void System::TimeDerivativeInPlace(Vec&) const; + extern template void System::TimeDerivativeInPlace(Vec&) const; + + extern template Vec System::TimeDerivative() const; + extern template Vec System::TimeDerivative() const; + + extern template void System::SetVariables(const Vec&) const; + extern template void System::SetVariables(const Vec&) const; + + extern template void System::SetPathVariable(dbl const&) const; + extern template void System::SetPathVariable(mpfr_complex const&) const; + + extern template void System::SetAndReset(Vec const&, dbl const&) const; + extern template void System::SetAndReset(Vec const&, mpfr_complex const&) const; + + extern template void System::SetAndReset(Vec const&) const; + extern template void System::SetAndReset(Vec const&) const; + } diff --git a/core/include/bertini2/trackers/adaptive_precision_utilities.hpp b/core/include/bertini2/trackers/adaptive_precision_utilities.hpp index 799abd8c0..82e48611d 100644 --- a/core/include/bertini2/trackers/adaptive_precision_utilities.hpp +++ b/core/include/bertini2/trackers/adaptive_precision_utilities.hpp @@ -111,7 +111,7 @@ Cannot change precision of fixed precision hardware doubles. This function is p \return The precision, which is now uniform. */ inline -unsigned EnsureAtUniformPrecision(TimeCont & times, SampCont & samples) +unsigned EnsureAtUniformPrecision(TimeCont & /*times*/, SampCont & /*samples*/) { return DoublePrecision(); } @@ -128,14 +128,14 @@ unsigned EnsureAtUniformPrecision(TimeCont & times, SampCont & samples inline unsigned EnsureAtUniformPrecision(TimeCont & times, SampCont & samples) { - auto def_prec = DefaultPrecision(); + auto def_prec = ThreadPrecision(); if (std::any_of(begin(times),end(times),[=](auto const& p){return Precision(p)!=def_prec;}) || std::any_of(begin(samples),end(samples),[=](auto const& p){return Precision(p)!=def_prec;})) { auto max_precision = max(MaxPrecision(samples), MaxPrecision(times)); - DefaultPrecision(max_precision); + SetThreadPrecision(max_precision); SetPrecision(times, max_precision); SetPrecision(samples, max_precision); return max_precision; @@ -159,14 +159,14 @@ This function does NOT do any refinement, it merely changes the precision of def inline unsigned EnsureAtUniformPrecision(TimeCont & times, SampCont & samples, SampCont & derivatives) { - auto def_prec = DefaultPrecision(); + auto def_prec = ThreadPrecision(); if (std::any_of(begin(samples),end(samples),[=](auto const& p){return Precision(p)!=def_prec;}) || std::any_of(begin(derivatives),end(derivatives),[=](auto const& p){return Precision(p)!=def_prec;})) { auto max_precision = max(MaxPrecision(samples),MaxPrecision(times),MaxPrecision(derivatives)); - DefaultPrecision(max_precision); + SetThreadPrecision(max_precision); SetPrecision(times, max_precision); SetPrecision(samples, max_precision); diff --git a/core/include/bertini2/trackers/amp_tracker.hpp b/core/include/bertini2/trackers/amp_tracker.hpp index b40b432fb..faf7e6f3a 100644 --- a/core/include/bertini2/trackers/amp_tracker.hpp +++ b/core/include/bertini2/trackers/amp_tracker.hpp @@ -35,8 +35,9 @@ #define BERTINI_AMP_TRACKER_HPP #include +#include -#pragma once +#pragma once #include "bertini2/trackers/base_tracker.hpp" @@ -92,15 +93,24 @@ namespace bertini{ /** \brief Compute the cost function for arithmetic versus precision. - From \cite AMP2, \f$C(P)\f$. As currently implemented, this is - \f$ 10.35 + 0.13 P \f$, where P is the precision. These numbers are stale, and need to be recomputed. Badly. Please do this. - - This function tells you the relative cost of arithmetic at a given precision. + From \cite AMP2, \f$C(P)\f$. As currently implemented, this is + \f$ 101.47 + 1.59 P \f$, where P is the precision in decimal digits. + + This function tells you the relative cost of arithmetic at a given precision, + relative to \c std::complex (the double-precision tracker's scalar type). + + Calibrated by benchmarking three operations representative of path tracking + (dot product / SLP evaluation, dense matvec, LU factorization+solve) using + GNU MPC (\c mpfr_complex) vs \c std::complex on modern hardware. + See \c tuning/arithmetic_cost.cpp for the methodology and compile instructions. - \todo Recompute this cost function for boost::multiprecision::mpfr_float + The paper \cite AMP2 reports \f$C(P) = 10.35 + 0.04 P_\mathrm{bits}\f$ (measured on + a 2009 Opteron 250). Converting to decimal digits gives \f$10.35 + 0.13 P\f$, which + was the previous value here. The ~12x increase in the intercept on modern hardware + reflects AVX2 vectorization of double-precision arithmetic that MPC cannot exploit. - \param precision An integral number of digits -- then this gives the cost for arithmetic - \return A double indicating how expensive arithmetic at a given precision is. 1 is the base-line for double-precision. + \param precision An integral number of decimal digits + \return A double indicating how expensive arithmetic at a given precision is. 1 is the base-line for double-precision. */ inline double ArithmeticCost(unsigned precision) @@ -108,7 +118,7 @@ namespace bertini{ if (precision==DoublePrecision()) return 1; else - return 10.35 + 0.13 * precision; + return 101.47 + 1.59 * precision; } @@ -132,7 +142,8 @@ namespace bertini{ unsigned num_newton_iterations, unsigned predictor_order = 0) { - return pow(RealT(10), -( digits_B - precision )*num_newton_iterations/(predictor_order+1.0)); + return pow(RealT(10), -(static_cast(digits_B) - static_cast(precision)) + * static_cast(num_newton_iterations) / (predictor_order+1.0)); } @@ -164,10 +175,47 @@ namespace bertini{ inline unsigned MinDigitsForStepsizeInterval(mpfr_float const& min_stepsize, mpfr_float const& max_stepsize, mpfr_float const& time_to_go) { - return max(MinDigitsForLogOfStepsize(-log10(min_stepsize),time_to_go), + return max(MinDigitsForLogOfStepsize(-log10(min_stepsize),time_to_go), MinDigitsForLogOfStepsize(-log10(max_stepsize),time_to_go)); } - + + /** + \brief Bertini 1's precision-decrease hysteresis margin (from B1's \c AMP2_update). + + The number of EXTRA digits a criterion must clear below the current precision before it is + allowed to actually lower the working precision. This prevents precision thrashing and + replaces a consecutive-successful-steps counter with a stateless, precision-derived margin. + + B1 uses (currPrec_bits/32)*2 ~ 2 digits per 32-bit precision packet (~20% slack). In + b2 precision is carried in decimal digits with packet size \c PrecisionIncrement(), so the + faithful analog is (digits/PrecisionIncrement())*2. Double precision gets no margin. + */ + inline + unsigned ExtraDigitsBeforePrecisionDecrease(unsigned current_precision) + { + if (current_precision <= DoublePrecision()) + return 0; + return (current_precision / PrecisionIncrement()) * 2; + } + + /** + \brief Apply B1's precision-decrease hysteresis to one digits requirement (B1 \c AMP2_update). + + If \p digits_required would permit precision to drop below the current precision, demand \p extra + additional digits before allowing the drop, and never raise the requirement above \p current_digits + (a requirement that suggests a decrease must not be turned into a request for an increase). + */ + inline + void ApplyPrecisionDecreaseMargin(int & digits_required, unsigned current_digits, unsigned extra) + { + if (digits_required < static_cast(current_digits)) + { + digits_required += static_cast(extra); + if (digits_required > static_cast(current_digits)) + digits_required = static_cast(current_digits); + } + } + /** \brief Compute precision and stepsize minimizing the ArithmeticCost() of tracking. @@ -205,11 +253,13 @@ namespace bertini{ double min_stepsize_lowprec = static_cast(min_stepsize); double max_stepsize_lowprec = static_cast(max_stepsize); - auto minimizer_routine = - [&min_cost, &minimizing_precision, digits_B, num_newton_iterations, predictor_order, max_stepsize_lowprec](unsigned p) + auto minimizer_routine = + [&min_cost, &minimizing_precision, digits_B, num_newton_iterations, predictor_order, min_stepsize_lowprec, max_stepsize_lowprec](unsigned p) { - double candidate_stepsize = min(StepsizeSatisfyingCriterionB(p, digits_B, num_newton_iterations, predictor_order), - max_stepsize_lowprec); + double criterion_b_stepsize = StepsizeSatisfyingCriterionB(p, digits_B, num_newton_iterations, predictor_order); + if (criterion_b_stepsize < min_stepsize_lowprec) + return; // precision too low to satisfy min_stepsize constraint + double candidate_stepsize = min(criterion_b_stepsize, max_stepsize_lowprec); using std::abs; double current_cost = ArithmeticCost(p) / abs(candidate_stepsize); @@ -246,9 +296,12 @@ namespace bertini{ new_precision = minimizing_precision; // copy the computed value // next, because the above computed the new stepsize in double precision, which may be lowprec, we compute in full precision - new_stepsize = min( - StepsizeSatisfyingCriterionB(new_precision, digits_B, num_newton_iterations, predictor_order), - max_stepsize + new_stepsize = max( + min( + StepsizeSatisfyingCriterionB(new_precision, digits_B, num_newton_iterations, predictor_order), + max_stepsize + ), + min_stepsize ); } @@ -389,7 +442,7 @@ namespace bertini{ } - const unsigned GetCurrentPrecision() const + unsigned GetCurrentPrecision() const { return current_precision_; } @@ -405,7 +458,18 @@ namespace bertini{ preserve_precision_ = should_preseve_precision; } - + /** + \brief Override the precision at which tracking starts. + + Pass a value to start tracking at that precision regardless of the start point's precision. + Pass std::nullopt (or call with no argument) to use the start point's precision (default). + */ + void SetStartPrecision(std::optional p = std::nullopt) + { + override_start_precision_ = p; + } + + virtual ~AMPTracker() = default; @@ -442,19 +506,19 @@ namespace bertini{ mpfr_complex const& end_time, Vec const& start_point) const override { + initial_precision_ = override_start_precision_.value_or(Precision(start_point(0))); + #ifndef BERTINI_DISABLE_ASSERTS assert( - (!preserve_precision_ - || - -log10(tracking_tolerance_) <= start_point(0).precision()) + (!preserve_precision_ + || + -log10(tracking_tolerance_) <= initial_precision_) && "when tracking a path, either preservation of precision must be turned off (so precision can be higher at the end of tracking), or the initial precision must be high enough to support the resulting points to the desired tolerance" ); #endif NotifyObservers(Initializing(*this,start_time, end_time, start_point)); - - initial_precision_ = Precision(start_point(0)); - DefaultPrecision(initial_precision_); + SetThreadPrecision(initial_precision_); // set up the master current time and the current step size current_time_ = start_time; @@ -471,7 +535,7 @@ namespace bertini{ if (reinitialize_stepsize_) { mpfr_float segment_length = abs(start_time-end_time)/Get().min_num_steps; - SetStepSize(min(NumTraits::FromRational(Get().initial_step_size, current_precision_),segment_length)); + SetStepSize(min(mpfr_float(Get().initial_step_size, current_precision_),segment_length)); } // populate the current space value with the start point, in appropriate precision @@ -481,11 +545,8 @@ namespace bertini{ MultipleToMultiple(initial_precision_, start_point); ChangePrecision(initial_precision_); - - ResetCounters(); - - ChangePrecision(start_point(0).precision()); + ResetCounters(); auto initial_refinement_code = InitialRefinement(); @@ -511,7 +572,7 @@ namespace bertini{ num_precision_decreases_ = 0; num_successful_steps_since_stepsize_increase_ = 0; num_successful_steps_since_precision_decrease_ = 0; - // initialize to the frequency so guaranteed to compute it the first try + // initialize to the frequency so guaranteed to compute it the first try num_steps_since_last_condition_number_computation_ = this->Get().frequency_of_CN_estimation; } @@ -588,14 +649,14 @@ namespace bertini{ // the current precision is the precision of the output solution point. if (current_precision_==DoublePrecision()) { - unsigned num_vars = GetSystem().NumVariables(); + unsigned num_vars = static_cast(GetSystem().NumVariables()); solution_at_endtime.resize(num_vars); for (unsigned ii=0; ii >(current_space_)(ii)); } else { - unsigned num_vars = GetSystem().NumVariables(); + unsigned num_vars = static_cast(GetSystem().NumVariables()); solution_at_endtime.resize(num_vars); for (unsigned ii=0; ii + template SuccessCode TrackerIteration() const // not an override, because it is templated { - using RealType = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; #ifndef BERTINI_DISABLE_ASSERTS - assert(PrecisionSanityCheck() && "precision sanity check failed. some internal variable is not in correct precision"); + assert(PrecisionSanityCheck() && "precision sanity check failed. some internal variable is not in correct precision"); #endif NotifyObservers(NewStep(*this)); - Vec& predicted_space = std::get >(temporary_space_); // this will be populated in the Predict step - Vec& current_space = std::get >(current_space_); // the thing we ultimately wish to update - ComplexType current_time = ComplexType(current_time_); - ComplexType delta_t = ComplexType(delta_t_); + Vec& predicted_space = std::get >(temporary_space_); // this will be populated in the Predict step + Vec& current_space = std::get >(current_space_); // the thing we ultimately wish to update + ComplexT current_time = ComplexT(current_time_); + ComplexT delta_t = ComplexT(delta_t_); #ifndef BERTINI_DISABLE_ASSERTS - PrecisionSanityCheck(); + PrecisionSanityCheck(); #endif - SuccessCode predictor_code = Predict(predicted_space, current_space, current_time, delta_t); + SuccessCode predictor_code = Predict(predicted_space, current_space, current_time, delta_t); if (predictor_code==SuccessCode::MatrixSolveFailureFirstPartOfPrediction) { NotifyObservers(FirstStepPredictorMatrixSolveFailure(*this)); @@ -666,25 +727,27 @@ namespace bertini{ return predictor_code; } else if (predictor_code==SuccessCode::HigherPrecisionNecessary) - { + { NotifyObservers(PredictorHigherPrecisionNecessary(*this)); - AMPCriterionError(); + auto adj_code = AMPCriterionError(); + if (adj_code != SuccessCode::Success) + return adj_code; return predictor_code; } - NotifyObservers(SuccessfulPredict(*this, predicted_space)); + NotifyObservers(SuccessfulPredict(*this, predicted_space)); - Vec& tentative_next_space = std::get >(tentative_space_); // this will be populated in the Correct step + Vec& tentative_next_space = std::get >(tentative_space_); // this will be populated in the Correct step - ComplexType tentative_next_time = current_time + delta_t; + ComplexT tentative_next_time = current_time + delta_t; - SuccessCode corrector_code = Correct(tentative_next_space, + SuccessCode corrector_code = Correct(tentative_next_space, predicted_space, tentative_next_time); #ifndef BERTINI_DISABLE_ASSERTS - assert(PrecisionSanityCheck() && "precision sanity check failed. some internal variable is not in correct precision"); + assert(PrecisionSanityCheck() && "precision sanity check failed. some internal variable is not in correct precision"); #endif if (corrector_code==SuccessCode::MatrixSolveFailure || corrector_code==SuccessCode::FailedToConverge) @@ -696,7 +759,9 @@ namespace bertini{ else if (corrector_code == SuccessCode::HigherPrecisionNecessary) { NotifyObservers(CorrectorHigherPrecisionNecessary(*this)); - AMPCriterionError(); + auto adj_code = AMPCriterionError(); + if (adj_code != SuccessCode::Success) + return adj_code; return corrector_code; } else if (corrector_code == SuccessCode::GoingToInfinity) @@ -705,11 +770,11 @@ namespace bertini{ return corrector_code; } - NotifyObservers(SuccessfulCorrect(*this, tentative_next_space)); + NotifyObservers(SuccessfulCorrect(*this, tentative_next_space)); // copy the tentative vector into the current space vector; current_space = tentative_next_space; - return AdjustAMPStepSuccess(); + return AdjustAMPStepSuccess(); } @@ -747,50 +812,110 @@ namespace bertini{ /** \brief Increase stepsize or decrease precision, because of consecutive successful steps. - \tparam ComplexType The complex number type. - \tparam RealType The real number type. - - If the most recent step was successful, maybe adjust down precision and up stepsize. + \tparam ComplexT The complex number type. + \tparam RealT The real number type. - The number of consecutive successful steps is recorded as state in this class, and if this number exceeds a user-determine threshold, the precision or stepsize are allowed to favorably change. If not, then precision can only go up or remain the same. Stepsize can only decrease. These changes depend on the AMP criteria and current tracking tolerance. - */ - template - SuccessCode AdjustAMPStepSuccess() const - { - // TODO: think about why we consider reducing the stepsize? this is despite documentation stating that it can only increase - mpfr_float min_stepsize = current_stepsize_ * NumTraits::FromRational(Get().step_size_fail_factor, current_precision_); - mpfr_float max_stepsize = min( current_stepsize_ * NumTraits::FromRational(Get().step_size_success_factor, current_precision_), NumTraits::FromRational(Get().max_step_size, current_precision_)); + If the most recent step was successful, maybe adjust down precision and up stepsize. + The number of consecutive successful steps is recorded as state in this class, and if this number exceeds a user-determined threshold, the precision or stepsize are allowed to favorably change. If not, then precision can only go up or remain the same. Stepsize can only decrease. These changes depend on the AMP criteria and current tracking tolerance. - unsigned min_precision = MinRequiredPrecision_BCTol(); - unsigned max_precision = max(min_precision,current_precision_); + \par Implementation note on the precision scan window - if (num_successful_steps_since_stepsize_increase_ < Get().consecutive_successful_steps_before_stepsize_increase) - max_stepsize = current_stepsize_; // disallow stepsize changing + The upper bound passed to MinimizeTrackingCost is \c maximum_precision (from + AdaptiveMultiplePrecisionConfig), not \c current_precision_. This is intentional + and faithful to the paper (Bates, Hauenstein, Sommese, Wampler 2009, p. 8). + MinimizeTrackingCost is a cost minimiser: it always returns the cheapest valid + \f$(p, h)\f$ pair. For well-conditioned paths it still selects double precision + with a large stepsize, so widening the ceiling has no effect on the common case. - if ( (num_successful_steps_since_precision_decrease_ < Get().consecutive_successful_steps_before_precision_decrease) - || - (num_precision_decreases_ >= Get().max_num_precision_decreases)) - min_precision = max(min_precision, current_precision_); // disallow precision changing - - - MinimizeTrackingCost(next_precision_, next_stepsize_, - min_precision, min_stepsize, - max_precision, max_stepsize, - DigitsB(), - Get().max_num_newton_iterations, - predictor_order_); + The ceiling matters only when the path enters a harder region immediately after a + successful step — for example, a condition-number spike at the new point pushes + \c digits_B above \c current_precision_. In that situation the minimum required + precision for the next step genuinely exceeds the precision that was + sufficient for the last step, and a ceiling of \c current_precision_ + would collapse the scan to a single candidate that fails criterion B, causing + MinimizeTrackingCost to throw even though valid pairs exist at higher precision. + The throw inside MinimizeTrackingCost remains semantically correct: it fires only + when no valid pair exists anywhere in \f$[p_{\min}, p_{\max}]\f$, indicating that + the path has entered a region that cannot be tracked at any supported precision. + That is a genuine path failure, and is reported via + SuccessCode::FailedToSelectPrecisionAndStepsize. + */ + template + SuccessCode AdjustAMPStepSuccess() const + { + // This mirrors Bertini 1's AMP2_update: pick the cost-minimizing (precision, stepsize) + // after a successful step. Faithful-to-B1 points, two of which were previously wrong: + // (1) The rule-B precision requirement CREDITS THE STEP. digits_B is the right-hand side + // of P + (p+1)*xi/N > digits_B (xi = -log10(stepsize)); the floor is therefore + // digits_B - (p+1)*(-log10(min_stepsize))/N, crediting the smallest allowed step, NOT + // the raw digits_B. Without this credit a tiny step (large size_proportion in the + // roundoff regime) spuriously forced precision up. B1: digits_B2 = ceil(P0 - eta_minSS/N). + // (2) Precision decrease (and stepsize increase) is gated by BOTH B1's StepsForIncrease + // consecutive-successful-steps counter AND the digits-margin hysteresis + // (ExtraDigitsBeforePrecisionDecrease). The single StepsForIncrease setting + // (SteppingConfig::consecutive_successful_steps_before_stepsize_increase) governs both + // gates -- the old, duplicate AMP-config precision-decrease setting was removed. + mpfr_float min_stepsize = current_stepsize_ * mpfr_float(Get().step_size_fail_factor, current_precision_); + mpfr_float max_stepsize = min( current_stepsize_ * mpfr_float(Get().step_size_success_factor, current_precision_), mpfr_float(Get().max_step_size, current_precision_)); + + const unsigned steps_for_increase = Get().consecutive_successful_steps_before_stepsize_increase; // B1 StepsForIncrease + + if (num_successful_steps_since_stepsize_increase_ < steps_for_increase) + max_stepsize = current_stepsize_; // not enough successes in a row: disallow stepsize increase + + const unsigned N = Get().max_num_newton_iterations; + const unsigned curr_digits = current_precision_; + const unsigned extra = ExtraDigitsBeforePrecisionDecrease(curr_digits); + + // rule B, step-credited (credit the smallest allowed step = the most credit) + int digits_B = static_cast(ceil( double(B_RHS()) + - (predictor_order_ + 1.0) * double(-log10(min_stepsize)) / N )); + int digits_C = static_cast(DigitsC()); + int digits_stepsize = static_cast(MinDigitsForStepsizeInterval(min_stepsize, max_stepsize, abs(current_time_ - endtime_))); + + // each requirement may only lower precision if it clears the hysteresis margin + ApplyPrecisionDecreaseMargin(digits_B, curr_digits, extra); + ApplyPrecisionDecreaseMargin(digits_C, curr_digits, extra); + ApplyPrecisionDecreaseMargin(digits_stepsize, curr_digits, extra); + + int min_digits = std::max({ static_cast(digits_tracking_tolerance_), + static_cast(digits_final_), + digits_B, digits_C, digits_stepsize, + static_cast(DoublePrecision()) }); + unsigned min_precision = static_cast(min_digits); // >= DoublePrecision(), so non-negative + + // StepsForIncrease gate + safety cap: precision may only be lowered after enough successful + // steps in a row and while under the decrease budget; otherwise hold the current precision. + const bool decrease_allowed = + (num_successful_steps_since_precision_decrease_ >= steps_for_increase) + && + (num_precision_decreases_ < Get().max_num_precision_decreases); + if (!decrease_allowed) + min_precision = max(min_precision, current_precision_); + + unsigned max_precision = Get().maximum_precision; + + try { + MinimizeTrackingCost(next_precision_, next_stepsize_, + min_precision, min_stepsize, + max_precision, max_stepsize, + DigitsB(), // RAW digits_B (=P0) for the stepsize relation, per B1 minimize_cost + N, + predictor_order_); + } catch (std::runtime_error const&) { + return SuccessCode::FailedToSelectPrecisionAndStepsize; + } if ( (next_stepsize_ > current_stepsize_) || (next_precision_ < current_precision_) ) num_successful_steps_since_stepsize_increase_ = 0; else ++num_successful_steps_since_stepsize_increase_; - if (next_precision_ < current_precision_) - { + { ++num_precision_decreases_; num_successful_steps_since_precision_decrease_ = 0; } @@ -854,12 +979,11 @@ namespace bertini{ Precision is REQUIRED to increase at least one increment. Stepsize is REQUIRED to decrease. - \tparam ComplexType The complex number type. + \tparam ComplexT The complex number type. */ - template - void AMPCriterionError() const - { - using RealT = typename Eigen::NumTraits::Real; + template + SuccessCode AMPCriterionError() const + { unsigned min_next_precision; // sure, i could use a trigraph here, but it'd be terrible if (current_precision_==DoublePrecision()) @@ -869,39 +993,51 @@ namespace bertini{ mpfr_float min_stepsize = MinStepSizeForPrecision(current_precision_, abs(current_time_ - endtime_)); - mpfr_float max_stepsize = current_stepsize_ * NumTraits::FromRational(Get().step_size_fail_factor,current_precision_); // Stepsize decreases. + mpfr_float max_stepsize = current_stepsize_ * mpfr_float(Get().step_size_fail_factor, current_precision_); // Stepsize decreases. if (min_stepsize > max_stepsize) { // stepsizes are incompatible, must increase precision next_precision_ = min_next_precision; // decrease stepsize somewhat less than the fail factor - next_stepsize_ = max(current_stepsize_ * (1+NumTraits::FromRational(Get().step_size_fail_factor,current_precision_))/2, min_stepsize); + next_stepsize_ = max(current_stepsize_ * (1+mpfr_float(Get().step_size_fail_factor, current_precision_))/2, min_stepsize); } else { - unsigned digits_B = DigitsB(); + unsigned digits_B = DigitsB(); // RAW digits_B (=P0); the stepsize relation in MinimizeTrackingCost needs it raw + + // rule-B precision floor CREDITS THE STEP (B1 AMP2_update: digits_B2 = ceil(P0 - eta_minSS/N)): + // floor = digits_B - (p+1)*(-log10(min_stepsize))/N, crediting the smallest allowed step. + // This keeps a small step (large size_proportion in the roundoff regime) from forcing + // precision up by itself; genuine ill-conditioning still escalates via D / rules A,C. + const unsigned N = Get().max_num_newton_iterations; + int digits_B_credited = static_cast(ceil( double(digits_B) + - (predictor_order_ + 1.0) * double(-log10(min_stepsize)) / N )); + if (digits_B_credited < 0) + digits_B_credited = 0; unsigned min_precision = max(min_next_precision, - digits_B, - DigitsC(), + static_cast(digits_B_credited), + DigitsC(), MinDigitsForStepsizeInterval(min_stepsize, max_stepsize, abs(current_time_ - endtime_)), digits_final_ ); - - unsigned a = ceil(digits_B - (predictor_order_+1)* -log10(max_stepsize)/Get().max_num_newton_iterations).convert_to(); - unsigned max_precision = max(min_precision, a); + try { + MinimizeTrackingCost(next_precision_, next_stepsize_, + min_precision, min_stepsize, + Get().maximum_precision, max_stepsize, + digits_B, + N, + predictor_order_); + } catch (std::runtime_error const&) { + return SuccessCode::FailedToSelectPrecisionAndStepsize; + } - MinimizeTrackingCost(next_precision_, next_stepsize_, - min_precision, min_stepsize, - Get().maximum_precision, max_stepsize, - digits_B, - Get().max_num_newton_iterations, - predictor_order_); } UpdatePrecisionAndStepsize(); + return SuccessCode::Success; } @@ -911,14 +1047,14 @@ namespace bertini{ /** \brief Get the raw right-hand side of Criterion B based on current state. */ - template + template NumErrorT B_RHS() const - { - return max(amp::CriterionBRHS(this->norm_J_, - this->norm_J_inverse_, - Get().max_num_newton_iterations, - tracking_tolerance_, - this->size_proportion_, + { + return max(amp::CriterionBRHS(this->norm_J_, + this->norm_J_inverse_, + Get().max_num_newton_iterations, + tracking_tolerance_, + this->size_proportion_, Get()), NumErrorT(0)); } @@ -940,10 +1076,11 @@ namespace bertini{ * the AMP configuration. */ - template + template unsigned DigitsB() const - { - return unsigned(B_RHS()); + { + unsigned d = unsigned(B_RHS()); + return d; } @@ -953,11 +1090,11 @@ namespace bertini{ /** \brief Get the raw right-hand side of Criterion B based on current state. */ - template + template NumErrorT C_RHS() const { return max(amp::CriterionCRHS(this->norm_J_inverse_, - NumErrorT(std::get > (current_space_).norm()), + NumErrorT(std::get > (current_space_).norm()), tracking_tolerance_, Get()), NumErrorT(0)); } @@ -977,46 +1114,20 @@ namespace bertini{ * the AMP configuration. */ - template + template unsigned DigitsC() const { - return unsigned(C_RHS()); + return unsigned(C_RHS()); } - /** - \brief Get the minimum required precision based on current state. - - The current state determining the minimum required precision is: - - * the norm of the Jacobian, - * the norm of the inverse of the Jacobian, - * the size_proportion, related to stepsize and predictor order, - * the norm of the current solution. - * the tracking tolerance. - - The min digits needed is the maximum of - - * Criterion B, - * Criterion C, and - * the digits required by the tracking tolerance. - - \tparam ComplexType The complex number type. - \tparam RealType The real number type. - */ - template - unsigned MinRequiredPrecision_BCTol() const - { - return max(DigitsB(), - DigitsC(), - digits_tracking_tolerance_, - DoublePrecision()); - } - - + // (MinRequiredPrecision_BCTol was removed: it returned the UNCREDITED rule-B floor + // max(DigitsB, DigitsC, tol, double), which is what spuriously escalated precision on + // small steps. AdjustAMPStepSuccess now computes the step-credited, margin-gated floor + // directly, faithful to Bertini 1's AMP2_update.) @@ -1043,8 +1154,8 @@ namespace bertini{ void OnStepFail() const override { Tracker::IncrementBaseCountersFail(); - num_successful_steps_since_precision_decrease_ = 0; num_successful_steps_since_stepsize_increase_ = 0; + num_successful_steps_since_precision_decrease_ = 0; NotifyObservers(FailedStep(*this)); } @@ -1078,25 +1189,25 @@ namespace bertini{ \param current_time The current time value. \param delta_t The time differential for this step. Allowed to be complex. - \tparam ComplexType The complex number type. - \tparam RealType The real number type. + \tparam ComplexT The complex number type. + \tparam RealT The real number type. */ - template - SuccessCode Predict(Vec & predicted_space, - const Vec& current_space, - ComplexType const& current_time, ComplexType const& delta_t) const + template + SuccessCode Predict(Vec & predicted_space, + const Vec& current_space, + ComplexT const& current_time, ComplexT const& delta_t) const { - static_assert(std::is_same< typename Eigen::NumTraits::Real, - typename Eigen::NumTraits::Real>::value, + static_assert(std::is_same< typename Eigen::NumTraits::Real, + typename Eigen::NumTraits::Real>::value, "underlying complex type and the type for comparisons must match"); - if (predictor_->HasErrorEstimate()) - return predictor_->Predict(predicted_space, + if (predictor_.HasErrorEstimate()) + return predictor_.Predict(predicted_space, this->error_estimate_, this->size_proportion_, this->norm_J_, @@ -1110,7 +1221,7 @@ namespace bertini{ tracking_tolerance_, Get()); else - return predictor_->Predict(predicted_space, + return predictor_.Predict(predicted_space, this->size_proportion_, this->norm_J_, this->norm_J_inverse_, @@ -1131,8 +1242,8 @@ namespace bertini{ Wrapper function for calling Correct and getting the error estimates etc directly into the tracker object. - \tparam ComplexType The complex number type. - \tparam RealType The real number type. + \tparam ComplexT The complex number type. + \tparam RealT The real number type. \param corrected_space[out] The spatial result of the correction loop. \param current_space The start point in space for running the corrector loop. @@ -1140,19 +1251,19 @@ namespace bertini{ \return A SuccessCode indicating whether the loop was successful in converging in the max number of allowable newton steps, to the current path tolerance. */ - template - SuccessCode Correct(Vec & corrected_space, - Vec const& current_space, - ComplexType const& current_time) const + template + SuccessCode Correct(Vec & corrected_space, + Vec const& current_space, + ComplexT const& current_time) const { - static_assert(std::is_same< typename Eigen::NumTraits::Real, - typename Eigen::NumTraits::Real>::value, + static_assert(std::is_same< typename Eigen::NumTraits::Real, + typename Eigen::NumTraits::Real>::value, "underlying complex type and the type for comparisons must match"); - return corrector_->Correct(corrected_space, + return corrector_.Correct(corrected_space, this->norm_delta_z_, this->norm_J_, this->norm_J_inverse_, @@ -1200,8 +1311,8 @@ namespace bertini{ Returns new space point by reference, as new_space. Operates at current precision. The tolerance is the tracking tolerance specified during Setup(...). - \tparam ComplexType The complex number type. - \tparam RealType The real number type. + \tparam ComplexT The complex number type. + \tparam RealT The real number type. \param[out] new_space The result of running the refinement. \param start_point The base point for running Newton's method. @@ -1209,14 +1320,14 @@ namespace bertini{ \return Code indicating whether was successful or not. Regardless, the value of new_space is overwritten with the correction result. */ - template - SuccessCode RefineImpl(Vec & new_space, - Vec const& start_point, ComplexType const& current_time) const + template + SuccessCode RefineImpl(Vec & new_space, + Vec const& start_point, ComplexT const& current_time) const { - using RealType = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; - static_assert(std::is_same< typename Eigen::NumTraits::Real, - typename Eigen::NumTraits::Real>::value, + static_assert(std::is_same< typename Eigen::NumTraits::Real, + typename Eigen::NumTraits::Real>::value, "underlying complex type and the type for comparisons must match"); auto target_precision = Precision(current_time); @@ -1227,7 +1338,7 @@ namespace bertini{ - return corrector_->Correct(new_space, + return corrector_.Correct(new_space, this->norm_delta_z_, this->norm_J_, this->norm_J_inverse_, @@ -1251,8 +1362,8 @@ namespace bertini{ Returns new space point by reference, as new_space. Operates at current precision. - \tparam ComplexType The complex number type. - \tparam RealType The real number type. + \tparam ComplexT The complex number type. + \tparam RealT The real number type. \param[out] new_space The result of running the refinement. \param start_point The base point for running Newton's method. @@ -1261,9 +1372,9 @@ namespace bertini{ \param max_iterations The maximum allowable number of iterations to perform. \return Code indicating whether was successful or not. Regardless, the value of new_space is overwritten with the correction result. */ - template - SuccessCode RefineImpl(Vec & new_space, - Vec const& start_point, ComplexType const& current_time, + template + SuccessCode RefineImpl(Vec & new_space, + Vec const& start_point, ComplexT const& current_time, NumErrorT const& tolerance, unsigned max_iterations) const { auto target_precision = Precision(current_time); @@ -1271,7 +1382,7 @@ namespace bertini{ ChangePrecision(target_precision); Precision(new_space,target_precision); - return corrector_->Correct(new_space, + return corrector_.Correct(new_space, this->norm_delta_z_, this->norm_J_, this->norm_J_inverse_, @@ -1319,6 +1430,7 @@ namespace bertini{ if (new_precision==current_precision_) // no op return SuccessCode::Success; + NotifyObservers(PrecisionChanged(*this,current_precision_,new_precision)); @@ -1373,7 +1485,7 @@ namespace bertini{ #endif current_precision_ = DoublePrecision(); - DefaultPrecision(DoublePrecision()); + SetThreadPrecision(DoublePrecision()); GetSystem().precision(16); @@ -1523,7 +1635,7 @@ namespace bertini{ { previous_precision_ = current_precision_; current_precision_ = new_precision; - DefaultPrecision(new_precision); + SetThreadPrecision(new_precision); } @@ -1547,8 +1659,8 @@ namespace bertini{ void AdjustInternalsPrecision(unsigned new_precision) const { GetSystem().precision(new_precision); - predictor_->ChangePrecision(new_precision); - corrector_->ChangePrecision(new_precision); + predictor_.ChangePrecision(new_precision); + corrector_.ChangePrecision(new_precision); endtime_ = endtime_highest_precision_; @@ -1587,16 +1699,16 @@ namespace bertini{ \return True if all internal state variables are in the same precision as current_precision_, will fail on assertion otherwise. */ - template + template bool PrecisionSanityCheck() const { - if constexpr (std::is_same::value){ + if constexpr (std::is_same::value){ return true; } - if constexpr (std::is_same::value){ - assert(DefaultPrecision()==current_precision_ && "current precision differs from the default precision"); + if constexpr (std::is_same::value){ + assert(ThreadPrecision()==current_precision_ && "current precision differs from the thread-local default precision"); assert(GetSystem().precision() == current_precision_ && "tracked system is out of precision"); assert(std::get >(current_space_)(0).precision() == current_precision_ && "current space out of precision"); @@ -1607,7 +1719,7 @@ namespace bertini{ assert(Precision(endtime_) == current_precision_ && "endtime_ out of precision"); assert(Precision(current_time_) == current_precision_ && "current_time_ out of precision"); assert(current_precision_ <= MaxPrecisionAllowed() && "current_precision_ exceeds max precision"); - assert(predictor_->precision() == current_precision_ && "predictor_ out of precision"); + assert(predictor_.precision() == current_precision_ && "predictor_ out of precision"); return true; } @@ -1632,13 +1744,14 @@ namespace bertini{ // state variables ///////////// bool preserve_precision_ = false; ///< Whether the tracker should change back to the initial precision after tracking paths. + std::optional override_start_precision_; ///< If set, tracking starts at this precision instead of the start point's precision. mutable unsigned previous_precision_; ///< The previous precision of the tracker. mutable unsigned current_precision_; ///< The current precision of the tracker, the system, and all temporaries. mutable unsigned next_precision_; ///< The next precision mutable unsigned num_precision_decreases_; ///< The number of times precision has decreased this track. mutable unsigned initial_precision_; ///< The precision at the start of tracking. - mutable unsigned num_successful_steps_since_precision_decrease_; ///< The number of successful steps since decreased precision. + mutable unsigned num_successful_steps_since_precision_decrease_; ///< Consecutive successful steps since precision last decreased; gated by B1's StepsForIncrease. mutable mpfr_complex endtime_highest_precision_; diff --git a/core/include/bertini2/trackers/base_predictor.hpp b/core/include/bertini2/trackers/base_predictor.hpp index d74aa3cb2..6e4211f1b 100644 --- a/core/include/bertini2/trackers/base_predictor.hpp +++ b/core/include/bertini2/trackers/base_predictor.hpp @@ -57,7 +57,13 @@ namespace bertini{ inline Predictor DefaultPredictor() { - return Predictor::Euler; + // RKF45 (Runge-Kutta-Fehlberg 4(5)): order-4 prediction with an embedded error + // estimate. Was Euler (order 1) -- an ODR clash with the RKF45 definition in + // explicit_predictors.hpp, and atrocious for performance (tiny steps), and it forced + // SetSizeProportion onto the no-error-estimate fallback maxCoeff(K)/|delta_t|^p, which + // blows up at small steps and spuriously inflates AMP precision. RKF45 has an error + // estimate, so size_proportion = err_est/|delta_t|^(p+1) stays bounded. + return Predictor::RKF45; } @@ -171,18 +177,18 @@ namespace bertini{ \param tracking_tolerance How tightly to track the path. */ - template - SuccessCode Predict(Vec & next_space, + template + SuccessCode Predict(Vec & next_space, System const& S, - Vec const& current_space, ComplexType current_time, - ComplexType const& delta_t, - RealType & condition_number_estimate, + Vec const& current_space, ComplexT current_time, + ComplexT const& delta_t, + RealT & condition_number_estimate, unsigned & num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation, - RealType const& tracking_tolerance) + RealT const& tracking_tolerance) { - return FullStep(next_space, S, current_space, current_time, delta_t); + return FullStep(next_space, S, current_space, current_time, delta_t); } @@ -209,23 +215,23 @@ namespace bertini{ \param AMP_config The settings for adaptive multiple precision. */ - template - SuccessCode Predict(Vec & next_space, - RealType & size_proportion, - RealType & norm_J, - RealType & norm_J_inverse, + template + SuccessCode Predict(Vec & next_space, + RealT & size_proportion, + RealT & norm_J, + RealT & norm_J_inverse, System const& S, - Vec const& current_space, ComplexType current_time, - ComplexType const& delta_t, - RealType & condition_number_estimate, + Vec const& current_space, ComplexT current_time, + ComplexT const& delta_t, + RealT & condition_number_estimate, unsigned & num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation, - RealType const& tracking_tolerance, + RealT const& tracking_tolerance, AdaptiveMultiplePrecisionConfig const& AMP_config) { - auto success_code = Predict(next_space, S, current_space, current_time, delta_t, + auto success_code = Predict(next_space, S, current_space, current_time, delta_t, condition_number_estimate, num_steps_since_last_condition_number_computation, frequency_of_CN_estimation, tracking_tolerance); @@ -233,11 +239,11 @@ namespace bertini{ return success_code; // Calculate condition number and updated if needed - Eigen::PartialPivLU>& dhdxref = std::get< Eigen::PartialPivLU> >(dh_dx_); - Mat& LUref = std::get< Mat >(LU_); + Eigen::PartialPivLU>& dhdxref = std::get< Eigen::PartialPivLU> >(dh_dx_); + Mat& LUref = std::get< Mat >(LU_); - Vec randy = RandomOfUnits(S.NumVariables()); - Vec temp_soln = LUref.solve(randy); + Vec randy = RandomOfUnits(S.NumVariables()); + Vec temp_soln = LUref.solve(randy); norm_J = dhdxref.norm(); norm_J_inverse = temp_soln.norm(); @@ -252,7 +258,7 @@ namespace bertini{ // Set size_proportion - SetSizeProportion(size_proportion, delta_t); + SetSizeProportion(size_proportion, delta_t); @@ -288,19 +294,19 @@ namespace bertini{ \param AMP_config The settings for adaptive multiple precision. */ - template - SuccessCode Predict(Vec & next_space, - RealType & error_estimate, - RealType & size_proportion, - RealType & norm_J, - RealType & norm_J_inverse, + template + SuccessCode Predict(Vec & next_space, + RealT & error_estimate, + RealT & size_proportion, + RealT & norm_J, + RealT & norm_J_inverse, System const& S, - Vec const& current_space, ComplexType current_time, - ComplexType const& delta_t, - RealType & condition_number_estimate, + Vec const& current_space, ComplexT current_time, + ComplexT const& delta_t, + RealT & condition_number_estimate, unsigned & num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation, - RealType const& tracking_tolerance, + RealT const& tracking_tolerance, AdaptiveMultiplePrecisionConfig const& AMP_config) { // If this is a method without an error estimator, then can't calculate size proportion and should throw an error @@ -313,7 +319,7 @@ namespace bertini{ - auto success_code = Predict(next_space, size_proportion, norm_J, norm_J_inverse, + auto success_code = Predict(next_space, size_proportion, norm_J, norm_J_inverse, S, current_space, current_time, delta_t, condition_number_estimate, num_steps_since_last_condition_number_computation, frequency_of_CN_estimation, tracking_tolerance, AMP_config); @@ -321,7 +327,7 @@ namespace bertini{ if(success_code != SuccessCode::Success) return success_code; - SetErrorEstimate(error_estimate, delta_t); + SetErrorEstimate(error_estimate, delta_t); return success_code; @@ -385,11 +391,11 @@ namespace bertini{ \return SuccessCode determining result of the computation */ - template - virtual SuccessCode FullStep(Vec & next_space, + template + virtual SuccessCode FullStep(Vec & next_space, System const& S, - Vec const& current_space, ComplexType current_time, - ComplexType const& delta_t) = 0; + Vec const& current_space, ComplexT current_time, + ComplexT const& delta_t) = 0; /** @@ -402,8 +408,8 @@ namespace bertini{ */ - template - virtual SuccessCode SetErrorEstimate(RealType & error_estimate, ComplexType const& delta_t) = 0; + template + virtual SuccessCode SetErrorEstimate(RealT & error_estimate, ComplexT const& delta_t) = 0; @@ -417,8 +423,8 @@ namespace bertini{ */ - template - virtual SuccessCode SetSizeProportion(RealType & size_proportion, ComplexType const& delta_t) = 0; + template + virtual SuccessCode SetSizeProportion(RealT & size_proportion, ComplexT const& delta_t) = 0; @@ -434,9 +440,9 @@ namespace bertini{ \return Success code of this computation */ - template + template virtual SuccessCode EvalRHS(System const& S, - Vec const& space, ComplexType time, Mat & K, int stage) = 0; + Vec const& space, ComplexT time, Mat & K, int stage) = 0; diff --git a/core/include/bertini2/trackers/base_tracker.hpp b/core/include/bertini2/trackers/base_tracker.hpp index 29dd14c15..cc7983260 100644 --- a/core/include/bertini2/trackers/base_tracker.hpp +++ b/core/include/bertini2/trackers/base_tracker.hpp @@ -140,11 +140,11 @@ namespace bertini{ > { using NeededTypes = typename TrackerTraits< D >::NeededTypes; - using BaseComplexType = typename TrackerTraits::BaseComplexType; - using BaseRealType = typename TrackerTraits::BaseRealType; + using BaseComplexT = typename TrackerTraits::BaseComplexT; + using BaseRealT = typename TrackerTraits::BaseRealT; - using CT = BaseComplexType; - using RT = BaseRealType; + using ComplexT = BaseComplexT; + using RealT = BaseRealT; public: @@ -153,10 +153,10 @@ namespace bertini{ using Newton = NewtonConfig; using PrecConf = typename TrackerTraits< D >::PrecisionConfig; - Tracker(System const& sys) : tracked_system_(std::ref(sys)) + Tracker(System const& sys) : tracked_system_(std::ref(sys)), + predictor_(predict::DefaultPredictor(), sys), + corrector_(sys) { - predictor_ = std::make_shared< predict::ExplicitRKPredictor >(predict::DefaultPredictor(), tracked_system_); - corrector_ = std::make_shared< correct::NewtonCorrector >(tracked_system_); SetPredictor(predict::DefaultPredictor()); } @@ -175,7 +175,7 @@ namespace bertini{ NewtonConfig const& newton) { SetPredictor(new_predictor_choice); - corrector_->Settings(newton); + corrector_.Settings(newton); SetTrackingTolerance(tracking_tolerance); @@ -184,7 +184,7 @@ namespace bertini{ this->template Set(stepping); this->template Set(newton); - current_stepsize_ = BaseRealType(stepping.initial_step_size); + current_stepsize_ = BaseRealT(stepping.initial_step_size); } @@ -218,6 +218,21 @@ namespace bertini{ } + /** + \brief (Re)draw the random probe direction used for condition-number estimation. + + Call this once, from the algorithm, at the start of tracking a point -- after any per-path + RNG reseed and before TrackPath -- so the direction is fixed for that whole point's track + (pre-endgame tracking and the endgame both) and is reproducible across runs/ranks. Do NOT + call it per TrackPath: the endgame issues many TrackPath calls per point, and refreshing + the direction mid-track perturbs condition estimates and churns the RNG. + */ + void RefreshConditionDirection() + { + corrector_.RefreshRandomDirection(); + } + + /** \brief Track a start point through time, from a start time to a target time. @@ -229,12 +244,12 @@ namespace bertini{ The is the fundamental method for the tracker. First, you create and set up the tracker, telling it what system you will solve, and the settings to use. Then, you actually do the tracking. */ - SuccessCode TrackPath(Vec & solution_at_endtime, - CT const& start_time, CT const& endtime, - Vec const& start_point + SuccessCode TrackPath(Vec & solution_at_endtime, + ComplexT const& start_time, ComplexT const& endtime, + Vec const& start_point ) const { - if (start_point.size()!=GetSystem().NumVariables()) + if (start_point.size()!=static_cast(GetSystem().NumVariables())) throw std::runtime_error("start point size must match the number of variables in the system to be tracked"); @@ -246,8 +261,10 @@ namespace bertini{ return initialization_code; } + NotifyObservers(TrackingStarted::EventEmitterType>(static_cast::EventEmitterType const&>(*this))); + // as precondition to this while loop, the correct container, either dbl or mpfr, must have the correct data. - while (!IsSymmRelDiffSmall(current_time_,endtime_, Eigen::NumTraits::epsilon())) + while (!IsSymmRelDiffSmall(current_time_,endtime_, Eigen::NumTraits::epsilon())) { SuccessCode pre_iteration_code = PreIterationCheck(); if (pre_iteration_code!=SuccessCode::Success) @@ -263,7 +280,6 @@ namespace bertini{ else delta_t_ = current_stepsize_ * (endtime_ - current_time_)/abs(endtime_ - current_time_); - step_success_code_ = TrackerIteration(); if (infinite_path_truncation_ && (CheckGoingToInfinity()==SuccessCode::GoingToInfinity)) @@ -343,8 +359,8 @@ namespace bertini{ */ void SetPredictor(Predictor new_predictor_choice) { - predictor_->PredictorMethod(new_predictor_choice); - predictor_order_ = predictor_->Order(); + predictor_.PredictorMethod(new_predictor_choice); + predictor_order_ = predictor_.Order(); } @@ -353,7 +369,7 @@ namespace bertini{ */ Predictor GetPredictor() const { - return predictor_->PredictorMethod(); + return predictor_.PredictorMethod(); } @@ -363,8 +379,8 @@ namespace bertini{ void SetSystem(const System & new_sys) { tracked_system_ = std::ref(new_sys); - predictor_->ChangeSystem(tracked_system_); - corrector_->ChangeSystem(tracked_system_); + predictor_.ChangeSystem(tracked_system_); + corrector_.ChangeSystem(tracked_system_); } /** @@ -390,7 +406,7 @@ namespace bertini{ \param new_stepsize The new value. */ - void SetStepSize(RT const& new_stepsize) const + void SetStepSize(RealT const& new_stepsize) const { current_stepsize_ = new_stepsize; } @@ -434,7 +450,7 @@ namespace bertini{ \param start_point The point from which to start tracking. */ virtual - SuccessCode TrackerLoopInitialization(CT const& start_time, CT const& end_time, Vec const& start_point) const = 0; + SuccessCode TrackerLoopInitialization(ComplexT const& start_time, ComplexT const& end_time, Vec const& start_point) const = 0; /** @@ -459,7 +475,7 @@ namespace bertini{ \param solution_at_endtime The output variable into which to copy the final solution. */ virtual - void CopyFinalSolution(Vec & solution_at_endtime) const = 0; + void CopyFinalSolution(Vec & solution_at_endtime) const = 0; // virtual // void CopyFinalSolution(Vec & solution_at_endtime) const = 0; @@ -471,10 +487,10 @@ namespace bertini{ - template + template SuccessCode CheckGoingToInfinity() const { - if (GetSystem().DehomogenizePoint(std::get >(current_space_)).norm() > path_truncation_threshold_) + if (GetSystem().DehomogenizePoint(std::get >(current_space_)).norm() > path_truncation_threshold_) return SuccessCode::GoingToInfinity; else return SuccessCode::Success; @@ -570,10 +586,18 @@ namespace bertini{ // configuration for tracking - std::shared_ptr predictor_; // The predictor to use while tracking + // + // predictor and corrector are held BY VALUE so that copying a tracker + // deep-copies them. (They were previously shared_ptr, which made every + // tracker copy share one predictor/corrector with its source — unusable + // from multiple threads.) Both hold only work buffers and settings; they + // store no reference to the System, so plain memberwise copy is correct. + // They are mutable for the same reason as the state members above: they + // hold scratch space mutated during the logically-const TrackPath. + mutable predict::ExplicitRKPredictor predictor_; // The predictor to use while tracking unsigned predictor_order_; ///< The order of the predictor -- one less than the error estimate order. - std::shared_ptr corrector_; + mutable correct::NewtonCorrector corrector_; @@ -582,21 +606,20 @@ namespace bertini{ NumErrorT tracking_tolerance_ = 1e-5; ///< The tracking tolerance. NumErrorT path_truncation_threshold_ = 1e5; ///< The threshold for path truncation. - mutable CT endtime_; ///< The time we are tracking to. - mutable CT current_time_; ///< The current time. - mutable CT delta_t_; ///< The current delta_t. - mutable RT current_stepsize_; ///< The current stepsize. + mutable ComplexT endtime_; ///< The time we are tracking to. + mutable ComplexT current_time_; ///< The current time. + mutable ComplexT delta_t_; ///< The current delta_t. + mutable RealT current_stepsize_; ///< The current stepsize. // permanent temporaries - mutable RT next_stepsize_; /// The next stepsize + mutable RealT next_stepsize_; /// The next stepsize mutable SuccessCode step_success_code_; ///< The code for step success. mutable unsigned num_steps_since_last_condition_number_computation_; ///< How many steps have passed since the most recent condition number estimate. mutable unsigned num_successful_steps_since_stepsize_increase_; ///< How many successful steps have been taken since increased stepsize. - mutable unsigned num_successful_steps_since_precision_decrease_; ///< The number of successful steps since decreased precision. using TupOfVec = typename NeededTypes::ToTupleOfVec; using TupOfReal = typename NeededTypes::ToTupleOfReal; @@ -648,7 +671,7 @@ namespace bertini{ unsigned NumVariables() const { - return GetSystem().NumVariables(); + return static_cast(GetSystem().NumVariables()); } auto CurrentTime() const @@ -667,7 +690,7 @@ namespace bertini{ } - virtual Vec CurrentPoint() const = 0; + virtual Vec CurrentPoint() const = 0; virtual unsigned CurrentPrecision() const = 0; diff --git a/core/include/bertini2/trackers/config.hpp b/core/include/bertini2/trackers/config.hpp index e5c960e45..1dbd412b1 100644 --- a/core/include/bertini2/trackers/config.hpp +++ b/core/include/bertini2/trackers/config.hpp @@ -48,6 +48,7 @@ namespace tracking{ enum class PrecisionType //E.2.1 { Fixed, + FixedMultiple, Adaptive }; @@ -74,14 +75,15 @@ namespace tracking{ struct SteppingConfig { - using T = mpq_rational; + // mpq_rational: exact rationals with no MPFR precision state — safe in DefaultConstruct::value statics. + // mpfr_float fields here would be initialized at BMP's startup precision (20) and contaminate + // tracker arithmetic when target precision < 20 via preserve_related_precision. + mpq_rational initial_step_size{1, 10}; ///< The length of the first time step when calling TrackPath. StepInitSize + mpq_rational max_step_size{1, 10}; ///< The largest allowed step size. MaxStepSize + double min_step_size = 1e-100; ///< The minimum allowed step size (threshold only, double precision is sufficient). MinStepSize - T initial_step_size = T(1)/T(10); ///< The length of the first time step when calling TrackPath. You can turn it resetting, so subsequent calls use the same stepsize, too. You make a call to the Tracker itself. - T max_step_size = T(1)/T(10); ///< The largest allowed step size. MaxStepSize - T min_step_size = T(1)/T(1e100); ///< The mimum allowed step size. MinStepSize - - T step_size_success_factor = T(2); ///< Factor by which to dilate the time step when triggered. StepSuccessFactor - T step_size_fail_factor = T(1)/T(2); ///< Factor by which to contract the time step when triggered. StepFailFactor + mpq_rational step_size_success_factor{2, 1}; ///< Factor by which to dilate the time step when triggered. StepSuccessFactor + mpq_rational step_size_fail_factor{1, 2}; ///< Factor by which to contract the time step when triggered. StepFailFactor unsigned consecutive_successful_steps_before_stepsize_increase = 5; ///< What it says. If you can come up with a better name, please suggest it. StepsForIncrease @@ -108,13 +110,25 @@ namespace tracking{ struct FixedPrecisionConfig { - using RealType = double; + using RealT = double; + + /** + \brief The number of digits to always work at. + + For a double-precision tracker this is DoublePrecision() (16) and cannot be changed. For a + fixed-multiple tracker it is the precision the whole solve runs at -- the tracker, the system, + the start points, and the working precision all sit at this one value. A tracker keeps this + field in sync with its actual precision, so reading it tells you the precision in effect; set it + to choose a different fixed precision (the algorithm then lifts the system and start points to + match). The sentinel 0 means "unset -- use the tracker's natural precision". + */ + unsigned precision = 0; /** \brief Construct a ready-to-go set of fixed precision settings from a system. */ explicit - FixedPrecisionConfig(System const& sys) + FixedPrecisionConfig(System const& /*sys*/) { } FixedPrecisionConfig() = default; @@ -122,7 +136,7 @@ namespace tracking{ inline - std::ostream& operator<<(std::ostream & out, FixedPrecisionConfig const& fpc) + std::ostream& operator<<(std::ostream & out, FixedPrecisionConfig const& /*fpc*/) { return out; } @@ -169,8 +183,13 @@ namespace tracking{ int safety_digits_1 = 1; ///< User-chosen setting for the number of safety digits used during Criteria A & B. int safety_digits_2 = 1; ///< User-chosen setting for the number of safety digits used during Criterion C. unsigned int maximum_precision = 300; ///< User-chosed setting for the maximum allowable precision. Paths will die if their precision is requested to be set higher than this threshold. - - unsigned consecutive_successful_steps_before_precision_decrease = 10; + + // Note: a single setting -- Bertini 1's StepsForIncrease, i.e. + // SteppingConfig::consecutive_successful_steps_before_stepsize_increase -- gates BOTH stepsize + // increase AND precision decrease (the required number of consecutive successful steps). + // Precision decrease is additionally subject to B1's digits-margin hysteresis (see + // ExtraDigitsBeforePrecisionDecrease in amp_tracker.hpp). The old, duplicate AMP-config setting + // `consecutive_successful_steps_before_precision_decrease` (a B2 deviation) was removed. unsigned max_num_precision_decreases = 10; ///< The maximum number of times precision can be lowered during tracking of a segment of path. @@ -230,7 +249,7 @@ namespace tracking{ out << "Psi: " << AMP.Psi << "\n"; out << "safety_digits_1: " << AMP.safety_digits_1 << "\n"; out << "safety_digits_2: " << AMP.safety_digits_2 << "\n"; - out << "consecutive_successful_steps_before_precision_decrease" << AMP.consecutive_successful_steps_before_precision_decrease << "\n"; + out << "max_num_precision_decreases: " << AMP.max_num_precision_decreases << "\n"; return out; } @@ -273,8 +292,8 @@ namespace tracking{ template<> struct TrackerTraits { - using BaseComplexType = dbl; - using BaseRealType = double; + using BaseComplexT = dbl; + using BaseRealT = double; using EventEmitterType = FixedPrecisionTracker; using PrecisionConfig = FixedPrecisionConfig; enum { @@ -294,8 +313,8 @@ namespace tracking{ template<> struct TrackerTraits { - using BaseComplexType = mpfr_complex; - using BaseRealType = mpfr_float; + using BaseComplexT = mpfr_complex; + using BaseRealT = mpfr_float; using EventEmitterType = FixedPrecisionTracker; using PrecisionConfig = FixedPrecisionConfig; @@ -318,8 +337,8 @@ namespace tracking{ template<> struct TrackerTraits { - using BaseComplexType = mpfr_complex; - using BaseRealType = mpfr_float; + using BaseComplexT = mpfr_complex; + using BaseRealT = mpfr_float; using EventEmitterType = AMPTracker; using PrecisionConfig = AdaptiveMultiplePrecisionConfig; @@ -343,8 +362,8 @@ namespace tracking{ template struct TrackerTraits > : public TrackerTraits { - using BaseComplexType = typename TrackerTraits::BaseComplexType; - using BaseRealType = typename TrackerTraits::BaseRealType; + using BaseComplexT = typename TrackerTraits::BaseComplexT; + using BaseRealT = typename TrackerTraits::BaseRealT; using EventEmitterType = typename TrackerTraits::EventEmitterType; using PrecisionConfig = typename TrackerTraits::PrecisionConfig; diff --git a/core/include/bertini2/trackers/explicit_predictors.hpp b/core/include/bertini2/trackers/explicit_predictors.hpp index f9010fc31..a8d580ed3 100644 --- a/core/include/bertini2/trackers/explicit_predictors.hpp +++ b/core/include/bertini2/trackers/explicit_predictors.hpp @@ -150,31 +150,6 @@ namespace bertini{ } - namespace { - template - struct LUSelector - {}; - - template<> - struct LUSelector - { - template - static Eigen::PartialPivLU>& Run(N & n) - { - return n.GetLU_d(); - } - }; - - template<> - struct LUSelector - { - template - static Eigen::PartialPivLU>& Run(N & n) - { - return n.GetLU_mp(); - } - }; - } /** \class ExplicitRKPredictor @@ -200,8 +175,6 @@ namespace bertini{ */ class ExplicitRKPredictor { - friend LUSelector; - friend LUSelector; public: /** @@ -209,7 +182,7 @@ namespace bertini{ \param S the system the predictor will be predicting on. */ - ExplicitRKPredictor(const System& S) : current_precision_(DefaultPrecision()), s_(0) + ExplicitRKPredictor(const System& S) : s_(0), current_precision_(DefaultPrecision()) { ChangeSystem(S); PredictorMethod(DefaultPredictor()); @@ -221,7 +194,7 @@ namespace bertini{ \param method The predictor method to be implemented. \param S the system to be predicting on. */ - ExplicitRKPredictor(Predictor method, const System& S) : current_precision_(DefaultPrecision()), s_(0) + ExplicitRKPredictor(Predictor method, const System& S) : s_(0), current_precision_(DefaultPrecision()) { ChangeSystem(S); PredictorMethod(method); @@ -356,8 +329,8 @@ namespace bertini{ */ void ChangeSystem(const System& S) { - numTotalFunctions_ = S.NumTotalFunctions(); - numVariables_ = S.NumVariables(); + numTotalFunctions_ = static_cast(S.NumTotalFunctions()); + numVariables_ = static_cast(S.NumVariables()); // you cannot set K_ here, because s_ may not have been set std::get< Mat >(dh_dx_0_).resize(numTotalFunctions_, numVariables_); std::get< Mat >(dh_dx_0_).resize(numTotalFunctions_, numVariables_); @@ -365,6 +338,12 @@ namespace bertini{ std::get< Mat >(dh_dx_temp_).resize(numTotalFunctions_, numVariables_); std::get< Vec >(dh_dt_temp_).resize(numTotalFunctions_); std::get< Vec >(dh_dt_temp_).resize(numTotalFunctions_); + std::get< Vec >(step_temp_).resize(numTotalFunctions_); + std::get< Vec >(step_temp_).resize(numTotalFunctions_); + std::get< Vec >(rand_temp_) = RandomOfUnits(numVariables_); + std::get< Vec >(rand_temp_) = RandomOfUnits(numVariables_); + std::get< Vec >(solve_temp_).resize(numVariables_); + std::get< Vec >(solve_temp_).resize(numVariables_); ResizeK(); } @@ -398,6 +377,9 @@ namespace bertini{ Precision(std::get< Vec >(dh_dt_temp_),new_precision); Precision(std::get< Mat >(dh_dx_0_),new_precision); Precision(std::get< Mat >(dh_dx_temp_),new_precision); + Precision(std::get< Vec >(step_temp_),new_precision); + Precision(std::get< Vec >(rand_temp_),new_precision); + Precision(std::get< Vec >(solve_temp_),new_precision); Precision(std::get< Mat >(a_),new_precision); Precision(std::get< Vec >(b_),new_precision); @@ -413,7 +395,10 @@ namespace bertini{ void PrecisionSanityCheck() const { - assert(current_precision_==DefaultPrecision()); +#ifndef NDEBUG + // ThreadPrecision: correct when running on a std::thread worker, + // where precision is set via SetThreadPrecision (thread-local only). + assert(current_precision_==ThreadPrecision()); Vec& dhdttemp = std::get< Vec >(dh_dt_temp_); Mat& dhdx0 = std::get< Mat >(dh_dx_0_); @@ -435,6 +420,7 @@ namespace bertini{ if (uses_embedded_) assert(Precision(bstar)==current_precision_); assert(Precision(c)==current_precision_); +#endif } @@ -456,21 +442,21 @@ namespace bertini{ \return SuccessCode indicating how the prediction went. */ - template - SuccessCode Predict(Vec & next_space, + template + SuccessCode Predict(Vec & next_space, System const& S, - const Vec& current_space, ComplexType current_time, - ComplexType const& delta_t, + const Vec& current_space, ComplexT current_time, + ComplexT const& delta_t, NumErrorT & condition_number_estimate, unsigned & num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation, - NumErrorT const& tracking_tolerance) + NumErrorT const& /*tracking_tolerance*/) { auto step_success = FullStep(next_space, S, current_space, current_time, delta_t); NumErrorT norm_J, norm_J_inverse; - SetNormsCond(norm_J, norm_J_inverse, condition_number_estimate, num_steps_since_last_condition_number_computation, frequency_of_CN_estimation); + SetNormsCond(norm_J, norm_J_inverse, condition_number_estimate, num_steps_since_last_condition_number_computation, frequency_of_CN_estimation); return step_success; } @@ -499,14 +485,14 @@ namespace bertini{ \return SuccessCode indicating how the prediction went. */ - template - SuccessCode Predict(Vec & next_space, + template + SuccessCode Predict(Vec & next_space, NumErrorT & size_proportion, NumErrorT & norm_J, NumErrorT & norm_J_inverse, System const& S, - const Vec& current_space, ComplexType current_time, - ComplexType const& delta_t, + const Vec& current_space, ComplexT current_time, + ComplexT const& delta_t, NumErrorT & condition_number_estimate, unsigned & num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation, @@ -515,11 +501,11 @@ namespace bertini{ { - auto success_code = Predict(next_space, S, current_space, current_time, delta_t, + auto success_code = Predict(next_space, S, current_space, current_time, delta_t, condition_number_estimate, num_steps_since_last_condition_number_computation, frequency_of_CN_estimation, tracking_tolerance); - SetNormsCond(norm_J, norm_J_inverse, condition_number_estimate, num_steps_since_last_condition_number_computation, frequency_of_CN_estimation); + SetNormsCond(norm_J, norm_J_inverse, condition_number_estimate, num_steps_since_last_condition_number_computation, frequency_of_CN_estimation); // Set size_proportion SetSizeProportion(size_proportion, delta_t); @@ -530,11 +516,11 @@ namespace bertini{ //AMP Criteria - if (!amp::CriterionA(norm_J, norm_J_inverse, AMP_config)) // AMP_criterion_A != ok + if (!amp::CriterionA(norm_J, norm_J_inverse, AMP_config)) // AMP_criterion_A != ok { return SuccessCode::HigherPrecisionNecessary; } - else if (!amp::CriterionC(norm_J_inverse, current_space, tracking_tolerance, AMP_config)) // AMP_criterion_C != ok + else if (!amp::CriterionC(norm_J_inverse, current_space, tracking_tolerance, AMP_config)) // AMP_criterion_C != ok { return SuccessCode::HigherPrecisionNecessary; } @@ -567,15 +553,15 @@ namespace bertini{ \return SuccessCode indicating how the prediction went. */ - template - SuccessCode Predict(Vec & next_space, + template + SuccessCode Predict(Vec & next_space, NumErrorT & error_estimate, NumErrorT & size_proportion, NumErrorT & norm_J, NumErrorT & norm_J_inverse, System const& S, - const Vec& current_space, ComplexType current_time, - ComplexType const& delta_t, + const Vec& current_space, ComplexT current_time, + ComplexT const& delta_t, NumErrorT & condition_number_estimate, unsigned & num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation, @@ -651,23 +637,7 @@ namespace bertini{ // //////////////////// - template - Eigen::PartialPivLU>& GetLU() - { - return LUSelector::Run(*this); - } - - - Eigen::PartialPivLU>& GetLU_d() - { - return LU_d_; - } - Eigen::PartialPivLU>& GetLU_mp() - { - assert(current_precision_==DefaultPrecision()); - return LU_mp_[current_precision_]; - } /** \brief Performs a full prediction step from current_time to current_time + delta_t @@ -681,11 +651,11 @@ namespace bertini{ \return SuccessCode determining result of the computation */ - template - SuccessCode FullStep(Vec & next_space, + template + SuccessCode FullStep(Vec & next_space, System const& S, - Vec const& current_space, ComplexType const& current_time, - ComplexType const& delta_t) + Vec const& current_space, ComplexT const& current_time, + ComplexT const& delta_t) { // If using constant predictor @@ -695,34 +665,33 @@ namespace bertini{ return SuccessCode::Success; } - using RealType = typename Eigen::NumTraits::Real; - - Mat& Kref = std::get< Mat >(K_); - Mat& aref = std::get< Mat >(a_); - Vec& bref = std::get< Vec >(b_); - Vec& cref = std::get< Vec >(c_); - Kref.fill(ComplexType(0)); - Vec temp(S.NumTotalFunctions()); - + using RealT = typename Eigen::NumTraits::Real; + + Mat& Kref = std::get< Mat >(K_); + Mat& aref = std::get< Mat >(a_); + Vec& bref = std::get< Vec >(b_); + Vec& cref = std::get< Vec >(c_); + Kref.fill(ComplexT(0)); + Vec& temp = std::get< Vec >(step_temp_); + if(EvalRHS(S, current_space, current_time, Kref, 0) != SuccessCode::Success) { return SuccessCode::MatrixSolveFailureFirstPartOfPrediction; } - for(int ii = 1; ii < s_; ++ii) + for(unsigned ii = 1; ii < s_; ++ii) { temp.setZero(); // see https://github.com/bertiniteam/b2/issues/198 - for(int jj = 0; jj < ii; ++jj) + for(unsigned jj = 0; jj < ii; ++jj) temp += aref(ii,jj)*Kref.col(jj); - // Vec wfp = - if(EvalRHS(S, current_space + delta_t*temp, current_time + cref(ii)*delta_t, Kref, ii) != SuccessCode::Success) + if(EvalRHS(S, current_space + delta_t*temp, current_time + cref(ii)*delta_t, Kref, ii) != SuccessCode::Success) return SuccessCode::MatrixSolveFailure; } temp.setZero(); - for(int ii = 0; ii < s_; ++ii) + for(unsigned ii = 0; ii < s_; ++ii) temp += bref(ii)*Kref.col(ii); next_space = current_space + delta_t*temp; @@ -731,20 +700,21 @@ namespace bertini{ }; - template + template void SetNormsCond(NumErrorT & norm_J, NumErrorT & norm_J_inverse, NumErrorT & condition_number_estimate, unsigned num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation) { // Calculate condition number and update if needed - Eigen::PartialPivLU>& LUref = GetLU(); - Mat& dhdxref = std::get< Mat >(dh_dx_0_); + Eigen::PartialPivLU>& LUref = std::get< Eigen::PartialPivLU> >(LU_); + Mat& dhdxref = std::get< Mat >(dh_dx_0_); + + Vec const& randy = std::get< Vec >(rand_temp_); + Vec& solve_ref = std::get< Vec >(solve_temp_); + solve_ref = LUref.solve(randy); - // TODO this random vector should not be made fresh every time. especiallyif the numeric type is mpfr_complex! - Vec randy = RandomOfUnits(numVariables_); - Vec temp_soln = LUref.solve(randy); - norm_J = NumErrorT(dhdxref.norm()); - norm_J_inverse = NumErrorT(temp_soln.norm()); - + norm_J_inverse = NumErrorT(solve_ref.norm()); + + if (num_steps_since_last_condition_number_computation >= frequency_of_CN_estimation) { condition_number_estimate = NumErrorT(norm_J * norm_J_inverse); @@ -765,19 +735,19 @@ namespace bertini{ */ - template - SuccessCode SetErrorEstimate(NumErrorT & error_estimate, ComplexType const& delta_t) + template + SuccessCode SetErrorEstimate(NumErrorT & error_estimate, ComplexT const& delta_t) { - using RealType = typename Eigen::NumTraits::Real; + using RealT = typename Eigen::NumTraits::Real; - Mat& Kref = std::get< Mat >(K_); - Vec& b_minus_bstar_ref = std::get< Vec >(b_minus_bstar_); + Mat& Kref = std::get< Mat >(K_); + Vec& b_minus_bstar_ref = std::get< Vec >(b_minus_bstar_); auto numFuncs = Kref.rows(); - Vec err(numFuncs); + Vec err(numFuncs); err.setZero(); - for(int ii = 0; ii < s_; ++ii) + for(unsigned ii = 0; ii < s_; ++ii) { err += (b_minus_bstar_ref(ii))*Kref.col(ii); } @@ -804,8 +774,8 @@ namespace bertini{ */ - template - SuccessCode SetSizeProportion(NumErrorT & size_proportion, ComplexType const& delta_t) + template + SuccessCode SetSizeProportion(NumErrorT & size_proportion, ComplexT const& delta_t) { if(predict::HasErrorEstimate(predictor_)) { @@ -819,9 +789,20 @@ namespace bertini{ } else { - Mat& Kref = std::get< Mat >(K_); - using std::pow; - size_proportion = NumErrorT(Kref.array().abs().maxCoeff()/(pow(abs(delta_t), p_))); + // No embedded error estimate (e.g. Euler, RK4). AMP2 (bhswAMP2, Eqs 9-10) + // derives the size proportion $a$ from the prediction step itself, written as + // the initial Newton residual: ||d|| = a|s|. The predicted step in z is + // delta_t*(sum_i b_i K_i), so ||d|| = |delta_t|*||K|| and therefore + // a = ||d|| / |s| = ||K|| ~ maxCoeff(|K|), + // with NO further division by |delta_t|. The previous + // maxCoeff(K)/|delta_t|^p over-divided by the step: it inflated $a$ like + // 1/|delta_t|^p as the step shrank, spuriously escalating AMP precision + // (DigitsB) on small steps. $a$ is meant to be an O(1), step-independent + // proportionality constant -- see SetSizeProportion's error-estimate branch, + // which divides err_est by |delta_t|^(p+1) for exactly the same reason + // (AMP3 / bhsODEAMP Eq. 6). + Mat& Kref = std::get< Mat >(K_); + size_proportion = NumErrorT(Kref.array().abs().maxCoeff()); return SuccessCode::Success; } }; @@ -840,32 +821,33 @@ namespace bertini{ \return Success code of this computation */ - template + template SuccessCode EvalRHS(System const& S, - const Vec& space, const ComplexType& time, Mat & K, unsigned stage) + const Vec& space, const ComplexT& time, Mat & K, unsigned stage) { - if (std::is_same::value) + + if (std::is_same::value) PrecisionSanityCheck(); if(stage == 0) { - Eigen::PartialPivLU>& LUref = GetLU(); - Mat& dhdxref = std::get< Mat >(dh_dx_0_); + Eigen::PartialPivLU>& LUref = std::get< Eigen::PartialPivLU> >(LU_); + Mat& dhdxref = std::get< Mat >(dh_dx_0_); - if (!std::is_same::value) + if (!std::is_same::value) { - assert(DefaultPrecision()==current_precision_); + assert(ThreadPrecision()==current_precision_); assert(Precision(space)==current_precision_); assert(Precision(time)==current_precision_); assert(Precision(dhdxref)==current_precision_); assert(Precision(K)==current_precision_); } - S.SetAndReset(space, time); + S.SetAndReset(space, time); S.JacobianInPlace(dhdxref); - LUref = dhdxref.lu(); - if (!std::is_same::value) + LUref.compute(dhdxref); + if (!std::is_same::value) { assert(Precision(dhdxref)==current_precision_); assert(Precision(LUref.matrixLU())==current_precision_); @@ -874,7 +856,7 @@ namespace bertini{ if (LUPartialPivotDecompositionSuccessful(LUref.matrixLU())!=MatrixSuccessCode::Success) return SuccessCode::MatrixSolveFailureFirstPartOfPrediction; - Vec& dhdtref = std::get< Vec >(dh_dt_temp_); + Vec& dhdtref = std::get< Vec >(dh_dt_temp_); S.TimeDerivativeInPlace(dhdtref); K.col(stage) = LUref.solve(-dhdtref); @@ -883,18 +865,19 @@ namespace bertini{ } else { - S.SetAndReset(space, time); + S.SetAndReset(space, time); - Mat& dhdxtempref = std::get< Mat >(dh_dx_temp_); + Mat& dhdxtempref = std::get< Mat >(dh_dx_temp_); S.JacobianInPlace(dhdxtempref); - auto LU = dhdxtempref.lu(); - - if (LUPartialPivotDecompositionSuccessful(LU.matrixLU())!=MatrixSuccessCode::Success) + Eigen::PartialPivLU>& LU_temp = std::get< Eigen::PartialPivLU> >(LU_); + LU_temp.compute(dhdxtempref); + + if (LUPartialPivotDecompositionSuccessful(LU_temp.matrixLU())!=MatrixSuccessCode::Success) return SuccessCode::MatrixSolveFailure; - - Vec& dhdtref = std::get< Vec >(dh_dt_temp_); + + Vec& dhdtref = std::get< Vec >(dh_dt_temp_); S.TimeDerivativeInPlace(dhdtref); - K.col(stage) = LU.solve(-dhdtref); + K.col(stage) = LU_temp.solve(-dhdtref); return SuccessCode::Success; } @@ -927,41 +910,41 @@ namespace bertini{ */ - template + template void FillButcherTable(int stages, const Mat& a, const Mat & b, const Mat & b_minus_bstar, const Mat & c) { - Mat& aref = std::get< Mat >(a_); + Mat& aref = std::get< Mat >(a_); aref.resize(stages, stages); for(int ii = 0; ii < stages; ++ii) { - for(int jj = 0; jj < s_; ++jj) + for(unsigned jj = 0; jj < s_; ++jj) { - aref(ii,jj) = static_cast(a(ii,jj)); + aref(ii,jj) = static_cast(a(ii,jj)); } } - Vec& bref = std::get< Vec >(b_); + Vec& bref = std::get< Vec >(b_); bref.resize(stages); for(int ii = 0; ii < stages; ++ii) { - bref(ii) = static_cast(b(ii)); + bref(ii) = static_cast(b(ii)); } - Vec& b_minus_bstar_ref = std::get< Vec >(b_minus_bstar_); + Vec& b_minus_bstar_ref = std::get< Vec >(b_minus_bstar_); b_minus_bstar_ref.resize(stages); for(int ii = 0; ii < stages; ++ii) { - b_minus_bstar_ref(ii) = static_cast(b_minus_bstar(ii)); + b_minus_bstar_ref(ii) = static_cast(b_minus_bstar(ii)); } - Vec& cref = std::get< Vec >(c_); + Vec& cref = std::get< Vec >(c_); cref.resize(stages); for(int ii = 0; ii < stages; ++ii) { - cref(ii) = static_cast(c(ii)); + cref(ii) = static_cast(c(ii)); } uses_embedded_ = true; @@ -981,33 +964,33 @@ namespace bertini{ */ - template + template void FillButcherTable(int stages, const Mat& a, const Mat & b, const Mat & c) { - Mat& aref = std::get< Mat >(a_); + Mat& aref = std::get< Mat >(a_); aref.resize(stages, stages); for(int ii = 0; ii < stages; ++ii) { - for(int jj = 0; jj < s_; ++jj) + for(unsigned jj = 0; jj < s_; ++jj) { - aref(ii,jj) = static_cast(a(ii,jj)); + aref(ii,jj) = static_cast(a(ii,jj)); } } - Vec& bref = std::get< Vec >(b_); + Vec& bref = std::get< Vec >(b_); bref.resize(stages); for(int ii = 0; ii < stages; ++ii) { - bref(ii) = static_cast(b(ii)); + bref(ii) = static_cast(b(ii)); } - Vec& cref = std::get< Vec >(c_); + Vec& cref = std::get< Vec >(c_); cref.resize(stages); for(int ii = 0; ii < stages; ++ii) { - cref(ii) = static_cast(c(ii)); + cref(ii) = static_cast(c(ii)); } uses_embedded_ = false; @@ -1034,8 +1017,11 @@ namespace bertini{ mutable std::tuple< Vec, Vec > dh_dt_temp_; // Temporary time derivative used for all stages // std::tuple< Eigen::PartialPivLU>, Eigen::PartialPivLU> > LU_0_; // LU from the intial stage used for AMP testing - mutable Eigen::PartialPivLU> LU_d_; - mutable std::map>> LU_mp_; + mutable std::tuple< Eigen::PartialPivLU>, Eigen::PartialPivLU> > LU_; + + mutable std::tuple< Vec, Vec > step_temp_; // reused scratch for FullStep stage accumulation + mutable std::tuple< Vec, Vec > rand_temp_; // reused scratch: random RHS for norm_J_inverse + mutable std::tuple< Vec, Vec > solve_temp_; // reused scratch: LU solve result // Butcher Table (notation from https://en.wikipedia.org/wiki/List_of_Runge%E2%80%93Kutta_methods ) @@ -1129,6 +1115,69 @@ namespace bertini{ + // Explicit instantiation declarations — suppress re-instantiation in every + // including TU. The definitions live in core/src/tracking/explicit_predictors.cpp. + // Concrete types: dbl = std::complex, mpfr_complex (multiprecision). + // NumErrorT = double (from bertini2/common/config.hpp). + + extern template SuccessCode ExplicitRKPredictor::Predict( + Vec&, System const&, Vec const&, dbl, dbl const&, + double&, unsigned&, unsigned, double const&); + extern template SuccessCode ExplicitRKPredictor::Predict( + Vec&, System const&, Vec const&, mpfr_complex, mpfr_complex const&, + double&, unsigned&, unsigned, double const&); + + extern template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, + System const&, Vec const&, dbl, dbl const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + extern template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, + System const&, Vec const&, mpfr_complex, mpfr_complex const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + + extern template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, double&, + System const&, Vec const&, dbl, dbl const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + extern template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, double&, + System const&, Vec const&, mpfr_complex, mpfr_complex const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + + extern template SuccessCode ExplicitRKPredictor::FullStep( + Vec&, System const&, Vec const&, dbl const&, dbl const&); + extern template SuccessCode ExplicitRKPredictor::FullStep( + Vec&, System const&, Vec const&, mpfr_complex const&, mpfr_complex const&); + + extern template void ExplicitRKPredictor::SetNormsCond( + double&, double&, double&, unsigned, unsigned); + extern template void ExplicitRKPredictor::SetNormsCond( + double&, double&, double&, unsigned, unsigned); + + extern template SuccessCode ExplicitRKPredictor::SetErrorEstimate(double&, dbl const&); + extern template SuccessCode ExplicitRKPredictor::SetErrorEstimate(double&, mpfr_complex const&); + + extern template SuccessCode ExplicitRKPredictor::SetSizeProportion(double&, dbl const&); + extern template SuccessCode ExplicitRKPredictor::SetSizeProportion(double&, mpfr_complex const&); + + extern template SuccessCode ExplicitRKPredictor::EvalRHS( + System const&, Vec const&, dbl const&, Mat&, unsigned); + extern template SuccessCode ExplicitRKPredictor::EvalRHS( + System const&, Vec const&, mpfr_complex const&, Mat&, unsigned); + + extern template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, + Mat const&, Mat const&); + extern template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, + Mat const&, Mat const&); + + extern template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, Mat const&); + extern template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, Mat const&); + } // re: predict }// re: tracking }// re: bertini diff --git a/core/include/bertini2/trackers/fixed_precision_tracker.hpp b/core/include/bertini2/trackers/fixed_precision_tracker.hpp index 0459cfa77..7554aef96 100644 --- a/core/include/bertini2/trackers/fixed_precision_tracker.hpp +++ b/core/include/bertini2/trackers/fixed_precision_tracker.hpp @@ -60,11 +60,11 @@ namespace bertini{ { public: - using BaseComplexType = typename TrackerTraits::BaseComplexType; - using BaseRealType = typename TrackerTraits::BaseRealType; + using BaseComplexT = typename TrackerTraits::BaseComplexT; + using BaseRealT = typename TrackerTraits::BaseRealT; - using CT = BaseComplexType; - using RT = BaseRealType; + using ComplexT = BaseComplexT; + using RealT = BaseRealT; virtual ~FixedPrecisionTracker() = default; @@ -85,9 +85,9 @@ namespace bertini{ { } - Vec CurrentPoint() const override + Vec CurrentPoint() const override { - return std::get>(this->current_space_); + return std::get>(this->current_space_); } @@ -138,16 +138,16 @@ namespace bertini{ \param[out] solution_at_endtime The solution at the end time */ - void CopyFinalSolution(Vec & solution_at_endtime) const override + void CopyFinalSolution(Vec & solution_at_endtime) const override { // the current precision is the precision of the output solution point. - unsigned num_vars = this->GetSystem().NumVariables(); + unsigned num_vars = static_cast(this->GetSystem().NumVariables()); solution_at_endtime.resize(num_vars); for (unsigned ii=0; ii >(this->current_space_)(ii); + solution_at_endtime(ii) = std::get >(this->current_space_)(ii); } } @@ -164,16 +164,16 @@ namespace bertini{ */ SuccessCode TrackerIteration() const override { - static_assert(std::is_same< typename Eigen::NumTraits::Real, - typename Eigen::NumTraits::Real>::value, + static_assert(std::is_same< typename Eigen::NumTraits::Real, + typename Eigen::NumTraits::Real>::value, "underlying complex type and the type for comparisons must match"); this->NotifyObservers(NewStep(*this)); - Vec& predicted_space = std::get >(this->temporary_space_); // this will be populated in the Predict step - Vec& current_space = std::get >(this->current_space_); // the thing we ultimately wish to update - CT current_time = CT(this->current_time_); - CT delta_t = CT(this->delta_t_); + Vec& predicted_space = std::get >(this->temporary_space_); // this will be populated in the Predict step + Vec& current_space = std::get >(this->current_space_); // the thing we ultimately wish to update + ComplexT current_time = ComplexT(this->current_time_); + ComplexT delta_t = ComplexT(this->delta_t_); SuccessCode predictor_code = Predict(predicted_space, current_space, current_time, delta_t); @@ -181,18 +181,18 @@ namespace bertini{ { this->NotifyObservers(FirstStepPredictorMatrixSolveFailure(*this)); - this->next_stepsize_ = RT(Get().step_size_fail_factor)*this->current_stepsize_; + this->next_stepsize_ = RealT(Get().step_size_fail_factor)*this->current_stepsize_; UpdateStepsize(); return predictor_code; } - this->NotifyObservers(SuccessfulPredict(*this, predicted_space)); + this->NotifyObservers(SuccessfulPredict(*this, predicted_space)); - Vec& tentative_next_space = std::get >(this->tentative_space_); // this will be populated in the Correct step + Vec& tentative_next_space = std::get >(this->tentative_space_); // this will be populated in the Correct step - CT tentative_next_time = current_time + delta_t; + ComplexT tentative_next_time = current_time + delta_t; SuccessCode corrector_code = Correct(tentative_next_space, predicted_space, @@ -207,14 +207,14 @@ namespace bertini{ { this->NotifyObservers(CorrectorMatrixSolveFailure(*this)); - this->next_stepsize_ = RT(Get().step_size_fail_factor)*this->current_stepsize_; + this->next_stepsize_ = RealT(Get().step_size_fail_factor)*this->current_stepsize_; UpdateStepsize(); return corrector_code; } - this->NotifyObservers(SuccessfulCorrect(*this, tentative_next_space)); + this->NotifyObservers(SuccessfulCorrect(*this, tentative_next_space)); // copy the tentative vector into the current space vector; current_space = tentative_next_space; @@ -227,7 +227,7 @@ namespace bertini{ */ SuccessCode CheckGoingToInfinity() const override { - return Base::template CheckGoingToInfinity(); + return Base::template CheckGoingToInfinity(); } @@ -296,12 +296,12 @@ namespace bertini{ \param current_time The current time value. \param delta_t The time differential for this step. Allowed to be complex. */ - SuccessCode Predict(Vec & predicted_space, - Vec const& current_space, - CT const& current_time, CT const& delta_t) const + SuccessCode Predict(Vec & predicted_space, + Vec const& current_space, + ComplexT const& current_time, ComplexT const& delta_t) const { - return this->predictor_->Predict( + return this->predictor_.Predict( predicted_space, this->tracked_system_, current_space, current_time, @@ -325,11 +325,11 @@ namespace bertini{ \return A SuccessCode indicating whether the loop was successful in converging in the max number of allowable newton steps, to the current path tolerance. */ - SuccessCode Correct(Vec & corrected_space, - Vec const& current_space, - CT const& current_time) const + SuccessCode Correct(Vec & corrected_space, + Vec const& current_space, + ComplexT const& current_time) const { - return this->corrector_->Correct(corrected_space, + return this->corrector_.Correct(corrected_space, this->tracked_system_, current_space, current_time, @@ -352,10 +352,10 @@ namespace bertini{ \return Code indicating whether was successful or not. Regardless, the value of new_space is overwritten with the correction result. */ - SuccessCode RefineImpl(Vec & new_space, - Vec const& start_point, CT const& current_time) const + SuccessCode RefineImpl(Vec & new_space, + Vec const& start_point, ComplexT const& current_time) const { - return this->corrector_->Correct(new_space, + return this->corrector_.Correct(new_space, this->tracked_system_, start_point, current_time, @@ -383,11 +383,11 @@ namespace bertini{ \return Code indicating whether was successful or not. Regardless, the value of new_space is overwritten with the correction result. */ - SuccessCode RefineImpl(Vec & new_space, - Vec const& start_point, CT const& current_time, + SuccessCode RefineImpl(Vec & new_space, + Vec const& start_point, ComplexT const& current_time, NumErrorT const& tolerance, unsigned max_iterations) const { - return this->corrector_->Correct(new_space, + return this->corrector_.Correct(new_space, this->tracked_system_, start_point, current_time, @@ -415,8 +415,8 @@ namespace bertini{ class DoublePrecisionTracker : public FixedPrecisionTracker { public: - using BaseComplexType = dbl; - using BaseRealType = double; + using BaseComplexT = dbl; + using BaseRealT = double; using EmitterType = typename TrackerTraits::EventEmitterType; @@ -425,7 +425,12 @@ namespace bertini{ \brief Construct a tracker, associating to it a System. */ DoublePrecisionTracker(class System const& sys) : FixedPrecisionTracker(sys) - { } + { + // Report the precision honestly: double tracking is always DoublePrecision() digits. + FixedPrecisionConfig c = this->template Get(); + c.precision = DoublePrecision(); + this->template Set(c); + } DoublePrecisionTracker() = delete; @@ -438,6 +443,20 @@ namespace bertini{ return DoublePrecision(); } + /** + \brief Double-precision tracking is fixed at DoublePrecision() digits. Accept that value (or + the 0 sentinel), but reject any attempt to set a different precision -- use mptype 'multiple' + or 'adaptive' for more digits. + */ + void PrecisionSetup(FixedPrecisionConfig const& c) + { + if (c.precision != 0 && c.precision != DoublePrecision()) + throw std::runtime_error("double-precision tracking is fixed at " + + std::to_string(DoublePrecision()) + + " digits; cannot set precision to " + std::to_string(c.precision) + + " (use mptype 'multiple' or 'adaptive')"); + } + /** \brief Set up the internals of the tracker for a fresh start. @@ -448,18 +467,18 @@ namespace bertini{ \param end_time The time to which to track. \param start_point The space values from which to start tracking. */ - SuccessCode TrackerLoopInitialization(BaseComplexType const& start_time, - BaseComplexType const& end_time, - Vec const& start_point) const override + SuccessCode TrackerLoopInitialization(BaseComplexT const& start_time, + BaseComplexT const& end_time, + Vec const& start_point) const override { - this->NotifyObservers(Initializing(*this,start_time, end_time, start_point)); + this->NotifyObservers(Initializing(*this,start_time, end_time, start_point)); // set up the master current time and the current step size this->current_time_ = start_time; this->endtime_ = end_time; - std::get >(this->current_space_) = start_point; + std::get >(this->current_space_) = start_point; if (this->reinitialize_stepsize_) - this->SetStepSize(min(BaseRealType(Get().initial_step_size),abs(start_time-end_time)/Get().min_num_steps)); + this->SetStepSize(min(BaseRealT(Get().initial_step_size),abs(start_time-end_time)/Get().min_num_steps)); ResetCounters(); @@ -475,8 +494,8 @@ namespace bertini{ class MultiplePrecisionTracker : public FixedPrecisionTracker { public: - using BaseComplexType = mpfr_complex; - using BaseRealType = mpfr_float; + using BaseComplexT = mpfr_complex; + using BaseRealT = mpfr_float; using EmitterType = FixedPrecisionTracker; @@ -487,9 +506,11 @@ namespace bertini{ The precision of the tracker will be whatever the current default is. The tracker cannot change its precision, and will require the default precision to be this precision whenever tracking is started. That is, the precision is fixed. */ MultiplePrecisionTracker(class System const& sys) : FixedPrecisionTracker(sys), precision_(DefaultPrecision()) - { } + { + SyncPrecisionToConfig(); + } + - MultiplePrecisionTracker() = delete; virtual ~MultiplePrecisionTracker() = default; @@ -500,6 +521,30 @@ namespace bertini{ return precision_; } + /** + \brief Set the fixed precision this tracker works at. + + The next TrackPath re-precisions all of the tracker's state to this value + (TrackerLoopInitialization does it). The caller must also ensure the system, the working + (thread) precision, and the start points are at this precision -- the zero-dim algorithm + does this from FixedPrecisionConfig.precision. + */ + void SetPrecision(unsigned p) + { + precision_ = p; + SyncPrecisionToConfig(); + } + + /** + \brief Adopt the precision named in a FixedPrecisionConfig (its sentinel 0 means "leave as + is"). Replaces the base no-op so the fixed-multiple precision is configurable. + */ + void PrecisionSetup(FixedPrecisionConfig const& c) + { + if (c.precision != 0) + SetPrecision(c.precision); + } + /** \brief Set up the internals of the tracker for a fresh start. @@ -510,15 +555,17 @@ namespace bertini{ \param end_time The time to which to track. \param start_point The space values from which to start tracking. */ - SuccessCode TrackerLoopInitialization(BaseComplexType const& start_time, - BaseComplexType const& end_time, - Vec const& start_point) const override + SuccessCode TrackerLoopInitialization(BaseComplexT const& start_time, + BaseComplexT const& end_time, + Vec const& start_point) const override { - if (start_point(0).precision()!=DefaultPrecision()) + // ThreadPrecision: thread-local read, correct both on the main thread + // (where it equals the global) and on std::thread workers. + if (start_point(0).precision()!=ThreadPrecision()) { std::stringstream err_msg; - err_msg << "start point for fixed multiple precision tracker has differing precision from default (" << start_point(0).precision() << "!=" << DefaultPrecision() << "), tracking cannot start"; + err_msg << "start point for fixed multiple precision tracker has differing precision from default (" << start_point(0).precision() << "!=" << ThreadPrecision() << "), tracking cannot start"; throw std::runtime_error(err_msg.str()); } @@ -529,20 +576,34 @@ namespace bertini{ throw std::runtime_error(err_msg.str()); } - if (DefaultPrecision()!=this->CurrentPrecision()) + if (ThreadPrecision()!=this->CurrentPrecision()) { std::stringstream err_msg; - err_msg << "current default precision differs from tracker's precision (" << DefaultPrecision() << "!=" << this->CurrentPrecision() << "), tracking cannot start"; + err_msg << "current default precision differs from tracker's precision (" << ThreadPrecision() << "!=" << this->CurrentPrecision() << "), tracking cannot start"; throw std::runtime_error(err_msg.str()); } - this->NotifyObservers(Initializing(*this,start_time, end_time, start_point)); + this->NotifyObservers(Initializing(*this,start_time, end_time, start_point)); + + // Reset precision of all persistent members before assignment so that + // BMP preserve_related_precision doesn't propagate stale high precision + // from a previous TrackPath call into the new one. + this->current_time_.precision(precision_); + this->endtime_.precision(precision_); + this->delta_t_.precision(precision_); + this->current_stepsize_.precision(precision_); + this->next_stepsize_.precision(precision_); + Precision(std::get>(this->current_space_), precision_); + Precision(std::get>(this->temporary_space_), precision_); + Precision(std::get>(this->tentative_space_), precision_); + this->predictor_.ChangePrecision(precision_); + this->corrector_.ChangePrecision(precision_); // set up the master current time and the current step size this->current_time_ = start_time; this->endtime_ = end_time; - std::get >(this->current_space_) = start_point; + std::get >(this->current_space_) = start_point; if (this->reinitialize_stepsize_) this->SetStepSize(min(mpfr_float(Get().initial_step_size),mpfr_float(abs(start_time-end_time)/Get().min_num_steps))); @@ -554,7 +615,7 @@ namespace bertini{ bool PrecisionSanityCheck() const { return GetSystem().precision() == precision_ && - DefaultPrecision()==precision_ && + ThreadPrecision()==precision_ && std::get >(current_space_)(0).precision() == precision_ && std::get >(tentative_space_)(0).precision() == precision_ && std::get >(temporary_space_)(0).precision() == precision_ && @@ -563,6 +624,15 @@ namespace bertini{ } private: + // Keep the stored FixedPrecisionConfig.precision equal to the tracker's actual precision, + // so reading the config tells you the precision in effect (not the 0 sentinel). + void SyncPrecisionToConfig() + { + FixedPrecisionConfig c = this->template Get(); + c.precision = precision_; + this->template Set(c); + } + unsigned precision_; }; // re: MultiplePrecisionTracker } // namespace tracking diff --git a/core/include/bertini2/trackers/fixed_precision_utilities.hpp b/core/include/bertini2/trackers/fixed_precision_utilities.hpp index 4ba56ce8c..d3defc985 100644 --- a/core/include/bertini2/trackers/fixed_precision_utilities.hpp +++ b/core/include/bertini2/trackers/fixed_precision_utilities.hpp @@ -69,13 +69,13 @@ unsigned MaxPrecision(TimeCont const& times) //does not a thing, because cannot. inline -unsigned EnsureAtUniformPrecision(TimeCont const & times, SampCont const & derivatives) +unsigned EnsureAtUniformPrecision(TimeCont const & /*times*/, SampCont const & /*derivatives*/) { return DoublePrecision(); } inline -unsigned EnsureAtUniformPrecision(TimeCont const & times, SampCont const & samples, SampCont const & derivatives) +unsigned EnsureAtUniformPrecision(TimeCont const & /*times*/, SampCont const & /*samples*/, SampCont const & /*derivatives*/) { return DoublePrecision(); } @@ -83,7 +83,7 @@ unsigned EnsureAtUniformPrecision(TimeCont const & times, SampCont con //changes precision of mpfr_complex to highest needed precision for the samples. inline -unsigned EnsureAtUniformPrecision(TimeCont const & times, SampCont const & samples) +unsigned EnsureAtUniformPrecision(TimeCont const & /*times*/, SampCont const & samples) { return MaxPrecision(samples); } @@ -91,7 +91,7 @@ unsigned EnsureAtUniformPrecision(TimeCont const & times, SampCont //returns max precision of all samples. inline -unsigned EnsureAtUniformPrecision(TimeCont const & times, SampCont const & samples, SampCont const & derivatives) +unsigned EnsureAtUniformPrecision(TimeCont const & /*times*/, SampCont const & samples, SampCont const & derivatives) { return max(MaxPrecision(samples), MaxPrecision(derivatives)); diff --git a/core/include/bertini2/trackers/newton_correct.hpp b/core/include/bertini2/trackers/newton_correct.hpp index 1e0276ed5..5ed8b4f89 100644 --- a/core/include/bertini2/trackers/newton_correct.hpp +++ b/core/include/bertini2/trackers/newton_correct.hpp @@ -51,8 +51,8 @@ namespace bertini{ \return The SuccessCode indicating what happened. - \tparam ComplexType The complex type for arithmetic - \tparam RealType The underlying real number type, used for comparitors. + \tparam ComplexT The complex type for arithmetic + \tparam RealT The underlying real number type, used for comparitors. \param[out] next_space The computed next space point. \param S The system we are tracking on. @@ -64,12 +64,12 @@ namespace bertini{ \param max_num_newton_iterations The maximum number of iterations to run Newton's method for. */ - template - SuccessCode NewtonLoop(Vec & next_space, + template + SuccessCode NewtonLoop(Vec & next_space, System const& S, - Vec const& current_space, // pass by value to get a copy of it - ComplexType const& current_time, - RealType const& tracking_tolerance, + Vec const& current_space, // pass by value to get a copy of it + ComplexT const& current_time, + RealT const& tracking_tolerance, unsigned min_num_newton_iterations, unsigned max_num_newton_iterations) { @@ -106,8 +106,8 @@ namespace bertini{ Run Newton's method until it converges (\f$\Delta z\f$ < tol), an AMP criterion (B or C) is violated, or the next point's norm exceeds the path truncation threshold. - \tparam ComplexType The complex type for arithmetic - \tparam RealType The underlying real number type, used for comparitors. + \tparam ComplexT The complex type for arithmetic + \tparam RealT The underlying real number type, used for comparitors. \param[out] next_space The computed next space point. \param S The system we are tracking on. @@ -119,12 +119,12 @@ namespace bertini{ \param max_num_newton_iterations The maximum number of iterations to run Newton's method for. \param AMP_config Adaptive multiple precision settings. Using this argument is how Bertini2 knows you want to use adaptive precision. */ - template - SuccessCode NewtonLoop(Vec & next_space, + template + SuccessCode NewtonLoop(Vec & next_space, System const& S, - Vec const& current_space, // pass by value to get a copy of it - ComplexType const& current_time, - RealType const& tracking_tolerance, + Vec const& current_space, // pass by value to get a copy of it + ComplexT const& current_time, + RealT const& tracking_tolerance, unsigned min_num_newton_iterations, unsigned max_num_newton_iterations, AdaptiveMultiplePrecisionConfig const& AMP_config) @@ -152,7 +152,7 @@ namespace bertini{ if ( (delta_z.norm() < tracking_tolerance) && (ii >= (min_num_newton_iterations-1)) ) return SuccessCode::Success; - auto norm_J_inverse = LU.solve(RandomOfUnits(S.NumVariables())).norm(); + auto norm_J_inverse = LU.solve(RandomOfUnits(S.NumVariables())).norm(); if (!amp::CriterionB(J.norm(), norm_J_inverse, max_num_newton_iterations - ii, tracking_tolerance, delta_z.norm(), AMP_config)) return SuccessCode::HigherPrecisionNecessary; @@ -174,8 +174,8 @@ namespace bertini{ Run Newton's method until it converges (\f$\Delta z\f$ < tol), an AMP criterion (B or C) is violated, or the next point's norm exceeds the path truncation threshold. - \tparam ComplexType The complex type for arithmetic - \tparam RealType The underlying real number type, used for comparitors. + \tparam ComplexT The complex type for arithmetic + \tparam RealT The underlying real number type, used for comparitors. \param[out] next_space The computed next space point. \param[out] norm_delta_z The norm of the last step size. @@ -191,16 +191,16 @@ namespace bertini{ \param max_num_newton_iterations The maximum number of iterations to run Newton's method for. \param AMP_config Adaptive multiple precision settings. Using this argument is how Bertini2 knows you want to use adaptive precision. */ - template - SuccessCode NewtonLoop(Vec & next_space, - RealType & norm_delta_z, - RealType & norm_J, - RealType & norm_J_inverse, - RealType & condition_number_estimate, + template + SuccessCode NewtonLoop(Vec & next_space, + RealT & norm_delta_z, + RealT & norm_J, + RealT & norm_J_inverse, + RealT & condition_number_estimate, System const& S, - Vec const& current_space, // pass by value to get a copy of it - ComplexType const& current_time, - RealType const& tracking_tolerance, + Vec const& current_space, // pass by value to get a copy of it + ComplexT const& current_time, + RealT const& tracking_tolerance, unsigned min_num_newton_iterations, unsigned max_num_newton_iterations, AdaptiveMultiplePrecisionConfig const& AMP_config) @@ -229,7 +229,7 @@ namespace bertini{ norm_delta_z = delta_z.norm(); norm_J = J.norm(); - norm_J_inverse = LU.solve(RandomOfUnits(S.NumVariables())).norm(); + norm_J_inverse = LU.solve(RandomOfUnits(S.NumVariables())).norm(); condition_number_estimate = norm_J*norm_J_inverse; diff --git a/core/include/bertini2/trackers/newton_corrector.hpp b/core/include/bertini2/trackers/newton_corrector.hpp index e697cf77a..e9d4eb331 100644 --- a/core/include/bertini2/trackers/newton_corrector.hpp +++ b/core/include/bertini2/trackers/newton_corrector.hpp @@ -84,10 +84,10 @@ namespace bertini{ Precision(std::get< Vec >(f_temp_), new_precision); Precision(std::get< Vec >(step_temp_), new_precision); Precision(std::get< Mat >(J_temp_), new_precision); + Precision(std::get< Vec >(rand_temp_), new_precision); + Precision(std::get< Vec >(solve_temp_), new_precision); - std::get< Eigen::PartialPivLU> >(LU_) = Eigen::PartialPivLU>(numTotalFunctions_); - - current_precision_ = new_precision; + current_precision_ = new_precision; } @@ -104,14 +104,34 @@ namespace bertini{ */ void ChangeSystem(const System& S) { - numTotalFunctions_ = S.NumTotalFunctions(); - numVariables_ = S.NumVariables(); + numTotalFunctions_ = static_cast(S.NumTotalFunctions()); + numVariables_ = static_cast(S.NumVariables()); std::get< Mat >(J_temp_).resize(numTotalFunctions_, numVariables_); std::get< Mat >(J_temp_).resize(numTotalFunctions_, numVariables_); std::get< Vec >(f_temp_).resize(numTotalFunctions_); std::get< Vec >(f_temp_).resize(numTotalFunctions_); std::get< Vec >(step_temp_).resize(numTotalFunctions_); std::get< Vec >(step_temp_).resize(numTotalFunctions_); + std::get< Vec >(solve_temp_).resize(numVariables_); + std::get< Vec >(solve_temp_).resize(numVariables_); + RefreshRandomDirection(); + } + + /** + \brief (Re)draw the random probe direction used to estimate ||J^{-1}|| (the condition + number) from this thread's RNG engine. + + The tracker calls this once at the start of a path track (see Tracker::TrackPath's + caller / the per-path reseed point), so the direction is held fixed for the ENTIRE + track of that point -- every Newton step and the endgame's sample-circle sub-tracks + -- and, given a per-path RNG reseed, is reproducible regardless of how paths were + distributed across workers. It is NOT redrawn per Newton step or per TrackPath, which + would both perturb condition estimates and (in the endgame) churn the RNG mid-track. + */ + void RefreshRandomDirection() + { + std::get< Vec >(rand_temp_) = RandomOfUnits(numVariables_); + std::get< Vec >(rand_temp_) = RandomOfUnits(numVariables_); } @@ -124,7 +144,7 @@ namespace bertini{ \return The SuccessCode indicating what happened. - \tparam ComplexType The complex type for arithmetic + \tparam ComplexT The complex type for arithmetic \param[out] next_space The computed next space point. \param S The system we are tracking on. @@ -137,11 +157,11 @@ namespace bertini{ */ - template - SuccessCode Correct(Vec & next_space, + template + SuccessCode Correct(Vec & next_space, System const& S, - Vec const& current_space, // pass by value to get a copy of it - ComplexType const& current_time, + Vec const& current_space, // pass by value to get a copy of it + ComplexT const& current_time, NumErrorT const& tracking_tolerance, unsigned min_num_newton_iterations, unsigned max_num_newton_iterations) @@ -150,7 +170,7 @@ namespace bertini{ assert(max_num_newton_iterations >= min_num_newton_iterations && "max number newton iterations must be at least the min."); #endif - Vec& step_ref = std::get< Vec >(step_temp_); + Vec& step_ref = std::get< Vec >(step_temp_); next_space = current_space; for (unsigned ii = 0; ii < max_num_newton_iterations; ++ii) @@ -182,7 +202,7 @@ namespace bertini{ Run Newton's method until it converges (\f$\Delta z\f$ < tol), an AMP criterion (B or C) is violated, or the next point's norm exceeds the path truncation threshold. - \tparam ComplexType The complex type for arithmetic + \tparam ComplexT The complex type for arithmetic \param[out] next_space The computed next space point. \param S The system we are tracking on. @@ -194,11 +214,11 @@ namespace bertini{ \param max_num_newton_iterations The maximum number of iterations to run Newton's method for. \param AMP_config Adaptive multiple precision settings. Using this argument is how Bertini2 knows you want to use adaptive precision. */ - template - SuccessCode Correct(Vec & next_space, + template + SuccessCode Correct(Vec & next_space, System const& S, - Vec const& current_space, // pass by value to get a copy of it - ComplexType const& current_time, + Vec const& current_space, // pass by value to get a copy of it + ComplexT const& current_time, NumErrorT const& tracking_tolerance, unsigned min_num_newton_iterations, unsigned max_num_newton_iterations, @@ -208,7 +228,7 @@ namespace bertini{ assert(max_num_newton_iterations >= min_num_newton_iterations && "max number newton iterations must be at least the min."); #endif - Vec& step_ref = std::get< Vec >(step_temp_); + Vec& step_ref = std::get< Vec >(step_temp_); next_space = current_space; for (unsigned ii = 0; ii < max_num_newton_iterations; ++ii) @@ -220,36 +240,39 @@ namespace bertini{ next_space += step_ref; - Mat& J_temp_ref = std::get< Mat >(J_temp_); - Eigen::PartialPivLU< Mat >& LU_ref = std::get< Eigen::PartialPivLU< Mat > >(LU_); + Mat& J_temp_ref = std::get< Mat >(J_temp_); + Eigen::PartialPivLU< Mat >& LU_ref = std::get< Eigen::PartialPivLU< Mat > >(LU_); if ( (step_ref.template lpNorm() < tracking_tolerance) && (ii >= (min_num_newton_iterations-1)) ) return SuccessCode::Success; - NumErrorT norm_J_inverse(LU_ref.solve(RandomOfUnits(S.NumVariables())).norm()); + Vec const& rand_ref = std::get< Vec >(rand_temp_); + Vec& solve_ref = std::get< Vec >(solve_temp_); + solve_ref = LU_ref.solve(rand_ref); + NumErrorT norm_J_inverse(solve_ref.norm()); - if (!amp::CriterionB(NumErrorT(J_temp_ref.norm()), norm_J_inverse, max_num_newton_iterations - ii, tracking_tolerance, NumErrorT(step_ref.template lpNorm()), AMP_config)) + if (!amp::CriterionB(NumErrorT(J_temp_ref.norm()), norm_J_inverse, max_num_newton_iterations - ii, tracking_tolerance, NumErrorT(step_ref.template lpNorm()), AMP_config)) return SuccessCode::HigherPrecisionNecessary; - - if (!amp::CriterionC(norm_J_inverse, next_space, tracking_tolerance, AMP_config)) + + if (!amp::CriterionC(norm_J_inverse, next_space, tracking_tolerance, AMP_config)) return SuccessCode::HigherPrecisionNecessary; } - + return SuccessCode::FailedToConverge; } - - - - - + + + + + /** \brief Run Newton's method in multiple precision. Run Newton's method until it converges (\f$\Delta z\f$ < tol), an AMP criterion (B or C) is violated, or the next point's norm exceeds the path truncation threshold. - \tparam ComplexType The complex type for arithmetic - \tparam RealType The underlying real number type, used for comparitors. + \tparam ComplexT The complex type for arithmetic + \tparam RealT The underlying real number type, used for comparitors. \param[out] next_space The computed next space point. \param[out] norm_delta_z The norm of the last step size. @@ -265,15 +288,15 @@ namespace bertini{ \param max_num_newton_iterations The maximum number of iterations to run Newton's method for. \param AMP_config Adaptive multiple precision settings. Using this argument is how Bertini2 knows you want to use adaptive precision. */ - template - SuccessCode Correct(Vec & next_space, + template + SuccessCode Correct(Vec & next_space, NumErrorT & norm_delta_z, NumErrorT & norm_J, NumErrorT & norm_J_inverse, NumErrorT & condition_number_estimate, System const& S, - Vec const& current_space, // pass by value to get a copy of it - ComplexType const& current_time, + Vec const& current_space, // pass by value to get a copy of it + ComplexT const& current_time, NumErrorT const& tracking_tolerance, unsigned min_num_newton_iterations, unsigned max_num_newton_iterations, @@ -283,7 +306,7 @@ namespace bertini{ assert(max_num_newton_iterations >= min_num_newton_iterations && "max number newton iterations must be at least the min."); #endif - Vec& step_ref = std::get< Vec >(step_temp_); + Vec& step_ref = std::get< Vec >(step_temp_); next_space = current_space; for (unsigned ii = 0; ii < max_num_newton_iterations; ++ii) @@ -295,29 +318,41 @@ namespace bertini{ next_space += step_ref; - Mat& J_temp_ref = std::get< Mat >(J_temp_); - Eigen::PartialPivLU< Mat >& LU_ref = std::get< Eigen::PartialPivLU< Mat > >(LU_); + Mat& J_temp_ref = std::get< Mat >(J_temp_); + Eigen::PartialPivLU< Mat >& LU_ref = std::get< Eigen::PartialPivLU< Mat > >(LU_); norm_delta_z = NumErrorT(step_ref.template lpNorm()); norm_J = NumErrorT(J_temp_ref.norm()); - norm_J_inverse = NumErrorT(LU_ref.solve(RandomOfUnits(S.NumVariables())).norm()); + { + // Reuse the FIXED probe vector generated once at setup (do NOT regenerate + // per call): a fresh random probe direction every Newton step occasionally + // produced an inflated ||J^{-1}|| estimate -> spurious HigherPrecisionNecessary + // -> precision escalation/grind (confirmed by A/B: regenerating reintroduces + // the spikes, fixed does not). A single fixed direction also makes the + // condition estimates comparable across steps, is cheaper, and keeps tracking + // deterministic / parallel-bit-identical. + Vec& rand_ref = std::get< Vec >(rand_temp_); + Vec& solve_ref = std::get< Vec >(solve_temp_); + solve_ref = LU_ref.solve(rand_ref); + norm_J_inverse = NumErrorT(solve_ref.norm()); + } condition_number_estimate = NumErrorT(norm_J*norm_J_inverse); if ( (norm_delta_z < tracking_tolerance) && (ii >= (min_num_newton_iterations-1)) ) return SuccessCode::Success; - if (!amp::CriterionB(norm_J, norm_J_inverse, max_num_newton_iterations - ii, tracking_tolerance, norm_delta_z, AMP_config)) + if (!amp::CriterionB(norm_J, norm_J_inverse, max_num_newton_iterations - ii, tracking_tolerance, norm_delta_z, AMP_config)) return SuccessCode::HigherPrecisionNecessary; - - if (!amp::CriterionC(norm_J_inverse, next_space, tracking_tolerance, AMP_config)) + + if (!amp::CriterionC(norm_J_inverse, next_space, tracking_tolerance, AMP_config)) return SuccessCode::HigherPrecisionNecessary; } - + return SuccessCode::FailedToConverge; } - + private: /////////////////////////// @@ -337,20 +372,20 @@ namespace bertini{ */ - template - SuccessCode EvalIterationStep(Vec & newton_step, + template + SuccessCode EvalIterationStep(Vec & newton_step, const System& S, - const Eigen::MatrixBase& current_space, const ComplexType& current_time) + const Eigen::MatrixBase& current_space, const ComplexT& current_time) { - Vec& f_temp_ref = std::get< Vec >(f_temp_); - Mat& J_temp_ref = std::get< Mat >(J_temp_); + Vec& f_temp_ref = std::get< Vec >(f_temp_); + Mat& J_temp_ref = std::get< Mat >(J_temp_); - Eigen::PartialPivLU< Mat >& LU_ref = std::get< Eigen::PartialPivLU< Mat > >(LU_); + Eigen::PartialPivLU< Mat >& LU_ref = std::get< Eigen::PartialPivLU< Mat > >(LU_); - S.SetAndReset(current_space, current_time); + S.SetAndReset(current_space, current_time); S.EvalInPlace(f_temp_ref); S.JacobianInPlace(J_temp_ref); - LU_ref = J_temp_ref.lu(); + LU_ref.compute(J_temp_ref); if (LUPartialPivotDecompositionSuccessful(LU_ref.matrixLU())!=MatrixSuccessCode::Success) return SuccessCode::MatrixSolveFailure; @@ -372,11 +407,13 @@ namespace bertini{ unsigned numTotalFunctions_; // Number of total functions for the current system unsigned numVariables_; // Number of variables for the current system - std::tuple< Vec, Vec > f_temp_; // Variable to hold temporary evaluation of the system - std::tuple< Vec, Vec > step_temp_; // Variable to hold temporary evaluation of the newton step - std::tuple< Mat, Mat > J_temp_; // Variable to hold temporary evaluation of the Jacobian - - std::tuple< Eigen::PartialPivLU>, Eigen::PartialPivLU> > LU_; // The LU factorization from the Newton iterates + std::tuple< Vec, Vec > f_temp_; + std::tuple< Vec, Vec > step_temp_; + std::tuple< Mat, Mat > J_temp_; + std::tuple< Vec, Vec > rand_temp_; // reused scratch: random RHS for norm_J_inverse + std::tuple< Vec, Vec > solve_temp_; // reused scratch: LU solve result + + std::tuple< Eigen::PartialPivLU>, Eigen::PartialPivLU> > LU_; unsigned current_precision_; diff --git a/core/include/bertini2/trackers/observers.hpp b/core/include/bertini2/trackers/observers.hpp index 01466c5ed..43c749450 100644 --- a/core/include/bertini2/trackers/observers.hpp +++ b/core/include/bertini2/trackers/observers.hpp @@ -45,38 +45,37 @@ namespace bertini { template - class FirstPrecisionRecorder : public Observer + class FirstPrecisionRecorder + : public TypedObserver< FirstPrecisionRecorder, TrackerT, + TrackingStarted::EventEmitterType>, + PrecisionChanged::EventEmitterType> > { BOOST_TYPE_INDEX_REGISTER_CLASS using EmitterT = typename TrackerTraits::EventEmitterType; - virtual void Observe(AnyEvent const& e) override + public: + + ObserveResult OnEvent(TrackingStarted const& e) { - if(auto p = dynamic_cast*>(&e)) - { - precision_increased_ = false; - starting_precision_ = p->Get().CurrentPrecision(); - } - else if (auto p = dynamic_cast*>(&e)) - { - auto& t = p->Get(); - auto next = p->Next(); - if (next > p->Previous()) - { - precision_increased_ = true; - next_precision_ = next; - time_of_first_increase_ = t.CurrentTime(); - t.RemoveObserver(*this); - } + precision_increased_ = false; + starting_precision_ = e.Get().CurrentPrecision(); + return ObserveResult::KeepObserving; + } + ObserveResult OnEvent(PrecisionChanged const& e) + { + auto next = e.Next(); + if (next > e.Previous()) + { + precision_increased_ = true; + next_precision_ = next; + time_of_first_increase_ = e.Get().CurrentTime(); + // done: ask to be dropped instead of mutating the list mid-dispatch. + return ObserveResult::Unsubscribe; } + return ObserveResult::KeepObserving; } - - - - public: - unsigned StartPrecision() const { return starting_precision_; @@ -92,7 +91,7 @@ namespace bertini { return precision_increased_; } - typename TrackerTraits::BaseComplexType TimeOfIncrease() const + typename TrackerTraits::BaseComplexT TimeOfIncrease() const { return time_of_first_increase_; } @@ -104,37 +103,37 @@ namespace bertini { unsigned starting_precision_; unsigned next_precision_; bool precision_increased_; - typename TrackerTraits::BaseComplexType time_of_first_increase_; + typename TrackerTraits::BaseComplexT time_of_first_increase_; }; template - class MinMaxPrecisionRecorder : public Observer + class MinMaxPrecisionRecorder + : public TypedObserver< MinMaxPrecisionRecorder, TrackerT, + TrackingStarted::EventEmitterType>, + PrecisionChanged::EventEmitterType> > { BOOST_TYPE_INDEX_REGISTER_CLASS using EmitterT = typename TrackerTraits::EventEmitterType; - virtual void Observe(AnyEvent const& e) override + public: + + ObserveResult OnEvent(TrackingStarted const& e) { - if (auto p = dynamic_cast*>(&e)) - { - auto next_precision = p->Next(); - if (next_precision < min_precision_) - min_precision_ = next_precision; - if (next_precision > max_precision_) - max_precision_ = next_precision; - } - else if(auto p = dynamic_cast*>(&e)) - { - min_precision_ = p->Get().CurrentPrecision(); - max_precision_ = p->Get().CurrentPrecision(); - } + min_precision_ = e.Get().CurrentPrecision(); + max_precision_ = e.Get().CurrentPrecision(); + return ObserveResult::KeepObserving; } - - - - public: + ObserveResult OnEvent(PrecisionChanged const& e) + { + auto next_precision = e.Next(); + if (next_precision < min_precision_) + min_precision_ = next_precision; + if (next_precision > max_precision_) + max_precision_ = next_precision; + return ObserveResult::KeepObserving; + } unsigned MinPrecision() const { @@ -167,13 +166,14 @@ namespace bertini { using EmitterT = typename TrackerTraits::EventEmitterType; - virtual void Observe(AnyEvent const& e) override + virtual ObserveResult Observe(AnyEvent const& e) override { const TrackingEvent* p = dynamic_cast*>(&e); if (p) { precisions_.push_back(p->Get().CurrentPrecision()); } + return ObserveResult::KeepObserving; } @@ -194,22 +194,21 @@ namespace bertini { PathAccumulator path_accumulator; */ template class EventT = SuccessfulStep> - class AMPPathAccumulator : public Observer + class AMPPathAccumulator + : public TypedObserver< AMPPathAccumulator, TrackerT, + EventT::EventEmitterType> > { BOOST_TYPE_INDEX_REGISTER_CLASS using EmitterT = typename TrackerTraits::EventEmitterType; - virtual void Observe(AnyEvent const& e) override + public: + + ObserveResult OnEvent(EventT const& e) { - const EventT* p = dynamic_cast*>(&e); - if (p) - { - path_.push_back(p->Get().CurrentPoint()); - } + path_.push_back(e.Get().CurrentPoint()); + return ObserveResult::KeepObserving; } - - public: const std::vector >& Path() const { return path_; @@ -232,7 +231,7 @@ namespace bertini { virtual ~GoryDetailLogger() = default; - virtual void Observe(AnyEvent const& e) override + virtual ObserveResult Observe(AnyEvent const& e) override { @@ -332,6 +331,8 @@ namespace bertini { else BOOST_LOG_TRIVIAL(severity_level::debug) << "unlogged event, of type: " << boost::typeindex::type_id_runtime(e).pretty_name(); + + return ObserveResult::KeepObserving; } }; @@ -339,16 +340,18 @@ namespace bertini { template - class StepFailScreenPrinter : public Observer + class StepFailScreenPrinter + : public TypedObserver< StepFailScreenPrinter, TrackerT, + FailedStep::EventEmitterType> > { BOOST_TYPE_INDEX_REGISTER_CLASS public: using EmitterT = typename TrackerTraits::EventEmitterType; - virtual void Observe(AnyEvent const& e) override + ObserveResult OnEvent(FailedStep const& e) { - if (auto p = dynamic_cast*>(&e)) - std::cout << "observed step failure" << std::endl; + std::cout << "observed step failure" << std::endl; + return ObserveResult::KeepObserving; } virtual ~StepFailScreenPrinter() = default; diff --git a/core/include/bertini2/trackers/step.hpp b/core/include/bertini2/trackers/step.hpp index 20a7fe4b0..9cc5be90d 100644 --- a/core/include/bertini2/trackers/step.hpp +++ b/core/include/bertini2/trackers/step.hpp @@ -45,20 +45,20 @@ namespace bertini { /** - \tparam ComplexType The type of number being used in the algorithm + \tparam ComplexT The type of number being used in the algorithm */ - template + template SuccessCode Step(Predictor predictor_choice, - Vec & next_space, ComplexType & next_time, + Vec & next_space, ComplexT & next_time, System & sys, - Vec const& current_space, ComplexType current_time, - ComplexType const& delta_t, - RealType & condition_number_estimate, + Vec const& current_space, ComplexT current_time, + ComplexT const& delta_t, + RealT & condition_number_estimate, unsigned & num_steps_since_last_condition_number_computation, unsigned frequency_of_CN_estimation, PrecisionType prec_type, - RealType const& tracking_tolerance, - RealType const& path_truncation_threshold, + RealT const& tracking_tolerance, + RealT const& path_truncation_threshold, unsigned min_num_newton_iterations, unsigned max_num_newton_iterations, AdaptiveMultiplePrecisionConfig const& AMP_config) diff --git a/core/src/basics/random.cpp b/core/src/basics/random.cpp index 28916e2cb..c472d5dad 100644 --- a/core/src/basics/random.cpp +++ b/core/src/basics/random.cpp @@ -30,11 +30,105 @@ #include "bertini2/random.hpp" +#include +#include +#include + namespace bertini { +namespace { + +inline uint64_t splitmix64(uint64_t x) +{ + x += 0x9e3779b97f4a7c15ULL; + x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9ULL; + x = (x ^ (x >> 27)) * 0x94d049bb133111ebULL; + return x ^ (x >> 31); +} + +std::atomic g_global_seed{0}; + +// Domain tags for stream derivation. Distinct domains (and distinct indices within a domain) +// produce distinct engine states, so no two streams ever coincide and no path/worker stream can +// reproduce the setup stream. (Setup = the stream that generates gamma / start coefficients / patch.) +constexpr uint64_t kDomainSetup = 0x5e7400000000ULL; // "setup" -- SetGlobalSeed +constexpr uint64_t kDomainPath = 0x9a7400000000ULL; // "path" -- per-path tracking streams +constexpr uint64_t kDomainWorker = 0x107ce000000ULL; // "worker" -- per-rank child seeds (MPI) + +// Seed the engine from the FULL (master, domain, index) tuple via std::seed_seq. Using the whole +// 64-bit words (not a uint32_t truncation) means distinct tuples set distinct mt19937 states, so +// streams cannot collide modulo 2^32 (the old bug). +inline void SeedEngine(std::mt19937& eng, uint64_t master, uint64_t domain, uint64_t index) +{ + std::seed_seq seq{ + static_cast(master), static_cast(master >> 32), + static_cast(domain), static_cast(domain >> 32), + static_cast(index), static_cast(index >> 32) + }; + eng.seed(seq); +} + +} // anon namespace + +// one mt19937 per thread — seeded from entropy on first use (overwritten deterministically by +// SetGlobalSeed / ReseedThisThread before any draw in a seeded run). +thread_local std::mt19937 g_thread_engine{std::random_device{}()}; + +std::mt19937& ThreadEngine() { return g_thread_engine; } + +unsigned long GetGlobalSeed() +{ + unsigned long s = g_global_seed.load(std::memory_order_relaxed); + if (s == 0) { + std::random_device rd; + s = static_cast(rd()); + if (s == 0) s = 1; + unsigned long expected = 0; + if (!g_global_seed.compare_exchange_strong(expected, s, std::memory_order_relaxed)) + s = g_global_seed.load(std::memory_order_relaxed); + } + return s; +} + +void SetGlobalSeed(unsigned long seed) +{ + if (seed == 0) { + std::random_device rd; + seed = static_cast(rd()); + if (seed == 0) seed = 1; + } + g_global_seed.store(seed, std::memory_order_relaxed); + // the setup stream: domain = setup, index = 0. Path/worker streams use other domains, so none + // of them can ever reproduce this stream (the old ReseedThisThread(0) == SetGlobalSeed collision). + SeedEngine(g_thread_engine, static_cast(seed), kDomainSetup, 0); +} + +void ReseedThisThread(uint64_t stream_key) +{ + // per-path / per-thread stream: domain = path, index = stream_key. Deterministic from the global + // seed and distinct for every stream_key (and distinct from the setup stream). + SeedEngine(g_thread_engine, static_cast(GetGlobalSeed()), kDomainPath, stream_key); +} + +// Derive a distinct, deterministic child seed for a worker rank, from the master seed. The manager +// computes these and hands one to each worker, which then calls SetGlobalSeed(child) -- so every +// process has its own non-overlapping deterministic stream, all reproducible from the one user seed. +unsigned long DerivedWorkerSeed(uint64_t worker_index) +{ + uint64_t s = static_cast(GetGlobalSeed()); + uint64_t h = splitmix64(s ^ kDomainWorker ^ splitmix64(worker_index)); + if (h == 0) h = 1; // SetGlobalSeed treats 0 as "draw from entropy"; avoid that + return static_cast(h); +} + + + mpfr_float RandomMp() { - return RandomMp(bertini::DefaultPrecision()); + // ThreadPrecision (thread-local) rather than DefaultPrecision (global): + // random numbers are generated during tracking, which may run on a + // std::thread worker whose precision differs from the global default. + return RandomMp(bertini::ThreadPrecision()); } mpfr_float RandomMp(unsigned num_digits) @@ -94,7 +188,8 @@ namespace bertini { mpfr_float RandomMp(const mpfr_float & a, const mpfr_float & b) { - return RandomMp(a,b,bertini::DefaultPrecision()); + // see RandomMp() above for why ThreadPrecision rather than DefaultPrecision + return RandomMp(a,b,bertini::ThreadPrecision()); } mpfr_float RandomMp(const mpfr_float & a, const mpfr_float & b, unsigned num_digits) diff --git a/core/src/blackbox/algorithm_builder.cpp b/core/src/blackbox/algorithm_builder.cpp index 060cd21bc..d93c021be 100644 --- a/core/src/blackbox/algorithm_builder.cpp +++ b/core/src/blackbox/algorithm_builder.cpp @@ -26,29 +26,85 @@ \brief Provides the methods for building algorithms from files or streamable sources. */ -namespace bertini{ +#include "bertini2/blackbox/algorithm_builder.hpp" +#include "bertini2/blackbox/global_configs.hpp" +#include "bertini2/blackbox/switches_zerodim.hpp" +#include "bertini2/io/parsing/settings_parsers.hpp" +#include "bertini2/io/parsing/system_parsers.hpp" +#include "bertini2/trackers/config.hpp" +#include "bertini2/endgames/config.hpp" +#include "bertini2/nag_algorithms/zero_dim_solve.hpp" + +#include +namespace bertini{ namespace blackbox{ -int AlgoBuilder::ClassicBuild(boost::filesystem::path const& input_file) +int AlgoBuilder::ClassicBuild(std::string const& config_str, std::string const& input_str) { - std::string config, input; - std::tie(config, input) = SplitIntoConfigAndInput(input_file); + // Parse the polynomial system from the INPUT section + System sys; + try { + sys = System{input_str}; + } catch (std::exception const& e) { + std::cerr << "error: failed to parse system from input section: " << e.what() << "\n"; + return 1; + } + // Parse all configuration structs (double precision versions suffice for + // choosing algorithm types; mpfr versions would be used for mp-specific defaults) + using AllConfsD = config::Configs::All::type; + decltype(parsing::classic::ConfigParser::Parse(config_str)) cfgs_d; + try { + cfgs_d = parsing::classic::ConfigParser::Parse(config_str); + } catch (std::exception const& e) { + std::cerr << "error: failed to parse configuration: " << e.what() << "\n"; + return 1; + } - template - using AllConfs = blackbox::config::Configs::All::type; - auto results_double = bertini::parsing::classic::GetConfigSettings>(config); - auto results_mp = bertini::parsing::classic::GetConfigSettings>(config); - // now we have all the settings needed for any algorithm, huzzah + // Select tracker type from PrecisionType (mptype in Bertini1 syntax): + // Fixed (mptype: 0) -> FixedDouble + // FixedMultiple (mptype: 1) -> FixedMultiple (fixed-precision multi) + // Adaptive (mptype: 2, default) -> Adaptive (AMP) + auto prec_type = std::get(cfgs_d); + type::Tracker tracker_type; + switch (prec_type) { + case tracking::PrecisionType::Fixed: tracker_type = type::Tracker::FixedDouble; break; + case tracking::PrecisionType::FixedMultiple: tracker_type = type::Tracker::FixedMultiple; break; + default: tracker_type = type::Tracker::Adaptive; break; + } + // Select endgame type from endgamenum (Bertini1 syntax): + // 1 -> PowerSeries (PSEG), 2 -> Cauchy (default) + using algorithm::classic::EndgameChoiceConfig; + using algorithm::classic::EndgameChoice; + auto eg_choice = std::get(cfgs_d).endgame; + type::Endgame endgame_type = (eg_choice == EndgameChoice::PowerSeries) + ? type::Endgame::PowerSeries + : type::Endgame::Cauchy; + // Infer the start system from the variable-group structure, the way classic + // Bertini does: a single affine variable group -> total degree; multiple + // variable groups or any homogeneous variable group -> multihomogeneous. + // (User-defined homotopies still need a dedicated input section.) + type::Start start_type = InferStartType(sys); - return 0; -} + ZeroDimRT rt{start_type, tracker_type, endgame_type}; + + std::unique_ptr zd_alg; + try { + zd_alg = MakeZeroDim(rt, sys); + } catch (std::exception const& e) { + std::cerr << "error: failed to instantiate zero-dim algorithm: " << e.what() << "\n"; + return 1; + } + zd_alg->ApplyParsedConfigs(config_str); + alg_ = std::move(zd_alg); + return 0; +} -} // blackbox -} // bertini +} // namespace blackbox +} // namespace bertini diff --git a/core/src/blackbox/argc_argv.cpp b/core/src/blackbox/argc_argv.cpp index 84a5a055d..cc90adb8e 100644 --- a/core/src/blackbox/argc_argv.cpp +++ b/core/src/blackbox/argc_argv.cpp @@ -26,11 +26,69 @@ \brief Provides the methods for parsing the command-line arguments. */ +#include "bertini2/blackbox/argc_argv.hpp" +#include "bertini2/io/splash.hpp" +#include +#include + namespace bertini{ -void ParseArgcArgv(int argc, char** argv){ +ParsedArgs ParseArgcArgv(int argc, char** argv) +{ + ParsedArgs result; + + for (int i = 1; i < argc; ++i) + { + std::string arg{argv[i]}; -} + if (arg == "--help" || arg == "-h") + { + std::cout << + "Usage: bertini2 [options] [input_file]\n" + "\n" + " input_file path to Bertini classic input file (default: \"input\")\n" + " -f specify input file explicitly\n" + " --help, -h print this message and exit\n" + " --version print version and exit\n" + "\n" + "Parallelism:\n" +#ifdef BERTINI2_HAVE_MPI + " MPI ranks: mpirun --bind-to none -n N bertini2 [input_file]\n" + " Threads/rank: OMP_NUM_THREADS=T mpirun --bind-to none -n N bertini2 [input_file]\n" + " Total capacity = N ranks x T threads. OMP_NUM_THREADS defaults to 1.\n" + " Note: threading uses std::thread, not OpenMP. OMP_NUM_THREADS is reused\n" + " as the thread-count variable because HPC schedulers (e.g. SLURM) set it\n" + " automatically from --cpus-per-task, so no extra configuration is needed.\n" +#else + " This build was compiled without MPI. Rebuild with MPI present for\n" + " multi-rank parallelism (it is auto-detected at configure time).\n" + " Thread count: set OMP_NUM_THREADS (default: 1). Uses std::thread, not OpenMP.\n" +#endif + ; + std::exit(0); + } + else if (arg == "--version") + { + std::cout << "Bertini2 " << bertini::Version() << "\n"; + std::cout << bertini::DependencyVersions(); + std::exit(0); + } + else if (arg == "-f" && i + 1 < argc) + { + result.input_file = argv[++i]; + } + else if (arg[0] != '-') + { + result.input_file = arg; + } + else + { + std::cerr << "warning: unrecognized argument '" << arg << "'\n"; + } + } + + return result; +} } diff --git a/core/src/blackbox/bertini.cpp b/core/src/blackbox/bertini.cpp index 04566c33e..ab5302e61 100644 --- a/core/src/blackbox/bertini.cpp +++ b/core/src/blackbox/bertini.cpp @@ -4,19 +4,15 @@ int main(int argument_count, char** arguments) { using namespace bertini; - ParseArgcArgv(argument_count, arguments); - - serial::Initialize(); - parallel::Initialize(); - - - - MainModeSwitch(); + auto parsed = ParseArgcArgv(argument_count, arguments); + parallel::Initialize(); // MPI_Init first so rank is known + serial::Initialize(); // splash on rank 0 only + int result = MainModeSwitch(parsed); parallel::Finalize(); serial::Finalize(); - return 0; + return result; } diff --git a/core/src/blackbox/main_mode_switch.cpp b/core/src/blackbox/main_mode_switch.cpp index 3f7d5cd83..b835d641e 100644 --- a/core/src/blackbox/main_mode_switch.cpp +++ b/core/src/blackbox/main_mode_switch.cpp @@ -27,10 +27,130 @@ */ +#include "bertini2/blackbox/main_mode_switch.hpp" +#include "bertini2/blackbox/algorithm_builder.hpp" +#include "bertini2/io/parsing/classic_utilities.hpp" +#include "bertini2/io/parsing/settings_parsers.hpp" +#include "bertini2/nag_algorithms/common/config.hpp" +#include "bertini2/nag_algorithms/zero_dim_solve.hpp" +#include "bertini2/parallel.hpp" +#include "bertini2/random.hpp" + +#include +#include + +#ifdef BERTINI2_HAVE_MPI +#include "bertini2/parallel/mpi_include.hpp" +#endif + + namespace bertini{ - void MainModeSwitch() +namespace { + +int RunZeroDim(std::string const& config_str, std::string const& input_str) +{ + // Parse and apply the RNG seed before any setup draws (gamma, TD-constants, patch). + auto rand_cfg = parsing::classic::FillConfigStruct(config_str); + +#ifdef BERTINI2_HAVE_MPI + // Manager sets the seed (possibly from entropy), then broadcasts the effective + // (non-zero) seed to workers so all ranks share identical per-path streams. + if (parallel::IsManager()) + SetGlobalSeed(rand_cfg.random_seed); + unsigned long effective_seed = GetGlobalSeed(); + MPI_Bcast(&effective_seed, 1, MPI_UNSIGNED_LONG, 0, MPI_COMM_WORLD); + if (!parallel::IsManager()) + SetGlobalSeed(effective_seed); +#else + SetGlobalSeed(rand_cfg.random_seed); +#endif + + std::cout << "bertini: random seed = " << GetGlobalSeed() << "\n"; + + blackbox::AlgoBuilder builder; + if (builder.ClassicBuild(config_str, input_str) != 0) + { + std::cerr << "error: failed to build zero-dim algorithm from input\n"; + return 1; + } + + auto* alg = dynamic_cast(builder.GetAlg()); + if (!alg) + { + std::cerr << "error: algorithm builder returned wrong type\n"; + return 1; + } + + alg->Run(); + + if (parallel::IsManager()) { - + { + std::ofstream main_data{"main_data"}; + alg->WriteMainData(main_data); + } + { + std::ofstream raw_data{"raw_data"}; + alg->WriteRawData(raw_data); + } + std::cout << "bertini: wrote main_data and raw_data\n"; } + return 0; } + +} // anonymous namespace + + +int MainModeSwitch(ParsedArgs const& args) +{ + std::string config_str, input_str; + +#ifdef BERTINI2_HAVE_MPI + // Rank 0 reads the input file, then broadcasts both strings to all ranks so + // every rank builds an identical algorithm object from the same source text. + if (parallel::IsManager()) + { + try { + std::tie(config_str, input_str) = parsing::classic::SplitIntoConfigAndInput(args.input_file); + } catch (std::exception const& e) { + std::cerr << "error reading input file '" << args.input_file.string() << "': " << e.what() << "\n"; + // Broadcast empty strings so workers don't hang waiting for broadcast. + parallel::mpi_broadcast_string(parallel::WorldComm(), config_str, 0); + parallel::mpi_broadcast_string(parallel::WorldComm(), input_str, 0); + return 1; + } + } + parallel::mpi_broadcast_string(parallel::WorldComm(), config_str, 0); + parallel::mpi_broadcast_string(parallel::WorldComm(), input_str, 0); + if (config_str.empty() && input_str.empty()) + return 1; // rank 0 failed to read; workers bail out cleanly +#else + try { + std::tie(config_str, input_str) = parsing::classic::SplitIntoConfigAndInput(args.input_file); + } catch (std::exception const& e) { + std::cerr << "error reading input file '" << args.input_file.string() << "': " << e.what() << "\n"; + return 1; + } +#endif + + using AlgoChoice = algorithm::classic::AlgoChoice; + auto choice = parsing::classic::FillConfigStruct(config_str); + + switch (choice) + { + case AlgoChoice::ZeroDim: + return RunZeroDim(config_str, input_str); + + case AlgoChoice::NID: + std::cerr << "error: tracktype 1 (NID) is not yet implemented in Bertini2\n"; + return 1; + + default: + std::cerr << "error: tracktype " << static_cast(choice) + << " is not yet implemented in Bertini2\n"; + return 1; + } +} + +} // namespace bertini diff --git a/core/src/eti/endgames_eti.cpp b/core/src/eti/endgames_eti.cpp new file mode 100644 index 000000000..ceb195cce --- /dev/null +++ b/core/src/eti/endgames_eti.cpp @@ -0,0 +1,61 @@ +//This file is part of Bertini 2. +// +//src/eti/endgames_eti.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//src/eti/endgames_eti.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with src/eti/endgames_eti.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin eau claire + +/** +\file endgames_eti.cpp + +Explicit instantiation definitions for the closed universe of endgame types: +{PowerSeries, Cauchy} x {fixed double, fixed multiple, AMP}. These pair with +the extern template declarations at the bottom of bertini2/endgames.hpp and +prevent every consumer TU (python bindings, blackbox, tests) from re-emitting +the entire endgame+tracker instantiation cone. See ADR-0014. + +Adding a new endgame flavor or tracker? Add its combinations here AND to the +extern block in endgames.hpp. +*/ + +#include "bertini2/endgames.hpp" + +namespace bertini{ namespace endgame{ + +using DPT = tracking::DoublePrecisionTracker; +using MPT = tracking::MultiplePrecisionTracker; + +// the bases first: explicitly instantiating a derived class does not +// instantiate the base's members. +template class EndgameBase>, FixedPrecEndgame>; +template class EndgameBase>, FixedPrecEndgame>; +template class EndgameBase, AMPEndgame>; +template class EndgameBase>, FixedPrecEndgame>; +template class EndgameBase>, FixedPrecEndgame>; +template class EndgameBase, AMPEndgame>; + +template class PowerSeriesEndgame>; +template class PowerSeriesEndgame>; +template class PowerSeriesEndgame; +template class CauchyEndgame>; +template class CauchyEndgame>; +template class CauchyEndgame; + +}} // namespaces diff --git a/core/src/eti/zero_dim_blackbox_eti.cpp b/core/src/eti/zero_dim_blackbox_eti.cpp new file mode 100644 index 000000000..3d0151fd6 --- /dev/null +++ b/core/src/eti/zero_dim_blackbox_eti.cpp @@ -0,0 +1,59 @@ +//This file is part of Bertini 2. +// +//src/eti/zero_dim_blackbox_eti.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//src/eti/zero_dim_blackbox_eti.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with src/eti/zero_dim_blackbox_eti.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin eau claire + +/** +\file zero_dim_blackbox_eti.cpp + +Explicit instantiation definitions for the ZeroDim combos reachable through +the blackbox switch ladder beyond the TotalDegree six (see zero_dim_eti.cpp): +MHomogeneous start (CloneGiven) and User start (RefToGiven), per +blackbox/config.hpp's StorageSelector. Pairs with the extern block at the +bottom of bertini2/nag_algorithms/zero_dim_solve.hpp. See ADR-0014. +*/ + +#include "bertini2/nag_algorithms/zero_dim_solve.hpp" +#include "bertini2/endgames.hpp" +#include "bertini2/system/start_systems.hpp" + +namespace bertini{ namespace algorithm{ + +using DPT = tracking::DoublePrecisionTracker; +using MPT = tracking::MultiplePrecisionTracker; +using AMPT = tracking::AMPTracker; + +template struct ZeroDim::PSEG, System, start_system::MHomogeneous>; +template struct ZeroDim::Cauchy, System, start_system::MHomogeneous>; +template struct ZeroDim::PSEG, System, start_system::MHomogeneous>; +template struct ZeroDim::Cauchy, System, start_system::MHomogeneous>; +template struct ZeroDim::PSEG, System, start_system::MHomogeneous>; +template struct ZeroDim::Cauchy, System, start_system::MHomogeneous>; + +template struct ZeroDim::PSEG, System, start_system::User, policy::RefToGiven>; +template struct ZeroDim::Cauchy, System, start_system::User, policy::RefToGiven>; +template struct ZeroDim::PSEG, System, start_system::User, policy::RefToGiven>; +template struct ZeroDim::Cauchy, System, start_system::User, policy::RefToGiven>; +template struct ZeroDim::PSEG, System, start_system::User, policy::RefToGiven>; +template struct ZeroDim::Cauchy, System, start_system::User, policy::RefToGiven>; + +}} // namespaces diff --git a/core/src/eti/zero_dim_eti.cpp b/core/src/eti/zero_dim_eti.cpp new file mode 100644 index 000000000..898bd7f0c --- /dev/null +++ b/core/src/eti/zero_dim_eti.cpp @@ -0,0 +1,56 @@ +//This file is part of Bertini 2. +// +//src/eti/zero_dim_eti.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//src/eti/zero_dim_eti.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with src/eti/zero_dim_eti.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin eau claire + +/** +\file zero_dim_eti.cpp + +Explicit instantiation definitions for the closed universe of ZeroDim +algorithm types: {PowerSeries, Cauchy} x {double, multiple, AMP} trackers, +TotalDegree start, CloneGiven system management (the default; both the python +bindings and blackbox use exactly these six). Pairs with the extern template +declarations at the bottom of bertini2/nag_algorithms/zero_dim_solve.hpp. +See ADR-0014. + +Adding a combo? Add it here AND to the extern block. +*/ + +#include "bertini2/nag_algorithms/zero_dim_solve.hpp" +#include "bertini2/endgames.hpp" +#include "bertini2/system/start_systems.hpp" + +namespace bertini{ namespace algorithm{ + +using DPT = tracking::DoublePrecisionTracker; +using MPT = tracking::MultiplePrecisionTracker; +using AMPT = tracking::AMPTracker; +using TD = start_system::TotalDegree; + +template struct ZeroDim::PSEG, System, TD>; +template struct ZeroDim::Cauchy, System, TD>; +template struct ZeroDim::PSEG, System, TD>; +template struct ZeroDim::Cauchy, System, TD>; +template struct ZeroDim::PSEG, System, TD>; +template struct ZeroDim::Cauchy, System, TD>; + +}} // namespaces diff --git a/core/src/function_tree/canonical.cpp b/core/src/function_tree/canonical.cpp new file mode 100644 index 000000000..b2d26b5d2 --- /dev/null +++ b/core/src/function_tree/canonical.cpp @@ -0,0 +1,165 @@ +//This file is part of Bertini 2. +// +//canonical.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//canonical.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with canonical.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +#include "bertini2/function_tree.hpp" +#include "bertini2/function_tree/canonical.hpp" +#include "bertini2/function_tree/gather.hpp" + +#include +#include +#include +#include +#include + +namespace bertini { +namespace node { + +namespace { + // session-global canonicalization settings. Canonicalization is ON by default: every Sum/Mult normalizes operand + // order, so structurally-equal expressions dedup to one interned node. + MonomialOrder& TheOrder() { static MonomialOrder o = MonomialOrder::GrevLex; return o; } + bool& TheEnabledFlag() { static bool on = true; return on; } + + bool AllNonNegative(std::vector const& v) + { + for (int e : v) if (e < 0) return false; + return true; + } + + // a constant factor: a polynomial monomial of total degree zero (no variables). + bool IsConstantKey(std::vector const& v) + { + return AllNonNegative(v) && std::accumulate(v.begin(), v.end(), 0) == 0; + } + + // Does monomial 'a' sort BEFORE monomial 'b' (i.e. is it the "greater" / leading one, + // since we sort descending)? Polynomial monomials precede non-polynomial operands. + bool MonomialGreater(std::vector const& a, std::vector const& b, MonomialOrder order) + { + const bool aPoly = AllNonNegative(a); + const bool bPoly = AllNonNegative(b); + if (aPoly != bPoly) return aPoly; // polynomial terms come first + if (!aPoly) return false; // both non-polynomial: a tie (printed-form breaks it) + + const std::size_t n = a.size(); + switch (order) + { + case MonomialOrder::Lex: + for (std::size_t i = 0; i < n; ++i) + if (a[i] != b[i]) return a[i] > b[i]; + return false; + + case MonomialOrder::RevLex: + for (std::size_t i = n; i-- > 0; ) + if (a[i] != b[i]) return a[i] < b[i]; + return false; + + case MonomialOrder::GrevLex: + { + const int da = std::accumulate(a.begin(), a.end(), 0); + const int db = std::accumulate(b.begin(), b.end(), 0); + if (da != db) return da > db; // graded: higher total degree first + for (std::size_t i = n; i-- > 0; ) // then reverse-lex on ties + if (a[i] != b[i]) return a[i] < b[i]; + return false; + } + } + return false; + } + + std::string PrintOf(std::shared_ptr const& n) + { + std::ostringstream oss; + n->print(oss); + return oss.str(); + } +} // anon namespace + +MonomialOrder CurrentMonomialOrder() { return TheOrder(); } +void SetMonomialOrder(MonomialOrder o){ TheOrder() = o; } +bool CanonicalizeByDefault() { return TheEnabledFlag(); } +void SetCanonicalizeByDefault(bool on){ TheEnabledFlag() = on; } + +void CanonicalizeNaryOperands(std::vector>& operands, + std::vector& flags, + bool multiplicands_first) +{ + if (!CanonicalizeByDefault() || operands.size() < 2) + return; + + // global variable order: the union of the operands' variables, alphabetical by name + // (GatherVariables already sorts by name; variables are canonical-by-name post-3b). + VariableGroup vars; + std::set seen; + for (auto const& op : operands) + for (auto const& v : GatherVariables(op)) + if (seen.insert(v->name()).second) + vars.push_back(v); + std::sort(vars.begin(), vars.end(), + [](std::shared_ptr const& a, std::shared_ptr const& b) + { return a->name() < b->name(); }); + + const std::size_t N = operands.size(); + std::vector> keys(N); + std::vector prints(N); + for (std::size_t i = 0; i < N; ++i) + { + keys[i] = operands[i]->MultiDegree(vars); + prints[i] = PrintOf(operands[i]); + } + + const MonomialOrder order = CurrentMonomialOrder(); + std::vector perm(N); + std::iota(perm.begin(), perm.end(), std::size_t{0}); + std::stable_sort(perm.begin(), perm.end(), + [&](std::size_t i, std::size_t j) -> bool + { + // for a Mult, multiplicands (flag true) sort ahead of divisors (flag false), so a + // divisor is never first; the +/- sign of a Sum term does not affect ordering. + if (multiplicands_first && flags[i] != flags[j]) + return flags[i]; + // within a product, a constant coefficient sorts first, so monomials read the + // conventional way: "3*x^2", not "x^2*3". (Sums keep degree order: the constant + // term stays last, e.g. "x^2+2*x*y-1".) + if (multiplicands_first) + { + const bool ci = IsConstantKey(keys[i]); + const bool cj = IsConstantKey(keys[j]); + if (ci != cj) return ci; + } + if (MonomialGreater(keys[i], keys[j], order)) return true; + if (MonomialGreater(keys[j], keys[i], order)) return false; + return prints[i] < prints[j]; // deterministic, content-based tie-break + }); + + std::vector> new_operands(N); + std::vector new_flags(N); + for (std::size_t i = 0; i < N; ++i) + { + new_operands[i] = operands[perm[i]]; + new_flags[i] = flags[perm[i]]; + } + operands.swap(new_operands); + flags.swap(new_flags); +} + +} // namespace node +} // namespace bertini diff --git a/core/src/function_tree/find.cpp b/core/src/function_tree/find.cpp new file mode 100644 index 000000000..f2e647bd3 --- /dev/null +++ b/core/src/function_tree/find.cpp @@ -0,0 +1,116 @@ +//This file is part of Bertini 2. +// +//src/function_tree/find.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//src/function_tree/find.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with src/function_tree/find.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire + + +#include "bertini2/function_tree/find.hpp" +#include "bertini2/function_tree.hpp" + +#include +#include + +namespace bertini { +namespace node { + + namespace { + + // Recursively descend the tree, collecting distinct nodes of kind T. A node that is a T + // is collected AND descended into (so nested Ts -- e.g. a NamedExpression whose entry + // contains another NamedExpression -- are all found). Descent uses only public child + // accessors, so every operator subtype is handled via its base class. + template + void FindImpl(std::shared_ptr const& n, + std::vector>& out, + std::set& seen, + std::set& visited) + { + if (!n) + return; + + if (!visited.insert(n.get()).second) // already processed this (possibly shared) node + return; + + if (auto t = std::dynamic_pointer_cast(n)) + if (seen.insert(t.get()).second) + out.push_back(std::const_pointer_cast(t)); + + // NamedExpression -- descend into the wrapped expression + if (auto h = std::dynamic_pointer_cast(n)) + FindImpl(h->EntryNode(), out, seen, visited); + // Sum, Mult, ... -- any number of operands + else if (auto nary = std::dynamic_pointer_cast(n)) + for (auto const& child : nary->Operands()) + FindImpl(child, out, seen, visited); + // Negate, Sqrt, Exp, Log, IntegerPower, trig, ... -- a single operand + else if (auto u = std::dynamic_pointer_cast(n)) + FindImpl(u->Operand(), out, seen, visited); + // PowerOperator derives from Operator directly (base and exponent) + else if (auto p = std::dynamic_pointer_cast(n)) + { + FindImpl(p->GetBase(), out, seen, visited); + FindImpl(p->GetExponent(), out, seen, visited); + } + // numbers, special numbers, variables, differentials are leaves with no children. + } + + template + std::vector>& SortByName(std::vector>& v) + { + std::sort(v.begin(), v.end(), + [](std::shared_ptr const& a, std::shared_ptr const& b) + { return a->name() < b->name(); }); + return v; + } + + } // unnamed namespace + + + template + std::vector> Find(std::shared_ptr const& n) + { + std::vector> out; + std::set seen; + std::set visited; + FindImpl(n, out, seen, visited); + return SortByName(out); + } + + template + std::vector> Find(std::vector> const& roots) + { + std::vector> out; + std::set seen; + std::set visited; + for (auto const& r : roots) + FindImpl(r, out, seen, visited); + return SortByName(out); + } + + // One explicit instantiation per discoverable kind (keeps the traversal in this TU). + template std::vector> Find(std::shared_ptr const&); + template std::vector> Find(std::vector> const&); + template std::vector> Find(std::shared_ptr const&); + template std::vector> Find(std::vector> const&); + +} // namespace node +} // namespace bertini diff --git a/core/src/function_tree/gather.cpp b/core/src/function_tree/gather.cpp new file mode 100644 index 000000000..f4d9876be --- /dev/null +++ b/core/src/function_tree/gather.cpp @@ -0,0 +1,48 @@ +//This file is part of Bertini 2. +// +//src/function_tree/gather.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//src/function_tree/gather.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with src/function_tree/gather.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire + + +#include "bertini2/function_tree/gather.hpp" +#include "bertini2/function_tree/find.hpp" +#include "bertini2/function_tree.hpp" + +namespace bertini { +namespace node { + + // GatherVariables is the variable-specific case of the general Find (sympy's find): the + // one traversal lives in find.cpp, and these delegate to Find. + + VariableGroup GatherVariables(std::shared_ptr const& n) + { + return Find(n); + } + + VariableGroup GatherVariables(std::vector> const& functions) + { + std::vector> roots(functions.begin(), functions.end()); + return Find(roots); + } + +} // namespace node +} // namespace bertini diff --git a/core/src/function_tree/linear_product.cpp b/core/src/function_tree/linear_product.cpp deleted file mode 100644 index c7c6d9b16..000000000 --- a/core/src/function_tree/linear_product.cpp +++ /dev/null @@ -1,502 +0,0 @@ -//This file is part of Bertini 2. -// -//variable.hpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//linear_product.cpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with variable.hpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// James Collins -// West Texas A&M University -// Spring, Summer 2015 -// -// Silviana Amethyst -// Summer 2021 -// -// Created by Collins, James B. on 3/6/2017. -// -// -// linear_product.cpp: Declares the class LinearProduct. - - -/** - \file linear_product.cpp - - \brief Provides the LinearProduct Node class and the DiffLinear node class. - - */ -#ifndef BERTINI_FUNCTION_TREE_LINPRODUCT_CPP -#define BERTINI_FUNCTION_TREE_LINPRODUCT_CPP - -#include "bertini2/function_tree/symbols/linear_product.hpp" - - -template using Mat = bertini::Mat; - -namespace bertini { - namespace node{ - - - ///////////////////////////////////////// - // - // Linear Product methods - // - //////////////////////////////////////// - - std::shared_ptr LinearProduct::Differentiate(std::shared_ptr const& v) const - { - std::shared_ptr ret_sum; - std::vector indices; //Those factors that are not differentiated in one particular term of the differentiated result. - - // First term of product rule - std::shared_ptr temp_mult = MultOperator::Make(DiffLinear::Make(GetLinears(0))); - for(int ii = 1; ii < num_factors_; ++ii) - { - indices.push_back(ii); // Indices 1 to num_factors-1 - } - if(indices.size() > 0) - temp_mult *= GetLinears(indices); - ret_sum = SumOperator::Make(temp_mult, true); - - - // Rest of the factors - for(int ii = 1; ii < num_factors_; ++ii) - { - temp_mult = MultOperator::Make(DiffLinear::Make(GetLinears(ii))); - indices.clear(); - for(int jj = 0; jj < num_factors_ ; ++jj) - { - if(ii != jj) - indices.push_back(jj); - } - temp_mult *= GetLinears(indices); - ret_sum->AddOperand(temp_mult,true); - } - - - - return ret_sum; - - - } - - - - - - - int LinearProduct::Degree(std::shared_ptr const& v) const - { - int deg = 0; - - // If v is part of the linear product - if(std::find(variables_.begin(), variables_.end(), v) != std::end(variables_)) - { - deg = num_factors_; - } - - return deg; - } - - - - - - int LinearProduct::Degree(VariableGroup const& vars) const - { - int deg = 0; - - for (auto v = vars.begin(); v != vars.end(); ++v) - { - // if v is a part of the linear product - if(std::find(variables_.begin(), variables_.end(), *v) != std::end(variables_)) - { - deg = num_factors_; - break; - } - } - - return deg; - } - - - - - std::vector LinearProduct::MultiDegree(VariableGroup const& vars) const - { - std::vector degs(vars.size(), 0); - - for (auto v = vars.begin(); v != vars.end(); ++v) - { - // If v is part of the linear product - if(std::find(variables_.begin(), variables_.end(), *v) != std::end(variables_)) - { - *(degs.begin()+(v-vars.begin())) = num_factors_; - } - } - - - return degs; - } - - - - - - void LinearProduct::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) - { - // Needed if this is for a homogeneous variable group - if(is_homogenized_) - { - return; - } - - - // Check if vars is the variable group for this linear product - bool is_vargroup_same = true; // Check if vars and variables_ are the same - bool is_v_in_vars = false; // Check if at least one v in variables_ is in vars - - - // Is homvar in vars? If so erase it from the temporary variable holder temp_vars. - VariableGroup temp_vars = vars; - temp_vars.erase(std::remove(temp_vars.begin(), temp_vars.end(), homvar), temp_vars.end()); - - - // Which group is larger, variables_ or vars? - VariableGroup larger_group; - VariableGroup smaller_group; - if(variables_.size() > temp_vars.size()) - { -// std::cout << "variables larger\n"; - larger_group = variables_; - smaller_group = temp_vars; - } - else - { -// std::cout << "homogenizing variable group larger\n"; - larger_group = temp_vars; - smaller_group = variables_; - } - - // Is vars the same as variables_? - for (auto const v : larger_group) - { - // If v is in vars? - if(std::find(smaller_group.begin(), smaller_group.end(), v) != std::end(smaller_group)) - { - is_v_in_vars = true; - } - else - { - is_vargroup_same = false; - } - } - - if(!is_vargroup_same && is_v_in_vars) - { - throw std::runtime_error("attempting to homogenize linear product with respect to part of the variables, but not all"); - } - - // If the homogenizing variable group is the same as the variables used in the linear product, then homogenize. - if(is_vargroup_same) - { - hom_variable_ = homvar; - is_homogenized_ = true; - } - - } - - - - - - bool LinearProduct::IsHomogeneous(std::shared_ptr const& v) const - { - - if(v == nullptr) - { - if(is_homogenized_) - { - return true; - } - else - { - return false; - } - } - else - { - // If v is not part of the linear product - if(std::find(variables_.begin(), variables_.end(), v) == std::end(variables_)) - { - return true; - } - else - { - return false; - } - } - } - - - - - - bool LinearProduct::IsHomogeneous(VariableGroup const& vars) const - { - bool is_hom = false; - - // Check if vars is the variable group for this linear product - bool is_vargroup_same = true; // Check if vars and variables_ are the same - bool is_v_in_vars = false; // Check if at least one v in variables_ is in vars - for (auto const v : variables_) - { - - // If v is in vars? - if(std::find(vars.begin(), vars.end(), v) != std::end(vars)) - { - is_v_in_vars = true; - } - else - { - is_vargroup_same = false; - } - } - - if(is_homogenized_ && !is_hom_vars_) - { - // Is hom_variable is in vars? - if(std::find(vars.begin(), vars.end(), hom_variable_) != std::end(vars)) - { - is_v_in_vars = true; - } - else - { - is_vargroup_same = false; - } - } - - if(!is_vargroup_same && is_v_in_vars) - { - return false; - } - - if(!is_vargroup_same) - { - is_hom = true; - } - else if(is_homogenized_) - { - is_hom = true; - } - - return is_hom; - } - - - - - - - - - - - - std::shared_ptr LinearProduct::GetLinears(size_t index) const - { - if(is_rational_coeffs_) - { - Mat temp_real(1,num_variables_+1); - Mat temp_imag(1,num_variables_+1); - for(int jj = 0; jj < num_variables_+1; ++jj) - { - temp_real(0,jj) = coeffs_rat_real_(index,jj); - temp_imag(0,jj) = coeffs_rat_imag_(index,jj); - } - - LinearProduct temp(variables_, hom_variable_, temp_real, temp_imag, is_hom_vars_); - return LinearProduct::Make(temp); - } - else - { - Mat temp_mpfr(1,num_variables_+1); - auto& coeffs_mp_ref = std::get>(coeffs_); - for(int jj = 0; jj < num_variables_+1; ++jj) - { - temp_mpfr(0,jj) = coeffs_mp_ref(index,jj); - } - - LinearProduct temp(variables_, hom_variable_, temp_mpfr, is_hom_vars_); - return LinearProduct::Make(temp); - - } - - } - - - - std::shared_ptr LinearProduct::GetLinears(std::vector indices) const - { - if(is_rational_coeffs_) - { - Mat temp_real(indices.size(),num_variables_+1); - Mat temp_imag(indices.size(),num_variables_+1); - for(int ii = 0; ii < indices.size(); ++ii) - { - for(int jj = 0; jj < num_variables_+1; ++jj) - { - temp_real(ii,jj) = coeffs_rat_real_(indices[ii],jj); - temp_imag(ii,jj) = coeffs_rat_imag_(indices[ii],jj); - } - } - - LinearProduct temp(variables_, hom_variable_, temp_real, temp_imag, is_hom_vars_); - return LinearProduct::Make(temp); - } - else - { - Mat temp_mpfr(indices.size(),num_variables_+1); - auto& coeffs_mp_ref = std::get>(coeffs_); - for(int ii = 0; ii < indices.size(); ++ii) - { - for(int jj = 0; jj < num_variables_+1; ++jj) - { - temp_mpfr(ii,jj) = coeffs_mp_ref(indices[ii],jj); - } - } - - LinearProduct temp(variables_, hom_variable_, temp_mpfr, is_hom_vars_); - return LinearProduct::Make(temp); - } - - } - - - - void LinearProduct::print(std::ostream & target) const - { - auto& coeff_ref = std::get>(coeffs_); - - for(int ii = 0; ii < num_factors_; ++ii) - { - target << "(" << coeff_ref(ii,0) << "*"; - variables_[0]->print(target); - for(int jj = 1; jj < num_variables_; ++jj) - { - target << " + " << coeff_ref(ii,jj) << "*"; - variables_[jj]->print(target); - } - - if(!is_hom_vars_) - { - target << " + " << coeff_ref(ii,num_variables_); - } - - if(ii == num_factors_ - 1) - { - target << ")"; - } - else - { - target << ")*"; - } - } - } - - - - - - - - - - - - ///////////////////////////////////////// - // - // Diff Linear methods - // - //////////////////////////////////////// - - DiffLinear::DiffLinear(std::shared_ptr const& linear) - { - // Set differentials of all variables in the linear - linear->GetVariables(variables_); - - linear->GetHomVariable(hom_variable_); - - - // Set coefficients with rational or mpfr_complex type - num_variables_ = variables_.size(); - Mat& coeffs_dbl_ref = std::get>(coeffs_); - Mat& coeffs_mpfr_ref = std::get>(coeffs_); - coeffs_dbl_ref.resize(1, num_variables_+1); - coeffs_mpfr_ref.resize(1, num_variables_+1); - - if(linear->IsRationalCoefficients()) - { - coeffs_rat_real_.resize(1, num_variables_+1); - coeffs_rat_real_.resize(1, num_variables_+1); - linear->GetRatCoeffs(coeffs_rat_real_, coeffs_rat_imag_); - for(int jj = 0; jj < num_variables_+1; ++jj) - { - coeffs_dbl_ref(0,jj).real( static_cast(coeffs_rat_real_(0,jj)) ); - coeffs_dbl_ref(0,jj).imag( static_cast(coeffs_rat_imag_(0,jj)) ); - coeffs_mpfr_ref(0,jj).real( static_cast(coeffs_rat_real_(0,jj)) ); - coeffs_mpfr_ref(0,jj).imag( static_cast(coeffs_rat_imag_(0,jj)) ); - } - } - else - { - linear->GetMPFRCoeffs(coeffs_mpfr_ref); - - for(int jj = 0; jj < num_variables_+1; ++jj) - { - coeffs_dbl_ref(jj) = static_cast(coeffs_mpfr_ref(jj)); - } - - } - - - } - - - - - void DiffLinear::print(std::ostream & target) const - { - auto& coeff_ref = std::get>(coeffs_); - - target << "(" << coeff_ref(0,0) << "*d"; - variables_[0]->print(target); - for(int jj = 1; jj < num_variables_; ++jj) - { - target << " + " << coeff_ref(0,jj) << "*d"; - variables_[jj]->print(target); - } - - target << ")"; - } - - - - } -} - -#endif diff --git a/core/src/function_tree/node.cpp b/core/src/function_tree/node.cpp index a79aed73e..2372a7f5a 100644 --- a/core/src/function_tree/node.cpp +++ b/core/src/function_tree/node.cpp @@ -26,6 +26,11 @@ #include "bertini2/function_tree.hpp" +#include +#include +#include +#include + BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::Variable) BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::Differential) @@ -36,8 +41,7 @@ BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::Rational) BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::special_number::Pi) BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::special_number::E) -BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::Function) -BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::Jacobian) +BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::NamedExpression) BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::SinOperator) BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::ArcSinOperator) @@ -61,32 +65,43 @@ BOOST_CLASS_EXPORT_IMPLEMENT(bertini::node::ExpOperator) namespace bertini{ namespace node{ - unsigned Node::ReduceDepth() + // Default: nothing to simplify -- return this node unchanged (sharing preserved). + // Operators override to recurse + reassemble through the Simplified* factories. + std::shared_ptr Node::Simplified() const + { + return std::const_pointer_cast(shared_from_this()); + } + + // Default: nothing to homogenize (leaves) -- return this node unchanged. Operators that + // can carry degree-deficient summands (and their ancestors) override to rebuild functionally. + std::shared_ptr Node::Homogenized(VariableGroup const& /*vars*/, std::shared_ptr const& /*homvar*/) const { - return 0; + return std::const_pointer_cast(shared_from_this()); } + // ---- structural hash / equality (predicate layer for hash-consing) ---- - template - void Node::EvalInPlace(T& eval_value, std::shared_ptr const& diff_variable) const + std::size_t Node::Hash() const { - auto& val_pair = std::get< std::pair >(current_value_); - if(!val_pair.second) - { - detail::FreshEvalSelector::RunInPlace(val_pair.first, *this,diff_variable); - val_pair.second = true; - } - eval_value = val_pair.first; + if (!structural_hash_) + structural_hash_ = HashImpl(); + return *structural_hash_; } - template void Node::EvalInPlace(dbl&, std::shared_ptr const&) const; - template void Node::EvalInPlace(mpfr_complex&, std::shared_ptr const&) const; + // Default: identity hash (the object address). Distinct objects hash distinctly; value + // and operator nodes override HashImpl to be structural. + std::size_t Node::HashImpl() const + { + return std::hash{}(this); + } - unsigned Node::precision() const + // Default: identity equality. Value/operator nodes override. + bool Node::IsSame(Node const& other) const { - return std::get >(current_value_).first.precision(); + return this == &other; } + bool Node::IsPolynomial(std::shared_ptr const&v) const { return Degree(v)>=0; @@ -97,16 +112,47 @@ namespace node{ return Degree(v)>=0; } - void Node::ResetStoredValues() const - { - std::get< std::pair >(current_value_).second = false; - std::get< std::pair >(current_value_).second = false; + Node::Node() + { } + + // ---- hash-consing intern table ---- + namespace { + // Process-global table: structural hash -> live nodes, held weakly so it self-cleans + // (a node dies when its last external shared_ptr drops; its weak_ptr is pruned on the + // next touch of that bucket). Lazy-init function-local statics avoid SIOF. + std::unordered_map>>& InternBuckets() + { + static std::unordered_map>> buckets; + return buckets; + } + std::mutex& InternMutex() + { + static std::mutex m; + return m; + } } - Node::Node() + std::shared_ptr Intern(std::shared_ptr const& candidate) { - std::get >(current_value_).second = false; - std::get >(current_value_).second = false; + std::lock_guard lock(InternMutex()); + auto& bucket = InternBuckets()[candidate->Hash()]; + + std::shared_ptr found; + // scan for a live, structurally-equal node; prune any expired weak_ptrs as we go + bucket.erase( + std::remove_if(bucket.begin(), bucket.end(), + [&](std::weak_ptr const& wp) { + auto sp = wp.lock(); + if (!sp) return true; // dead -> prune + if (!found && sp->IsSame(*candidate)) found = sp; + return false; + }), + bucket.end()); + + if (found) + return found; // hit: discard the candidate + bucket.push_back(candidate); // miss: register and keep + return candidate; } } // namespace node } // namespace bertini diff --git a/core/src/function_tree/operators/arithmetic.cpp b/core/src/function_tree/operators/arithmetic.cpp index 305859c5b..f30e992c7 100644 --- a/core/src/function_tree/operators/arithmetic.cpp +++ b/core/src/function_tree/operators/arithmetic.cpp @@ -26,6 +26,10 @@ #include "bertini2/function_tree/operators/arithmetic.hpp" +#include +#include +#include + @@ -39,179 +43,407 @@ namespace bertini{ // ////////////////////// -unsigned SumOperator::EliminateZeros() + + + + +namespace{ + // print an operand, wrapping in parentheses only when its precedence is + // too low for the position it occupies + void PrintOperand(std::ostream& target, std::shared_ptr const& n, bool needs_parens) + { + if (needs_parens) + target << "("; + n->print(target); + if (needs_parens) + target << ")"; + } +} + + +std::shared_ptr SimplifiedNegate(std::shared_ptr const& n) { - assert(!operands_.empty() && "operands_ must not be empty to eliminate zeros"); + if (n->IsLiteralZero()) + return n; + return NegateOperator::Make(n); +} - unsigned num_eliminated{0}; - if (NumOperands()>1) - { - std::vector> new_children; std::vector new_ops; - std::vector is_zero(operands_.size(), false); - for (unsigned ii=0; iiEval()==0.) - is_zero[ii] = true; +namespace{ + // split a term into (rational coefficient, core). the core is the term with a leading + // constant factor stripped, so 3*x*y and 2*x*y share the core x*y and combine to 5*x*y. a + // null core means the term was a pure exact constant (whose value is the coefficient). + std::pair> SplitTerm(std::shared_ptr const& t) + { + if (auto as_int = std::dynamic_pointer_cast(t)) + return { mpq_rational(as_int->GetValue()), nullptr }; + if (auto as_rat = std::dynamic_pointer_cast(t)) + if (as_rat->GetValueImag() == 0) + return { as_rat->GetValueReal(), nullptr }; - for (unsigned ii=0; ii(t)) + { + auto const& ops = as_mult->Operands(); + auto const& flags = as_mult->GetMultOrDiv(); + if (!ops.empty() && flags[0]) // canonical order puts a constant coefficient first { - ++num_eliminated; + mpq_rational coeff(1); + bool have = false; + if (auto as_int = std::dynamic_pointer_cast(ops[0])) + { + coeff = as_int->GetValue(); + have = true; + } + else if (auto as_rat = std::dynamic_pointer_cast(ops[0])) + { + if (as_rat->GetValueImag() == 0) + { + coeff = as_rat->GetValueReal(); + have = true; + } + } + if (have) + { + std::vector, bool>> rest; + for (std::size_t ii = 1; ii < ops.size(); ++ii) + rest.emplace_back(ops[ii], flags[ii]); + std::shared_ptr core = (rest.size() == 1 && rest[0].second) + ? rest[0].first + : std::static_pointer_cast(MultOperator::Make(rest)); + return { coeff, core }; + } } + } + return { mpq_rational(1), t }; + } - if (new_children.empty()) + std::shared_ptr RationalToNode(mpq_rational const& v) + { + if (denominator(v) == 1) + return std::static_pointer_cast(Integer::Make(numerator(v))); + return std::static_pointer_cast(Rational::Make(v, mpq_rational(0))); + } +} + +std::shared_ptr SimplifiedSum(std::vector, bool>> const& terms) +{ + // combine like terms: group by core identity (the term sans constant coefficient) and sum the + // signed coefficients, so x+x -> 2*x, 3*x+2*x -> 5*x, x-x -> 0; pure constants fold together. + mpq_rational constant_sum(0); + std::vector> cores; // representative core, first-seen order + std::map core_index; + std::vector coeffs; + for (auto const& t : terms) + { + if (t.first->IsLiteralZero()) + continue; + auto split = SplitTerm(t.first); + mpq_rational coeff = t.second ? split.first : -split.first; + if (!split.second) // a pure constant term { - new_children.push_back(Integer::Make(0)); - new_ops.push_back(true); - --num_eliminated; + constant_sum += coeff; + continue; } + auto it = core_index.find(split.second.get()); + if (it == core_index.end()) + { + core_index.emplace(split.second.get(), cores.size()); + cores.push_back(split.second); + coeffs.push_back(coeff); + } + else + coeffs[it->second] += coeff; + } - using std::swap; - swap(operands_, new_children); - swap(signs_, new_ops); + std::vector, bool>> out; + out.reserve(cores.size() + 1); + for (std::size_t ii = 0; ii < cores.size(); ++ii) + { + mpq_rational c = coeffs[ii]; + if (c == 0) // x - x -> 0 (the term cancels) + continue; + const bool positive = c > 0; + mpq_rational mag = positive ? c : -c; + std::shared_ptr term = (mag == 1) + ? cores[ii] + : SimplifiedMult({ {RationalToNode(mag), true}, {cores[ii], true} }); + out.emplace_back(term, positive); + } + if (constant_sum != 0) + { + const bool positive = constant_sum > 0; + out.emplace_back(RationalToNode(positive ? constant_sum : -constant_sum), positive); } - // recurse over the remaining children - for (auto& iter : operands_) - num_eliminated += iter->EliminateZeros(); + if (out.empty()) + return Zero(); + if (out.size() == 1) + return out[0].second ? out[0].first : SimplifiedNegate(out[0].first); - return num_eliminated; + return SumOperator::Make(out); } - -unsigned SumOperator::EliminateOnes() -{ - unsigned num_eliminated{0}; - for (auto& iter : operands_) - num_eliminated += iter->EliminateOnes(); - - return num_eliminated; +namespace{ + // flatten nested Mult factors into one list so constants can meet and fold + // (3*(2*x) -> {3,2,x} -> 6*x). reads the nested node's operands; never + // modifies it. a divided nested Mult inverts its flags: x/(a/b) = x/a*b. + void FlattenFactor(std::vector, bool>>& out, + std::shared_ptr const& n, bool mult) + { + if (auto as_mult = std::dynamic_pointer_cast(n)) + { + auto const& ops = as_mult->Operands(); + auto const& flags = as_mult->GetMultOrDiv(); + for (size_t ii = 0; ii < ops.size(); ++ii) + FlattenFactor(out, ops[ii], mult ? flags[ii] : !flags[ii]); + return; + } + out.emplace_back(n, mult); + } } -unsigned SumOperator::ReduceSubSums() +std::shared_ptr SimplifiedMult(std::vector, bool>> const& factors_in) { - std::vector> new_children; - std::vector new_ops; - unsigned num_eliminated{0}; + std::vector, bool>> factors; + factors.reserve(factors_in.size()); + for (auto const& f : factors_in) + FlattenFactor(factors, f.first, f.second); + + mpq_rational constant(1); + bool have_constant = false; + std::vector, bool>> remaining; + remaining.reserve(factors.size()); - for (unsigned ii=0; ii(operands_[ii]); - if (converted) - { // we have a sum! reduce it into this one - for (unsigned jj=0; jjNumOperands(); ++jj) + auto const& n = f.first; + const bool mult = f.second; + + if (n->IsLiteralZero()) + { + if (mult) + return Zero(); + remaining.push_back(f); // division by a literal zero stays visible + continue; + } + if (n->IsLiteralOne()) + continue; + + // fold exact constants together; Floats are deliberately NOT folded + if (auto as_int = std::dynamic_pointer_cast(n)) + { + mpq_rational val(as_int->GetValue()); + if (mult) + constant *= val; + else + constant /= val; + have_constant = true; + continue; + } + if (auto as_rat = std::dynamic_pointer_cast(n)) + { + if (as_rat->GetValueImag() == 0) { - new_children.push_back(converted->operands_[jj]); - new_ops.push_back(!(converted->signs_[jj] ^ signs_[ii])); - num_eliminated++; + if (mult) + constant *= as_rat->GetValueReal(); + else + constant /= as_rat->GetValueReal(); + have_constant = true; + continue; } - + } + remaining.push_back(f); + } + + // combine like factors into powers: x*x -> x^2, x^a * x^b -> x^(a+b); because identical + // subexpressions are one interned node, this also folds e.g. (x+y)*(x+y) -> (x+y)^2. group + // the non-constant factors by base identity (pointer), summing exponents (division counts + // negative), then re-emit one power per base. + std::vector> bases; // representative base, first-seen order + std::map base_index; + std::vector exponents; + for (auto const& f : remaining) + { + std::shared_ptr base; + long e; + if (auto as_pow = std::dynamic_pointer_cast(f.first)) + { + base = as_pow->Operand(); + e = as_pow->exponent(); } else { - new_children.push_back(this->operands_[ii]); - new_ops.push_back(this->signs_[ii]); + base = f.first; + e = 1; } - } - swap(this->operands_, new_children); - swap(this->signs_, new_ops); - return num_eliminated; -} + if (!f.second) // a divided factor lowers the exponent + e = -e; + auto it = base_index.find(base.get()); + if (it == base_index.end()) + { + base_index.emplace(base.get(), bases.size()); + bases.push_back(base); + exponents.push_back(e); + } + else + exponents[it->second] += e; + } -unsigned SumOperator::ReduceSubMults() -{ - std::vector> new_children; - std::vector new_ops; - unsigned num_eliminated{0}; + std::vector, bool>> combined; + combined.reserve(bases.size()); + for (std::size_t ii = 0; ii < bases.size(); ++ii) + { + const long e = exponents[ii]; + if (e == 0) // x/x -> 1 (the factor drops out) + continue; + const long mag = std::labs(e); + std::shared_ptr factor = (mag == 1) + ? bases[ii] + : std::static_pointer_cast(IntegerPowerOperator::Make(bases[ii], static_cast(mag))); + combined.emplace_back(factor, e > 0); + } - for (unsigned ii=0; ii constant_node = nullptr; + if (have_constant && constant != 1) { - auto converted = std::dynamic_pointer_cast(operands_[ii]); - if (converted && converted->NumOperands()==1 && converted->mult_or_div_[0]) - { // we have a multiply node! if its a single node and is mult, not div, then its operand can be folded into this sum. - new_children.push_back(converted->operands_[0]); - new_ops.push_back(signs_[ii]); - num_eliminated++; - } + if (denominator(constant) == 1) + constant_node = Integer::Make(numerator(constant)); else - { - new_children.push_back(this->operands_[ii]); - new_ops.push_back(this->signs_[ii]); - } + constant_node = Rational::Make(constant, mpq_rational(0)); } - swap(this->operands_, new_children); - swap(this->signs_, new_ops); + if (combined.empty()) + return constant_node ? constant_node : std::shared_ptr(One()); + + std::vector, bool>> finals; + if (constant_node) + finals.emplace_back(constant_node, true); // canonical order: constant first + finals.insert(finals.end(), combined.begin(), combined.end()); + + if (finals.size() == 1 && finals[0].second) + return finals[0].first; + + // for a leading divisor, materialize the canonical '1/...' form + if (!finals[0].second) + finals.insert(finals.begin(), {One(), true}); + + return MultOperator::Make(finals); // build the complete product, then intern once +} + +// ---- functional Simplified() (non-mutating successors to Eliminate*/ReduceDepth) ---- +// Each recurses on children (which return fresh simplified subtrees, sharing what they +// didn't change) and re-assembles through the Simplified* factories, so literal zeros/ones +// vanish, exact constants fold, and nested same-type operators flatten. - return num_eliminated; +std::shared_ptr SumOperator::Simplified() const +{ + std::vector, bool>> terms; + terms.reserve(operands_.size()); + for (size_t ii = 0; ii < operands_.size(); ++ii) + terms.emplace_back(operands_[ii]->Simplified(), signs_[ii]); + return SimplifiedSum(terms); } +std::shared_ptr MultOperator::Simplified() const +{ + std::vector, bool>> factors; + factors.reserve(operands_.size()); + for (size_t ii = 0; ii < operands_.size(); ++ii) + factors.emplace_back(operands_[ii]->Simplified(), mult_or_div_[ii]); + return SimplifiedMult(factors); +} + +std::shared_ptr NegateOperator::Simplified() const +{ + return SimplifiedNegate(operand_->Simplified()); +} + +std::shared_ptr PowerOperator::Simplified() const +{ + auto base_s = base_->Simplified(); + auto exp_s = exponent_->Simplified(); + if (exp_s->IsLiteralZero()) return Integer::Make(1); // x^0 -> 1 + if (exp_s->IsLiteralOne()) return base_s; // x^1 -> x + return PowerOperator::Make(base_s, exp_s); +} + +std::shared_ptr IntegerPowerOperator::Simplified() const +{ + if (exponent_ == 0) return Integer::Make(1); // x^0 -> 1 + auto op_s = operand_->Simplified(); + if (exponent_ == 1) return op_s; // x^1 -> x + return IntegerPowerOperator::Make(op_s, exponent_); +} -unsigned SumOperator::ReduceDepth() +std::shared_ptr SqrtOperator::Simplified() const { - auto num_eliminated = ReduceSubSums() + ReduceSubMults(); + return SqrtOperator::Make(operand_->Simplified()); +} - for (auto& iter : operands_) - num_eliminated += iter->ReduceDepth(); +std::shared_ptr ExpOperator::Simplified() const +{ + return ExpOperator::Make(operand_->Simplified()); +} - return num_eliminated; +std::shared_ptr LogOperator::Simplified() const +{ + return LogOperator::Make(operand_->Simplified()); } void SumOperator::print(std::ostream & target) const { - target << "("; - for (auto iter = operands_.begin(); iter!= operands_.end(); iter++) { - if (iter==operands_.begin()) { - // on the first iteration, no need to put a + if a + - if ( !(*(signs_.begin()+(iter-operands_.begin()))) ) - target << "-"; - } - else + // Print a positive term first. Canonical ordering sorts by degree, which can put + // a subtracted term ahead of a constant ("1-t" -> operands t,1), and leading with that minus + // reads as an extra negation ("-t+1"). Leading with a '+' term gives the natural, fewer-ops + // form ("1-t", "x-(y+z)"). (Print order only; the canonical operand order is unchanged. An + // all-negative sum still leads with '-', e.g. "-x-y".) + size_t lead = 0; + for (size_t ii = 0; ii < operands_.size(); ++ii) + if (signs_[ii]) { lead = ii; break; } + + auto print_one = [&](size_t ii, bool is_lead) + { + const bool plus = signs_[ii]; + if (is_lead) { - if ( !(*(signs_.begin()+(iter-operands_.begin()))) ) + if (!plus) target << "-"; - else - target << "+"; } - (*iter)->print(target); - - } - target << ")"; + else + target << (plus ? "+" : "-"); + + const auto prec = operands_[ii]->Precedence(); + // after '-', wrap sums (grouping) and anything printing a leading '-' + // (avoids "--"); after '+' or in the lead, wrap only leading-'-' printers + bool needs_parens; + if (is_lead) + needs_parens = plus ? false : (prec <= PrecNegate); + else + needs_parens = plus ? (prec == PrecNegate) : (prec <= PrecNegate); + PrintOperand(target, operands_[ii], needs_parens); + }; + + print_one(lead, true); + for (size_t ii = 0; ii < operands_.size(); ++ii) + if (ii != lead) + print_one(ii, false); } std::shared_ptr SumOperator::Differentiate(std::shared_ptr const& v) const { - unsigned int counter = 0; - std::shared_ptr ret_sum = Zero(); - for (int ii = 0; ii < operands_.size(); ++ii) + std::vector, bool>> terms; + terms.reserve(operands_.size()); + for (size_t ii = 0; ii < operands_.size(); ++ii) { - auto converted = std::dynamic_pointer_cast(operands_[ii]); - if (converted) - continue; - - auto temp_node = operands_[ii]->Differentiate(v); - converted = std::dynamic_pointer_cast(temp_node); - if (converted) - if (converted->Eval()==dbl(0.0)) - continue; - - counter++; - if (counter==1) - ret_sum = SumOperator::Make(temp_node,signs_[ii]); - else - std::dynamic_pointer_cast(ret_sum)->AddOperand(temp_node,signs_[ii]); - + if (std::dynamic_pointer_cast(operands_[ii])) + continue; // constants differentiate to 0; don't even build it + + terms.emplace_back(operands_[ii]->Differentiate(v), signs_[ii]); } - - return ret_sum; + return SimplifiedSum(terms); } int SumOperator::Degree(std::shared_ptr const& v) const @@ -261,56 +493,45 @@ std::vector SumOperator::MultiDegree(VariableGroup const& vars) const return deg; } -void SumOperator::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) +std::shared_ptr SumOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const { - - - - // first homogenize each summand. - for (auto iter: operands_) - { - iter->Homogenize(vars, homvar); - } - - // then, homogenize this sum. - - // compute the highest degree among all summands. + // homogenize each summand functionally (fresh subtrees), measure degrees, then pad + // degree-deficient summands with powers of homvar -- all into a freshly-built sum. + // the input is never touched, and the throw below happens before anything is built, + // so a non-polynomial term can't leave a half-homogenized tree behind. + std::vector> new_ops; + new_ops.reserve(operands_.size()); + for (auto const& op : operands_) + new_ops.push_back(op->Homogenized(vars, homvar)); + int maxdegree = 0; std::vector term_degrees; - // first homogenize each summand. - for (auto iter: operands_) + term_degrees.reserve(new_ops.size()); + for (auto const& op : new_ops) { - auto local_degree = iter->Degree(vars); - if (local_degree<0) + auto local_degree = op->Degree(vars); + if (local_degree < 0) throw std::runtime_error("asking for homogenization on non-polynomial node"); - // TODO: this throw would leave the tree in a partially homogenized state. this is scary. - term_degrees.push_back(local_degree); maxdegree = std::max(maxdegree, local_degree); } - - for (auto iter = operands_.begin(); iter!=operands_.end(); iter++) - { - auto degree_deficiency = maxdegree - *(term_degrees.begin() + (iter-operands_.begin())); - if ( degree_deficiency > 0) - { - // hold the operand temporarily. - if (degree_deficiency==1) - { - std::shared_ptr M = MultOperator::Make(homvar,std::dynamic_pointer_cast(*iter)); - swap(*iter,M); - } - else{ - std::shared_ptr P = IntegerPowerOperator::Make(std::dynamic_pointer_cast(homvar),degree_deficiency); - std::shared_ptr M = MultOperator::Make(P,std::dynamic_pointer_cast(*iter)); - swap(*iter,M); - } - - - } + for (size_t ii = 0; ii < new_ops.size(); ++ii) + { + auto degree_deficiency = maxdegree - term_degrees[ii]; + if (degree_deficiency == 1) + new_ops[ii] = MultOperator::Make(homvar, new_ops[ii]); + else if (degree_deficiency > 1) + new_ops[ii] = MultOperator::Make( + IntegerPowerOperator::Make(std::static_pointer_cast(homvar), degree_deficiency), + new_ops[ii]); } - + + std::vector, bool>> terms; + terms.reserve(new_ops.size()); + for (size_t ii = 0; ii < new_ops.size(); ++ii) + terms.emplace_back(new_ops[ii], signs_[ii]); + return SumOperator::Make(terms); // complete sum, interned once } @@ -371,66 +592,12 @@ bool SumOperator::IsHomogeneous(VariableGroup const& v) const -dbl SumOperator::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - dbl retval; - this->FreshEval_d(retval, diff_variable); - return retval; -} -void SumOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = dbl(0); - for(int ii = 0; ii < operands_.size(); ++ii) - { - if(signs_[ii]) - { - operands_[ii]->EvalInPlace(temp_d_, diff_variable); - evaluation_value += temp_d_; - } - else - { - operands_[ii]->EvalInPlace(temp_d_, diff_variable); - evaluation_value -= temp_d_; - } - } -} -mpfr_complex SumOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - mpfr_complex retval; - this->FreshEval_mp(retval, diff_variable); - return retval; -} - -void SumOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - if (signs_[0]) - operands_[0]->EvalInPlace(evaluation_value, diff_variable); - else - { - operands_[0]->EvalInPlace(temp_mp_, diff_variable); - evaluation_value = -temp_mp_; - } - for(int ii = 1; ii < operands_.size(); ++ii) - { - if(signs_[ii]) - { - operands_[ii]->EvalInPlace(temp_mp_, diff_variable); - evaluation_value += temp_mp_; - } - else - { - operands_[ii]->EvalInPlace(temp_mp_, diff_variable); - evaluation_value -= temp_mp_; - } - } - -} @@ -455,49 +622,21 @@ void SumOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptrprint(target); - target << ")"; + target << "-"; + PrintOperand(target, operand_, operand_->Precedence() <= PrecNegate); } std::shared_ptr NegateOperator::Differentiate(std::shared_ptr const& v) const { - return NegateOperator::Make(operand_->Differentiate(v)); + return SimplifiedNegate(operand_->Differentiate(v)); } -dbl NegateOperator::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return -(operand_->Eval(diff_variable)); -} -void NegateOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = -evaluation_value; -} -mpfr_complex NegateOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return -operand_->Eval(diff_variable); -} -void NegateOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = -evaluation_value; -} @@ -527,187 +666,33 @@ void NegateOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_pt // ////////////////////// -unsigned MultOperator::EliminateZeros() -{ - assert(!operands_.empty() && "operands_ must not be empty to eliminate zeros"); - - // find those zeros in the sum. then, compress. - std::vector non_zeros_ops; - - bool have_a_zero = false; - for (const auto& iter : operands_) - { - if (iter->Eval() == 0.) - { - have_a_zero = true; - break; - } - } - - if (have_a_zero) // if there is a single zero, the whole thing should collapse. - { - unsigned num_eliminated = operands_.size()-1; - operands_.clear(); mult_or_div_.clear(); - AddOperand(Integer::Make(0), true); - return num_eliminated; - } - - // recurse over the remaining children - unsigned num_eliminated{0}; - for (auto& iter : operands_) - num_eliminated += iter->EliminateZeros(); - - return num_eliminated; -} - -unsigned MultOperator::EliminateOnes() -{ - assert(!operands_.empty() && "operands_ must not be empty to eliminate ones"); - unsigned num_eliminated{0}; - if (operands_.size()>1) - { - std::vector is_one(operands_.size(),false); - - for (unsigned ii=0; iiEval()==1.0; - std::vector> new_children; - std::vector new_mult_div; - for (unsigned ii=0; iiEliminateOnes(); - - return num_eliminated; -} - - -unsigned MultOperator::ReduceSubSums() +void MultOperator::print(std::ostream & target) const { - std::vector> new_children; - std::vector new_ops; - unsigned num_eliminated{0}; - - for (unsigned ii=0; ii(operands_[ii]); - if (converted && converted->NumOperands()==1) - { // we have a sum node! if its a single add node, then its operand can be folded into this sum. - if (converted->signs_[0]) - new_children.push_back(converted->operands_[0]); - else - new_children.push_back(-converted->operands_[0]); - - new_ops.push_back(mult_or_div_[ii]); - num_eliminated++; - } - else + const bool mult = mult_or_div_[ii]; + if (ii == 0) { - new_children.push_back(this->operands_[ii]); - new_ops.push_back(this->mult_or_div_[ii]); - } - } - - swap(this->operands_, new_children); - swap(this->mult_or_div_, new_ops); - - return num_eliminated; -} - -unsigned MultOperator::ReduceSubMults() -{ - std::vector> new_children; - std::vector new_ops; - unsigned num_eliminated{0}; - - for (unsigned ii=0; ii(operands_[ii]); - if (converted) - { // we have a multiply! reduce it into this one - for (unsigned jj=0; jjNumOperands(); ++jj) - { - new_children.push_back(converted->operands_[jj]); - new_ops.push_back(!(converted->mult_or_div_[jj] ^ this->mult_or_div_[ii])); - num_eliminated++; - } - + if (!mult) + target << "1/"; } else - { - new_children.push_back(this->operands_[ii]); - new_ops.push_back(this->mult_or_div_[ii]); - } - } - - swap(this->operands_, new_children); - swap(this->mult_or_div_, new_ops); - - return num_eliminated; -} - - -unsigned MultOperator::ReduceDepth() -{ - auto num_eliminated = ReduceSubSums() + ReduceSubMults(); - - for (auto& iter : operands_) - num_eliminated += iter->ReduceDepth(); - - return num_eliminated; -} - + target << (mult ? "*" : "/"); - -void MultOperator::print(std::ostream & target) const -{ - target << "("; - for (auto iter = operands_.begin(); iter!= operands_.end(); iter++) { - if (iter==operands_.begin()) - if (! *mult_or_div_.begin() ) - target << "1/"; - (*iter)->print(target); - if (iter!=(operands_.end()-1)){ - if (*(mult_or_div_.begin() + (iter-operands_.begin())+1)) { // TODO i think this +1 is wrong... dab - target << "*"; - } - else{ - target << "/"; - } - - } + const auto prec = operands_[ii]->Precedence(); + // multiplied positions: wrap below-mult precedence (sums, leading-'-' + // printers); divided positions: also wrap other mults (grouping) + const bool needs_parens = mult ? (prec < PrecMult) : (prec <= PrecMult); + PrintOperand(target, operands_[ii], needs_parens); } - target << ")"; } @@ -715,49 +700,31 @@ void MultOperator::print(std::ostream & target) const std::shared_ptr MultOperator::Differentiate(std::shared_ptr const& v) const { - std::shared_ptr ret_sum = node::Zero(); - - unsigned term_counter {0}; + std::vector, bool>> sum_terms; // this loop implements the generic product rule, perhaps inefficiently. - for (int ii = 0; ii < operands_.size(); ++ii) + for (size_t ii = 0; ii < operands_.size(); ++ii) { auto local_derivative = operands_[ii]->Differentiate(v); - - // if the derivative of the current term is 0, then stop - auto is_it_a_number = std::dynamic_pointer_cast(local_derivative); - if (is_it_a_number) - if (is_it_a_number->Eval()==dbl(0.0)) - continue; - - // no, the term's derivative is not 0. - - // create the product of the remaining terms - auto term_ii = MultOperator::Make(local_derivative); - for (int jj = 0; jj < operands_.size(); ++jj) - { - if(jj != ii) - term_ii->AddOperand(operands_[jj],mult_or_div_[jj]); - } - - // and then either 1. multiply it against the current derivative, or 2. invoke the quotient rule. - if (is_it_a_number) - if (is_it_a_number->Eval()==dbl(1.0)) - continue; - - // if the derivative of the term under consideration is equal to 1, no need to go on. already have the product we need. - + if (local_derivative->IsLiteralZero()) + continue; + + // the product of the derivative with the remaining factors + std::vector, bool>> factors; + factors.reserve(operands_.size() + 1); + factors.emplace_back(local_derivative, true); + for (size_t jj = 0; jj < operands_.size(); ++jj) + if (jj != ii) + factors.emplace_back(operands_[jj], mult_or_div_[jj]); + // if is division, need this for the quotient rule if ( !(mult_or_div_[ii]) ) - term_ii->AddOperand(pow(operands_[ii],2),false); // draw a line and square below - - term_counter++; - if (term_counter==1) - ret_sum = SumOperator::Make(term_ii,mult_or_div_[ii]); - else - std::dynamic_pointer_cast(ret_sum)->AddOperand(term_ii,mult_or_div_[ii]); + factors.emplace_back(pow(operands_[ii],2), false); // draw a line and square below + + // a divided factor's term enters subtracted (quotient rule) + sum_terms.emplace_back(SimplifiedMult(factors), mult_or_div_[ii]); } // re: for ii - - return ret_sum; + + return SimplifiedSum(sum_terms); } int MultOperator::Degree(std::shared_ptr const& v) const @@ -829,12 +796,21 @@ std::vector MultOperator::MultiDegree(VariableGroup const& vars) const -void MultOperator::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) +std::shared_ptr MultOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const { - for (auto iter: operands_) - { - iter->Homogenize(vars, homvar); - } + // product of homogenized factors, preserving the multiply/divide flags (no simplification). + std::vector> ops; + ops.reserve(operands_.size()); + for (auto const& op : operands_) + ops.push_back(op->Homogenized(vars, homvar)); + + std::vector, bool>> factors; + factors.reserve(ops.size() + 1); + if (!mult_or_div_[0]) + factors.emplace_back(One(), true); // leading divisor -> canonical 1/... + for (size_t ii = 0; ii < ops.size(); ++ii) + factors.emplace_back(ops[ii], mult_or_div_[ii]); + return MultOperator::Make(factors); // complete product, interned once } @@ -864,60 +840,10 @@ bool MultOperator::IsHomogeneous(VariableGroup const& v) const return true; } -dbl MultOperator::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - dbl retval; - this->FreshEval_d(retval, diff_variable); - return retval; -} - -void MultOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = dbl(1); - for(int ii = 0; ii < operands_.size(); ++ii) - { - if(mult_or_div_[ii]) - { - operands_[ii]->EvalInPlace(temp_d_, diff_variable); - evaluation_value *= temp_d_; - } - else - { - operands_[ii]->EvalInPlace(temp_d_, diff_variable); - evaluation_value /= temp_d_; - } - } - -} -mpfr_complex MultOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - mpfr_complex retval; - this->FreshEval_mp(retval, diff_variable); - return retval; -} -void MultOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - if (mult_or_div_[0]) - operands_[0]->EvalInPlace(evaluation_value, diff_variable); - else - { - operands_[0]->EvalInPlace(temp_mp_, diff_variable); - evaluation_value = static_cast(1)/temp_mp_; - } - for(int ii = 1; ii < operands_.size(); ++ii) - { - operands_[ii]->EvalInPlace(temp_mp_, diff_variable); - if(mult_or_div_[ii]) - evaluation_value *= temp_mp_; - else - evaluation_value /= temp_mp_; - } - -} @@ -928,40 +854,43 @@ void MultOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr< ///////////////// -unsigned PowerOperator::EliminateZeros() -{ - return 0; -} -unsigned PowerOperator::EliminateOnes() -{ - return 0; -} - -void PowerOperator::Reset() const -{ - Node::ResetStoredValues(); - base_->Reset(); - exponent_->Reset(); -} void PowerOperator::print(std::ostream & target) const { - target << "(" << *base_ << ")^(" << *exponent_ << ")"; + // '^' is right-associative and binds tightest, so wrap anything that is + // not an atom -- including other powers, to keep x^y^z unambiguous + PrintOperand(target, base_, base_->Precedence() <= PrecPower); + target << "^"; + PrintOperand(target, exponent_, exponent_->Precedence() <= PrecPower); } std::shared_ptr PowerOperator::Differentiate(std::shared_ptr const& v) const { - auto exp_minus_one = exponent_-1; - auto ret_mult = MultOperator::Make(base_->Differentiate(v)); - ret_mult->AddOperand(exponent_); - ret_mult->AddOperand(PowerOperator::Make(base_, exp_minus_one)); - return ret_mult; + return SimplifiedMult({ + {base_->Differentiate(v), true}, + {exponent_, true}, + {PowerOperator::Make(base_, exp_minus_one), true} + }); } +namespace { + // The double value of a constant (degree-0) exponent node, for testing whether a PowerOperator's + // constant exponent is a non-negative integer. Returns NaN for anything that is not a plain + // numeric literal, so the integer test fails (the conservative answer). Node evaluation is gone, + // so this reads the literal directly rather than evaluating. + dbl ConstantExponentValue(std::shared_ptr const& n) + { + if (auto i = std::dynamic_pointer_cast(n)) return dbl(double(i->GetValue()), 0); + if (auto f = std::dynamic_pointer_cast(n)) return dbl(f->GetValue()); + if (auto r = std::dynamic_pointer_cast(n)) return r->Value(); + return dbl(std::numeric_limits::quiet_NaN(), 0); + } +} + int PowerOperator::Degree(std::shared_ptr const& v) const { @@ -970,7 +899,7 @@ int PowerOperator::Degree(std::shared_ptr const& v) const if (exp_deg==0) { - auto exp_val = exponent_->Eval(); + dbl exp_val = ConstantExponentValue(exponent_); bool exp_is_int = false; if (fabs(imag(exp_val))< 10*std::numeric_limits::epsilon()) // so a real thresholding step @@ -988,7 +917,7 @@ int PowerOperator::Degree(std::shared_ptr const& v) const if (base_deg<0) return -1; else - return base_deg*std::round(real(exp_val)); + return base_deg*static_cast(std::round(real(exp_val))); } } @@ -1034,16 +963,109 @@ std::vector PowerOperator::MultiDegree(VariableGroup const& vars) const -void PowerOperator::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) +std::shared_ptr PowerOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const +{ + if (exponent_->Degree(vars) == 0) + return PowerOperator::Make(base_->Homogenized(vars, homvar), exponent_); + // non-constant exponent -> non-polynomial; throw before building anything. + throw std::runtime_error("asking for homogenization on non-polynomial node"); +} + +// ---- unary operators: rebuild the same op type from the homogenized operand ---- +std::shared_ptr NegateOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const +{ + return NegateOperator::Make(operand_->Homogenized(vars, homvar)); +} + +std::shared_ptr IntegerPowerOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const +{ + return IntegerPowerOperator::Make(operand_->Homogenized(vars, homvar), exponent_); +} + +std::shared_ptr SqrtOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const +{ + return SqrtOperator::Make(operand_->Homogenized(vars, homvar)); +} + +std::shared_ptr ExpOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const +{ + return ExpOperator::Make(operand_->Homogenized(vars, homvar)); +} + +std::shared_ptr LogOperator::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const { - if (exponent_->Degree(vars)==0) + return LogOperator::Make(operand_->Homogenized(vars, homvar)); +} + +// ---- structural hash / equality ---- +// Order-sensitive; operands folded in by Hash() for the hash, compared by pointer for IsSame. + +std::size_t SumOperator::HashImpl() const +{ + std::size_t h = typeid(SumOperator).hash_code(); + for (size_t ii = 0; ii < operands_.size(); ++ii) { - base_->Homogenize(vars, homvar); + Node::HashCombine(h, operands_[ii]->Hash()); + Node::HashCombine(h, signs_[ii] ? 1u : 0u); } - else{ - throw std::runtime_error("asking for homogenization on non-polynomial node"); - //TODO: this will leave the system in a broken state, partially homogenized... + return h; +} +bool SumOperator::IsSame(Node const& other) const +{ + auto o = dynamic_cast(&other); + if (!o || operands_.size() != o->operands_.size() || signs_ != o->signs_) + return false; + for (size_t ii = 0; ii < operands_.size(); ++ii) + if (operands_[ii].get() != o->operands_[ii].get()) + return false; + return true; +} + +std::size_t MultOperator::HashImpl() const +{ + std::size_t h = typeid(MultOperator).hash_code(); + for (size_t ii = 0; ii < operands_.size(); ++ii) + { + Node::HashCombine(h, operands_[ii]->Hash()); + Node::HashCombine(h, mult_or_div_[ii] ? 1u : 0u); } + return h; +} +bool MultOperator::IsSame(Node const& other) const +{ + auto o = dynamic_cast(&other); + if (!o || operands_.size() != o->operands_.size() || mult_or_div_ != o->mult_or_div_) + return false; + for (size_t ii = 0; ii < operands_.size(); ++ii) + if (operands_[ii].get() != o->operands_[ii].get()) + return false; + return true; +} + +std::size_t PowerOperator::HashImpl() const +{ + std::size_t h = typeid(PowerOperator).hash_code(); + Node::HashCombine(h, base_->Hash()); + Node::HashCombine(h, exponent_->Hash()); + return h; +} +bool PowerOperator::IsSame(Node const& other) const +{ + auto o = dynamic_cast(&other); + return o && base_.get() == o->base_.get() && exponent_.get() == o->exponent_.get(); +} + +std::size_t IntegerPowerOperator::HashImpl() const +{ + std::size_t h = typeid(IntegerPowerOperator).hash_code(); + Node::HashCombine(h, operand_->Hash()); + Node::HashCombine(h, std::hash{}(exponent_)); + return h; +} +bool IntegerPowerOperator::IsSame(Node const& other) const +{ + auto o = dynamic_cast(&other); + return o && exponent_ == o->exponent_ && operand_.get() == o->operand_.get(); } bool PowerOperator::IsHomogeneous(std::shared_ptr const& v) const @@ -1051,7 +1073,7 @@ bool PowerOperator::IsHomogeneous(std::shared_ptr const& v) const // the only hope this has of being homogeneous, is that the degree of the exponent is 0 (it's constant), and that it's an integer if (exponent_->Degree(v)==0) { - auto exp_val = exponent_->Eval(); + dbl exp_val = ConstantExponentValue(exponent_); if (fabs(imag(exp_val)) < 10*std::numeric_limits::epsilon()) if (fabs(std::round(real(exp_val)) - real(exp_val)) < 10*std::numeric_limits::epsilon()) if (real(exp_val) >=0 ) @@ -1066,7 +1088,7 @@ bool PowerOperator::IsHomogeneous(VariableGroup const& v) const // the only hope this has of being homogeneous, is that the degree of the exponent is 0 (it's constant), and that it's an integer if (exponent_->Degree(v)==0) { - auto exp_val = exponent_->Eval(); + dbl exp_val = ConstantExponentValue(exponent_); if (fabs(imag(exp_val)) < 10*std::numeric_limits::epsilon()) if (fabs(std::round(real(exp_val)) - real(exp_val)) < 10*std::numeric_limits::epsilon()) if (real(exp_val) >=0 ) @@ -1075,34 +1097,10 @@ bool PowerOperator::IsHomogeneous(VariableGroup const& v) const return false; } -dbl PowerOperator::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return std::pow( base_->Eval(diff_variable), exponent_->Eval()); -} -void PowerOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - dbl temp_d; - exponent_->EvalInPlace(temp_d); - base_->EvalInPlace(evaluation_value, diff_variable); - - evaluation_value = std::pow(evaluation_value, temp_d); -} -mpfr_complex PowerOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return pow( base_->Eval(diff_variable), exponent_->Eval()); -} -void PowerOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - mpfr_complex temp_mp; - exponent_->EvalInPlace(temp_mp); - base_->EvalInPlace(evaluation_value, diff_variable); - - evaluation_value = pow(evaluation_value, temp_mp); -} @@ -1125,20 +1123,13 @@ void PowerOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr // //////////////////// -unsigned IntegerPowerOperator::EliminateZeros() -{ - return 0; -} -unsigned IntegerPowerOperator::EliminateOnes() -{ - return 0; -} - void IntegerPowerOperator::print(std::ostream & target) const { - target << "("; - operand_->print(target); - target << "^" << exponent() << ")"; + PrintOperand(target, operand_, operand_->Precedence() <= PrecPower); + if (exponent() < 0) + target << "^(" << exponent() << ")"; + else + target << "^" << exponent(); } @@ -1149,16 +1140,13 @@ std::shared_ptr IntegerPowerOperator::Differentiate(std::shared_ptrDifferentiate(v); - else if (exponent_==2){ - auto M = MultOperator::Make(Integer::Make(2), operand_); - M->AddOperand(operand_->Differentiate(v)); - return M; - } else{ - auto M = MultOperator::Make(Integer::Make(exponent_), - IntegerPowerOperator::Make(operand_, exponent_-1) ); - M->AddOperand(operand_->Differentiate(v)); - return M; + std::shared_ptr power_part = (exponent_==2) ? operand_ : std::shared_ptr(IntegerPowerOperator::Make(operand_, exponent_-1)); + return SimplifiedMult({ + {Integer::Make(exponent_), true}, + {power_part, true}, + {operand_->Differentiate(v), true} + }); } } @@ -1190,15 +1178,6 @@ int IntegerPowerOperator::Degree(std::shared_ptr const& v) const // ///////////////// -unsigned SqrtOperator::EliminateZeros() -{ - return 0; -} -unsigned SqrtOperator::EliminateOnes() -{ - return 0; -} - void SqrtOperator::print(std::ostream & target) const { target << "sqrt("; @@ -1210,10 +1189,11 @@ void SqrtOperator::print(std::ostream & target) const std::shared_ptr SqrtOperator::Differentiate(std::shared_ptr const& v) const { - auto ret_mult = MultOperator::Make(PowerOperator::Make(operand_, Rational::Make(mpq_rational(-1,2),0))); - ret_mult->AddOperand(operand_->Differentiate(v)); - ret_mult->AddOperand(Rational::Make(mpq_rational(1,2),0)); - return ret_mult; + return SimplifiedMult({ + {Rational::Make(mpq_rational(1,2),0), true}, + {PowerOperator::Make(operand_, Rational::Make(mpq_rational(-1,2),0)), true}, + {operand_->Differentiate(v), true} + }); } int SqrtOperator::Degree(std::shared_ptr const& v) const @@ -1229,28 +1209,10 @@ int SqrtOperator::Degree(std::shared_ptr const& v) const } -dbl SqrtOperator::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return sqrt(operand_->Eval(diff_variable)); -} -void SqrtOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = sqrt(evaluation_value); -} -mpfr_complex SqrtOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return sqrt(operand_->Eval(diff_variable)); -} -void SqrtOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = sqrt(evaluation_value); -} @@ -1272,15 +1234,6 @@ void SqrtOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr< // ////////////// -unsigned ExpOperator::EliminateZeros() -{ - return 0; -} -unsigned ExpOperator::EliminateOnes() -{ - return 0; -} - void ExpOperator::print(std::ostream & target) const { target << "exp("; @@ -1293,7 +1246,10 @@ void ExpOperator::print(std::ostream & target) const std::shared_ptr ExpOperator::Differentiate(std::shared_ptr const& v) const { - return exp(operand_)*operand_->Differentiate(v); + return SimplifiedMult({ + {exp(operand_), true}, + {operand_->Differentiate(v), true} + }); } @@ -1309,28 +1265,10 @@ int ExpOperator::Degree(std::shared_ptr const& v) const } } -dbl ExpOperator::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return exp(operand_->Eval(diff_variable)); -} -void ExpOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = exp(evaluation_value); -} -mpfr_complex ExpOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return exp(operand_->Eval(diff_variable)); -} -void ExpOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = exp(evaluation_value); -} @@ -1348,15 +1286,6 @@ void ExpOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr LogOperator::Differentiate(std::shared_ptr const& v) const { - return MultOperator::Make(operand_,false,operand_->Differentiate(v),true); + return SimplifiedMult({ + {operand_->Differentiate(v), true}, + {operand_, false} + }); } @@ -1384,28 +1316,10 @@ int LogOperator::Degree(std::shared_ptr const& v) const } } -dbl LogOperator::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return log(operand_->Eval(diff_variable)); -} -void LogOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = log(evaluation_value); -} -mpfr_complex LogOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return log(operand_->Eval(diff_variable)); -} -void LogOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = log(evaluation_value); -} } // re: namespace node diff --git a/core/src/function_tree/operators/operator.cpp b/core/src/function_tree/operators/operator.cpp index 23732d964..576ffd310 100644 --- a/core/src/function_tree/operators/operator.cpp +++ b/core/src/function_tree/operators/operator.cpp @@ -30,11 +30,6 @@ namespace bertini { namespace node{ -void UnaryOperator::Reset() const -{ - Node::ResetStoredValues(); - operand_->Reset(); -} void UnaryOperator::SetOperand(std::shared_ptr n) { @@ -82,9 +77,20 @@ std::vector UnaryOperator::MultiDegree(VariableGroup const& vars) const return deg; } -void UnaryOperator::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) + +std::size_t UnaryOperator::HashImpl() const { - operand_->Homogenize(vars, homvar); + std::size_t h = typeid(*this).hash_code(); // distinguishes Sin/Cos/Tan/Exp/Log/Sqrt/Negate/... + HashCombine(h, operand_->Hash()); + return h; +} + +bool UnaryOperator::IsSame(Node const& other) const +{ + if (typeid(*this) != typeid(other)) + return false; + // same concrete unary type -> safe to view as UnaryOperator and compare operand identity + return operand_.get() == static_cast(other).operand_.get(); } @@ -119,13 +125,6 @@ bool UnaryOperator::IsHomogeneous(VariableGroup const& vars) const \param prec the number of digits to change precision to. */ -void UnaryOperator::precision(unsigned int prec) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); - - operand_->precision(prec); -} @@ -135,13 +134,6 @@ void UnaryOperator::precision(unsigned int prec) const // //////////// -void NaryOperator::Reset() const -{ - Node::ResetStoredValues(); - for (const auto& ii : operands_) - ii->Reset(); - -} // Add an operand onto the container for this operator void NaryOperator::AddOperand(std::shared_ptr n) @@ -172,19 +164,9 @@ std::shared_ptr NaryOperator::FirstOperand() const \param prec the number of digits to change precision to. */ -void NaryOperator::precision(unsigned int prec) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); - - this->PrecisionChangeSpecific(prec); - - for (const auto& iter : operands_) - iter->precision(prec); -} -void NaryOperator::PrecisionChangeSpecific(unsigned prec) const +void NaryOperator::PrecisionChangeSpecific(unsigned /*prec*/) const {} diff --git a/core/src/function_tree/operators/trig.cpp b/core/src/function_tree/operators/trig.cpp index 8195bb65c..7b00a7126 100644 --- a/core/src/function_tree/operators/trig.cpp +++ b/core/src/function_tree/operators/trig.cpp @@ -43,15 +43,6 @@ namespace node{ } - unsigned SinOperator::EliminateZeros() - { - return 0; - } - unsigned SinOperator::EliminateOnes() - { - return 0; - } - void SinOperator::print(std::ostream & target) const { target << "sin("; @@ -62,41 +53,16 @@ namespace node{ std::shared_ptr SinOperator::Differentiate(std::shared_ptr const& v) const { - return cos(operand_) * operand_->Differentiate(v); + return SimplifiedMult({ + {cos(operand_), true}, + {operand_->Differentiate(v), true} + }); } - // Specific implementation of FreshEval for negate. - dbl SinOperator::FreshEval_d(std::shared_ptr const& diff_variable) const - { - return sin(operand_->Eval(diff_variable)); - } - mpfr_complex SinOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const - { - return sin(operand_->Eval(diff_variable)); - } - void SinOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = sin(evaluation_value); - } - void SinOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = sin(evaluation_value); - } - - unsigned ArcSinOperator::EliminateZeros() - { - return 0; - } - unsigned ArcSinOperator::EliminateOnes() - { - return 0; - } void ArcSinOperator::print(std::ostream & target) const { @@ -107,42 +73,17 @@ namespace node{ std::shared_ptr ArcSinOperator::Differentiate(std::shared_ptr const& v) const { - return operand_->Differentiate(v)/sqrt(1-pow(operand_,2)); + return SimplifiedMult({ + {operand_->Differentiate(v), true}, + {sqrt(1-pow(operand_,2)), false} + }); } - // Specific implementation of FreshEval for negate. - dbl ArcSinOperator::FreshEval_d(std::shared_ptr const& diff_variable) const - { - return asin(operand_->Eval(diff_variable)); - } - mpfr_complex ArcSinOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const - { - return asin(operand_->Eval(diff_variable)); - } - void ArcSinOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = asin(evaluation_value); - } - void ArcSinOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = asin(evaluation_value); - } - - unsigned CosOperator::EliminateZeros() - { - return 0; - } - unsigned CosOperator::EliminateOnes() - { - return 0; - } void CosOperator::print(std::ostream & target) const { @@ -153,41 +94,17 @@ namespace node{ std::shared_ptr CosOperator::Differentiate(std::shared_ptr const& v) const { - return -sin(operand_) * operand_->Differentiate(v); + return SimplifiedNegate(SimplifiedMult({ + {sin(operand_), true}, + {operand_->Differentiate(v), true} + })); } - dbl CosOperator::FreshEval_d(std::shared_ptr const& diff_variable) const - { - return cos(operand_->Eval(diff_variable)); - } - mpfr_complex CosOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const - { - return cos(operand_->Eval(diff_variable)); - } - void CosOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = cos(evaluation_value); - } - void CosOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = cos(evaluation_value); - } - - unsigned ArcCosOperator::EliminateZeros() - { - return 0; - } - unsigned ArcCosOperator::EliminateOnes() - { - return 0; - } void ArcCosOperator::print(std::ostream & target) const { @@ -199,42 +116,17 @@ namespace node{ std::shared_ptr ArcCosOperator::Differentiate(std::shared_ptr const& v) const { - return -operand_->Differentiate(v)/sqrt(1-pow(operand_,2)); + return SimplifiedNegate(SimplifiedMult({ + {operand_->Differentiate(v), true}, + {sqrt(1-pow(operand_,2)), false} + })); } - // Specific implementation of FreshEval for negate. - dbl ArcCosOperator::FreshEval_d(std::shared_ptr const& diff_variable) const - { - return acos(operand_->Eval(diff_variable)); - } - mpfr_complex ArcCosOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const - { - return acos(operand_->Eval(diff_variable)); - } - void ArcCosOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = acos(evaluation_value); - } - void ArcCosOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = acos(evaluation_value); - } - - unsigned TanOperator::EliminateZeros() - { - return 0; - } - unsigned TanOperator::EliminateOnes() - { - return 0; - } void TanOperator::print(std::ostream & target) const { @@ -245,42 +137,18 @@ namespace node{ std::shared_ptr TanOperator::Differentiate(std::shared_ptr const& v) const { - return operand_->Differentiate(v) / pow(cos(operand_),2); + return SimplifiedMult({ + {operand_->Differentiate(v), true}, + {pow(cos(operand_),2), false} + }); } - dbl TanOperator::FreshEval_d(std::shared_ptr const& diff_variable) const - { - return tan(operand_->Eval(diff_variable)); - } - mpfr_complex TanOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const - { - return tan(operand_->Eval(diff_variable)); - } - void TanOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = tan(evaluation_value); - } - void TanOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = tan(evaluation_value); - } - unsigned ArcTanOperator::EliminateZeros() - { - return 0; - } - unsigned ArcTanOperator::EliminateOnes() - { - return 0; - } - void ArcTanOperator::print(std::ostream & target) const { target << "atan("; @@ -291,30 +159,31 @@ namespace node{ std::shared_ptr ArcTanOperator::Differentiate(std::shared_ptr const& v) const { - return operand_->Differentiate(v) / (1 + pow(operand_,2)); + return SimplifiedMult({ + {operand_->Differentiate(v), true}, + {1 + pow(operand_,2), false} + }); } - dbl ArcTanOperator::FreshEval_d(std::shared_ptr const& diff_variable) const - { - return atan(operand_->Eval(diff_variable)); - } - mpfr_complex ArcTanOperator::FreshEval_mp(std::shared_ptr const& diff_variable) const - { - return atan(operand_->Eval(diff_variable)); - } - void ArcTanOperator::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = atan(evaluation_value); - } - void ArcTanOperator::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const - { - operand_->EvalInPlace(evaluation_value, diff_variable); - evaluation_value = atan(evaluation_value); - } -} // re: namespace node + + // ---- functional Simplified(): rebuild same op type from the simplified operand ---- + std::shared_ptr SinOperator::Simplified() const { return SinOperator::Make(operand_->Simplified()); } + std::shared_ptr ArcSinOperator::Simplified() const { return ArcSinOperator::Make(operand_->Simplified()); } + std::shared_ptr CosOperator::Simplified() const { return CosOperator::Make(operand_->Simplified()); } + std::shared_ptr ArcCosOperator::Simplified() const { return ArcCosOperator::Make(operand_->Simplified()); } + std::shared_ptr TanOperator::Simplified() const { return TanOperator::Make(operand_->Simplified()); } + std::shared_ptr ArcTanOperator::Simplified() const { return ArcTanOperator::Make(operand_->Simplified()); } + + // ---- functional Homogenized(): rebuild same op type from the homogenized operand ---- + std::shared_ptr SinOperator::Homogenized(VariableGroup const& v, std::shared_ptr const& h) const { return SinOperator::Make(operand_->Homogenized(v,h)); } + std::shared_ptr ArcSinOperator::Homogenized(VariableGroup const& v, std::shared_ptr const& h) const { return ArcSinOperator::Make(operand_->Homogenized(v,h)); } + std::shared_ptr CosOperator::Homogenized(VariableGroup const& v, std::shared_ptr const& h) const { return CosOperator::Make(operand_->Homogenized(v,h)); } + std::shared_ptr ArcCosOperator::Homogenized(VariableGroup const& v, std::shared_ptr const& h) const { return ArcCosOperator::Make(operand_->Homogenized(v,h)); } + std::shared_ptr TanOperator::Homogenized(VariableGroup const& v, std::shared_ptr const& h) const { return TanOperator::Make(operand_->Homogenized(v,h)); } + std::shared_ptr ArcTanOperator::Homogenized(VariableGroup const& v, std::shared_ptr const& h) const { return ArcTanOperator::Make(operand_->Homogenized(v,h)); } +} // re: namespace node } // re: bertini namespace diff --git a/core/src/function_tree/roots/function.cpp b/core/src/function_tree/roots/function.cpp deleted file mode 100644 index 51a5d76b3..000000000 --- a/core/src/function_tree/roots/function.cpp +++ /dev/null @@ -1,231 +0,0 @@ -//This file is part of Bertini 2. -// -//src/function_tree/roots/jacobian.cpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//src/function_tree/roots/jacobian.cpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with src/function_tree/roots/jacobian.cpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// silviana amethyst, university of notre dame -// Jeb Collins, West Texas A&M -// -// silviana amethyst -// UWEC -// Spring 2018 - - -#include "bertini2/function_tree/roots/jacobian.hpp" - - - - -namespace bertini{ -namespace node{ - - - -Handle::Handle(std::string const& new_name) : NamedSymbol(new_name) -{ } - - -Handle::Handle(const std::shared_ptr & entry, std::string const& name) : NamedSymbol(name), entry_node_(entry) -{ -} - - - -/** - Calls FreshEval on the entry node to the tree. - */ -dbl Handle::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return entry_node_->Eval(diff_variable); -} - -/** - Calls FreshEval in place on the entry node to the tree. - */ -void Handle::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - entry_node_->EvalInPlace(evaluation_value, diff_variable); -} - - -/** - Calls FreshEval on the entry node to the tree. - */ -mpfr_complex Handle::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return entry_node_->Eval(diff_variable); -} - -/** - Calls FreshEval in place on the entry node to the tree. - */ -void Handle::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - entry_node_->EvalInPlace(evaluation_value, diff_variable); -} - - - - - - - - -const std::shared_ptr & Handle::EntryNode() const -{ - assert (entry_node_!=nullptr); - - return entry_node_; -} - - -void Handle::print(std::ostream & target) const -{ - EnsureNotEmpty(); - NamedSymbol::print(target); - target << " function ("; - - entry_node_->print(target); - target << ")"; -} - - -void Handle::Reset() const -{ - EnsureNotEmpty(); - - Node::ResetStoredValues(); - entry_node_->Reset(); -} - - -void Handle::SetRoot(std::shared_ptr const& entry) -{ - entry_node_ = entry; -} - -void Handle::EnsureNotEmpty() const -{ - if (entry_node_==nullptr) - { - throw std::runtime_error("Function node type has empty root node"); - } -} - -std::shared_ptr Handle::Differentiate(std::shared_ptr const& v) const -{ - return entry_node_->Differentiate(v); -} - -/** -Compute the degree of a node. For functions, the degree is the degree of the entry node. -*/ -int Handle::Degree(std::shared_ptr const& v) const -{ - return entry_node_->Degree(v); -} - - -int Handle::Degree(VariableGroup const& vars) const -{ - return entry_node_->Degree(vars); -} - -std::vector Handle::MultiDegree(VariableGroup const& vars) const -{ - - std::vector deg(vars.size()); - for (auto iter = vars.begin(); iter!= vars.end(); ++iter) - { - *(deg.begin()+(iter-vars.begin())) = this->Degree(*iter); - } - return deg; -} - - -void Handle::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) -{ - entry_node_->Homogenize(vars, homvar); -} - -bool Handle::IsHomogeneous(std::shared_ptr const& v) const -{ - return entry_node_->IsHomogeneous(v); -} - -/** -Check for homogeneity, with respect to a variable group. -*/ -bool Handle::IsHomogeneous(VariableGroup const& vars) const -{ - return entry_node_->IsHomogeneous(vars); -} - - -/** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ -void Handle::precision(unsigned int prec) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - if (val_pair.first.precision()==prec) - return; - else{ - val_pair.first.precision(prec); - entry_node_->precision(prec); - } - -} - - - - - - - - - - - - - - - - -Function::Function(std::string const& new_name) : Handle(new_name) -{ } - - -Function::Function(const std::shared_ptr & entry, std::string const& name) : Handle(entry, name) -{ -} - - - - - - -} // re: namespace node -} // re: namespace bertini - - - diff --git a/core/src/function_tree/roots/jacobian.cpp b/core/src/function_tree/roots/jacobian.cpp deleted file mode 100644 index 05d3b6a71..000000000 --- a/core/src/function_tree/roots/jacobian.cpp +++ /dev/null @@ -1,93 +0,0 @@ -//This file is part of Bertini 2. -// -//src/function_tree/roots/jacobian.cpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//src/function_tree/roots/jacobian.cpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with src/function_tree/roots/jacobian.cpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// silviana amethyst, university of notre dame -// Jeb Collins, West Texas A&M - - -#include "bertini2/function_tree/roots/jacobian.hpp" - - - - -namespace bertini{ - namespace node{ - -Jacobian::Jacobian(const std::shared_ptr & entry) : Handle(entry) -{ -} - - -// Evaluate the node. If flag false, just return value, if flag true -// run the specific FreshEval of the node, then set flag to false. -template -T Jacobian::EvalJ(std::shared_ptr const& diff_variable) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - - if(diff_variable == current_diff_variable_ && val_pair.second) - return val_pair.first; - else - { - current_diff_variable_ = diff_variable; - Reset(); - detail::FreshEvalSelector::RunInPlace(val_pair.first, *this, diff_variable); - val_pair.second = true; - return val_pair.first; - } -} - -template dbl Jacobian::EvalJ(std::shared_ptr const& diff_variable) const; -template mpfr_complex Jacobian::EvalJ(std::shared_ptr const& diff_variable) const; - - - -template -void Jacobian::EvalJInPlace(T& eval_value, std::shared_ptr const& diff_variable) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - - if(diff_variable == current_diff_variable_ && val_pair.second) - eval_value = val_pair.first; - else - { - current_diff_variable_ = diff_variable; - Reset(); - detail::FreshEvalSelector::RunInPlace(val_pair.first,*this,diff_variable); - val_pair.second = true; - eval_value = val_pair.first; - } -} - -template void Jacobian::EvalJInPlace( dbl&, std::shared_ptr const& diff_variable) const; -template void Jacobian::EvalJInPlace( mpfr_complex&, std::shared_ptr const& diff_variable) const; - - - - - - - } // re: namespace node -} // re: namespace bertini - - - diff --git a/core/src/function_tree/roots/named_expression.cpp b/core/src/function_tree/roots/named_expression.cpp new file mode 100644 index 000000000..adc3f75bc --- /dev/null +++ b/core/src/function_tree/roots/named_expression.cpp @@ -0,0 +1,107 @@ +//This file is part of Bertini 2. +// +//src/function_tree/roots/named_expression.cpp is free software: you can redistribute it and/or +//modify it under the terms of the GNU General Public License as published by the Free Software +//Foundation, either version 3 of the License, or (at your option) any later version. +// +//src/function_tree/roots/named_expression.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +//PARTICULAR PURPOSE. See the GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License along with +//src/function_tree/roots/named_expression.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire +// Jeb Collins, West Texas A&M + + +#include "bertini2/function_tree.hpp" + +namespace bertini{ +namespace node{ + + + + + + + + +const std::shared_ptr & NamedExpression::EntryNode() const +{ + assert (entry_node_!=nullptr); + + return entry_node_; +} + + + + +void NamedExpression::EnsureNotEmpty() const +{ + if (entry_node_==nullptr) + { + throw std::runtime_error("NamedExpression node type has empty entry node"); + } +} + +std::shared_ptr NamedExpression::Differentiate(std::shared_ptr const& v) const +{ + return entry_node_->Differentiate(v); +} + +int NamedExpression::Degree(std::shared_ptr const& v) const +{ + return entry_node_->Degree(v); +} + + +int NamedExpression::Degree(VariableGroup const& vars) const +{ + return entry_node_->Degree(vars); +} + +std::vector NamedExpression::MultiDegree(VariableGroup const& vars) const +{ + + std::vector deg(vars.size()); + for (auto iter = vars.begin(); iter!= vars.end(); ++iter) + { + *(deg.begin()+(iter-vars.begin())) = this->Degree(*iter); + } + return deg; +} + + +std::shared_ptr NamedExpression::Homogenized(VariableGroup const& vars, std::shared_ptr const& homvar) const +{ + auto homogenized_entry = entry_node_->Homogenized(vars, homvar); + if (homogenized_entry == entry_node_) + return std::const_pointer_cast(shared_from_this()); // unchanged -- share + // functional: a fresh NamedExpression wrapping the homogenized entry; the original is + // untouched, so a user (or another System) holding this node never observes it change. + return NamedExpression::Make(homogenized_entry, name()); +} + +bool NamedExpression::IsHomogeneous(std::shared_ptr const& v) const +{ + return entry_node_->IsHomogeneous(v); +} + +bool NamedExpression::IsHomogeneous(VariableGroup const& vars) const +{ + return entry_node_->IsHomogeneous(vars); +} + + + + +} // re: namespace node +} // re: namespace bertini diff --git a/core/src/function_tree/simplify.cpp b/core/src/function_tree/simplify.cpp index 8c903396b..43e5edf65 100644 --- a/core/src/function_tree/simplify.cpp +++ b/core/src/function_tree/simplify.cpp @@ -30,25 +30,9 @@ namespace bertini { -unsigned Simplify(std::shared_ptr const& n) +std::shared_ptr Simplify(std::shared_ptr const& n) { - unsigned num_reductions = 0; - unsigned num_ones = 0; - unsigned num_zeros = 0; - - num_reductions = n->ReduceDepth(); - num_ones = n->EliminateOnes(); - num_zeros = n->EliminateZeros(); - - unsigned num_rounds{0}; - while (num_reductions || num_ones || num_zeros) - { - ++num_rounds; - num_reductions = n->ReduceDepth(); - num_ones = n->EliminateOnes(); - num_zeros = n->EliminateZeros(); - } - return num_rounds; + return n->Simplified(); } } // namespace bertini diff --git a/core/src/function_tree/symbols/differential.cpp b/core/src/function_tree/symbols/differential.cpp index 1d984694b..d2ed67601 100644 --- a/core/src/function_tree/symbols/differential.cpp +++ b/core/src/function_tree/symbols/differential.cpp @@ -36,10 +36,6 @@ Differential::Differential(std::shared_ptr diff_variable, std::s { } -void Differential::Reset() const -{ - Node::ResetStoredValues(); -} const std::shared_ptr& Differential::GetVariable() const { @@ -52,7 +48,7 @@ void Differential::print(std::ostream & target) const } -std::shared_ptr Differential::Differentiate(std::shared_ptr const& v) const +std::shared_ptr Differential::Differentiate(std::shared_ptr const& /*v*/) const { throw std::runtime_error("differentiating a differential is not correctly implemented yet"); return Integer::Make(0); @@ -61,12 +57,12 @@ std::shared_ptr Differential::Differentiate(std::shared_ptr cons /** Compute the degree with respect to a single variable. For differentials, the degree is 0. */ -int Differential::Degree(std::shared_ptr const& v) const +int Differential::Degree(std::shared_ptr const& /*v*/) const { return 0; } -int Differential::Degree(VariableGroup const& vars) const +int Differential::Degree(VariableGroup const& /*vars*/) const { return 0; } @@ -76,12 +72,8 @@ std::vector Differential::MultiDegree(VariableGroup const& vars) const return std::vector(vars.size(),0); } -void Differential::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) -{ - -} -bool Differential::IsHomogeneous(std::shared_ptr const& v) const +bool Differential::IsHomogeneous(std::shared_ptr const& /*v*/) const { return true; } @@ -89,7 +81,7 @@ bool Differential::IsHomogeneous(std::shared_ptr const& v) const /** Check for homogeneity, with respect to a variable group. */ -bool Differential::IsHomogeneous(VariableGroup const& vars) const +bool Differential::IsHomogeneous(VariableGroup const& /*vars*/) const { return true; } @@ -100,60 +92,11 @@ bool Differential::IsHomogeneous(VariableGroup const& vars) const \param prec the number of digits to change precision to. */ -void Differential::precision(unsigned int prec) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); -} // This should never be called for a Differential. Only for Jacobians. -dbl Differential::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - if(differential_variable_ == diff_variable) - { - return 1.0; - } - else - { - return 0.0; - } -} -void Differential::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - if(differential_variable_ == diff_variable) - { - evaluation_value = 1.0; - } - else - { - evaluation_value = 0.0; - } -} -mpfr_complex Differential::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - if(differential_variable_ == diff_variable) - { - return mpfr_complex(1); - } - else - { - return mpfr_complex(0); - } -} -void Differential::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - if(differential_variable_ == diff_variable) - { - evaluation_value = 1; - } - else - { - evaluation_value = 0; - } -} } // re: namespace node } // re: namespace bertini diff --git a/core/src/function_tree/symbols/number.cpp b/core/src/function_tree/symbols/number.cpp index f4d4edb45..3d82badac 100644 --- a/core/src/function_tree/symbols/number.cpp +++ b/core/src/function_tree/symbols/number.cpp @@ -33,21 +33,7 @@ namespace bertini{ namespace node{ using ::pow; -void Number::Reset() const -{ - // ResetStoredValues(); -} - - -void Number::precision(unsigned int prec) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); - val_pair.second = false; // false indicates to re-evaluate -} - - -std::shared_ptr Number::Differentiate(std::shared_ptr const& v) const +std::shared_ptr Number::Differentiate(std::shared_ptr const& /*v*/) const { return Integer::Make(0); } @@ -64,28 +50,6 @@ void Integer::print(std::ostream & target) const target << true_value_; } -// Return value of constant -dbl Integer::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return dbl(double(true_value_),0); -} - -void Integer::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = dbl(double(true_value_),0); -} - - -mpfr_complex Integer::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return mpfr_complex(true_value_,0,DefaultPrecision()); -} - -void Integer::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = mpfr_complex(true_value_,0,DefaultPrecision()); -} - ///////////// // @@ -96,29 +60,12 @@ void Integer::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - return dbl(highest_precision_value_); -} - -void Float::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = dbl(highest_precision_value_); -} - - -mpfr_complex Float::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return mpfr_complex(highest_precision_value_,DefaultPrecision()); -} - -void Float::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = mpfr_complex(highest_precision_value_,DefaultPrecision()); + // real-valued floats print bare; the complex pair form is reserved for + // genuinely complex values + if (highest_precision_value_.imag() == 0) + target << highest_precision_value_.real(); + else + target << highest_precision_value_; } @@ -130,29 +77,12 @@ void Float::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - return dbl(double(true_value_real_),double(true_value_imag_)); -} - -void Rational::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = dbl(double(true_value_real_),double(true_value_imag_)); -} - - -mpfr_complex Rational::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return mpfr_complex(boost::multiprecision::mpfr_float(true_value_real_,DefaultPrecision()),boost::multiprecision::mpfr_float(true_value_imag_,DefaultPrecision())); -} - -void Rational::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = mpfr_complex(boost::multiprecision::mpfr_float(true_value_real_,DefaultPrecision()),boost::multiprecision::mpfr_float(true_value_imag_,DefaultPrecision())); + // real-valued rationals print bare (parseable as a rational literal); the + // complex pair form is reserved for genuinely complex values + if (true_value_imag_ == 0) + target << true_value_real_; + else + target << "(" << true_value_real_ << "," << true_value_imag_ << ")"; } diff --git a/core/src/function_tree/symbols/special_number.cpp b/core/src/function_tree/symbols/special_number.cpp index b03b26d74..ac8ec22e5 100644 --- a/core/src/function_tree/symbols/special_number.cpp +++ b/core/src/function_tree/symbols/special_number.cpp @@ -26,6 +26,8 @@ #include "bertini2/function_tree/symbols/special_number.hpp" +#include + @@ -34,52 +36,26 @@ namespace bertini{ namespace special_number{ using ::pow; -// Return value of constant -dbl Pi::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return acos(-1.0); -} +// Return value of constant. +// +// pi is computed from the authoritative source -- Boost.Math's `pi` constant, which for the mpfr +// backend defers to MPFR's `mpfr_const_pi` (correctly rounded to the working precision, and cached) +// -- rather than `acos(-1)`, whose result is not guaranteed correctly rounded and varies with the +// inverse-cosine implementation. This keeps pi identical across ranks/runs at a given precision, +// which matters because the Cauchy endgame's roots of unity and the total-degree start points are +// built from pi. See https://github.com/bertiniteam/b2/issues/156. -void Pi::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = acos(-1.0); -} -mpfr_complex Pi::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return mpfr_complex(mpfr_float(acos(mpfr_float(-1)))); -} -void Pi::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = mpfr_complex(mpfr_float(acos(mpfr_float(-1)))); -} -// Return value of constant -dbl E::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return dbl(exp(1.0),0.0); -} -void E::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = dbl(exp(1.0),0.0); -} -mpfr_complex E::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return mpfr_complex(mpfr_float(exp(mpfr_float(1)))); -} -void E::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = mpfr_complex(mpfr_float(exp(mpfr_float(1)))); -} }// special number namespace diff --git a/core/src/function_tree/symbols/symbol.cpp b/core/src/function_tree/symbols/symbol.cpp index a36a57558..76f0a2fd5 100644 --- a/core/src/function_tree/symbols/symbol.cpp +++ b/core/src/function_tree/symbols/symbol.cpp @@ -32,15 +32,6 @@ namespace bertini{ namespace node{ -unsigned Symbol::EliminateZeros() -{ - return 0; -} -unsigned Symbol::EliminateOnes() -{ - return 0; -} - const std::string& NamedSymbol::name() const { diff --git a/core/src/function_tree/symbols/variable.cpp b/core/src/function_tree/symbols/variable.cpp index 575e30273..18556fe8f 100644 --- a/core/src/function_tree/symbols/variable.cpp +++ b/core/src/function_tree/symbols/variable.cpp @@ -35,77 +35,19 @@ namespace bertini{ using ::pow; Variable::Variable(std::string new_name) : NamedSymbol(new_name) -{ - SetToRandUnit(); - set_current_value(dbl(Eval())); -} +{ } Variable::Variable() : NamedSymbol("unnamed_variable_be_scared") -{ - SetToRandUnit(); - set_current_value(dbl(Eval())); -} - -template -void Variable::set_current_value(T const& val) -{ - using CT = typename NumTraits::Complex; - static_assert(!Eigen::NumTraits::IsInteger,"type must be floating point in nature, with a real-complex pair defined in NumTraits"); - - assert(Precision(std::get< std::pair >(current_value_).first)==Precision(val) && "precision of value setting into variable doesn't match precision of variable. is default precision correct?"); - - std::get< std::pair >(current_value_).first = static_cast(val); - std::get< std::pair >(current_value_).second = false; -} - -template void Variable::set_current_value(double const&); -template void Variable::set_current_value(dbl const&); -template void Variable::set_current_value(mpfr_float const&); -template void Variable::set_current_value(mpfr_complex const&); - - -template -void Variable::SetToNan() -{ - set_current_value(static_cast(std::numeric_limits::quiet_NaN())); -} - -template void Variable::SetToNan(); -template void Variable::SetToNan(); - - -template -void Variable::SetToRand() -{ - set_current_value(RandomUnit()); -} -template void Variable::SetToRand(); -template void Variable::SetToRand(); - - -template -void Variable::SetToRandUnit() -{ - set_current_value(RandomUnit()); -} -template void Variable::SetToRandUnit(); -template void Variable::SetToRandUnit(); +{ } std::shared_ptr Variable::Differentiate(std::shared_ptr const& v) const { if (v==nullptr) - return Differential::Make(shared_from_this(), name()); + return Differential::Make(std::static_pointer_cast(shared_from_this()), name()); else return v.get() == this ? Integer::Make(1) : Integer::Make(0); } -void Variable::Reset() const -{ - Node::ResetStoredValues(); -} - - - int Variable::Degree(std::shared_ptr const& v) const { if (v) @@ -143,12 +85,8 @@ std::vector Variable::MultiDegree(VariableGroup const& vars) const } -void Variable::Homogenize(VariableGroup const& vars, std::shared_ptr const& homvar) -{ - -} -bool Variable::IsHomogeneous(std::shared_ptr const& v) const +bool Variable::IsHomogeneous(std::shared_ptr const& /*v*/) const { return true; } @@ -156,47 +94,11 @@ bool Variable::IsHomogeneous(std::shared_ptr const& v) const /** Check for homogeneity, with respect to a variable group. */ -bool Variable::IsHomogeneous(VariableGroup const& vars) const +bool Variable::IsHomogeneous(VariableGroup const& /*vars*/) const { return true; } -/** - Change the precision of this variable-precision tree node. - - \param prec the number of digits to change precision to. - */ -void Variable::precision(unsigned int prec) const -{ - auto& val_pair = std::get< std::pair >(current_value_); - val_pair.first.precision(prec); -} - - -// Return current value of the variable. -dbl Variable::FreshEval_d(std::shared_ptr const& diff_variable) const -{ - return std::get< std::pair >(current_value_).first; -} - -void Variable::FreshEval_d(dbl& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = std::get< std::pair >(current_value_).first; -} - - -mpfr_complex Variable::FreshEval_mp(std::shared_ptr const& diff_variable) const -{ - return std::get< std::pair >(current_value_).first; -} - -void Variable::FreshEval_mp(mpfr_complex& evaluation_value, std::shared_ptr const& diff_variable) const -{ - evaluation_value = std::get< std::pair >(current_value_).first; -} - - - } // re: namespace node } // re: namespace bertini diff --git a/core/src/parallel/initialize_finalize.cpp b/core/src/parallel/initialize_finalize.cpp index 8072994de..958f340c6 100644 --- a/core/src/parallel/initialize_finalize.cpp +++ b/core/src/parallel/initialize_finalize.cpp @@ -34,22 +34,83 @@ namespace bertini{ namespace parallel{ + +#ifdef BERTINI2_HAVE_MPI + MPI_Comm WorldComm() + { + return MPI_COMM_WORLD; + } +#endif + void Initialize() { - +#ifdef BERTINI2_HAVE_MPI + // Check first so mpi4py (or another library) that already called MPI_Init + // is handled gracefully — we skip the init in that case. + int already_initialized = 0; + MPI_Initialized(&already_initialized); + if (!already_initialized) + { + // MPI_THREAD_FUNNELED: all MPI calls occur on the main thread only. + // Worker threads communicate via local queues; they never call MPI. + int provided = 0; + MPI_Init_thread(nullptr, nullptr, MPI_THREAD_FUNNELED, &provided); + } +#endif } void Finalize() { +#ifdef BERTINI2_HAVE_MPI + int already_finalized = 0; + MPI_Finalized(&already_finalized); + if (!already_finalized) + MPI_Finalize(); +#endif + } + + int Rank() + { +#ifdef BERTINI2_HAVE_MPI + int initialized = 0; + MPI_Initialized(&initialized); + if (!initialized) return 0; + int r = 0; + MPI_Comm_rank(MPI_COMM_WORLD, &r); + return r; +#else + return 0; +#endif + } + int Size() + { +#ifdef BERTINI2_HAVE_MPI + int initialized = 0; + MPI_Initialized(&initialized); + if (!initialized) return 1; + int s = 1; + MPI_Comm_size(MPI_COMM_WORLD, &s); + return s; +#else + return 1; +#endif } - } + + bool IsManager() { return Rank() == 0; } + bool IsWorker() { return Rank() != 0; } + + } // namespace parallel namespace serial{ void Initialize() { - std::cout << "\n\n\n" << SplashScreen() << "\n\n\n"; - std::cout << "\n\n" << DependencyVersions() << "\n\n"; + // Splash screen only on rank 0 — guard handles both serial and parallel builds. + if (parallel::IsManager()) + { + std::cout << "\n\n\n" << SplashScreen() << "\n\n\n"; + std::cout << "\n\n" << DependencyVersions() << "\n\n"; + } logging::Logging::Init(); } diff --git a/core/src/system/start/mhom.cpp b/core/src/system/start/mhom.cpp index f1b07d03f..956bdb3b9 100644 --- a/core/src/system/start/mhom.cpp +++ b/core/src/system/start/mhom.cpp @@ -26,6 +26,12 @@ #include "bertini2/system/start/mhom.hpp" +#include "bertini2/system/blocks/block.hpp" +#include "bertini2/random.hpp" + +#include +#include + BOOST_CLASS_EXPORT(bertini::start_system::MHomogeneous); @@ -41,7 +47,13 @@ namespace bertini MHomogeneous::MHomogeneous(System const& s) { - if (s.NumTotalFunctions() != s.NumVariables()) + // A square multiprojective system has one equation per dimension. Each + // projective (homogeneous) variable group of size k spans P^{k-1}: its k + // coordinates carry only k-1 dimensions (scale is free), so subtract one per hom + // group. This equals the old NumTotalFunctions()==NumVariables() for affine + // systems and for homogenized+patched systems, but is also correct for raw systems + // that carry projective variable groups. + if (s.NumNaturalFunctions() != s.NumNaturalVariables() - s.NumHomVariableGroups()) throw std::runtime_error("attempting to construct multi homogeneous start system from non-square target system"); if (s.HavePathVariable()) @@ -59,49 +71,122 @@ namespace bertini CopyVariableStructure(s); - linprod_matrix_ = Mat>(degree_matrix_.rows(), degree_matrix_.cols()); - std::shared_ptr func; - for (int ii = 0; ii < degree_matrix_.rows(); ++ii) + // Generate the random linear-factor coefficients directly (no node::LinearProduct). + // linear_coeffs_(ii,jj) holds degree_matrix_(ii,jj) factors over group jj's + // variables; each factor is a row of (group_size + 1) coefficients, the trailing + // one being the constant. Projective groups (the leading var_groups_, indices < + // NumHomVariableGroups) have homogeneous factors, so their constant is 0. We + // generate at MaxPrecisionAllowed so the block's master is precision-faithful. { - func = Integer::Make(1); - - for (int jj = 0; jj < s.NumHomVariableGroups(); ++jj) - { - if(degree_matrix_(ii,jj) != 0) + auto const saved_prec = DefaultPrecision(); + DefaultPrecision(MaxPrecisionAllowed()); + + linear_coeffs_ = Mat>(degree_matrix_.rows(), degree_matrix_.cols()); + for (Eigen::Index ii = 0; ii < degree_matrix_.rows(); ++ii) + for (Eigen::Index jj = 0; jj < degree_matrix_.cols(); ++jj) { - // Fill the linear product matrix - linprod_matrix_(ii,jj) = LinearProduct::Make(var_groups_[jj], degree_matrix_(ii,jj), true); - - func *= linprod_matrix_(ii,jj); + const int d = degree_matrix_(ii, jj); + if (d == 0) + continue; + const Eigen::Index gsize = static_cast(var_groups_[jj].size()); + const bool projective = (static_cast(jj) < s.NumHomVariableGroups()); + Mat C(d, gsize + 1); + for (Eigen::Index f = 0; f < d; ++f) + { + for (Eigen::Index k = 0; k < gsize; ++k) + C(f, k) = mpfr_complex(mpfr_float(RandomRat()), mpfr_float(RandomRat())); + C(f, gsize) = projective ? mpfr_complex(0) + : mpfr_complex(mpfr_float(RandomRat()), mpfr_float(RandomRat())); + } + linear_coeffs_(ii, jj) = std::move(C); } - } - for (int jj = s.NumHomVariableGroups(); jj < degree_matrix_.cols(); ++jj) + + DefaultPrecision(saved_prec); + } + + // Homogenize the variable structure (adds a homogenizing variable per affine + // group; projective groups already carry their own) and copy the target's patch. + // There are no node functions to homogenize -- the start system evaluates through + // the products-of-linears block built below. + if (s.IsHomogeneous()) + Homogenize(); + + if (s.IsPatched()) + CopyPatches(s); + + // Build the products-of-linears evaluation block from linear_coeffs_, so the start + // system evaluates via the block (the SLP compiler cannot compile linear-product + // node trees, which is what blocked MHom in a homotopy). Per function we assemble + // an augmented coefficient matrix (one row per factor, length NumVariables()+1), + // placing each factor's coefficients by VARIABLE IDENTITY into the full variable + // ordering. A factor's constant multiplies the group's homogenizing variable when + // the system is homogeneous (so it goes in that variable's column), else it is the + // augmented constant in the trailing column. Built at MaxPrecisionAllowed so the + // block's master is precision-faithful. + { + auto const saved_prec = DefaultPrecision(); + DefaultPrecision(MaxPrecisionAllowed()); + this->precision(MaxPrecisionAllowed()); + + const VariableGroup& vars = this->Variables(); + std::map col_of; + for (Eigen::Index c = 0; c < static_cast(vars.size()); ++c) + col_of[vars[c].get()] = c; + const Eigen::Index n = static_cast(this->NumVariables()); + + std::vector> per_function; + per_function.reserve(static_cast(degree_matrix_.rows())); + + for (Eigen::Index ii = 0; ii < degree_matrix_.rows(); ++ii) { - if(degree_matrix_(ii,jj) != 0) + std::vector> rows; + for (Eigen::Index g = 0; g < degree_matrix_.cols(); ++g) { - // Fill the linear product matrix - linprod_matrix_(ii,jj) = LinearProduct::Make(var_groups_[jj], degree_matrix_(ii,jj)); - - func *= linprod_matrix_(ii,jj); + const int d = degree_matrix_(ii, g); + if (d == 0) + continue; + + const Mat& C = linear_coeffs_(ii, g); + const VariableGroup& gvars = var_groups_[static_cast(g)]; + // the homogenizing variable of an affine group, if the system is + // homogenized; projective groups (g < num_hom_groups_) have none. + std::shared_ptr hom_var; + if (static_cast(g) >= num_hom_groups_ && + !this->HomogenizingVariables().empty()) + hom_var = this->HomogenizingVariables()[static_cast(g) - num_hom_groups_]; + + for (Eigen::Index f = 0; f < d; ++f) + { + Vec row = Vec::Zero(n + 1); + for (size_t k = 0; k < gvars.size(); ++k) + row(col_of.at(gvars[k].get())) = C(f, static_cast(k)); + const mpfr_complex& constant = C(f, static_cast(gvars.size())); + if (hom_var && col_of.count(hom_var.get())) + row(col_of.at(hom_var.get())) = constant; + else + row(n) = constant; // augmented constant (0 for projective groups) + rows.push_back(std::move(row)); + } } - } - - AddFunction(func); - } + Mat M(static_cast(rows.size()), n + 1); + for (size_t r = 0; r < rows.size(); ++r) + M.row(static_cast(r)) = rows[r].transpose(); + per_function.push_back(std::move(M)); + } - if (s.IsHomogeneous()) - Homogenize(); + this->AddBlock(blocks::ProductsOfLinearsBlock(static_cast(n), std::move(per_function))); - if (s.IsPatched()) - CopyPatches(s); + DefaultPrecision(saved_prec); + this->precision(saved_prec); + } }// M-Hom constructor MHomogeneous& MHomogeneous::operator*=(Nd const& n) { - *this *= n; + System::operator*=(n); return *this; } @@ -122,7 +207,7 @@ namespace bertini unsigned long long MHomogeneous::NumStartPoints() const { unsigned long long num_start_points = 0; - for(int ii = 0; ii < valid_partitions_.size(); ii++) + for(size_t ii = 0; ii < valid_partitions_.size(); ii++) { num_start_points += NumStartPointsForPartition(valid_partitions_[ii]); } @@ -150,8 +235,9 @@ namespace bertini degree_matrix_ = Mat::Zero(target_system.NumNaturalFunctions(),target_system.NumTotalVariableGroups()); var_groups_ = target_system.HomVariableGroups(); + num_hom_groups_ = var_groups_.size(); // the leading var_groups_ entries are projective auto affine_var_groups = target_system.VariableGroups(); - //This concatenates the affine variable groups to the hom variable groups. + //This concatenates the affine variable groups to the hom variable groups. var_groups_.insert(var_groups_.end(), affine_var_groups.begin(), affine_var_groups.end()); int col_count = 0; @@ -165,7 +251,7 @@ namespace bertini std::vector degs = target_system.Degrees(*it); std::vector temp_v; - for(int ii = 0; ii < (*it).size(); ++ii) + for(size_t ii = 0; ii < (*it).size(); ++ii) { temp_v.push_back(var_count); var_count++; @@ -178,7 +264,7 @@ namespace bertini throw std::runtime_error("zero column in degree matrix for m-homogeneous start system!"); } - for(int ii = 0; ii <= degs.size() - 1; ++ii) + for(size_t ii = 0; ii < degs.size(); ++ii) { degree_matrix_(ii,col_count) = degs[ii]; } @@ -220,34 +306,37 @@ namespace bertini void MHomogeneous::GenerateValidPartitions(System const& target_system) { int row = 0; - int old_current_part_row = -1; int bad_choice = 0; Vec current_partition = -1*Vec::Ones(target_system.NumNaturalFunctions()); Vec variable_group_counter = Vec::Zero(target_system.NumTotalVariableGroups()); - auto size_of_each_var_gp = target_system.VariableGroupSizes(); //K - - - - - - for(int ii = 0; ii < target_system.NumTotalVariableGroups(); ++ii) + // Capacity per group = the number of functions that group may be assigned, which + // is the group's declared dimension. Use var_groups_ (the groups as concatenated + // for the degree matrix in CreateDegreeMatrix: hom groups then affine groups), + // NOT target_system.VariableGroupSizes(): once the target is homogenized the + // latter counts the added homogenizing variable, doubling the capacity and + // admitting invalid over-filled partitions (e.g. both functions in one size-1 + // group), whose linear solve is singular and yields NaN start points. + for(size_t ii = 0; ii < target_system.NumTotalVariableGroups(); ++ii) { - variable_group_counter[ii] = size_of_each_var_gp[ii]; - } + // A projective (homogeneous) group of size k spans P^{k-1}, so it takes only + // k-1 functions; an affine group of size m takes m. The leading num_hom_groups_ + // entries of var_groups_ are the projective ones. + const int dim = static_cast(var_groups_[ii].size()) - (ii < num_hom_groups_ ? 1 : 0); + variable_group_counter[ii] = dim; + } // std::cout << "variable_group_counter is " << std::endl; // std::cout << variable_group_counter << std::endl; while (row > -1) // Algorithm will move up and down rows, kicking out to row=-1 at end { // std::cout << "current_partition before is " << std::endl; // std::cout << current_partition << std::endl; - old_current_part_row = current_partition[row]; //Hang on to previous choice of column for this row, in case we are done with this row. current_partition[row] = ChooseColumnInRow(target_system,variable_group_counter,row,current_partition[row]); //Pick next column (var gp) for the current row (func) // std::cout << "current_partition after is " << std::endl; // std::cout << current_partition << std::endl; //ChooseColumnInRow() will make this happen if it runs into col being equal to system.NumVariables()! - if (current_partition[row] == target_system.NumTotalVariableGroups()) // means we have exhausted all good columns for the current row, so we go back up a row + if (current_partition[row] == static_cast(target_system.NumTotalVariableGroups())) // means we have exhausted all good columns for the current row, so we go back up a row { //no choices for current row row = row - 1; //go back up a row bad_choice = 1; @@ -255,12 +344,12 @@ namespace bertini else //found a good choice of column for this row! { row = row + 1; //move on to next row! - if (row < target_system.NumNaturalFunctions()) + if (row < static_cast(target_system.NumNaturalFunctions())) current_partition[row] = -1; //This allows us to consider all possible columns from left to right. //since we are starting a new row, we start with the left-most entry (ChooseColumnInRow() first increments col) } - if((row == target_system.NumNaturalFunctions()) && (!bad_choice)) + if((row == static_cast(target_system.NumNaturalFunctions())) && (!bad_choice)) { // std::cout << "Good partition!!!!" << std::endl; // std::cout << current_partition << std::endl; @@ -309,7 +398,7 @@ namespace bertini /*We have reached the end of the degree matrix. Return and (current_partition[row] == target_system.NumTotalVariableGroups()) in GenerateValidPartitions() gets executed. */ - if (col == target_system.NumTotalVariableGroups()) + if (col == static_cast(target_system.NumTotalVariableGroups())) { done = 1; //got to the right end of the degree matrix! } @@ -349,15 +438,15 @@ namespace bertini // First, determine which partition we are looking through - int counter = 0; + unsigned long long counter = 0; int partition_ii = -1; - for (int ii = 0; ii < valid_partitions_.size(); ++ii) + for (size_t ii = 0; ii < valid_partitions_.size(); ++ii) { counter += NumStartPointsForPartition(valid_partitions_[ii]); if(index < counter) { - partition_ii = ii; + partition_ii = static_cast(ii); counter -= NumStartPointsForPartition(valid_partitions_[ii]); index -= counter; break; @@ -383,37 +472,66 @@ namespace bertini - // Create a linear system to solve. + // Create the linear system whose solution is this start point, in natural + // coordinates. Each function assigned by the partition contributes its chosen + // linear factor = 0: + // affine group (m vars): m factors -> m rows; the factor's trailing coeff is + // its constant, so it moves to the right-hand side. + // projective group (k coords): k-1 factors -> k-1 rows; these are homogeneous + // (trailing coeff 0), so the per-group block is k-1 by + // k -- one short of square. We pin one coordinate of + // each projective group to 1 (an affine-chart normaliza- + // tion) to pick a representative; HomogenizePoint then + // rescales it onto the patch. Adding one such row per + // projective group makes A exactly square. size_t num_grouped_variables = NumNaturalVariables() - NumUngroupedVariables(); - Mat A(partition.size(), num_grouped_variables); - Vec v(num_grouped_variables); - Vec b(partition.size()); - + Mat A = Mat::Zero(static_cast(num_grouped_variables), + static_cast(num_grouped_variables)); + Vec b = Vec::Zero(static_cast(num_grouped_variables)); + + // coefficients come from linear_coeffs_ (mpfr master); cast each to the working + // type T (a no-op widen for mpfr, a narrowing for dbl). + auto as_T = [](mpfr_complex const& z) -> T { + if constexpr (std::is_same::value) return dbl(z); + else return z; + }; for(int ii = 0; ii < partition.size(); ++ii) { - v.setZero(); std::vector cols = variable_cols_[partition[ii]]; - auto coeff = linprod_matrix_(ii,partition[ii])->GetCoeffs(subscript[ii]); - for(int jj = 0; jj < cols.size(); ++jj) + const Mat& C = linear_coeffs_(ii, partition[ii]); + const Eigen::Index f = static_cast(subscript[ii]); + for(size_t jj = 0; jj < cols.size(); ++jj) { - v(cols[jj]) = coeff[jj]; + A(ii, static_cast(cols[jj])) = as_T(C(f, static_cast(jj))); } - - A.row(ii) = v; - b(ii) = -coeff[cols.size()]; + b(ii) = -as_T(C(f, static_cast(cols.size()))); // affine: the constant term; projective: 0 } - - start_point = A.partialPivLu().solve(b); - - - var_groups_[0][0]->set_current_value(start_point(0)); - var_groups_[0][1]->set_current_value(start_point(1)); - - std::shared_ptr f = Function(0); - // !!! superfluous line? - - + // normalization row per projective group: pin its last coordinate to 1. + Eigen::Index extra = static_cast(partition.size()); + for (size_t g = 0; g < num_hom_groups_; ++g) + { + A(extra, static_cast(variable_cols_[g].back())) = T(1); + b(extra) = T(1); + ++extra; + } + + Vec affine_solution = A.partialPivLu().solve(b); + + + // The linear solve gives the start point in affine (dehomogenized) coordinates; + // lift it onto the homogenized + patched coordinate system the homotopy is + // tracked in (HomogenizePoint inserts the homogenizing coordinates per affine + // group and rescales onto the patch, and is a no-op if not homogenized). + start_point = this->HomogenizePoint(affine_solution); + + // Return the point at the current working precision. The linear-factor + // coefficients and the patch are stored at MaxPrecisionAllowed, which would + // otherwise leak into the start point and mismatch the tracker's working + // precision (the adaptive tracker begins at the ambient precision). + if constexpr (!std::is_same::value) + for (Eigen::Index i = 0; i < start_point.size(); ++i) + start_point(i).precision(DefaultPrecision()); } diff --git a/core/src/system/start/total_degree.cpp b/core/src/system/start/total_degree.cpp index 4ffd55a92..7f29dcf41 100644 --- a/core/src/system/start/total_degree.cpp +++ b/core/src/system/start/total_degree.cpp @@ -24,6 +24,8 @@ #include "bertini2/system/start/total_degree.hpp" +#include + BOOST_CLASS_EXPORT(bertini::start_system::TotalDegree); @@ -39,7 +41,7 @@ namespace bertini { SanityChecks(s); CopyDegrees(s); CopyVariableStructure(s); - SeedRandomValues(s.NumNaturalFunctions()); + SeedRandomValues(static_cast(s.NumNaturalFunctions())); GenerateFunctions(); if (s.IsHomogeneous()) @@ -52,7 +54,7 @@ namespace bertini { TotalDegree& TotalDegree::operator*=(Nd const& n) { - *this *= n; + System::operator*=(n); return *this; } @@ -80,10 +82,11 @@ namespace bertini { offset = 1; } - auto two_i_pi = std::acos(-1.0) * dbl(0,2); + // authoritative pi (see issue #156 / special_number.cpp), not acos(-1) + auto two_i_pi = boost::math::constants::pi() * dbl(0,2); for (size_t ii = 0; ii< NumNaturalVariables(); ++ii) - start_point(ii+offset) = exp( two_i_pi * static_cast(indices[ii]) / static_cast(degrees_[ii]) ) * pow(random_values_[ii]->Eval(), 1.0 / static_cast(degrees_[ii])); + start_point(ii+offset) = exp( two_i_pi * static_cast(indices[ii]) / static_cast(degrees_[ii]) ) * pow(random_values_[ii]->Value(), 1.0 / static_cast(degrees_[ii])); if (IsPatched()) RescalePointToFitPatchInPlace(start_point); @@ -94,7 +97,7 @@ namespace bertini { Vec TotalDegree::GenerateStartPoint(mpfr_complex,unsigned long long index) const { - using bertini::DefaultPrecision; + using bertini::ThreadPrecision; Vec start_point(NumVariables()); // make the value we're returning auto indices = IndexToSubscript(index, degrees_); // get the position of it -- used in the angle of the coordinates of the produced point. @@ -102,21 +105,22 @@ namespace bertini { unsigned offset = 0; if (IsPatched()) { - start_point(0) = mpfr_complex(1,0,DefaultPrecision()); + start_point(0) = mpfr_complex(1,0,ThreadPrecision()); offset = 1; } // TODO: this code should be cleaned up after issue 308 is solved -- namely, the two precision adjustment calls should be removed. They're only necessary because prec16 / ulonglog = prec19. auto one = mpfr_float(1); - mpfr_complex two_i_pi = mpfr_complex(0,2) * acos( mpfr_float(-1) ); + // authoritative pi (see issue #156 / special_number.cpp), not acos(-1) + mpfr_complex two_i_pi = mpfr_complex(0,2) * boost::math::constants::pi(); for (size_t ii = 0; ii< NumNaturalVariables(); ++ii) { mpfr_complex a = exp( (two_i_pi * indices[ii]) / degrees_[ii]); - mpfr_complex b = pow(random_values_[ii]->Eval(), one / degrees_[ii]); + mpfr_complex b = pow(random_values_[ii]->Value(), one / degrees_[ii]); - Precision(a,DefaultPrecision()); - Precision(b,DefaultPrecision()); + Precision(a,ThreadPrecision()); + Precision(b,ThreadPrecision()); start_point(ii+offset) = a*b; } @@ -156,14 +160,14 @@ namespace bertini { { auto deg = s.Degrees(); for (const auto& d : deg) - degrees_.push_back(static_cast(d)); + degrees_.push_back(static_cast(d)); } void TotalDegree::SeedRandomValues(int num_functions) { random_values_.resize(num_functions); - for (unsigned ii = 0; ii < num_functions; ++ii) + for (int ii = 0; ii < num_functions; ++ii) random_values_[ii] = Rational::Make(node::Rational::Rand()); } diff --git a/core/src/system/straight_line_program.cpp b/core/src/system/straight_line_program.cpp index a559fc8c2..9b0d6bccc 100644 --- a/core/src/system/straight_line_program.cpp +++ b/core/src/system/straight_line_program.cpp @@ -25,6 +25,9 @@ #include "bertini2/system/straight_line_program.hpp" #include "bertini2/system/system.hpp" +#include "bertini2/system/blocks/polynomial_block.hpp" + +#include @@ -55,6 +58,38 @@ namespace bertini{ case Assign: return "Assign"; case IntPower: return "IntPower"; } + throw std::runtime_error("unrecognized operation in OpcodeToString"); + } + + + // Produce a constant's value directly from its exact recipe --- no function-tree node, no node + // evaluation (ADR-0027). These mirror the number nodes' FreshEval_d / FreshEval_mp exactly + // (Integer/Float/Rational in number.cpp; Pi/E in special_number.cpp) so the compiled program + // evaluates bit-for-bit identically to the old node-backed path, at the ambient working + // precision (ThreadPrecision). + template<> + dbl_complex ConstantRecipe::Produce() const { + switch (kind) { + case Kind::Integer: return dbl_complex(double(int_value), 0); + case Kind::Rational: return dbl_complex(double(rat_real), double(rat_imag)); + case Kind::Float: return dbl_complex(float_value); + case Kind::Pi: return dbl_complex(boost::math::constants::pi(), 0); + case Kind::E: return dbl_complex(exp(1.0), 0.0); + } + throw std::runtime_error("unrecognized ConstantRecipe kind in Produce"); + } + + template<> + mpfr_complex ConstantRecipe::Produce() const { + using boost::multiprecision::mpfr_float; + switch (kind) { + case Kind::Integer: return mpfr_complex(int_value, 0, ThreadPrecision()); + case Kind::Rational: return mpfr_complex(mpfr_float(rat_real, ThreadPrecision()), mpfr_float(rat_imag, ThreadPrecision())); + case Kind::Float: return mpfr_complex(float_value, ThreadPrecision()); + case Kind::Pi: return mpfr_complex(boost::math::constants::pi()); + case Kind::E: return mpfr_complex(mpfr_float(exp(mpfr_float(1)))); + } + throw std::runtime_error("unrecognized ConstantRecipe kind in Produce"); } @@ -67,33 +102,64 @@ namespace bertini{ void StraightLineProgram::precision(unsigned new_precision) const{ - if (new_precision==this->precision_){ + if (new_precision==memory_.precision_){ return; } else{ - auto& mem = std::get>(memory_); - - for (auto& p: true_values_of_numbers_) - { - auto& n = std::get(p); - auto& loc = std::get(p); + auto& mem = memory_.Get(); - mem[loc] = n->Eval(); - } + // Refill the constants from their exact recipes (no node evaluation), then normalize + // every slot to the new precision. (Matches the historical node-backed path.) + for (auto const& c : program_->constant_recipes_) + mem[c.slot] = c.Produce(); for (auto& n : mem) Precision(n, new_precision); - this->precision_ = new_precision; + memory_.precision_ = new_precision; + } + + + } + + + + template + void StraightLineProgram::CopyNumbersIntoMemory() const + { + for (auto const& c : program_->constant_recipes_){ + memory_.Get()[c.slot] = c.Produce(); + if (std::is_same::value) + Precision(memory_.Get()[c.slot], memory_.precision_); } + } + + template void StraightLineProgram::CopyNumbersIntoMemory() const; + template void StraightLineProgram::CopyNumbersIntoMemory() const; + + void StraightLineProgram::SetupMemory() + { + // Re-seed the thread-local default precision from the program's own precision before + // growing the mpfr_complex memory block. resize() default-constructs each new element via + // mpfr_init2(x, thread_default_precision()); on Boost 1.87 that value can be 0 on fresh + // threads, which aborts. DefaultPrecision() sets both the static and thread-local defaults + // so the default-constructed slots are valid. + DefaultPrecision(memory_.precision_); + + // adjust the sizes of the memory blocks to match the number expected via compilation + memory_.Get().resize(program_->num_slots_); + memory_.Get().resize(program_->num_slots_); + // downsample to get ready for evaluation + CopyNumbersIntoMemory(); + CopyNumbersIntoMemory(); } - void StraightLineProgram::AddInstruction(Operation binary_op, size_t in_loc1, size_t in_loc2, size_t out_loc){ + void SLPProgram::AddInstruction(Operation binary_op, size_t in_loc1, size_t in_loc2, size_t out_loc){ this->instructions_.push_back(binary_op); this->instructions_.push_back(in_loc1); this->instructions_.push_back(in_loc2); @@ -102,88 +168,96 @@ namespace bertini{ - void StraightLineProgram::AddInstruction(Operation unary_op, size_t in_loc, size_t out_loc){ + void SLPProgram::AddInstruction(Operation unary_op, size_t in_loc, size_t out_loc){ this->instructions_.push_back(unary_op); this->instructions_.push_back(in_loc); this->instructions_.push_back(out_loc); } - void StraightLineProgram::AddNumber(Nd const num, size_t loc){ - this->true_values_of_numbers_.push_back(std::pair(num, loc)); + void SLPProgram::AddConstant(ConstantRecipe recipe){ + this->constant_recipes_.push_back(std::move(recipe)); } std::ostream& operator <<(std::ostream& out, const StraightLineProgram & s){ + auto const& prog = *s.program_; + out << "\n\n#fns: " << s.NumFunctions() << " #vars: " << s.NumVariables() << std::endl; out << "have path variable: " << s.HavePathVariable() << std::endl; out << std::endl << "numbers of things:" << std::endl; - out << "Functions: " << s.number_of_.Functions << std::endl; - out << "Variables: " << s.number_of_.Variables << std::endl; - out << "Jacobian: " << s.number_of_.Jacobian << std::endl; + out << "Functions: " << prog.number_of_.Functions << std::endl; + out << "Variables: " << prog.number_of_.Variables << std::endl; + out << "Jacobian: " << prog.number_of_.Jacobian << std::endl; if (s.HavePathVariable()) - out << "TimeDeriv: " << s.number_of_.TimeDeriv << std::endl; + out << "TimeDeriv: " << prog.number_of_.TimeDeriv << std::endl; out << std::endl << " output locations:" << std::endl; - out << "Functions " << s.output_locations_.Functions << std::endl; - out << "Jacobian " << s.output_locations_.Jacobian << std::endl; + out << "Functions " << prog.output_locations_.Functions << std::endl; + out << "Jacobian " << prog.output_locations_.Jacobian << std::endl; if (s.HavePathVariable()) - out << "TimeDeriv " << s.output_locations_.TimeDeriv << std::endl; + out << "TimeDeriv " << prog.output_locations_.TimeDeriv << std::endl; out << std::endl << " input locations:" << std::endl; - out << "Variables " << s.input_locations_.Variables << std::endl; + out << "Variables " << prog.input_locations_.Variables << std::endl; if (s.HavePathVariable()) - out << "Time " << s.input_locations_.Time << std::endl; + out << "Time " << prog.input_locations_.Time << std::endl; - out << std::endl << "true values of numbers: (number, location to downsample to)" << std::endl; - for (auto const& x : s.true_values_of_numbers_) - out << *(x.first) << ':' << x.second << std::endl; + out << std::endl << "constants: (kind, location to downsample to)" << std::endl; + for (auto const& c : prog.constant_recipes_) + out << static_cast(c.kind) << ':' << c.slot << std::endl; out << std::endl << std::endl; out << std::endl << "instructions: " << std::endl; - for (size_t ii(0); ii(s.instructions_[ii++]); + for (size_t ii(0); ii(prog.instructions_[ii++]); out << OpcodeToString(op) << "("; - if (IsUnary(op)) - out << s.instructions_[ii++] << ") --> " << s.instructions_[ii++] << std::endl; - - else - out << s.instructions_[ii++] << "," << s.instructions_[ii++] << ") --> " << s.instructions_[ii++] << std::endl; + if (IsUnary(op)){ + auto operand = prog.instructions_[ii++]; + auto result = prog.instructions_[ii++]; + out << operand << ") --> " << result << std::endl; + } + else{ + auto operand1 = prog.instructions_[ii++]; + auto operand2 = prog.instructions_[ii++]; + auto result = prog.instructions_[ii++]; + out << operand1 << "," << operand2 << ") --> " << result << std::endl; + } } - auto& memory_dbl = std::get>(s.memory_); - auto& memory_mpfr = std::get>(s.memory_); + auto& memory_dbl = s.memory_.Get(); + auto& memory_mpfr = s.memory_.Get(); out << "\nvariable values in dbl memory:\n"; - for (unsigned ii=0; ii - void StraightLineProgram::Eval() const{ + void SLPProgram::Eval(SLPMemory& mem) const{ - auto& memory = std::get>(memory_); + auto& memory = mem.Get(); #ifndef BERTINI_DISABLE_PRECISION_CHECKS - if (! std::is_same::value && Precision(memory[0])!=this->precision_){ + if (! std::is_same::value && Precision(memory[0])!=mem.precision_){ throw std::runtime_error("memory and SLP are out-of-sync WRT precision"); } #endif - if (is_evaluated_) + if (mem.is_evaluated_) return; - for (int ii = 0; ii::value) + frozen_valid = mem.frozen_valid_dbl_; + else + frozen_valid = (mem.frozen_valid_mp_precision_ == mem.precision_); + + const size_t loop_start = frozen_valid ? first_live_instruction_ : 0; + + for (size_t ii = loop_start; ii::value) + mem.frozen_valid_dbl_ = true; + else + mem.frozen_valid_mp_precision_ = mem.precision_; + } + + mem.is_evaluated_ = true; } - template void StraightLineProgram::Eval() const; - template void StraightLineProgram::Eval() const; + template void SLPProgram::Eval(SLPMemory&) const; + template void SLPProgram::Eval(SLPMemory&) const; - template - void StraightLineProgram::CopyNumbersIntoMemory() const + void SLPProgram::PartitionInstructions() { - for (auto const& x: true_values_of_numbers_){ - GetMemory()[x.second] = (x.first)->Eval(); - if (std::is_same::value) - Precision(GetMemory()[x.second], this->precision_); + // A memory slot is "frozen" if its value depends only on frozen inputs. Seed: the literal + // numbers (Integer/Float/Rational and Pi/E, all in true_values_of_numbers_) are frozen; the + // variable and time slots are live. Then a single forward pass propagates frozenness: an + // instruction is frozen iff all its input slots are frozen, and it freezes its output slot. + const size_t num_slots = num_slots_; + std::vector slot_frozen(num_slots, false); + for (auto const& c : constant_recipes_) + slot_frozen[c.slot] = true; + + struct Instr { size_t off; size_t len; bool frozen; }; + std::vector parsed; + + for (size_t ii = 0; ii < instructions_.size(); ) + { + const auto op = static_cast(instructions_[ii]); + const bool unary = IsUnary(op); + const size_t len = unary ? 3 : 4; + + bool frozen; + size_t out; + if (unary) + { + out = instructions_[ii + 2]; + frozen = slot_frozen[instructions_[ii + 1]]; + } + else if (op == IntPower) + { + // The second operand of IntPower is an index into integers_, not a memory slot; the + // exponent is a literal, so frozenness depends only on the base slot. + out = instructions_[ii + 3]; + frozen = slot_frozen[instructions_[ii + 1]]; + } + else + { + out = instructions_[ii + 3]; + frozen = slot_frozen[instructions_[ii + 1]] && slot_frozen[instructions_[ii + 2]]; + } + + slot_frozen[out] = frozen; + parsed.push_back({ii, len, frozen}); + ii += len; } - } - template void StraightLineProgram::CopyNumbersIntoMemory() const; - template void StraightLineProgram::CopyNumbersIntoMemory() const; + // Stable partition: frozen instructions first (preserving relative order), then live ones. + // This is dependency-safe because no live instruction is an input to a frozen one. + std::vector reordered; + reordered.reserve(instructions_.size()); + size_t frozen_words = 0; + for (auto const& I : parsed) + if (I.frozen) + { + reordered.insert(reordered.end(), instructions_.begin() + I.off, instructions_.begin() + I.off + I.len); + frozen_words += I.len; + } + for (auto const& I : parsed) + if (!I.frozen) + reordered.insert(reordered.end(), instructions_.begin() + I.off, instructions_.begin() + I.off + I.len); + + instructions_ = std::move(reordered); + first_live_instruction_ = frozen_words; + } } @@ -341,8 +486,42 @@ namespace bertini{ using SLP = StraightLineProgram; - void SLPCompiler::Visit(node::Variable const& n){ + // Build an exact ConstantRecipe straight from a number node's true value --- no node evaluation + // (ADR-0027). One overload per concrete constant kind. + namespace { + ConstantRecipe RecipeFor(node::Integer const& n){ + ConstantRecipe r; r.kind = ConstantRecipe::Kind::Integer; r.int_value = n.GetValue(); return r; + } + ConstantRecipe RecipeFor(node::Rational const& n){ + ConstantRecipe r; r.kind = ConstantRecipe::Kind::Rational; + r.rat_real = n.GetValueReal(); r.rat_imag = n.GetValueImag(); return r; + } + ConstantRecipe RecipeFor(node::Float const& n){ + ConstantRecipe r; r.kind = ConstantRecipe::Kind::Float; r.float_value = n.GetValue(); return r; + } + ConstantRecipe RecipeFor(node::special_number::Pi const&){ + ConstantRecipe r; r.kind = ConstantRecipe::Kind::Pi; return r; + } + ConstantRecipe RecipeFor(node::special_number::E const&){ + ConstantRecipe r; r.kind = ConstantRecipe::Kind::E; return r; + } + } + + void SLPCompiler::RegisterConstant(Nd const& nd, ConstantRecipe recipe){ + recipe.slot = next_available_complex_; + program_under_construction_.AddConstant(std::move(recipe)); + locations_encountered_nodes_[nd] = next_available_complex_++; + } + + + void SLPCompiler::Visit(node::Variable const& n){ + // A system's variables are all pre-registered before its function trees are compiled, so + // reaching this Visit means a function references a variable that is not in the system's + // variable ordering. That is unsupported: to bake a constant into a function, build it with + // a literal (Float / Integer / Rational), not a variable removed from the ordering. + throw std::runtime_error("SLP compile: a function references the variable '" + n.name() + + "', which is not in the system's variable ordering"); } @@ -350,44 +529,34 @@ namespace bertini{ // // implementer note: // - // if you add another type to be visited, you must list it in TWO locations in the SLPCompiler type in the .hpp. - // + // if you add another type to be visited, you must list it in TWO locations in the SLPCompiler type in the .hpp. + // // - // wtb: factor out this pattern void SLPCompiler::Visit(node::Integer const& n){ - auto as_ptr = n.shared_from_this(); - this->DealWithNumber(n); // that sweet template magic. see slp.hpp for the definition of this template function + this->RegisterConstant(n.shared_from_this(), RecipeFor(n)); } void SLPCompiler::Visit(node::Float const& n){ - auto as_ptr = n.shared_from_this(); - this->DealWithNumber(n); + this->RegisterConstant(n.shared_from_this(), RecipeFor(n)); } void SLPCompiler::Visit(node::Rational const& n){ - auto as_ptr = n.shared_from_this(); - this->DealWithNumber(n); + this->RegisterConstant(n.shared_from_this(), RecipeFor(n)); } void SLPCompiler::Visit(node::special_number::Pi const& n){ - this->DealWithNumber(n); + this->RegisterConstant(n.shared_from_this(), RecipeFor(n)); } void SLPCompiler::Visit(node::special_number::E const& n){ - this->DealWithNumber(n); + this->RegisterConstant(n.shared_from_this(), RecipeFor(n)); } - void SLPCompiler::Visit(node::Jacobian const& n){ - throw std::runtime_error("unimplemented visit to node of type Jacobian"); - - - } - - void SLPCompiler::Visit(node::Differential const& n){ + void SLPCompiler::Visit(node::Differential const& /*n*/){ throw std::runtime_error("unimplemented visit to node of type Differential"); } @@ -395,17 +564,17 @@ namespace bertini{ - void SLPCompiler::Visit(node::Function const & f){ - // put the location of the accepted node into memory, and copy into an output location. + void SLPCompiler::Visit(node::NamedExpression const & f){ + // A named subexpression appearing inside a tree (a = x^2+y^2, used elsewhere): compute the + // entry once and copy its value into the NamedExpression's own slot, so every reference to + // the name shares that one result. (Same wiring as an embedded Function.) const std::shared_ptr& n = f.EntryNode(); - const std::shared_ptr f_as_ptr = std::dynamic_pointer_cast(f.shared_from_this()); - + const std::shared_ptr f_as_ptr = std::dynamic_pointer_cast(f.shared_from_this()); if (this->locations_encountered_nodes_.find(n) == this->locations_encountered_nodes_.end()) - n->Accept(*this); + n->Accept(*this); size_t location_entry = this->locations_encountered_nodes_[n]; - size_t location_this_node; if (this->locations_encountered_nodes_.find(f_as_ptr) == this->locations_encountered_nodes_.end()){ location_this_node = next_available_complex_; @@ -414,13 +583,7 @@ namespace bertini{ else location_this_node = locations_encountered_nodes_[f_as_ptr]; - - if (locations_top_level_functions_and_derivatives_.find(f_as_ptr)!= locations_top_level_functions_and_derivatives_.end()){ // top-level - slp_under_construction_.AddInstruction(Assign, location_entry, location_this_node); - } - else{ // not a top-level - slp_under_construction_.AddInstruction(Assign, location_entry, location_this_node); - } + program_under_construction_.AddInstruction(Assign, location_entry, location_this_node); } @@ -434,12 +597,12 @@ namespace bertini{ for (auto& n : n.Operands()){ if (this->locations_encountered_nodes_.find(n)==this->locations_encountered_nodes_.end()) - n->Accept(*this); + n->Accept(*this); operand_locations.push_back(this->locations_encountered_nodes_[n]); } - + const auto& signs = n.GetSigns(); size_t prev_result_loc; // for tracking where the output of the previous iteration went @@ -447,18 +610,18 @@ namespace bertini{ if (signs[0]) prev_result_loc = operand_locations[0]; else{ - slp_under_construction_.AddInstruction(Negate, operand_locations[0], next_available_complex_); + program_under_construction_.AddInstruction(Negate, operand_locations[0], next_available_complex_); prev_result_loc = next_available_complex_++; } - + // this loop // does the additions for the rest of the operands for (size_t ii{1}; iilocations_encountered_nodes_.find(operand)==this->locations_encountered_nodes_.end()) { - operand->Accept(*this); + operand->Accept(*this); } operand_locations.push_back(this->locations_encountered_nodes_[operand]); } - + const auto& mult_or_div = n.GetMultOrDiv();// true is multiply and false is divide size_t prev_result_loc; // for tracking where the output of the previous iteration went @@ -502,26 +665,26 @@ namespace bertini{ // seed the loop. if (mult_or_div[0]) prev_result_loc = operand_locations[0]; - else{ + else{ // this case is reciprocation of the first operand // this code sucks. really, there should be a bank of integers that we pull from, instead of many copies of the same integer. auto one = Integer::Make(1); - this->DealWithNumber(*one); + this->RegisterConstant(one, RecipeFor(*one)); auto location_one = locations_encountered_nodes_[one]; - slp_under_construction_.AddInstruction(Divide, location_one, operand_locations[0], next_available_complex_); + program_under_construction_.AddInstruction(Divide, location_one, operand_locations[0], next_available_complex_); prev_result_loc = next_available_complex_++; } - + // this loop // does the additions for the rest of the operands for (size_t ii{1}; iilocations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) { - operand->Accept(*this); + operand->Accept(*this); } auto location_operand = locations_encountered_nodes_[operand]; @@ -552,16 +715,16 @@ namespace bertini{ if (this->locations_integers_.find(expo) == this->locations_integers_.end()) { - locations_integers_[expo] = slp_under_construction_.integers_.size(); - slp_under_construction_.integers_.push_back(expo); + locations_integers_[expo] = program_under_construction_.integers_.size(); + program_under_construction_.integers_.push_back(expo); } auto location_exponent = locations_integers_[expo]; // this is a map lookup - + this->locations_encountered_nodes_[as_ptr] = next_available_complex_; - slp_under_construction_.AddInstruction(IntPower,location_operand,location_exponent, next_available_complex_++); + program_under_construction_.AddInstruction(IntPower,location_operand,location_exponent, next_available_complex_++); } @@ -573,141 +736,141 @@ namespace bertini{ const auto& exponent = n.GetExponent(); if (this->locations_encountered_nodes_.find(base) == this->locations_encountered_nodes_.end()) - base->Accept(*this); + base->Accept(*this); if (this->locations_encountered_nodes_.find(exponent) == this->locations_encountered_nodes_.end()) - exponent->Accept(*this); + exponent->Accept(*this); auto loc_base = locations_encountered_nodes_[base]; auto loc_exponent = locations_encountered_nodes_[exponent]; this->locations_encountered_nodes_[as_ptr] = next_available_complex_; - slp_under_construction_.AddInstruction(Power, loc_base, loc_exponent, next_available_complex_++); + program_under_construction_.AddInstruction(Power, loc_base, loc_exponent, next_available_complex_++); } void SLPCompiler::Visit(node::ExpOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Exp,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Exp,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::LogOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Log,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Log,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::NegateOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Negate,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Negate,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::SqrtOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Sqrt,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Sqrt,location_operand, next_available_complex_++); } // the trig operators void SLPCompiler::Visit(node::SinOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Sin,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Sin,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::ArcSinOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Asin,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Asin,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::CosOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Cos,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Cos,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::ArcCosOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Acos,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Acos,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::TanOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Tan,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Tan,location_operand, next_available_complex_++); } void SLPCompiler::Visit(node::ArcTanOperator const& n){ - + auto operand = n.Operand(); if (this->locations_encountered_nodes_.find(operand) == this->locations_encountered_nodes_.end()) - operand->Accept(*this); + operand->Accept(*this); auto location_operand = locations_encountered_nodes_[operand]; this->locations_encountered_nodes_[std::dynamic_pointer_cast(n.shared_from_this())] = next_available_complex_; - slp_under_construction_.AddInstruction(Atan,location_operand, next_available_complex_++); + program_under_construction_.AddInstruction(Atan,location_operand, next_available_complex_++); } @@ -721,165 +884,126 @@ namespace bertini{ - - - - - - - - - - - - - - - - SLP SLPCompiler::Compile(System const& sys){ + template + SLP SLPCompiler::Compile(SourceT const& sys){ this->Clear(); - this->slp_under_construction_.precision_ = DefaultPrecision(); + // ThreadPrecision (thread-local), not DefaultPrecision (global): SLPs are + // (re)compiled lazily during Eval, which may run on a std::thread worker + // whose precision was set via SetThreadPrecision. The global can be stale + // (e.g. still at the boost default) on MPI worker ranks. + const unsigned prec = ThreadPrecision(); // deal with variables - + // 1. ADD VARIABLES - slp_under_construction_.input_locations_.Variables = next_available_complex_; + program_under_construction_.input_locations_.Variables = next_available_complex_; auto variable_ordering = sys.VariableOrdering(); for (auto v: variable_ordering){ locations_encountered_nodes_[ v ] = next_available_complex_++; } - slp_under_construction_.number_of_.Variables = variable_ordering.size(); - // slp_under_construction_.input_locations_.Variables = variable_counter; + program_under_construction_.number_of_.Variables = variable_ordering.size(); // deal with path variable if (sys.HavePathVariable()) { // do this action only if the system has a path variable defined - slp_under_construction_.input_locations_.Time = next_available_complex_; + program_under_construction_.input_locations_.Time = next_available_complex_; locations_encountered_nodes_[ sys.GetPathVariable() ] = next_available_complex_++; - slp_under_construction_.has_path_variable_ = true; - } - - - - // make space for natural functions and derivatives. we omit the patches. - // 3. ADD FUNCTIONS - slp_under_construction_.number_of_.Functions = sys.NumNaturalFunctions(); - slp_under_construction_.output_locations_.Functions = next_available_complex_; - for (auto f: sys.GetNaturalFunctions()) - { - locations_top_level_functions_and_derivatives_[f] = next_available_complex_; // don't increment yet, we're listing it a few places. this is for an optimization that elides a copy for assignment. - locations_encountered_nodes_[f] = next_available_complex_++; - - #ifndef BERTINI_DISABLE_FUNCTION_TREE_SANITY_CHECKS - try{ - f->shared_from_this(); - } - catch (std::exception){ - throw std::runtime_error("top level function is not a function"); - } - #endif - - + program_under_construction_.has_path_variable_ = true; } + // 3. ADD FUNCTIONS AND DERIVATIVES (we omit the patches). + // + // Each output is a bare expression root: the natural functions' entry expressions, + // then the space derivatives, then the time derivatives. We reserve a contiguous + // output slot for every output, then visit each root (computing its value into its + // own slot) and emit an Assign copying that value into the reserved output slot. The + // compiler marks entry points itself via this explicit output list, rather than + // relying on a Function wrapper node (ADR-0027). + std::vector> function_roots; + for (auto const& f : sys.GetNaturalFunctions()) + function_roots.push_back(f); - - // always have space derivatives - - auto ds_dx = sys.GetSpaceDerivatives(); // a linear object, so can just run down the object - slp_under_construction_.number_of_.Jacobian = ds_dx.size(); - slp_under_construction_.output_locations_.Jacobian = next_available_complex_; - for (auto n: ds_dx) - { - locations_top_level_functions_and_derivatives_[n] = next_available_complex_; // don't increment yet, we're listing it a few places. this is for an optimization that elides a copy for assignment. - locations_encountered_nodes_[n] = next_available_complex_++; - } - - - - - // sometimes have time derivatives + auto ds_dx = sys.GetSpaceDerivatives(); + std::vector> ds_dt; + if (sys.HavePathVariable()) + for (auto const& d : sys.GetTimeDerivatives()) + ds_dt.push_back(d); + + // reserve the output slots, contiguously, in the order [functions | jacobian | timederiv] + program_under_construction_.number_of_.Functions = function_roots.size(); + program_under_construction_.output_locations_.Functions = next_available_complex_; + std::vector function_output_slots; + for (size_t i = 0; i < function_roots.size(); ++i) + function_output_slots.push_back(next_available_complex_++); + + program_under_construction_.number_of_.Jacobian = ds_dx.size(); + program_under_construction_.output_locations_.Jacobian = next_available_complex_; + std::vector jacobian_output_slots; + for (size_t i = 0; i < ds_dx.size(); ++i) + jacobian_output_slots.push_back(next_available_complex_++); + + std::vector time_deriv_output_slots; if (sys.HavePathVariable()) { - - auto ds_dt = sys.GetTimeDerivatives(); // a linear object, so can just run down the object - slp_under_construction_.number_of_.TimeDeriv = ds_dt.size(); - slp_under_construction_.output_locations_.TimeDeriv = next_available_complex_; // note the start of the block in memory. the size was also recorded in the previous line. - for (auto n: ds_dt) - { - locations_top_level_functions_and_derivatives_[n] = next_available_complex_; // don't increment yet, we're listing it a few places. this is for an optimization that elides a copy for assignment. - locations_encountered_nodes_[n] = next_available_complex_++; - } + program_under_construction_.number_of_.TimeDeriv = ds_dt.size(); + program_under_construction_.output_locations_.TimeDeriv = next_available_complex_; + for (size_t i = 0; i < ds_dt.size(); ++i) + time_deriv_output_slots.push_back(next_available_complex_++); } + // visit each output root and copy its value into the reserved output slot. A shared root + // is visited once (CSE), but every output position gets its own Assign. + auto wire_outputs = [&](auto const& roots, std::vector const& out_slots) { + for (size_t i = 0; i < roots.size(); ++i) { + auto const& r = roots[i]; + if (this->locations_encountered_nodes_.find(r) == this->locations_encountered_nodes_.end()) + r->Accept(*this); + program_under_construction_.AddInstruction(Assign, this->locations_encountered_nodes_[r], out_slots[i]); + } + }; + wire_outputs(function_roots, function_output_slots); + wire_outputs(ds_dx, jacobian_output_slots); + if (sys.HavePathVariable()) + wire_outputs(ds_dt, time_deriv_output_slots); + // the program's total slot count is the number of memory slots allocated during compilation + program_under_construction_.num_slots_ = next_available_complex_; - // now we're actually ready to start visiting using recursion, since we've recorded the locations of inputs and outputs. - - for (auto& f: sys.GetNaturalFunctions()) - { - f->shared_from_this(); - f->Accept(*this); - - // post visit function - /* code */ - } - - - - - // always do derivatives with respect to space variables - for (auto n: ds_dx) - n->Accept(*this); - - - - - // sometimes have time derivatives - if (sys.HavePathVariable()) { - - // we need derivatives with respect to time only if the system has a path variable defined - auto ds_dt = sys.GetTimeDerivatives(); // a linear object, so can just run down the object - for (auto n: ds_dt) - n->Accept(*this); - } - - - // Re-seed the thread-local default precision from the SLP's own precision before - // growing the mpfr_complex memory block. resize() default-constructs each new - // element via mpfr_init2(x, thread_default_precision()); on Boost 1.87 that value - // can be 0 on fresh threads, which aborts. DefaultPrecision() sets both the static - // and thread-local defaults so the default-constructed slots are valid. - DefaultPrecision(slp_under_construction_.precision_); - - // adjust the sizes of the memory blocks to match the number expected via compilation - slp_under_construction_.GetMemory().resize(next_available_complex_); - slp_under_construction_.GetMemory().resize(next_available_complex_); - + // Split the tape into a frozen (constants-only) prologue and a live segment, so a + // point-only re-evaluation can skip recomputing the constants (ADR-0027). This is a pure + // program operation (no memory needed). + program_under_construction_.PartitionInstructions(); - // downsample to get ready for evaluation - slp_under_construction_.CopyNumbersIntoMemory(); - slp_under_construction_.CopyNumbersIntoMemory(); + // Wrap the now-immutable program in a facade and set up a per-thread memory for it. + SLP result; + result.program_ = std::make_shared(std::move(program_under_construction_)); + result.memory_.precision_ = prec; + result.SetupMemory(); - return slp_under_construction_; + return result; } + // Explicit instantiations: compile from a whole System (classic / slp_test path) and from a + // PolynomialBlock (the fold -- the block compiles its own SLP in PolynomialBlock::Differentiate). + template SLP SLPCompiler::Compile(System const&); + template SLP SLPCompiler::Compile(blocks::PolynomialBlock const&); + void SLPCompiler::Clear(){ next_available_complex_ = 0; next_available_int_ = 0; locations_encountered_nodes_.clear(); - slp_under_construction_ = SLP(); + program_under_construction_ = SLPProgram(); } } diff --git a/core/src/system/system.cpp b/core/src/system/system.cpp index 4c0c8dd1a..1da2ceba5 100644 --- a/core/src/system/system.cpp +++ b/core/src/system/system.cpp @@ -24,9 +24,10 @@ #include "bertini2/system/system.hpp" +#include "bertini2/function_tree/find.hpp" -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using Nd = std::shared_ptr; BOOST_CLASS_EXPORT(bertini::System) @@ -37,21 +38,6 @@ namespace bertini { using namespace bertini::node; - - EvalMethod DefaultEvalMethod() - { - return EvalMethod::SLP; - } - - DerivMethod DefaultDerivMethod() - { - return DerivMethod::Derivatives; - } - - bool DefaultAutoSimplify() - { - return true; - } void swap(System & a, System & b) { @@ -73,24 +59,22 @@ namespace bertini swap(a.implicit_parameters_,b.implicit_parameters_); swap(a.explicit_parameters_,b.explicit_parameters_); - swap(a.constant_subfunctions_,b.constant_subfunctions_); - swap(a.subfunctions_,b.subfunctions_); - swap(a.functions_,b.functions_); - + // the polynomial path (functions / derivatives / SLP / eval+deriv method) lives in blocks_ + swap(a.blocks_,b.blocks_); swap(a.is_differentiated_,b.is_differentiated_); - swap(a.jacobian_,b.jacobian_); - - swap(a.space_derivatives_,b.space_derivatives_); - swap(a.time_derivatives_,b.time_derivatives_); - - swap(a.assume_uniform_precision_,b.assume_uniform_precision_); - swap(a.eval_method_,b.eval_method_); swap(a.precision_,b.precision_); swap(a.is_patched_,b.is_patched_); swap(a.patch_,b.patch_); } + // construct from a list of functions, auto-discovering the variables + System::System(std::vector const& functions) : System() + { + AddFunctions(functions); + AddVariableGroup( node::GatherVariables(functions) ); + } + // the copy constructor System::System(System const& other) : System() { @@ -105,15 +89,8 @@ namespace bertini patch_ = other.patch_; is_patched_ = other.is_patched_; - jacobian_ = other.jacobian_; - space_derivatives_ = other.space_derivatives_; - time_derivatives_ = other.time_derivatives_; - is_differentiated_ = other.is_differentiated_; - assume_uniform_precision_ = other.assume_uniform_precision_; - eval_method_ = other.eval_method_; - time_order_of_variable_groups_ = other.time_order_of_variable_groups_; current_variable_values_ = other.current_variable_values_; @@ -124,11 +101,11 @@ namespace bertini precision_ = other.precision_; - constant_subfunctions_ = other.constant_subfunctions_; - subfunctions_ = other .subfunctions_; - functions_ = other .functions_; explicit_parameters_ = other .explicit_parameters_; + // the polynomial path (functions / subfunctions / derivatives / SLP) lives in blocks_ + blocks_ = other.blocks_; + // // now to do the members which are not simply copied // constant_subfunctions_.resize(other.constant_subfunctions_.size()); // for (unsigned ii = 0; ii < constant_subfunctions_.size(); ++ii) @@ -164,7 +141,10 @@ namespace bertini size_t System::NumNaturalFunctions() const { - return functions_.size(); + size_t n = 0; + for (auto const& blk : blocks_) + n += std::visit([](auto const& b){ return b.NumFunctions(); }, blk); + return n; } @@ -215,7 +195,7 @@ namespace bertini size_t System::NumConstants() const { - return constant_subfunctions_.size(); + return PolyBlockPtr() ? PolyBlockPtr()->NumConstants() : 0; } size_t System::NumParameters() const @@ -237,81 +217,16 @@ namespace bertini void System::precision(unsigned new_precision) const { - if (this->assume_uniform_precision_ && new_precision == this->precision_) - return; - - for (const auto& iter : functions_) { - iter->precision(new_precision); - } - - for (const auto& iter : subfunctions_) { - iter->precision(new_precision); - } - - for (const auto& iter : explicit_parameters_) { - iter->precision(new_precision); - } - - - for (const auto& iter :implicit_parameters_) { - iter->precision(new_precision); - } - - for (const auto& iter : constant_subfunctions_) { - iter->precision(new_precision); - } - - if (is_differentiated_) - { - switch (eval_method_) - { - case EvalMethod::FunctionTree:{ - - switch (deriv_method_){ - case DerivMethod::JacobianNode:{ - for (const auto& iter : jacobian_) - iter->precision(new_precision); - break; - } - case DerivMethod::Derivatives:{ - for (const auto& iter : space_derivatives_) - iter->precision(new_precision); - for (const auto& iter : time_derivatives_) - iter->precision(new_precision); - break; - } - } - break; - } - case EvalMethod::SLP: - { - this->slp_.precision(new_precision); - break; - } - } - - } - - if (have_path_variable_) - path_variable_->precision(new_precision); - - - for (const auto& iter : homogenizing_variables_) - iter->precision(new_precision); - - for (const auto& iter : variable_groups_) - for (const auto& jter : iter) - jter->precision(new_precision); - - for (const auto& iter : hom_variable_groups_) - for (const auto& jter : iter) - jter->precision(new_precision); - - for (const auto& iter : ungrouped_variables_) - iter->precision(new_precision); + // Each block precisions its own evaluator (its SLP / coefficient sub-system). The + // parameter / function / variable nodes are no longer evaluated during tracking, so their + // precision is vestigial and left untouched -- this keeps the shared node DAG read-only + // across threads (ADR-0027). + for (auto const& blk : blocks_) + std::visit([&](auto const& b){ b.Precision(new_precision); }, blk); using bertini::Precision; Precision(std::get >(current_variable_values_),new_precision); + Precision(std::get(current_path_value_),new_precision); if (IsPatched()) patch_.Precision(new_precision); @@ -322,88 +237,29 @@ namespace bertini void System::Differentiate() const { - switch (deriv_method_){ - case DerivMethod::JacobianNode: - { - DifferentiateUsingJacobianNode(); - break; - } - case DerivMethod::Derivatives: - { - DifferentiateUsingDerivatives(); - break; - } - } - - - if (auto_simplify_) - this->SimplifyDerivatives(); - - - switch (eval_method_) - { - case EvalMethod::FunctionTree:{ - break; - } - case EvalMethod::SLP: - { - SLPCompiler compiler; - this->slp_ = compiler.Compile(*this); - break; - } - } - - - - } - - void System::DifferentiateUsingJacobianNode() const - { - auto num_functions = NumNaturalFunctions(); - jacobian_.resize(num_functions); - for (int ii = 0; ii < num_functions; ++ii) - jacobian_[ii] = Jacobian::Make(functions_[ii]->Differentiate()); - - is_differentiated_ = true; - } - - void System::DifferentiateUsingDerivatives() const - { - const auto& vars = this->Variables(); - const auto num_vars = NumVariables(); - const auto num_functions = NumNaturalFunctions(); - - space_derivatives_.resize(num_functions*num_vars); - // again, computing these in column major, so staying with one variable at a time. - for (int jj = 0; jj < num_vars; ++jj) - for (int ii = 0; ii < num_functions; ++ii) - space_derivatives_[ii+jj*num_functions] = Function::Make(functions_[ii]->Differentiate(vars[jj])); - - if (HavePathVariable()) - { - const auto& t = path_variable_; - time_derivatives_.resize(num_functions); - for (int ii = 0; ii < num_functions; ++ii) - time_derivatives_[ii] = Function::Make(functions_[ii]->Differentiate(t)); - } - + // push the System's variable ordering / path variable / auto-simplify into the + // polynomial block, then let each block differentiate itself (the polynomial block + // builds its derivatives + compiles its SLP; structured blocks are analytic no-ops). + SyncPolyBlock(); + for (auto const& blk : blocks_) + std::visit([](auto const& b){ b.Differentiate(); }, blk); is_differentiated_ = true; } std::vector< Nd > System::GetSpaceDerivatives() const { - if ( (deriv_method_==DerivMethod::JacobianNode) || (!is_differentiated_) ) - DifferentiateUsingDerivatives(); - - return space_derivatives_; + SyncPolyBlock(); + if (auto* p = PolyBlockPtr()) + return p->GetSpaceDerivatives(); + return {}; } std::vector< Nd > System::GetTimeDerivatives() const { - if ( (deriv_method_==DerivMethod::JacobianNode) || (!is_differentiated_) ) - DifferentiateUsingDerivatives(); - - return time_derivatives_; + SyncPolyBlock(); + if (auto* p = PolyBlockPtr()) + return p->GetTimeDerivatives(); + return {}; } void System::Homogenize() @@ -417,23 +273,26 @@ namespace bertini // * not partially homogenized, in the sense that some groups have been homogenized, and others haven't // // - for (const auto& curr_function : functions_) - { - for (const auto& curr_var_gp : hom_variable_groups_) - { - if (!curr_function->IsHomogeneous(curr_var_gp)) + for (const auto& curr_var_gp : hom_variable_groups_) + for (auto const& b : blocks_) + if (!std::visit([&](auto const& blk){ return blk.IsHomogeneous(curr_var_gp); }, b)) throw std::runtime_error("inhomogeneous function, with homogeneous variable group"); - } - } if (!IsPolynomial()) throw std::runtime_error("trying to homogenize a non-polynomial system."); bool already_had_homvars = NumHomVariables()!=0; - + if (already_had_homvars && NumHomVariables()!=NumVariableGroups()) throw std::runtime_error("size mismatch on number of homogenizing variables and number of variable groups"); + // idempotency: homogenizing an already-homogenized system must be a no-op. + // without this, a second call re-homogenizes each function with respect to + // the group INCLUDING its homogenizing variable, inflating degrees (observed + // 2026-06-06: degrees (2,1) -> (3,2), turning a 2-path TD into a 6-path one). + if (already_had_homvars && IsHomogeneous()) + return; + if (!already_had_homvars) { homogenizing_variables_.resize(NumVariableGroups()); @@ -462,22 +321,63 @@ namespace bertini PushFront(temp_group, hom_var); - // temp_group.push_front(hom_var); - for (const auto& curr_function : functions_) - curr_function->Homogenize(temp_group, hom_var); + // every block homogenizes itself w.r.t. this group: the polynomial block walks + // its trees, structured blocks fold the constant onto the homogenizing variable. + for (auto& b : blocks_) + std::visit([&](auto& blk){ blk.Homogenize(temp_group, hom_var); }, b); } else { Var hom_var = Variable::Make(converter.str()); homogenizing_variables_[group_counter] = hom_var; - for (const auto& curr_function : functions_) - curr_function->Homogenize(*curr_var_gp, hom_var); + for (auto& b : blocks_) + std::visit([&](auto& blk){ blk.Homogenize(*curr_var_gp, hom_var); }, b); } group_counter++; } - is_differentiated_ = false; + InvalidateDifferentiation(); + have_ordering_ = false; + + #ifndef BERTINI_DISABLE_ASSERTS + assert(homogenizing_variables_.size() == variable_groups_.size()); + #endif + } + + + void System::Homogenize(VariableGroup const& provided_hom_vars) + { + // Like Homogenize(), but adopt the supplied homogenizing variables (one per affine variable + // group, in group order) instead of minting fresh ones. Mirrors Homogenize()'s + // fresh-system branch exactly; the only difference is where the hom var comes from. + for (const auto& curr_var_gp : hom_variable_groups_) + for (auto const& b : blocks_) + if (!std::visit([&](auto const& blk){ return blk.IsHomogeneous(curr_var_gp); }, b)) + throw std::runtime_error("inhomogeneous function, with homogeneous variable group"); + + if (!IsPolynomial()) + throw std::runtime_error("trying to homogenize a non-polynomial system."); + + if (NumHomVariables()!=0) + throw std::runtime_error("Homogenize(provided homogenizing variables): system is already homogenized."); + + if (provided_hom_vars.size()!=NumVariableGroups()) + throw std::runtime_error("Homogenize(provided homogenizing variables): need exactly one homogenizing variable per affine variable group."); + + homogenizing_variables_.resize(NumVariableGroups()); + + auto group_counter = 0; + for (auto curr_var_gp = variable_groups_.begin(); curr_var_gp!=variable_groups_.end(); curr_var_gp++) + { + Var hom_var = provided_hom_vars[group_counter]; + homogenizing_variables_[group_counter] = hom_var; + for (auto& b : blocks_) + std::visit([&](auto& blk){ blk.Homogenize(*curr_var_gp, hom_var); }, b); + group_counter++; + } + + InvalidateDifferentiation(); have_ordering_ = false; #ifndef BERTINI_DISABLE_ASSERTS @@ -501,28 +401,29 @@ namespace bertini if (NumHomVariables()!=NumVariableGroups()) return false; - for (const auto& iter : functions_) - { - auto counter = 0; - for (const auto& vars : variable_groups_) - { - auto tempvars = vars; - if (have_homvars) - PushFront(tempvars, homogenizing_variables_[counter]); - counter++; - - if (!iter->IsHomogeneous(tempvars)) - return false; - } - - for (const auto& vars : hom_variable_groups_) - if (!iter->IsHomogeneous(vars)) + auto all_blocks_homogeneous = [&](VariableGroup const& tempvars) -> bool { + for (auto const& b : blocks_) + if (!std::visit([&](auto const& blk){ return blk.IsHomogeneous(tempvars); }, b)) return false; + return true; + }; - if (NumUngroupedVariables()>0) - if (!iter->IsHomogeneous(ungrouped_variables_)) - return false; + auto counter = 0; + for (const auto& vars : variable_groups_) + { + auto tempvars = vars; + if (have_homvars) + PushFront(tempvars, homogenizing_variables_[counter]); + counter++; + if (!all_blocks_homogeneous(tempvars)) + return false; } + for (const auto& vars : hom_variable_groups_) + if (!all_blocks_homogeneous(vars)) + return false; + if (NumUngroupedVariables()>0) + if (!all_blocks_homogeneous(ungrouped_variables_)) + return false; return true; } @@ -541,27 +442,26 @@ namespace bertini if (have_homvars && NumHomVariables()!=NumVariableGroups()) throw std::runtime_error("trying to check polynomiality on a partially-formed system. mismatch between number of homogenizing variables, and number of variable groups"); - - for (const auto& iter : functions_) - { - auto counter = 0; - for (const auto& vars : variable_groups_) - { - auto tempvars = vars; - if (have_homvars) - PushFront(tempvars,homogenizing_variables_[counter]); - - counter++; - - if (!iter->IsPolynomial(tempvars)) - return false; - - } - for (const auto& vars : hom_variable_groups_) - if (!iter->IsPolynomial(vars)) + auto all_blocks_polynomial = [&](VariableGroup const& tempvars) -> bool { + for (auto const& b : blocks_) + if (!std::visit([&](auto const& blk){ return blk.IsPolynomial(tempvars); }, b)) return false; + return true; + }; + auto counter = 0; + for (const auto& vars : variable_groups_) + { + auto tempvars = vars; + if (have_homvars) + PushFront(tempvars,homogenizing_variables_[counter]); + counter++; + if (!all_blocks_polynomial(tempvars)) + return false; } + for (const auto& vars : hom_variable_groups_) + if (!all_blocks_polynomial(vars)) + return false; return true; } @@ -585,7 +485,7 @@ namespace bertini void System::AddVariableGroup(VariableGroup const& v) { variable_groups_.push_back(v); - is_differentiated_ = false; + InvalidateDifferentiation(); have_ordering_ = false; is_patched_ = false; time_order_of_variable_groups_.push_back( VariableGroupType::Affine); @@ -594,10 +494,32 @@ namespace bertini + void System::SetVariableGroups(std::vector const& groups) + { + // clear the existing variable structure, but preserve the path variable. + ungrouped_variables_.clear(); + variable_groups_.clear(); + hom_variable_groups_.clear(); + homogenizing_variables_.clear(); + time_order_of_variable_groups_.clear(); + + // install the supplied groups as affine variable groups. AddVariableGroup + // takes care of the FIFO time-ordering entries and resets the relevant flags. + for (auto const& g : groups) + AddVariableGroup(g); + + InvalidateDifferentiation(); + have_ordering_ = false; + is_patched_ = false; + } + + + + void System::AddHomVariableGroup(VariableGroup const& v) { hom_variable_groups_.push_back(v); - is_differentiated_ = false; + InvalidateDifferentiation(); have_ordering_ = false; is_patched_ = false; time_order_of_variable_groups_.push_back( VariableGroupType::Homogeneous); @@ -610,7 +532,7 @@ namespace bertini void System::AddUngroupedVariable(Var const& v) { ungrouped_variables_.push_back(v); - is_differentiated_ = false; + InvalidateDifferentiation(); have_ordering_ = false; is_patched_ = false; time_order_of_variable_groups_.push_back( VariableGroupType::Ungrouped); @@ -622,11 +544,10 @@ namespace bertini void System::AddUngroupedVariables(VariableGroup const& v) { ungrouped_variables_.insert( ungrouped_variables_.end(), v.begin(), v.end() ); - is_differentiated_ = false; + InvalidateDifferentiation(); have_ordering_ = false; is_patched_ = false; - for (const auto& iter : v) - time_order_of_variable_groups_.push_back( VariableGroupType::Ungrouped); + time_order_of_variable_groups_.insert(time_order_of_variable_groups_.end(), v.size(), VariableGroupType::Ungrouped); } @@ -635,7 +556,7 @@ namespace bertini void System::AddImplicitParameter(Var const& v) { implicit_parameters_.push_back(v); - is_differentiated_ = false; + InvalidateDifferentiation(); } @@ -644,7 +565,7 @@ namespace bertini void System::AddImplicitParameters(VariableGroup const& v) { implicit_parameters_.insert( implicit_parameters_.end(), v.begin(), v.end() ); - is_differentiated_ = false; + InvalidateDifferentiation(); } @@ -655,62 +576,33 @@ namespace bertini - void System::AddParameter(Fn const& F) + void System::AddParameter(NE const& F) { explicit_parameters_.push_back(F); - is_differentiated_ = false; - } - - - - void System::AddParameters(std::vector const& v) - { - explicit_parameters_.insert( explicit_parameters_.end(), v.begin(), v.end() ); - is_differentiated_ = false; - } - - - - - - void System::AddSubfunction(Fn const& F) - { - subfunctions_.push_back(F); - is_differentiated_ = false; + InvalidateDifferentiation(); } - void System::AddSubfunctions(std::vector const& v) - { - subfunctions_.insert( subfunctions_.end(), v.begin(), v.end() ); - is_differentiated_ = false; - } - - void System::AddFunction(Fn const& F) - { - functions_.push_back(F); - is_differentiated_ = false; - } - void System::AddFunction(Nd const& N, std::string const& name) + void System::AddFunction(Nd const& N) { - functions_.push_back(Function::Make(N, name)); - is_differentiated_ = false; + PolyBlock().AddFunction(N); + InvalidateDifferentiation(); } - void System::AddFunctions(std::vector const& v) + void System::AddFunctions(std::vector const& v) { - functions_.insert( functions_.end(), v.begin(), v.end() ); - is_differentiated_ = false; + for (auto const& f : v) PolyBlock().AddFunction(f); + InvalidateDifferentiation(); } @@ -718,17 +610,10 @@ namespace bertini - void System::AddConstant(Fn const& F) - { - constant_subfunctions_.push_back(F); - is_differentiated_ = false; - } - - - void System::AddConstants(std::vector const& v) + void System::AddConstant(NE const& F) { - constant_subfunctions_.insert( constant_subfunctions_.end(), v.begin(), v.end() ); - is_differentiated_ = false; + PolyBlock().AddConstant(F); + InvalidateDifferentiation(); } @@ -739,7 +624,7 @@ namespace bertini void System::AddPathVariable(Var const& v) { path_variable_ = v; - is_differentiated_ = false; + InvalidateDifferentiation(); have_path_variable_ = true; } @@ -877,13 +762,13 @@ namespace bertini std::vector s; - unsigned hom_group_counter(0), affine_group_counter(0), patch_counter(0); + unsigned hom_group_counter(0), affine_group_counter(0); for (auto curr_grouptype : time_order_of_variable_groups_) { if (curr_grouptype==VariableGroupType::Homogeneous) - s.push_back(hom_variable_groups_[hom_group_counter++].size()); + s.push_back(static_cast(hom_variable_groups_[hom_group_counter++].size())); else if (curr_grouptype==VariableGroupType::Affine) - s.push_back(variable_groups_[affine_group_counter++].size() + static_cast(have_homvars)); + s.push_back(static_cast(variable_groups_[affine_group_counter++].size() + (have_homvars ? 1 : 0))); } return s; } @@ -934,21 +819,21 @@ namespace bertini { static_assert(Eigen::NumTraits::IsComplex,"NumT must be a complex type"); - using RT = typename Eigen::NumTraits::Real; - using CT = NumT; + using RealT = typename Eigen::NumTraits::Real; + using ComplexT = NumT; - RT bound(0); + RealT bound(0); for (unsigned ii=0; ii < num_evaluations; ii++) { - Vec randy = RandomOfUnits(NumVariables()); - Vec f_vals; + Vec randy = RandomOfUnits(static_cast(NumVariables())); + Vec f_vals; if (HavePathVariable()) - f_vals = Eval(randy, RandomUnit()); + f_vals = Eval(randy, RandomUnit()); else f_vals = Eval(randy); - Mat dh_dx = Jacobian(); + Mat dh_dx = Jacobian(); bound = max(f_vals.array().abs().maxCoeff(), dh_dx.array().abs().maxCoeff(), bound); @@ -962,6 +847,8 @@ namespace bertini int System::DegreeBound() const { auto degs = Degrees(Variables()); + if (degs.empty()) + return 0; // a system with no functions has no degree bound return *std::max_element(degs.begin(), degs.end()); } @@ -969,8 +856,11 @@ namespace bertini std::vector System::Degrees() const { std::vector degs; - for (const auto& iter : functions_) - degs.push_back(iter->Degree()); + for (auto const& b : blocks_) + { + auto d = std::visit([](auto const& blk){ return blk.Degrees(); }, b); + degs.insert(degs.end(), d.begin(), d.end()); + } return degs; } @@ -978,11 +868,315 @@ namespace bertini std::vector System::Degrees(VariableGroup const& vars) const { std::vector degs; - for (const auto& iter : functions_) - degs.push_back(iter->Degree(vars)); + for (auto const& b : blocks_) + { + auto d = std::visit([&](auto const& blk){ return blk.Degrees(vars); }, b); + degs.insert(degs.end(), d.begin(), d.end()); + } return degs; + } + + + // + // ExpandToFunctionTree -- build the pure function-tree twin of a block-composed system. + // A verification / interop oracle (see the header). Scoped to the current block types. + // + namespace { + + // Build the function-tree node for a single linear form sum_c M(r,c)*var_c + M(r,n), + // where row r of M holds the (augmented) coefficients and `vars` are the ordered variable + // nodes (column c <-> vars[c]); the last column is the constant / augmenting term. Zero + // coefficients are skipped to keep the tree compact (and exact: a skipped term is +0). + Nd LinearFormNode(Mat const& M, Eigen::Index r, + VariableGroup const& vars, size_t num_vars) + { + Nd form = node::Float::Make(M(r, static_cast(num_vars))); // constant term + for (size_t c = 0; c < num_vars; ++c) + { + mpfr_complex const& coeff = M(r, static_cast(c)); + if (coeff.real() == 0 && coeff.imag() == 0) + continue; + form = form + node::Float::Make(coeff) * vars[c]; + } + return form; } + } // anonymous namespace + + + std::vector System::NaturalFunctionsAsNodes() const + { + using namespace bertini::node; + std::vector out; + + auto const& vars = Variables(); // ordered variable nodes; block column c <-> vars[c] + + for (auto const& blk : blocks_) + { + std::visit([&](auto const& b) + { + using B = std::decay_t; + + if constexpr (std::is_same_v) + { + // already function-tree: each stored function is the bare expression node. + for (auto const& f : b.Functions()) + out.push_back(f); + } + else if constexpr (std::is_same_v) + { + // f_i = prod_r ( row r of factor-matrix i . [vars ; 1] ) + const size_t n = b.NumVariables(); + if (static_cast(vars.size()) != n) + throw std::runtime_error("ExpandToFunctionTree: products-of-linears variable count mismatch"); + for (auto const& M : b.Factors()) + { + Nd prod = nullptr; + for (Eigen::Index r = 0; r < M.rows(); ++r) + { + Nd factor = LinearFormNode(M, r, vars, n); + prod = prod ? (prod * factor) : factor; + } + out.push_back(prod ? prod : Nd(Integer::Make(1))); // empty product == 1 + } + } + else if constexpr (std::is_same_v>) + { + // H = sum_i c_i(t) * operand_i, each operand expanded to nodes (recursion). + auto const& coeffs = b.Coefficients(); + auto const& operands = b.Operands(); + const size_t k = b.NumFunctions(); + std::vector blended(k, nullptr); + for (size_t i = 0; i < operands.size(); ++i) + { + std::vector fi = operands[i]->NaturalFunctionsAsNodes(); + if (fi.size() < k) + throw std::runtime_error("ExpandToFunctionTree: blend operand has too few functions"); + for (size_t j = 0; j < k; ++j) + { + Nd term = coeffs[i] * fi[j]; + blended[j] = blended[j] ? (blended[j] + term) : term; + } + } + for (auto& f : blended) + out.push_back(f ? f : Nd(Integer::Make(0))); + } + else if constexpr (std::is_same_v>) + { + // g_i = sum_j c_ij * f_j * prod_g h_g^{(D_{i,g} - d_{j,g})}, the operand functions + // expanded recursively and the homogenizing-variable powers folded back in (so the + // expansion matches the block's homogenized evaluation). + std::vector fj = b.Operand()->NaturalFunctionsAsNodes(); // N nodes + auto const& R = b.RandomizationMatrix(); // n x N + auto const& tgt = b.TargetMultidegrees(); + auto const& omd = b.OperandMultidegrees(); + auto const& homvars = b.HomVars(); + const bool hom = b.IsHomogenized(); + const size_t n = b.NumFunctions(); + const size_t N = fj.size(); + for (size_t i = 0; i < n; ++i) + { + Nd gi = nullptr; + for (size_t j = 0; j < N; ++j) + { + mpfr_complex const& c = R(static_cast(i), static_cast(j)); + if (c.real() == 0 && c.imag() == 0) + continue; + Nd term = Float::Make(c) * fj[j]; + if (hom) + for (size_t g = 0; g < homvars.size(); ++g) + { + const int e = tgt[i][g] - omd[j][g]; + if (e > 0) + term = term * pow(homvars[g], e); + } + gi = gi ? (gi + term) : term; + } + out.push_back(gi ? gi : Nd(Integer::Make(0))); + } + } + else if constexpr (std::is_same_v) + { + // each row is one affine linear form sum_c M(r,c)*vars[c] (+ constant). Affine: + // the trailing column is the constant. Homogeneous (post-Homogenize): every column + // is a variable column (the old constant is now the homogenizing-variable coeff). + auto const& M = b.Coefficients(); + const size_t n = b.NumVariables(); + if (static_cast(vars.size()) != n) + throw std::runtime_error("ExpandToFunctionTree: linear-forms variable count mismatch"); + for (Eigen::Index r = 0; r < M.rows(); ++r) + { + if (b.IsHomogenized()) + { + Nd form = nullptr; + for (size_t c = 0; c < n; ++c) + { + mpfr_complex const& coeff = M(r, static_cast(c)); + if (coeff.real() == 0 && coeff.imag() == 0) + continue; + Nd term = node::Float::Make(coeff) * vars[c]; + form = form ? (form + term) : term; + } + out.push_back(form ? form : Nd(Integer::Make(0))); + } + else + { + out.push_back(LinearFormNode(M, r, vars, n)); // augmented: last col is the constant + } + } + } + else // any future block + { + throw std::runtime_error("ExpandToFunctionTree: block type not yet supported"); + } + }, blk); + } + + return out; + } + + + System System::ExpandToFunctionTree() const + { + // expand THIS system's blocks to nodes first (reads the current block structure)... + std::vector nodes = NaturalFunctionsAsNodes(); + + // ...then build the twin from a copy (same variables / groups / hom vars / path variable / + // patch / ordering), with all blocks replaced by one PolynomialBlock of those nodes. + System result = *this; + result.ClearBlocks(); + for (auto const& f : nodes) + result.AddFunction(f); + result.InvalidateDifferentiation(); + return result; + } + + + // + // Randomize -- square up an overdetermined system (see the header). Construction (degrees, + // sorting, the coefficient matrix) happens here, on a copy, where System is complete; the + // RandomizationBlock just stores the finished matrix and multidegrees and evaluates. + // + namespace { + + // operand->Degrees(group_g)[j] gathered into operand_multidegrees[j][g]. + std::vector> OperandMultidegrees(System const& operand) + { + auto groups = operand.VariableGroups(); + const size_t G = groups.size(); + const size_t N = operand.NumNaturalFunctions(); + std::vector> md(N, std::vector(G, 0)); + for (size_t g = 0; g < G; ++g) + { + auto dg = operand.Degrees(groups[g]); // length N: degree of each function in group g + for (size_t j = 0; j < N && j < dg.size(); ++j) + md[j][g] = dg[j]; + } + return md; + } + + } // anonymous namespace + + + System System::AssembleRandomized(std::shared_ptr operand, Mat coefficients) const + { + const size_t G = operand->NumVariableGroups(); + const size_t N = operand->NumNaturalFunctions(); + const size_t n = static_cast(coefficients.rows()); + + if (static_cast(coefficients.cols()) != N) + throw std::runtime_error("Randomize: coefficient matrix column count must equal the number of natural functions."); + + auto operand_md = OperandMultidegrees(*operand); + + // Row i's target multidegree is, per group, the largest degree among the functions actually + // combined into it (those with a nonzero coefficient). This makes every h-power deficit + // D_{i,g} - d_{j,g} >= 0, and -- with the descending sort the auto path uses -- equal to the + // row's own leading-function degree, so the path count is minimal. + std::vector> target_md(n, std::vector(G, 0)); + for (size_t i = 0; i < n; ++i) + for (size_t j = 0; j < N; ++j) + { + mpfr_complex const& c = coefficients(static_cast(i), static_cast(j)); + if (c.real() == 0 && c.imag() == 0) + continue; + for (size_t g = 0; g < G; ++g) + target_md[i][g] = std::max(target_md[i][g], operand_md[j][g]); + } + + blocks::RandomizationBlock block(operand, std::move(coefficients), + std::move(target_md), std::move(operand_md), G); + + System result = *this; // share variables / groups / path variable / ordering + result.ClearBlocks(); // drop this system's own functions... + result.AddBlock(std::move(block)); // ...the randomized rows come from the block + result.InvalidateDifferentiation(); + return result; + } + + + System System::Randomize() const + { + const size_t G = NumVariableGroups(); + const size_t N = NumNaturalFunctions(); + const size_t n = NumVariables() - NumHomVariableGroups(); + + if (N < n) + throw std::runtime_error("Randomize: system is underdetermined (fewer functions than variables), so it has no isolated solutions to capture."); + + auto operand = std::make_shared(*this); + + Mat R(static_cast(n), static_cast(N)); + + if (G == 1) + { + // single affine group: sort the operand's functions by descending degree, then R = [I | C]. + // The identity block makes g_i carry f_i with coefficient 1 (degree d_i); the random tail + // C folds the lower-degree functions in, padded by hom-var powers. deg g_i = d_i, so the + // total-degree path count is the product of the n largest degrees -- optimal. + operand->ReorderFunctionsByDegreeDecreasing(); + for (size_t i = 0; i < n; ++i) + for (size_t j = 0; j < N; ++j) + { + if (j < n) + R(static_cast(i), static_cast(j)) = + (i == j) ? mpfr_complex(1) : mpfr_complex(0); + else + R(static_cast(i), static_cast(j)) = + bertini::multiprecision::RandomComplex(DefaultPrecision()); + } + } + else + { + // several variable groups: multidegrees are only partially ordered, so use a dense random + // R with a common (componentwise-max) target multidegree -- correct, and optimal when the + // functions share a multidegree. + for (Eigen::Index r = 0; r < R.rows(); ++r) + for (Eigen::Index c = 0; c < R.cols(); ++c) + R(r, c) = bertini::multiprecision::RandomComplex(DefaultPrecision()); + } + + return AssembleRandomized(operand, std::move(R)); + } + + + System System::Randomize(Mat const& R) const + { + if (static_cast(R.cols()) != NumNaturalFunctions()) + throw std::runtime_error("Randomize: supplied matrix must have one column per natural function of the system."); + auto operand = std::make_shared(*this); // functions kept in their current order + return AssembleRandomized(operand, R); + } + + + Mat System::RandomizationMatrix() const + { + for (auto const& b : blocks_) + if (auto const* rb = std::get_if>(&b)) + return rb->RandomizationMatrix(); + throw std::runtime_error("RandomizationMatrix: this system has no randomization block (it was not produced by Randomize())."); + } + void System::ReorderFunctionsByDegreeDecreasing() { @@ -998,16 +1192,16 @@ namespace bertini // finally, we re-order the functions based on the indices we just computed - std::vector > re_ordered_functions(degs.size()); + std::vector > re_ordered_functions(degs.size()); size_t ind = 0; for (auto iter : indices) { - re_ordered_functions[ind] = functions_[iter]; + re_ordered_functions[ind] = PolyBlock().Functions()[iter]; ind++; } - swap(functions_, re_ordered_functions); - is_differentiated_ = false; + swap(PolyBlock().Functions(), re_ordered_functions); + InvalidateDifferentiation(); } @@ -1026,16 +1220,16 @@ namespace bertini // finally, we re-order the functions based on the indices we just computed - std::vector > re_ordered_functions(degs.size()); + std::vector > re_ordered_functions(degs.size()); size_t ind = 0; for (auto iter : indices) { - re_ordered_functions[ind] = functions_[iter]; + re_ordered_functions[ind] = PolyBlock().Functions()[iter]; ind++; } - swap(functions_, re_ordered_functions); - is_differentiated_ = false; + swap(PolyBlock().Functions(), re_ordered_functions); + InvalidateDifferentiation(); } @@ -1064,82 +1258,34 @@ namespace bertini path_variable_.reset(); have_path_variable_ = false; - is_differentiated_ = false; + InvalidateDifferentiation(); have_ordering_ = false; } + + + void System::SimplifyFunctions() { using bertini::Simplify; - for (auto& iter : this->functions_) - Simplify(iter); + if (auto* p = PolyBlockPtr()) + p->SimplifyFunctions(); - is_differentiated_ = false; + InvalidateDifferentiation(); } void System::SimplifyDerivatives() const { - using bertini::Simplify; - - auto num_vars = this->NumVariables(); - std::vector old_vals(num_vars); dbl old_path_var_val; - - auto vars = this->Variables(); - for (unsigned ii=0; iiEval(); - vars[ii]->SetToRandUnit(); - } - - if (HavePathVariable()) + SyncPolyBlock(); + if (auto* p = PolyBlockPtr()) { - old_path_var_val = path_variable_->Eval(); - path_variable_->SetToRandUnit(); + if (!p->IsDifferentiated()) p->Differentiate(); + p->SimplifyDerivatives(); } - - - for (const auto& n : jacobian_) - n->Reset(); - for (const auto& n : space_derivatives_) - n->Reset(); - for (const auto& n : time_derivatives_) - n->Reset(); - - - - switch (deriv_method_){ - case DerivMethod::JacobianNode:{ - for (auto& iter : this->jacobian_) - Simplify(iter); - break; - } - case DerivMethod::Derivatives:{ - for (auto& iter : this->space_derivatives_) - Simplify(iter); - for (auto& iter : this->time_derivatives_) - Simplify(iter); - break; - } - } - - - for (unsigned ii=0; iiset_current_value(old_vals[ii]); - if (HavePathVariable()) - path_variable_->set_current_value(old_path_var_val); - - - for (const auto& n : jacobian_) - n->Reset(); - for (const auto& n : space_derivatives_) - n->Reset(); - for (const auto& n : time_derivatives_) - n->Reset(); - } @@ -1166,121 +1312,96 @@ namespace bertini // //////////////////// - std::ostream& operator<<(std::ostream& out, const bertini::System & s) + void System::Describe(std::ostream& out, bool verbose) const { - - - out << s.NumVariableGroups() << " variable groups, containing these variables:\n"; - auto counter = 0; - for (const auto& iter : s.variable_groups_) + // --- variables --- + out << NumVariableGroups() << (NumVariableGroups() == 1 ? " variable group:\n" : " variable groups:\n"); { - out << "group " << counter << ": "<< "\n"; - for (auto jter : iter) - out << *jter << " "; - - - out << "\n"; - counter++; + auto counter = 0; + for (const auto& grp : variable_groups_) + { + out << " group " << counter++ << ": "; + for (auto const& v : grp) out << *v << " "; + out << "\n"; + } } - out << "\n"; - - out << s.NumHomVariables() << " homogenizing variables:\n"; - for (const auto& iter : s.homogenizing_variables_) - out << (*iter) << " "; - out << "\n\n"; - - - out << s.ungrouped_variables_.size() << " ungrouped variables:\n"; - for (const auto& v :s.ungrouped_variables_) - out << (*v) << " "; - out << "\n\n"; - - - out << s.NumNaturalFunctions() << " functions:\n"; - for (const auto& iter : s.functions_) - out << (iter)->name() << " = " << *iter << "\n"; - out << "\n"; - - - if (s.NumParameters()) { - out << s.NumParameters() << " explicit parameters:\n"; - for (const auto& iter : s.explicit_parameters_) - out << (iter)->name() << " = " << *iter << "\n"; + if (!hom_variable_groups_.empty()) + { + out << NumHomVariableGroups() << " projective variable groups:\n"; + auto counter = 0; + for (const auto& grp : hom_variable_groups_) + { + out << " group " << counter++ << ": "; + for (auto const& v : grp) out << *v << " "; + out << "\n"; + } + } + if (NumHomVariables() != 0) + { + out << " homogenizing variables: "; + for (const auto& v : homogenizing_variables_) out << *v << " "; out << "\n"; } - - - if (s.NumConstants()) { - out << s.NumConstants() << " constants:\n"; - for (const auto& iter : s.constant_subfunctions_) - out << (iter)->name() << " = " << *iter << "\n"; + if (!ungrouped_variables_.empty()) + { + out << " ungrouped variables: "; + for (const auto& v : ungrouped_variables_) out << *v << " "; out << "\n"; } - if (s.path_variable_) - out << "path variable defined. named " << s.path_variable_->name() << "\n"; - else - out << "no path variable defined\n"; - - if (s.is_differentiated_) + // --- functions, block by block --- + out << "\n" << NumNaturalFunctions() << (NumNaturalFunctions() == 1 ? " function:\n" : " functions:\n"); + VariableGroup vars; + try { vars = VariableOrdering(); } catch (...) { /* unordered/malformed: print without var names */ } + size_t row = 0; + for (auto const& blk : blocks_) + std::visit([&](auto const& b){ b.Describe(out, row, vars, verbose); }, blk); + + // --- named subexpressions: the functions above print these by name; show each one's value + // here. They are not stored separately --- they are discovered (Find) in the function trees + // they are embedded in (nested ones included). + if (auto* p = PolyBlockPtr()) { - out << "system is differentiated; jacobian:\n"; - - switch (s.deriv_method_){ - case DerivMethod::JacobianNode:{ - out << "using the JacobianNode method of differentiation:" << std::endl; - - for (const auto& iter : s.jacobian_) - out << (iter)->name() << " = " << *iter << "\n"; - break; - } - - case DerivMethod::Derivatives:{ - out << "using the Derivatives method of differentiation:" << std::endl; - - for (int jj = 0; jj < s.NumVariables(); ++jj) - for (int ii = 0; ii < s.NumNaturalFunctions(); ++ii) - { - const auto& d = s.space_derivatives_[ii+jj*s.NumNaturalFunctions()]; - out << "jac_space_der(" << ii << "," << jj << ") = " << d << "\n"; - } - - if (s.HavePathVariable()) - for (int ii = 0; ii < s.NumNaturalFunctions(); ++ii) - { - const auto& d = s.time_derivatives_[ii]; - out << "jac_time_der(" << ii << ") = " << d << "\n"; - } - break; - } - } // switch on deriv method - - - - if (s.eval_method_ == EvalMethod::SLP) - { - out << "since using SLP for evaluation, here's the SLP:" << std::endl; - out << s.slp_; - } + std::vector> roots(p->Functions().begin(), p->Functions().end()); + auto named = node::Find(roots); + if (!named.empty()) + { + out << "\n" << named.size() << (named.size() == 1 ? " named subexpression:\n" : " named subexpressions:\n"); + for (auto const& ne : named) + out << " " << ne->name() << " = " << ne->EntryNode() << "\n"; + } + } - out << "\n"; + // --- parameters / constants (only when present) --- + if (NumParameters()) + { + out << "\n" << NumParameters() << " explicit parameters:\n"; + for (const auto& p : explicit_parameters_) + out << " " << p->name() << " = " << p->EntryNode() << "\n"; } - else{ - out << "system not differentiated\n"; + if (NumConstants()) + { + out << "\n" << NumConstants() << " constants:\n"; + for (const auto& c : PolyBlockPtr()->ConstantSubfunctions()) + out << " " << c->name() << " = " << c->EntryNode() << "\n"; } - if (s.IsPatched()) + // --- path variable / patch (only the informative bits) --- + if (path_variable_) + out << "\npath variable: " << path_variable_->name() << "\n"; + if (IsPatched()) { - out << s.patch_; - } - else{ - out << "system not patched\n"; + if (verbose) + out << "\n" << patch_; + else + out << "\npatched (" << NumPatches() << (NumPatches() == 1 ? " patch)\n" : " patches)\n"); } + } - out << "\ncurrent variable values:\n"; - out << std::get< Vec > (s.current_variable_values_) << "\n"; - out << std::get< Vec > (s.current_variable_values_) << "\n"; + std::ostream& operator<<(std::ostream& out, const bertini::System & s) + { + s.Describe(out, /*verbose=*/false); return out; } @@ -1326,10 +1447,20 @@ namespace bertini if (this->patch_ != rhs.patch_) throw std::runtime_error("System+=System cannot combine two patched systems whose patches differ."); - for (auto iter=functions_.begin(); iter!=functions_.end(); iter++) - (*iter)->SetRoot( (*(rhs.functions_.begin()+(iter-functions_.begin())))->EntryNode() + (*iter)->EntryNode()); + // make NEW Function wrappers rather than calling SetRoot on the existing + // ones: the existing Function nodes are shared_ptrs, SHARED with whatever + // system this one was (shallowly) copied from. mutating them in place + // rewrites that system's functions too — e.g. forming the homotopy + // (1-t)*target + gamma*t*start used to corrupt both the target and the + // start system (observed 2026-06-06). + { + auto& lhsf = PolyBlock().Functions(); + auto const& rhsf = rhs.PolyFunctions(); + for (size_t ii = 0; ii < lhsf.size(); ++ii) + lhsf[ii] = rhsf[ii] + lhsf[ii]; + } - is_differentiated_ = false; + InvalidateDifferentiation(); return *this; } @@ -1341,11 +1472,12 @@ namespace bertini System& System::operator*=(std::shared_ptr const& N) { - for (auto iter=functions_.begin(); iter!=functions_.end(); iter++) + // new wrappers, not SetRoot — see comment in operator+= above. + for (auto& f : PolyBlock().Functions()) { - (*iter)->SetRoot( N * (*iter)->EntryNode()); + f = N * f; } - is_differentiated_ = false; + InvalidateDifferentiation(); return *this; } @@ -1377,7 +1509,7 @@ namespace bertini throw std::runtime_error("concatenating systems with incompatible patches"); if (sys2.IsPatched() && !sys1.IsPatched()) - sys1.CopyPatches(sys1); + sys1.CopyPatches(sys2); // give the unpatched result sys2's patch // the other cases are automatically covered. sys1 already patched, or neither patched. for (unsigned ii(0); ii const& gamma) { + auto t = node::Variable::Make(path_variable_name); + auto g = gamma ? gamma + : std::static_pointer_cast(node::Float::Make(bertini::multiprecision::RandomUnit(MaxPrecisionAllowed()))); // gamma trick: norm-1 complex at max precision (a Float node caps at its creation precision, so generate the constant at the AMP ceiling -- like patch coefficients -- rather than the current default) -////////////////// attempt 1. generates a npos == null problem of some sort. i couldn't figure it out. + System homotopy; + if (start.HasStructuredBlocks() || target.HasStructuredBlocks()) + { + // A block-backed system (e.g. a products-of-linears start, or a randomized target) + // cannot be fused into a node-arithmetic homotopy: operator+ / operator* only combine + // the PolynomialBlock functions and silently ignore structured blocks. So whenever + // EITHER side carries a structured block, combine the two systems with a blend block: + // H = (1-t)*target + gamma*t*start, evaluated by blending whole Systems. The homotopy + // carries target's variable structure and patch; the blend contributes the natural + // rows. ClearBlocks drops the shell's own function blocks (a structured target's rows + // live in a structured block, not a PolynomialBlock, so ClearFunctions would leave them + // to be evaluated a second time alongside the blend). Mirrors policy::CloneGiven::FormHomotopy. + homotopy = target; + homotopy.ClearBlocks(); + homotopy.AddPathVariable(t); + std::vector> coeffs{ 1 - t, g * t }; + std::vector> operands{ + std::make_shared(target), + std::make_shared(start) }; + homotopy.AddBlock(blocks::BlendBlock(t, std::move(coeffs), std::move(operands))); + } + else + { + homotopy = (1-t)*target + g*t*start; + homotopy.AddPathVariable(t); + } + return homotopy; + } - // namespace io = boost::iostreams; - // using buffer_type = std::vector; - // buffer_type buffer; + System MakeMovingHomotopy(System const& fixed, System const& start_moving, System const& end_moving, + std::string const& path_variable_name, + std::shared_ptr const& gamma) + { + if (start_moving.NumNaturalFunctions() != end_moving.NumNaturalFunctions()) + throw std::runtime_error("MakeMovingHomotopy: start_moving and end_moving must have the same number of functions (they are the two endpoints of the moving rows)."); + if (fixed.NumVariables() != start_moving.NumVariables() || fixed.NumVariables() != end_moving.NumVariables()) + throw std::runtime_error("MakeMovingHomotopy: fixed, start_moving and end_moving must share the same variable structure."); + if (start_moving.HavePathVariable() || end_moving.HavePathVariable() || fixed.HavePathVariable()) + throw std::runtime_error("MakeMovingHomotopy: the fixed and moving systems must not already have a path variable."); - // io::stream > output_stream(buffer); - // boost::archive::binary_oarchive oa(output_stream); + auto t = node::Variable::Make(path_variable_name); + auto g = gamma ? gamma + : std::static_pointer_cast(node::Float::Make(bertini::multiprecision::RandomUnit(MaxPrecisionAllowed()))); // gamma trick: norm-1 complex at max precision (a Float node caps at its creation precision, so generate the constant at the AMP ceiling -- like patch coefficients -- rather than the current default) - // oa << sys; - // output_stream.flush(); + // Keep the fixed system's blocks as sibling blocks (do NOT clear them): they are autonomous, + // so they are evaluated once per point and contribute nothing to dH/dt as the moving rows + // slide. Append a single blend block that moves only the moving rows: + // moving = (1-t)*end_moving + gamma*t*start_moving (t=1 -> gamma*start, t=0 -> end). + // Mirrors MakeHomotopy's blend branch, but blends only the moving operands instead of whole + // systems, so the fixed equations are never duplicated or scaled. + System homotopy = fixed; + homotopy.AddPathVariable(t); + std::vector> coeffs{ 1 - t, g * t }; + std::vector> operands{ + std::make_shared(end_moving), + std::make_shared(start_moving) }; + homotopy.AddBlock(blocks::BlendBlock(t, std::move(coeffs), std::move(operands))); + return homotopy; + } - - // io::basic_array_source source(&buffer[0],buffer.size()); - // io::stream > input_stream(source); - // boost::archive::binary_iarchive ia(input_stream); + System Clone(System const& sys) + { + // Memory-isolating clone (ADR-0027). Since the evaluation path no longer + // writes shared node state, per-thread copies may share the immutable node DAG + // and the compiled SLP Program; each copy only needs its own evaluation Memory. The System + // copy constructor provides exactly that: it shares the node DAG (nodes are shared_ptr) and, + // per block, shares the compiled Program while copying the per-thread SLPMemory; the + // operand-holding blocks (BlendBlock, RandomizationBlock) deep-copy their nested operand + // Systems the same way (own Memory, shared DAG). No mutable state is shared, so a clone is + // safe to evaluate concurrently with the original. + // + // This replaces the old text-archive serialize/deserialize round trip + re-Differentiate() + // (issue #246): no deep copy of the DAG, and no SLP recompile (the clone reuses the source's + // compiled Program). + return System(sys); + } - // System sys_clone; - // ia >> sys_clone; - // return sys_clone; + void Simplify(System & sys) + { + sys.Simplify(); + } + const System::Var& System::GetPathVariable() const + { + if (this->HavePathVariable()) + return this->path_variable_; + throw std::runtime_error("trying to get path variable for a system which doesn't have a path variable defined"); + } -/////////////////////// attempt2 generates crashes. :( - // std::string serial_str; - // { - // boost::iostreams::back_insert_device inserter(serial_str); - // boost::iostreams::stream > s(inserter); - // boost::archive::binary_oarchive oa(s); - // oa << sys; + // Explicit instantiation definitions — paired with extern template declarations in system.hpp. - // // don't forget to flush the stream to finish writing into the buffer - // s.flush(); - // } - - // boost::iostreams::basic_array_source device(serial_str.data(), serial_str.size()); - // boost::iostreams::stream > t(device); - // boost::archive::binary_iarchive ia(t); - // System sys_clone; - // ia >> sys_clone; + template void System::EvalInPlace(Vec&) const; + template void System::EvalInPlace(Vec&) const; + template Vec System::Eval() const; + template Vec System::Eval() const; + template void System::JacobianInPlace(Mat&) const; + template void System::JacobianInPlace(Mat&) const; + template Mat System::Jacobian() const; + template Mat System::Jacobian() const; -///////////////////// attempt3. works. why the others generate problems with the binary archive baffles me. + template Mat System::Jacobian(const Vec&) const; + template Mat System::Jacobian(const Vec&) const; - std::stringstream ss; - { - boost::archive::text_oarchive oa(ss); - oa << sys; - } + template void System::JacobianInPlace(Mat&, const Vec&) const; + template void System::JacobianInPlace(Mat&, const Vec&) const; - System sys_clone; - { - boost::archive::text_iarchive ia(ss); - ia >> sys_clone; - } + template void System::TimeDerivativeInPlace(Vec&) const; + template void System::TimeDerivativeInPlace(Vec&) const; - return sys_clone; - } + template Vec System::TimeDerivative() const; + template Vec System::TimeDerivative() const; + template void System::SetVariables(const Vec&) const; + template void System::SetVariables(const Vec&) const; - void Simplify(System & sys) - { - sys.Simplify(); - } + template void System::SetPathVariable(dbl const&) const; + template void System::SetPathVariable(mpfr_complex const&) const; + + template void System::SetAndReset(Vec const&, dbl const&) const; + template void System::SetAndReset(Vec const&, mpfr_complex const&) const; + + template void System::SetAndReset(Vec const&) const; + template void System::SetAndReset(Vec const&) const; } diff --git a/core/src/tracking/explicit_predictors.cpp b/core/src/tracking/explicit_predictors.cpp index cc8858206..ea7ecb1d7 100644 --- a/core/src/tracking/explicit_predictors.cpp +++ b/core/src/tracking/explicit_predictors.cpp @@ -297,6 +297,68 @@ const Eigen::Matrix ExplicitRKPredictor::cRKV67_(cRKV67Ptr_); + // Explicit instantiation definitions for the two concrete numeric types. + // These pair with the extern template declarations in explicit_predictors.hpp + // and prevent each including TU from emitting its own copy. + + template SuccessCode ExplicitRKPredictor::Predict( + Vec&, System const&, Vec const&, dbl, dbl const&, + double&, unsigned&, unsigned, double const&); + template SuccessCode ExplicitRKPredictor::Predict( + Vec&, System const&, Vec const&, mpfr_complex, mpfr_complex const&, + double&, unsigned&, unsigned, double const&); + + template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, + System const&, Vec const&, dbl, dbl const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, + System const&, Vec const&, mpfr_complex, mpfr_complex const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + + template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, double&, + System const&, Vec const&, dbl, dbl const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + template SuccessCode ExplicitRKPredictor::Predict( + Vec&, double&, double&, double&, double&, + System const&, Vec const&, mpfr_complex, mpfr_complex const&, + double&, unsigned&, unsigned, double const&, AdaptiveMultiplePrecisionConfig const&); + + template SuccessCode ExplicitRKPredictor::FullStep( + Vec&, System const&, Vec const&, dbl const&, dbl const&); + template SuccessCode ExplicitRKPredictor::FullStep( + Vec&, System const&, Vec const&, mpfr_complex const&, mpfr_complex const&); + + template void ExplicitRKPredictor::SetNormsCond( + double&, double&, double&, unsigned, unsigned); + template void ExplicitRKPredictor::SetNormsCond( + double&, double&, double&, unsigned, unsigned); + + template SuccessCode ExplicitRKPredictor::SetErrorEstimate(double&, dbl const&); + template SuccessCode ExplicitRKPredictor::SetErrorEstimate(double&, mpfr_complex const&); + + template SuccessCode ExplicitRKPredictor::SetSizeProportion(double&, dbl const&); + template SuccessCode ExplicitRKPredictor::SetSizeProportion(double&, mpfr_complex const&); + + template SuccessCode ExplicitRKPredictor::EvalRHS( + System const&, Vec const&, dbl const&, Mat&, unsigned); + template SuccessCode ExplicitRKPredictor::EvalRHS( + System const&, Vec const&, mpfr_complex const&, Mat&, unsigned); + + template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, + Mat const&, Mat const&); + template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, + Mat const&, Mat const&); + + template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, Mat const&); + template void ExplicitRKPredictor::FillButcherTable( + int, Mat const&, Mat const&, Mat const&); + } // re: predict }// re: tracking }// re: bertini diff --git a/core/test/blackbox/parsing.cpp b/core/test/blackbox/parsing.cpp index f79c2225b..a28174d2e 100644 --- a/core/test/blackbox/parsing.cpp +++ b/core/test/blackbox/parsing.cpp @@ -26,8 +26,8 @@ #include "bertini2/system/precon.hpp" #include "bertini2/blackbox/global_configs.hpp" - #include "bertini2/io/parsing.hpp" +#include "bertini2/io/parsing/system_parsers.hpp" BOOST_AUTO_TEST_SUITE(blackbox_test) @@ -76,10 +76,99 @@ maxnewtonits: 1;)"; auto results_double = bertini::parsing::classic::ConfigParser::Parse(config); auto results_mp = bertini::parsing::classic::ConfigParser::Parse(config); + + BOOST_CHECK_EQUAL(std::get(results_double).random_seed, 72ul); + BOOST_CHECK_EQUAL(std::get(results_mp).random_seed, 72ul); } BOOST_AUTO_TEST_SUITE_END() // end the parsing sub-suite + + +BOOST_AUTO_TEST_SUITE(parser_errors) + +BOOST_AUTO_TEST_CASE(system_missing_semicolon) +{ + // "variable_group x, y" is missing a semicolon — expectation operator fires + std::string bad = "variable_group x, y\nfunction f;\nf = x+y;"; + BOOST_CHECK_THROW(bertini::System{bad}, std::runtime_error); +} + +BOOST_AUTO_TEST_CASE(system_syntax_error_in_expression) +{ + std::string bad = "variable_group x, y;\nfunction f;\nf = x + * y;"; + BOOST_CHECK_THROW(bertini::System{bad}, std::runtime_error); +} + +BOOST_AUTO_TEST_CASE(system_garbage_input) +{ + // Completely nonsensical input — parser cannot make progress + BOOST_CHECK_THROW(bertini::System{"@#$% not bertini at all"}, std::runtime_error); +} + +BOOST_AUTO_TEST_SUITE_END() // end parser_errors suite + + + +BOOST_AUTO_TEST_SUITE(unary_minus_precedence) + +using dbl = bertini::dbl; + +namespace { + // parse "f = " over variable_group x,y,z and evaluate at the given point + dbl ParseEval(std::string const& expr, dbl x, dbl y, dbl z) + { + bertini::System s{"variable_group x, y, z;\nfunction f;\nf = " + expr + ";"}; + bertini::Vec pt(3); + pt << x, y, z; + return s.Eval(pt)(0); + } +} + +// Regression for the grammar bug where a leading unary '-' negated the ENTIRE following +// expression ("-y+x" parsed as "-(y+x)") instead of just its operand. Unary +/- bind one +// factor_, so "-y+x" is (-y)+x and "-x^2" is -(x^2). +BOOST_AUTO_TEST_CASE(leading_minus_negates_only_its_operand) +{ + const dbl x(2,0), y(5,0), z(3,0); + // "-y+x" == x-y == -3, NOT -(y+x) == -7 + BOOST_CHECK_SMALL(std::abs(ParseEval("-y+x", x,y,z) - ParseEval("x-y", x,y,z)), 1e-12); + BOOST_CHECK_SMALL(std::abs(ParseEval("-y+x", x,y,z) - dbl(-3,0)), 1e-12); +} + +BOOST_AUTO_TEST_CASE(leading_minus_on_a_parenthesized_sum) +{ + const dbl x(2,0), y(5,0), z(3,0); + // the bug found via round-tripping: "-(y-z)+x" == x-(y-z) == 0, NOT -((y-z)+x) + BOOST_CHECK_SMALL(std::abs(ParseEval("-(y-z)+x", x,y,z) - ParseEval("x-(y-z)", x,y,z)), 1e-12); + BOOST_CHECK_SMALL(std::abs(ParseEval("-(y-z)+x", x,y,z) - dbl(0,0)), 1e-12); +} + +BOOST_AUTO_TEST_CASE(all_negative_sum_is_unchanged) +{ + const dbl x(2,0), y(5,0), z(3,0); + // "-y-x" == -(y+x) == -7 (here the greedy reading happened to agree) + BOOST_CHECK_SMALL(std::abs(ParseEval("-y-x", x,y,z) - dbl(-7,0)), 1e-12); +} + +BOOST_AUTO_TEST_CASE(unary_minus_binds_looser_than_power) +{ + const dbl x(2,0), y(5,0), z(3,0); + // "-x^2" == -(x^2) == -4, NOT (-x)^2 == 4 + BOOST_CHECK_SMALL(std::abs(ParseEval("-x^2", x,y,z) - dbl(-4,0)), 1e-12); +} + +BOOST_AUTO_TEST_CASE(unary_minus_then_product) +{ + const dbl x(2,0), y(5,0), z(3,0); + // "-x*y" == -(x*y) == -10 + BOOST_CHECK_SMALL(std::abs(ParseEval("-x*y", x,y,z) - dbl(-10,0)), 1e-12); +} + +BOOST_AUTO_TEST_SUITE_END() // unary_minus_precedence + + + BOOST_AUTO_TEST_SUITE_END() // end the blackbox suite diff --git a/core/test/blackbox/zerodim.cpp b/core/test/blackbox/zerodim.cpp index b33e85b0a..1c2836a22 100644 --- a/core/test/blackbox/zerodim.cpp +++ b/core/test/blackbox/zerodim.cpp @@ -53,10 +53,127 @@ BOOST_AUTO_TEST_CASE(make_zero_dim_nondefaults) my_runtime_type_options.endgame = bertini::blackbox::type::Endgame::Cauchy; auto zd_ptr = blackbox::MakeZeroDim(my_runtime_type_options, sys); - + // zd_ptr->DefaultSetup(); } + +BOOST_AUTO_TEST_CASE(make_zero_dim_honors_endgame_choice) +{ + // regression: ZeroDimSpecifyShouldClone hardcoded the Cauchy endgame, + // ignoring its EndgameType parameter -- selecting PowerSeries silently + // built a Cauchy ZeroDim. pin the dynamic types. + using namespace bertini::tracking; + using namespace bertini::endgame; + auto sys = system::Precon::GriewankOsborn(); + + blackbox::ZeroDimRT rt; + rt.tracker = blackbox::type::Tracker::Adaptive; + + using PSEGZD = algorithm::ZeroDim::PSEG, System, start_system::TotalDegree>; + using CauchyZD = algorithm::ZeroDim::Cauchy, System, start_system::TotalDegree>; + + rt.endgame = blackbox::type::Endgame::PowerSeries; + auto zd_pseg = blackbox::MakeZeroDim(rt, sys); + BOOST_CHECK(dynamic_cast(zd_pseg.get()) != nullptr); + BOOST_CHECK(dynamic_cast(zd_pseg.get()) == nullptr); + + rt.endgame = blackbox::type::Endgame::Cauchy; + auto zd_cauchy = blackbox::MakeZeroDim(rt, sys); + BOOST_CHECK(dynamic_cast(zd_cauchy.get()) != nullptr); + BOOST_CHECK(dynamic_cast(zd_cauchy.get()) == nullptr); +} + BOOST_AUTO_TEST_SUITE_END() // end the zerodim sub-suite + +// The CLI infers the start system from the variable-group structure (classic +// Bertini behavior), rather than reading a setting: a single affine variable +// group -> total degree; multiple groups or any homogeneous group -> mhom. +BOOST_AUTO_TEST_SUITE(start_system_inference) + +using namespace bertini; +using Var = std::shared_ptr; + +BOOST_AUTO_TEST_CASE(single_affine_group_infers_total_degree) +{ + Var x = node::Variable::Make("x"); + Var y = node::Variable::Make("y"); + System sys; + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddFunction(x*x + y*y - 1); + sys.AddFunction(x + y); + + BOOST_CHECK(blackbox::InferStartType(sys) == blackbox::type::Start::TotalDegree); +} + +BOOST_AUTO_TEST_CASE(two_affine_groups_infer_mhom) +{ + Var x = node::Variable::Make("x"); + Var y = node::Variable::Make("y"); + System sys; + sys.AddVariableGroup(VariableGroup{x}); + sys.AddVariableGroup(VariableGroup{y}); + sys.AddFunction(x*y - 1); + sys.AddFunction(x - y); + + BOOST_CHECK(blackbox::InferStartType(sys) == blackbox::type::Start::MHom); +} + +BOOST_AUTO_TEST_CASE(homogeneous_group_infers_mhom) +{ + // a single homogeneous variable group is projective, not total-degree + Var x = node::Variable::Make("x"); + Var y = node::Variable::Make("y"); + System sys; + sys.AddHomVariableGroup(VariableGroup{x, y}); + sys.AddFunction(x*x + y*y); + + BOOST_CHECK(blackbox::InferStartType(sys) == blackbox::type::Start::MHom); +} + +BOOST_AUTO_TEST_CASE(griewank_osborn_single_group_infers_total_degree) +{ + auto sys = system::Precon::GriewankOsborn(); + BOOST_CHECK(blackbox::InferStartType(sys) == blackbox::type::Start::TotalDegree); +} + +// DISABLED pending the block-composed MHom start system (plan +// typed-wondering-sutherland, tasks #4-#8). Today MakeZeroDim for an MHom +// target throws "unknown visitor: LinearProduct" because the start system is +// built from LinearProduct nodes the SLP compiler can't compile. Once the MHom +// start is rebuilt on ProductsOfLinearsBlock, re-enable this and extend it from +// "constructs" to "actually solves". +BOOST_AUTO_TEST_CASE(inferred_mhom_builds_an_mhomogeneous_zerodim, + * boost::unit_test::disabled()) +{ + // end to end: a two-variable-group system, inferred to MHom, must build a + // ZeroDim backed by the MHomogeneous start system (not total degree). + using namespace bertini::tracking; + using namespace bertini::endgame; + + Var x = node::Variable::Make("x"); + Var y = node::Variable::Make("y"); + System sys; + sys.AddVariableGroup(VariableGroup{x}); + sys.AddVariableGroup(VariableGroup{y}); + sys.AddFunction(x*y - 1); + sys.AddFunction(x - y); + + blackbox::ZeroDimRT rt; + rt.start = blackbox::InferStartType(sys); + rt.tracker = blackbox::type::Tracker::Adaptive; + rt.endgame = blackbox::type::Endgame::Cauchy; + BOOST_CHECK(rt.start == blackbox::type::Start::MHom); + + using MHomZD = algorithm::ZeroDim::Cauchy, System, start_system::MHomogeneous>; + using TotDegZD = algorithm::ZeroDim::Cauchy, System, start_system::TotalDegree>; + + auto zd = blackbox::MakeZeroDim(rt, sys); + BOOST_CHECK(dynamic_cast(zd.get()) != nullptr); + BOOST_CHECK(dynamic_cast(zd.get()) == nullptr); +} + +BOOST_AUTO_TEST_SUITE_END() // end the start_system_inference sub-suite + BOOST_AUTO_TEST_SUITE_END() // end the blackbox suite diff --git a/core/test/classes/blend_block_test.cpp b/core/test/classes/blend_block_test.cpp new file mode 100644 index 000000000..64b99c4d5 --- /dev/null +++ b/core/test/classes/blend_block_test.cpp @@ -0,0 +1,152 @@ +//This file is part of Bertini 2. +// +//blend_block_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//blend_block_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/system/blocks/block.hpp" +#include "bertini2/system/blocks/blend_block.hpp" + +BOOST_AUTO_TEST_SUITE(blend_block_suite) + +using namespace bertini; +using bertini::blocks::BlendBlock; +using bertini::DefaultPrecision; + +using Blend = BlendBlock; + +static_assert(bertini::blocks::is_block_v, + "BlendBlock must satisfy the block contract"); + +// A: f = x + 2y B: f = x*y (variable group {x,y}, one function each, no patch) +// H = (1-t)*A + t*B +// at (x,y)=(1,1): A = 3, B = 1 +// t = 1/2: H = 0.5*3 + 0.5*1 = 2 +// dH/dt = -A + B = -2 +// dH/dx = 0.5*[1,2] + 0.5*[y,x]=0.5*[1,1] = [1, 1.5] +static Blend MakeTestBlend() +{ + auto x = node::Variable::Make("x"); + auto y = node::Variable::Make("y"); + + auto A = std::make_shared(); + A->AddVariableGroup(VariableGroup{x, y}); + A->AddFunction(x + 2 * y); + + auto B = std::make_shared(); + B->AddVariableGroup(VariableGroup{x, y}); + B->AddFunction(x * y); + + auto t = node::Variable::Make("t"); + std::vector> coeffs{node::Integer::Make(1) - t, t}; + std::vector> ops{A, B}; + + return Blend(t, coeffs, ops); +} + +BOOST_AUTO_TEST_CASE(shape) +{ + DefaultPrecision(30); + auto blend = MakeTestBlend(); + BOOST_CHECK_EQUAL(blend.NumFunctions(), 1u); + BOOST_CHECK(blend.DependsOnPathVariable()); +} + +BOOST_AUTO_TEST_CASE(eval_double) +{ + DefaultPrecision(30); + auto blend = MakeTestBlend(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + bertini::Vec result(1); + blend.EvalInPlace(result, x, dbl(0.5)); + + BOOST_CHECK_CLOSE(result(0).real(), 2.0, 1e-11); + BOOST_CHECK_SMALL(result(0).imag(), 1e-11); +} + +BOOST_AUTO_TEST_CASE(jacobian_double) +{ + DefaultPrecision(30); + auto blend = MakeTestBlend(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + bertini::Mat J(1, 2); + blend.JacobianInPlace(J, x, dbl(0.5)); + + BOOST_CHECK_CLOSE(J(0, 0).real(), 1.0, 1e-11); + BOOST_CHECK_CLOSE(J(0, 1).real(), 1.5, 1e-11); +} + +BOOST_AUTO_TEST_CASE(time_derivative_double) +{ + DefaultPrecision(30); + auto blend = MakeTestBlend(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + bertini::Vec dHdt(1); + blend.TimeDerivInPlace(dHdt, x, dbl(0.5)); + + BOOST_CHECK_CLOSE(dHdt(0).real(), -2.0, 1e-11); +} + +BOOST_AUTO_TEST_CASE(eval_and_time_derivative_mpfr) +{ + DefaultPrecision(30); + auto blend = MakeTestBlend(); + blend.Precision(30); + + bertini::Vec x(2); x << mpfr_complex(1), mpfr_complex(1); + mpfr_complex t("0.5"); + + bertini::Vec result(1); + blend.EvalInPlace(result, x, t); + BOOST_CHECK(abs(result(0) - mpfr_complex(2)) < mpfr_float("1e-25")); + + bertini::Vec dHdt(1); + blend.TimeDerivInPlace(dHdt, x, t); + BOOST_CHECK(abs(dHdt(0) - mpfr_complex(-2)) < mpfr_float("1e-25")); +} + +// The coefficients are evaluated through a cached coefficient System (compiled once, +// reused), not by node-level tree evaluation. Repeated calls --- and a call at a changed +// value --- must keep giving the right answer, exercising the cache across invocations. +BOOST_AUTO_TEST_CASE(repeated_evaluation_reuses_coefficients_consistently) +{ + DefaultPrecision(30); + auto blend = MakeTestBlend(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + bertini::Vec result(1); + + // First call builds the cached coefficient system; subsequent calls reuse it. + blend.EvalInPlace(result, x, dbl(0.5)); + BOOST_CHECK_CLOSE(result(0).real(), 2.0, 1e-11); + + blend.EvalInPlace(result, x, dbl(0.5)); + BOOST_CHECK_CLOSE(result(0).real(), 2.0, 1e-11); + + // A different path value: H = (1-t)*3 + t*1 = 3 - 2t; at t=0 -> 3. + blend.EvalInPlace(result, x, dbl(0.0)); + BOOST_CHECK_CLOSE(result(0).real(), 3.0, 1e-11); + + // And at t=1 -> 1. + blend.EvalInPlace(result, x, dbl(1.0)); + BOOST_CHECK_CLOSE(result(0).real(), 1.0, 1e-11); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/complex_test.cpp b/core/test/classes/complex_test.cpp index 8a7260585..e3bba3232 100644 --- a/core/test/classes/complex_test.cpp +++ b/core/test/classes/complex_test.cpp @@ -499,7 +499,6 @@ BOOST_AUTO_TEST_CASE(complex_argument) BOOST_AUTO_TEST_CASE(complex_make_random_50) { - using mpfr_float = bertini::mpfr_float; DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); bertini::mpfr_complex z = bertini::multiprecision::rand(); @@ -510,7 +509,6 @@ BOOST_AUTO_TEST_CASE(complex_make_random_50) BOOST_AUTO_TEST_CASE(complex_make_random_100) { - using mpfr_float = bertini::mpfr_float; DefaultPrecision(100); bertini::mpfr_complex z = bertini::multiprecision::rand(); } diff --git a/core/test/classes/degrees_test.cpp b/core/test/classes/degrees_test.cpp new file mode 100644 index 000000000..6a4e39904 --- /dev/null +++ b/core/test/classes/degrees_test.cpp @@ -0,0 +1,328 @@ +//This file is part of Bertini 2. +// +//degrees_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//degrees_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file degrees_test.cpp + +\brief Degrees of block-composed Systems. + +Now that the polynomial path is folded into a PolynomialBlock and every structured block +(products-of-linears, linear-forms, blend) reports its own degrees, System::Degrees() / +Degrees(vars) / DegreeBound() concatenate degrees across all blocks. These tests pin the +per-block degree semantics -- with particular attention to the linear-algebra path +(LinearFormsBlock = degree 1), where being seen as degree-1 is exactly what makes A@x+b a +linear form to the start-system / degree machinery. + +Block degree contract under test: + PolynomialBlock -- the function tree's Degree() / Degree(vars) + LinearFormsBlock -- always 1 + ProductsOfLinearsBlock -- the number of linear factors (== rows of the factor matrix) + BlendBlock -- the elementwise max of its operands' degrees +*/ + +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/system/start_systems.hpp" +#include "bertini2/system/blocks/block.hpp" +#include "bertini2/system/blocks/polynomial_block.hpp" +#include "bertini2/system/blocks/products_of_linears_block.hpp" +#include "bertini2/system/blocks/linear_forms_block.hpp" +#include "bertini2/system/blocks/blend_block.hpp" + +BOOST_AUTO_TEST_SUITE(degrees_suite) + +using namespace bertini; +using Var = std::shared_ptr; +using bertini::node::Variable; +using bertini::node::Integer; +using bertini::blocks::PolynomialBlock; +using bertini::blocks::ProductsOfLinearsBlock; +using bertini::blocks::LinearFormsBlock; +using bertini::blocks::BlendBlock; +using bertini::DefaultPrecision; + +namespace { + +// augmented coefficient matrix (rows x (num_vars+1)), last column the constant term. +Mat AugMat(std::vector> const& rows) +{ + Mat M(static_cast(rows.size()), + static_cast(rows.front().size())); + for (Eigen::Index i = 0; i < M.rows(); ++i) + for (Eigen::Index j = 0; j < M.cols(); ++j) + M(i, j) = mpfr_complex(rows[static_cast(i)][static_cast(j)]); + return M; +} + +} // namespace + + +//////////////////////////////////////////////////////////////////////////////// +// LinearFormsBlock -- the linear-algebra path. Every function is degree 1. +//////////////////////////////////////////////////////////////////////////////// + +BOOST_AUTO_TEST_CASE(linear_forms_block_is_degree_one) +{ + DefaultPrecision(30); + // three linear forms in two variables (A @ x + b shape): 3 x (2+1) augmented matrix. + LinearFormsBlock block(2, AugMat({{2, 3, 1}, {1, -1, 4}, {0, 5, -2}})); + + std::vector expected{1, 1, 1}; + BOOST_CHECK(block.Degrees() == expected); + + // the structured-block contract: Degrees(group) forwards to the total Degrees(). + VariableGroup grp{Variable::Make("x"), Variable::Make("y")}; + BOOST_CHECK(block.Degrees(grp) == block.Degrees()); +} + +BOOST_AUTO_TEST_CASE(system_of_only_linear_forms_has_degree_bound_one) +{ + DefaultPrecision(30); + System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddBlock(LinearFormsBlock(2, AugMat({{2, 3, 1}, {1, -1, 4}}))); + + std::vector expected{1, 1}; + BOOST_CHECK(sys.Degrees() == expected); + BOOST_CHECK(sys.Degrees(sys.Variables()) == expected); + BOOST_CHECK_EQUAL(sys.DegreeBound(), 1); +} + + +//////////////////////////////////////////////////////////////////////////////// +// PolynomialBlock -- the folded classic path. Degrees come from the trees. +//////////////////////////////////////////////////////////////////////////////// + +BOOST_AUTO_TEST_CASE(polynomial_block_degrees_from_trees) +{ + DefaultPrecision(30); + System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddFunction(x * y - Integer::Make(1)); // degree 2 + sys.AddFunction(x + y); // degree 1 + sys.AddFunction(Integer::Make(7)); // degree 0 (constant) + + std::vector expected{2, 1, 0}; + BOOST_CHECK(sys.Degrees() == expected); + BOOST_CHECK_EQUAL(sys.DegreeBound(), 2); +} + +BOOST_AUTO_TEST_CASE(polynomial_degrees_match_hand_derived) +{ + // behavior-preservation guard: folding functions into the PolynomialBlock did not change + // degree reporting -- it is still the function tree's Degree(). + DefaultPrecision(30); + System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddFunction(x * x * y * y * y); // x^2 y^3, total degree 5 + sys.AddFunction(x + y + Integer::Make(1)); // degree 1 + + std::vector expected{5, 1}; + BOOST_CHECK(sys.Degrees(sys.Variables()) == expected); + BOOST_CHECK_EQUAL(sys.DegreeBound(), 5); +} + + +//////////////////////////////////////////////////////////////////////////////// +// The eigenvalue / linear-algebra distinction: (A - lam I) x is *bilinear* (degree 2), +// while a constant-coefficient normalization c.x - 1 is *linear* (degree 1). The degree +// machinery must see exactly this difference. +//////////////////////////////////////////////////////////////////////////////// + +BOOST_AUTO_TEST_CASE(bilinear_eigenvalue_row_is_degree_two) +{ + DefaultPrecision(30); + Var x0 = Variable::Make("x0"), x1 = Variable::Make("x1"), lam = Variable::Make("lam"); + // row of (A - lam I) x : a*x0 + b*x1 - lam*x0 (the lam*x0 term is degree 2) + auto row = Integer::Make(2) * x0 + Integer::Make(3) * x1 - lam * x0; + BOOST_CHECK_EQUAL(row->Degree(), 2); +} + +BOOST_AUTO_TEST_CASE(eigenproblem_degrees_bilinear_rows_plus_linear_normalization) +{ + DefaultPrecision(30); + System sys; + Var x0 = Variable::Make("x0"), x1 = Variable::Make("x1"), lam = Variable::Make("lam"); + sys.AddVariableGroup(VariableGroup{x0, x1, lam}); + + // the two (A - lam I) x rows -- degree 2 each (eigenvalue * eigenvector entry). + sys.AddFunction(Integer::Make(4) * x0 + Integer::Make(1) * x1 - lam * x0); + sys.AddFunction(Integer::Make(1) * x0 + Integer::Make(3) * x1 - lam * x1); + // generic normalization c.x - 1 as a linear-forms block -- degree 1. + // (LinearFormsBlock spans all 3 columns; the lam coefficient is 0.) + sys.AddBlock(LinearFormsBlock(3, AugMat({{2, 5, 0, -1}}))); + + // poly block (added first by AddFunction) then the linear-forms block. + std::vector expected{2, 2, 1}; + BOOST_CHECK(sys.Degrees() == expected); + BOOST_CHECK(sys.Degrees(sys.Variables()) == expected); + BOOST_CHECK_EQUAL(sys.DegreeBound(), 2); +} + + +//////////////////////////////////////////////////////////////////////////////// +// ProductsOfLinearsBlock -- degree == number of linear factors. +//////////////////////////////////////////////////////////////////////////////// + +BOOST_AUTO_TEST_CASE(products_of_linears_degree_is_factor_count) +{ + DefaultPrecision(30); + // f0 = product of 2 linear factors (degree 2); f1 = product of 3 (degree 3). + Mat f0 = AugMat({{2, 3, 1}, {1, -1, 4}}); + Mat f1 = AugMat({{1, 0, 1}, {0, 1, -2}, {1, 1, 0}}); + ProductsOfLinearsBlock block(2, std::vector>{f0, f1}); + + std::vector expected{2, 3}; + BOOST_CHECK(block.Degrees() == expected); + // structured-block contract: Degrees(group) forwards to total Degrees(). + VariableGroup grp{Variable::Make("x"), Variable::Make("y")}; + BOOST_CHECK(block.Degrees(grp) == block.Degrees()); + + System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddBlock(ProductsOfLinearsBlock(2, std::vector>{f0, f1})); + BOOST_CHECK(sys.Degrees() == expected); + BOOST_CHECK_EQUAL(sys.DegreeBound(), 3); +} + + +//////////////////////////////////////////////////////////////////////////////// +// BlendBlock -- elementwise max of its operands' degrees (the homotopy case). +//////////////////////////////////////////////////////////////////////////////// + +BOOST_AUTO_TEST_CASE(blend_degree_is_elementwise_max_of_operands) +{ + DefaultPrecision(30); + Var x = Variable::Make("x"), y = Variable::Make("y"), t = Variable::Make("t"); + + auto target = std::make_shared(); + target->AddVariableGroup(VariableGroup{x, y}); + target->AddFunction(x * y); // degree 2 + target->AddFunction(x * x * x); // degree 3 + + auto start = std::make_shared(); + start->AddVariableGroup(VariableGroup{x, y}); + start->AddFunction(x); // degree 1 + start->AddFunction(y); // degree 1 + + std::vector> coeffs{ Integer::Make(1) - t, t }; + std::vector> operands{ target, start }; + BlendBlock blend(t, std::move(coeffs), operands); + + std::vector expected{2, 3}; // max({2,3},{1,1}) + BOOST_CHECK(blend.Degrees() == expected); +} + +BOOST_AUTO_TEST_CASE(system_with_blend_block_has_nonempty_degree_bound) +{ + // regression: the MHom-style blend homotopy carries no polynomial block, yet its degrees + // (and DegreeBound, needed by the AMP config) must come from the blend's operands rather + // than being empty (which previously dereferenced an empty range in DegreeBound()). + DefaultPrecision(30); + Var x = Variable::Make("x"), y = Variable::Make("y"), t = Variable::Make("t"); + + auto target = std::make_shared(); + target->AddVariableGroup(VariableGroup{x, y}); + target->AddFunction(x * y); // degree 2 + target->AddFunction(x * x * x); // degree 3 + + auto start = std::make_shared(); + start->AddVariableGroup(VariableGroup{x, y}); + start->AddFunction(x); + start->AddFunction(y); + + System H; + H.AddVariableGroup(VariableGroup{x, y}); + H.AddPathVariable(t); + std::vector> coeffs{ Integer::Make(1) - t, t }; + std::vector> operands{ target, start }; + H.AddBlock(BlendBlock(t, std::move(coeffs), operands)); + + BOOST_REQUIRE(H.HasStructuredBlocks()); // it does have a structured block + std::vector expected{2, 3}; + BOOST_CHECK(H.Degrees() == expected); + BOOST_CHECK_EQUAL(H.DegreeBound(), 3); +} + + +//////////////////////////////////////////////////////////////////////////////// +// Mixed and edge cases. +//////////////////////////////////////////////////////////////////////////////// + +BOOST_AUTO_TEST_CASE(mixed_poly_and_linear_forms_concatenate_in_block_order) +{ + DefaultPrecision(30); + System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddFunction(x * x); // poly block, degree 2 + sys.AddFunction(x * x * x); // poly block, degree 3 + sys.AddBlock(LinearFormsBlock(2, AugMat({{2, 3, 1}, {1, -1, 4}}))); // degree 1, 1 + + // AddFunction creates the polynomial block first, then the linear-forms block is appended. + std::vector expected{2, 3, 1, 1}; + BOOST_CHECK(sys.Degrees() == expected); + BOOST_CHECK_EQUAL(sys.DegreeBound(), 3); +} + +BOOST_AUTO_TEST_CASE(system_with_no_functions_has_no_degrees) +{ + // A System with variables but no functions/blocks reports no degrees. (DegreeBound() has + // a non-empty precondition -- it max-reduces the degree list -- so it is not called here.) + DefaultPrecision(30); + System sys; + sys.AddVariableGroup(VariableGroup{Variable::Make("x"), Variable::Make("y")}); + BOOST_CHECK(sys.Degrees().empty()); + BOOST_CHECK(sys.Degrees(sys.Variables()).empty()); +} + + +//////////////////////////////////////////////////////////////////////////////// +// The real MHom start system: products-of-linears block, degrees == factor counts, +// and (the regression) a finite DegreeBound. +//////////////////////////////////////////////////////////////////////////////// + +BOOST_AUTO_TEST_CASE(mhom_start_system_has_factor_count_degrees) +{ + DefaultPrecision(30); + using namespace bertini::start_system; + + System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddVariableGroup(VariableGroup{y}); + sys.AddFunction(x * y - Integer::Make(1)); + sys.AddFunction(x + y); + sys.Homogenize(); + sys.AutoPatch(); + + auto mhom = MHomogeneous(sys); + + auto degs = mhom.Degrees(); + BOOST_REQUIRE(!degs.empty()); // was empty -> crashed DegreeBound + BOOST_CHECK_EQUAL(degs.size(), mhom.NumNaturalFunctions()); + for (int d : degs) + BOOST_CHECK_GE(d, 1); // each start function is a product of >=1 linear + BOOST_CHECK_GE(mhom.DegreeBound(), 1); // finite, no empty-range deref +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/differentiate_test.cpp b/core/test/classes/differentiate_test.cpp deleted file mode 100644 index 6a19b69a6..000000000 --- a/core/test/classes/differentiate_test.cpp +++ /dev/null @@ -1,1304 +0,0 @@ -//This file is part of Bertini 2. -// -//differentiate_test.cpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//differentiate_test.cpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with differentiate_test.cpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// silviana amethyst, university of wisconsin eau claire -// -// Created by Collins, James B. on 4/30/15. -// Copyright (c) 2015 West Texas A&M University. All rights reserved. - -#include - -#include -#include -#include - -#include "bertini2/function_tree.hpp" -#include "bertini2/system/system.hpp" -#include "bertini2/io/parsing/system_parsers.hpp" - -#include -#include - -#include - - - - -#include "externs.hpp" - - -BOOST_AUTO_TEST_SUITE(differentiate) - - -using bertini::DefaultPrecision; -using dbl = std::complex; -using Variable = bertini::node::Variable; -using Node = bertini::node::Node; -using Function = bertini::node::Function; -using Jacobian = bertini::node::Jacobian; - -using dbl = bertini::dbl; -using mpfr = bertini::mpfr_complex; -using mpfr_float = bertini::mpfr_float; - -/////////// Basic Operations Alone /////////////////// - -BOOST_AUTO_TEST_CASE(just_diff_a_function){ - std::string str = "function f; variable_group x,y,z; f = x*y +y^2 - z*x + 9;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - for(auto vv : vars) - { - JFunc->EvalJ(vv); - JFunc->EvalJ(vv); - } - - std::vector multidegree{1,2,1}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 2); -} - - -BOOST_AUTO_TEST_CASE(diff_3xyz){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = 3*x*y*z;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - BOOST_CHECK_EQUAL(func->Degree(),3); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),1); - - - - std::vector multidegree{1,1,1}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 3); - - std::vector exact_dbl = {3.0*ynum_dbl*znum_dbl, 3.0*xnum_dbl*znum_dbl, 3.0*ynum_dbl*xnum_dbl}; - std::vector exact_mpfr = {mpfr("3.0")*ynum_mpfr*znum_mpfr,mpfr("3.0")*xnum_mpfr*znum_mpfr,mpfr("3.0")*ynum_mpfr*xnum_mpfr}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() / exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() / exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() / exact_dbl[2].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() / exact_dbl[2].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() / exact_mpfr[2].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() / exact_mpfr[2].imag() -1) < threshold_clearance_mp); - - var_mpfr << bertini::multiprecision::rand(),bertini::multiprecision::rand(),bertini::multiprecision::rand(); - sys.SetVariables(var_mpfr); - exact_mpfr[0] = 3*var_mpfr(1)*var_mpfr(2); - exact_mpfr[1] = 3*var_mpfr(0)*var_mpfr(2); - exact_mpfr[2] = 3*var_mpfr(0)*var_mpfr(1); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() / exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() / exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() / exact_mpfr[2].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() / exact_mpfr[2].imag() -1) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_constant){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = 4.5 + i*8.2;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{0,0,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 0); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - - - std::vector exact_dbl = {0.0, 0.0, 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.0"),mpfr("0.0")}; - - for(int ii = 0; ii < vars.size(); ++ii) - { - BOOST_CHECK(fabs(JFunc->EvalJ(vars[ii]).real() - exact_dbl[ii].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[ii]).imag() - exact_dbl[ii].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[ii]).real() - exact_mpfr[ii].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[ii]).imag() - exact_mpfr[ii].imag() ) < threshold_clearance_mp); - } -} - - -BOOST_AUTO_TEST_CASE(diff_sum_xyz_constant){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = x-y+z-4.5+i*7.3;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{1,1,1}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 1); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),1); - - - std::vector exact_dbl = {1.0, -1.0, 1.0}; - std::vector exact_mpfr = {mpfr("1.0"),mpfr("-1.0"),mpfr("1.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); - -} - - - -BOOST_AUTO_TEST_CASE(diff_x_squared_times_z_cubed){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (x^2)*(y^3);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{2,3,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 5); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),2); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),3); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - std::vector exact_dbl = {2.0*xnum_dbl*pow(ynum_dbl,3.0), 3.0*pow(ynum_dbl*xnum_dbl,2.0), 0.0}; - std::vector exact_mpfr = {mpfr("2.0")*xnum_mpfr*pow(ynum_mpfr,3),mpfr("3.0")*pow(ynum_mpfr,2)*pow(xnum_mpfr,2),mpfr("0.0")}; - - - - - JFunc->Reset(); - sys.SetVariables(var_dbl); - auto J_val_0_d = JFunc->EvalJ(vars[0]); - - JFunc->Reset(); - sys.SetVariables(var_dbl); - auto J_val_1_d = JFunc->EvalJ(vars[1]); - - JFunc->Reset(); - sys.SetVariables(var_dbl); - auto J_val_2_d = JFunc->EvalJ(vars[2]); - - - JFunc->Reset(); - sys.SetVariables(var_mpfr); - auto J_val_0_mp = JFunc->EvalJ(vars[0]); - - JFunc->Reset(); - sys.SetVariables(var_mpfr); - auto J_val_1_mp = JFunc->EvalJ(vars[1]); - - JFunc->Reset(); - sys.SetVariables(var_mpfr); - auto J_val_2_mp = JFunc->EvalJ(vars[2]); - - - BOOST_CHECK(fabs(J_val_0_d.real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_0_d.imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_0_mp.real()/exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(J_val_0_mp.imag()/exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(J_val_1_d.real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_1_d.imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_1_mp.real()/exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(J_val_1_mp.imag()/exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(J_val_2_d.real() - exact_dbl[2].real()) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_2_d.imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_2_mp.real()-exact_mpfr[2].real()) < threshold_clearance_mp); - BOOST_CHECK(fabs(J_val_2_mp.imag()-exact_mpfr[2].imag()) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_x_squared_over_y_cubed){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (x^2)/(y^3);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{2,-1,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),2); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - - - std::vector exact_dbl = {2.0*xnum_dbl/pow(ynum_dbl,3.0), -3.0*pow(xnum_dbl,2.0)/pow(ynum_dbl,4.0), 0.0}; - std::vector exact_mpfr = {mpfr("2.0")*xnum_mpfr/pow(ynum_mpfr,3),mpfr("-3.0")*pow(xnum_mpfr,2)/pow(ynum_mpfr,4),mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_x_squared_times_lx_plus_numl){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (x^2)*(x+3);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{3,0,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 3); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),3); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - - std::vector exact_dbl = {3.0*pow(xnum_dbl,2.0) + 6.0*xnum_dbl, 0.0, 0.0}; - std::vector exact_mpfr = {mpfr("3.0")*pow(xnum_mpfr,mpfr("2.0")) + mpfr("6.0")*xnum_mpfr,mpfr("0.0"),mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - -BOOST_AUTO_TEST_CASE(diff_2y_over_ly_squared_minus_numl){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = y/(y+1);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{0,-1,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - - std::vector exact_dbl = {0.0, pow(ynum_dbl+1.0,-2.0), 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),pow(ynum_mpfr+mpfr("1.0"),mpfr("-2.0")),mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - -BOOST_AUTO_TEST_CASE(diff_sin_x){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = sin(x);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{-1,0,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - std::vector exact_dbl = {cos(xnum_dbl), 0.0, 0.0}; - std::vector exact_mpfr = {cos(xnum_mpfr),mpfr("0.0"),mpfr("0.0")}; - - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[0]).real(), exact_dbl[0].real(), threshold_clearance_d); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[0]).imag(), exact_dbl[0].imag(), threshold_clearance_d); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[0]).real(), exact_mpfr[0].real(), threshold_clearance_mp); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[0]).imag(), exact_mpfr[0].imag(), threshold_clearance_mp); - - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[1]).real(), exact_dbl[1].real(), threshold_clearance_d); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[1]).imag(), exact_dbl[1].imag(), threshold_clearance_d); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[1]).real(), exact_mpfr[1].real(), threshold_clearance_mp); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[1]).imag(), exact_mpfr[1].imag(), threshold_clearance_mp); - - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[2]).real(), exact_dbl[2].real(), threshold_clearance_d); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[2]).imag(), exact_dbl[2].imag(), threshold_clearance_d); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[2]).real(), exact_mpfr[2].real(), threshold_clearance_mp); - BOOST_CHECK_CLOSE( JFunc->EvalJ(vars[2]).imag(), exact_mpfr[2].imag(), threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_cos_y){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = cos(y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{0,-1,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - - std::vector exact_dbl = {0.0, -1.0*sin(ynum_dbl), 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),-sin(ynum_mpfr),mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_tan_z){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = tan(z);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),-1); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - std::vector exact_dbl = {0.0,0.0, (1.0/cos(znum_dbl))*(1.0/cos(znum_dbl))}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.0"),(mpfr("1.0")/cos(znum_mpfr))*(mpfr("1.0")/cos(znum_mpfr))}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_exp_x){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = exp(x);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - - std::vector exact_dbl = {exp(xnum_dbl), 0.0, 0.0}; - std::vector exact_mpfr = {exp(xnum_mpfr),mpfr("0.0"),mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_log_x){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = log(x^2+y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - sys.Differentiate(); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - - std::vector exact_dbl = {2.0*xnum_dbl/(xnum_dbl*xnum_dbl+ynum_dbl), 1.0/(xnum_dbl*xnum_dbl+ynum_dbl), 0.0}; - std::vector exact_mpfr = {mpfr(2.0)*xnum_mpfr/(xnum_mpfr*xnum_mpfr+ynum_mpfr), mpfr(1.0)/(xnum_mpfr*xnum_mpfr+ynum_mpfr),mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_sqrt_y){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = sqrt(y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - std::vector exact_dbl = {0.0, 0.5/sqrt(ynum_dbl), 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.5")/sqrt(ynum_mpfr),mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - -/////////// Chain Rule /////////////////// -BOOST_AUTO_TEST_CASE(diff_lz_plus_3l_cubed){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (z+3)^3;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{0,0,3}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),3); - - - BOOST_CHECK_EQUAL(func->Degree(vars), 3); - - std::vector exact_dbl = {0.0, 0.0, 3.0*(pow(znum_dbl+3.0,2.0))}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.0"),mpfr("3.0")*pow(znum_mpfr+mpfr("3.0"),mpfr("2.0"))}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - - -BOOST_AUTO_TEST_CASE(diff_x_squared_plus_y_squared_plus_z_squared){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = x^2+y^2+z^2;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{2,2,2}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 2); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),2); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),2); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),2); - - - - - std::vector exact_dbl = {2.0*xnum_dbl, 2.0*ynum_dbl, 2.0*znum_dbl}; - std::vector exact_mpfr = {mpfr("2.0")*xnum_mpfr, mpfr("2.0")*ynum_mpfr, mpfr("2.0")*znum_mpfr}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - - - -BOOST_AUTO_TEST_CASE(diff_sin_lx_squared_times_yl){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = sin(x*y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{-1,-1,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - std::vector exact_dbl = {cos(xnum_dbl*ynum_dbl)*ynum_dbl, cos(xnum_dbl*ynum_dbl)*xnum_dbl, 0.0}; - std::vector exact_mpfr = {cos(xnum_mpfr*ynum_mpfr)*ynum_mpfr, - cos(xnum_mpfr*ynum_mpfr)*xnum_mpfr, mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() / exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() / exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_cos_lx_squaredl){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = cos(x^2);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{-1,0,0}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),0); - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - std::vector exact_dbl = {-2.0*sin(pow(xnum_dbl,2.0))*xnum_dbl, 0.0, 0.0}; - std::vector exact_mpfr = {mpfr("-2.0")*sin(pow(xnum_mpfr,mpfr("2.0")))*xnum_mpfr,mpfr("0.0"), mpfr("0.0")}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_tan_lx_over_zl){ - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = tan(x/z);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto JFunc = Jacobian::Make(func->Differentiate()); - - std::vector multidegree{-1,0,-1}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars[0]),-1); - BOOST_CHECK_EQUAL(func->Degree(vars[1]),0); - BOOST_CHECK_EQUAL(func->Degree(vars[2]),-1); - - - - BOOST_CHECK_EQUAL(func->Degree(vars), -1); - - std::vector exact_dbl = {1.0/( znum_dbl*pow( cos(xnum_dbl/znum_dbl), 2.0 ) ), 0.0, -xnum_dbl/( pow(znum_dbl, 2.0)*pow( cos(xnum_dbl/znum_dbl), 2.0 ) )}; - std::vector exact_mpfr = {mpfr("1.0")/( znum_mpfr*pow( cos(xnum_mpfr/znum_mpfr), mpfr("2.0") ) ), mpfr("0.0"), -xnum_mpfr/( pow(znum_mpfr, mpfr("2.0"))*pow( cos(xnum_mpfr/znum_mpfr), mpfr("2.0") ) )}; - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[0]).imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[1]).imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(JFunc->EvalJ(vars[2]).imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - - - -BOOST_AUTO_TEST_CASE(arcsine_differentiate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = asin(pow(x,2)+1); - auto J = Jacobian::Make(N->Differentiate()); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - //(2*x)/(1 - (x^2 + 1)^2)^(1/2) - dbl exact_dbl = 2.0*xnum_dbl / pow(1.0 - pow((xnum_dbl*xnum_dbl + 1.0),2),0.5); - mpfr exact_mpfr = bertini::mpfr_complex(2.0)*xnum_mpfr / pow(bertini::mpfr_complex(1.0) - pow(xnum_mpfr*xnum_mpfr + bertini::mpfr_complex(1.0),2),mpfr(0.5)); - - BOOST_CHECK(fabs(J->EvalJ(x).real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J->EvalJ(x).imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J->EvalJ(x).real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(J->EvalJ(x).imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(arccosine_differentiate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = acos(pow(x,2)+1); - auto J = Jacobian::Make(N->Differentiate()); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = -2.0*xnum_dbl / pow(1.0 - pow((xnum_dbl*xnum_dbl + 1.0),2),0.5); - mpfr exact_mpfr = -bertini::mpfr_complex(2.0)*xnum_mpfr / pow(bertini::mpfr_complex(1.0) - pow(xnum_mpfr*xnum_mpfr + bertini::mpfr_complex(1.0),2),mpfr(0.5)); - - BOOST_CHECK(fabs(J->EvalJ(x).real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J->EvalJ(x).imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J->EvalJ(x).real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(J->EvalJ(x).imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); -} - -BOOST_AUTO_TEST_CASE(arctangent_differentiate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = atan(pow(x,2)+1); - auto J = Jacobian::Make(N->Differentiate()); - - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - //(2*x)/((x^2 + 1)^2 + 1) - dbl exact_dbl = 2.0*xnum_dbl / ( pow((xnum_dbl*xnum_dbl + 1.0),2) + 1.0); - mpfr exact_mpfr = bertini::mpfr_complex(2.0)*xnum_mpfr / ( pow((xnum_mpfr*xnum_mpfr + bertini::mpfr_complex(1.0)),2) + bertini::mpfr_complex(1.0)); - - BOOST_CHECK(fabs(J->EvalJ(x).real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J->EvalJ(x).imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J->EvalJ(x).real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(J->EvalJ(x).imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(integer_power) -{ - - DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr t = Variable::Make("t"); - - auto f = pow(x - 1,2)*(1-t) + (pow(x,2) + 1)*t; - std::shared_ptr j = Jacobian::Make(f->Differentiate()); - - - x->set_current_value(mpfr("-0.844487","-0.535576")); - t->set_current_value(mpfr("0.779871","0.712645")); - - auto J = j->EvalJ(x); - BOOST_CHECK(abs(J - mpfr("-2.129232","0.354138")) < threshold_clearance_mp); - - - x->set_current_value(mpfr("0.900000000000000","0.435889894354067355223698198386")); - t->set_current_value(mpfr("0.1")); - j->Reset(); - J = j->EvalJ(x); - - BOOST_CHECK(abs(real(J)-mpfr_float(0)) < threshold_clearance_mp); - BOOST_CHECK_CLOSE(imag(J), mpfr_float("0.871779788708134710447396396772"), 100*threshold_clearance_mp); -} - - - - -BOOST_AUTO_TEST_CASE(integer_power_system) -{ - using System = bertini::System; - - DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - System sys; - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr t = Variable::Make("t"); - - sys.AddFunction( pow(x - 1,2)*(1-t) + (pow(x,2) + 1)*t); - - sys.AddVariableGroup(bertini::VariableGroup({x})); - sys.AddPathVariable(t); - - bertini::Vec curr_x(1); - curr_x << mpfr("-0.844487","-0.535576"); - mpfr curr_t("0.779871","0.712645"); - - auto J = sys.Jacobian(curr_x,curr_t); - - BOOST_CHECK_CLOSE( real(J(0,0)), mpfr_float("-2.129232"), threshold_clearance_mp); - BOOST_CHECK_CLOSE( imag(J(0,0)), mpfr_float("0.354138"), threshold_clearance_mp); - - curr_x << mpfr("0.900000000000000","0.435889894354067355223698198386"); - curr_t = mpfr("0.1"); - - J = sys.Jacobian(curr_x,curr_t); - - BOOST_CHECK_SMALL(real(J(0,0)), threshold_clearance_mp); // target value is 0, so relative is not useful - BOOST_CHECK_CLOSE(imag(J(0,0)), mpfr_float("0.871779788708134710447396396772"), 100*threshold_clearance_mp); -} - - - - -BOOST_AUTO_TEST_CASE(linprod_diff_eval) -{ - using bertini::VariableGroup; - - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - auto h0 = Variable::Make("HOM0"); - auto h1 = Variable::Make("HOM1"); - auto z = Variable::Make("z"); - - - - VariableGroup v0{x,z,y}; - VariableGroup v1{y}; - Mat coeff_dbl(3,4); - Mat coeff_mpfr(3,4); - - for(int ii = 0; ii < 3; ++ii) - { - for(int jj = 0; jj < 4; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+1, jj+1); - coeff_mpfr(ii,jj) = mpfr(ii+1, jj+1); - } - } - - - - std::shared_ptr linprod_node = (mpfr(1,1)*x + mpfr(1,2)*z + mpfr(1,3)*y+ mpfr(1,4)) * (mpfr(2,1)*x + mpfr(2,2)*z + mpfr(2,3)*y+ mpfr(2,4))*(mpfr(3,1)*x + mpfr(3,2)*z + mpfr(3,3)*y+ mpfr(3,4)); - std::shared_ptr linprod = bertini::node::LinearProduct::Make(v0, coeff_mpfr); - - dbl xval_d = dbl(.5,1); - dbl yval_d = dbl(.6,1); - dbl zval_d = dbl(.7,1); - dbl h0val_d = dbl(.34, -2.1); - dbl h1val_d = dbl(-1.2, .0043); - mpfr xval_mp = mpfr(".5", "1"); - mpfr yval_mp = mpfr(".6", "1"); - mpfr zval_mp = mpfr(".7", "1"); - mpfr h0val_mp = mpfr(".34", "-2.1"); - mpfr h1val_mp = mpfr("-1.2", ".0043"); - - v0[0]->set_current_value(xval_d); - v0[1]->set_current_value(zval_d); - v1[0]->set_current_value(yval_d); - v0[0]->set_current_value(xval_mp); - v0[1]->set_current_value(zval_mp); - v1[0]->set_current_value(yval_mp); - - auto J_node = bertini::node::Jacobian::Make(linprod_node->Differentiate()); - auto J = bertini::node::Jacobian::Make(linprod->Differentiate()); - - dbl evalx_d = J->EvalJ(x); - dbl exactx_d = J_node->EvalJ(x); - mpfr evalx_mp = J->EvalJ(x); - mpfr exactx_mp = J_node->EvalJ(x); - - - - BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - - - evalx_d = J->EvalJ(z); - exactx_d = J_node->EvalJ(z); - evalx_mp = J->EvalJ(z); - exactx_mp = J_node->EvalJ(z); - - BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - - evalx_d = J->EvalJ(y); - exactx_d = J_node->EvalJ(y); - evalx_mp = J->EvalJ(y); - exactx_mp = J_node->EvalJ(y); - - BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - - -} - - -BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/differentiate_wrt_var.cpp b/core/test/classes/differentiate_wrt_var.cpp deleted file mode 100644 index 6c2baacda..000000000 --- a/core/test/classes/differentiate_wrt_var.cpp +++ /dev/null @@ -1,1064 +0,0 @@ -//This file is part of Bertini 2. -// -//differentiate_test.cpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//differentiate_test.cpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with differentiate_test.cpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// silviana amethyst, university of wisconsin eau claire -// -// Created by Collins, James B. on 4/30/15. -// Copyright (c) 2015 West Texas A&M University. All rights reserved. - -#include - -#include -#include -#include - -#include "bertini2/function_tree.hpp" -#include "bertini2/system/system.hpp" -#include "bertini2/io/parsing/system_parsers.hpp" - -#include -#include - -#include - - - - -#include "externs.hpp" - - - - - -BOOST_AUTO_TEST_SUITE(differentiate_wrt_variable) - -using bertini::DefaultPrecision; -using dbl = std::complex; -using Variable = bertini::node::Variable; -using Node = bertini::node::Node; -using Function = bertini::node::Function; -using Jacobian = bertini::node::Jacobian; - -using dbl = bertini::dbl; -using mpfr = bertini::mpfr_complex; -using mpfr_float = bertini::mpfr_float; -/////////// Basic Operations Alone /////////////////// - -BOOST_AUTO_TEST_CASE(just_diff_a_function){ - std::string str = "function f; variable_group x,y,z; f = x*y +y^2 - z*x + 9;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - for(auto vv : vars) - { - func->Differentiate(vv); - } - - std::vector multidegree{1,2,1}; - bool multidegree_ok = multidegree==func->MultiDegree(vars); - BOOST_CHECK(multidegree_ok); - - BOOST_CHECK_EQUAL(func->Degree(vars), 2); -} - - -BOOST_AUTO_TEST_CASE(diff_3xyz){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = 3*x*y*z;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - std::vector exact_dbl = {3.0*ynum_dbl*znum_dbl, 3.0*xnum_dbl*znum_dbl, 3.0*ynum_dbl*xnum_dbl}; - std::vector exact_mpfr = {mpfr("3.0")*ynum_mpfr*znum_mpfr,mpfr("3.0")*xnum_mpfr*znum_mpfr,mpfr("3.0")*ynum_mpfr*xnum_mpfr}; - - BOOST_CHECK(fabs(dx->Eval().real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() / exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() / exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() / exact_dbl[2].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() / exact_dbl[2].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() / exact_mpfr[2].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() / exact_mpfr[2].imag() -1) < threshold_clearance_mp); - - var_mpfr << bertini::multiprecision::rand(),bertini::multiprecision::rand(),bertini::multiprecision::rand(); - sys.SetVariables(var_mpfr); - exact_mpfr[0] = 3*var_mpfr(1)*var_mpfr(2); - exact_mpfr[1] = 3*var_mpfr(0)*var_mpfr(2); - exact_mpfr[2] = 3*var_mpfr(0)*var_mpfr(1); - - dx->Reset(); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - dy->Reset(); - BOOST_CHECK(fabs(dy->Eval().real() / exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() / exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - dz->Reset(); - BOOST_CHECK(fabs(dz->Eval().real() / exact_mpfr[2].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() / exact_mpfr[2].imag() -1) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_constant){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = 4.5 + i*8.2;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - std::vector exact_dbl = {0.0, 0.0, 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.0"),mpfr("0.0")}; - - dx->Reset(); - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real()) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag()) < threshold_clearance_mp); - - dy->Reset(); - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real()) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag()) < threshold_clearance_mp); - - dz->Reset(); - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real()) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag()) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_sum_xyz_constant){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = x-y+z-4.5+i*7.3;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - std::vector exact_dbl {1.0, -1.0, 1.0}; - std::vector exact_mpfr {mpfr("1.0"),mpfr("-1.0"),mpfr("1.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); - -} - - - -BOOST_AUTO_TEST_CASE(diff_x_squared_times_z_cubed){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (x^2)*(y^3);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - std::vector exact_dbl = {2.0*xnum_dbl*pow(ynum_dbl,3.0), 3.0*pow(ynum_dbl*xnum_dbl,2.0), 0.0}; - std::vector exact_mpfr = {mpfr("2.0")*xnum_mpfr*pow(ynum_mpfr,3),mpfr("3.0")*pow(ynum_mpfr,2)*pow(xnum_mpfr,2),mpfr("0.0")}; - - - - - dx->Reset(); - sys.SetVariables(var_dbl); - auto J_val_0_d = dx->Eval(); - - dy->Reset(); - sys.SetVariables(var_dbl); - auto J_val_1_d = dy->Eval(); - - dz->Reset(); - sys.SetVariables(var_dbl); - auto J_val_2_d = dz->Eval(); - - - dx->Reset(); - sys.SetVariables(var_mpfr); - auto J_val_0_mp = dx->Eval(); - - dy->Reset(); - sys.SetVariables(var_mpfr); - auto J_val_1_mp = dy->Eval(); - - dz->Reset(); - sys.SetVariables(var_mpfr); - auto J_val_2_mp = dz->Eval(); - - - BOOST_CHECK(fabs(J_val_0_d.real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_0_d.imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_0_mp.real()/exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(J_val_0_mp.imag()/exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(J_val_1_d.real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_1_d.imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_1_mp.real()/exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(J_val_1_mp.imag()/exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(J_val_2_d.real() - exact_dbl[2].real()) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_2_d.imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(J_val_2_mp.real()-exact_mpfr[2].real()) < threshold_clearance_mp); - BOOST_CHECK(fabs(J_val_2_mp.imag()-exact_mpfr[2].imag()) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_x_squared_over_y_cubed){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (x^2)/(y^3);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - - std::vector exact_dbl = {2.0*xnum_dbl/pow(ynum_dbl,3.0), -3.0*pow(xnum_dbl,2.0)/pow(ynum_dbl,4.0), 0.0}; - std::vector exact_mpfr = {mpfr("2.0")*xnum_mpfr/pow(ynum_mpfr,3),mpfr("-3.0")*pow(xnum_mpfr,2)/pow(ynum_mpfr,4),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_x_squared_times_lx_plus_numl){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (x^2)*(x+3);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - - - std::vector exact_dbl = {3.0*pow(xnum_dbl,2.0) + 6.0*xnum_dbl, 0.0, 0.0}; - std::vector exact_mpfr = {mpfr("3.0")*pow(xnum_mpfr,mpfr("2.0")) + mpfr("6.0")*xnum_mpfr,mpfr("0.0"),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - -BOOST_AUTO_TEST_CASE(diff_2y_over_ly_squared_minus_numl){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = y/(y+1);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - - std::vector exact_dbl = {0.0, pow(ynum_dbl+1.0,-2.0), 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),pow(ynum_mpfr+mpfr("1.0"),mpfr("-2.0")),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - -BOOST_AUTO_TEST_CASE(diff_sin_x){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = sin(x);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - - std::vector exact_dbl = {cos(xnum_dbl), 0.0, 0.0}; - std::vector exact_mpfr = {cos(xnum_mpfr),mpfr("0.0"),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_cos_y){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = cos(y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - - std::vector exact_dbl = {0.0, -1.0*sin(ynum_dbl), 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),-sin(ynum_mpfr),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_tan_z){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = tan(z);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - std::vector exact_dbl = {0.0,0.0, (1.0/cos(znum_dbl))*(1.0/cos(znum_dbl))}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.0"),(mpfr("1.0")/cos(znum_mpfr))*(mpfr("1.0")/cos(znum_mpfr))}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_exp_x){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = exp(x);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - std::vector exact_dbl = {exp(xnum_dbl), 0.0, 0.0}; - std::vector exact_mpfr = {exp(xnum_mpfr),mpfr("0.0"),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_log_x){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = log(x^2+y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - sys.Differentiate(); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - std::vector exact_dbl = {2.0*xnum_dbl/(xnum_dbl*xnum_dbl+ynum_dbl), 1.0/(xnum_dbl*xnum_dbl+ynum_dbl), 0.0}; - std::vector exact_mpfr = {mpfr(2.0)*xnum_mpfr/(xnum_mpfr*xnum_mpfr+ynum_mpfr), mpfr(1.0)/(xnum_mpfr*xnum_mpfr+ynum_mpfr),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_sqrt_y){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = sqrt(y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - std::vector exact_dbl = {0.0, 0.5/sqrt(ynum_dbl), 0.0}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.5")/sqrt(ynum_mpfr),mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - -/////////// Chain Rule /////////////////// -BOOST_AUTO_TEST_CASE(diff_lz_plus_3l_cubed){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = (z+3)^3;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - std::vector exact_dbl = {0.0, 0.0, 3.0*(pow(znum_dbl+3.0,2.0))}; - std::vector exact_mpfr = {mpfr("0.0"),mpfr("0.0"),mpfr("3.0")*pow(znum_mpfr+mpfr("3.0"),mpfr("2.0"))}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - - -BOOST_AUTO_TEST_CASE(diff_x_squared_plus_y_squared_plus_z_squared){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = x^2+y^2+z^2;"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - - - - std::vector exact_dbl = {2.0*xnum_dbl, 2.0*ynum_dbl, 2.0*znum_dbl}; - std::vector exact_mpfr = {mpfr("2.0")*xnum_mpfr, mpfr("2.0")*ynum_mpfr, mpfr("2.0")*znum_mpfr}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - - - -BOOST_AUTO_TEST_CASE(diff_sin_lx_squared_times_yl){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = sin(x*y);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - std::vector exact_dbl = {cos(xnum_dbl*ynum_dbl)*ynum_dbl, cos(xnum_dbl*ynum_dbl)*xnum_dbl, 0.0}; - std::vector exact_mpfr = {cos(xnum_mpfr*ynum_mpfr)*ynum_mpfr, - cos(xnum_mpfr*ynum_mpfr)*xnum_mpfr, mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() / exact_dbl[1].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() / exact_dbl[1].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() / exact_mpfr[1].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() / exact_mpfr[1].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_cos_lx_squaredl){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = cos(x^2);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - std::vector exact_dbl = {-2.0*sin(pow(xnum_dbl,2.0))*xnum_dbl, 0.0, 0.0}; - std::vector exact_mpfr = {mpfr("-2.0")*sin(pow(xnum_mpfr,mpfr("2.0")))*xnum_mpfr,mpfr("0.0"), mpfr("0.0")}; - - BOOST_CHECK(fabs(dx->Eval().real() / exact_dbl[0].real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_dbl[0].imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr[0].real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr[0].imag() -1) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(diff_tan_lx_over_zl){ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::string str = "function f; variable_group x,y,z; f = tan(x/z);"; - - bertini::System sys; - bertini::parsing::classic::parse(str.begin(), str.end(), sys); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); - - Eigen::Matrix var_dbl; - Eigen::Matrix var_mpfr; - - var_dbl << xnum_dbl, ynum_dbl, znum_dbl; - var_mpfr << xnum_mpfr, ynum_mpfr, znum_mpfr; - sys.SetVariables(var_dbl); - sys.SetVariables(var_mpfr); - - auto func = sys.Function(0); - auto vars = sys.Variables(); - - auto dx = func->Differentiate(vars[0]); - auto dy = func->Differentiate(vars[1]); - auto dz = func->Differentiate(vars[2]); - - std::vector exact_dbl = {1.0/( znum_dbl*pow( cos(xnum_dbl/znum_dbl), 2.0 ) ), 0.0, -xnum_dbl/( pow(znum_dbl, 2.0)*pow( cos(xnum_dbl/znum_dbl), 2.0 ) )}; - std::vector exact_mpfr = {mpfr("1.0")/( znum_mpfr*pow( cos(xnum_mpfr/znum_mpfr), mpfr("2.0") ) ), mpfr("0.0"), -xnum_mpfr/( pow(znum_mpfr, mpfr("2.0"))*pow( cos(xnum_mpfr/znum_mpfr), mpfr("2.0") ) )}; - - BOOST_CHECK(fabs(dx->Eval().real() - exact_dbl[0].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_dbl[0].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() - exact_mpfr[0].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() - exact_mpfr[0].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dy->Eval().real() - exact_dbl[1].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_dbl[1].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dy->Eval().real() - exact_mpfr[1].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dy->Eval().imag() - exact_mpfr[1].imag() ) < threshold_clearance_mp); - - BOOST_CHECK(fabs(dz->Eval().real() - exact_dbl[2].real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_dbl[2].imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(dz->Eval().real() - exact_mpfr[2].real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(dz->Eval().imag() - exact_mpfr[2].imag() ) < threshold_clearance_mp); -} - - - - - -BOOST_AUTO_TEST_CASE(arcsine_differentiate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = asin(pow(x,2)+1); - auto dx = N->Differentiate(x); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - //(2*x)/(1 - (x^2 + 1)^2)^(1/2) - dbl exact_dbl = 2.0*xnum_dbl / pow(1.0 - pow((xnum_dbl*xnum_dbl + 1.0),2),0.5); - mpfr exact_mpfr = bertini::mpfr_complex(2.0)*xnum_mpfr / pow(bertini::mpfr_complex(1.0) - pow(xnum_mpfr*xnum_mpfr + bertini::mpfr_complex(1.0),2),mpfr(0.5)); - - BOOST_CHECK(fabs(dx->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(arccosine_differentiate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = acos(pow(x,2)+1); - auto dx = N->Differentiate(x); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = -2.0*xnum_dbl / pow(1.0 - pow((xnum_dbl*xnum_dbl + 1.0),2),0.5); - mpfr exact_mpfr = -bertini::mpfr_complex(2.0)*xnum_mpfr / pow(bertini::mpfr_complex(1.0) - pow(xnum_mpfr*xnum_mpfr + bertini::mpfr_complex(1.0),2),mpfr(0.5)); - - BOOST_CHECK(fabs(dx->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); -} - -BOOST_AUTO_TEST_CASE(arctangent_differentiate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = atan(pow(x,2)+1); - auto dx = N->Differentiate(x); - - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - //(2*x)/((x^2 + 1)^2 + 1) - dbl exact_dbl = 2.0*xnum_dbl / ( pow((xnum_dbl*xnum_dbl + 1.0),2) + 1.0); - mpfr exact_mpfr = bertini::mpfr_complex(2.0)*xnum_mpfr / ( pow((xnum_mpfr*xnum_mpfr + bertini::mpfr_complex(1.0)),2) + bertini::mpfr_complex(1.0)); - - BOOST_CHECK(fabs(dx->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(dx->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(dx->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(integer_power) -{ - - - DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr t = Variable::Make("t"); - - auto f = pow(x - 1,2)*(1-t) + (pow(x,2) + 1)*t; - auto dx = f->Differentiate(x); - - - x->set_current_value(mpfr("-0.844487","-0.535576")); - t->set_current_value(mpfr("0.779871","0.712645")); - - auto J = dx->Eval(); - BOOST_CHECK(abs(J - mpfr("-2.129232","0.354138")) < threshold_clearance_mp); - - - x->set_current_value(mpfr("0.900000000000000","0.435889894354067355223698198386")); - t->set_current_value(mpfr("0.1")); - dx->Reset(); - J = dx->Eval(); - - BOOST_CHECK(abs( real(J) ) < threshold_clearance_mp); - BOOST_CHECK(abs( imag(J) - mpfr_float("0.871779788708134710447396396772")) < threshold_clearance_mp); -} - - - -BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/eigen_test.cpp b/core/test/classes/eigen_test.cpp index 6c7d4a0cd..2cf335693 100644 --- a/core/test/classes/eigen_test.cpp +++ b/core/test/classes/eigen_test.cpp @@ -89,8 +89,8 @@ using bertini::KahanMatrix; Eigen::Matrix A = KahanMatrix(size, 0.285), B(size,size), C; - for (int ii=0; ii, Eigen::Dynamic, Eigen::Dynamic> A = KahanMatrix(size, std::complex(0.285)), B(size,size), C; - for (int ii=0; ii A = KahanMatrix(size, bertini::mpfr_complex("0.285","0.0")), B(size,size), C; - for (int ii=0; ii A(1,3); A << 1, 2, 3; - double n = A.norm(); + [[maybe_unused]] double n = A.norm(); } BOOST_AUTO_TEST_CASE(dot_product_with_mpfr_type) @@ -375,7 +375,7 @@ using bertini::KahanMatrix; data_type q(1); int a(1); - auto b = a*q; + [[maybe_unused]] auto b = a*q; Eigen::Matrix A(2,2); A << data_type(2), data_type(1), data_type(1), data_type(2); @@ -396,7 +396,7 @@ using bertini::KahanMatrix; data_type q(1); long a(1); - auto b = a*q; + [[maybe_unused]] auto b = a*q; Eigen::Matrix A(2,2); A << data_type(2), data_type(1), data_type(1), data_type(2); @@ -416,7 +416,7 @@ using bertini::KahanMatrix; data_type q(1); bertini::mpz_int a(1); - auto b = a*q; + [[maybe_unused]] auto b = a*q; Eigen::Matrix A(2,2); A << data_type(2), data_type(1), data_type(1), data_type(2); @@ -470,7 +470,7 @@ using bertini::KahanMatrix; data_type q(1); int a(1); - auto b = a*q; + [[maybe_unused]] auto b = a*q; Eigen::Matrix A(2,2); A << data_type(2), data_type(1), data_type(1), data_type(2); diff --git a/core/test/classes/eval_expression_test.cpp b/core/test/classes/eval_expression_test.cpp new file mode 100644 index 000000000..531c38b13 --- /dev/null +++ b/core/test/classes/eval_expression_test.cpp @@ -0,0 +1,189 @@ +#include + +#include "bertini2/system/eval_expression.hpp" +#include "bertini2/function_tree.hpp" + +#include +#include + +using bertini::EvalExpression; +using bertini::node::Variable; +using bertini::node::Integer; +using Nd = std::shared_ptr; +using dbl = bertini::dbl; +using mpfr_complex = bertini::mpfr_complex; + +BOOST_AUTO_TEST_SUITE(eval_expression_without_a_system) + +// Evaluate a bare expression at a point given as a name->value map, with no System owned +// by the caller --- the expression's evaluator is compiled internally and memoized on the node. +BOOST_AUTO_TEST_CASE(evaluates_a_bare_polynomial_in_double) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + + Nd f = x*x + y; // 2^2 + 5 == 9 + + std::map values{ {"x", dbl(2,0)}, {"y", dbl(5,0)} }; + auto result = EvalExpression(f, values); + + BOOST_CHECK_SMALL(std::abs(result - dbl(9,0)), 1e-14); +} + +// Variables are matched to values by name regardless of the order they appear in the map +// or in the expression --- variables are discovered and ordered by name internally. +BOOST_AUTO_TEST_CASE(binds_values_by_name_not_position) +{ + auto a = Variable::Make("a"); + auto b = Variable::Make("b"); + + Nd f = a - b; // a=7, b=3 -> 4, even though 'b' is listed first in the map + + std::map values{ {"b", dbl(3,0)}, {"a", dbl(7,0)} }; + auto result = EvalExpression(f, values); + + BOOST_CHECK_SMALL(std::abs(result - dbl(4,0)), 1e-14); +} + +// A constant expression has no variables; an empty value map is the correct input. +BOOST_AUTO_TEST_CASE(evaluates_a_constant_with_no_variables) +{ + Nd f = Integer::Make(3) * Integer::Make(4); // 12 + + std::map values; // empty + auto result = EvalExpression(f, values); + + BOOST_CHECK_SMALL(std::abs(result - dbl(12,0)), 1e-14); +} + +// A constant expression evaluates in multiple precision too (the empty-point path). +BOOST_AUTO_TEST_CASE(evaluates_a_constant_in_multiple_precision) +{ + bertini::DefaultPrecision(50); + + Nd f = Integer::Make(3) * Integer::Make(4); // 12 + + std::map values; // empty + auto result = EvalExpression(f, values); + + BOOST_CHECK(abs(result - mpfr_complex(12)) < 1e-40); +} + +// Every variable of the expression must be given a value; a missing one is an error. +BOOST_AUTO_TEST_CASE(missing_variable_value_is_an_error) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + + Nd f = x + y; + + std::map values{ {"x", dbl(1,0)} }; // y omitted + BOOST_CHECK_THROW(EvalExpression(f, values), std::runtime_error); +} + +// Supplying a value for a name not in the expression is an error (typo guard). +BOOST_AUTO_TEST_CASE(value_for_unknown_variable_is_an_error) +{ + auto x = Variable::Make("x"); + + Nd f = x*x; + + std::map values{ {"x", dbl(2,0)}, {"z", dbl(9,0)} }; // z not present + BOOST_CHECK_THROW(EvalExpression(f, values), std::runtime_error); +} + +// The same expression evaluates correctly in multiple precision. +BOOST_AUTO_TEST_CASE(evaluates_in_multiple_precision) +{ + bertini::DefaultPrecision(50); + + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + + Nd f = x*x + y; // 3^2 + 4 == 13 + + std::map values{ + {"x", mpfr_complex(3)}, {"y", mpfr_complex(4)} }; + auto result = EvalExpression(f, values); + + BOOST_CHECK(abs(result - mpfr_complex(13)) < 1e-40); +} + +// The compiled evaluator is memoized on the (immutable, hash-consed) expression node: the first +// evaluation compiles and caches it; subsequent evaluations reuse the same cached program rather +// than recompiling a fresh one each call. +BOOST_AUTO_TEST_CASE(memoizes_the_compiled_program_on_the_node) +{ + auto x = Variable::Make("x"); + Nd f = x*x + Integer::Make(1); + + BOOST_CHECK(f->EvalProgram() == nullptr); // nothing compiled yet + + std::map values{ {"x", dbl(3,0)} }; + auto r1 = EvalExpression(f, values); // x=3 -> 10 + BOOST_CHECK_SMALL(std::abs(r1 - dbl(10,0)), 1e-14); + + auto cached = f->EvalProgram(); + BOOST_CHECK(cached != nullptr); // first eval compiled + cached the program + + values["x"] = dbl(5,0); + auto r2 = EvalExpression(f, values); // x=5 -> 26, still correct + BOOST_CHECK_SMALL(std::abs(r2 - dbl(26,0)), 1e-14); + BOOST_CHECK(f->EvalProgram() == cached); // and reused the very same cached program +} + +// Every operator node compiles and evaluates correctly through the straight-line program. This +// (with the differentiation case below) is the focused successor to the old node-level evaluation +// matrices: node evaluation is gone, so operator correctness is verified through the one engine. +BOOST_AUTO_TEST_CASE(every_operator_compiles_and_evaluates_through_the_slp) +{ + using bertini::node::Pi; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + const double tol = 1e-12; + auto E1 = [&](Nd f, double xv){ return EvalExpression(f, {{"x", dbl(xv,0)}}); }; + auto E2 = [&](Nd f, double xv, double yv){ return EvalExpression(f, {{"x", dbl(xv,0)}, {"y", dbl(yv,0)}}); }; + + BOOST_CHECK_SMALL(std::abs(E2(x + y + Integer::Make(3), 2, 5) - dbl(10)), tol); // Sum + BOOST_CHECK_SMALL(std::abs(E2(x - y - Integer::Make(1), 5, 2) - dbl(2)), tol); // Subtract + BOOST_CHECK_SMALL(std::abs(E2(x * y * Integer::Make(2), 3, 4) - dbl(24)), tol); // Multiply + BOOST_CHECK_SMALL(std::abs(E2(x / y, 6, 2) - dbl(3)), tol); // Divide + BOOST_CHECK_SMALL(std::abs(E1(-x, 3) - dbl(-3)), tol); // Negate + BOOST_CHECK_SMALL(std::abs(E1(pow(x, 3), 2) - dbl(8)), tol); // IntPower + BOOST_CHECK_SMALL(std::abs(E2(pow(x, y), 2, 3) - dbl(8)), tol); // Power (variable exponent) + BOOST_CHECK_SMALL(std::abs(E1(sqrt(x), 4) - dbl(2)), tol); // Sqrt + BOOST_CHECK_SMALL(std::abs(E1(exp(x), 0) - dbl(1)), tol); // Exp + BOOST_CHECK_SMALL(std::abs(E1(log(x), 1) - dbl(0)), tol); // Log + BOOST_CHECK_SMALL(std::abs(E1(sin(x), 0) - dbl(0)), tol); // Sin + BOOST_CHECK_SMALL(std::abs(E1(cos(x), 0) - dbl(1)), tol); // Cos + BOOST_CHECK_SMALL(std::abs(E1(tan(x), 0) - dbl(0)), tol); // Tan + BOOST_CHECK_SMALL(std::abs(E1(asin(x), 0) - dbl(0)), tol); // Asin + BOOST_CHECK_SMALL(std::abs(E1(acos(x), 1) - dbl(0)), tol); // Acos + BOOST_CHECK_SMALL(std::abs(E1(atan(x), 0) - dbl(0)), tol); // Atan + BOOST_CHECK_SMALL(std::abs(E1(Pi() * x, 1) - dbl(3.141592653589793, 0)), 1e-12); // Pi +} + +// Derivatives produced by Differentiate compile and evaluate correctly through the SLP -- the +// focused successor to the old differentiate-then-node-eval matrix. The point map is filtered to +// each (simplified) derivative's actual variables. +BOOST_AUTO_TEST_CASE(derivatives_compile_and_evaluate_through_the_slp) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + const double tol = 1e-12; + auto evald = [](Nd d, std::map known){ + std::map m; + for (auto const& v : bertini::node::GatherVariables(d)) m[v->name()] = known.at(v->name()); + return EvalExpression(d, m); + }; + std::map pt{ {"x", dbl(2,0)}, {"y", dbl(5,0)} }; + + BOOST_CHECK_SMALL(std::abs(evald((x*x)->Differentiate(x), pt) - dbl(4)), tol); // d/dx x^2 = 2x, x=2 -> 4 + BOOST_CHECK_SMALL(std::abs(evald((x*y)->Differentiate(x), pt) - dbl(5)), tol); // d/dx x*y = y, y=5 -> 5 + BOOST_CHECK_SMALL(std::abs(evald(pow(x,3)->Differentiate(x), pt)- dbl(12)), tol); // d/dx x^3 = 3x^2, x=2 -> 12 + BOOST_CHECK_SMALL(std::abs(evald(sin(x)->Differentiate(x), pt) - dbl(std::cos(2.0))), tol); // d/dx sin = cos + BOOST_CHECK_SMALL(std::abs(evald(exp(x)->Differentiate(x), pt) - dbl(std::exp(2.0))), tol); // d/dx exp = exp + BOOST_CHECK_SMALL(std::abs(evald((x/y)->Differentiate(x), pt) - dbl(0.2)), tol); // d/dx x/y = 1/y, y=5 -> 0.2 +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/eval_helper.hpp b/core/test/classes/eval_helper.hpp new file mode 100644 index 000000000..c7e61c9da --- /dev/null +++ b/core/test/classes/eval_helper.hpp @@ -0,0 +1,43 @@ +//This file is part of Bertini 2. +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file eval_helper.hpp + +\brief Test-only helper to evaluate a bare function-tree node at a point. + +Node-level evaluation has been removed (the straight-line program is the sole evaluator), so tests +that used to set values on nodes and call node->Eval() now evaluate through the SLP. EvalAt +compiles+runs the expression's program (via EvalExpression) at a point supplied as a name->value +map. Unlike EvalExpression, it ignores extra entries: callers pass one superset map and each +expression takes only the variables it actually contains, so a single point serves many sub-expressions. +*/ + +#pragma once + +#include +#include +#include + +#include "bertini2/system/eval_expression.hpp" +#include "bertini2/function_tree.hpp" + +namespace bertini { +namespace test { + +template +T EvalAt(std::shared_ptr const& n, std::map const& known = {}) +{ + std::map point; + for (auto const& v : node::GatherVariables(n)) + point[v->name()] = known.at(v->name()); + return EvalExpression(n, point); +} + +} // namespace test +} // namespace bertini diff --git a/core/test/classes/function_tree_test.cpp b/core/test/classes/function_tree_test.cpp deleted file mode 100755 index db7d4b42a..000000000 --- a/core/test/classes/function_tree_test.cpp +++ /dev/null @@ -1,2187 +0,0 @@ -//This file is part of Bertini 2. -// -//function_tree_test.cpp is free software: you can redistribute it and/or modify -//it under the terms of the GNU General Public License as published by -//the Free Software Foundation, either version 3 of the License, or -//(at your option) any later version. -// -//function_tree_test.cpp is distributed in the hope that it will be useful, -//but WITHOUT ANY WARRANTY; without even the implied warranty of -//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -//GNU General Public License for more details. -// -//You should have received a copy of the GNU General Public License -//along with function_tree_test.cpp. If not, see . -// -// Copyright(C) Bertini2 Development Team -// -// See for a copy of the license, -// as well as COPYING. Bertini2 is provided with permitted -// additional terms in the b2/licenses/ directory. - -// individual authors of this file include: -// silviana amethyst, university of wisconsin eau claire - -// Created by Collins, James B. on 4/30/15. -// Copyright (c) 2015 West Texas A&M University. All rights reserved. - -#include - -#include -#include - -#include "bertini2/function_tree.hpp" - - -#include -#include - -#include "externs.hpp" - - - -BOOST_AUTO_TEST_SUITE(function_tree_class) - - -using mpq_rational = bertini::mpq_rational; - -using Variable = bertini::node::Variable; -using Node = bertini::node::Node; -using Float = bertini::node::Float; - -using dbl = bertini::dbl; -using mpfr = bertini::mpfr_complex; - - - -template using Mat = bertini::Mat; - -using namespace bertini; - -/////////// Basic Operations Alone /////////////////// - -BOOST_AUTO_TEST_CASE(manual_construction_num_squared){ - using mpfr_float = bertini::mpfr_float; - - - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - dbl exact_dbl = anum_dbl*anum_dbl; - mpfr exact_mpfr{anum_mpfr*anum_mpfr}; - - std::shared_ptr N = a; - - N *= N; - BOOST_CHECK_EQUAL(N->Degree(),0); - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_squared){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - - dbl exact_dbl = xnum_dbl*xnum_dbl; - mpfr exact_mpfr{xnum_mpfr*xnum_mpfr}; - - std::shared_ptr N = x; - - N *= N; - - x->set_current_value(std::complex(xnum_dbl)); - x->set_current_value(bertini::mpfr_complex(xnum_mpfr)); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),2); - - -} - - - -BOOST_AUTO_TEST_CASE(default_constructed_variable_is_not_nan){ - - - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - - using std::isnan; - using bertini::isnan; - BOOST_CHECK(!isnan(x->Eval())); - BOOST_CHECK(!isnan(x->Eval())); -} - -BOOST_AUTO_TEST_CASE(default_constructed_variable_is_not_zero){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - - BOOST_CHECK(x->Eval()!=dbl(0,0)); - BOOST_CHECK(x->Eval()!=mpfr(0,0)); -} - - -BOOST_AUTO_TEST_CASE(default_constructed_variable_is_not_one){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - - BOOST_CHECK(x->Eval()!=dbl(1,0)); - BOOST_CHECK(x->Eval()!=mpfr(1,0)); -} - -BOOST_AUTO_TEST_CASE(self_multiplication){ - - auto rat_coeff = [](){return bertini::node::Rational::Make(bertini::node::Rational::Rand());}; - - std::shared_ptr v = rat_coeff(); - std::shared_ptr N = v*v; -} - - -BOOST_AUTO_TEST_CASE(rational_node_eval_sane_precision_random_rat){ - - DefaultPrecision(16); - - std::shared_ptr frac = bertini::node::Rational::Make(bertini::node::Rational::Rand()); - mpfr_complex result = frac->Eval(); - - BOOST_CHECK_EQUAL(Precision(result),16); -} - -BOOST_AUTO_TEST_CASE(rational_node_eval_sane_precision_one_half){ - - DefaultPrecision(16); - - std::shared_ptr frac = bertini::node::Rational::Make(mpq_rational(1,2)); - mpfr_complex result = frac->Eval(); - - BOOST_CHECK_EQUAL(Precision(result),16); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_sqrt_x){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - - dbl exact_dbl = sqrt(xnum_dbl); - mpfr exact_mpfr{sqrt(xnum_mpfr)}; - - std::shared_ptr N = pow(x, mpq_rational(1,2)); - - x->set_current_value(std::complex(xnum_dbl)); - x->set_current_value(bertini::mpfr_complex(xnum_mpfr)); - - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_plus_y_plus_number){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = xnum_dbl+ynum_dbl+anum_dbl; - mpfr exact_mpfr{xnum_mpfr+ynum_mpfr+anum_mpfr}; - - dbl temp_d; - mpfr temp_mp; - - std::shared_ptr N = x+y+a; - - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = a+x+y; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = y+a+x; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - - N = y+x+a; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - - N = x+a+y; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = a+y+x; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_minus_y_minus_number){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = xnum_dbl-ynum_dbl-anum_dbl; - mpfr exact_mpfr{xnum_mpfr-ynum_mpfr-anum_mpfr}; - - dbl temp_d; - mpfr temp_mp; - - std::shared_ptr N = x-y-a; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - N = x-a-y; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - - N = -a-y+x; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - - N = -a+x-y; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = -y-a+x; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - - N = -y+x-a; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_times_y_times_number){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = xnum_dbl*ynum_dbl*anum_dbl; - mpfr exact_mpfr{xnum_mpfr*ynum_mpfr*anum_mpfr}; - - - dbl temp_d; - mpfr temp_mp; - - std::shared_ptr N = x*y*a; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - N = a*x*y; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - N = y*a*x; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - - N = y*x*a; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - - N = x*a*y; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - - N = a*y*x; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_divide_y){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = xnum_dbl/ynum_dbl; - mpfr exact_mpfr{xnum_mpfr/ynum_mpfr}; - - std::shared_ptr N = x/y; - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - exact_dbl = ynum_dbl/xnum_dbl; - exact_mpfr = ynum_mpfr/xnum_mpfr; - - N = y/x; - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_negate_x){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = -xnum_dbl; - mpfr exact_mpfr{-xnum_mpfr}; - - std::shared_ptr N = -x; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - - - - - - -/////////// Basic Operations Combined /////////////////// - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_plus_y_plus_num1l_pow_num2){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = pow(xnum_dbl+ynum_dbl+anum_dbl,pnum_dbl); - mpfr exact_mpfr{pow(xnum_mpfr+ynum_mpfr+anum_mpfr,pnum_mpfr)}; - - dbl temp_d; - mpfr temp_mp; - - std::shared_ptr N = pow(x+y+a,p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_minus_y_minus_num1l_pow_num2){ - using mpfr_float = bertini::mpfr_float; - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = pow(xnum_dbl-ynum_dbl-anum_dbl,pnum_dbl); - mpfr exact_mpfr{pow(xnum_mpfr-ynum_mpfr-anum_mpfr,pnum_mpfr)}; - - std::shared_ptr N = pow(x-y-a,p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_times_y_times_num1l_pow_num2){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = pow(xnum_dbl*ynum_dbl*anum_dbl,pnum_dbl); - mpfr exact_mpfr{pow(xnum_mpfr*ynum_mpfr*anum_mpfr,pnum_mpfr)}; - - dbl temp_d; - mpfr temp_mp; - - std::shared_ptr N = pow(x*y*a,p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag()) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = pow(x,p)*pow(y,p)*pow(a,p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - exact_dbl = pow(xnum_dbl,pnum_dbl)*pow(ynum_dbl,pnum_dbl)*pow(anum_dbl,pnum_dbl); - exact_mpfr = pow(xnum_mpfr,pnum_mpfr)*pow(ynum_mpfr,pnum_mpfr)*pow(anum_mpfr,pnum_mpfr); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_over_yl_pow_num2){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = pow(xnum_dbl/ynum_dbl,pnum_dbl); - mpfr exact_mpfr{pow(xnum_mpfr/ynum_mpfr,pnum_mpfr)}; - - std::shared_ptr N = pow(x/y,p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = pow(x,p)/pow(y,p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lnegative_xl_pow_num2){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(-xnum_dbl,pnum_dbl); - mpfr exact_mpfr{pow(-xnum_mpfr,pnum_mpfr)}; - - std::shared_ptr N = -x; - N = pow(N,p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_negate_x_plus_y_plus_num1){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = -(xnum_dbl+ynum_dbl+anum_dbl); - mpfr exact_mpfr{-(xnum_mpfr+ynum_mpfr+anum_mpfr)}; - - std::shared_ptr N = -(x+y+a); - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = -x-y-a; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_negate_x_minus_y_minus_num1){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = -(xnum_dbl-ynum_dbl-anum_dbl); - mpfr exact_mpfr{-(xnum_mpfr-ynum_mpfr-anum_mpfr)}; - - std::shared_ptr N = -(x-y-a); - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = -x+y+a; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - - -BOOST_AUTO_TEST_CASE(manual_construction_negate_x_times_y_times_num1){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = -(xnum_dbl*ynum_dbl*anum_dbl); - mpfr exact_mpfr{-(xnum_mpfr*ynum_mpfr*anum_mpfr)}; - - std::shared_ptr N = -(x*y*a); - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = (-x)*y*a; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = x*(-y)*a; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = x*y*(-a); - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - - -BOOST_AUTO_TEST_CASE(manual_construction_negate_x_over_y){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = -(xnum_dbl/ynum_dbl); - mpfr exact_mpfr{-(xnum_mpfr/ynum_mpfr)}; - - std::shared_ptr N = -(x/y); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - - N = -x/y; - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - - N = x/(-y); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - ; - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_negate_x_pow_num2){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = -pow(xnum_dbl,pnum_dbl); - mpfr exact_mpfr{-pow(xnum_mpfr,pnum_mpfr)}; - - std::shared_ptr N = pow(x,p); - N = -N; - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - - - - - - -/////////// Order of Operations /////////////////// - -BOOST_AUTO_TEST_CASE(manual_construction_x_times_y_over_num){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = xnum_dbl*ynum_dbl/anum_dbl; - mpfr exact_mpfr{xnum_mpfr*ynum_mpfr/anum_mpfr}; - - std::shared_ptr N = x*y/a; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - - - N = (x*y)/a; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = x/a*y; - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK_SMALL(N->Eval().real()/exact_dbl.real()-1.0, threshold_clearance_d); - BOOST_CHECK_SMALL(N->Eval().imag()/exact_dbl.imag()-1.0, threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_plus_num1l_times_ly_plus_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr b = Float::Make(bstr_real, bstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = (xnum_dbl+anum_dbl)*(ynum_dbl+bnum_dbl); - mpfr exact_mpfr{(xnum_mpfr+anum_mpfr)*(ynum_mpfr+bnum_mpfr)}; - - std::shared_ptr N = (x+a)*(y+b); - BOOST_CHECK_EQUAL(N->Degree(),2); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_plus_num1_times_y_plus_num2){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr b = Float::Make(bstr_real, bstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = xnum_dbl+anum_dbl*ynum_dbl+bnum_dbl; - mpfr exact_mpfr{xnum_mpfr+anum_mpfr*ynum_mpfr+bnum_mpfr}; - - std::shared_ptr N = x+a*y+b; - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_plus_num1l_over_ly_plus_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr b = Float::Make(bstr_real, bstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = (xnum_dbl+anum_dbl)/(ynum_dbl+bnum_dbl); - mpfr exact_mpfr{(xnum_mpfr+anum_mpfr)/(ynum_mpfr+bnum_mpfr)}; - - std::shared_ptr N = (x+a)/(y+b); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_plus_num1_over_y_plus_num2){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr b = Float::Make(bstr_real, bstr_imag); - - x->set_current_value(xnum_dbl); - y->set_current_value(ynum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - y->set_current_value(bertini::mpfr_complex(ystr_real,ystr_imag)); - - dbl exact_dbl = xnum_dbl+anum_dbl/ynum_dbl+bnum_dbl; - mpfr exact_mpfr{xnum_mpfr+anum_mpfr/ynum_mpfr+bnum_mpfr}; - - std::shared_ptr N = x+a/y+b; - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - BOOST_CHECK_EQUAL(N->Degree(y),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_pow_num2l_plus_num1){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(xnum_dbl,pnum_dbl)+anum_dbl; - mpfr exact_mpfr{pow(xnum_mpfr,pnum_mpfr)+anum_mpfr}; - - std::shared_ptr N = pow(x,p)+a; - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_plus_lnum1_pow_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(anum_dbl,pnum_dbl)+xnum_dbl; - mpfr exact_mpfr{pow(anum_mpfr,pnum_mpfr)+xnum_mpfr}; - - std::shared_ptr N = x+pow(a,p); - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_times_lnum1_pow_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(anum_dbl,pnum_dbl)*xnum_dbl; - mpfr exact_mpfr{pow(anum_mpfr,pnum_mpfr)*xnum_mpfr}; - - std::shared_ptr N = x*pow(a,p); - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_pow_num2l_times_num1){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(xnum_dbl,pnum_dbl)*anum_dbl; - mpfr exact_mpfr{pow(xnum_mpfr,pnum_mpfr)*anum_mpfr}; - - std::shared_ptr N = pow(x,p)*a; - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_lx_pow_num2l_over_num1){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(xnum_dbl,pnum_dbl)/anum_dbl; - mpfr exact_mpfr{pow(xnum_mpfr,pnum_mpfr)/anum_mpfr}; - - std::shared_ptr N = pow(x,p)/a; - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(! N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_pow_lsqrt_xl_num) -{ - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - auto exact_dbl = pow(sqrt(xnum_dbl),anum_dbl); - bertini::mpfr_complex exact_mpfr = pow(sqrt(xnum_mpfr),anum_mpfr); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(xnum_mpfr); - - std::shared_ptr N = pow(sqrt(x),a); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = pow(x,mpq_rational(1,2)); // N = sqrt(x) - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - N = pow(N,a); // N = sqrt(x)^a - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK_SMALL(N->Eval().real() / exact_dbl.real() -1, threshold_clearance_d); - BOOST_CHECK_SMALL(N->Eval().imag() - exact_dbl.imag(), 10*threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_over_lnum1_pow_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = xnum_dbl/pow(anum_dbl,pnum_dbl); - mpfr exact_mpfr{xnum_mpfr/pow(anum_mpfr,pnum_mpfr)}; - - std::shared_ptr N = x/pow(a,p); - BOOST_CHECK_EQUAL(N->Degree(),1); - BOOST_CHECK_EQUAL(N->Degree(x),1); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_pow_lnum1_plus_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(xnum_dbl,anum_dbl+pnum_dbl); - mpfr exact_mpfr{pow(xnum_mpfr,anum_mpfr+pnum_mpfr)}; - - std::shared_ptr N = pow(x,a+p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_pow_lnum1_times_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(xnum_dbl,anum_dbl*pnum_dbl); - mpfr exact_mpfr{pow(xnum_mpfr,anum_mpfr*pnum_mpfr)}; - - std::shared_ptr N = pow(x,a*p); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - // the correct values for this test, from Matlab with VPA and digits 100, is: - // x = vpa('3.1')+1i*vpa('4.1') - // (x)^(((vpa('3.4')+1i*vpa('5.6'))*(vpa('0.8')+1i*vpa('-1.7')))) - // - 1621052733.527456564742398610927236083674601272970056970095361659563831586052755151849639759025786702 + - // 414768162.6460206267766585173685553695858892142182608105258574256927813150731719446202973174663762327i - BOOST_CHECK_CLOSE(N->Eval().real(), exact_dbl.real(),100*threshold_clearance_d); // made these bigger because optimization changes the values ever so slightly. - BOOST_CHECK_CLOSE(N->Eval().imag(), exact_dbl.imag(),100*threshold_clearance_d); // - BOOST_CHECK_CLOSE(N->Eval().real(), exact_mpfr.real(),threshold_clearance_mp); - BOOST_CHECK_CLOSE(N->Eval().imag(), exact_mpfr.imag(),threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_x_pow_lnum1_over_num2l){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - std::shared_ptr p = Float::Make(pstr_real, pstr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = pow(xnum_dbl,pnum_dbl/anum_dbl); - mpfr exact_mpfr{pow(xnum_mpfr,pnum_mpfr/anum_mpfr)}; - - std::shared_ptr N = pow(x,p/a); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - - - - - - - - -///////////// Special Functions /////////////////// -BOOST_AUTO_TEST_CASE(manual_construction_sin_num){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - dbl exact_dbl = sin(anum_dbl); - mpfr exact_mpfr{sin(anum_mpfr)}; - - dbl temp_d; - mpfr temp_mp; - - std::shared_ptr N = sin(a); - - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_cos_num){ - using mpfr_float = bertini::mpfr_float; - - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - dbl exact_dbl = cos(anum_dbl); - mpfr exact_mpfr{cos(anum_mpfr)}; - - std::shared_ptr N = cos(a); - - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_tan_num){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - dbl exact_dbl = tan(anum_dbl); - mpfr exact_mpfr{tan(anum_mpfr)}; - - std::shared_ptr N = tan(a); - - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_exp_num){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - dbl exact_dbl = exp(anum_dbl); - mpfr exact_mpfr{exp(anum_mpfr)}; - - dbl temp_d; - mpfr temp_mp; - - std::shared_ptr N = exp(a); - - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N->EvalInPlace(temp_d); - N->EvalInPlace(temp_mp); - BOOST_CHECK(fabs(temp_d.real() / exact_dbl.real() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_d.imag() / exact_dbl.imag() -1) < threshold_clearance_d); - BOOST_CHECK(fabs(temp_mp.real() / exact_mpfr.real() -1) < threshold_clearance_mp); - BOOST_CHECK(fabs(temp_mp.imag() / exact_mpfr.imag() -1) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(manual_construction_sqrt_num){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - dbl exact_dbl = sqrt(anum_dbl); - mpfr exact_mpfr{sqrt(anum_mpfr)}; - - std::shared_ptr N = sqrt(a); - - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_sin_of_lx_plus_numl){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = sin(xnum_dbl+anum_dbl); - mpfr exact_mpfr{sin(xnum_mpfr+anum_mpfr)}; - - std::shared_ptr N = sin(x+a); - - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_cos_of_lx_times_numl){ - using mpfr_float = bertini::mpfr_float; - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = cos(xnum_dbl*anum_dbl); - mpfr exact_mpfr{cos(xnum_mpfr*anum_mpfr)}; - - std::shared_ptr N = cos(x*a); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_tan_of_lx_over_numl){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = tan(xnum_dbl/anum_dbl); - mpfr exact_mpfr{tan(xnum_mpfr/anum_mpfr)}; - - std::shared_ptr N = tan(x/a); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_exp_of_negative_num){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - dbl exact_dbl = exp(-anum_dbl); - mpfr exact_mpfr{exp(-anum_mpfr)}; - - std::shared_ptr N = exp(-a); - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_sqrt_of_lx_pow_numl){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr a = Float::Make(astr_real, astr_imag); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = sqrt(pow(xnum_dbl,anum_dbl)); - mpfr exact_mpfr{sqrt(pow(xnum_mpfr,anum_mpfr))}; - - std::shared_ptr N = sqrt(pow(x,a)); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(!N->IsPolynomial()); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - N = pow(x,a); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - N = pow(N,mpq_rational(1,2)); - BOOST_CHECK_EQUAL(N->Degree(),-1); - BOOST_CHECK_EQUAL(N->Degree(x),-1); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); -} - - - - - - -BOOST_AUTO_TEST_CASE(arcsine_evaluate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = asin(pow(x,2)+1); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = asin(pow(xnum_dbl,2)+1.0); - mpfr exact_mpfr{asin(pow(xnum_mpfr,2)+bertini::mpfr_complex(1.0))}; - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - BOOST_CHECK(!N->IsPolynomial()); -} - - -BOOST_AUTO_TEST_CASE(arccosine_evaluate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = acos(pow(x,2)+1); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = acos(pow(xnum_dbl,2)+1.0); - mpfr exact_mpfr{acos(pow(xnum_mpfr,2)+bertini::mpfr_complex(1.0))}; - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - BOOST_CHECK(!N->IsPolynomial()); -} - -BOOST_AUTO_TEST_CASE(arctangent_evaluate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = atan(pow(x,2)+1); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = atan(pow(xnum_dbl,2)+1.0); - mpfr exact_mpfr{atan(pow(xnum_mpfr,2)+bertini::mpfr_complex(1.0))}; - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - BOOST_CHECK(!N->IsPolynomial()); -} - - -BOOST_AUTO_TEST_CASE(log_evaluate) -{ - std::shared_ptr x = Variable::Make("x"); - auto N = log(pow(x,2)+1); - - x->set_current_value(xnum_dbl); - x->set_current_value(bertini::mpfr_complex(xstr_real,xstr_imag)); - - dbl exact_dbl = log(pow(xnum_dbl,2)+1.0); - mpfr exact_mpfr{log(pow(xnum_mpfr,2)+bertini::mpfr_complex(1.0))}; - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().imag() - exact_dbl.imag() ) < threshold_clearance_d); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(fabs(N->Eval().imag() - exact_mpfr.imag() ) < threshold_clearance_mp); - - BOOST_CHECK(!N->IsPolynomial()); -} - - -///////////// Special Numbers /////////////////// -BOOST_AUTO_TEST_CASE(manual_construction_pi){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - using std::atan; - dbl exact_dbl(4*atan(1.0),0); - mpfr exact_mpfr{mpfr_float("4.0")*atan(mpfr_float("1.0"))}; - - auto N = bertini::node::Pi(); - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK_EQUAL(N->Eval().imag(),0.0); - - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK_EQUAL(N->Eval().imag(),0.0); - - BOOST_CHECK(N->IsPolynomial()); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_e){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - dbl exact_dbl(exp(1.0),0); - mpfr exact_mpfr{exp(mpfr_float("1.0"))}; - - auto N = bertini::node::E(); - BOOST_CHECK_EQUAL(N->Degree(),0); - BOOST_CHECK(fabs(N->Eval().real() - exact_dbl.real() ) < threshold_clearance_d); - BOOST_CHECK(N->Eval().imag() == 0.0); - BOOST_CHECK(fabs(N->Eval().real() - exact_mpfr.real() ) < threshold_clearance_mp); - BOOST_CHECK(N->Eval().imag() - exact_mpfr.imag() == 0.0); -} - - -BOOST_AUTO_TEST_CASE(manual_construction_i){ - using mpfr_float = bertini::mpfr_float; - bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); - - dbl exact_dbl(0.0,1.0); - mpfr exact_mpfr{mpfr("0.0","1.0")}; - - auto N = bertini::node::I(); - BOOST_CHECK_EQUAL(N->Degree(),0); - - BOOST_CHECK(N->IsPolynomial()); - - BOOST_CHECK(N->Eval().real() == 0.0); - BOOST_CHECK(N->Eval().imag() == 1.0); - BOOST_CHECK(N->Eval().real() == 0.0); - BOOST_CHECK(N->Eval().imag() == 1.0); -} - - - - - -BOOST_AUTO_TEST_CASE(function_tree_combine_product_of_two_integer_powers) -{ - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr N,M,P; - - N = pow(x,5); - M = pow(x,2); - - P = N*M; - BOOST_CHECK_EQUAL(P->Degree(), 7); - - BOOST_CHECK(std::dynamic_pointer_cast(P)); - - P = N/M; - BOOST_CHECK(!std::dynamic_pointer_cast(P)); - BOOST_CHECK_EQUAL(P->Degree(), -1); - - N = pow(x,3); - M = pow(x,-1); - - P = N/M; - BOOST_CHECK(!std::dynamic_pointer_cast(P)); - BOOST_CHECK_EQUAL(P->Degree(), -1); -} - -BOOST_AUTO_TEST_CASE(long_arithmetic_chain) -{ - - std::vector> polytypes(9); - - auto rat_coeff = [](){return bertini::node::Rational::Make(bertini::node::Rational::Rand());}; - for (unsigned int ii=0; ii<9; ++ii) - polytypes[ii] = rat_coeff(); - - std::shared_ptr v = rat_coeff(); - std::shared_ptr m12_2 = rat_coeff(); - std::shared_ptr cosb = rat_coeff(); - std::shared_ptr sinb = rat_coeff(); - std::shared_ptr sin2b = rat_coeff(); - - std::shared_ptr N = 2*m12_2/sin2b - v*v * polytypes[9-1] / 16; - -} - -BOOST_AUTO_TEST_CASE(make_linear_product) -{ - using namespace bertini::node; - - bertini::VariableGroup vargp; - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr z = Variable::Make("z"); - std::shared_ptr U = Variable::Make("U"); - vargp.push_back(x); - vargp.push_back(y); - vargp.push_back(z); - - // Make with automatically genearted coefficients - std::shared_ptr linprod = LinearProduct::Make(vargp,4); - - // Make with user defined coefficients - Mat coeff_mpfr(3,4); - for(int ii = 0; ii < 3; ++ii) - { - for(int jj = 0; jj < 4; ++jj) - { - coeff_mpfr(ii,jj) = mpfr(ii+1, jj+1); - } - } - - std::shared_ptr linprod2 = LinearProduct::Make(vargp, coeff_mpfr); - -} - - - -BOOST_AUTO_TEST_CASE(eval_linear_product) -{ - using namespace bertini::node; - - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - auto z = Variable::Make("z"); - auto w = Variable::Make("w"); - - - - VariableGroup v0{x,z,y}; - VariableGroup v1{w}; - Mat coeff_dbl(3,4); - Mat coeff_mpfr(3,4); - - for(int ii = 0; ii < 3; ++ii) - { - for(int jj = 0; jj < 4; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+1, jj+1); - coeff_mpfr(ii,jj) = mpfr(ii+1, jj+1); - } - } - - std::shared_ptr linprod1 = bertini::node::LinearProduct::Make(v0, coeff_mpfr); - - coeff_dbl = Mat(1,2); - coeff_mpfr = Mat(1,2); - - for(int ii = 0; ii < 1; ++ii) - { - for(int jj = 0; jj < 2; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+3, jj+3); - coeff_mpfr(ii,jj) = mpfr(ii+3, jj+3); - } - } - - std::shared_ptr linprod2 = bertini::node::LinearProduct::Make(v1, coeff_mpfr); - - - - std::shared_ptr linprod_node = (mpfr(1,1)*x + mpfr(1,2)*z + mpfr(1,3)*y+ mpfr(1,4)) * (mpfr(2,1)*x + mpfr(2,2)*z + mpfr(2,3)*y+ mpfr(2,4))*(mpfr(3,1)*x + mpfr(3,2)*z + mpfr(3,3)*y+ mpfr(3,4))* - (mpfr(3,3)*w + mpfr(3,4)); - std::shared_ptr linprod = linprod1*linprod2; - - dbl xval_d = dbl(.5,1); - dbl yval_d = dbl(.6,1); - dbl zval_d = dbl(.7,1); - dbl wval_d = dbl(2.1, -.03); - mpfr xval_mp = mpfr(".5", "1"); - mpfr yval_mp = mpfr(".6", "1"); - mpfr zval_mp = mpfr(".7", "1"); - mpfr wval_mp = mpfr(".22", "1.11"); - - v0[0]->set_current_value(xval_d); - v0[1]->set_current_value(zval_d); - v0[2]->set_current_value(yval_d); - v0[0]->set_current_value(xval_mp); - v0[1]->set_current_value(zval_mp); - v0[2]->set_current_value(yval_mp); - - v1[0]->set_current_value(wval_d); - v1[0]->set_current_value(wval_mp); - - - dbl eval_d = linprod->Eval(); - dbl exact_d = linprod_node->Eval(); - mpfr eval_mp = linprod->Eval(); - mpfr exact_mp = linprod_node->Eval(); - - - BOOST_CHECK(fabs(eval_d.real()/exact_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_d.imag()/exact_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_mp.real()/exact_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(eval_mp.imag()/exact_mp.imag() - 1) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_CASE(linear_product_degree) -{ - using namespace bertini::node; - - bertini::VariableGroup vargp, vargp2, vargp3; - std::shared_ptr x = Variable::Make("x"); - std::shared_ptr y = Variable::Make("y"); - std::shared_ptr z = Variable::Make("z"); - std::shared_ptr w = Variable::Make("w"); - vargp.push_back(x); - vargp.push_back(y); - vargp.push_back(z); - - vargp2.push_back(x); - vargp2.push_back(z); - vargp2.push_back(w); - - vargp3.push_back(w); - - - - // Make with automatically genearted coefficients - std::shared_ptr linprod = LinearProduct::Make(vargp,4); - - BOOST_CHECK_EQUAL(linprod->Degree(x), 4); - BOOST_CHECK_EQUAL(linprod->Degree(y), 4); - BOOST_CHECK_EQUAL(linprod->Degree(z), 4); - BOOST_CHECK_EQUAL(linprod->Degree(w), 0); - - BOOST_CHECK_EQUAL(linprod->Degree(vargp), 4); - BOOST_CHECK_EQUAL(linprod->Degree(vargp2), 4); - BOOST_CHECK_EQUAL(linprod->Degree(vargp3), 0); - - BOOST_CHECK_EQUAL(linprod->MultiDegree(vargp)[0], 4); - BOOST_CHECK_EQUAL(linprod->MultiDegree(vargp)[1], 4); - BOOST_CHECK_EQUAL(linprod->MultiDegree(vargp)[2], 4); - BOOST_CHECK_EQUAL(linprod->MultiDegree(vargp2)[0], 4); - BOOST_CHECK_EQUAL(linprod->MultiDegree(vargp2)[2], 0); - BOOST_CHECK_EQUAL(linprod->MultiDegree(vargp3)[0], 0); - -} - - - - -BOOST_AUTO_TEST_CASE(linear_prod_get_linears) -{ - using namespace bertini::node; - - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - auto z = Variable::Make("z"); - auto w = Variable::Make("w"); - - int num_vars = 3; - - - - VariableGroup v0{x,z,y}; - VariableGroup v1{w}; - Mat coeff_dbl(3,4); - Mat coeff_mpfr(3,4); - - for(int ii = 0; ii < 3; ++ii) - { - for(int jj = 0; jj < 4; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+1, jj+1); - coeff_mpfr(ii,jj) = mpfr(ii+1, jj+1); - } - } - - std::shared_ptr linprod = bertini::node::LinearProduct::Make(v0, coeff_mpfr); - - - - std::shared_ptr linprod_node1 = (mpfr(1,1)*x + mpfr(1,2)*z + mpfr(1,3)*y+ mpfr(1,4)); - std::shared_ptr linprod_node2 = (mpfr(2,1)*x + mpfr(2,2)*z + mpfr(2,3)*y+ mpfr(2,4)); - std::shared_ptr linprod_node3 = (mpfr(3,1)*x + mpfr(3,2)*z + mpfr(3,3)*y+ mpfr(3,4)); - std::shared_ptr linprod_node12 = linprod_node1 * linprod_node3; - std::shared_ptr linprod_node = linprod_node1 * linprod_node2 * linprod_node3; - - - dbl xval_d = dbl(.5,1); - dbl yval_d = dbl(.6,1); - dbl zval_d = dbl(.7,1); - dbl wval_d = dbl(2.1, -.03); - mpfr xval_mp = mpfr(".5", "1"); - mpfr yval_mp = mpfr(".6", "1"); - mpfr zval_mp = mpfr(".7", "1"); - mpfr wval_mp = mpfr(".22", "1.11"); - - v0[0]->set_current_value(xval_d); - v0[1]->set_current_value(zval_d); - v0[2]->set_current_value(yval_d); - v0[0]->set_current_value(xval_mp); - v0[1]->set_current_value(zval_mp); - v0[2]->set_current_value(yval_mp); - - v1[0]->set_current_value(wval_d); - v1[0]->set_current_value(wval_mp); - - bertini::Vec coeff_d = linprod->GetCoeffs(1); - bertini::Vec coeff_mp = linprod->GetCoeffs(2); - - - - for(int ii = 0; ii < num_vars+1; ++ii) - { - BOOST_CHECK(fabs(coeff_d[ii].real()/coeff_dbl(1,ii).real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(coeff_d[ii].imag()/coeff_dbl(1,ii).imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(coeff_mp[ii].real()/coeff_mpfr(2,ii).real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(coeff_mp[ii].imag()/coeff_mpfr(2,ii).imag() - 1) < threshold_clearance_mp); - } - - std::vector ind{0,2}; - -// std::shared_ptr linprod12 = linprod->GetLinears(ind); -// std::shared_ptr linprod2 = linprod->GetLinears(1); -// std::shared_ptr linprod3 = linprod->GetLinears(2); -// -// -// dbl evalx_d = linprod12->Eval(); -// dbl exactx_d = linprod_node12->Eval(); -// mpfr evalx_mp = linprod12->Eval(); -// mpfr exactx_mp = linprod_node12->Eval(); -// -// BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); -// BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); -// BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); -// BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); -// -// evalx_d = linprod2->Eval(); -// exactx_d = linprod_node2->Eval(); -// evalx_mp = linprod2->Eval(); -// exactx_mp = linprod_node2->Eval(); -// -// BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); -// BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); -// BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); -// BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); -// -// evalx_d = linprod3->Eval(); -// exactx_d = linprod_node3->Eval(); -// evalx_mp = linprod3->Eval(); -// exactx_mp = linprod_node3->Eval(); -// -// BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); -// BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); -// BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); -// BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - -} - - -BOOST_AUTO_TEST_SUITE_END() - - - - - - diff --git a/core/test/classes/function_tree_transform.cpp b/core/test/classes/function_tree_transform.cpp index 8a94d00f6..31883a7da 100644 --- a/core/test/classes/function_tree_transform.cpp +++ b/core/test/classes/function_tree_transform.cpp @@ -27,6 +27,7 @@ */ #include +#include #include #include @@ -38,6 +39,9 @@ #include #include "externs.hpp" +#include "eval_helper.hpp" + +using bertini::test::EvalAt; using Nd = std::shared_ptr; @@ -53,6 +57,9 @@ using dbl = bertini::dbl; auto MakeZero(){return Nd(Integer::Make(0));} auto MakeOne(){return Nd(Integer::Make(1));} +// the printed form of a simplified expression +inline std::string SimplifiedForm(Nd const& n){ std::ostringstream o; bertini::Simplify(n)->print(o); return o.str(); } + BOOST_AUTO_TEST_SUITE(function_tree) BOOST_AUTO_TEST_SUITE(transform) @@ -62,682 +69,166 @@ BOOST_AUTO_TEST_SUITE(transform) // | (_) (_) | | // ___| |_ _ __ ___ _ _ __ __ _| |_ ___ _______ _ __ ___ ___ // / _ \ | | '_ ` _ \| | '_ \ / _` | __/ _ \ |_ / _ \ '__/ _ \/ __| -// | __/ | | | | | | | | | | | (_| | || __/ / / __/ | | (_) \__ \ +// | __/ | | | | | | | | | | | (_| | || __/ / / __/ | | (_) \__ \. // \___|_|_|_| |_| |_|_|_| |_|\__,_|\__\___| /___\___|_| \___/|___/ -BOOST_AUTO_TEST_SUITE(eliminate_zeros) - - -// ______ -// \ ___) -// \ \ -// > > -// / /__ -// /_____) - +BOOST_AUTO_TEST_SUITE(simplified) - -BOOST_AUTO_TEST_SUITE(sum) +// Node::Simplified() is the non-mutating successor to the old in-place +// EliminateZeros / EliminateOnes / ReduceDepth machinery: it returns a NEW simplified +// tree and never touches its input. Only *literal* zeros/ones are folded (per ADR-0011); +// an expression that merely happens to evaluate to zero is deliberately left alone. -BOOST_AUTO_TEST_CASE(cant_eliminate_self) +// ---- literal zeros vanish / collapse ---- +BOOST_AUTO_TEST_CASE(leaf_zero_is_unchanged) { auto zero = MakeZero(); - auto num_eliminated = zero->EliminateZeros(); - BOOST_CHECK_EQUAL(num_eliminated, 0); - BOOST_CHECK_EQUAL(zero->Eval(), 0.); + BOOST_CHECK_EQUAL(EvalAt(zero->Simplified()), 0.); } -BOOST_AUTO_TEST_CASE(cant_eliminate_self2) -{ - auto zero = MakeZero(); - auto n = zero; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK_EQUAL(num_eliminated, 0); - BOOST_CHECK_EQUAL(n->Eval(), 0.); -} - - - -BOOST_AUTO_TEST_CASE(level_one_signs_preserved) +BOOST_AUTO_TEST_CASE(zeros_drop_from_sum_signs_preserved) { auto zero = MakeZero(); auto n = 2 + zero - 1 + zero - 2; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated > 0); - BOOST_CHECK_EQUAL(n->Eval(), -1.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(n)), -1.); } - -BOOST_AUTO_TEST_CASE(level_one) +BOOST_AUTO_TEST_CASE(sums_of_zeros_are_zero) { auto zero = MakeZero(); - auto n = zero+zero; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 0.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(zero+zero)), 0.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(zero+0)), 0.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(0+zero)), 0.); } +// ---- like factors combine into powers, like terms into coefficients ---- +// Identical subexpressions are one interned node, so "structurally equal" is pointer identity: +// grouping factors/terms is a hash on the operand pointer. -BOOST_AUTO_TEST_CASE(level_one2) +BOOST_AUTO_TEST_CASE(like_factors_combine_into_powers) { - auto zero = MakeZero(); - auto n = zero+0; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 0.); + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + BOOST_CHECK_EQUAL(SimplifiedForm(x*x), "x^2"); + BOOST_CHECK_EQUAL(SimplifiedForm(x*x*x), "x^3"); + BOOST_CHECK_EQUAL(SimplifiedForm(pow(x,2)*pow(x,3)), "x^5"); // x^a * x^b -> x^(a+b) + BOOST_CHECK_EQUAL(SimplifiedForm((x+y)*(x+y)), "(x+y)^2"); // any repeated base, not just vars } - -BOOST_AUTO_TEST_CASE(level_one3) +BOOST_AUTO_TEST_CASE(divided_like_factors_lower_the_exponent) { - auto zero = MakeZero(); - auto n = 0+zero; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 0.); + auto x = Variable::Make("x"); + BOOST_CHECK_EQUAL(SimplifiedForm(x*x/x), "x"); // x^2 / x -> x + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(x/x)), 1.); // x / x -> 1 } - - - - - -BOOST_AUTO_TEST_CASE(level_one_more_complicated) +BOOST_AUTO_TEST_CASE(like_terms_combine_into_coefficients) { auto x = Variable::Make("x"); - auto zero = MakeZero(); - - - auto n = pow(x,2) + zero*2; - - x->set_current_value(dbl(2.0)); - - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 4.); + auto y = Variable::Make("y"); + BOOST_CHECK_EQUAL(SimplifiedForm(x+x), "2*x"); + BOOST_CHECK_EQUAL(SimplifiedForm(x+x+x), "3*x"); + BOOST_CHECK_EQUAL(SimplifiedForm(Integer::Make(3)*x + Integer::Make(2)*x), "5*x"); + BOOST_CHECK_EQUAL(SimplifiedForm(x*y + x*y), "2*x*y"); } - - -BOOST_AUTO_TEST_CASE(level_one_more_complicated2) +BOOST_AUTO_TEST_CASE(opposite_like_terms_cancel) { auto x = Variable::Make("x"); - auto zero = MakeZero(); - - - auto n = pow(x,2) + x*zero*2*(x*x*x); - - x->set_current_value(dbl(2.0)); - - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 4.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(x - x)), 0.); + BOOST_CHECK_EQUAL(SimplifiedForm(Integer::Make(3)*x - x), "2*x"); } -BOOST_AUTO_TEST_CASE(level_two) +BOOST_AUTO_TEST_CASE(combining_preserves_value) { auto x = Variable::Make("x"); - auto zero = MakeZero(); - - - auto n = (x + sqrt(x) + zero) + x*2*(x*x*x); - - x->set_current_value(dbl(2.0)); - - auto num_eliminated = n->EliminateZeros(); - - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 35.414213562373095048801688724209698078569671875377); + Nd f = x*x*x + x*x*x; // 2*x^3 = 16 at x=2 + auto s = bertini::Simplify(f); + BOOST_CHECK_EQUAL(EvalAt(s, {{"x", dbl(2.0, 0.0)}}), dbl(16.0, 0.0)); } -BOOST_AUTO_TEST_CASE(level_two_variable_set_to_zero_eliminated) +BOOST_AUTO_TEST_CASE(zero_terms_drop_keeping_value) { auto x = Variable::Make("x"); auto zero = MakeZero(); - - - auto n = (x + sqrt(x) + zero) + x*2*(x*x*x); - - auto as_op = std::dynamic_pointer_cast(n); - - - x->set_current_value(dbl(0)); - - auto num_eliminated = n->EliminateZeros(); - - x->set_current_value(dbl(2.0)); - - BOOST_CHECK_EQUAL(as_op->NumOperands(), 1); - BOOST_CHECK_EQUAL(n->Eval(), 0.0); + std::map pt{ {"x", dbl(2.0)} }; + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(pow(x,2) + zero*2), pt), 4.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(pow(x,2) + x*zero*2*(x*x*x)), pt), 4.); } - -BOOST_AUTO_TEST_SUITE_END() // eliminate zeros :: sum - - - -// _______ -// ( _ ) -// | | | | -// | | | | -// | | | | -// |_| |_| - - -BOOST_AUTO_TEST_SUITE(product) - - -BOOST_AUTO_TEST_CASE(level_one) +BOOST_AUTO_TEST_CASE(zero_term_drops_in_deeper_sum) { + auto x = Variable::Make("x"); auto zero = MakeZero(); - auto n = 1*zero; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 0.); + auto n = (x + sqrt(x) + zero) + x*2*(x*x*x); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(n), {{"x", dbl(2.0)}}), + 35.414213562373095048801688724209698078569671875377); } - -BOOST_AUTO_TEST_CASE(level_one2) +BOOST_AUTO_TEST_CASE(literal_zero_collapses_product) { auto zero = MakeZero(); - auto n = 1*zero; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 0.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(1*zero)), 0.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(zero*zero)), 0.); } -BOOST_AUTO_TEST_CASE(level_one3) -{ - auto zero = MakeZero(); - auto n = zero*zero; - auto num_eliminated = n->EliminateZeros(); - BOOST_CHECK(num_eliminated >= 1); - BOOST_CHECK_EQUAL(n->Eval(), 0.); -} - -BOOST_AUTO_TEST_SUITE_END() // eliminate zeros :: product - - -BOOST_AUTO_TEST_SUITE_END() // eliminate zeros - - - - - - - -// _ _ _ -// | | (_) (_) _ -// _____ | | _ ____ _ ____ _____ _| |_ _____ ___ ____ _____ ___ -// | ___ || | | || \ | || _ \ (____ |(_ _)| ___ | / _ \ | _ \ | ___ | /___) -// | ____|| | | || | | || || | | |/ ___ | | |_ | ____| | |_| || | | || ____||___ | -// |_____) \_)|_||_|_|_||_||_| |_|\_____| \__)|_____) \___/ |_| |_||_____)(___/ - - - - -BOOST_AUTO_TEST_SUITE(eliminate_ones) - - -// _ -// ( ) -// _ ___ __ ___ ______ _ _ _ __ _ _| | -// /'_`\ /' _ `\ /'__`\/',__) (______) ( '_`\ ( '__) /'_`\ /'_` | -// ( (_) )| ( ) |( ___/\__, \ | (_) )| | ( (_) )( (_| | -// `\___/'(_) (_)`\____)(____/ | ,__/'(_) `\___/'`\__,_) -// | | -// (_) - - -BOOST_AUTO_TEST_SUITE(product) - -BOOST_AUTO_TEST_CASE(cant_eliminate_self) +// ---- literal ones drop from products ---- +BOOST_AUTO_TEST_CASE(leaf_one_is_unchanged) { auto one = MakeOne(); - auto num_eliminated = one->EliminateOnes(); - BOOST_CHECK_EQUAL(num_eliminated, 0); - BOOST_CHECK_EQUAL(one->Eval(), 1.); + BOOST_CHECK_EQUAL(EvalAt(one->Simplified()), 1.); } -BOOST_AUTO_TEST_CASE(cant_eliminate_self2) -{ - auto one = MakeOne(); - auto n = one; - auto num_eliminated = n->EliminateOnes(); - BOOST_CHECK_EQUAL(num_eliminated, 0); - BOOST_CHECK_EQUAL(n->Eval(), 1.); -} - - - - -BOOST_AUTO_TEST_CASE(level_one) -{ - auto one = MakeOne(); - auto n = one*one; - auto num_eliminated = n->EliminateOnes(); - BOOST_CHECK(num_eliminated > 0); - BOOST_CHECK_EQUAL(n->Eval(), 1.); -} - - -BOOST_AUTO_TEST_CASE(level_one2) +BOOST_AUTO_TEST_CASE(ones_drop_from_product) { auto x = Variable::Make("x"); auto one = MakeOne(); - auto n = x*one; - auto num_eliminated = n->EliminateOnes(); - x->set_current_value(2.); - BOOST_CHECK(num_eliminated > 0); - BOOST_CHECK_EQUAL(n->Eval(), 2.); + std::map pt{ {"x", dbl(2.)} }; + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(one*one)), 1.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(x*one), pt), 2.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(one*x), pt), 2.); } - -BOOST_AUTO_TEST_CASE(level_one3) -{ - auto x = Variable::Make("x"); - auto one = MakeOne(); - auto n = one*x; - auto num_eliminated = n->EliminateOnes(); - x->set_current_value(2.); - BOOST_CHECK(num_eliminated > 0); - BOOST_CHECK_EQUAL(n->Eval(), 2.); -} - - -BOOST_AUTO_TEST_CASE(level_one4) +BOOST_AUTO_TEST_CASE(ones_fold_through_division_chains) { auto x = Variable::Make("x"); auto one = Integer::Make(1); auto two = Integer::Make(2); - auto n = (two*one/two)*x; - auto num_eliminated = n->EliminateOnes(); - x->set_current_value(2.); - BOOST_CHECK(num_eliminated > 0); - BOOST_CHECK_EQUAL(n->Eval(), 2.); -} - - -BOOST_AUTO_TEST_CASE(level_one5) -{ - auto x = Variable::Make("x"); - auto one = Integer::Make(1); - auto n = one*one/one*one/one*one*x; - auto num_eliminated = n->EliminateOnes(); - x->set_current_value(2.); - BOOST_CHECK(num_eliminated > 0); - BOOST_CHECK_EQUAL(n->Eval(), 2.); -} - - -BOOST_AUTO_TEST_CASE(level_two) -{ - auto x = Variable::Make("x"); - auto one = Integer::Make(1); - auto n = (one*one*2) * (one*x*one); - x->set_current_value(2.); - - auto num_eliminated = n->EliminateOnes(); - - BOOST_CHECK(num_eliminated > 0); - BOOST_CHECK_EQUAL(n->Eval(), 4.); -} - - -BOOST_AUTO_TEST_SUITE_END() // predict -BOOST_AUTO_TEST_SUITE_END() // eliminate ones - - - - - - - - - -// _..._ -// _______ .-'_..._''. -// __.....__ \ ___ `'. .' .' '.\ __.....__ -// .-'' '. ' |--.\ \ / .' .-'' '. -// .-,.--. / .-''"'-. `. | | \ ' . ' / .-''"'-. `. -// | .-. |/ /________\ \| | | ' | | / /________\ \ -// | | | || || | | | _ _ | | | | -// | | | |\ .-------------'| | ' .'| ' / |. ' \ .-------------' -// | | '- \ '-.____...---.| |___.' /'.' | .' | \ '. .\ '-.____...---. -// | | `. .'/_______.'/ / | / | '. `._____.-'/ `. .' -// | | `''-...... -' \_______|/ | `'. | `-.______ / `''-...... -' -// |_| ' .'| '/ ` -// `-' `--' - - - -BOOST_AUTO_TEST_SUITE(reduce_depth) - - - - -// _ -// ( ) -// _ __ __ _| | _ _ ___ __ ______ ___ _ _ ___ ___ -// ( '__) /'__`\ /'_` |( ) ( ) /'___) /'__`\ (______) /',__)( ) ( )/' _ ` _ `\ -// | | ( ___/( (_| || (_) |( (___ ( ___/ \__, \| (_) || ( ) ( ) | -// (_) `\____)`\__,_)`\___/'`\____)`\____) (____/`\___/'(_) (_) (_) - -BOOST_AUTO_TEST_SUITE(sum) - -BOOST_AUTO_TEST_CASE(eliminates_sum_of_single) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = SumOperator::Make(x, true); - Nd n = SumOperator::Make(y, true); - - auto p = m+n; - - unsigned num_eliminated = p->ReduceDepth(); - - BOOST_CHECK(num_eliminated > 0); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - p->Reset(); - auto result = p->Eval(); - BOOST_CHECK_EQUAL(result, a+b); -} - - -BOOST_AUTO_TEST_CASE(double_sum_signs_distribute) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = SumOperator::Make(x, true); - Nd n = SumOperator::Make(y, true); - - auto p = m+n; - auto q = m-n; - - auto r = p+q; - - { - unsigned num_eliminated = r->ReduceDepth(); - BOOST_CHECK(num_eliminated > 0); - auto R = std::dynamic_pointer_cast(r); - BOOST_CHECK_EQUAL(R->NumOperands(), 4); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - r->Reset(); - auto result = r->Eval(); - BOOST_CHECK_EQUAL(result, (a+b) + (a-b)); - } - - { - unsigned num_eliminated = r->ReduceDepth(); - BOOST_CHECK(num_eliminated > 0); - auto R = std::dynamic_pointer_cast(r); - BOOST_CHECK_EQUAL(R->NumOperands(), 4); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - r->Reset(); - auto result = r->Eval(); - BOOST_CHECK_EQUAL(result, (a+b) + (a-b)); - } - -} - -BOOST_AUTO_TEST_CASE(double_sum_signs_distribute2) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = SumOperator::Make(x, true); - Nd n = SumOperator::Make(y, true); - - auto p = m+n; - auto q = m-n; - - auto r = p-q; - - { - unsigned num_eliminated = r->ReduceDepth(); - BOOST_CHECK(num_eliminated > 0); - auto R = std::dynamic_pointer_cast(r); - BOOST_CHECK_EQUAL(R->NumOperands(), 4); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - r->Reset(); - auto result = r->Eval(); - BOOST_CHECK_EQUAL(result, (a+b) - (a-b)); - } - - { - unsigned num_eliminated = r->ReduceDepth(); - BOOST_CHECK(num_eliminated > 0); - auto R = std::dynamic_pointer_cast(r); - BOOST_CHECK_EQUAL(R->NumOperands(), 4); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - r->Reset(); - auto result = r->Eval(); - BOOST_CHECK_EQUAL(result, (a+b) - (a-b)); - } - + std::map pt{ {"x", dbl(2.)} }; + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify((two*one/two)*x), pt), 2.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify(one*one/one*one/one*one*x), pt), 2.); + BOOST_CHECK_EQUAL(EvalAt(bertini::Simplify((one*one*2) * (one*x*one)), pt), 4.); } - -BOOST_AUTO_TEST_CASE(eliminates_mult_of_single) +// ---- nested singletons unwrap to the variable itself ---- +BOOST_AUTO_TEST_CASE(nested_singleton_mults_unwrap_to_the_variable) { auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = MultOperator::Make(x); - Nd n = MultOperator::Make(y); - - auto p = m+n; - - unsigned num_eliminated = p->ReduceDepth(); - - BOOST_CHECK(num_eliminated > 0); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - p->Reset(); - auto result = p->Eval(); - BOOST_CHECK_EQUAL(result, a+b); + Nd n = MultOperator::Make(x); + for (int i = 0; i < 4; ++i) n = MultOperator::Make(n); + auto s = bertini::Simplify(n); + BOOST_CHECK(std::dynamic_pointer_cast(s)); // unwrapped + BOOST_CHECK_EQUAL(EvalAt(s, {{"x", dbl(5.)}}), 5.); } - - - -BOOST_AUTO_TEST_CASE(eliminates_mult_single_regular) +// ---- the load-bearing contract: Simplified() does NOT mutate its input ---- +BOOST_AUTO_TEST_CASE(simplify_does_not_mutate_input) { auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = MultOperator::Make(x); - - auto p = m+y; - - unsigned num_eliminated = p->ReduceDepth(); - - BOOST_CHECK(num_eliminated > 0); - - auto a = x->Eval(); - auto b = y->Eval(); - p->Reset(); - auto result = p->Eval(); - BOOST_CHECK_EQUAL(result, a+b); -} - -BOOST_AUTO_TEST_CASE(eliminates_mult_single_regular2) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = MultOperator::Make(x); - Nd n = MultOperator::Make(y); - - auto p = x+n; - - unsigned num_eliminated = p->ReduceDepth(); - - BOOST_CHECK(num_eliminated > 0); - - auto a = x->Eval(); - auto b = y->Eval(); - p->Reset(); - auto result = p->Eval(); - BOOST_CHECK_EQUAL(result, a+b); -} - -BOOST_AUTO_TEST_SUITE_END() // sum - - - -// _______ -// ( _ ) -// | | | | -// | | | | -// | | | | -// |_| |_| - -BOOST_AUTO_TEST_SUITE(prod) - -BOOST_AUTO_TEST_CASE(eliminates_sum_of_single) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = SumOperator::Make(x, true); - Nd n = SumOperator::Make(y, true); - - auto p = m*n; - - unsigned num_eliminated = p->ReduceDepth(); - - BOOST_CHECK(num_eliminated > 0); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - p->Reset(); - auto result = p->Eval(); - BOOST_CHECK_EQUAL(result, a*b); -} - - -BOOST_AUTO_TEST_CASE(eliminates_mult_of_single) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - Nd m = MultOperator::Make(x); - Nd n = MultOperator::Make(y); - - auto p = m*n; - - unsigned num_eliminated = p->ReduceDepth(); - - BOOST_CHECK(num_eliminated > 0); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - p->Reset(); - auto result = p->Eval(); - BOOST_CHECK_EQUAL(result, a*b); -} - - - -BOOST_AUTO_TEST_CASE(distributive) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - - auto m = x*y; - auto n = x/y; - - auto p = m/n; // should reduce to y*y - - unsigned num_eliminated = p->ReduceDepth(); - - BOOST_CHECK(num_eliminated > 0); - - dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); - dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); - - x->set_current_value(a); y->set_current_value(b); - p->Reset(); - auto result = p->Eval(); - BOOST_CHECK_SMALL(abs(result - b*b), 1e-13); -} - - -BOOST_AUTO_TEST_CASE(a_nested_one) -{ - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - - auto n = ((((((2*x)*1)*y))/2)); - while(n->ReduceDepth()) - ; //deliberately empty statement - - auto as_op = std::dynamic_pointer_cast(n); - BOOST_CHECK_EQUAL(as_op->NumOperands(), 5); - - auto a = x->Eval(); - auto b = y->Eval(); - - BOOST_CHECK_EQUAL(n->Eval(), a*b); -} - - -BOOST_AUTO_TEST_CASE(many_nested_singletons) -{ - auto x = Variable::Make("x"); - - Nd n1 = MultOperator::Make(x); - Nd n2 = MultOperator::Make(n1); - Nd n3 = MultOperator::Make(n2); - Nd n4 = MultOperator::Make(n3); - Nd n5 = MultOperator::Make(n4); - - while(n5->ReduceDepth()) - ; //deliberately empty statement - - auto as_op = std::dynamic_pointer_cast(n5); - BOOST_CHECK_EQUAL(as_op->NumOperands(), 1); - - BOOST_CHECK_EQUAL(as_op->FirstOperand(), x); + auto zero = MakeZero(); + auto n = std::dynamic_pointer_cast((x + zero) + x); + BOOST_REQUIRE(n); + auto before = n->NumOperands(); + auto s = bertini::Simplify(n); + BOOST_CHECK_EQUAL(n->NumOperands(), before); // input untouched + BOOST_CHECK(s != n); // a fresh tree was returned + BOOST_CHECK_EQUAL(EvalAt(s, {{"x", dbl(3.)}}), 6.); // value preserved (x + x) } -BOOST_AUTO_TEST_SUITE_END() // prod - -BOOST_AUTO_TEST_SUITE_END() // reduce_depth +BOOST_AUTO_TEST_SUITE_END() // simplified @@ -786,15 +277,10 @@ BOOST_AUTO_TEST_CASE(flattens_completely) dbl a(4.1203847861962345182734, -5.1234768951256847623781614314); dbl b(-8.98798649152356714919234, 0.49879892634876018735619234); -x->set_current_value(a); y->set_current_value(b); - auto num_rounds = bertini::Simplify(r); + auto rs = bertini::Simplify(r); // functional: r is untouched, rs is simplified - BOOST_CHECK(num_rounds >= 2); - - - r->Reset(); - auto result = r->Eval(); + auto result = EvalAt(rs, {{"x", a}, {"y", b}}); BOOST_CHECK_EQUAL(result, a+a); } @@ -805,13 +291,13 @@ BOOST_AUTO_TEST_CASE(complicated) auto n = (((((2*x*1)*y)+(0*(pow(x,2))))/2)-(0*((pow(x,2))*y)/(pow(2,2)))); - auto num_rounds = bertini::Simplify(n); + auto ns = bertini::Simplify(n); - auto a = x->Eval(); - auto b = y->Eval(); + dbl a(1.7, -0.3); + dbl b(-0.9, 2.1); + std::map pt{ {"x", a}, {"y", b} }; - BOOST_CHECK(num_rounds >= 2); - BOOST_CHECK_SMALL(abs(n->Eval() - a*b), threshold_clearance_d); + BOOST_CHECK_SMALL(abs(EvalAt(ns, pt) - a*b), threshold_clearance_d); } @@ -824,14 +310,13 @@ BOOST_AUTO_TEST_CASE(complicated2) auto dfdx = f->Differentiate(x); - bertini::Simplify(dfdx); + auto dfdxs = bertini::Simplify(dfdx); - dfdx->Reset(); + dbl xval(1.3, -0.7); + dbl tval(0.4, 0.2); + std::map pt{ {"x", xval}, {"t", tval} }; -auto xval = x->Eval(); -auto tval = t->Eval(); - - BOOST_CHECK_SMALL(abs(dfdx->Eval()- 2.*(xval+tval-1.)), 1e-15); + BOOST_CHECK_SMALL(abs(EvalAt(dfdxs, pt) - 2.*(xval+tval-1.)), 1e-15); } @@ -847,13 +332,16 @@ BOOST_AUTO_TEST_CASE(complicated3) auto f = ((zero*pow((y-(HOM_VAR_0*one)),2))+((2*(y-(HOM_VAR_0*one))*(one-((zero*one)+(zero*HOM_VAR_0))))*(one-t))); - auto init_val = f->Eval(); + std::map pt{ {"x", dbl(1.1,-0.2)}, {"y", dbl(0.7,1.3)}, + {"HOM_VAR_0", dbl(-0.5,0.9)}, {"t", dbl(0.3,0.6)} }; - bertini::Simplify(f); + auto init_val = EvalAt(f, pt); - f->Reset(); + auto fs = bertini::Simplify(f); - BOOST_CHECK_EQUAL(init_val, f->Eval()); + // simplify reorders the arithmetic (like-factor/like-term combining), so compare with a + // tolerance rather than for bit-exact equality. + BOOST_CHECK_SMALL(std::abs(init_val - EvalAt(fs, pt)), 1e-12); } @@ -867,21 +355,22 @@ BOOST_AUTO_TEST_CASE(complicated4) auto HOM_VAR_0 = Variable::Make("HOM_VAR_0"); auto t = Variable::Make("t"); - auto a = x->Eval(); - auto b = y->Eval(); - auto h = HOM_VAR_0->Eval(); - auto T = t->Eval(); + dbl a(1.4, -0.6); + dbl h(-0.8, 0.5); + dbl T(0.25, 0.7); + std::map pt{ {"x", a}, {"y", dbl(0.3,0.1)}, {"HOM_VAR_0", h}, {"t", T} }; auto f = ((zero*(pow((x-(HOM_VAR_0*one)),3)))+((3*(pow((x-(HOM_VAR_0*one)),2))*(-((one*one)+(zero*HOM_VAR_0))))*(one-t))); - auto f_val_init = f->Eval(); - auto actual_val = 3.*pow((h - a),2)*(T - 1.); + auto f_val_init = EvalAt(f, pt); + [[maybe_unused]] auto actual_val = 3.*pow((h - a),2)*(T - 1.); - bertini::Simplify(f); + auto fs = bertini::Simplify(f); - f->Reset(); - auto f_val_after = f->Eval(); - BOOST_CHECK_EQUAL(f_val_init,f_val_after); + auto f_val_after = EvalAt(fs, pt); + // canonical operand ordering reorders sums/products, so simplify preserves the + // value only up to reorder rounding -- compare with a tolerance, not exact equality. + BOOST_CHECK_SMALL(std::abs(f_val_init - f_val_after), 1e-12); } //((0*((x-(HOM_VAR_0*1))^3))+((3*((x-(HOM_VAR_0*1))^2)*(-((1*1)+(0*HOM_VAR_0))))*(1-t))) @@ -897,11 +386,15 @@ BOOST_AUTO_TEST_CASE(yet_more_complicated) // jac_fn(1,2) = (((0*((y^2)-((HOM_VAR_0^2)*(9215126146405988386300422552813438491596469014004/18831809439874092151531390861220941995712612447011,-34979570316540871529966189550041755413443953059471/3298415588464117388268211317293113094514010909093))))+(((2*y*1)-(((2*HOM_VAR_0*0)*(9215126146405988386300422552813438491596469014004/18831809439874092151531390861220941995712612447011,-34979570316540871529966189550041755413443953059471/3298415588464117388268211317293113094514010909093))+(0*(HOM_VAR_0^2))))*t))+((0*((y-(HOM_VAR_0*1))^2))+((2*(y-(HOM_VAR_0*1))*(1-((0*1)+(0*HOM_VAR_0))))*(1-t)))) - auto init_val = f->Eval(); + std::map pt{ {"x", dbl(0.6,-0.4)}, {"y", dbl(1.2,0.8)}, + {"t", dbl(0.35,0.15)}, {"HOM_VAR_0", dbl(-0.7,0.3)} }; + + auto init_val = EvalAt(f, pt); - bertini::Simplify(f); -f->Reset(); - BOOST_CHECK_EQUAL(init_val, f->Eval()); + auto fs = bertini::Simplify(f); + // simplify combines like factors/terms, which reorders the arithmetic, so it preserves the + // value only up to floating-point reorder rounding -- compare with a tolerance. + BOOST_CHECK_SMALL(std::abs(init_val - EvalAt(fs, pt)), 1e-12); } BOOST_AUTO_TEST_SUITE_END() // simplify diff --git a/core/test/classes/fundamentals_test.cpp b/core/test/classes/fundamentals_test.cpp index 578a27b08..90bc40f87 100644 --- a/core/test/classes/fundamentals_test.cpp +++ b/core/test/classes/fundamentals_test.cpp @@ -27,6 +27,7 @@ #include #include +#include #include "bertini2/double_extensions.hpp" #include "bertini2/num_traits.hpp" @@ -295,6 +296,42 @@ BOOST_AUTO_TEST_CASE(RandomMP_nondefault_precision_100) BOOST_CHECK_EQUAL(100, DefaultPrecision()); } +// The multiprecision RandomMp generator now draws from the single per-thread +// engine (ThreadEngine) shared by every random type, so SetGlobalSeed controls +// it. Before this it used a private, unseedable independent_bits_engine. These +// tests pin that contract: same seed -> identical mp draws; different seed -> +// different draws. +BOOST_AUTO_TEST_CASE(RandomMP_honors_global_seed) +{ + using namespace bertini; + DefaultPrecision(50); + + auto draw_five = []{ + std::vector v; + for (int ii = 0; ii < 5; ++ii) + v.push_back(RandomMp(50)); + return v; + }; + + SetGlobalSeed(1234u); + auto first = draw_five(); + + SetGlobalSeed(1234u); + auto second = draw_five(); + + for (int ii = 0; ii < 5; ++ii) + BOOST_CHECK_EQUAL(first[ii], second[ii]); + + SetGlobalSeed(5678u); + auto third = draw_five(); + + bool any_different = false; + for (int ii = 0; ii < 5; ++ii) + if (third[ii] != first[ii]) + any_different = true; + BOOST_CHECK(any_different); +} + BOOST_AUTO_TEST_CASE(max_et_on) { mpfr_float a(1), b(2), c(4); diff --git a/core/test/classes/homogenization_test.cpp b/core/test/classes/homogenization_test.cpp index f11b83faa..f95785dcf 100644 --- a/core/test/classes/homogenization_test.cpp +++ b/core/test/classes/homogenization_test.cpp @@ -56,14 +56,14 @@ BOOST_AUTO_TEST_CASE(no_homogenization_needed_x) Var x = Variable::Make("x"); Var h = Variable::Make("h"); - auto f1 = x; + std::shared_ptr f1 = x; // widened to Node for functional Homogenized() BOOST_CHECK(f1->IsHomogeneous()); VariableGroup vars; vars.push_back(x); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 0); BOOST_CHECK(f1->IsHomogeneous()); BOOST_CHECK( f1->IsHomogeneous(x)); @@ -92,7 +92,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_x_minus_1) VariableGroup vars; vars.push_back(x); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 1); BOOST_CHECK(f1->IsHomogeneous()); @@ -120,7 +120,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_1_minus_t_x_plus_t_1_minus_x) VariableGroup vars; vars.push_back(x); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 1); BOOST_CHECK(!f1->IsHomogeneous()); @@ -149,7 +149,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_x_minus_t) VariableGroup vars; vars.push_back(x); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 1); BOOST_CHECK(!f1->IsHomogeneous()); @@ -180,7 +180,7 @@ BOOST_AUTO_TEST_CASE(no_homogenization_needed_x_minus_y_t) vars.push_back(x); vars.push_back(y); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 0); BOOST_CHECK(!f1->IsHomogeneous()); @@ -215,7 +215,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_sphere) vars.push_back(y); vars.push_back(z); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 2); BOOST_CHECK(f1->IsHomogeneous()); @@ -249,7 +249,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_quadric) vars.push_back(y); vars.push_back(z); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 2); BOOST_CHECK(f1->IsHomogeneous()); @@ -282,7 +282,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_quadratic) VariableGroup vars; vars.push_back(x); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 2); BOOST_CHECK(f1->IsHomogeneous()); @@ -310,7 +310,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_quadratic_no_constant) VariableGroup vars; vars.push_back(x); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 1); BOOST_CHECK(f1->IsHomogeneous()); @@ -339,7 +339,7 @@ BOOST_AUTO_TEST_CASE(homogenization_needed_quadratic_no_constant_wrt_y) VariableGroup vars; vars.push_back(y); - f1->Homogenize(vars,h); + f1 = f1->Homogenized(vars,h); // functional: rebind to the homogenized copy BOOST_CHECK_EQUAL(f1->Degree(h), 0); BOOST_CHECK(!f1->IsHomogeneous()); @@ -493,417 +493,6 @@ BOOST_AUTO_TEST_CASE(not_homogeneous_summands_inhomogeneous) -BOOST_AUTO_TEST_CASE(linprod_ishom) -{ - using bertini::VariableGroup; - using namespace bertini::node; - - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - auto z = Variable::Make("z"); - auto w = Variable::Make("w"); - - - - VariableGroup v1{x,w}; - VariableGroup v2{y}; - VariableGroup v3{z}; - VariableGroup hom{x,w,z}; - - std::shared_ptr linprod_node = (2 + 3*x + 2*w) * (4 - 2*x - 6*w)*(4+8*y); - std::shared_ptr linprod = LinearProduct::Make(v1,2)*LinearProduct::Make(v2,1); - - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(v1), linprod_node->IsHomogeneous(v1)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(v2), linprod_node->IsHomogeneous(v2)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(x), linprod_node->IsHomogeneous(x)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(y), linprod_node->IsHomogeneous(y)); - - linprod_node->Homogenize(v1, z); - linprod->Homogenize(v1,z); - VariableGroup badgp{x,y}; - - - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(v1), linprod_node->IsHomogeneous(v1)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(v2), linprod_node->IsHomogeneous(v2)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(x), linprod_node->IsHomogeneous(x)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(y), linprod_node->IsHomogeneous(y)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(badgp), linprod_node->IsHomogeneous(badgp)); - - linprod = LinearProduct::Make(v1,2)*LinearProduct::Make(v2,1); - BOOST_CHECK_THROW(linprod->Homogenize(badgp,y), std::exception); - - linprod_node = (2 + 3*x + 2*w) * (4 - 2*x - 6*w)*(4+8*y); - linprod = LinearProduct::Make(v1,2)*LinearProduct::Make(v2,1); - - BOOST_CHECK_THROW(linprod->Homogenize(badgp,y), std::exception); - - linprod = LinearProduct::Make(v1,2)*LinearProduct::Make(v2,1); - - linprod_node->Homogenize(hom, z); - linprod->Homogenize(hom,z); - - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(v1), linprod_node->IsHomogeneous(v1)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(v2), linprod_node->IsHomogeneous(v2)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(x), linprod_node->IsHomogeneous(x)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(y), linprod_node->IsHomogeneous(y)); - BOOST_CHECK_EQUAL(linprod->IsHomogeneous(badgp), linprod_node->IsHomogeneous(badgp)); - - - -} - - - -BOOST_AUTO_TEST_CASE(linprod_hom_eval) -{ - using bertini::VariableGroup; - using namespace bertini::node; - - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - auto h0 = Variable::Make("HOM0"); - auto h1 = Variable::Make("HOM1"); - auto z = Variable::Make("z"); - - - - VariableGroup v0{x,z}; - VariableGroup v1{y}; - Mat coeff_dbl(2,3); - Mat coeff_mpfr(2,3); - - for(int ii = 0; ii < 2; ++ii) - { - for(int jj = 0; jj < 3; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+1, jj+1); - coeff_mpfr(ii,jj) = mpfr(ii+1, jj+1); - } - } - - std::shared_ptrlinprod1 = bertini::node::LinearProduct::Make(v0, coeff_mpfr); - - coeff_dbl = Mat(1,2); - coeff_mpfr = Mat(1,2); - - for(int ii = 0; ii < 1; ++ii) - { - for(int jj = 0; jj < 2; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+3, jj+3); - coeff_mpfr(ii,jj) = mpfr(ii+3, jj+3); - } - } - - std::shared_ptr linprod2 = bertini::node::LinearProduct::Make(v1, coeff_mpfr); - - std::shared_ptr linprod_node = (mpfr(1,1)*x + mpfr(1,2)*z + mpfr(1,3)) * (mpfr(2,1)*x + mpfr(2,2)*z+ mpfr(2,3))* (mpfr(3,3)*y + mpfr(3,4)); - std::shared_ptr linprod = linprod1*linprod2; - - dbl xval_d = dbl(.5,1); - dbl yval_d = dbl(.6,1); - dbl zval_d = dbl(.7,1); - dbl h0val_d = dbl(.34, -2.1); - dbl h1val_d = dbl(-1.2, .0043); - mpfr xval_mp = mpfr(".5", "1"); - mpfr yval_mp = mpfr(".6", "1"); - mpfr zval_mp = mpfr(".7", "1"); - mpfr h0val_mp = mpfr(".34", "-2.1"); - mpfr h1val_mp = mpfr("-1.2", ".0043"); - - v0[0]->set_current_value(xval_d); - v0[1]->set_current_value(zval_d); - v1[0]->set_current_value(yval_d); - v0[0]->set_current_value(xval_mp); - v0[1]->set_current_value(zval_mp); - v1[0]->set_current_value(yval_mp); - - h0->set_current_value(h0val_d); - h1->set_current_value(h1val_d); - h0->set_current_value(h0val_mp); - h1->set_current_value(h1val_mp); - - - linprod_node->Homogenize(v0,h0); - linprod->Homogenize(v0,h0); - - dbl eval_d = linprod->Eval(); - dbl exact_d = linprod_node->Eval(); - mpfr eval_mp = linprod->Eval(); - mpfr exact_mp = linprod_node->Eval(); - - BOOST_CHECK(fabs(eval_d.real()/exact_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_d.imag()/exact_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_mp.real()/exact_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(eval_mp.imag()/exact_mp.imag() - 1) < threshold_clearance_mp); - - linprod_node->Homogenize(v1,h1); - linprod->Homogenize(v1,h1); - - eval_d = linprod->Eval(); - exact_d = linprod_node->Eval(); - eval_mp = linprod->Eval(); - exact_mp = linprod_node->Eval(); - - BOOST_CHECK(fabs(eval_d.real()/exact_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_d.imag()/exact_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_mp.real()/exact_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(eval_mp.imag()/exact_mp.imag() - 1) < threshold_clearance_mp); - - -} - -BOOST_AUTO_TEST_CASE(linprod_hom_eval_homvars) -{ - using bertini::VariableGroup; - using namespace bertini::node; - - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - auto h0 = Variable::Make("HOM0"); - auto h1 = Variable::Make("HOM1"); - auto z = Variable::Make("z"); - - - - VariableGroup v0{x,z}; - VariableGroup v1{y}; - Mat coeff_dbl(2,3); - Mat coeff_mpfr(2,3); - - for(int ii = 0; ii < 2; ++ii) - { - for(int jj = 0; jj < 3; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+1, jj+1); - coeff_mpfr(ii,jj) = mpfr(ii+1, jj+1); - } - } - - std::shared_ptrlinprod1 = bertini::node::LinearProduct::Make(v0, coeff_mpfr, true); - - coeff_dbl = Mat(1,2); - coeff_mpfr = Mat(1,2); - - for(int ii = 0; ii < 1; ++ii) - { - for(int jj = 0; jj < 2; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+3, jj+3); - coeff_mpfr(ii,jj) = mpfr(ii+3, jj+3); - } - } - - std::shared_ptr linprod2 = bertini::node::LinearProduct::Make(v1, coeff_mpfr, true); - - std::shared_ptr linprod_node = (mpfr(1,1)*x + mpfr(1,2)*z) * (mpfr(2,1)*x + mpfr(2,2)*z)* (mpfr(3,3)*y); - std::shared_ptr linprod = linprod1*linprod2; - - dbl xval_d = dbl(.5,1); - dbl yval_d = dbl(.6,1); - dbl zval_d = dbl(.7,1); - dbl h0val_d = dbl(.34, -2.1); - dbl h1val_d = dbl(-1.2, .0043); - mpfr xval_mp = mpfr(".5", "1"); - mpfr yval_mp = mpfr(".6", "1"); - mpfr zval_mp = mpfr(".7", "1"); - mpfr h0val_mp = mpfr(".34", "-2.1"); - mpfr h1val_mp = mpfr("-1.2", ".0043"); - - v0[0]->set_current_value(xval_d); - v0[1]->set_current_value(zval_d); - v1[0]->set_current_value(yval_d); - v0[0]->set_current_value(xval_mp); - v0[1]->set_current_value(zval_mp); - v1[0]->set_current_value(yval_mp); - - h0->set_current_value(h0val_d); - h1->set_current_value(h1val_d); - h0->set_current_value(h0val_mp); - h1->set_current_value(h1val_mp); - - - linprod_node->Homogenize(v0,h0); - linprod->Homogenize(v0,h0); - - dbl eval_d = linprod->Eval(); - dbl exact_d = linprod_node->Eval(); - mpfr eval_mp = linprod->Eval(); - mpfr exact_mp = linprod_node->Eval(); - - BOOST_CHECK(fabs(eval_d.real()/exact_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_d.imag()/exact_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_mp.real()/exact_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(eval_mp.imag()/exact_mp.imag() - 1) < threshold_clearance_mp); - - linprod_node->Homogenize(v1,h1); - linprod->Homogenize(v1,h1); - - eval_d = linprod->Eval(); - exact_d = linprod_node->Eval(); - eval_mp = linprod->Eval(); - exact_mp = linprod_node->Eval(); - - BOOST_CHECK(fabs(eval_d.real()/exact_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_d.imag()/exact_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(eval_mp.real()/exact_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(eval_mp.imag()/exact_mp.imag() - 1) < threshold_clearance_mp); - - -} - - - -BOOST_AUTO_TEST_CASE(linprod_hom_diff_eval) -{ - using bertini::VariableGroup; - using namespace bertini::node; - - auto x = Variable::Make("x"); - auto y = Variable::Make("y"); - auto h0 = Variable::Make("HOM0"); - auto h1 = Variable::Make("HOM1"); - auto z = Variable::Make("z"); - auto w = Variable::Make("w"); - - - - VariableGroup v0{x,z,y}; - VariableGroup v1{w}; - int num_factors = 3; - int num_variables = v0.size(); - Mat coeff_dbl(num_factors,num_variables+1); - Mat coeff_mpfr(num_factors,num_variables+1); - - for(int ii = 0; ii < num_factors; ++ii) - { - for(int jj = 0; jj < num_variables+1; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+1, jj+1); - coeff_mpfr(ii,jj) = mpfr(ii+1, jj+1); - } - } - - std::shared_ptr linprod1 = bertini::node::LinearProduct::Make(v0, coeff_mpfr); - - num_factors = 1; - num_variables = v1.size(); - coeff_dbl = Mat(num_factors,num_variables+1); - coeff_mpfr = Mat(num_factors,num_variables+1); - - for(int ii = 0; ii < num_factors; ++ii) - { - for(int jj = 0; jj < num_variables+1; ++jj) - { - coeff_dbl(ii,jj) = dbl(ii+3, jj+3); - coeff_mpfr(ii,jj) = mpfr(ii+3, jj+3); - } - } - - - std::shared_ptr linprod2 = bertini::node::LinearProduct::Make(v1, coeff_mpfr); - - std::shared_ptr linprod1_node = (mpfr(1,1)*x + mpfr(1,2)*z + mpfr(1,3)*y+ mpfr(1,4)) * (mpfr(2,1)*x + mpfr(2,2)*z + mpfr(2,3)*y+ mpfr(2,4))*(mpfr(3,1)*x + mpfr(3,2)*z + mpfr(3,3)*y+ mpfr(3,4)); - std::shared_ptr linprod2_node = (mpfr(3,3)*w + mpfr(3,4)); - - std::shared_ptr linprod_node = linprod1_node*linprod2_node; - std::shared_ptr linprod = linprod1*linprod2; - - - dbl xval_d = dbl(.5,1); - dbl yval_d = dbl(.6,1); - dbl zval_d = dbl(.7,1); - dbl wval_d = dbl(2.1, -.03); - dbl h0val_d = dbl(.34, -2.1); - dbl h1val_d = dbl(-1.2, .0043); - mpfr xval_mp = mpfr(".5", "1"); - mpfr yval_mp = mpfr(".6", "1"); - mpfr zval_mp = mpfr(".7", "1"); - mpfr wval_mp = mpfr("2.1", "-.03"); - mpfr h0val_mp = mpfr(".34", "-2.1"); - mpfr h1val_mp = mpfr("-1.2", ".0043"); - - v0[0]->set_current_value(xval_d); - v0[1]->set_current_value(zval_d); - v0[2]->set_current_value(yval_d); - v0[0]->set_current_value(xval_mp); - v0[1]->set_current_value(zval_mp); - v0[2]->set_current_value(yval_mp); - - v1[0]->set_current_value(wval_d); - v1[0]->set_current_value(wval_mp); - - h0->set_current_value(h0val_d); - h1->set_current_value(h1val_d); - h0->set_current_value(h0val_mp); - h1->set_current_value(h1val_mp); - - linprod_node->Homogenize(v0,h0); - linprod->Homogenize(v0,h0); - - linprod_node->Homogenize(v1,h1); - linprod->Homogenize(v1,h1); - - - auto J_node = bertini::node::Jacobian::Make(linprod_node->Differentiate()); - auto J = bertini::node::Jacobian::Make(linprod->Differentiate()); - - - dbl evalx_d = J->EvalJ(x); - dbl exactx_d = J_node->EvalJ(x); - mpfr evalx_mp = J->EvalJ(x); - mpfr exactx_mp = J_node->EvalJ(x); - - - - BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - - - evalx_d = J->EvalJ(z); - exactx_d = J_node->EvalJ(z); - evalx_mp = J->EvalJ(z); - exactx_mp = J_node->EvalJ(z); - - - - BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - - - evalx_d = J->EvalJ(y); - exactx_d = J_node->EvalJ(y); - evalx_mp = J->EvalJ(y); - exactx_mp = J_node->EvalJ(y); - - - BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < relaxed_threshold_clearance_d); - BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - - - evalx_d = J->EvalJ(w); - exactx_d = J_node->EvalJ(w); - evalx_mp = J->EvalJ(w); - exactx_mp = J_node->EvalJ(w); - - - - BOOST_CHECK(fabs(evalx_d.real()/exactx_d.real() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_d.imag()/exactx_d.imag() - 1) < threshold_clearance_d); - BOOST_CHECK(fabs(evalx_mp.real()/exactx_mp.real() - 1) < threshold_clearance_mp); - BOOST_CHECK(fabs(evalx_mp.imag()/exactx_mp.imag() - 1) < threshold_clearance_mp); - -} - - - - BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/interning_test.cpp b/core/test/classes/interning_test.cpp new file mode 100644 index 000000000..dc631ee55 --- /dev/null +++ b/core/test/classes/interning_test.cpp @@ -0,0 +1,262 @@ +//This file is part of Bertini 2. +// +//interning_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//interning_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with interning_test.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file Tests for hash-consing: every Make() routes through Intern(), so structurally +equal nodes collapse to one shared object. Pins the dedup behavior, the differentiation- +sharing win, and -- critically -- that building/simplifying never mutates a shared interned +node (the bug where SimplifiedSum did Make(first) then AddOperand(rest), corrupting a shared +single-operand sum). +*/ + +#include +#include +#include +#include "bertini2/function_tree.hpp" +#include "bertini2/function_tree/canonical.hpp" +#include + +#include "eval_helper.hpp" + +using Nd = std::shared_ptr; +using Variable = bertini::node::Variable; +using Integer = bertini::node::Integer; +using SumOperator = bertini::node::SumOperator; +using dbl = bertini::dbl; +using bertini::test::EvalAt; + +BOOST_AUTO_TEST_SUITE(interning) + +// ---- the regression: building/simplifying must not mutate a shared interned node ---- + +BOOST_AUTO_TEST_CASE(simplify_does_not_corrupt_a_shared_single_operand_sum) +{ + // This is the minimal form of a bug interning exposed: a single-operand Sum is reused, + // and SimplifiedSum used to do Make(first) (which now returns the *interned* shared sum) + // then AddOperand(rest) -- mutating that shared node and corrupting every other holder. + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + + Nd m = SumOperator::Make(x, true); // a one-term sum (+x); the node that got corrupted + Nd n = SumOperator::Make(y, true); // (+y) + + // build two expressions that reuse m and n, with cancelling y-terms + Nd p = m + n; // x + y + Nd q = m - n; // x - y + Nd r = p + q + 0*x; // 2x (the +0 forces simplification work) + + dbl xv(2.0, -3.0), yv(5.0, 1.0); + std::map pt{ {"x", xv}, {"y", yv} }; + + Nd rs = bertini::Simplify(r); + BOOST_CHECK_EQUAL(EvalAt(rs, pt), xv + xv); // == 2x, with the y's cancelled + + // and the reused sub-objects must be intact (not mutated by the simplification above) + BOOST_CHECK_EQUAL(EvalAt(m, pt), xv); + BOOST_CHECK_EQUAL(EvalAt(n, pt), yv); +} + +// ---- basic dedup: structurally equal builds return the same object ---- + +BOOST_AUTO_TEST_CASE(equal_constants_are_one_object) +{ + BOOST_CHECK_EQUAL(Integer::Make(5).get(), Integer::Make(5).get()); + BOOST_CHECK(Integer::Make(5).get() != Integer::Make(6).get()); +} + +BOOST_AUTO_TEST_CASE(equal_operator_trees_are_one_object) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + BOOST_CHECK_EQUAL((x*y + y).get(), (x*y + y).get()); // whole tree shares + BOOST_CHECK_EQUAL((x*y).get(), (x*y).get()); // inner subexpr shares + BOOST_CHECK_EQUAL((x + y).get(), (y + x).get()); // canonicalized: same node +} + +BOOST_AUTO_TEST_CASE(variables_are_canonical_by_name) +{ + // Make("x") interns to a single canonical x -- "system1's x IS system2's x". + auto x1 = Variable::Make("x"); + auto x2 = Variable::Make("x"); + BOOST_CHECK_EQUAL(x1.get(), x2.get()); // one object: identity is the canonicalization + BOOST_CHECK(Variable::Make("x").get() != Variable::Make("z").get()); + + // and two independently-built expressions over "x" share their structure + BOOST_CHECK_EQUAL((Variable::Make("x") * Variable::Make("x")).get(), + (Variable::Make("x") * Variable::Make("x")).get()); +} + +// ---- the headline win: a shared subexpression's derivative is one interned node ---- + +BOOST_AUTO_TEST_CASE(differentiation_results_are_interned) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = x*x + y*y; // a single shared subexpression object + + // differentiating the SAME expression twice yields the SAME derivative node + BOOST_CHECK_EQUAL(a->Differentiate(x).get(), a->Differentiate(x).get()); + + // and when 'a' is reused in two functions, the d(a)/dx inside each derivative is the + // same interned node -- the payoff of hash-consing derivatives. Checked the direct way: + Nd da = a->Differentiate(x); + Nd f = a * y; + Nd g = a + x; + // d(a)/dx built independently equals the one embedded via reuse of 'a' + BOOST_CHECK_EQUAL(a->Differentiate(x).get(), da.get()); + (void)f; (void)g; +} + +// ---- eval correctness on a heavily shared DAG ---- + +BOOST_AUTO_TEST_CASE(eval_correct_with_shared_subexpression) +{ + auto x = Variable::Make("x"); + Nd a = x*x; // shared + Nd f = a + a + a; // 3 * x^2, all the same interned 'a' + BOOST_CHECK_EQUAL(EvalAt(f, {{"x", dbl(2.0, 0.0)}}), dbl(12.0, 0.0)); // 3 * 4 +} + +// ---- immutability under interning: simplifying never changes a held input ---- + +BOOST_AUTO_TEST_CASE(simplify_leaves_a_held_shared_node_untouched) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd held = (x + y) * x; // hold a shared node + std::map pt{ {"x", dbl(3.0, 0.0)}, {"y", dbl(4.0, 0.0)} }; + auto before = EvalAt(held, pt); + + Nd s = bertini::Simplify(held + 0*y); // simplify an expression that contains 'held' + (void)s; + + BOOST_CHECK_EQUAL(EvalAt(held, pt), before); // held is unchanged by simplifying around it +} + +BOOST_AUTO_TEST_SUITE_END() // interning + + +// ---- canonical operand ordering (reorder-only) ---- + +BOOST_AUTO_TEST_SUITE(canonicalization) + +// enable canonicalization (with a chosen monomial order) for the duration of a test, then +// restore the global state -- the setting is session-global, so it must not leak. +struct CanonGuard +{ + bool prev_on; + bertini::node::MonomialOrder prev_order; + explicit CanonGuard(bertini::node::MonomialOrder o = bertini::node::MonomialOrder::GrevLex) + : prev_on(bertini::node::CanonicalizeByDefault()), + prev_order(bertini::node::CurrentMonomialOrder()) + { + bertini::node::SetMonomialOrder(o); + bertini::node::SetCanonicalizeByDefault(true); + } + ~CanonGuard() + { + bertini::node::SetCanonicalizeByDefault(prev_on); + bertini::node::SetMonomialOrder(prev_order); + } +}; + +BOOST_AUTO_TEST_CASE(disabling_canonicalization_preserves_authored_order) +{ + // canonicalization is ON by default; turn it off and the authored operand order is kept, + // so x+y and y+x become distinct interned nodes again. + bool prev = bertini::node::CanonicalizeByDefault(); + bertini::node::SetCanonicalizeByDefault(false); + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = x + y, b = y + x; + BOOST_CHECK(a.get() != b.get()); + bertini::node::SetCanonicalizeByDefault(prev); +} + +BOOST_AUTO_TEST_CASE(commutative_sum_dedups_when_enabled) +{ + CanonGuard g; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = x + y; + Nd b = y + x; + BOOST_CHECK_EQUAL(a.get(), b.get()); // canonicalized to one node + BOOST_CHECK_EQUAL(EvalAt(a, {{"x", dbl(2.0, 0.0)}, {"y", dbl(5.0, 0.0)}}), dbl(7.0, 0.0)); +} + +BOOST_AUTO_TEST_CASE(commutative_product_dedups_when_enabled) +{ + CanonGuard g; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + BOOST_CHECK_EQUAL((x*y).get(), (y*x).get()); +} + +BOOST_AUTO_TEST_CASE(division_stays_correct_under_canonicalization) +{ + CanonGuard g; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd q = y / x; // a divisor must not become the leading factor + BOOST_CHECK_EQUAL(EvalAt(q, {{"x", dbl(2.0, 0.0)}, {"y", dbl(6.0, 0.0)}}), dbl(3.0, 0.0)); // 6/2 + BOOST_CHECK((x/y).get() != (y/x).get()); // x/y and y/x stay distinct +} + +BOOST_AUTO_TEST_CASE(all_three_orders_are_selectable_and_dedup) +{ + for (auto ord : { bertini::node::MonomialOrder::Lex, + bertini::node::MonomialOrder::RevLex, + bertini::node::MonomialOrder::GrevLex }) + { + CanonGuard g(ord); + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + BOOST_CHECK_EQUAL((x + y).get(), (y + x).get()); + } +} + +BOOST_AUTO_TEST_CASE(canonicalization_shows_up_in_printing) +{ + auto str = [](Nd const& n){ std::ostringstream o; n->print(o); return o.str(); }; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + { + CanonGuard g; + BOOST_CHECK_EQUAL(str(x + y), str(y + x)); // canonical: same printed form + BOOST_CHECK_EQUAL(str(x * y), str(y * x)); + BOOST_CHECK_EQUAL(str(Integer::Make(3) * pow(x, 2)), "3*x^2"); // coefficient prints first + } + // turning it off preserves the authored operand order in the print + bool prev = bertini::node::CanonicalizeByDefault(); + bertini::node::SetCanonicalizeByDefault(false); + BOOST_CHECK_EQUAL(str(y + x), "y+x"); + bertini::node::SetCanonicalizeByDefault(prev); +} + +BOOST_AUTO_TEST_CASE(guard_restores_global_canonicalization_state) +{ + // the canonicalization setting is session-global; confirm the preceding tests' guards + // left it back at the default (on, GrevLex) so they cannot leak into other suites. + BOOST_CHECK(bertini::node::CanonicalizeByDefault()); + BOOST_CHECK(bertini::node::CurrentMonomialOrder() == bertini::node::MonomialOrder::GrevLex); +} + +BOOST_AUTO_TEST_SUITE_END() // canonicalization diff --git a/core/test/classes/linear_forms_block_test.cpp b/core/test/classes/linear_forms_block_test.cpp new file mode 100644 index 000000000..c1d40691e --- /dev/null +++ b/core/test/classes/linear_forms_block_test.cpp @@ -0,0 +1,131 @@ +//This file is part of Bertini 2. +// +//linear_forms_block_test.cpp is free software: you can redistribute it and/or +//modify it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//linear_forms_block_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +#include + +#include "bertini2/system/blocks/block.hpp" +#include "bertini2/system/blocks/linear_forms_block.hpp" + +BOOST_AUTO_TEST_SUITE(linear_forms_block_suite) + +using namespace bertini; +using bertini::blocks::LinearFormsBlock; +using bertini::DefaultPrecision; + +static_assert(bertini::blocks::is_block_v, + "LinearFormsBlock must satisfy the block contract"); + +// f0 = 2x + 3y + 1, f1 = x - y + 4 (augmented rows: [coef_x, coef_y, const]) +// at (x,y) = (1,1): f0 = 6, f1 = 4 +// Jacobian is constant: [[2, 3], [1, -1]] +static LinearFormsBlock MakeTestBlock() +{ + bertini::Mat M(2, 3); + M << mpfr_complex(2), mpfr_complex(3), mpfr_complex(1), + mpfr_complex(1), mpfr_complex(-1), mpfr_complex(4); + return LinearFormsBlock(2, std::move(M)); +} + +BOOST_AUTO_TEST_CASE(shape) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + BOOST_CHECK_EQUAL(block.NumFunctions(), 2u); + BOOST_CHECK_EQUAL(block.NumVariables(), 2u); + BOOST_CHECK(!block.DependsOnPathVariable()); + BOOST_CHECK(block.HasConstantJacobian()); +} + +BOOST_AUTO_TEST_CASE(eval_double) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + bertini::Vec result(2); + block.EvalInPlace(result, x, dbl(0)); + + BOOST_CHECK_CLOSE(result(0).real(), 6.0, 1e-11); + BOOST_CHECK_CLOSE(result(1).real(), 4.0, 1e-11); + BOOST_CHECK_SMALL(result(0).imag(), 1e-11); + BOOST_CHECK_SMALL(result(1).imag(), 1e-11); +} + +BOOST_AUTO_TEST_CASE(eval_mpfr) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + block.Precision(30); + + bertini::Vec x(2); x << mpfr_complex(1), mpfr_complex(1); + bertini::Vec result(2); + block.EvalInPlace(result, x, mpfr_complex(0)); + + BOOST_CHECK(abs(result(0) - mpfr_complex(6)) < mpfr_float("1e-25")); + BOOST_CHECK(abs(result(1) - mpfr_complex(4)) < mpfr_float("1e-25")); +} + +BOOST_AUTO_TEST_CASE(jacobian_is_constant_coefficient_matrix) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + + // evaluate the Jacobian at two different points; it must not change. + bertini::Mat J0(2, 2), J1(2, 2); + bertini::Vec p0(2); p0 << dbl(1), dbl(1); + bertini::Vec p1(2); p1 << dbl(-5), dbl(7); + block.JacobianInPlace(J0, p0, dbl(0)); + block.JacobianInPlace(J1, p1, dbl(0)); + + BOOST_CHECK_CLOSE(J0(0,0).real(), 2.0, 1e-11); + BOOST_CHECK_CLOSE(J0(0,1).real(), 3.0, 1e-11); + BOOST_CHECK_CLOSE(J0(1,0).real(), 1.0, 1e-11); + BOOST_CHECK_CLOSE(J0(1,1).real(), -1.0, 1e-11); + BOOST_CHECK_SMALL((J0 - J1).norm(), 1e-11); // constant in x +} + +BOOST_AUTO_TEST_CASE(time_derivative_is_zero) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + bertini::Vec x(2); x << dbl(3), dbl(4); + bertini::Vec dt(2); + block.TimeDerivInPlace(dt, x, dbl(0)); + BOOST_CHECK_SMALL(dt.norm(), 1e-12); +} + +// eval against an independent matrix-vector reference at a generic point, in mpfr. +BOOST_AUTO_TEST_CASE(eval_matches_matrix_vector_reference) +{ + DefaultPrecision(40); + bertini::Mat M(3, 4); + M << mpfr_complex(2), mpfr_complex(-1), mpfr_complex(0), mpfr_complex(5), + mpfr_complex(1), mpfr_complex(3), mpfr_complex(-2), mpfr_complex(-1), + mpfr_complex(0), mpfr_complex(4), mpfr_complex(7), mpfr_complex(2); + LinearFormsBlock block(3, M); + block.Precision(40); + + bertini::Vec x(3); x << mpfr_complex(2), mpfr_complex(-3), mpfr_complex(1); + bertini::Vec result(3); + block.EvalInPlace(result, x, mpfr_complex(0)); + + bertini::Vec aug(4); aug << x(0), x(1), x(2), mpfr_complex(1); + bertini::Vec reference = M * aug; + BOOST_CHECK((result - reference).norm() < mpfr_float("1e-30")); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/m_hom_start_system.cpp b/core/test/classes/m_hom_start_system.cpp index bd59de6a0..a1c6386d5 100644 --- a/core/test/classes/m_hom_start_system.cpp +++ b/core/test/classes/m_hom_start_system.cpp @@ -25,6 +25,10 @@ #include #include "bertini2/system/start_systems.hpp" +#include "bertini2/system/blocks/block.hpp" +#include "bertini2/system/blocks/blend_block.hpp" +#include +#include using System = bertini::System; @@ -38,8 +42,8 @@ using mpfr_float = bertini::mpfr_float; using mpz_int = bertini::mpz_int; using dbl = bertini::dbl; using mpfr = bertini::mpfr_complex; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; #include "externs.hpp" @@ -68,18 +72,24 @@ BOOST_AUTO_TEST_CASE(m_hom_system_preliminary_construction_small_example) */ auto x = Variable::Make("x"); auto y = Variable::Make("y"); + // second projective coordinate of each homogeneous group: a hom group of size k is + // P^{k-1}, so a size-1 group is the degenerate P^0. Use size-2 groups (P^1, dimension + // 1) so each group absorbs one function -- the degree matrix, partitions, and start- + // point count are exactly as for the original (capacity = dimension = size - 1 = 1). + auto x1 = Variable::Make("x1"); + auto y1 = Variable::Make("y1"); System sys; - VariableGroup v1{x}; - VariableGroup v2{y}; + VariableGroup v1{x, x1}; + VariableGroup v2{y, y1}; sys.AddHomVariableGroup(v1); sys.AddHomVariableGroup(v2); sys.AddFunction(x*y); sys.AddFunction(pow(x,2)*pow(y,2)); - + auto mhom_start_system = bertini::start_system::MHomogeneous(sys); Vec partition_1(2); @@ -101,6 +111,87 @@ BOOST_AUTO_TEST_CASE(m_hom_system_preliminary_construction_small_example) } +// Each generated start point must be an actual root of the (homogenized + patched) +// start system: evaluating the start system there is ~0. This validates both the +// start-point linear solve and its homogenization onto the patch (the part the solve +// flow needs). +BOOST_AUTO_TEST_CASE(start_points_are_roots_of_the_start_system) +{ + DefaultPrecision(30); + bertini::SetGlobalSeed(1u); + + System sys; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddVariableGroup(VariableGroup{y}); + sys.AddFunction(x*y - 1); + sys.AddFunction(x + y); + sys.Homogenize(); + sys.AutoPatch(); + + auto mhom = MHomogeneous(sys); + + // Partition capacity respects each group's declared dimension even after homogenization + // (VariableGroupSizes() would count the homogenizing variable and double it): the + // m-homogeneous Bezout number here is 2, not 4. + BOOST_CHECK_EQUAL(mhom.NumStartPoints(), 2ull); + + const auto n = mhom.NumStartPoints(); + for (unsigned long long i = 0; i < n; ++i) + { + auto sp = mhom.StartPoint(i); + BOOST_CHECK_EQUAL(static_cast(sp.size()), mhom.NumVariables()); + auto v = mhom.Eval(sp); + for (Eigen::Index j = 0; j < v.size(); ++j) + BOOST_CHECK(std::abs(v(j)) < 1e-10); // each start point is a root, on the patch + } + + // The blend homotopy (built as FormHomotopy does) is correct: zero at the start points + // at t=1, and its Jacobian and dH/dt agree with finite differences. + auto t = Variable::Make("t"); + auto gamma = bertini::node::Rational::Make(bertini::node::Rational::Rand()); + System H = sys; // target's variable structure + patch + H.ClearFunctions(); // the blend supplies the rows; don't also eval target's own functions + H.AddPathVariable(t); + std::vector> coeffs{ 1 - t, gamma * t }; + std::vector> operands{ + std::make_shared(sys), + std::make_shared(mhom) }; + H.AddBlock(bertini::blocks::BlendBlock(t, coeffs, operands)); + + for (unsigned long long i = 0; i < n; ++i) + { + auto Hval = H.Eval(mhom.StartPoint(i), dbl(1)); + for (Eigen::Index j = 0; j < Hval.size(); ++j) + BOOST_CHECK(std::abs(Hval(j)) < 1e-9); + } + + bertini::Vec xq(H.NumVariables()); + for (Eigen::Index k = 0; k < xq.size(); ++k) + xq(k) = dbl(0.37 * (k + 1) + 0.11, -0.19 * k + 0.07); + const dbl tv(0.42, -0.13); + const dbl hstep(1e-6, 0); + + auto J = H.Jacobian(xq, tv); + auto f0 = H.Eval(xq, tv); + for (Eigen::Index c = 0; c < xq.size(); ++c) + { + auto xp = xq; xp(c) += hstep; + auto fp = H.Eval(xp, tv); + for (Eigen::Index r = 0; r < f0.size(); ++r) + BOOST_CHECK(std::abs((fp(r) - f0(r)) / hstep - J(r, c)) < 1e-5); + } + + auto dHdt = H.TimeDerivative(xq, tv); + auto fpt = H.Eval(xq, tv + hstep); + for (Eigen::Index r = 0; r < f0.size(); ++r) + BOOST_CHECK(std::abs((fpt(r) - f0(r)) / hstep - dHdt(r)) < 1e-5); +} + + + + BOOST_AUTO_TEST_CASE(m_hom_system_preliminary_construction_larger_example) { @@ -121,13 +212,20 @@ BOOST_AUTO_TEST_CASE(m_hom_system_preliminary_construction_larger_example) auto x = Variable::Make("x"); auto y = Variable::Make("y"); auto z = Variable::Make("z"); + // second projective coordinate of each homogeneous group (see the small example above): + // a hom group of size k is P^{k-1}, so use size-2 groups (P^1, dimension 1). Degree + // matrix, partitions, and start-point count are exactly as for size-1 groups under the + // correct convention (capacity = dimension = size - 1 = 1). + auto x1 = Variable::Make("x1"); + auto y1 = Variable::Make("y1"); + auto z1 = Variable::Make("z1"); System sys; - VariableGroup v1{x}; - VariableGroup v2{y}; - VariableGroup v3{z}; + VariableGroup v1{x, x1}; + VariableGroup v2{y, y1}; + VariableGroup v3{z, z1}; sys.AddHomVariableGroup(v1); sys.AddHomVariableGroup(v2); diff --git a/core/test/classes/moving_homotopy_test.cpp b/core/test/classes/moving_homotopy_test.cpp new file mode 100644 index 000000000..6c7bde08d --- /dev/null +++ b/core/test/classes/moving_homotopy_test.cpp @@ -0,0 +1,261 @@ +//This file is part of Bertini 2. +// +//moving_homotopy_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//moving_homotopy_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file moving_homotopy_test.cpp + +\brief Unit tests for MakeMovingHomotopy: move only the moving rows, keep the fixed system as its +own blocks (evaluated once, contributing zero to dH/dt). +*/ + +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/system/blocks/block.hpp" + +BOOST_AUTO_TEST_SUITE(moving_homotopy_suite) + +using namespace bertini; +using bertini::node::Variable; + +// gamma fixed off the real axis for reproducibility. +static std::shared_ptr Gamma() +{ + return node::Float::Make(mpfr_complex("0.6", "0.8")); +} + +// A LinearFormsBlock-backed single-form system c.[x;1] over variables {x,y}. +static System LinearFormSystem(std::shared_ptr x, std::shared_ptr y, + mpfr_complex cx, mpfr_complex cy, mpfr_complex c1) +{ + System s; + s.AddVariableGroup(VariableGroup{x, y}); + Mat M(1, 3); + M << cx, cy, c1; + s.AddBlock(blocks::LinearFormsBlock(2, M)); + return s; +} + +// H, its expansion twin, agree on values AND Jacobian at several (point, t), in dbl and mpfr. +static void AgreesWithExpansionAtTimes(System const& H) +{ + System twin = H.ExpandToFunctionTree(); + BOOST_REQUIRE_EQUAL(H.NumNaturalFunctions(), twin.NumNaturalFunctions()); + // (no Degrees() comparison: the blend reports space-only degree, while the expanded node tree + // counts the path variable t too, so they legitimately differ for a homotopy.) + + const Eigen::Index nv = static_cast(H.NumVariables()); + std::vector times{dbl(1.0), dbl(0.0), dbl(0.37, -0.21)}; + + Vec p(nv); + for (Eigen::Index k = 0; k < nv; ++k) p(k) = dbl(0.3 + 0.13 * static_cast(k), 0.4 - 0.07 * static_cast(k)); + + for (auto t : times) + { + Vec a = H.Eval(p, t), b = twin.Eval(p, t); + for (Eigen::Index i = 0; i < a.size(); ++i) + BOOST_CHECK(std::abs(a(i) - b(i)) < 1e-10); + + Mat ja = H.Jacobian(p, t), jb = twin.Jacobian(p, t); + for (Eigen::Index i = 0; i < ja.rows(); ++i) + for (Eigen::Index j = 0; j < ja.cols(); ++j) + BOOST_CHECK(std::abs(ja(i, j) - jb(i, j)) < 1e-10); + + Vec da = H.TimeDerivative(p, t), db = twin.TimeDerivative(p, t); + for (Eigen::Index i = 0; i < da.size(); ++i) + BOOST_CHECK(std::abs(da(i) - db(i)) < 1e-10); + } + + // one mpfr cross-check + DefaultPrecision(40); + Vec pm(nv); + for (Eigen::Index k = 0; k < nv; ++k) pm(k) = mpfr_complex(p(k).real(), p(k).imag()); + mpfr_complex tm("0.37", "-0.21"); + Vec am = H.Eval(pm, tm), bm = twin.Eval(pm, tm); + for (Eigen::Index i = 0; i < am.size(); ++i) + BOOST_CHECK(abs(am(i) - bm(i)) < mpfr_float("1e-30")); + Mat jam = H.Jacobian(pm, tm), jbm = twin.Jacobian(pm, tm); + for (Eigen::Index i = 0; i < jam.rows(); ++i) + for (Eigen::Index j = 0; j < jam.cols(); ++j) + BOOST_CHECK(abs(jam(i, j) - jbm(i, j)) < mpfr_float("1e-30")); +} + + +// fixed = unit circle (PolynomialBlock); moving slice y (start) -> y-x (end), both polynomial. +static System CircleMovingSlice() +{ + auto x = Variable::Make("x"), y = Variable::Make("y"); + System fixed; fixed.AddVariableGroup(VariableGroup{x, y}); + fixed.AddFunction(x*x + y*y - node::Integer::Make(1)); + System start_moving; start_moving.AddVariableGroup(VariableGroup{x, y}); start_moving.AddFunction(y); + System end_moving; end_moving.AddVariableGroup(VariableGroup{x, y}); end_moving.AddFunction(y - x); + return MakeMovingHomotopy(fixed, start_moving, end_moving, "t", Gamma()); +} + + +BOOST_AUTO_TEST_CASE(shape_and_path_variable) +{ + DefaultPrecision(30); + System H = CircleMovingSlice(); + BOOST_CHECK_EQUAL(H.NumNaturalFunctions(), 2u); // 1 fixed (circle) + 1 moving (slice) + BOOST_CHECK(H.HavePathVariable()); +} + +BOOST_AUTO_TEST_CASE(endpoints_t0_and_t1) +{ + DefaultPrecision(40); + System H = CircleMovingSlice(); + + Vec p(2); p << dbl(0.4, 0.2), dbl(-0.3, 0.5); + const dbl circle = p(0)*p(0) + p(1)*p(1) - dbl(1); + + // t = 0: moving row = end_moving = y - x ; fixed row = circle. + Vec at0 = H.Eval(p, dbl(0)); + BOOST_CHECK(std::abs(at0(0) - circle) < 1e-12); + BOOST_CHECK(std::abs(at0(1) - (p(1) - p(0))) < 1e-12); + + // t = 1: moving row = gamma * start_moving = gamma * y ; fixed row still circle. + const dbl gamma(0.6, 0.8); + Vec at1 = H.Eval(p, dbl(1)); + BOOST_CHECK(std::abs(at1(0) - circle) < 1e-12); + BOOST_CHECK(std::abs(at1(1) - gamma * p(1)) < 1e-12); +} + +BOOST_AUTO_TEST_CASE(fixed_row_is_left_out_of_time_derivative) +{ + DefaultPrecision(40); + System H = CircleMovingSlice(); + + Vec p(2); p << dbl(0.4, 0.2), dbl(-0.3, 0.5); + // dH/dt = [ 0 (circle is t-independent) ; -end + gamma*start = -(y-x) + gamma*y ] + Vec dt = H.TimeDerivative(p, dbl(0.5)); + BOOST_CHECK(std::abs(dt(0)) < 1e-14); // fixed row: exactly out + const dbl gamma(0.6, 0.8); + BOOST_CHECK(std::abs(dt(1) - (-(p(1) - p(0)) + gamma * p(1))) < 1e-12); // moving row +} + +BOOST_AUTO_TEST_CASE(matches_expansion_polynomial_moving) +{ + DefaultPrecision(40); + AgreesWithExpansionAtTimes(CircleMovingSlice()); +} + +BOOST_AUTO_TEST_CASE(static_slice_and_moving_slice_both_fixed) +{ + // fixed = circle (poly) + a STATIC linear-forms slice x ; moving slice y -> y - x. + DefaultPrecision(40); + auto x = Variable::Make("x"), y = Variable::Make("y"); + System fixed; fixed.AddVariableGroup(VariableGroup{x, y}); + fixed.AddFunction(x*x + y*y - node::Integer::Make(1)); // row 0: circle (PolynomialBlock) + Mat M(1, 3); M << mpfr_complex(1), mpfr_complex(0), mpfr_complex(0); + fixed.AddBlock(blocks::LinearFormsBlock(2, M)); // row 1: static slice x=0 (LinearFormsBlock) + + System start_moving = LinearFormSystem(x, y, mpfr_complex(0), mpfr_complex(1), mpfr_complex(0)); // y + System end_moving; end_moving.AddVariableGroup(VariableGroup{x, y}); end_moving.AddFunction(y - x); + + System H = MakeMovingHomotopy(fixed, start_moving, end_moving, "t", Gamma()); + BOOST_CHECK_EQUAL(H.NumNaturalFunctions(), 3u); + + // both fixed rows (circle, static slice) are out of dH/dt; only the moving row survives. + Vec p(2); p << dbl(0.4, 0.2), dbl(-0.3, 0.5); + Vec dt = H.TimeDerivative(p, dbl(0.5)); + BOOST_CHECK(std::abs(dt(0)) < 1e-14); + BOOST_CHECK(std::abs(dt(1)) < 1e-14); + BOOST_CHECK(std::abs(dt(2)) > 1e-3); + + AgreesWithExpansionAtTimes(H); +} + +BOOST_AUTO_TEST_CASE(deform_products_of_linears_into_polynomial) +{ + // regeneration step: deform (x-1)(x+1) (a products-of-linears block) into the circle, with a + // static slice y (= the fixed row). + DefaultPrecision(40); + auto x = Variable::Make("x"), y = Variable::Make("y"); + System fixed = LinearFormSystem(x, y, mpfr_complex(0), mpfr_complex(1), mpfr_complex(0)); // y (static) + + System start_moving; start_moving.AddVariableGroup(VariableGroup{x, y}); + Mat f(2, 3); + f << mpfr_complex(1), mpfr_complex(0), mpfr_complex(-1), // (x - 1) + mpfr_complex(1), mpfr_complex(0), mpfr_complex(1); // (x + 1) + start_moving.AddBlock(blocks::ProductsOfLinearsBlock(2, std::vector>{f})); + + System end_moving; end_moving.AddVariableGroup(VariableGroup{x, y}); + end_moving.AddFunction(x*x + y*y - node::Integer::Make(1)); + + System H = MakeMovingHomotopy(fixed, start_moving, end_moving, "t", Gamma()); + BOOST_CHECK_EQUAL(H.NumNaturalFunctions(), 2u); + + // at t=1 the moving row is gamma*(x-1)(x+1) = gamma*(x^2-1); at t=0 it is the circle. + Vec p(2); p << dbl(2.0), dbl(3.0); + const dbl gamma(0.6, 0.8); + Vec at1 = H.Eval(p, dbl(1)); + BOOST_CHECK(std::abs(at1(1) - gamma * (p(0)*p(0) - dbl(1))) < 1e-10); + Vec at0 = H.Eval(p, dbl(0)); + BOOST_CHECK(std::abs(at0(1) - (p(0)*p(0) + p(1)*p(1) - dbl(1))) < 1e-10); + + // fixed (static slice) row out of dH/dt + Vec dt = H.TimeDerivative(p, dbl(0.5)); + BOOST_CHECK(std::abs(dt(0)) < 1e-14); + + AgreesWithExpansionAtTimes(H); +} + +BOOST_AUTO_TEST_CASE(rejects_mismatched_endpoints) +{ + DefaultPrecision(30); + auto x = Variable::Make("x"), y = Variable::Make("y"); + System fixed; fixed.AddVariableGroup(VariableGroup{x, y}); fixed.AddFunction(x*x + y*y - node::Integer::Make(1)); + System sm; sm.AddVariableGroup(VariableGroup{x, y}); sm.AddFunction(y); + System em; em.AddVariableGroup(VariableGroup{x, y}); em.AddFunction(y - x); em.AddFunction(x); // 2 != 1 + BOOST_CHECK_THROW(MakeMovingHomotopy(fixed, sm, em, "t", Gamma()), std::runtime_error); +} + +// Regression: Clone(System) is now a Memory-isolating shallow copy --- it shares the +// immutable node DAG and the compiled SLP Program, but copies the per-thread evaluation Memory at +// every level, INCLUDING a BlendBlock's nested operand Systems (which are deep-copied via the +// System copy constructor: own Memory, shared DAG). A clone of a moving homotopy must therefore +// reproduce the original exactly, and the two must evaluate independently. (Thread-safety itself +// --- no shared mutable state --- is by construction here and is exercised by the threaded zero-dim +// solves; a sequential test cannot observe a data race because each Eval re-sets its inputs.) +BOOST_AUTO_TEST_CASE(clone_of_moving_homotopy_reproduces_and_is_independent) +{ + DefaultPrecision(30); + System H = CircleMovingSlice(); + H.Differentiate(); + + Vec p1(2); p1 << dbl(0.3, 0.1), dbl(-0.2, 0.4); + Vec p2(2); p2 << dbl(1.5, -0.7), dbl(0.9, 0.2); + const dbl t1(0.25, 0.0), t2(0.8, -0.1); + + const Vec f1 = H.Eval(p1, t1); + const Mat j1 = H.Jacobian(p1, t1); + + System H_clone = Clone(H); + + // The clone (its deep-copied operands included) reproduces the original exactly. + BOOST_CHECK(H_clone.Eval(p1, t1).isApprox(f1)); + BOOST_CHECK(H_clone.Jacobian(p1, t1).isApprox(j1)); + + // Evaluating the clone elsewhere leaves the original's results unchanged (independent state). + (void) H_clone.Eval(p2, t2); + (void) H_clone.Jacobian(p2, t2); + BOOST_CHECK(H.Eval(p1, t1).isApprox(f1)); + BOOST_CHECK(H.Jacobian(p1, t1).isApprox(j1)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/named_expression_test.cpp b/core/test/classes/named_expression_test.cpp new file mode 100644 index 000000000..515b7109b --- /dev/null +++ b/core/test/classes/named_expression_test.cpp @@ -0,0 +1,95 @@ +#include + +#include "bertini2/function_tree.hpp" +#include "bertini2/function_tree/find.hpp" +#include "bertini2/system/eval_expression.hpp" + +#include +#include + +using bertini::node::Variable; +using bertini::node::Named; +using bertini::node::NamedExpression; +using bertini::node::Find; +using Nd = std::shared_ptr; +using dbl = bertini::dbl; +using bertini::EvalExpression; + +BOOST_AUTO_TEST_SUITE(named_expression) + +// A NamedExpression prints as just its name (the expansion is revealed elsewhere, by Describe). +BOOST_AUTO_TEST_CASE(prints_as_its_name) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = Named(x*x + y*y, "a"); + + std::stringstream ss; ss << a; + BOOST_CHECK_EQUAL(ss.str(), "a"); +} + +// It evaluates to its wrapped expression. +BOOST_AUTO_TEST_CASE(evaluates_to_its_expression) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = Named(x*x + y*y, "a"); + // a at (3,4) = 9 + 16 = 25 + auto v = EvalExpression(a, {{"x", dbl(3,0)}, {"y", dbl(4,0)}}); + BOOST_CHECK_SMALL(std::abs(v - dbl(25,0)), 1e-12); +} + +// Hash-consed by (expression, name): same -> one node; different name or bare expr -> different. +BOOST_AUTO_TEST_CASE(hash_consed_by_expression_and_name) +{ + auto x = Variable::Make("x"); + Nd e = x*x; + Nd a1 = Named(e, "a"); + Nd a2 = Named(e, "a"); + Nd b = Named(e, "b"); + + BOOST_CHECK(a1.get() == a2.get()); // same expression + name -> the same interned node + BOOST_CHECK(a1.get() != b.get()); // different name -> a different node + BOOST_CHECK(a1.get() != e.get()); // distinct from the bare expression it wraps +} + +// Usable as a subexpression: a named node referenced multiple times evaluates correctly. +BOOST_AUTO_TEST_CASE(usable_as_a_subexpression) +{ + auto x = Variable::Make("x"); + Nd a = Named(x*x, "a"); + Nd f = a*a + a; // a^2 + a, with a = x^2; at x=2 -> 16 + 4 = 20 + auto v = EvalExpression(f, {{"x", dbl(2,0)}}); + BOOST_CHECK_SMALL(std::abs(v - dbl(20,0)), 1e-12); +} + +// Find discovers the named subexpressions in a tree, including nested ones, +// sorted by name. +BOOST_AUTO_TEST_CASE(find_discovers_named_expressions) +{ + auto x = Variable::Make("x"); + Nd a = Named(x*x, "a"); + Nd b = Named(a + 1, "b"); // nested: b's entry references a + Nd f = b*b; + + auto found = Find(f); + BOOST_REQUIRE_EQUAL(found.size(), 2u); // a and b + BOOST_CHECK_EQUAL(found[0]->name(), "a"); // sorted by name + BOOST_CHECK_EQUAL(found[1]->name(), "b"); +} + +// Find is GatherVariables' general form: sorted, deduped, descending through Named. +BOOST_AUTO_TEST_CASE(find_variables_descends_through_named) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = Named(x*x, "a"); + Nd f = a + y; // x lives inside the named expression a; y is bare + + auto vars = Find(f); + BOOST_REQUIRE_EQUAL(vars.size(), 2u); + BOOST_CHECK_EQUAL(vars[0]->name(), "x"); + BOOST_CHECK_EQUAL(vars[1]->name(), "y"); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/node_serialization_test.cpp b/core/test/classes/node_serialization_test.cpp index 12ded1845..f609ea1ba 100644 --- a/core/test/classes/node_serialization_test.cpp +++ b/core/test/classes/node_serialization_test.cpp @@ -48,13 +48,16 @@ #include "externs.hpp" +#include "eval_helper.hpp" + +using bertini::test::EvalAt; BOOST_AUTO_TEST_SUITE(node_serialization) -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using Variable = bertini::node::Variable; using Node = bertini::node::Node; using Float = bertini::node::Float; @@ -115,7 +118,7 @@ BOOST_AUTO_TEST_CASE(serialize_float) ia >> two_point_oh_four2; } - BOOST_CHECK(two_point_oh_four->Eval()==two_point_oh_four2->Eval()); + BOOST_CHECK(EvalAt(two_point_oh_four)==EvalAt(two_point_oh_four2)); } BOOST_AUTO_TEST_CASE(serialize_complicated_expression) @@ -147,10 +150,8 @@ BOOST_AUTO_TEST_CASE(serialize_complicated_expression) BOOST_CHECK(x->name()==x2->name()); - x->set_current_value(dbl(1.2,0.9)); - x2->set_current_value(dbl(1.2,0.9)); - - BOOST_CHECK(abs(f->Eval() - f2->Eval()) < threshold_clearance_d); + std::map pt{ {"x", dbl(1.2,0.9)} }; + BOOST_CHECK(abs(EvalAt(f, pt) - EvalAt(f2, pt)) < threshold_clearance_d); } @@ -353,12 +354,21 @@ BOOST_AUTO_TEST_CASE(system_clone) BOOST_CHECK_EQUAL(variables1.size(), variables2.size()); - - for (int ii=0; ii other(2); other(0) = dbl(5.0); other(1) = dbl(7.0); + (void) sys1.Eval(other); + Vec v2 = sys2.Eval(values); + BOOST_CHECK_EQUAL(v2(0), 36.0); + BOOST_CHECK_EQUAL(v2(1), 12.0); } diff --git a/core/test/classes/patch_test.cpp b/core/test/classes/patch_test.cpp index 2e6f5cac5..e53f190bc 100644 --- a/core/test/classes/patch_test.cpp +++ b/core/test/classes/patch_test.cpp @@ -49,8 +49,8 @@ BOOST_AUTO_TEST_SUITE(patch_class) using bertini::DefaultPrecision; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using Patch = bertini::Patch; using dbl = bertini::dbl; @@ -122,6 +122,41 @@ BOOST_AUTO_TEST_CASE(patch_jacobian_two_variable_groups_prec16) } +// A patch row is sparse (one coefficient block per variable group, zero elsewhere), so +// JacobianInPlace must FULLY define the rows it owns -- including zeroing the off-coefficient +// entries -- rather than relying on the caller to hand it a zeroed matrix. The block-composed +// System::Jacobian path allocates J uninitialized and assigns only the function (block) rows, so a +// patch that left its off-coefficient entries untouched read uninitialized memory there: benign +// (zeroed pages) on Linux/macOS, but garbage on Windows, where a degree-2 homotopy's Jacobian +// "evaluated" to ~1e252 and wrecked the AMP condition-number estimate. Hand it a fully-poisoned +// buffer and require every owned entry to be correct. +BOOST_AUTO_TEST_CASE(patch_jacobian_fully_defines_its_rows_into_a_dirty_buffer) +{ + std::vector s{2,3}; + + Patch p(s); + p.Precision(16); + + Vec v(5); + v << dbl(1), dbl(1), dbl(1), dbl(1), dbl(1); + + Mat J = Mat::Constant(2, 5, dbl(1e300)); // poison every entry + + p.JacobianInPlace(J, v); + + // off-coefficient entries of each patch row must be overwritten with zero, not left poisoned + BOOST_CHECK_EQUAL(J(0,2), dbl(0)); + BOOST_CHECK_EQUAL(J(0,3), dbl(0)); + BOOST_CHECK_EQUAL(J(0,4), dbl(0)); + BOOST_CHECK_EQUAL(J(1,0), dbl(0)); + BOOST_CHECK_EQUAL(J(1,1), dbl(0)); + + // the coefficient entries are still written (no longer the poison value) + BOOST_CHECK_NE(J(0,0), dbl(1e300)); + BOOST_CHECK_NE(J(1,2), dbl(1e300)); +} + + BOOST_AUTO_TEST_CASE(patch_eval_two_variable_groups_prec30) { diff --git a/core/test/classes/precision_propagation_test.cpp b/core/test/classes/precision_propagation_test.cpp new file mode 100644 index 000000000..755ca430a --- /dev/null +++ b/core/test/classes/precision_propagation_test.cpp @@ -0,0 +1,208 @@ +//This file is part of Bertini 2. +// +//test/classes/precision_propagation_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//test/classes/precision_propagation_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with test/classes/precision_propagation_test.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team + +// individual authors of this file include: +// silviana amethyst, university of wisconsin-eau claire + +#include + +#include "bertini2/bertini.hpp" +#include "bertini2/mpfr_extensions.hpp" +#include "bertini2/num_traits.hpp" +#include "bertini2/trackers/config.hpp" +#include "bertini2/endgames/config.hpp" +#include "bertini2/detail/enable_permuted_arguments.hpp" + +using bertini::DefaultPrecision; +using bertini::Precision; +using mpfr_float = bertini::mpfr_float; +using mpfr_complex = bertini::mpfr_complex; +using mpq_rational = bertini::mpq_rational; + +// ----------------------------------------------------------------------- +// Helpers +// ----------------------------------------------------------------------- + +// Simulate a stale non-local-static mpfr_float: created at STARTUP_PREC +// then used in arithmetic at a different target precision. +static constexpr unsigned STARTUP_PREC = 20; // BMP default at program start + +// ----------------------------------------------------------------------- +BOOST_AUTO_TEST_SUITE(precision_propagation) +// ----------------------------------------------------------------------- + + +// --- 1. DefaultPrecision is uniform across float and complex ------------ + +BOOST_AUTO_TEST_CASE(default_precision_sets_both_float_and_complex) +{ + DefaultPrecision(30); + BOOST_CHECK_EQUAL(mpfr_float::default_precision(), 30u); + BOOST_CHECK_EQUAL(mpfr_complex::default_precision(), 30u); + + DefaultPrecision(16); + BOOST_CHECK_EQUAL(mpfr_float::default_precision(), 16u); + BOOST_CHECK_EQUAL(mpfr_complex::default_precision(), 16u); + + DefaultPrecision(50); + BOOST_CHECK_EQUAL(mpfr_float::default_precision(), 50u); + BOOST_CHECK_EQUAL(mpfr_complex::default_precision(), 50u); +} + + +// --- 2. Precision policy is uniform after DefaultPrecision() ----------- +// (only meaningful with expression templates on, but harmless either way) + +#ifdef BMP_EXPRESSION_TEMPLATES +BOOST_AUTO_TEST_CASE(default_precision_sets_preserve_related_on_both_types) +{ + using bmp = boost::multiprecision::variable_precision_options; + DefaultPrecision(30); + BOOST_CHECK(mpfr_float::thread_default_variable_precision_options() + == bmp::preserve_related_precision); + BOOST_CHECK(mpfr_complex::thread_default_variable_precision_options() + == bmp::preserve_related_precision); +} +#endif + + +// --- 3. mpq_rational carries no MPFR precision state ------------------ +// +// Key property: mpq_rational * mpfr_float(prec=N) → result at prec=N. +// This is the fix for EndgameConfig::sample_factor. + +BOOST_AUTO_TEST_CASE(mpq_rational_times_mpfr_float_result_at_target_precision) +{ + DefaultPrecision(16); + mpfr_float t("0.3"); + BOOST_REQUIRE_EQUAL(t.precision(), 16u); + + mpq_rational half{1, 2}; + mpfr_float result = t * half; + BOOST_CHECK_EQUAL(result.precision(), 16u); +} + +BOOST_AUTO_TEST_CASE(mpq_rational_times_mpfr_float_at_high_precision) +{ + DefaultPrecision(50); + mpfr_float t("0.3"); + mpq_rational r{647, 1000}; + mpfr_float result = t * r; + BOOST_CHECK_EQUAL(result.precision(), 50u); +} + + +// --- 4. Stale mpfr_float at prec > target contaminates arithmetic ----- +// +// DOCUMENTARY test: demonstrates the PROBLEM that motivates using +// mpq_rational for config fields. With preserve_related_precision, +// an mpfr_float stale at prec=STARTUP_PREC poisons results when +// STARTUP_PREC > target precision. + +BOOST_AUTO_TEST_CASE(stale_mpfr_float_contaminates_when_startup_prec_exceeds_target) +{ + // Create a stale config value at "startup precision" (20 digits). + DefaultPrecision(STARTUP_PREC); + mpfr_float stale_factor("0.5"); + BOOST_REQUIRE_EQUAL(stale_factor.precision(), STARTUP_PREC); + + // Now switch to a lower target precision. + DefaultPrecision(16); + mpfr_float t("0.3"); + BOOST_REQUIRE_EQUAL(t.precision(), 16u); + + // With preserve_related_precision the result takes max(stale, target) = 20 — NOT 16. + // This test DOCUMENTS the contamination and is expected to fail/warn until + // all config fields that appear in non-local statics are changed to mpq_rational. +#ifdef BMP_EXPRESSION_TEMPLATES + mpfr_float contaminated = t * stale_factor; + BOOST_WARN_EQUAL(contaminated.precision(), 16u); // WARN: currently 20 (contaminated) + BOOST_CHECK_EQUAL(contaminated.precision(), STARTUP_PREC); // confirms contamination +#endif +} + + +// --- 5. EndgameConfig::sample_factor is mpq_rational (regression guard) +// +// If someone changes sample_factor back to mpfr_float, this test +// catches the precision contamination at prec < STARTUP_PREC. + +BOOST_AUTO_TEST_CASE(endgame_config_sample_factor_no_precision_contamination) +{ + // Use the static DefaultConstruct value — exactly what happens in production + // when an endgame is constructed without an explicit config. + auto const& cfg = bertini::detail::DefaultConstruct::value; + + DefaultPrecision(16); + mpfr_float t("0.3"); + BOOST_REQUIRE_EQUAL(t.precision(), 16u); + + mpfr_float result = t * mpfr_float(cfg.sample_factor); + BOOST_CHECK_EQUAL(result.precision(), 16u); +} + +BOOST_AUTO_TEST_CASE(endgame_config_default_sample_factor_is_one_half) +{ + bertini::endgame::EndgameConfig cfg; + BOOST_CHECK_EQUAL(cfg.sample_factor, mpq_rational(1, 2)); +} + + +// --- 6. SteppingConfig step-size fields should not contaminate --------- +// +// These tests currently FAIL because SteppingConfig uses mpfr_float for +// its step-size fields — they get initialized at prec=STARTUP_PREC in +// DefaultConstruct::value. They will PASS once those +// fields are changed to mpq_rational (or double for the min_step_size). + +BOOST_AUTO_TEST_CASE(stepping_config_success_factor_no_precision_contamination) +{ + auto const& cfg = bertini::detail::DefaultConstruct::value; + + DefaultPrecision(16); + mpfr_float step("0.1"); + BOOST_REQUIRE_EQUAL(step.precision(), 16u); + + mpfr_float result = step * mpfr_float(cfg.step_size_success_factor); + BOOST_CHECK_EQUAL(result.precision(), 16u); +} + +BOOST_AUTO_TEST_CASE(stepping_config_fail_factor_no_precision_contamination) +{ + auto const& cfg = bertini::detail::DefaultConstruct::value; + + DefaultPrecision(16); + mpfr_float step("0.1"); + BOOST_REQUIRE_EQUAL(step.precision(), 16u); + + mpfr_float result = step * mpfr_float(cfg.step_size_fail_factor); + BOOST_CHECK_EQUAL(result.precision(), 16u); +} + +BOOST_AUTO_TEST_CASE(stepping_config_max_step_size_no_stale_precision) +{ + auto const& cfg = bertini::detail::DefaultConstruct::value; + + DefaultPrecision(16); + // max_step_size itself should be at (or convertible to) target precision without contamination + mpfr_float result = mpfr_float(cfg.max_step_size); + BOOST_CHECK_EQUAL(result.precision(), 16u); +} + + +// ----------------------------------------------------------------------- +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/products_of_linears_block_test.cpp b/core/test/classes/products_of_linears_block_test.cpp new file mode 100644 index 000000000..ea9ac83e2 --- /dev/null +++ b/core/test/classes/products_of_linears_block_test.cpp @@ -0,0 +1,118 @@ +//This file is part of Bertini 2. +// +//products_of_linears_block_test.cpp is free software: you can redistribute it and/or +//modify it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//products_of_linears_block_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +#include + +#include "bertini2/system/blocks/block.hpp" +#include "bertini2/system/blocks/products_of_linears_block.hpp" + +BOOST_AUTO_TEST_SUITE(products_of_linears_block_suite) + +using namespace bertini; +using bertini::blocks::ProductsOfLinearsBlock; +using bertini::DefaultPrecision; + +static_assert(bertini::blocks::is_block_v, + "ProductsOfLinearsBlock must satisfy the block contract"); + +// f0 = (2x + 3y + 1)(x - y + 4), f1 = (x + 1) +// at (x,y) = (1,1): f0 = 6*4 = 24, f1 = 2 +// df0/dx = 2*4 + 6*1 = 14, df0/dy = 3*4 + 6*(-1) = 6 +// df1/dx = 1, df1/dy = 0 +static ProductsOfLinearsBlock MakeTestBlock() +{ + bertini::Mat f0(2, 3); + f0 << mpfr_complex(2), mpfr_complex(3), mpfr_complex(1), + mpfr_complex(1), mpfr_complex(-1), mpfr_complex(4); + + bertini::Mat f1(1, 3); + f1 << mpfr_complex(1), mpfr_complex(0), mpfr_complex(1); + + std::vector> factors{f0, f1}; + return ProductsOfLinearsBlock(2, std::move(factors)); +} + +BOOST_AUTO_TEST_CASE(shape) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + BOOST_CHECK_EQUAL(block.NumFunctions(), 2u); + BOOST_CHECK_EQUAL(block.NumVariables(), 2u); + BOOST_CHECK(!block.DependsOnPathVariable()); +} + +BOOST_AUTO_TEST_CASE(eval_double) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + bertini::Vec result(2); + block.EvalInPlace(result, x, dbl(0)); + + BOOST_CHECK_CLOSE(result(0).real(), 24.0, 1e-11); + BOOST_CHECK_CLOSE(result(1).real(), 2.0, 1e-11); + BOOST_CHECK_SMALL(result(0).imag(), 1e-11); + BOOST_CHECK_SMALL(result(1).imag(), 1e-11); +} + +BOOST_AUTO_TEST_CASE(jacobian_double) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + bertini::Mat J(2, 2); + block.JacobianInPlace(J, x, dbl(0)); + + BOOST_CHECK_CLOSE(J(0, 0).real(), 14.0, 1e-11); + BOOST_CHECK_CLOSE(J(0, 1).real(), 6.0, 1e-11); + BOOST_CHECK_CLOSE(J(1, 0).real(), 1.0, 1e-11); + BOOST_CHECK_SMALL(J(1, 1).real(), 1e-11); +} + +BOOST_AUTO_TEST_CASE(eval_mpfr) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + block.Precision(30); + + bertini::Vec x(2); x << mpfr_complex(1), mpfr_complex(1); + bertini::Vec result(2); + block.EvalInPlace(result, x, mpfr_complex(0)); + + BOOST_CHECK(abs(result(0) - mpfr_complex(24)) < mpfr_float("1e-25")); + BOOST_CHECK(abs(result(1) - mpfr_complex(2)) < mpfr_float("1e-25")); +} + +BOOST_AUTO_TEST_CASE(jacobian_mpfr) +{ + DefaultPrecision(30); + auto block = MakeTestBlock(); + block.Precision(30); + + bertini::Vec x(2); x << mpfr_complex(1), mpfr_complex(1); + bertini::Mat J(2, 2); + block.JacobianInPlace(J, x, mpfr_complex(0)); + + BOOST_CHECK(abs(J(0, 0) - mpfr_complex(14)) < mpfr_float("1e-25")); + BOOST_CHECK(abs(J(0, 1) - mpfr_complex(6)) < mpfr_float("1e-25")); + BOOST_CHECK(abs(J(1, 0) - mpfr_complex(1)) < mpfr_float("1e-25")); + BOOST_CHECK(abs(J(1, 1)) < mpfr_float("1e-25")); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/randomization_block_test.cpp b/core/test/classes/randomization_block_test.cpp new file mode 100644 index 000000000..1146d1899 --- /dev/null +++ b/core/test/classes/randomization_block_test.cpp @@ -0,0 +1,332 @@ +//This file is part of Bertini 2. +// +//randomization_block_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//randomization_block_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file randomization_block_test.cpp + +\brief Unit tests for RandomizationBlock and System::Randomize. + +The workhorse oracle is ExpandToFunctionTree(): a randomized system evaluated through its block +must agree, value-for-value and derivative-for-derivative, with the same system expanded to plain +function-tree nodes -- in both dbl and mpfr_complex, before AND after homogenization (so the +homogenizing-variable power deficits are exercised), for single- and multi-projective systems. +That cross-check is deterministic on every platform, unlike a heap-dirtiness-dependent bug. +*/ + +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/system/blocks/block.hpp" +#include "bertini2/system/blocks/randomization_block.hpp" + +BOOST_AUTO_TEST_SUITE(randomization_block_suite) + +using namespace bertini; +using bertini::node::Variable; +using bertini::blocks::RandomizationBlock; +using Randomization = RandomizationBlock; + +// The contract is checkable here (System is complete), even though block.hpp cannot static_assert +// it (System is only forward-declared there). +static_assert(bertini::blocks::is_block_v, + "RandomizationBlock must satisfy the block contract"); + + +// ---- helpers ------------------------------------------------------------------------------- + +// An overdetermined single-affine-group system of UNEQUAL degrees (so randomization needs the +// h-power deficits once homogenized): f0 = x^2 + y^2 - 1 (deg 2), f1 = x - y (deg 1), +// f2 = 2x^2 - 1 (deg 2). Common zeros are the two points (+/- 1/sqrt2, +/- 1/sqrt2) with x = y. +static System OverdeterminedSingleGroup() +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + System s; + s.AddVariableGroup(VariableGroup{x, y}); + s.AddFunction(x * x + y * y - node::Integer::Make(1)); + s.AddFunction(x - y); + s.AddFunction(node::Integer::Make(2) * x * x - node::Integer::Make(1)); + return s; +} + +// An overdetermined two-affine-group system of UNEQUAL multidegrees: with groups {x}, {y}, +// f0 = x*y (md (1,1)), f1 = x*x*y (md (2,1)), f2 = x (md (1,0)), f3 = y (md (0,1)). +static System OverdeterminedMultiGroup() +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + System s; + s.AddVariableGroup(VariableGroup{x}); + s.AddVariableGroup(VariableGroup{y}); + s.AddFunction(x * y); + s.AddFunction(x * x * y); + s.AddFunction(x); + s.AddFunction(y); + return s; +} + +// Compare a block-composed system against its function-tree expansion at a handful of points, in +// both numeric types. Covers values AND the Jacobian. +static void AgreesWithExpansion(System const& sys) +{ + System twin = sys.ExpandToFunctionTree(); + + BOOST_REQUIRE_EQUAL(sys.NumNaturalFunctions(), twin.NumNaturalFunctions()); + BOOST_REQUIRE_EQUAL(sys.NumVariables(), twin.NumVariables()); + // the randomized rows' degrees must survive the expansion + BOOST_CHECK(sys.Degrees() == twin.Degrees()); + + const Eigen::Index nv = static_cast(sys.NumVariables()); + + // a few non-degenerate complex points + std::vector> pts; + { + Vec p(nv); + for (Eigen::Index k = 0; k < nv; ++k) p(k) = dbl(0.3 + 0.17 * k, 0.5 - 0.11 * k); + pts.push_back(p); + Vec q(nv); + for (Eigen::Index k = 0; k < nv; ++k) q(k) = dbl(-0.7 + 0.05 * k, 0.9 + 0.03 * k); + pts.push_back(q); + } + + for (auto const& p : pts) + { + // --- dbl --- + Vec ea = sys.Eval(p); + Vec eb = twin.Eval(p); + BOOST_REQUIRE_EQUAL(ea.size(), eb.size()); + for (Eigen::Index i = 0; i < ea.size(); ++i) + BOOST_CHECK(std::abs(ea(i) - eb(i)) < 1e-10); + + Mat ja = sys.Jacobian(p); + Mat jb = twin.Jacobian(p); + BOOST_REQUIRE_EQUAL(ja.rows(), jb.rows()); + BOOST_REQUIRE_EQUAL(ja.cols(), jb.cols()); + for (Eigen::Index i = 0; i < ja.rows(); ++i) + for (Eigen::Index j = 0; j < ja.cols(); ++j) + BOOST_CHECK(std::abs(ja(i, j) - jb(i, j)) < 1e-10); + + // --- mpfr_complex --- + DefaultPrecision(40); + Vec pm(nv); + for (Eigen::Index k = 0; k < nv; ++k) + pm(k) = mpfr_complex(p(k).real(), p(k).imag()); + + Vec ema = sys.Eval(pm); + Vec emb = twin.Eval(pm); + for (Eigen::Index i = 0; i < ema.size(); ++i) + BOOST_CHECK(abs(ema(i) - emb(i)) < mpfr_float("1e-30")); + + Mat jma = sys.Jacobian(pm); + Mat jmb = twin.Jacobian(pm); + for (Eigen::Index i = 0; i < jma.rows(); ++i) + for (Eigen::Index j = 0; j < jma.cols(); ++j) + BOOST_CHECK(abs(jma(i, j) - jmb(i, j)) < mpfr_float("1e-30")); + } +} + + +// ---- shape / construction ------------------------------------------------------------------ + +BOOST_AUTO_TEST_CASE(randomize_squares_the_system) +{ + DefaultPrecision(30); + System s = OverdeterminedSingleGroup(); + BOOST_REQUIRE_EQUAL(s.NumNaturalFunctions(), 3u); + + System r = s.Randomize(); + + BOOST_CHECK_EQUAL(r.NumNaturalFunctions(), 2u); // n = NumVariables - NumHomVariableGroups = 2 + BOOST_CHECK_EQUAL(r.NumVariables(), 2u); + // the original system is NOT mutated + BOOST_CHECK_EQUAL(s.NumNaturalFunctions(), 3u); +} + +BOOST_AUTO_TEST_CASE(randomize_does_not_mutate_original_ordering) +{ + DefaultPrecision(30); + System s = OverdeterminedSingleGroup(); + auto degs_before = s.Degrees(); // (2,1,2) in author order + s.Randomize(); + auto degs_after = s.Degrees(); + BOOST_CHECK(degs_before == degs_after); // the descending sort happened on the copy, not on s +} + +BOOST_AUTO_TEST_CASE(optimal_path_count_is_product_of_top_n_degrees) +{ + DefaultPrecision(30); + System s = OverdeterminedSingleGroup(); // degrees 2,1,2 -> top two are 2,2 + System r = s.Randomize(); + auto d = r.Degrees(); + BOOST_REQUIRE_EQUAL(d.size(), 2u); + int product = 1; + for (int e : d) product *= e; + BOOST_CHECK_EQUAL(product, 4); // 2*2, not 2^3 nor (maxdeg)^n with the low one inflated +} + +BOOST_AUTO_TEST_CASE(matrix_getter_round_trips_and_is_I_C) +{ + DefaultPrecision(30); + System s = OverdeterminedSingleGroup(); + System r = s.Randomize(); + + auto const& block = std::get(r.Blocks().front()); + auto const& R = block.RandomizationMatrix(); + BOOST_REQUIRE_EQUAL(R.rows(), 2); + BOOST_REQUIRE_EQUAL(R.cols(), 3); + // leading 2x2 is the identity (the [I | C] structure after the descending sort) + BOOST_CHECK(R(0, 0) == mpfr_complex(1) && R(1, 1) == mpfr_complex(1)); + BOOST_CHECK(R(0, 1) == mpfr_complex(0) && R(1, 0) == mpfr_complex(0)); +} + +BOOST_AUTO_TEST_CASE(underdetermined_throws) +{ + DefaultPrecision(30); + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + auto z = Variable::Make("z"); + System s; + s.AddVariableGroup(VariableGroup{x, y, z}); + s.AddFunction(x + y + z); // 1 function, 3 variables + BOOST_CHECK_THROW(s.Randomize(), std::runtime_error); +} + + +// ---- evaluation correctness via the expansion oracle --------------------------------------- + +BOOST_AUTO_TEST_CASE(affine_single_group_matches_expansion) +{ + DefaultPrecision(40); + System r = OverdeterminedSingleGroup().Randomize(); // affine, not yet homogenized + AgreesWithExpansion(r); +} + +BOOST_AUTO_TEST_CASE(homogenized_single_group_matches_expansion) +{ + DefaultPrecision(40); + System r = OverdeterminedSingleGroup().Randomize(); + r.Homogenize(); // exercises the h-power deficit (deg-1 fn padded by h) + AgreesWithExpansion(r); +} + +BOOST_AUTO_TEST_CASE(affine_multi_group_matches_expansion) +{ + DefaultPrecision(40); + System r = OverdeterminedMultiGroup().Randomize(); + AgreesWithExpansion(r); +} + +BOOST_AUTO_TEST_CASE(homogenized_multi_group_matches_expansion) +{ + DefaultPrecision(40); + System r = OverdeterminedMultiGroup().Randomize(); + r.Homogenize(); // per-group hom-var power deficits + AgreesWithExpansion(r); +} + + +// ---- user-supplied matrix ------------------------------------------------------------------ + +BOOST_AUTO_TEST_CASE(user_supplied_matrix_reproduces_combination) +{ + DefaultPrecision(40); + System s = OverdeterminedSingleGroup(); // f0=x^2+y^2-1, f1=x-y, f2=2x^2-1 (order preserved) + + // g0 = 1*f0 + 0*f1 + 0*f2 ; g1 = 3*f0 + 0*f1 + 5*f2 (kept in author order: f0 has max degree) + Mat R(2, 3); + R << mpfr_complex(1), mpfr_complex(0), mpfr_complex(0), + mpfr_complex(3), mpfr_complex(0), mpfr_complex(5); + System r = s.Randomize(R); + + BOOST_CHECK_EQUAL(r.NumNaturalFunctions(), 2u); + + // at (x,y) = (2,3): f0 = 4+9-1 = 12, f2 = 8-1 = 7. g0 = 12, g1 = 3*12 + 5*7 = 71. + Vec p(2); p << dbl(2), dbl(3); + Vec g = r.Eval(p); + BOOST_CHECK(std::abs(g(0) - dbl(12)) < 1e-10); + BOOST_CHECK(std::abs(g(1) - dbl(71)) < 1e-10); + + // and it agrees with its own expansion, affine and homogenized + AgreesWithExpansion(r); + r.Homogenize(); + AgreesWithExpansion(r); +} + +BOOST_AUTO_TEST_CASE(user_supplied_matrix_wrong_columns_throws) +{ + DefaultPrecision(30); + System s = OverdeterminedSingleGroup(); // 3 natural functions + Mat R(2, 2); // 2 columns != 3 + R << mpfr_complex(1), mpfr_complex(0), mpfr_complex(0), mpfr_complex(1); + BOOST_CHECK_THROW(s.Randomize(R), std::runtime_error); +} + + +// ---- ADR-0021: a block fully defines its own rows into a dirty buffer ---------------------- + +BOOST_AUTO_TEST_CASE(eval_and_jacobian_fully_define_rows_in_a_dirty_buffer) +{ + DefaultPrecision(40); + System r = OverdeterminedSingleGroup().Randomize(); + r.Homogenize(); + + auto const& block = std::get(r.Blocks().front()); + const Eigen::Index n = static_cast(r.NumNaturalFunctions()); + const Eigen::Index nv = static_cast(r.NumVariables()); + + Vec p(nv); + for (Eigen::Index k = 0; k < nv; ++k) p(k) = dbl(0.4 + 0.1 * k, 0.2 - 0.05 * k); + + // poison the output buffers with a huge value; the block must overwrite every owned entry. + Vec seg(n); seg.setConstant(dbl(1e300, 1e300)); + Mat jac(n, nv); jac.setConstant(dbl(1e300, 1e300)); + + block.EvalInPlace(seg, p, dbl(0)); + block.JacobianInPlace(jac, p, dbl(0)); + + // reference: the function-tree expansion of just the block's rows + System twin = r.ExpandToFunctionTree(); + Vec seg_ref = twin.Eval(p); + Mat jac_ref = twin.Jacobian(p); + + for (Eigen::Index i = 0; i < n; ++i) + BOOST_CHECK(std::abs(seg(i) - seg_ref(i)) < 1e-10); + for (Eigen::Index i = 0; i < n; ++i) + for (Eigen::Index j = 0; j < nv; ++j) + BOOST_CHECK(std::abs(jac(i, j) - jac_ref(i, j)) < 1e-10); +} + + +// ---- metadata ------------------------------------------------------------------------------ + +BOOST_AUTO_TEST_CASE(metadata) +{ + DefaultPrecision(30); + System r = OverdeterminedSingleGroup().Randomize(); + auto const& block = std::get(r.Blocks().front()); + BOOST_CHECK_EQUAL(block.NumFunctions(), 2u); + BOOST_CHECK(!block.DependsOnPathVariable()); // autonomous operand + BOOST_CHECK(!block.HasConstantJacobian()); + + // time-derivative is identically zero + Vec p(2); p << dbl(1), dbl(1); + Vec dt(2); dt.setConstant(dbl(7)); + block.TimeDerivInPlace(dt, p, dbl(0)); + BOOST_CHECK(std::abs(dt(0)) < 1e-14 && std::abs(dt(1)) < 1e-14); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/slp_test.cpp b/core/test/classes/slp_test.cpp index f838a7a52..bcefc2931 100644 --- a/core/test/classes/slp_test.cpp +++ b/core/test/classes/slp_test.cpp @@ -4,12 +4,15 @@ #include "bertini2/io/parsing/system_parsers.hpp" #include "bertini2/system/start_systems.hpp" +#include +#include + using Variable = bertini::node::Variable; using bertini::Operation; using SLP = bertini::StraightLineProgram; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using dbl = bertini::dbl; BOOST_AUTO_TEST_SUITE(SLP_tests) @@ -20,7 +23,7 @@ bertini::System SingleVariableTestSystem(){ std::string str = "function f; variable_group x; f = x+1;"; bertini::System sys; - bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + [[maybe_unused]] bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); return sys; } @@ -30,7 +33,7 @@ bertini::System TwoVariableTestSystem(){ std::string str = "function f,g; variable_group x,y; f = x^2+y^2-1; g = x-y;"; bertini::System sys; - bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + [[maybe_unused]] bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); return sys; } @@ -42,7 +45,7 @@ bertini::System ThreeVariableTestSystem(){ bertini::System sys; - bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + [[maybe_unused]] bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); return sys; } @@ -55,7 +58,7 @@ bertini::System HomotopyTotalDegreeTestSystem(){ bertini::System sys; - bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + [[maybe_unused]] bool success = bertini::parsing::classic::parse(str.begin(), str.end(), sys); sys.Homogenize(); sys.AutoPatch(); @@ -298,3 +301,197 @@ BOOST_AUTO_TEST_CASE(evaluate_three_variable_system) BOOST_AUTO_TEST_SUITE_END() + + +// ---- common-subexpression elimination via hash-consing ---- +// +// The SLP compiler keys a node->result-slot map by node pointer and compiles each node only +// once. Because identical subexpressions are hash-consed to a single shared node, the +// compiled program tracks the *DAG* of distinct subexpressions, not the fully-expanded +// expression tree -- so repeated/shared structure is computed once. + +BOOST_AUTO_TEST_SUITE(SLP_cse) + +namespace { + using Node = bertini::node::Node; + using NaryOperator = bertini::node::NaryOperator; + using Nd = std::shared_ptr; + + // nodes in the fully-expanded expression TREE: a shared subnode is counted once per place + // it appears -- the work a naive, non-CSE tree-walk evaluator would do. + std::size_t ExpandedTreeNodes(Nd const& n) + { + if (auto nary = std::dynamic_pointer_cast(n)) + { + std::size_t c = 1; + for (auto const& op : nary->Operands()) + c += ExpandedTreeNodes(op); + return c; + } + return 1; + } + + // DISTINCT nodes (the DAG): what hash-consing + the SLP compiler actually share. + void CollectDistinct(Nd const& n, std::set& seen) + { + if (!seen.insert(n.get()).second) + return; + if (auto nary = std::dynamic_pointer_cast(n)) + for (auto const& op : nary->Operands()) + CollectDistinct(op, seen); + } + std::size_t DistinctNodes(Nd const& n) + { + std::set seen; + CollectDistinct(n, seen); + return seen.size(); + } + + std::size_t SlpSlots(bertini::System const& s) + { + bertini::StraightLineProgram slp(s); + return slp.NumMemorySlots(); + } +} + +BOOST_AUTO_TEST_CASE(hash_consing_unifies_independently_built_subexpressions) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + // (x+y) built twice, independently, then added: hash-consing makes them one node, so the + // DAG has a single (x+y) even though the expanded tree repeats it. + Nd f = (x + y) + (x + y); + BOOST_CHECK_EQUAL(DistinctNodes(f), 4u); // x, y, (x+y), the outer sum + BOOST_CHECK_EQUAL(ExpandedTreeNodes(f), 7u); // outer + 2*(sum + x + y) +} + +BOOST_AUTO_TEST_CASE(cse_benchmark_squaring_chain) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + + std::cout << "\nCSE_TABLE_BEGIN\n"; + std::cout << "| K | expanded tree nodes | distinct nodes (DAG) | SLP slots (fns+Jac) | reduction (tree/DAG) |\n"; + std::cout << "|--:|--------------------:|---------------------:|--------------------:|---------------------:|\n"; + + for (int K = 1; K <= 16; ++K) + { + Nd e = x + y; + for (int i = 0; i < K; ++i) + e = e * e; // e*e reuses the same node; the DAG grows by one per level + + const auto expanded = ExpandedTreeNodes(e); + const auto distinct = DistinctNodes(e); + + bertini::System sys; + sys.AddVariableGroup(bertini::VariableGroup{x, y}); + sys.AddFunction(e); + const auto slots = SlpSlots(sys); + + std::cout << "| " << K << " | " << expanded << " | " << distinct + << " | " << slots << " | " << (expanded / distinct) << "x |\n"; + + // the same function is a linear DAG but an exponential tree: hash-consing collapses it + BOOST_CHECK_EQUAL(distinct, static_cast(K + 3)); // x, y, (x+y), e_1..e_K + BOOST_CHECK_EQUAL(expanded, (std::size_t{1} << (K + 2)) - 1); // a binary tree + if (K >= 8) + BOOST_CHECK_LT(slots, expanded); // the compiled program stays DAG-sized + } + std::cout << "CSE_TABLE_END\n" << std::endl; +} + +BOOST_AUTO_TEST_SUITE_END() // SLP_cse + + +// (the SLP-vs-tree oracle suite lived here; removed when the FunctionTree eval method was +// retired -- the SLP is now the sole system evaluator.) + + +// ---- freeze-set tape partition (ADR-0027) ---- +// +// The compiler stably reorders the instruction tape so every constant-only ("frozen") +// instruction precedes every variable-dependent ("live") one. A point-only re-evaluation +// then skips the frozen prologue (reusing the constants already in memory), and the whole +// tape runs only when the constants are not yet valid for the working precision. + +BOOST_AUTO_TEST_SUITE(SLP_freeze_partition) + +using bertini::node::Integer; +using mpfr_complex = bertini::mpfr_complex; + +namespace { + // f = x + sin(1): sin(1) is a constant unary operation -> a frozen instruction. + bertini::System ConstantSubexpressionSystem() + { + auto x = Variable::Make("x"); + bertini::System sys; + sys.AddVariableGroup(bertini::VariableGroup{x}); + sys.AddFunction(x + sin(Integer::Make(1))); + return sys; + } +} + +BOOST_AUTO_TEST_CASE(constant_subexpression_yields_a_frozen_prologue) +{ + auto slp = SLP(ConstantSubexpressionSystem()); + // sin(1) compiles to at least one frozen instruction, so the live segment does not start at 0. + BOOST_CHECK_GT(slp.FirstLiveInstructionOffset(), 0u); +} + +BOOST_AUTO_TEST_CASE(no_constant_operator_means_no_prologue) +{ + auto x = Variable::Make("x"); + bertini::System sys; + sys.AddVariableGroup(bertini::VariableGroup{x}); + sys.AddFunction(x * x); // every instruction depends on x -> nothing frozen + auto slp = SLP(sys); + BOOST_CHECK_EQUAL(slp.FirstLiveInstructionOffset(), 0u); +} + +// Skipping the frozen prologue on a point-only change must not corrupt the result: the +// constant stays in memory and the second eval (at a new point) reuses it. +BOOST_AUTO_TEST_CASE(point_only_change_reuses_constants_correctly) +{ + auto slp = SLP(ConstantSubexpressionSystem()); + + const double s1 = std::sin(1.0); + + Vec p(1); + p(0) = dbl(2.0); + slp.Eval(p); + BOOST_CHECK_CLOSE(slp.GetFuncVals()(0).real(), 2.0 + s1, 1e-10); + + p(0) = dbl(5.0); // point-only change: prologue skipped, sin(1) reused + slp.Eval(p); + BOOST_CHECK_CLOSE(slp.GetFuncVals()(0).real(), 5.0 + s1, 1e-10); + + p(0) = dbl(2.0); // back again + slp.Eval(p); + BOOST_CHECK_CLOSE(slp.GetFuncVals()(0).real(), 2.0 + s1, 1e-10); +} + +// A precision change must invalidate the frozen prologue so the constant is recomputed at the +// new precision. The tolerance (1e-40) is tighter than the original precision (30 digits): if +// invalidation were broken and sin(1) stayed at 30-digit accuracy, this would fail. +BOOST_AUTO_TEST_CASE(precision_change_recomputes_constants) +{ + auto sys = ConstantSubexpressionSystem(); + + bertini::DefaultPrecision(30); + sys.precision(30); + Vec p30(1); + p30(0) = mpfr_complex(2); + auto f30 = sys.Eval(p30); + BOOST_CHECK(abs(f30(0) - (mpfr_complex(2) + sin(mpfr_complex(1)))) < 1e-25); + + bertini::DefaultPrecision(50); + sys.precision(50); + Vec p50(1); + p50(0) = mpfr_complex(2); + auto f50 = sys.Eval(p50); + // sin(1) recomputed at 50 digits -> accurate well past 30 digits. + BOOST_CHECK(abs(f50(0) - (mpfr_complex(2) + sin(mpfr_complex(1)))) < 1e-40); +} + +BOOST_AUTO_TEST_SUITE_END() // SLP_freeze_partition + diff --git a/core/test/classes/start_system_test.cpp b/core/test/classes/start_system_test.cpp index 7e8340ba5..2ffda14cf 100644 --- a/core/test/classes/start_system_test.cpp +++ b/core/test/classes/start_system_test.cpp @@ -52,8 +52,8 @@ using mpz_int = bertini::mpz_int; using dbl = bertini::dbl; using mpfr = bertini::mpfr_complex; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; @@ -264,6 +264,7 @@ BOOST_AUTO_TEST_CASE(linear_total_degree_start_system) BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_total_degree_start_system) { + bertini::SetGlobalSeed(1u); // deterministic random TotalDegree values; see ADR-0003 bertini::System sys; Var x = Variable::Make("x"), y = Variable::Make("y"), z = Variable::Make("z"); @@ -341,6 +342,7 @@ BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_total_degree_start_system) BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_start_points) { + bertini::SetGlobalSeed(1u); // deterministic random TotalDegree values; see ADR-0003 bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); bertini::System sys; @@ -364,7 +366,7 @@ BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_start_points) for (decltype(function_values.size()) jj = 0; jj < function_values.size(); ++jj) BOOST_CHECK(abs(function_values(jj)) < - abs(vs[jj]->Eval())*relaxed_threshold_clearance_d); + abs(vs[jj]->Value())*relaxed_threshold_clearance_d); } for (decltype(TD.NumStartPoints()) ii = 0; ii < TD.NumStartPoints(); ++ii) @@ -384,6 +386,7 @@ BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_start_points) // this one differs from the above only in that the target system was homogenized and patched BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_start_points_homogenized_patched) { + bertini::SetGlobalSeed(1u); // deterministic random TotalDegree values; see ADR-0003 bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); bertini::System sys; @@ -410,14 +413,23 @@ BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_start_points_homogenized_patched) // evaluate the start system at that point. // // the function values must be near 0 + const auto& vs = TD.RandomValues(); for (decltype(TD.NumStartPoints()) ii = 0; ii < TD.NumStartPoints(); ++ii) { auto start = TD.StartPoint(ii); auto function_values = TD.Eval(start); for (decltype(function_values.size()) jj = 0; jj < function_values.size(); ++jj) - BOOST_CHECK(abs(function_values(jj)) < - 1000*relaxed_threshold_clearance_d); + { + // Scale-relative residual. A start point is an exact root of x^d - r, so the + // residual of natural function jj scales with |r_jj|; comparing raw |f| against + // a fixed absolute threshold is scale-naive and flakes when a random r happens + // to be large. Rows with jj >= vs.size() are patch/homogenization equations of + // O(1) scale, so a unit scale (absolute floor) is correct for them. + double scale = (jj < vs.size()) ? abs(vs[jj]->Value()) : 1.0; + if (scale < 1.0) scale = 1.0; + BOOST_CHECK(abs(function_values(jj)) < scale*1000*relaxed_threshold_clearance_d); + } } for (decltype(TD.NumStartPoints()) ii = 0; ii < TD.NumStartPoints(); ++ii) @@ -428,7 +440,10 @@ BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_start_points_homogenized_patched) for (decltype(function_values.size()) jj = 0; jj < function_values.size(); ++jj) { - BOOST_CHECK(abs(function_values(jj)) < threshold_clearance_mp); + // scale-relative, as in the double-precision loop above + mpfr_float scale = (jj < vs.size()) ? abs(vs[jj]->Value()) : mpfr_float(1); + if (scale < 1) scale = mpfr_float(1); + BOOST_CHECK(abs(function_values(jj)) < scale*threshold_clearance_mp); } } @@ -539,6 +554,7 @@ BOOST_AUTO_TEST_CASE(total_degree_start_system_homogenized_patched_precision_16) BOOST_AUTO_TEST_CASE(quadratic_cubic_quartic_all_the_way_to_final_system) { + bertini::SetGlobalSeed(1u); // deterministic random TotalDegree values; see ADR-0003 bertini::System sys; Var x = Variable::Make("x"), y = Variable::Make("y"), z = Variable::Make("z"); diff --git a/core/test/classes/structural_hash_test.cpp b/core/test/classes/structural_hash_test.cpp new file mode 100644 index 000000000..f4bd92a4b --- /dev/null +++ b/core/test/classes/structural_hash_test.cpp @@ -0,0 +1,182 @@ +//This file is part of Bertini 2. +// +//structural_hash_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//structural_hash_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with structural_hash_test.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file Tests for Node::Hash() and Node::IsSame() -- the order-sensitive, shallow (operands +compared by pointer) structural predicates that the hash-consing intern table will use. +Nothing is wired into construction yet; these just pin the predicate semantics. +*/ + +#include +#include "bertini2/function_tree.hpp" +#include + +using Nd = std::shared_ptr; +using Variable = bertini::node::Variable; +using Integer = bertini::node::Integer; +using Rational = bertini::node::Rational; + +BOOST_AUTO_TEST_SUITE(structural_hash) + +// ---- value leaves: equal-by-value, distinct-by-value ---- + +BOOST_AUTO_TEST_CASE(equal_integers_are_same_and_hash_equal) +{ + auto a = Integer::Make(7); + auto b = Integer::Make(7); + BOOST_CHECK(a->IsSame(*b)); + BOOST_CHECK(b->IsSame(*a)); + BOOST_CHECK_EQUAL(a->Hash(), b->Hash()); +} + +BOOST_AUTO_TEST_CASE(distinct_integers_are_not_same) +{ + auto a = Integer::Make(7); + auto b = Integer::Make(8); + BOOST_CHECK(!a->IsSame(*b)); + // hashes are allowed to collide in principle, but for these they should differ + BOOST_CHECK(a->Hash() != b->Hash()); +} + +BOOST_AUTO_TEST_CASE(integer_and_rational_of_same_value_are_not_same) +{ + auto i = Integer::Make(2); + auto r = Rational::Make("2", "0"); + BOOST_CHECK(!i->IsSame(*r)); // different dynamic type +} + +BOOST_AUTO_TEST_CASE(equal_rationals_are_same) +{ + auto a = Rational::Make("3/4", "0"); + auto b = Rational::Make("3/4", "0"); + BOOST_CHECK(a->IsSame(*b)); + BOOST_CHECK_EQUAL(a->Hash(), b->Hash()); +} + +// ---- variables are identity (until interned by name) ---- + +BOOST_AUTO_TEST_CASE(same_name_variables_are_the_same) +{ + // variables are canonical by name -- two Make("x") are the SAME node. + auto x1 = Variable::Make("x"); + auto x2 = Variable::Make("x"); + BOOST_CHECK(x1->IsSame(*x2)); + BOOST_CHECK_EQUAL(x1.get(), x2.get()); + BOOST_CHECK_EQUAL(x1->Hash(), x2->Hash()); + // different names remain distinct + BOOST_CHECK(!x1->IsSame(*Variable::Make("y"))); +} + +// ---- operators: same structure over shared children -> same ---- + +BOOST_AUTO_TEST_CASE(sums_over_shared_children_are_same) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = x + y; + Nd b = x + y; // same x, y objects + BOOST_CHECK(a->IsSame(*b)); + BOOST_CHECK_EQUAL(a->Hash(), b->Hash()); +} + +BOOST_AUTO_TEST_CASE(commutative_sum_canonicalizes_to_same) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = x + y; + Nd b = y + x; // canonical operand ordering (on by default) -> same node + BOOST_CHECK(a->IsSame(*b)); + BOOST_CHECK_EQUAL(a.get(), b.get()); +} + +BOOST_AUTO_TEST_CASE(sum_vs_difference_differ_by_signs) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = x + y; + Nd b = x - y; // same operands, different signs + BOOST_CHECK(!a->IsSame(*b)); + BOOST_CHECK(a->Hash() != b->Hash()); +} + +BOOST_AUTO_TEST_CASE(mult_vs_divide_differ) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + Nd a = x * y; + Nd b = x / y; + BOOST_CHECK(!a->IsSame(*b)); +} + +BOOST_AUTO_TEST_CASE(interning_collapses_equal_subtrees) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + auto z = Variable::Make("z"); + // With hash-consing, the inner (x+y) is itself interned, so building (x+y)*z + // twice returns the SAME interned object -- you can no longer make distinct-but-equal + // subtrees. (That is the whole point of hash-consing.) + Nd a = (x + y) * z; + Nd b = (x + y) * z; + BOOST_CHECK(a->IsSame(*b)); + BOOST_CHECK_EQUAL(a.get(), b.get()); // literally the same node + BOOST_CHECK_EQUAL(a->Hash(), b->Hash()); +} + +BOOST_AUTO_TEST_CASE(transcendentals_distinguished_by_type) +{ + auto x = Variable::Make("x"); + Nd s = sin(x); + Nd c = cos(x); + Nd s2 = sin(x); + BOOST_CHECK(s->IsSame(*s2)); + BOOST_CHECK_EQUAL(s->Hash(), s2->Hash()); + BOOST_CHECK(!s->IsSame(*c)); // sin vs cos: different concrete unary type + BOOST_CHECK(s->Hash() != c->Hash()); +} + +BOOST_AUTO_TEST_CASE(integer_power_folds_exponent) +{ + auto x = Variable::Make("x"); + Nd a = pow(x, 2); + Nd b = pow(x, 2); + Nd c = pow(x, 3); + BOOST_CHECK(a->IsSame(*b)); + BOOST_CHECK_EQUAL(a->Hash(), b->Hash()); + BOOST_CHECK(!a->IsSame(*c)); // different integer exponent +} + +// ---- interning unifies whole trees; hash is stable across precision ---- + +BOOST_AUTO_TEST_CASE(interning_unifies_whole_trees_and_hash_ignores_precision) +{ + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + // two identical builds -> the same interned node (constants Integer(3) and the operators + // all hash-cons), so there is no longer any distinct-but-equal pair to compare. + Nd f1 = (x + y) * x + Integer::Make(3) * y; + Nd f2 = (x + y) * x + Integer::Make(3) * y; + BOOST_CHECK(f1->IsSame(*f2)); + BOOST_CHECK_EQUAL(f1.get(), f2.get()); + BOOST_CHECK_EQUAL(f1->Hash(), f2->Hash()); +} + +BOOST_AUTO_TEST_SUITE_END() // structural_hash diff --git a/core/test/classes/system_blocks_test.cpp b/core/test/classes/system_blocks_test.cpp new file mode 100644 index 000000000..c97399084 --- /dev/null +++ b/core/test/classes/system_blocks_test.cpp @@ -0,0 +1,192 @@ +//This file is part of Bertini 2. +// +//system_blocks_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//system_blocks_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +#include + +#include +#include +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/system/blocks/products_of_linears_block.hpp" +#include "bertini2/system/blocks/linear_forms_block.hpp" +#include "bertini2/system/blocks/blend_block.hpp" + +BOOST_AUTO_TEST_SUITE(system_blocks_suite) + +using namespace bertini; +using Var = std::shared_ptr; +using bertini::blocks::ProductsOfLinearsBlock; +using bertini::blocks::LinearFormsBlock; +using bertini::blocks::BlendBlock; +using bertini::DefaultPrecision; + +// A block-composed System with one affine variable group {x,y} (unhomogenized, no +// patch) whose single function is the product of linears (2x+3y+1)(x-y+4). +// At (x,y)=(1,1): value = 6*4 = 24; Jacobian = [14, 6]. +static System MakeBlockSystem() +{ + System sys; + Var x = node::Variable::Make("x"), y = node::Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x, y}); + + bertini::Mat f(2, 3); + f << mpfr_complex(2), mpfr_complex(3), mpfr_complex(1), + mpfr_complex(1), mpfr_complex(-1), mpfr_complex(4); + sys.AddBlock(ProductsOfLinearsBlock(2, std::vector>{f})); + return sys; +} + +BOOST_AUTO_TEST_CASE(counts) +{ + DefaultPrecision(30); + auto sys = MakeBlockSystem(); + BOOST_CHECK(sys.HasBlocks()); + BOOST_CHECK_EQUAL(sys.NumNaturalFunctions(), 1u); + BOOST_CHECK_EQUAL(sys.NumVariables(), 2u); + BOOST_CHECK_EQUAL(sys.NumTotalFunctions(), 1u); // no patch +} + +BOOST_AUTO_TEST_CASE(eval_double) +{ + DefaultPrecision(30); + auto sys = MakeBlockSystem(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + auto v = sys.Eval(x); + + BOOST_CHECK_EQUAL(v.size(), 1); + BOOST_CHECK_CLOSE(v(0).real(), 24.0, 1e-11); + BOOST_CHECK_SMALL(v(0).imag(), 1e-11); +} + +BOOST_AUTO_TEST_CASE(jacobian_double) +{ + DefaultPrecision(30); + auto sys = MakeBlockSystem(); + + bertini::Vec x(2); x << dbl(1), dbl(1); + sys.Eval(x); // sets the current variable values + auto J = sys.Jacobian(); // uses the current variable values + + BOOST_CHECK_EQUAL(J.rows(), 1); + BOOST_CHECK_EQUAL(J.cols(), 2); + BOOST_CHECK_CLOSE(J(0, 0).real(), 14.0, 1e-11); + BOOST_CHECK_CLOSE(J(0, 1).real(), 6.0, 1e-11); +} + +BOOST_AUTO_TEST_CASE(eval_mpfr) +{ + DefaultPrecision(30); + auto sys = MakeBlockSystem(); + sys.precision(30); // propagate precision to variables and blocks + + bertini::Vec x(2); x << mpfr_complex(1), mpfr_complex(1); + auto v = sys.Eval(x); + + BOOST_CHECK(abs(v(0) - mpfr_complex(24)) < mpfr_float("1e-25")); +} + +// round-trip a System through a boost text archive, returning the deserialized copy. +static System RoundTrip(System const& sys) +{ + std::stringstream ss; + { + boost::archive::text_oarchive oa(ss); + oa << sys; + } + System out; + { + boost::archive::text_iarchive ia(ss); + ia >> out; + } + return out; +} + +BOOST_AUTO_TEST_CASE(serialize_products_of_linears_block) +{ + DefaultPrecision(30); + auto sys = MakeBlockSystem(); + bertini::Vec x(2); x << dbl(1), dbl(1); + auto before = sys.Eval(x); + + auto sys2 = RoundTrip(sys); + BOOST_REQUIRE(sys2.HasBlocks()); + auto after = sys2.Eval(x); + + BOOST_CHECK_EQUAL(after.size(), before.size()); + BOOST_CHECK_CLOSE(after(0).real(), before(0).real(), 1e-11); // 24 +} + +BOOST_AUTO_TEST_CASE(serialize_linear_forms_block) +{ + DefaultPrecision(30); + System sys; + Var x = node::Variable::Make("x"), y = node::Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x, y}); + // f0 = 2x + 3y + 1, f1 = x - y + 4 (augmented rows) + bertini::Mat M(2, 3); + M << mpfr_complex(2), mpfr_complex(3), mpfr_complex(1), + mpfr_complex(1), mpfr_complex(-1), mpfr_complex(4); + sys.AddBlock(LinearFormsBlock(2, M)); + + bertini::Vec p(2); p << dbl(1), dbl(1); + auto before = sys.Eval(p); // [6, 4] + + auto sys2 = RoundTrip(sys); + BOOST_REQUIRE(sys2.HasBlocks()); + auto after = sys2.Eval(p); + + BOOST_REQUIRE_EQUAL(after.size(), 2); + BOOST_CHECK_CLOSE(after(0).real(), before(0).real(), 1e-11); + BOOST_CHECK_CLOSE(after(1).real(), before(1).real(), 1e-11); +} + +BOOST_AUTO_TEST_CASE(serialize_blend_block) +{ + DefaultPrecision(30); + // H(x,t) = (1-t)(x-2) + t(x-5) = x - 2 - 3t, blending two single-function systems. + auto t = node::Variable::Make("t"); + auto x = node::Variable::Make("x"); + + auto target = std::make_shared(); + target->AddVariableGroup(VariableGroup{x}); + target->AddFunction(x - node::Integer::Make(2)); + + auto start = std::make_shared(); + start->AddVariableGroup(VariableGroup{x}); + start->AddFunction(x - node::Integer::Make(5)); + + System H; + H.AddVariableGroup(VariableGroup{x}); + H.AddPathVariable(t); + std::vector> coeffs{ node::Integer::Make(1) - t, t }; + std::vector> operands{ target, start }; + H.AddBlock(BlendBlock(t, std::move(coeffs), std::move(operands))); + + bertini::Vec p(1); p << dbl(1); + auto before = H.Eval(p, dbl(0)); // x - 2 - 0 = -1 + + auto H2 = RoundTrip(H); + BOOST_REQUIRE(H2.HasBlocks()); + auto after = H2.Eval(p, dbl(0)); + + BOOST_REQUIRE_EQUAL(after.size(), 1); + BOOST_CHECK_CLOSE(after(0).real(), before(0).real(), 1e-9); // -1 +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/system_printing_test.cpp b/core/test/classes/system_printing_test.cpp new file mode 100644 index 000000000..313d280ce --- /dev/null +++ b/core/test/classes/system_printing_test.cpp @@ -0,0 +1,122 @@ +//This file is part of Bertini 2. +// +//system_printing_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//system_printing_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with this file. If not, see . +// +// Copyright(C) Bertini2 Development Team + +/** +\file system_printing_test.cpp + +\brief Human-facing printing of block-composed systems (System::Describe / operator<<): terse +placeholders by default, actual numbers + underlying functions when verbose, and none of the old +debugging noise. +*/ + +#include +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/system/blocks/block.hpp" + +BOOST_AUTO_TEST_SUITE(system_printing_suite) + +using namespace bertini; +using bertini::node::Variable; + +static std::string Terse(System const& s) { std::ostringstream ss; s.Describe(ss, false); return ss.str(); } +static std::string Verbose(System const& s) { std::ostringstream ss; s.Describe(ss, true); return ss.str(); } +static bool Has(std::string const& hay, std::string const& needle) { return hay.find(needle) != std::string::npos; } + +// none of the old debugging leftovers should appear. +static void NoNoise(std::string const& t) +{ + BOOST_CHECK(!Has(t, "current variable values")); + BOOST_CHECK(!Has(t, "differentiated")); + BOOST_CHECK(!Has(t, "unnamed_function")); +} + + +BOOST_AUTO_TEST_CASE(plain_polynomial) +{ + DefaultPrecision(30); + auto x = Variable::Make("x"), y = Variable::Make("y"); + System s; s.AddVariableGroup(VariableGroup{x, y}); + s.AddFunction(x*x + y*y - node::Integer::Make(1)); + s.AddFunction(x - y); + + std::string t = Terse(s); + BOOST_CHECK(Has(t, "f_0 = ")); + BOOST_CHECK(Has(t, "f_1 = ")); + NoNoise(t); +} + +BOOST_AUTO_TEST_CASE(randomization_placeholder_and_underlying) +{ + DefaultPrecision(30); + auto x = Variable::Make("x"), y = Variable::Make("y"); + System o; o.AddVariableGroup(VariableGroup{x, y}); + o.AddFunction(x*x + y*y - node::Integer::Make(1)); + o.AddFunction(x*y); + o.AddFunction(x*x + y*y - x - y); + System r = o.Randomize(); + + std::string t = Terse(r); + BOOST_CHECK(Has(t, "R . g")); + BOOST_CHECK(Has(t, "(R: 2x3 randomization matrix)")); + BOOST_CHECK(Has(t, "g_0 = ")); + BOOST_CHECK(Has(t, "g_2 = ")); + BOOST_CHECK(!Has(t, "R =\n")); // the matrix is not in the terse form + NoNoise(t); + + std::string v = Verbose(r); + BOOST_CHECK(Has(v, "R =")); // ... but it is in verbose +} + +BOOST_AUTO_TEST_CASE(linear_forms_placeholder_vs_actual) +{ + DefaultPrecision(30); + auto x = Variable::Make("x"), y = Variable::Make("y"); + System m; m.AddVariableGroup(VariableGroup{x, y}); + m.AddFunction(x*x + y*y - node::Integer::Make(1)); // row 0: polynomial + Mat M(1, 3); M << mpfr_complex(2), mpfr_complex(1), mpfr_complex(-1); + m.AddBlock(blocks::LinearFormsBlock(2, M)); // row 1: 2x + y - 1 + + std::string t = Terse(m); + BOOST_CHECK(Has(t, "f_0 = ")); + BOOST_CHECK(Has(t, "f_1 = c.[x, y, 1]")); // both rows visible; structured row is a placeholder + + std::string v = Verbose(m); + BOOST_CHECK(Has(v, "*x") && Has(v, "*y")); // actual coefficients shown +} + +BOOST_AUTO_TEST_CASE(moving_homotopy_blend) +{ + DefaultPrecision(30); + auto x = Variable::Make("x"), y = Variable::Make("y"); + System fixed; fixed.AddVariableGroup(VariableGroup{x, y}); fixed.AddFunction(x*x + y*y - node::Integer::Make(1)); + System sm; sm.AddVariableGroup(VariableGroup{x, y}); sm.AddFunction(y); + System em; em.AddVariableGroup(VariableGroup{x, y}); em.AddFunction(y - x); + System H = MakeMovingHomotopy(fixed, sm, em, "t", node::Float::Make(mpfr_complex("0.6", "0.8"))); + + std::string t = Terse(H); + BOOST_CHECK(Has(t, "f_0 = ")); // the fixed polynomial row + BOOST_CHECK(Has(t, "blend of 2 systems")); + BOOST_CHECK(Has(t, "path variable: t")); + BOOST_CHECK(!Has(t, "f_1..f_1")); // single moving row reads f_1 + + std::string v = Verbose(H); + BOOST_CHECK(Has(v, "A_0 = ") && Has(v, "B_0 = ")); // operand functions listed +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classes/system_test.cpp b/core/test/classes/system_test.cpp index bde326dcf..6be1038a2 100644 --- a/core/test/classes/system_test.cpp +++ b/core/test/classes/system_test.cpp @@ -35,6 +35,8 @@ +#include + #include "bertini2/system/system.hpp" #include "bertini2/system/precon.hpp" #include "bertini2/io/parsing/system_parsers.hpp" @@ -80,6 +82,58 @@ BOOST_AUTO_TEST_CASE(system_create_parser) } +/** +\class bertini::System +\test \b parsed_system_has_functions_and_evaluates A parsed system must actually contain its +functions and evaluate them. (Regression: when classic functions became eager-bound, a parse +path that skipped the post-parse emit produced a silently empty, size-0 system -- the earlier +parse tests only checked parse success / homogeneity, never the function count or a value.) +*/ +BOOST_AUTO_TEST_CASE(parsed_system_has_functions_and_evaluates) +{ + System sys; + std::string str = "variable_group x, y; function f, g; f = x*y; g = x + y;"; + bool ok = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + + BOOST_CHECK(ok); + BOOST_CHECK_EQUAL(sys.NumNaturalFunctions(), 2u); + + Vec pt(2); pt << dbl(2,0), dbl(3,0); + auto v = sys.Eval(pt); + BOOST_REQUIRE_EQUAL(v.size(), 2); + BOOST_CHECK_SMALL(std::abs(v(0) - dbl(6,0)), 1e-12); // x*y at (2,3) + BOOST_CHECK_SMALL(std::abs(v(1) - dbl(5,0)), 1e-12); // x+y at (2,3) +} + + +/** +\class bertini::System +\test \b parsed_subfunction_is_a_named_expression A `s = expr` subfunction parses to an immutable +NamedExpression embedded in the function that references it; the System evaluates correctly, prints +the function with `s` by name, and lists the named subexpression's value below (discovered, not stored). +*/ +BOOST_AUTO_TEST_CASE(parsed_subfunction_is_a_named_expression) +{ + System sys; + std::string str = "variable_group x, y; function f; s = x*y; f = s + x;"; + bool ok = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + + BOOST_CHECK(ok); + BOOST_CHECK_EQUAL(sys.NumNaturalFunctions(), 1u); + + // f = s + x = x*y + x; at (2,3) -> 6 + 2 = 8 + Vec pt(2); pt << dbl(2,0), dbl(3,0); + auto v = sys.Eval(pt); + BOOST_REQUIRE_EQUAL(v.size(), 1); + BOOST_CHECK_SMALL(std::abs(v(0) - dbl(8,0)), 1e-12); + + std::stringstream ss; ss << sys; + const std::string text = ss.str(); + BOOST_CHECK(text.find("named subexpression") != std::string::npos); // discovered + shown + BOOST_CHECK(text.find("s = x*y") != std::string::npos); // its value +} + + @@ -386,7 +440,7 @@ BOOST_AUTO_TEST_CASE(system_evaluate_double) std::string str = "function f; variable_group x1, x2; y = x1*x2; f = y*y;"; bertini::System sys; - bool s = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + [[maybe_unused]] bool s = bertini::parsing::classic::parse(str.begin(), str.end(), sys); Vec values(2); @@ -419,7 +473,7 @@ BOOST_AUTO_TEST_CASE(system_evaluate_mpfr) std::string str = "function f; variable_group x1, x2; y = x1*x2; f = y*y;"; bertini::System sys; - bool s = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + [[maybe_unused]] bool s = bertini::parsing::classic::parse(str.begin(), str.end(), sys); Vec values(2); @@ -447,6 +501,12 @@ BOOST_AUTO_TEST_CASE(system_jacobian) auto y = Variable::Make("y"); auto z = Variable::Make("z"); + // Modest magnitudes keep the high-degree SLP-vs-analytic rounding comfortably inside the + // 1e-15 tolerance below. + dbl a(0.5, 0.25); + dbl b(0.4, -0.30); + dbl c(0.6, 0.20); + System sys; sys.AddVariableGroup(VariableGroup{x, y, z}); @@ -454,10 +514,6 @@ BOOST_AUTO_TEST_CASE(system_jacobian) sys.AddFunction(pow(x,2)*pow(y,3)*pow(z,4) + 1); sys.AddFunction(pow(x,3)*pow(y,4)*pow(z,5) + 4); - auto a = x->Eval(); - auto b = y->Eval(); - auto c = z->Eval(); - Vec v(3); v << a, b, c; @@ -584,7 +640,6 @@ BOOST_AUTO_TEST_CASE(add_system_to_self_doubles_under_function_tree_eval) sys.AddVariableGroup(vars); sys.AddFunction(y+1); sys.AddFunction(x*y); - sys.SetEvalMethod(bertini::EvalMethod::FunctionTree); Vec values(2); values << dbl(2.0), dbl(3.0); @@ -987,6 +1042,104 @@ BOOST_AUTO_TEST_CASE(system_dehomogenize_FIFO_one_hom_group) } +/** +\class bertini::System +\test \b system_homogenize_point_unhomogenized_is_identity HomogenizePoint on an unhomogenized, unpatched system returns the point unchanged. +*/ +BOOST_AUTO_TEST_CASE(system_homogenize_point_unhomogenized_is_identity) +{ + bertini::System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + VariableGroup vars{x, y}; + sys.AddVariableGroup(vars); + + Vec p(2); + p << dbl(3,4), dbl(4,5); + + auto h = sys.HomogenizePoint(p); + + BOOST_CHECK_EQUAL(h.size(),2); + BOOST_CHECK(abs(h(0) - p(0)) < threshold_clearance_d); + BOOST_CHECK(abs(h(1) - p(1)) < threshold_clearance_d); +} + + +/** +\class bertini::System +\test \b system_homogenize_point_FIFO_one_aff_group HomogenizePoint inserts the homogenizing coordinate with value 1, and dehomogenization inverts it. +*/ +BOOST_AUTO_TEST_CASE(system_homogenize_point_FIFO_one_aff_group) +{ + bertini::System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + VariableGroup vars{x, y}; + sys.AddVariableGroup(vars); + + sys.Homogenize(); + + Vec p(2); + p << dbl(3,4), dbl(4,5); + + auto h = sys.HomogenizePoint(p); + + BOOST_CHECK_EQUAL(h.size(),3); + BOOST_CHECK(abs(h(0) - dbl(1)) < threshold_clearance_d); + BOOST_CHECK(abs(h(1) - p(0)) < threshold_clearance_d); + BOOST_CHECK(abs(h(2) - p(1)) < threshold_clearance_d); + + auto p_again = sys.DehomogenizePoint(h); + BOOST_CHECK_EQUAL(p_again.size(),2); + BOOST_CHECK(abs(p_again(0) - p(0)) < threshold_clearance_d); + BOOST_CHECK(abs(p_again(1) - p(1)) < threshold_clearance_d); +} + + +/** +\class bertini::System +\test \b system_homogenize_point_FIFO_mixed_groups HomogenizePoint handles affine groups, hom groups, and dehomogenization inverts it, with multiple group types in play. +*/ +BOOST_AUTO_TEST_CASE(system_homogenize_point_FIFO_mixed_groups) +{ + bertini::System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + Var z = Variable::Make("z"), w = Variable::Make("w"); + Var h1 = Variable::Make("h1"), h2 = Variable::Make("h2"); + VariableGroup vars{x, y}; + VariableGroup vars2{h1,h2}; + VariableGroup vars3{z, w}; + sys.AddVariableGroup(vars); + sys.AddHomVariableGroup(vars2); + sys.AddVariableGroup(vars3); + + sys.Homogenize(); + + Vec p(6); + p << dbl(3,4), dbl(4,5), + dbl(10,11), dbl(11,12), + dbl(6,7), dbl(7,8); + + auto h = sys.HomogenizePoint(p); + + BOOST_CHECK_EQUAL(h.size(),8); + // [hom0, x, y, hom-group passthrough, hom1, z, w] + BOOST_CHECK(abs(h(0) - dbl(1)) < threshold_clearance_d); + BOOST_CHECK(abs(h(1) - p(0)) < threshold_clearance_d); + BOOST_CHECK(abs(h(2) - p(1)) < threshold_clearance_d); + BOOST_CHECK(abs(h(3) - p(2)) < threshold_clearance_d); + BOOST_CHECK(abs(h(4) - p(3)) < threshold_clearance_d); + BOOST_CHECK(abs(h(5) - dbl(1)) < threshold_clearance_d); + BOOST_CHECK(abs(h(6) - p(4)) < threshold_clearance_d); + BOOST_CHECK(abs(h(7) - p(5)) < threshold_clearance_d); + + auto p_again = sys.DehomogenizePoint(h); + BOOST_CHECK_EQUAL(p_again.size(),6); + for (int ii = 0; ii < 6; ++ii) + BOOST_CHECK(abs(p_again(ii) - p(ii)) < threshold_clearance_d); +} + + + + /** \class bertini::System \test \b system_dehomogenize_FIFO_one_hom_group_two_ungrouped_vars Test the dehomogenization of a point using the first-in-first-out variable ordering which is standard in Bertini 1. @@ -1130,6 +1283,11 @@ BOOST_AUTO_TEST_CASE(system_estimate_coeff_bound_homogenized_quartic) { bertini::DefaultPrecision(CLASS_TEST_MPFR_DEFAULT_DIGITS); + // AutoPatch coefficients come from RandomMp; seeding makes this test's draws + // (and hence the <10 threshold below) deterministic regardless of which other + // tests ran first. RandomMp now honors SetGlobalSeed (it shares ThreadEngine). + bertini::SetGlobalSeed(1u); + bertini::System sys; Var x = Variable::Make("x"), y = Variable::Make("y"), z = Variable::Make("z"); @@ -1148,6 +1306,47 @@ BOOST_AUTO_TEST_CASE(system_estimate_coeff_bound_homogenized_quartic) BOOST_CHECK(coefficient_bound > mpfr_float("2")); } + +/** +\class bertini::System +\test \b system_homogenize_point_lands_on_patch On a patched system, HomogenizePoint produces a point ON the patch (rescaling it again is the identity), and dehomogenizing recovers the user point. +*/ +BOOST_AUTO_TEST_CASE(system_homogenize_point_lands_on_patch) +{ + bertini::System sys; + Var x = Variable::Make("x"), y = Variable::Make("y"); + VariableGroup vars{x, y}; + sys.AddVariableGroup(vars); + + sys.Homogenize(); + sys.AutoPatch(); + + Vec p(2); + p << dbl(3,4), dbl(4,5); + + auto h = sys.HomogenizePoint(p); + BOOST_CHECK_EQUAL(h.size(),3); + + // already on the patch: rescaling is the identity + auto h_rescaled = sys.RescalePointToFitPatch(h); + for (int ii = 0; ii < 3; ++ii) + BOOST_CHECK(abs(h_rescaled(ii) - h(ii)) < threshold_clearance_d); + + // projectively the same point: dehomogenizing recovers the user coordinates + auto p_again = sys.DehomogenizePoint(h); + BOOST_CHECK(abs(p_again(0) - p(0)) < threshold_clearance_d); + BOOST_CHECK(abs(p_again(1) - p(1)) < threshold_clearance_d); + + // and the round trip from an on-patch internal point is exact: + // Hom(Dehom(s)) == s for s on the patch + Vec v(3); + v << dbl(2,3), dbl(3,4), dbl(4,5); + auto s = sys.RescalePointToFitPatch(v); + auto s_again = sys.HomogenizePoint(sys.DehomogenizePoint(s)); + for (int ii = 0; ii < 3; ++ii) + BOOST_CHECK(abs(s_again(ii) - s(ii)) < threshold_clearance_d); +} + /** \class bertini::System \test \b system_estimate_degree_bound_linear Test the estimation of the degree in a system, including its derivatives. @@ -1255,6 +1454,38 @@ BOOST_AUTO_TEST_CASE(concatenate_two_systems) BOOST_CHECK_EQUAL(sys3.NumNaturalFunctions(),6); } +/** +\class bertini::System +\test \b concatenate_copies_patch_from_patched_operand When exactly one operand is patched, +Concatenate must give the result that operand's patch. Regression: Concatenate used to call +sys1.CopyPatches(sys1) -- copying patches from itself, a no-op -- so the patch was silently +dropped and the result came out unpatched. +*/ +BOOST_AUTO_TEST_CASE(concatenate_copies_patch_from_patched_operand) +{ + bertini::SetGlobalSeed(1u); // AutoPatch draws random patch coefficients + + Var x = Variable::Make("x"), y = Variable::Make("y"); + VariableGroup vars{x,y}; + + bertini::System base; + base.AddVariableGroup(vars); + base.AddFunction(x*x + y*y - 1); + base.Homogenize(); + + // Two systems with identical variable ordering (clones share the node DAG), only one patched. + auto sys1 = Clone(base); // unpatched + auto sys2 = Clone(base); + sys2.AutoPatch(); // patched + + BOOST_CHECK(!sys1.IsPatched()); + BOOST_CHECK(sys2.IsPatched()); + + auto sys3 = Concatenate(sys1, sys2); + BOOST_CHECK(sys3.IsPatched()); + BOOST_CHECK(sys3.GetPatch() == sys2.GetPatch()); +} + /** \class bertini::System \test \b parsed_system_evaluates_correctly @@ -1265,7 +1496,7 @@ BOOST_AUTO_TEST_CASE(parsed_system_evaluates_correctly) std::string str = "function f; variable_group x1, x2; y = x1*x2; f = y*y;"; bertini::System sys; - bool s = bertini::parsing::classic::parse(str.begin(), str.end(), sys); + [[maybe_unused]] bool s = bertini::parsing::classic::parse(str.begin(), str.end(), sys); Vec values(2); @@ -1375,6 +1606,84 @@ BOOST_AUTO_TEST_CASE(clone_system_new_variables_evaluation) BOOST_CHECK_EQUAL(f_clone2,f2); } + + +/** +\class bertini::node +\test \b gather_variables_alphabetical GatherVariables returns the distinct +variables of a set of functions, de-duplicated and sorted alphabetically by name. +*/ +BOOST_AUTO_TEST_CASE(gather_variables_alphabetical) +{ + Var x = Variable::Make("x"); + Var y = Variable::Make("y"); + Var z = Variable::Make("z"); + + // note: declared out of alphabetical order, x used twice + auto f1 = (pow(z,2) + y*x); + auto f2 = (x - y); + + auto found = bertini::node::GatherVariables(std::vector>{f1, f2}); + + BOOST_CHECK_EQUAL(found.size(), 3); + BOOST_CHECK_EQUAL(found[0]->name(), "x"); + BOOST_CHECK_EQUAL(found[1]->name(), "y"); + BOOST_CHECK_EQUAL(found[2]->name(), "z"); +} + + +/** +\class bertini::System +\test \b system_construct_from_functions Constructing a System from a list of +functions auto-discovers the variables into a single affine variable group. +*/ +BOOST_AUTO_TEST_CASE(system_construct_from_functions) +{ + Var x = Variable::Make("x"); + Var y = Variable::Make("y"); + Var z = Variable::Make("z"); + + auto f1 = (x*y*z); + auto f2 = (x + y + z); + + bertini::System sys(std::vector>{f1, f2}); + + BOOST_CHECK_EQUAL(sys.NumTotalFunctions(), 2); + BOOST_CHECK_EQUAL(sys.NumVariableGroups(), 1); + BOOST_CHECK_EQUAL(sys.NumVariables(), 3); + + auto const& ordering = sys.Variables(); + BOOST_CHECK_EQUAL(ordering[0]->name(), "x"); + BOOST_CHECK_EQUAL(ordering[1]->name(), "y"); + BOOST_CHECK_EQUAL(ordering[2]->name(), "z"); +} + + +/** +\class bertini::System +\test \b system_set_variable_groups SetVariableGroups replaces the whole +variable-group structure with the supplied affine groups. +*/ +BOOST_AUTO_TEST_CASE(system_set_variable_groups) +{ + Var x = Variable::Make("x"); + Var y = Variable::Make("y"); + Var z = Variable::Make("z"); + + auto f1 = (x*y*z); + + bertini::System sys(std::vector>{f1}); + BOOST_CHECK_EQUAL(sys.NumVariableGroups(), 1); + + bertini::VariableGroup g1{x}; + bertini::VariableGroup g2{y, z}; + sys.SetVariableGroups(std::vector{g1, g2}); + + BOOST_CHECK_EQUAL(sys.NumVariableGroups(), 2); + BOOST_CHECK_EQUAL(sys.NumVariables(), 3); +} + + BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/classic/classic_parsing_test.cpp b/core/test/classic/classic_parsing_test.cpp index d2a53cb26..65f490cc6 100644 --- a/core/test/classic/classic_parsing_test.cpp +++ b/core/test/classic/classic_parsing_test.cpp @@ -27,6 +27,8 @@ #include "bertini2/bertini.hpp" #include +#include +#include #include #include @@ -616,7 +618,32 @@ BOOST_AUTO_TEST_CASE(test_split_and_uncomment) BOOST_CHECK(input.find("variable_group x,y;")!=std::string::npos); BOOST_CHECK(input.find("f = x^2 + y;")!=std::string::npos); - + +} + + +// The classic WRITER is the inverse of the parser: emitting a system to classic syntax and parsing +// it back must reconstruct an equivalent system (so a Bertini 1 run sees the same problem). +BOOST_AUTO_TEST_CASE(classic_writer_round_trips_a_system) +{ + using namespace bertini; + auto x = node::Variable::Make("x"); + auto y = node::Variable::Make("y"); + System sys; + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddFunction(x*x + y*y - 1); + sys.AddFunction(x - y); + + System reparsed{ classic::SystemToClassic(sys) }; + BOOST_CHECK_EQUAL(reparsed.NumNaturalFunctions(), sys.NumNaturalFunctions()); + + // identical values at a generic point -> the emitted classic text is a faithful round-trip + Vec pt(2); pt << dbl(0.3, 0.7), dbl(-0.4, 0.2); + auto a = sys.Eval(pt); + auto b = reparsed.Eval(pt); + BOOST_REQUIRE_EQUAL(a.size(), b.size()); + for (Eigen::Index i = 0; i < a.size(); ++i) + BOOST_CHECK_SMALL(abs(a(i) - b(i)), 1e-12); } diff --git a/core/test/endgames/generic_cauchy_test.hpp b/core/test/endgames/generic_cauchy_test.hpp index 8148eed67..faa97e82e 100644 --- a/core/test/endgames/generic_cauchy_test.hpp +++ b/core/test/endgames/generic_cauchy_test.hpp @@ -51,14 +51,14 @@ using mpq_rational = bertini::mpq_rational; using bertini::Precision; -template using Vec = Eigen::Matrix; -template using Mat = Eigen::Matrix; +template using Vec = Eigen::Matrix; +template using Mat = Eigen::Matrix; using PrecisionConfig = bertini::tracking::TrackerTraits::PrecisionConfig; -using BRT = bertini::tracking::TrackerTraits::BaseRealType; -using BCT = bertini::tracking::TrackerTraits::BaseComplexType; +using BRT = bertini::tracking::TrackerTraits::BaseRealT; +using BCT = bertini::tracking::TrackerTraits::BaseComplexT; using SuccessCode = bertini::SuccessCode; @@ -127,7 +127,7 @@ BOOST_AUTO_TEST_CASE(circle_track_cycle_num_1) cauchy_samples.back() << ComplexFromString("7.999999999999999e-01", "2.168404344971009e-19"); // - auto first_track_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto first_track_success = my_endgame.CircleTrack(origin); BOOST_CHECK((my_endgame.GetCauchySamples().back() - cauchy_samples.front()).template lpNorm() < 1e-5); @@ -192,7 +192,7 @@ BOOST_AUTO_TEST_CASE(circle_track_cycle_num_greater_than_1) my_endgame.SetCauchySamples(cauchy_samples); my_endgame.SetCauchyTimes(cauchy_times); - auto tracking_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto tracking_success = my_endgame.CircleTrack(origin); const auto& first_track_sample = my_endgame.GetCauchySamples().back(); @@ -262,7 +262,7 @@ BOOST_AUTO_TEST_CASE(circle_track__nonzero_target_time) my_endgame.SetCauchySamples(cauchy_samples); my_endgame.SetCauchyTimes(cauchy_times); - auto tracking_success = my_endgame.CircleTrack(center); + [[maybe_unused]] auto tracking_success = my_endgame.CircleTrack(center); const auto& first_track_sample = my_endgame.GetCauchySamples().back(); @@ -382,12 +382,13 @@ BOOST_AUTO_TEST_CASE(stabilization_of_C_over_K) sys.AddFunction( pow(x-1,3)*(1-t) + (pow(x,3) + 1)*t); VariableGroup vars{x}; - sys.AddVariableGroup(vars); + sys.AddVariableGroup(vars); + sys.AddPathVariable(t); auto precision_config = PrecisionConfig(sys); TrackerType tracker(sys); - + bertini::tracking::SteppingConfig stepping_preferences; bertini::tracking::NewtonConfig newton_preferences; @@ -532,7 +533,7 @@ BOOST_AUTO_TEST_CASE(check_closed_loop_for_cycle_num_1) my_endgame.SetCauchyTimes(cauchy_times); - auto tracking_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto tracking_success = my_endgame.CircleTrack(origin); BOOST_CHECK(my_endgame.CheckClosedLoop() == true); } // end check closed loop if cycle num is 1 for cauchy class test @@ -594,7 +595,7 @@ BOOST_AUTO_TEST_CASE(check_closed_loop_for_cycle_num_greater_than_1) my_endgame.SetCauchySamples(cauchy_samples); my_endgame.SetCauchyTimes(cauchy_times); - auto tracking_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto tracking_success = my_endgame.CircleTrack(origin); BOOST_CHECK(my_endgame.CheckClosedLoop() == false); tracking_success = my_endgame.CircleTrack(origin); @@ -662,7 +663,7 @@ BOOST_AUTO_TEST_CASE(compare_cauchy_ratios) my_endgame.SetCauchyTimes(cauchy_times); my_endgame.SetCauchySamples(cauchy_samples); - auto tracking_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto tracking_success = my_endgame.CircleTrack(origin); BOOST_CHECK(my_endgame.RatioEGOperatingZoneTest(origin) == true); } // end compare cauchy ratios for cauchy class test @@ -724,7 +725,7 @@ BOOST_AUTO_TEST_CASE(compare_cauchy_ratios_cycle_num_greater_than_1) my_endgame.SetCauchyTimes(cauchy_times); my_endgame.SetCauchySamples(cauchy_samples); - auto tracking_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto tracking_success = my_endgame.CircleTrack(origin); BOOST_CHECK(my_endgame.RatioEGOperatingZoneTest(origin) == true); } // end compare cauchy ratios for cycle num greater than 1 cauchy class test @@ -1032,7 +1033,7 @@ BOOST_AUTO_TEST_CASE(first_approximation_nonzero_target_time) #endif - auto first_approx_success = my_endgame.InitialApproximation(start_time,start_sample,target_time,first_approx); + [[maybe_unused]] auto first_approx_success = my_endgame.InitialApproximation(start_time,start_sample,target_time,first_approx); BOOST_CHECK((first_approx - x_to_check_against).template lpNorm() < 1e-2); BOOST_CHECK(my_endgame.CycleNumber() == 1); @@ -1098,12 +1099,12 @@ BOOST_AUTO_TEST_CASE(compute_cauchy_approximation_cycle_num_1) my_endgame.SetCauchyTimes(cauchy_times); my_endgame.SetCauchySamples(cauchy_samples); - auto first_track_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto first_track_success = my_endgame.CircleTrack(origin); my_endgame.CycleNumber(1); // manually set cycle number to 1 for this test Vec first_cauchy_approx; - auto code = my_endgame.ComputeCauchyApproximationOfXAtT0(first_cauchy_approx); + [[maybe_unused]] auto code = my_endgame.ComputeCauchyApproximationOfXAtT0(first_cauchy_approx); BOOST_CHECK((first_cauchy_approx - x_origin).template lpNorm() < 1e-5); @@ -1171,13 +1172,13 @@ BOOST_AUTO_TEST_CASE(compute_cauchy_approximation_cycle_num_greater_than_1) my_endgame.SetCauchyTimes(cauchy_times); my_endgame.SetCauchySamples(cauchy_samples); - auto first_track_success = my_endgame.CircleTrack(origin); - auto second_track_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto first_track_success = my_endgame.CircleTrack(origin); + [[maybe_unused]] auto second_track_success = my_endgame.CircleTrack(origin); my_endgame.CycleNumber(2); Vec first_cauchy_approx; - auto code = my_endgame.ComputeCauchyApproximationOfXAtT0(first_cauchy_approx); + [[maybe_unused]] auto code = my_endgame.ComputeCauchyApproximationOfXAtT0(first_cauchy_approx); BOOST_CHECK((first_cauchy_approx - x_origin).template lpNorm() < 1e-5); }// end compute_cauchy_approximation_cycle_num_greater_than_1 @@ -1234,7 +1235,7 @@ BOOST_AUTO_TEST_CASE(cauchy_samples_cycle_num_1) TestedEGType my_endgame(tracker); my_endgame.AddToPSData(time, sample); - auto finding_cauchy_samples_success = my_endgame.ComputeCauchySamples(origin); + [[maybe_unused]] auto finding_cauchy_samples_success = my_endgame.ComputeCauchySamples(origin); BOOST_CHECK((my_endgame.GetCauchySamples().back() - my_endgame.GetCauchySamples().front()).template lpNorm() < 1e-5); BOOST_CHECK(my_endgame.GetCauchySamples().size() == 4); @@ -1293,7 +1294,7 @@ BOOST_AUTO_TEST_CASE(find_cauchy_samples_cycle_num_greater_than_1) TestedEGType my_endgame(tracker); my_endgame.AddToPSData(time, sample); - auto finding_cauchy_samples_success = my_endgame.ComputeCauchySamples(origin); + [[maybe_unused]] auto finding_cauchy_samples_success = my_endgame.ComputeCauchySamples(origin); BOOST_CHECK((my_endgame.GetCauchySamples().back() - my_endgame.GetCauchySamples().front()).template lpNorm() < 1e-6); BOOST_CHECK_EQUAL(my_endgame.GetCauchySamples().size(), 7); @@ -1352,13 +1353,14 @@ BOOST_AUTO_TEST_CASE(full_test_cycle_num_1) solution << BCT(1,0); TestedEGType my_endgame(tracker); + my_endgame.SetBoundaryTime(time); #ifdef B2_OBSERVE_TRACKERS bertini::tracking::GoryDetailLogger tons_of_detail; tracker.AddObserver(tons_of_detail); #endif - auto cauchy_endgame_success = my_endgame.Run(time,sample); + [[maybe_unused]] auto cauchy_endgame_success = my_endgame.Run(sample); BOOST_CHECK((my_endgame.FinalApproximation() - solution).template lpNorm() < 1e-5); BOOST_CHECK_EQUAL(my_endgame.CycleNumber(), 1); @@ -1413,8 +1415,9 @@ BOOST_AUTO_TEST_CASE(full_test_cycle_num_greater_than_1) TestedEGType my_endgame(tracker); + my_endgame.SetBoundaryTime(time); - auto cauchy_endgame_success = my_endgame.Run(time,sample); + auto cauchy_endgame_success = my_endgame.Run(sample); #ifdef B2_OBSERVE_TRACKERS bertini::tracking::GoryDetailLogger tons_of_detail; @@ -1480,7 +1483,8 @@ BOOST_AUTO_TEST_CASE(cauchy_endgame_test_cycle_num_greater_than_1_base) TestedEGType my_endgame(tracker); - auto cauchy_endgame_success = my_endgame.Run(time,sample); + my_endgame.SetBoundaryTime(time); + auto cauchy_endgame_success = my_endgame.Run(sample); #ifdef B2_OBSERVE_TRACKERS bertini::tracking::GoryDetailLogger tons_of_detail; @@ -1540,13 +1544,14 @@ BOOST_AUTO_TEST_CASE(cauchy_multiple_variables) SecurityConfig security_settings; TestedEGType my_endgame(tracker,cauchy_settings,endgame_settings,security_settings); + my_endgame.SetBoundaryTime(current_time); #ifdef B2_OBSERVE_TRACKERS bertini::tracking::GoryDetailLogger tons_of_detail; tracker.AddObserver(tons_of_detail); #endif - auto code = my_endgame.Run(current_time,current_space); + auto code = my_endgame.Run(current_space); BOOST_CHECK(code == SuccessCode::Success); BOOST_CHECK((my_endgame.FinalApproximation() - correct).template lpNorm() < 1e-11); @@ -1677,8 +1682,10 @@ BOOST_AUTO_TEST_CASE(cauchy_full_run_nonzero_target_time) CauchyConfig cauchy_settings; SecurityConfig security_settings; TestedEGType my_endgame(tracker,cauchy_settings,endgame_settings,security_settings); + my_endgame.SetBoundaryTime(start_time); + my_endgame.SetTargetTime(target_time); - auto endgame_success = my_endgame.Run(start_time,start_sample,target_time); + auto endgame_success = my_endgame.Run(start_sample); BOOST_CHECK(endgame_success == SuccessCode::Success); BOOST_CHECK((my_endgame.FinalApproximation() - x_to_check_against).template lpNorm() < 1e-10); @@ -1700,8 +1707,11 @@ the endgame. */ BOOST_AUTO_TEST_CASE(griewank_osborne) { - - + // Deterministic RNG: the random TotalDegree start system below (and the + // endgame's internal random draws) otherwise seed from std::random_device, + // which makes this marginal double-precision case flake run-to-run and + // across platforms. Seed it so the test is reproducible. See ADR-0003. + bertini::SetGlobalSeed(1u); DefaultPrecision(ambient_precision); @@ -1756,7 +1766,16 @@ BOOST_AUTO_TEST_CASE(griewank_osborne) SuccessCode tracking_success; tracking_success = tracker.TrackPath(result,t_start,t_endgame_boundary,start_point); - BOOST_CHECK(tracking_success==SuccessCode::Success); + // Tracking from t=1 to the endgame boundary must succeed for every path. + // REQUIRE (not CHECK) so that a tracking failure reports cleanly right here + // instead of leaving `result` empty and throwing a cryptic + // "dehomogenizing point with incorrect number of coordinates" from + // DehomogenizePoint below. + BOOST_REQUIRE_MESSAGE(tracking_success==SuccessCode::Success, + "tracking path " << ii << " to the endgame boundary failed with code " + << static_cast(tracking_success)); + BOOST_REQUIRE_EQUAL(result.size(), + static_cast(final_griewank_osborn_system.NumVariables())); griewank_homogenized_solutions.push_back(result); griewank_solutions.push_back(final_griewank_osborn_system.DehomogenizePoint(result)); @@ -1781,15 +1800,15 @@ BOOST_AUTO_TEST_CASE(griewank_osborne) unsigned num_fails = 0; unsigned num_infinite_solutions = 0; + unsigned num_converged_at_origin = 0; for (auto& s : griewank_homogenized_solutions) //current_space_values) { auto init_prec = Precision(s(0)); DefaultPrecision(init_prec); final_griewank_osborn_system.precision(init_prec); - BCT time_at_correct_prec = t_endgame_boundary; - Precision(time_at_correct_prec,init_prec); - SuccessCode endgame_success = my_endgame.Run(time_at_correct_prec,s); + my_endgame.SetBoundaryTime(t_endgame_boundary); + SuccessCode endgame_success = my_endgame.Run(s); // i took this check out jan 27, 2018. the tracker's precision literally cannot be adjusted as currently written. hence, there's no way to correct a failing test like this. i do hope there isn't something nasty lurking about causing it to fail, but causing other problems as well. i guess it would be nice if the tracker came back in the same precision as the endgame result, but ... do i really care? if i did, i would have to add a ChangePrecision(p) method to all flavors of trackers, but that's not really what they do... they're a tool, and adjust precision according to the input they receive, just like the endgames, and other things. // BOOST_CHECK_EQUAL(Precision(my_endgame.FinalApproximation()), tracker.CurrentPrecision()); @@ -1797,6 +1816,24 @@ BOOST_AUTO_TEST_CASE(griewank_osborne) if(endgame_success == SuccessCode::Success) { num_paths_converging++; + + // Post-solve tolerance check: griewank-osborne has exactly one finite + // solution, the origin (multiplicity 3). Any path the endgame reports + // as converged must land on the origin to tolerance (unless it is a + // diverging path, caught below by the security max_norm). This guards + // against the endgame returning Success on a point that is not actually + // a solution. Observed distance-to-origin is ~1e-12 across all + // instantiations (double, fixed-multiple, AMP); 1e-9 leaves margin + // against run-to-run variation while still catching a non-solution. + Vec dehomogenized_final = griewank_osborn_sys.DehomogenizePoint(my_endgame.FinalApproximation()); + auto final_norm = dehomogenized_final.template lpNorm(); + if (final_norm <= security_settings.max_norm) // a finite (non-diverging) converged solution + { + auto dist_to_origin = (dehomogenized_final - correct).template lpNorm(); + BOOST_CHECK_MESSAGE(dist_to_origin < 1e-9, + "griewank converged finite path did not meet tolerance at the origin; distance = " << dist_to_origin); + ++num_converged_at_origin; + } } else if(endgame_success == SuccessCode::SecurityMaxNormReached || endgame_success == SuccessCode::GoingToInfinity) { @@ -1811,6 +1848,8 @@ BOOST_AUTO_TEST_CASE(griewank_osborne) } BOOST_CHECK(num_paths_converging>=3); BOOST_CHECK(num_paths_diverging<=3); + // the three converged paths must actually be the (multiplicity-3) origin, to tolerance + BOOST_CHECK(num_converged_at_origin>=3); }//end compute griewank osborne @@ -1846,6 +1885,9 @@ has six solutions at t = .1: */ BOOST_AUTO_TEST_CASE(total_degree_start_system) { + // Deterministic RNG: random TotalDegree start system + endgame draws. See ADR-0003. + bertini::SetGlobalSeed(1u); + DefaultPrecision(ambient_precision); Var x = Variable::Make("x"); @@ -1904,7 +1946,14 @@ BOOST_AUTO_TEST_CASE(total_degree_start_system) SuccessCode tracking_success; tracking_success = tracker.TrackPath(result,t_start,t_endgame_boundary,start_point); - BOOST_CHECK(tracking_success==SuccessCode::Success); + // REQUIRE success and a full-size result before dehomogenizing, so a tracking + // failure reports cleanly here instead of throwing a cryptic + // "dehomogenizing point with incorrect number of coordinates" on an empty result. + BOOST_REQUIRE_MESSAGE(tracking_success==SuccessCode::Success, + "tracking path " << ii << " to the endgame boundary failed with code " + << static_cast(tracking_success)); + BOOST_REQUIRE_EQUAL(result.size(), + static_cast(final_system.NumVariables())); homogenized_solutions.push_back(result); solutions.push_back(final_system.DehomogenizePoint(result)); @@ -1916,6 +1965,7 @@ BOOST_AUTO_TEST_CASE(total_degree_start_system) correct << BCT(1,0),BCT(1,0); TestedEGType my_endgame(tracker); + my_endgame.SetBoundaryTime(BCT{0.1}); tracker.Setup(TestedPredictor, 1e-6, 1e5, @@ -1935,9 +1985,8 @@ BOOST_AUTO_TEST_CASE(total_degree_start_system) { auto eg_prec = Precision(s); DefaultPrecision(eg_prec); - BCT t_endgame_boundary{0.1}; final_system.precision(eg_prec); - SuccessCode endgame_success = my_endgame.Run(t_endgame_boundary,s); + SuccessCode endgame_success = my_endgame.Run(s); if(endgame_success == SuccessCode::Success) { ++num_successful_occurences; @@ -2014,12 +2063,12 @@ BOOST_AUTO_TEST_CASE(gory_detail_logging) TestedEGType my_endgame(tracker); - + my_endgame.SetBoundaryTime(time); bertini::endgame::GoryDetailLogger eg_logger; my_endgame.AddObserver(eg_logger); - auto cauchy_endgame_success = my_endgame.Run(time,sample); + auto cauchy_endgame_success = my_endgame.Run(sample); BOOST_CHECK(cauchy_endgame_success==SuccessCode::Success); diff --git a/core/test/endgames/generic_interpolation.hpp b/core/test/endgames/generic_interpolation.hpp index 8901a277e..b75f842dd 100644 --- a/core/test/endgames/generic_interpolation.hpp +++ b/core/test/endgames/generic_interpolation.hpp @@ -32,10 +32,10 @@ using namespace bertini::endgame; using bertini::DefaultPrecision; -template using Vec = Eigen::Matrix; -template using Mat = Eigen::Matrix; +template using Vec = Eigen::Matrix; +template using Mat = Eigen::Matrix; -using BCT = BaseComplexType; +using BCT = BaseComplexT; using BRT = Eigen::NumTraits::Real; template diff --git a/core/test/endgames/generic_pseg_test.hpp b/core/test/endgames/generic_pseg_test.hpp index f49513a69..f6a2bc167 100644 --- a/core/test/endgames/generic_pseg_test.hpp +++ b/core/test/endgames/generic_pseg_test.hpp @@ -52,14 +52,14 @@ using mpq_rational = bertini::mpq_rational; using bertini::Precision; using bertini::DefaultPrecision; -template using Vec = Eigen::Matrix; -template using Mat = Eigen::Matrix; +template using Vec = Eigen::Matrix; +template using Mat = Eigen::Matrix; using PrecisionConfig = bertini::tracking::TrackerTraits::PrecisionConfig; -using BRT = bertini::tracking::TrackerTraits::BaseRealType; -using BCT = bertini::tracking::TrackerTraits::BaseComplexType; +using BRT = bertini::tracking::TrackerTraits::BaseRealT; +using BCT = bertini::tracking::TrackerTraits::BaseComplexT; template BCT ComplexFromString(T... s) @@ -294,13 +294,13 @@ BOOST_AUTO_TEST_CASE(compute_bound_on_cycle_num) my_endgame.SetTimes(times); my_endgame.SetSamples(samples); - my_endgame.SetRandVec(samples.back().size()); + my_endgame.SetRandVec(static_cast(samples.back().size())); my_endgame.ComputeBoundOnCycleNumber(); BOOST_CHECK(my_endgame.UpperBoundOnCycleNumber() == 6); // max_cycle_num implemented max(5,6) = 6 - auto first_upper_bound = my_endgame.UpperBoundOnCycleNumber(); + [[maybe_unused]] auto first_upper_bound = my_endgame.UpperBoundOnCycleNumber(); //Setting up a new sample for approximation. time = ComplexFromString(".0125"); //.025/2 = .0125 @@ -412,7 +412,7 @@ BOOST_AUTO_TEST_CASE(compute_cycle_number) my_endgame.ComputeAllDerivatives(); - my_endgame.SetRandVec(samples.back().size()); + my_endgame.SetRandVec(static_cast(samples.back().size())); my_endgame.ComputeCycleNumber(BCT(0)); BOOST_CHECK(my_endgame.CycleNumber() == 1); @@ -493,7 +493,7 @@ BOOST_AUTO_TEST_CASE(compute_approximation_of_x_at_t0) Vec first_approx; Vec approx_1(1); - my_endgame.SetRandVec(samples.back().size()); + my_endgame.SetRandVec(static_cast(samples.back().size())); my_endgame.ComputeAllDerivatives(); @@ -585,7 +585,7 @@ BOOST_AUTO_TEST_CASE(compute_initial_samples) tracker.PrecisionSetup(precision_config); - BCT sample_factor = ComplexFromString(".5"); + [[maybe_unused]] BCT sample_factor = ComplexFromString(".5"); BCT origin = BCT(0); Vec x_origin(1); x_origin << BCT(1); @@ -773,12 +773,13 @@ BOOST_AUTO_TEST_CASE(pseg_full_run) bertini::endgame::SecurityConfig security_settings; TestedEGType my_endgame(tracker,endgame_settings,security_settings); - my_endgame.Run(current_time,current_space); + my_endgame.SetBoundaryTime(current_time); + my_endgame.Run(current_space); BOOST_CHECK((my_endgame.FinalApproximation() - correct).norm() < 1e-11); -}//end pseg mp for power series class +}//end pseg mp for power series class /** @@ -832,7 +833,9 @@ BOOST_AUTO_TEST_CASE(pseg_full_run_non_zero_target_time) bertini::endgame::SecurityConfig security_settings; TestedEGType my_endgame(tracker,endgame_settings,security_settings); - my_endgame.Run(current_time,current_space,target_time); + my_endgame.SetBoundaryTime(current_time); + my_endgame.SetTargetTime(target_time); + my_endgame.Run(current_space); BOOST_CHECK((my_endgame.FinalApproximation() - correct).norm() < 1e-11); @@ -901,7 +904,8 @@ BOOST_AUTO_TEST_CASE(full_run_cycle_num_2) bertini::endgame::SecurityConfig security_settings; TestedEGType my_endgame(tracker,endgame_settings,security_settings); - my_endgame.Run(t_endgame_boundary,eg_boundary_point); + my_endgame.SetBoundaryTime(t_endgame_boundary); + my_endgame.Run(eg_boundary_point); BOOST_CHECK_EQUAL(my_endgame.CycleNumber(),1); BOOST_CHECK((my_endgame.FinalApproximation() - correct_root).norm() < 1e-11); @@ -963,8 +967,8 @@ BOOST_AUTO_TEST_CASE(full_run_multiple_variables) bertini::endgame::SecurityConfig security_settings; TestedEGType my_endgame(tracker,endgame_settings,power_series_settings,security_settings); - - my_endgame.Run(current_time,current_space); + my_endgame.SetBoundaryTime(current_time); + my_endgame.Run(current_space); BOOST_CHECK((my_endgame.FinalApproximation() - correct).norm() < 1e-10);//my_endgame.GetTrackToleranceDuringEndgame()); @@ -1013,6 +1017,10 @@ has six solutions at t = .1: */ BOOST_AUTO_TEST_CASE(griewank_osborne) { + // Deterministic RNG: the endgame's internal random draws otherwise seed from + // std::random_device, flaking this marginal case run-to-run. See ADR-0003. + bertini::SetGlobalSeed(1u); + DefaultPrecision(ambient_precision); bertini::System sys; @@ -1081,7 +1089,8 @@ BOOST_AUTO_TEST_CASE(griewank_osborne) for (const auto& s : current_space_values) { DefaultPrecision(ambient_precision); - SuccessCode endgame_success = my_endgame.Run(endgame_time,s); + my_endgame.SetBoundaryTime(endgame_time); + SuccessCode endgame_success = my_endgame.Run(s); if(endgame_success == SuccessCode::Success){ BOOST_CHECK((my_endgame.FinalApproximation() - correct).norm() < 1e-11);// my_endgame.GetTrackToleranceDuringEndgame()); num_paths_converging++; @@ -1111,6 +1120,8 @@ values we have. BOOST_AUTO_TEST_CASE(total_degree_start_system) { using namespace bertini::tracking; + // Deterministic RNG: random TotalDegree start system + endgame draws. See ADR-0003. + bertini::SetGlobalSeed(1u); DefaultPrecision(ambient_precision); Var x = Variable::Make("x"); @@ -1206,8 +1217,8 @@ BOOST_AUTO_TEST_CASE(total_degree_start_system) unsigned num_successful_occurences = 0; for (auto const& s : endgame_boundary_solutions) { - Precision(t_endgame_boundary, Precision(s)); - SuccessCode endgame_success = my_endgame.Run(t_endgame_boundary,s); + my_endgame.SetBoundaryTime(t_endgame_boundary); + SuccessCode endgame_success = my_endgame.Run(s); if(endgame_success == SuccessCode::Success) { BOOST_CHECK_EQUAL(Precision(my_endgame.FinalApproximation()), tracker.CurrentPrecision()); @@ -1278,9 +1289,9 @@ BOOST_AUTO_TEST_CASE(parabola) stepping_settings, newton_settings); TestedEGType my_endgame(tracker); + my_endgame.SetBoundaryTime(t_endgame_boundary); - - auto endgame_success = my_endgame.Run(t_endgame_boundary,soln_at_EG_bdry); + auto endgame_success = my_endgame.Run(soln_at_EG_bdry); BOOST_CHECK(endgame_success==SuccessCode::Success); auto endgame_solution = my_endgame.FinalApproximation(); @@ -1341,8 +1352,10 @@ BOOST_AUTO_TEST_CASE(pseg_full_run_nonzero_target_time) bertini::endgame::SecurityConfig security_settings; bertini::endgame::PowerSeriesConfig power_series_settings; TestedEGType my_endgame(tracker,endgame_settings,power_series_settings, security_settings); + my_endgame.SetBoundaryTime(start_time); + my_endgame.SetTargetTime(target_time); - auto endgame_success = my_endgame.Run(start_time,start_sample,target_time); + auto endgame_success = my_endgame.Run(start_sample); BOOST_CHECK(endgame_success == SuccessCode::Success); BOOST_CHECK((my_endgame.FinalApproximation() - x_to_check_against).norm() < 1e-10); diff --git a/core/test/endgames/interpolation.cpp b/core/test/endgames/interpolation.cpp index edb586e0f..d1313f2aa 100644 --- a/core/test/endgames/interpolation.cpp +++ b/core/test/endgames/interpolation.cpp @@ -41,34 +41,34 @@ BOOST_AUTO_TEST_SUITE(interpolation) BOOST_AUTO_TEST_SUITE(generic_tests_double_16) -using BaseComplexType = bertini::dbl; +using BaseComplexT = bertini::dbl; unsigned ambient_precision = bertini::DoublePrecision(); #include "test/endgames/generic_interpolation.hpp" BOOST_AUTO_TEST_SUITE_END() // generic tests at some precision BOOST_AUTO_TEST_SUITE(generic_tests_double_30) -using BaseComplexType = bertini::dbl; +using BaseComplexT = bertini::dbl; unsigned ambient_precision = 30; #include "test/endgames/generic_interpolation.hpp" BOOST_AUTO_TEST_SUITE_END() // generic tests at some precision BOOST_AUTO_TEST_SUITE(generic_tests_mpfr_16) -using BaseComplexType = bertini::mpfr_complex; +using BaseComplexT = bertini::mpfr_complex; unsigned ambient_precision = bertini::DoublePrecision(); #include "test/endgames/generic_interpolation.hpp" BOOST_AUTO_TEST_SUITE_END() // generic tests at some precision BOOST_AUTO_TEST_SUITE(generic_tests_mpfr_30) -using BaseComplexType = bertini::mpfr_complex; +using BaseComplexT = bertini::mpfr_complex; unsigned ambient_precision = 30; #include "test/endgames/generic_interpolation.hpp" BOOST_AUTO_TEST_SUITE_END() // generic tests at some precision BOOST_AUTO_TEST_SUITE(generic_tests_mpfr_100) -using BaseComplexType = bertini::mpfr_complex; +using BaseComplexT = bertini::mpfr_complex; unsigned ambient_precision = 100; #include "test/endgames/generic_interpolation.hpp" BOOST_AUTO_TEST_SUITE_END() // generic tests at some precision diff --git a/core/test/nag_algorithms/zero_dim.cpp b/core/test/nag_algorithms/zero_dim.cpp index 4b07cf3f6..3c927d2f1 100644 --- a/core/test/nag_algorithms/zero_dim.cpp +++ b/core/test/nag_algorithms/zero_dim.cpp @@ -32,6 +32,12 @@ #include "bertini2/system/start_systems.hpp" #include #include "bertini2/nag_algorithms/output.hpp" +#include "bertini2/trackers/observers.hpp" +#include "bertini2/trackers/events.hpp" +#include +#include +#include +#include using Variable = bertini::node::Variable; @@ -190,4 +196,890 @@ BOOST_AUTO_TEST_CASE(reference_managed_systems_GO) } +// Run ZeroDim from a USER-CONSTRUCTED homotopy and a GIVEN list of start points -- the +// parameter-homotopy workflow. Crucially this reuses the ENTIRE ZeroDim solve pipeline +// (pre-endgame tracking, the midpath check, the endgame, post-processing) UNCHANGED: it is the +// same ZeroDim class template, merely instantiated with start_system::User (start points come +// from the supplied list, not generated) and policy::RefToGiven (the homotopy is taken as-is, +// not formed by homogenizing + coupling a start system). +// +// Parameter homotopy H(x,t) = x^2 - (9 - 5 t). At t = 1 the roots are +/-2 (the given start +// points); at t = 0 they are +/-3 (the target x^2 - 9). Tracking the two start points down to +// t = 0 must recover +/-3 -- i.e. ZeroDim moved the t=1 solutions to the t=0 parameter. +BOOST_AUTO_TEST_CASE(user_homotopy_parameter_homotopy_solves) +{ + using namespace bertini; + using namespace tracking; + using mpfr = bertini::mpfr_complex; + + auto x = Variable::Make("x"); + auto t = Variable::Make("t"); + + // the homotopy H(x,t) = x^2 - (9 - 5 t), with t as its path variable + System H; + H.AddVariableGroup(VariableGroup{x}); + H.AddFunction(x*x - (9 - 5*t)); + H.AddPathVariable(t); + + // the target system (H at t = 0): x^2 - 9, used for dehomogenize / residual in post-processing + System target; + target.AddVariableGroup(VariableGroup{x}); + target.AddFunction(x*x - 9); + + // the GIVEN start points: the t = 1 solutions +/- 2 (as if computed by an earlier solve) + SampCont start_points; + { + Vec p(1); p(0) = mpfr(2); start_points.push_back(p); + Vec q(1); q(0) = mpfr(-2); start_points.push_back(q); + } + + auto user_start = start_system::User(target, start_points); + + auto zd = algorithm::ZeroDim< + AMPTracker, + bertini::endgame::EndgameSelector::Cauchy, + System, + start_system::User, + policy::RefToGiven> + (target, user_start, H); // (target, start, homotopy) -- references, so all three + + zd.DefaultSetup(); + zd.Solve(); + + auto const& sols = zd.SolutionsUserCoords(); + auto const& md = zd.FinalSolutionMetadata(); + std::vector ends; + for (size_t i = 0; i < sols.size(); ++i) + if (md[i].endgame_success == SuccessCode::Success && sols[i].size() == 1) + ends.push_back(dbl(sols[i](0))); + + BOOST_CHECK_EQUAL(ends.size(), 2u); + bool has_pos = false, has_neg = false; + for (auto const& e : ends) + { + if (std::abs(e - dbl(3,0)) < 1e-7) has_pos = true; + if (std::abs(e - dbl(-3,0)) < 1e-7) has_neg = true; + } + BOOST_CHECK(has_pos); // tracked +2 -> +3 + BOOST_CHECK(has_neg); // tracked -2 -> -3 +} + + +// End-to-end multihomogeneous solve through the block-composed start system and the +// blend-block homotopy. x*y - 1 = 0, x + y = 0 over variable groups {x}, {y}: +// y = -x gives -x^2 - 1 = 0, so x = +/- i -> exactly the two solutions (i,-i),(-i,i). +// The m-homogeneous Bezout number for bidegrees (1,1),(1,1) is 2, below the total-degree +// Bezout number 4 -- so MHom tracks 2 paths, not 4. This exercises the whole new chain: +// the products-of-linears start block, the blend-block homotopy formed by FormHomotopy, +// and the block-aware System eval/Jacobian/time-derivative through the AMP tracker + Cauchy endgame. +// +// Deterministic: SetGlobalSeed pins the homotopy gamma and the MHom start coefficients (RandomMp is +// reseedable now), so this is a single, reproducible solve -- no gamma-retry loop. +BOOST_AUTO_TEST_CASE(mhom_solves_two_variable_group_system) +{ + using namespace bertini; + using namespace tracking; + + SetGlobalSeed(1); // reproducible homotopy gamma + MHom start coefficients + + System sys; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddVariableGroup(VariableGroup{y}); + sys.AddFunction(x*y - 1); + sys.AddFunction(x + y); + + auto zd = algorithm::ZeroDim::Cauchy, + decltype(sys), + start_system::MHomogeneous>(sys); + zd.DefaultSetup(); + zd.Solve(); + + // Collect the successfully-tracked solutions (a failed path leaves an empty placeholder). + auto const& sols = zd.SolutionsUserCoords(); + auto const& md = zd.FinalSolutionMetadata(); + using SolVec = std::decay_t; + std::vector good; + for (size_t i = 0; i < sols.size(); ++i) + if (md[i].endgame_success == SuccessCode::Success && sols[i].size() == 2) + good.push_back(sols[i]); + + BOOST_REQUIRE_EQUAL(good.size(), 2u); // both MHom paths solved (the m-homogeneous Bezout number) + + for (auto const& s : good) // AMP returns mpfr_complex coords; double is plenty for a root check + { + dbl a(s(0)), b(s(1)); + BOOST_CHECK_SMALL(std::abs(a * b - dbl(1)), 1e-8); + BOOST_CHECK_SMALL(std::abs(a + b), 1e-8); + } + BOOST_CHECK_GT(std::abs(dbl(good[0](0)) - dbl(good[1](0))), 1e-3); // the two distinct roots +} + + + + +// The decisive block-vs-function-tree check on the ACTUAL problematic homotopy: take the seed-6 +// MHom blend homotopy (a BlendBlock of the homogenized target's polynomial block and the +// products-of-linears start), build its pure-function-tree twin via ExpandToFunctionTree(), and +// assert the two evaluate and (crucially) DIFFERENTIATE identically -- across random points and +// near t->0 (the endgame region), in double and mpfr. If the block Jacobian were wrong (inflating +// ||J^{-1}|| and hence DigitsB), this is where it would show. Together with the faithful +// ||J^{-1}|| estimate (amp_jacobian_estimate) and DegreeBound=2 (so Phi is tiny), agreement here +// means the DigitsB escalation reflects a GENUINE near-singular pass for that gamma, not a bug in +// the block representation. +BOOST_AUTO_TEST_CASE(mhom_homotopy_block_matches_function_tree) +{ + using namespace bertini; + using namespace tracking; + + SetGlobalSeed(6); // the gamma that escalated to 140 digits in the probe + + System sys; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddVariableGroup(VariableGroup{y}); + sys.AddFunction(x*y - 1); + sys.AddFunction(x + y); + + auto zd = algorithm::ZeroDim::Cauchy, + decltype(sys), + start_system::MHomogeneous>(sys); + zd.DefaultSetup(); + + System const& H = zd.Homotopy(); // the block-composed blend homotopy + System He = H.ExpandToFunctionTree(); // its pure-function-tree twin + + BOOST_CHECK_EQUAL(H.NumVariables(), He.NumVariables()); + BOOST_CHECK_EQUAL(H.NumTotalFunctions(), He.NumTotalFunctions()); + BOOST_CHECK_EQUAL(H.DegreeBound(), He.DegreeBound()); // drives Phi/Psi; must match + + auto compare = [](auto const& A, auto const& B, double tol) + { + BOOST_REQUIRE_EQUAL(A.rows(), B.rows()); + BOOST_REQUIRE_EQUAL(A.cols(), B.cols()); + for (Eigen::Index i = 0; i < A.rows(); ++i) + for (Eigen::Index j = 0; j < A.cols(); ++j) + { + double err = static_cast(abs(A(i,j) - B(i,j))); + double scale = 1.0 + static_cast(abs(A(i,j))); + BOOST_CHECK_SMALL(err / scale, tol); + } + }; + + const int n = static_cast(H.NumVariables()); + + // double, including t very close to 0 (endgame region, where the spike lives) + for (int trial = 0; trial < 10; ++trial) + { + Vec p = Vec::Random(n); + dbl t = (trial < 5) ? dbl(0.4, -0.3) * dbl(trial + 1) + : dbl(std::pow(10.0, -(trial - 1)), 0.0); // 1e-4 .. 1e-8 + compare(H.Eval(p, t), He.Eval(p, t), 1e-11); + compare(H.Jacobian(p, t), He.Jacobian(p, t), 1e-11); + } + + // mpfr at 80 digits + DefaultPrecision(80); + H.precision(80); + He.precision(80); + for (int trial = 0; trial < 5; ++trial) + { + Vec p = RandomOfUnits(n); + mpfr_complex t = RandomOfUnits(1)(0); + compare(H.Eval(p, t), He.Eval(p, t), 1e-70); + compare(H.Jacobian(p, t), He.Jacobian(p, t), 1e-70); + } +} + + +// SolveReport: the end-of-solve diagnostic summary. SummarizeSolve is pure aggregation, so unit-test +// it on synthetic metadata that includes a genuine tracking failure. +BOOST_AUTO_TEST_CASE(solve_report_buckets_metadata_and_flags_failures) +{ + using bertini::algorithm::SolutionMetaData; + using bertini::algorithm::SummarizeSolve; + using bertini::algorithm::MidpathCheckReport; + using SC = bertini::SuccessCode; + using CT = bertini::dbl; + + auto set = [](SolutionMetaData& m, SC code, bool finite, int mult, + bool real, bool sing, double cond){ + m.endgame_success = code; m.is_finite = finite; m.multiplicity = mult; + m.is_real = real; m.is_singular = sing; m.condition_number = cond; m.max_precision_used = 16; + }; + + std::vector> md(8); + set(md[0], SC::Success, true, 1, true, false, 1e3); // finite, real + set(md[1], SC::Success, true, 1, false, false, 1e8); // finite, complex + set(md[2], SC::Success, true, 2, false, true, 1e9); // a double root: two paths... + set(md[3], SC::Success, true, 2, false, true, 1e9); // ...counted once (sum 1/mult) + set(md[4], SC::Success, false, 1, false, false, 0); // diverged (success + infinite) + set(md[5], SC::GoingToInfinity, false, 1, false, false, 0); // diverged (clean) + set(md[6], SC::MinStepSizeReached, false, 1, false, false, 0); // FAILED -- a lost path + set(md[7], SC::FailedToConverge, false, 1, false, false, 0); // FAILED + + auto r = SummarizeSolve(md, MidpathCheckReport{}); + BOOST_CHECK_EQUAL(r.num_paths_tracked, 8u); + BOOST_CHECK_EQUAL(r.num_finite_endpoints, 4u); + BOOST_CHECK_EQUAL(r.num_finite_solutions, 3u); // 1 + 1 + (1/2 + 1/2) + BOOST_CHECK_EQUAL(r.num_diverged, 2u); + BOOST_CHECK_EQUAL(r.num_failed, 2u); + BOOST_CHECK_EQUAL(r.num_real, 1u); + BOOST_CHECK_EQUAL(r.num_singular, 2u); + BOOST_CHECK_EQUAL(r.failures_by_reason[SC::MinStepSizeReached], 1u); + BOOST_CHECK_EQUAL(r.failures_by_reason[SC::FailedToConverge], 1u); + BOOST_CHECK(r.max_condition_number >= 1e9); + BOOST_CHECK(!r.all_paths_resolved); // failures present -> not trustworthy + + // drop the two failures: now every path resolved (and the midpath check passed by default) + std::vector> clean(md.begin(), md.begin() + 6); + auto rc = SummarizeSolve(clean, MidpathCheckReport{}); + BOOST_CHECK_EQUAL(rc.num_failed, 0u); + BOOST_CHECK(rc.all_paths_resolved); +} + +// A real solve: x^2-1, y^2-1 -> exactly 4 finite roots, no losses. +BOOST_AUTO_TEST_CASE(solve_report_from_a_real_solve) +{ + using namespace bertini; + + auto x = node::Variable::Make("x"); + auto y = node::Variable::Make("y"); + System sys; + sys.AddFunction(pow(x, 2) - 1); + sys.AddFunction(pow(y, 2) - 1); + sys.AddVariableGroup(VariableGroup{x, y}); + + auto zd = algorithm::ZeroDim::Cauchy, System, start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto const& meta = zd.FinalSolutionMetadata(); + auto r = zd.Report(); + + BOOST_CHECK_EQUAL(r.num_paths_tracked, meta.size()); // total degree 2*2 = 4 + BOOST_CHECK_EQUAL(r.num_finite_endpoints + r.num_diverged + r.num_failed, // the buckets partition + r.num_paths_tracked); + BOOST_CHECK_EQUAL(r.num_finite_solutions, 4u); + BOOST_CHECK_EQUAL(r.num_failed, 0u); + BOOST_CHECK(r.all_paths_resolved); + + std::ostringstream oss; oss << r; // it prints + BOOST_CHECK(!oss.str().empty()); +} + +// The filtered-solution convenience accessors, on the same x^2-1, y^2-1 solve: all four roots +// (+-1, +-1) are finite, real, and nonsingular. +BOOST_AUTO_TEST_CASE(filtered_solution_accessors) +{ + using namespace bertini; + + auto x = node::Variable::Make("x"); + auto y = node::Variable::Make("y"); + System sys; + sys.AddFunction(pow(x, 2) - 1); + sys.AddFunction(pow(y, 2) - 1); + sys.AddVariableGroup(VariableGroup{x, y}); + + auto zd = algorithm::ZeroDim::Cauchy, System, start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto r = zd.Report(); + BOOST_CHECK_EQUAL(zd.FiniteSolutions().size(), 4u); + BOOST_CHECK_EQUAL(zd.RealSolutions().size(), 4u); + BOOST_CHECK_EQUAL(zd.NonsingularSolutions().size(), 4u); + BOOST_CHECK_EQUAL(zd.SingularSolutions().size(), 0u); + + // the accessors agree with the report's counts + BOOST_CHECK_EQUAL(zd.FiniteSolutions().size(), r.num_finite_endpoints); + BOOST_CHECK_EQUAL(zd.RealSolutions().size(), r.num_real); + BOOST_CHECK_EQUAL(zd.SingularSolutions().size(), r.num_singular); + + // singular + nonsingular partition the finite set + BOOST_CHECK_EQUAL(zd.SingularSolutions().size() + zd.NonsingularSolutions().size(), + zd.FiniteSolutions().size()); + + // user vs internal coordinates: same count, different representation + BOOST_CHECK_EQUAL(zd.FiniteSolutions(false).size(), zd.FiniteSolutions(true).size()); +} + +// InfiniteSolutions: the at-infinity complement of FiniteSolutions. The system {x*y - 1, x - 1} +// has Bezout number 2 but exactly one affine solution (1,1); the second total-degree path must +// diverge -- so there is one finite endpoint and one at infinity. +BOOST_AUTO_TEST_CASE(infinite_solutions_at_infinity) +{ + using namespace bertini; + + auto x = node::Variable::Make("x"); + auto y = node::Variable::Make("y"); + System sys; + sys.AddFunction(x*y - 1); + sys.AddFunction(x - 1); + sys.AddVariableGroup(VariableGroup{x, y}); + + auto zd = algorithm::ZeroDim::Cauchy, System, start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto r = zd.Report(); + BOOST_CHECK_EQUAL(zd.FinalSolutionMetadata().size(), 2u); // Bezout 2 + BOOST_CHECK_EQUAL(zd.FiniteSolutions().size(), 1u); + BOOST_CHECK_EQUAL(zd.InfiniteSolutions().size(), 1u); + + // finite + infinite partition the whole list (no failed paths on this clean solve) + BOOST_CHECK_EQUAL(r.num_failed, 0u); + BOOST_CHECK_EQUAL(zd.FiniteSolutions().size() + zd.InfiniteSolutions().size(), + zd.FinalSolutionMetadata().size()); + + // the at-infinity count matches the report's diverged bucket + BOOST_CHECK_EQUAL(zd.InfiniteSolutions().size(), r.num_diverged); + + // user vs internal coordinates: same count + BOOST_CHECK_EQUAL(zd.InfiniteSolutions(false).size(), zd.InfiniteSolutions(true).size()); +} + +// multiplicity_representative: a multiplicity-m solution arrives as m coincident endpoints, and +// the solver must mark exactly ONE of them the representative (the rest false), so a consumer can +// collapse the cluster to one row. {x^2, y^2} has a single solution (0,0) of multiplicity 4. +BOOST_AUTO_TEST_CASE(multiplicity_representative_marks_one_per_cluster) +{ + using namespace bertini; + + auto x = node::Variable::Make("x"); + auto y = node::Variable::Make("y"); + System sys; + sys.AddFunction(pow(x, 2)); + sys.AddFunction(pow(y, 2)); + sys.AddVariableGroup(VariableGroup{x, y}); + + auto zd = algorithm::ZeroDim::Cauchy, System, start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto const& md = zd.FinalSolutionMetadata(); + BOOST_CHECK_EQUAL(md.size(), 4u); // Bezout 2*2 = 4 paths + + unsigned representatives = 0, duplicates = 0; + for (auto const& m : md) + { + if (!m.is_finite) continue; + if (m.multiplicity_representative) { ++representatives; BOOST_CHECK_EQUAL(m.multiplicity, 4); } + else ++duplicates; + } + BOOST_CHECK_EQUAL(representatives, 1u); // exactly one representative + BOOST_CHECK_EQUAL(duplicates, 3u); // the other m-1 copies + + // the representative count equals the number of DISTINCT finite solutions in the report + BOOST_CHECK_EQUAL(representatives, zd.Report().num_finite_solutions); + + // a clean simple-root solve marks every finite endpoint a representative (nothing to merge): + // x^2 - 1, y^2 - 1 has four distinct simple roots. + System simple; + simple.AddFunction(pow(x, 2) - 1); + simple.AddFunction(pow(y, 2) - 1); + simple.AddVariableGroup(VariableGroup{x, y}); + auto zd2 = algorithm::ZeroDim::Cauchy, System, start_system::TotalDegree>(simple); + zd2.DefaultSetup(); + zd2.Solve(); + unsigned reps2 = 0; + for (auto const& m : zd2.FinalSolutionMetadata()) + if (m.is_finite && m.multiplicity_representative) ++reps2; + BOOST_CHECK_EQUAL(reps2, 4u); +} + +// Regression: a fixed-multiple (MultiplePrecisionTracker) zero-dim solve used to throw at the start +// of tracking -- "start point ... differing precision from default (20!=16)" -- because the tracker +// (built at DefaultPrecision) and the config-driven ambient/thread precision (DoublePrecision) +// disagreed. ZeroDimConfig::initial_ambient_precision now defaults to the multiprecision +// DefaultPrecision, so the precision is uniform everywhere and the solve completes -- at whatever +// precision is the default when the solver is constructed. +BOOST_AUTO_TEST_CASE(fixed_multiple_precision_solves_uniformly) +{ + using namespace bertini; + using MPTracker = tracking::MultiplePrecisionTracker; + + auto solve_at = [](unsigned at_precision) -> unsigned long long { + auto saved = DefaultPrecision(); + DefaultPrecision(at_precision); + auto x = node::Variable::Make("x"); + System sys; + sys.AddFunction(pow(x, 2) - 1); + sys.AddVariableGroup(VariableGroup{x}); + auto zd = algorithm::ZeroDim::Cauchy, + System, start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); // used to throw on the precision mismatch + auto n = zd.Report().num_finite_solutions; + DefaultPrecision(saved); + return n; + }; + + BOOST_CHECK_EQUAL(solve_at(30), 2u); // a typical multiprecision default + BOOST_CHECK_EQUAL(solve_at(60), 2u); // and a higher one -- still uniform, still solves +} + +// FixedPrecisionConfig.precision sets a fixed-multiple solve's precision *after* construction -- no +// DefaultPrecision()-before-construct dance. This is the "real precision setter" ADR-0030 deferred: +// the algorithm lifts the tracker, the systems, and the ambient/start-point precision to the config's +// value at PreSolveSetup. +BOOST_AUTO_TEST_CASE(fixed_multiple_precision_set_via_config) +{ + using namespace bertini; + using MPTracker = tracking::MultiplePrecisionTracker; + + auto saved = DefaultPrecision(); + DefaultPrecision(30); // construct at a modest default + auto x = node::Variable::Make("x"); + System sys; + sys.AddFunction(pow(x, 2) - 1); + sys.AddVariableGroup(VariableGroup{x}); + auto zd = algorithm::ZeroDim::Cauchy, + System, start_system::TotalDegree>(sys); + zd.DefaultSetup(); + + // the tracker reports its precision honestly + BOOST_CHECK_EQUAL(zd.GetTracker().template Get().precision, 30u); + + // choose a different fixed precision via the config, then solve at it + auto fp = zd.GetTracker().template Get(); + fp.precision = 70; + zd.GetTracker().template Set(fp); + + zd.Solve(); + BOOST_CHECK_EQUAL(zd.Report().num_finite_solutions, 2u); + BOOST_CHECK_EQUAL(zd.GetTracker().CurrentPrecision(), 70u); // the solve ran at 70 + DefaultPrecision(saved); +} + +// Double-precision tracking is fixed at DoublePrecision(); asking the config for any other precision +// is rejected (use mptype multiple/adaptive for more digits). +BOOST_AUTO_TEST_CASE(double_precision_rejects_a_different_precision) +{ + using namespace bertini; + auto x = node::Variable::Make("x"); + System sys; + sys.AddFunction(pow(x, 2) - 1); + sys.AddVariableGroup(VariableGroup{x}); + tracking::DoublePrecisionTracker tracker(sys); + + BOOST_CHECK_EQUAL(tracker.template Get().precision, DoublePrecision()); + tracking::FixedPrecisionConfig fp; + fp.precision = 50; + BOOST_CHECK_THROW(tracker.PrecisionSetup(fp), std::runtime_error); + fp.precision = DoublePrecision(); + BOOST_CHECK_NO_THROW(tracker.PrecisionSetup(fp)); // the native precision is fine +} + +// PROBE observer (branch perf/amp-block-precision-escalation): record, per successful step, the +// |t|, working precision, and the tracker's condition-number estimate -- so we can SEE whether the +// condition number (||J|| * ||J^{-1}||) spikes then RECOVERS along the actual seed-6 path, and at +// what |t| (mid-path vs the t->0 endgame region). +template + + + + + + +BOOST_AUTO_TEST_SUITE_END() + + + +// Path-crossing (midpath) detection and the re-track machinery (RunMidpathResolution). +// +// Two distinct paths landing on the same point at the endgame boundary is a probability-0 event: +// it signals a path crossing (under-resolved tracking), which the algorithm is supposed to detect +// and re-track. These tests guard the two historical defects in that machinery: +// * Bug 1: MidpathChecker computed `same_start` with an inverted comparison, so a genuine +// crossing (distinct starts, coincident boundary points) was flagged rerun==false and never +// re-tracked. +// * Bug 2: MidpathChecker::Check did not reset its pass flag / crossed-path list between calls, +// so the resolve loop never saw a clean pass and operated on stale data. +// The first three cases are deterministic (hand-built boundary + start data) so they always pass +// once the logic is correct, independent of tracker numerics. The end-to-end "provoke a spurious +// crossing and watch it get resolved" scenario is exercised in the Python suite on the cyclic +// system (see python/test/zero_dim/crossed_paths_test.py), where a cyclic builder exists and the +// phenomenon is documented (ADR-0017); the C++ wiring case below just confirms a clean solve +// reports no crossings and the new accessors work. +BOOST_AUTO_TEST_SUITE(crossed_paths) + +using bertini::dbl_complex; +using bertini::Vec; +using bertini::SuccessCode; +using MidPathConfig = bertini::algorithm::MidPathConfig; +using BoundaryMD = bertini::algorithm::EGBoundaryMetaData; +using Checker = bertini::algorithm::MidpathChecker; + +namespace { + // Minimal stand-in providing just the StartPoint(index) interface MidpathChecker + // needs, so the test controls both the boundary points and the start points. + struct MockStartSystem + { + std::vector> pts; + + template + Vec StartPoint(unsigned long long i) const + { + return pts[i].template cast(); + } + }; + + Vec Pt(dbl_complex a, dbl_complex b) + { + Vec v(2); + v << a, b; + return v; + } + + BoundaryMD MakeBoundaryPoint(Vec const& p) + { + return BoundaryMD(p, SuccessCode::Success, 0.01, 16); + } +} + + +// Bug 1: two coincident boundary points from DISTINCT start points is a genuine crossing -- it must +// be detected (Check fails) and both involved paths must be flagged for re-tracking (rerun==true). +BOOST_AUTO_TEST_CASE(distinct_starts_coincident_endpoints_are_rerun) +{ + Checker mp{MidPathConfig()}; + + // paths 0 and 1 land on (essentially) the same boundary point; path 2 is elsewhere. + auto p = Pt(1.0, 1.0); + auto p_nudged = Pt(1.0 + 1e-9, 1.0 - 1e-9); + auto p_far = Pt(5.0, 5.0); + Checker::BoundaryData boundary{MakeBoundaryPoint(p), MakeBoundaryPoint(p_nudged), MakeBoundaryPoint(p_far)}; + + MockStartSystem starts{{Pt(1.0, 0.0), Pt(2.0, 0.0), Pt(3.0, 0.0)}}; // all distinct + + bool passed = mp.Check(boundary, starts); + + BOOST_CHECK(!passed); + auto crossed = mp.GetCrossedPaths(); + BOOST_REQUIRE_EQUAL(crossed.size(), 2u); // paths 0 and 1 + for (auto const& c : crossed) + { + BOOST_CHECK(c.index() == 0 || c.index() == 1); + BOOST_CHECK(c.rerun()); // distinct starts -> genuine crossing -> must re-track + } +} + + +// Two coincident boundary points that began at the SAME start point are not a crossing to re-track: +// the crossing is still detected (Check fails) but rerun must be false. +BOOST_AUTO_TEST_CASE(same_start_coincident_endpoints_are_not_rerun) +{ + Checker mp{MidPathConfig()}; + + auto p = Pt(1.0, 1.0); + auto p_nudged = Pt(1.0 + 1e-9, 1.0 - 1e-9); + auto p_far = Pt(5.0, 5.0); + Checker::BoundaryData boundary{MakeBoundaryPoint(p), MakeBoundaryPoint(p_nudged), MakeBoundaryPoint(p_far)}; + + // paths 0 and 1 share a start point. + MockStartSystem starts{{Pt(1.0, 0.0), Pt(1.0, 0.0), Pt(3.0, 0.0)}}; + + bool passed = mp.Check(boundary, starts); + + BOOST_CHECK(!passed); + auto crossed = mp.GetCrossedPaths(); + BOOST_REQUIRE_EQUAL(crossed.size(), 2u); + for (auto const& c : crossed) + BOOST_CHECK(!c.rerun()); // same start -> not a re-trackable crossing +} + + +// Bug 2: Check must reset its state each call. A first call that finds a crossing must not leave +// the checker permanently "failed": a subsequent call on clean data must pass and report no +// crossings. +BOOST_AUTO_TEST_CASE(check_resets_state_between_calls) +{ + Checker mp{MidPathConfig()}; + + auto p = Pt(1.0, 1.0); + auto p_nudged = Pt(1.0 + 1e-9, 1.0 - 1e-9); + auto p_far = Pt(5.0, 5.0); + MockStartSystem starts{{Pt(1.0, 0.0), Pt(2.0, 0.0), Pt(3.0, 0.0)}}; + + Checker::BoundaryData crossing{MakeBoundaryPoint(p), MakeBoundaryPoint(p_nudged), MakeBoundaryPoint(p_far)}; + BOOST_CHECK(!mp.Check(crossing, starts)); + BOOST_CHECK(!mp.GetCrossedPaths().empty()); + + // now feed clean (all-distinct) data: must pass, and the stale crossing list must be gone. + Checker::BoundaryData clean{MakeBoundaryPoint(Pt(1.0, 1.0)), MakeBoundaryPoint(Pt(5.0, 5.0)), MakeBoundaryPoint(Pt(9.0, 9.0))}; + BOOST_CHECK(mp.Check(clean, starts)); + BOOST_CHECK(mp.GetCrossedPaths().empty()); +} + + +// Wiring: a clean solve populates the midpath report (no crossings) and the boundary accessors work. +BOOST_AUTO_TEST_CASE(clean_solve_reports_no_crossings) +{ + using namespace bertini; + using TrackerT = tracking::DoublePrecisionTracker; + + auto sys = system::Precon::GriewankOsborn(); + auto zd = algorithm::ZeroDim::Cauchy, decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto const& report = zd.EndgameBoundaryMetadata(); + BOOST_CHECK(report.passed); + BOOST_CHECK_EQUAL(report.num_crossings_detected, 0u); + BOOST_CHECK_EQUAL(report.num_resolve_attempts, 0u); + BOOST_CHECK(zd.EndgameBoundarySolutions().size() > 0u); +} + +BOOST_AUTO_TEST_SUITE_END() + + + +// Solution-metadata classification (is_finite / is_real / is_singular / multiplicity) and the +// PostProcessing config knobs that drive it. These pin the Bertini-1 behaviour: an endpoint is +// at infinity if the infinity norm of its dehomogenized coordinates exceeds +// endpoint_finite_threshold; real if the imaginary parts are below real_threshold; singular if it +// is a multiple endpoint or its condition number exceeds condition_number_threshold; and two +// endpoints are the same when their dehomogenized coordinates agree to +// final_tolerance * same_point_tolerance_multiplier (infinity norm). +BOOST_AUTO_TEST_SUITE(zero_dim_solution_metadata) + +using TrackerT = bertini::tracking::DoublePrecisionTracker; +using PostProcessing = bertini::algorithm::PostProcessingConfig; + +// tally the classification flags over the successfully-tracked endpoints +struct Counts { int success = 0, finite = 0, real = 0, singular = 0; }; +template +Counts Tally(MDVec const& md) +{ + Counts c; + for (auto const& m : md) + { + if (m.endgame_success != bertini::SuccessCode::Success) continue; + ++c.success; + if (m.is_finite) ++c.finite; + if (m.is_real) ++c.real; + if (m.is_singular) ++c.singular; + } + return c; +} + +// x^2 - 1 -> roots +/-1: two finite, real, nonsingular solutions. +BOOST_AUTO_TEST_CASE(finite_real_nonsingular) +{ + using namespace bertini; + System sys; + auto x = Variable::Make("x"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddFunction(x*x - 1); + + auto zd = algorithm::ZeroDim::Cauchy, decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto c = Tally(zd.FinalSolutionMetadata()); + BOOST_CHECK_EQUAL(c.success, 2); + BOOST_CHECK_EQUAL(c.finite, 2); + BOOST_CHECK_EQUAL(c.real, 2); + BOOST_CHECK_EQUAL(c.singular, 0); + for (auto const& m : zd.FinalSolutionMetadata()) + if (m.endgame_success == SuccessCode::Success) + BOOST_CHECK_EQUAL(m.multiplicity, 1); +} + + +// x^2 + 1 -> roots +/-i: two finite, NON-real, nonsingular solutions. +BOOST_AUTO_TEST_CASE(finite_complex_not_real) +{ + using namespace bertini; + System sys; + auto x = Variable::Make("x"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddFunction(x*x + 1); + + auto zd = algorithm::ZeroDim::Cauchy, decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto c = Tally(zd.FinalSolutionMetadata()); + BOOST_CHECK_EQUAL(c.success, 2); + BOOST_CHECK_EQUAL(c.finite, 2); + BOOST_CHECK_EQUAL(c.real, 0); // +/- i are not real + BOOST_CHECK_EQUAL(c.singular, 0); +} + + +// x^2 = 0 -> a double root at 0: singular (multiplicity 2, and ill-conditioned), finite, real. +BOOST_AUTO_TEST_CASE(singular_double_root) +{ + using namespace bertini; + System sys; + auto x = Variable::Make("x"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddFunction(x*x); + + auto zd = algorithm::ZeroDim::Cauchy, decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + zd.Solve(); + + auto const& md = zd.FinalSolutionMetadata(); + auto c = Tally(md); + BOOST_CHECK(c.success >= 1); // the endgame should reach the singular endpoint + BOOST_CHECK_EQUAL(c.singular, c.success); // every successful endpoint here is singular + BOOST_CHECK_EQUAL(c.finite, c.success); // ... and finite (at 0) + if (c.success == 2) // both paths clustered -> multiplicity 2 + for (auto const& m : md) + if (m.endgame_success == SuccessCode::Success) + BOOST_CHECK_EQUAL(m.multiplicity, 2); +} + + +// endpoint_finite_threshold is actually applied: lower it below the solutions' norm and the +// finite roots get classified as at infinity. +BOOST_AUTO_TEST_CASE(endpoint_finite_threshold_is_applied) +{ + using namespace bertini; + System sys; + auto x = Variable::Make("x"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddFunction(x*x - 1); // roots +/-1, infinity norm 1 + + auto zd = algorithm::ZeroDim::Cauchy, decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + auto pp = zd.Get(); + pp.endpoint_finite_threshold = 0.5; // 1 > 0.5 -> "at infinity" + zd.Set(pp); + zd.Solve(); + + auto c = Tally(zd.FinalSolutionMetadata()); + BOOST_CHECK_EQUAL(c.success, 2); + BOOST_CHECK_EQUAL(c.finite, 0); // the lowered threshold reclassifies both as infinite +} + + +// condition_number_threshold is actually applied: lower it below the (well-conditioned) roots' +// condition number and they get classified as singular. +BOOST_AUTO_TEST_CASE(condition_number_threshold_is_applied) +{ + using namespace bertini; + System sys; + auto x = Variable::Make("x"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddFunction(x*x - 1); // simple, well-conditioned roots + + auto zd = algorithm::ZeroDim::Cauchy, decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + auto pp = zd.Get(); + pp.condition_number_threshold = 1e-3; // any condition number exceeds this + zd.Set(pp); + zd.Solve(); + + auto c = Tally(zd.FinalSolutionMetadata()); + BOOST_CHECK_EQUAL(c.success, 2); + BOOST_CHECK_EQUAL(c.singular, 2); // reclassified singular purely by the lowered threshold +} + + +// the PostProcessing config round-trips through Get/Set. +BOOST_AUTO_TEST_CASE(postprocessing_config_roundtrip) +{ + using namespace bertini; + System sys; + auto x = Variable::Make("x"); + sys.AddVariableGroup(VariableGroup{x}); + sys.AddFunction(x*x - 1); + + auto zd = algorithm::ZeroDim::Cauchy, decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + + auto pp = zd.Get(); + pp.endpoint_finite_threshold = 12345.0; + pp.same_point_tolerance_multiplier = 7.0; + pp.condition_number_threshold = 99.0; + pp.real_threshold = 1e-3; + zd.Set(pp); + + auto pp2 = zd.Get(); + BOOST_CHECK_CLOSE(pp2.endpoint_finite_threshold, 12345.0, 1e-10); + BOOST_CHECK_CLOSE(pp2.same_point_tolerance_multiplier, 7.0, 1e-10); + BOOST_CHECK_CLOSE(pp2.condition_number_threshold, 99.0, 1e-10); + BOOST_CHECK_CLOSE(pp2.real_threshold, 1e-3, 1e-10); +} + + +// the PostProcessing defaults match Bertini 1 (guards against the inverted endpoint-finite default +// that used to ship, 1e-5 instead of 1e5). +BOOST_AUTO_TEST_CASE(postprocessing_config_defaults_match_bertini1) +{ + bertini::algorithm::PostProcessingConfig pp; + BOOST_CHECK_CLOSE(pp.endpoint_finite_threshold, 1e5, 1e-8); + BOOST_CHECK_CLOSE(pp.same_point_tolerance_multiplier, 10.0, 1e-8); + BOOST_CHECK_CLOSE(pp.condition_number_threshold, 1e8, 1e-8); + BOOST_CHECK_CLOSE(pp.real_threshold, 1e-8, 1e-8); +} + + +// Observe a whole zero-dim solve: AlgorithmStarted once, a PathStarted/PathComplete +// pair per path, AlgorithmComplete once. The observer attaches to the ZeroDim itself +// (the AnyZeroDim emitter), not to the tracker. +namespace { + +struct ZeroDimLifecycleCounter : public bertini::Observer +{ + int started = 0, completed = 0, path_begin = 0, path_end = 0; + + bertini::ObserveResult Observe(bertini::AnyEvent const& e) override + { + using namespace bertini::algorithm; + if (dynamic_cast*>(&e)) ++started; + else if (dynamic_cast*>(&e)) ++completed; + else if (dynamic_cast*>(&e)) ++path_begin; + else if (dynamic_cast*>(&e)) ++path_end; + return bertini::ObserveResult::KeepObserving; + } +}; + +} // anon namespace + +BOOST_AUTO_TEST_CASE(zerodim_emits_lifecycle_events) +{ + using namespace bertini; + using namespace tracking; + + auto sys = system::Precon::GriewankOsborn(); + auto zd = algorithm::ZeroDim::Cauchy, + decltype(sys), start_system::TotalDegree>(sys); + zd.DefaultSetup(); + + ZeroDimLifecycleCounter counter; + zd.AddObserver(counter); // attaches to the ZeroDim (accepts an AnyZeroDim observer) + + zd.Solve(); + + BOOST_CHECK_EQUAL(counter.started, 1); + BOOST_CHECK_EQUAL(counter.completed, 1); + BOOST_CHECK_GT(counter.path_begin, 0); + BOOST_CHECK_EQUAL(counter.path_begin, counter.path_end); // every path that began also completed +} + + +BOOST_AUTO_TEST_CASE(zerodim_rejects_a_tracker_observer) +{ + using namespace bertini; + using namespace tracking; + + auto sys = system::Precon::GriewankOsborn(); + auto zd = algorithm::ZeroDim::Cauchy, + decltype(sys), start_system::TotalDegree>(sys); + + // a tracker observer is for a tracker, not the ZeroDim -> rejected + GoryDetailLogger tracker_observer; + BOOST_CHECK_THROW(zd.AddObserver(tracker_observer), bertini::IncompatibleObserver); +} + + BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/pools/pool_test.cpp b/core/test/pools/pool_test.cpp index b370205da..932be317d 100644 --- a/core/test/pools/pool_test.cpp +++ b/core/test/pools/pool_test.cpp @@ -49,7 +49,7 @@ using namespace bertini; BOOST_AUTO_TEST_CASE(make_system_pool) { - SystemPool sp; + [[maybe_unused]] SystemPool sp; } @@ -123,7 +123,7 @@ using namespace bertini; BOOST_AUTO_TEST_CASE(make_double_point_point) { - PointPool pool; + [[maybe_unused]] PointPool pool; } BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/settings/settings_test.cpp b/core/test/settings/settings_test.cpp index a2f4af7a7..f5ff5e0d4 100644 --- a/core/test/settings/settings_test.cpp +++ b/core/test/settings/settings_test.cpp @@ -177,7 +177,6 @@ BOOST_AUTO_TEST_CASE(read_security) { using namespace bertini::parsing::classic; using namespace bertini::tracking; - using T = double; double tol = 1e-16; SplitInputFile inputfile = ParseInputFile("Config \n heLlo: 9 \n SecurityLevel: 1;\n SecurityMaxNorm: -2.34; % the predictor type\n NeWTon: 1; \n end; \n iNpUt % \n \n variable x; \n ENd;"); @@ -276,11 +275,10 @@ BOOST_AUTO_TEST_CASE(read_tolerances) { using namespace bertini::parsing::classic; using namespace bertini::tracking; - using T = double; bertini::DefaultPrecision(30); - double tol = 1e-15; + [[maybe_unused]] double tol = 1e-15; SplitInputFile inputfile = ParseInputFile("Config \n heLlo: 9 \n FinalTol : -.845e-7 ;\n TrackTolDuringEG : 234e-4 ; % the predictor type\n TrackTolBeforeEG : 7.32e3; \n end; \n iNpUt % \n \n variable x; \n ENd;"); @@ -307,46 +305,78 @@ BOOST_AUTO_TEST_CASE(read_tolerances) -BOOST_AUTO_TEST_CASE(read_stepping) +BOOST_AUTO_TEST_CASE(read_postprocessing) { - bool check_rational = false; - using namespace bertini::parsing::classic; using namespace bertini::tracking; - using T = double; - - + bertini::DefaultPrecision(30); + double tol = 1e-15; + + // all four PostProcessing settings present, in scrambled order, with noise between them. + SplitInputFile inputfile = ParseInputFile("Config \n heLlo: 9 \n EndpointSameThreshold: 25; \n CondNumThreshold : 1e6 ; \n ImagThreshold: 1.5e-6;\n EndpointFiniteThreshold : 1e4 ; % a comment\n end; \n iNpUt % \n \n variable x; \n ENd;"); + + std::string configStr = inputfile.Config(); + + std::string::const_iterator iter = configStr.begin(); + std::string::const_iterator end = configStr.end(); + + algorithm::PostProcessingConfig pp; + ConfigSettingParser parser; + bool parsed = phrase_parse(iter, end, parser, boost::spirit::ascii::space, pp); + + BOOST_CHECK(parsed && iter == end); + BOOST_CHECK(abs(pp.real_threshold - 1.5e-6) < tol); + BOOST_CHECK(abs(pp.endpoint_finite_threshold - 1e4) < tol); + BOOST_CHECK(abs(pp.same_point_tolerance_multiplier - 25) < tol); + BOOST_CHECK(abs(pp.condition_number_threshold - 1e6) < tol); + + + // CondNumThreshold omitted -> its Bertini-1 default (1e8) must survive. + SplitInputFile inputfile2 = ParseInputFile("Config \n ImagThreshold: 1e-7; \n end; \n iNpUt % \n \n variable x; \n ENd;"); + + std::string configStr2 = inputfile2.Config(); + + std::string::const_iterator iter2 = configStr2.begin(); + std::string::const_iterator end2 = configStr2.end(); + + algorithm::PostProcessingConfig pp2; + bool parsed2 = phrase_parse(iter2, end2, parser, boost::spirit::ascii::space, pp2); + + BOOST_CHECK(parsed2 && iter2 == end2); + BOOST_CHECK(abs(pp2.real_threshold - 1e-7) < tol); + BOOST_CHECK(abs(pp2.condition_number_threshold - 1e8) < tol); // default, not set in input +} + + + +BOOST_AUTO_TEST_CASE(read_stepping) +{ + using namespace bertini::parsing::classic; + using namespace bertini::tracking; + + + mpfr_float tol{"1e-25"}; // settings are parsed as mpfr_float at current default precision, so values are far more accurate than double SplitInputFile inputfile = ParseInputFile("Config \n heLlo: 9 \n StepSuccessFactor : 4.2; FinalTol: 1.845e-7;\n MaxNumberSteps: 234; % the predictor type\nMaxStepSize: 1e-2; StepsForIncrease: 7;\n end; \n iNpUt % \n \n variable x; \n ENd;"); - - + + std::string configStr = inputfile.Config(); - - + + std::string::const_iterator iter = configStr.begin(); std::string::const_iterator end = configStr.end(); - - + + SteppingConfig structure; ConfigSettingParser parser; bool parsed = phrase_parse(iter, end, parser,boost::spirit::ascii::space, structure); - - + + BOOST_CHECK(parsed && iter == end); - if (check_rational) - { - BOOST_CHECK_EQUAL(structure.max_step_size, mpq_rational(1,100)); - BOOST_CHECK_EQUAL(structure.step_size_success_factor, mpq_rational(42,10)); - BOOST_CHECK_EQUAL(structure.step_size_fail_factor, mpq_rational(1/2)); - } - else - { - double tol_double = 1e-14; - BOOST_CHECK_CLOSE(structure.max_step_size, mpq_rational(1,100), tol_double); - BOOST_CHECK_CLOSE(structure.step_size_success_factor, mpq_rational(42,10), tol_double); - BOOST_CHECK_CLOSE(structure.step_size_fail_factor, mpq_rational(1/2), tol_double); - } + BOOST_CHECK(abs(structure.max_step_size - mpfr_float("1e-2")) < tol); + BOOST_CHECK(abs(structure.step_size_success_factor - mpfr_float("4.2")) < tol); + BOOST_CHECK(abs(structure.step_size_fail_factor - mpfr_float(1)/2) < tol); // not set in the input, so should be the default value BOOST_CHECK_EQUAL(structure.consecutive_successful_steps_before_stepsize_increase, 7); BOOST_CHECK_EQUAL(structure.max_num_steps, 234); @@ -362,14 +392,13 @@ BOOST_AUTO_TEST_CASE(read_newton) { using namespace bertini::parsing::classic; using namespace bertini::tracking; - using T = double; - double tol = 1e-15; + [[maybe_unused]] double tol = 1e-15; SplitInputFile inputfile = ParseInputFile("Config \n MaxNewtonIts : 5; \n heLlo: 9 \n StepSuccessFactor: 4.2; end; \n iNpUt % \n \n variable x; \n ENd;"); - std::string configStr = inputfile.Config(); + [[maybe_unused]] std::string configStr = inputfile.Config(); std::string::const_iterator iter = configStr.begin(); @@ -410,11 +439,10 @@ BOOST_AUTO_TEST_CASE(read_endgame) { using namespace bertini::parsing::classic; using namespace bertini::tracking; - using T = double; bertini::DefaultPrecision(30); - double tol = 1e-15; + [[maybe_unused]] double tol = 1e-15; SplitInputFile inputfile = ParseInputFile("Config \n heLlo: 9 \n NumSamplePoints: 34;\n TrackTolDuringEG: 234e-4; % the predictor type\n NbhdRadius: 4.3e-7; \n SampleFactor : 8e-3;\n end; \n iNpUt % \n \n variable x; \n ENd;"); @@ -446,10 +474,9 @@ BOOST_AUTO_TEST_CASE(read_powerseries) { using namespace bertini::parsing::classic; using namespace bertini::tracking; - using T = double; - double tol = 1e-15; + [[maybe_unused]] double tol = 1e-15; SplitInputFile inputfile = ParseInputFile("Config \n MaxNewtonIts: 5; \n heLlo: 9 \n MaxCycleNum : 4; StepSuccessFactor: 4.2; end; \n iNpUt % \n \n variable x; \n ENd;"); @@ -492,10 +519,9 @@ BOOST_AUTO_TEST_CASE(read_cauchy) { using namespace bertini::parsing::classic; using namespace bertini::tracking; - using T = double; - double tol = 1e-15; + [[maybe_unused]] double tol = 1e-15; SplitInputFile inputfile = ParseInputFile("Config \n heLlo: 9 \n StepSuccessFactor: 4.2; CycleTimeCutoff: 5.76e2 ;\n MaxNumberSteps: 234; % the predictor type \n RAtioTimeCutoff: 1e-12; StepsForIncrease: 7;\n end; \n iNpUt % \n \n variable x; \n ENd;"); @@ -568,7 +594,7 @@ BOOST_AUTO_TEST_CASE(all_config_settings) { using namespace bertini::parsing::classic; using namespace bertini::tracking; - double tol = 1e-15; + [[maybe_unused]] double tol = 1e-15; SplitInputFile inputfile = ParseInputFile("Config \n ODEPredictor: 7; \n MPType: 0; \n MaxNewtonIts: 7; FinalTol: 1.845e-7;\n SampleFactor: 0.647; \n NumSamplePoints: 7;\n\n end; \n iNpUt % \n \n variable x; \n ENd;"); @@ -584,11 +610,11 @@ BOOST_AUTO_TEST_CASE(all_config_settings) bertini::endgame::EndgameConfig end = std::get(sets); BOOST_CHECK(pred == Predictor::RKDormandPrince56); - BOOST_CHECK_EQUAL( steps.max_step_size, mpq_rational(1,10)); + BOOST_CHECK_EQUAL( steps.max_step_size, mpfr_float(1)/10); // not set in the input, so should be the default value BOOST_CHECK_EQUAL(newt.max_num_newton_iterations, 7); BOOST_CHECK_EQUAL(newt.min_num_newton_iterations, 1); BOOST_CHECK(abs(tols.final_tolerance - 1.845e-7) < tol); - BOOST_CHECK_CLOSE( end.sample_factor, mpq_rational(647,1000), tol); + BOOST_CHECK(abs(end.sample_factor - mpfr_float("0.647")) < mpfr_float("1e-25")); BOOST_CHECK_EQUAL(end.num_sample_points, 7); BOOST_CHECK_EQUAL(end.min_track_time, 1e-100); } diff --git a/core/test/tracking_basics/amp_criteria_test.cpp b/core/test/tracking_basics/amp_criteria_test.cpp index d7bcea5fc..b1438d1f2 100644 --- a/core/test/tracking_basics/amp_criteria_test.cpp +++ b/core/test/tracking_basics/amp_criteria_test.cpp @@ -58,8 +58,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; BOOST_AUTO_TEST_CASE(AMP_criteriaA_double) diff --git a/core/test/tracking_basics/amp_jacobian_estimate_test.cpp b/core/test/tracking_basics/amp_jacobian_estimate_test.cpp new file mode 100644 index 000000000..e7362598a --- /dev/null +++ b/core/test/tracking_basics/amp_jacobian_estimate_test.cpp @@ -0,0 +1,135 @@ +//This file is part of Bertini 2. +// +//amp_jacobian_estimate_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//amp_jacobian_estimate_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with amp_jacobian_estimate_test.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file amp_jacobian_estimate_test.cpp + +\brief Verifies the ||J^{-1}|| estimate the AMP corrector uses to drive precision. + +The corrector estimates ||J^{-1}|| as ||J^{-1} r|| for a random unit-modulus vector r (one LU +solve), and feeds it into CriterionB -> DigitsB. The AMP-escalation investigation found DigitsB +spiking to ~147 (forcing precision to ~140 digits). Since DigitsB ~ log10(||J^{-1}|| * (...)), +that requires ||J^{-1}|| ~ 1e145 -- IF the estimate is faithful. These tests check that the +estimate faithfully tracks the true spectral ||J^{-1}|| = 1/sigma_min(J): it is a lower bound up +to the ||r||=sqrt(n) factor, and it does NOT spuriously inflate by many orders of magnitude. So +a reported huge ||J^{-1}|| reflects a genuine near-singularity, not an estimation artifact. +*/ + +#include + +#include +#include + +#include "bertini2/mpfr_complex.hpp" +#include "bertini2/eigen_extensions.hpp" + +BOOST_AUTO_TEST_SUITE(amp_jacobian_estimate) + +using dbl = bertini::dbl; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; + +namespace { + + // The corrector's estimate: ||J^{-1} r|| for a random unit-modulus r (max over a few draws, + // as a path accumulates many draws). This mirrors newton_corrector.hpp. + double NormJInverseEstimate(Mat const& J, int draws = 12) + { + double best = 0.0; + auto lu = J.partialPivLu(); + for (int i = 0; i < draws; ++i) + { + Vec r = bertini::RandomOfUnits(static_cast(J.cols())); + best = std::max(best, lu.solve(r).norm()); + } + return best; + } + + // True spectral ||J^{-1}|| = 1 / smallest singular value of J. + double TrueNormJInverse(Mat const& J) + { + Eigen::JacobiSVD> svd(J); + double sigma_min = svd.singularValues()(svd.singularValues().size() - 1); + return 1.0 / sigma_min; + } + +} // anonymous namespace + + +// For well-conditioned matrices the estimate is the right order of magnitude (it never reports a +// huge ||J^{-1}|| for a benign matrix -- the spike is not manufactured here). +BOOST_AUTO_TEST_CASE(estimate_tracks_truth_well_conditioned) +{ + for (int n = 2; n <= 6; ++n) + for (int trial = 0; trial < 20; ++trial) + { + Mat J = Mat::Random(n, n); + double est = NormJInverseEstimate(J); + double tru = TrueNormJInverse(J); + + // estimate is a lower bound up to the ||r|| = sqrt(n) factor: est <= sqrt(n)*true. + BOOST_CHECK_LE(est, std::sqrt(double(n)) * tru * (1 + 1e-9)); + // and it captures the magnitude: not absurdly small either. + BOOST_CHECK_GE(est, tru / (100.0 * n)); + } +} + + +// A deliberately near-singular matrix with sigma_min ~ 1e-k. Representing this conditioning at +// all requires precision > k digits -- exactly why AMP escalates -- so this is done in mpfr. The +// estimate must report ~1e+k: it faithfully reflects the genuine ill-conditioning, neither missing +// it nor inflating it by orders. This is the case the DigitsB~147 spike falls into. +BOOST_AUTO_TEST_CASE(estimate_reflects_genuine_near_singularity) +{ + using mpfr_complex = bertini::mpfr_complex; + using mpfr_float = bertini::mpfr_float; + + for (int k = 20; k <= 140; k += 40) + { + bertini::DefaultPrecision(k + 50); // enough digits to represent eps = 1e-k + + mpfr_float eps = pow(mpfr_float(10), -k); + // J = [[1, 1], [1, 1+eps]] : det = eps, so ||J^{-1}||_2 ~ 2/eps ~ 1e+k (analytic). + Mat J(2, 2); + J(0,0) = mpfr_complex(1); J(0,1) = mpfr_complex(1); + J(1,0) = mpfr_complex(1); J(1,1) = mpfr_complex(1) + mpfr_complex(eps); + + auto lu = J.partialPivLu(); + mpfr_float best(0); + for (int i = 0; i < 12; ++i) + { + Vec r = bertini::RandomOfUnits(2); + mpfr_float nrm = lu.solve(r).norm(); + if (nrm > best) best = nrm; + } + + double log_est = static_cast(log10(best)); + double log_tru = static_cast(log10(mpfr_float(2) / eps)); // ~ k + + // the estimate is the right order of magnitude (within ~2 decades) of the true value -- + // so a DigitsB spike of ~147 means a genuinely ~1e145 ||J^{-1}||, NOT an estimation artifact. + BOOST_CHECK_SMALL(log_est - log_tru, 2.0); // est <= tru * 1e2 + BOOST_CHECK_GT(log_est, log_tru - 2.0); // est >= tru / 1e2 + } +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/tracking_basics/amp_tracker_test.cpp b/core/test/tracking_basics/amp_tracker_test.cpp index e1ddf4fd0..a64609943 100644 --- a/core/test/tracking_basics/amp_tracker_test.cpp +++ b/core/test/tracking_basics/amp_tracker_test.cpp @@ -52,8 +52,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using bertini::DefaultPrecision; BOOST_AUTO_TEST_CASE(minstepsize) @@ -857,7 +857,7 @@ BOOST_AUTO_TEST_CASE(AMP_track_total_degree_start_system) std::vector > track_total_degree(bertini::tracking::AMPTracker const& tracker, bertini::start_system::TotalDegree const& TD) { - auto initial_precision = DefaultPrecision(); + [[maybe_unused]] auto initial_precision = DefaultPrecision(); using namespace bertini::tracking; mpfr t_start(1), t_end(0); std::vector > solutions; @@ -946,6 +946,177 @@ BOOST_AUTO_TEST_CASE(AMP_track_TD_functionalized) } +BOOST_AUTO_TEST_CASE(AMP_tracker_track_circle_line_RKF45) +{ + // Regression test: RKF45 + AMPTracker used to throw MinimizeTrackingCost + // because AdjustAMPStepSuccess capped max_precision at current_precision_, + // collapsing the scan window to a single value that failed criterion B. + DefaultPrecision(30); + using namespace bertini::tracking; + + Var x = Variable::Make("x"); + Var y = Variable::Make("y"); + Var t = Variable::Make("t"); + + System sys; + VariableGroup v{x, y}; + sys.AddVariableGroup(v); + sys.AddPathVariable(t); + sys.AddFunction(t * (pow(x, 2) - 1) + (1 - t) * (pow(x, 2) + pow(y, 2) - 4)); + sys.AddFunction(t * (y - 1) + (1 - t) * (2 * x + 5 * y)); + + auto AMP = bertini::tracking::AMPConfigFrom(sys); + + bertini::tracking::AMPTracker tracker(sys); + SteppingConfig stepping_preferences; + NewtonConfig newton_preferences; + + tracker.Setup(Predictor::RKF45, + 1e-5, + 1e5, + stepping_preferences, + newton_preferences); + tracker.PrecisionSetup(AMP); + + mpfr t_start(1); + mpfr t_end(0); + + Vec start_point(2); + start_point << mpfr(1), mpfr(1); + + Vec end_point; + bertini::SuccessCode tracking_success; + tracking_success = tracker.TrackPath(end_point, t_start, t_end, start_point); + + BOOST_CHECK(tracking_success == bertini::SuccessCode::Success); + BOOST_CHECK_EQUAL(end_point.size(), 2); +} + + +BOOST_AUTO_TEST_CASE(arithmetic_cost_double_precision_is_one) +{ + using namespace bertini::tracking; + BOOST_CHECK_EQUAL(ArithmeticCost(bertini::DoublePrecision()), 1.0); +} + +BOOST_AUTO_TEST_CASE(arithmetic_cost_increases_with_precision) +{ + using namespace bertini::tracking; + BOOST_CHECK_GT(ArithmeticCost(30), 1.0); + BOOST_CHECK_GT(ArithmeticCost(100), ArithmeticCost(30)); +} + +// StepsizeSatisfyingCriterionB(p, digits_B=50, newton=2, predictor=0) = 10^(-(50-p)*2). +// At p<=40 this is <= 1e-20, well below min_stepsize=1e-5, so those precisions are +// skipped. Only p=50 survives (stepsize=1), so that must be the result. +BOOST_AUTO_TEST_CASE(minimize_tracking_cost_skips_precision_below_min_stepsize) +{ + DefaultPrecision(50); + using namespace bertini::tracking; + + unsigned new_precision = 0; + mpfr_float new_stepsize("0"); + mpfr_float min_stepsize("1e-5"); + mpfr_float max_stepsize("1"); + + MinimizeTrackingCost(new_precision, new_stepsize, + bertini::DoublePrecision(), min_stepsize, + 50u, max_stepsize, + 50u, 2u, 0u); + + BOOST_CHECK_EQUAL(new_precision, 50u); + BOOST_CHECK(new_stepsize >= min_stepsize); +} + +// With min_stepsize=2, max_precision=30, digits_B=30, newton=2: +// criterion B gives at most stepsize=1 (at p=30), which is < 2, so nothing passes. +BOOST_AUTO_TEST_CASE(minimize_tracking_cost_throws_when_no_precision_satisfies_min_stepsize) +{ + DefaultPrecision(30); + using namespace bertini::tracking; + + unsigned new_precision = 0; + mpfr_float new_stepsize("0"); + mpfr_float min_stepsize("2"); + mpfr_float max_stepsize("10"); + + BOOST_CHECK_THROW( + MinimizeTrackingCost(new_precision, new_stepsize, + bertini::DoublePrecision(), min_stepsize, + 30u, max_stepsize, + 30u, 2u, 0u), + std::runtime_error + ); +} + +BOOST_AUTO_TEST_CASE(set_start_precision_overrides_to_higher_precision) +{ + DefaultPrecision(16); + using namespace bertini::tracking; + + Var y = Variable::Make("y"); + Var t = Variable::Make("t"); + + System sys; + VariableGroup v{y}; + sys.AddFunction(y - t); + sys.AddPathVariable(t); + sys.AddVariableGroup(v); + + auto AMP = bertini::tracking::AMPConfigFrom(sys); + bertini::tracking::AMPTracker tracker(sys); + + SteppingConfig stepping_preferences; + NewtonConfig newton_preferences; + tracker.Setup(Predictor::Euler, 1e-5, 1e5, stepping_preferences, newton_preferences); + tracker.PrecisionSetup(AMP); + tracker.SetStartPrecision(30); + + Vec y_start(1); + y_start << mpfr(1); + mpfr t_start(1), t_end(0); + Vec y_end; + + auto code = tracker.TrackPath(y_end, t_start, t_end, y_start); + BOOST_CHECK(code == bertini::SuccessCode::Success); + BOOST_CHECK(abs(y_end(0) - mpfr(0)) < 1e-5); +} + +BOOST_AUTO_TEST_CASE(set_start_precision_clear_restores_default_behavior) +{ + DefaultPrecision(30); + using namespace bertini::tracking; + + Var y = Variable::Make("y"); + Var t = Variable::Make("t"); + + System sys; + VariableGroup v{y}; + sys.AddFunction(y - t); + sys.AddPathVariable(t); + sys.AddVariableGroup(v); + + auto AMP = bertini::tracking::AMPConfigFrom(sys); + bertini::tracking::AMPTracker tracker(sys); + + SteppingConfig stepping_preferences; + NewtonConfig newton_preferences; + tracker.Setup(Predictor::Euler, 1e-5, 1e5, stepping_preferences, newton_preferences); + tracker.PrecisionSetup(AMP); + tracker.SetStartPrecision(50); + tracker.SetStartPrecision(); // clear override + + Vec y_start(1); + y_start << mpfr(1); + mpfr t_start(1), t_end(0); + Vec y_end; + + auto code = tracker.TrackPath(y_end, t_start, t_end, y_start); + BOOST_CHECK(code == bertini::SuccessCode::Success); + BOOST_CHECK(abs(y_end(0) - mpfr(0)) < 1e-5); +} + + BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/tracking_basics/euler_test.cpp b/core/test/tracking_basics/euler_test.cpp index f1841fed3..35a43d2e2 100644 --- a/core/test/tracking_basics/euler_test.cpp +++ b/core/test/tracking_basics/euler_test.cpp @@ -60,8 +60,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using bertini::DefaultPrecision; using bertini::Precision; @@ -368,7 +368,7 @@ BOOST_AUTO_TEST_CASE(circle_line_euler_double) sys.AddFunction( t*(pow(x,2)-1) + (1-t)*(pow(x,2) + pow(y,2) - 4) ); sys.AddFunction( t*(y-1) + (1-t)*(2*x - 5*y) ); - auto AMP = bertini::tracking::AMPConfigFrom(sys); + [[maybe_unused]] auto AMP = bertini::tracking::AMPConfigFrom(sys); AMP.coefficient_bound = 5; @@ -428,7 +428,7 @@ BOOST_AUTO_TEST_CASE(circle_line_euler_double) sys.AddFunction( t*(pow(x,2)-1) + (1-t)*(pow(x,2) + pow(y,2) - 4) ); sys.AddFunction( t*(y-1) + (1-t)*(2*x - 5*y) ); - auto AMP = bertini::tracking::AMPConfigFrom(sys); + [[maybe_unused]] auto AMP = bertini::tracking::AMPConfigFrom(sys); AMP.coefficient_bound = 5; @@ -486,7 +486,7 @@ BOOST_AUTO_TEST_CASE(circle_line_euler_double) auto AMP = bertini::tracking::AMPConfigFrom(sys); - double norm_J, norm_J_inverse, size_proportion, error_est; + [[maybe_unused]] double norm_J, norm_J_inverse, size_proportion, error_est; AMP.coefficient_bound = 5; AMP.safety_digits_1 = 100; @@ -547,7 +547,7 @@ BOOST_AUTO_TEST_CASE(circle_line_euler_double) auto AMP = bertini::tracking::AMPConfigFrom(sys); - double norm_J, norm_J_inverse, size_proportion, error_est; + [[maybe_unused]] double norm_J, norm_J_inverse, size_proportion, error_est; AMP.coefficient_bound = 5; AMP.safety_digits_1 = 100; @@ -608,7 +608,7 @@ BOOST_AUTO_TEST_CASE(circle_line_euler_double) auto AMP = bertini::tracking::AMPConfigFrom(sys); - double norm_J, norm_J_inverse, size_proportion, error_est; + [[maybe_unused]] double norm_J, norm_J_inverse, size_proportion, error_est; AMP.coefficient_bound = 5; AMP.safety_digits_2 = 100; @@ -670,7 +670,7 @@ BOOST_AUTO_TEST_CASE(circle_line_euler_double) auto AMP = bertini::tracking::AMPConfigFrom(sys); - double norm_J, norm_J_inverse, size_proportion, error_est; + [[maybe_unused]] double norm_J, norm_J_inverse, size_proportion, error_est; AMP.coefficient_bound = 5; AMP.safety_digits_2 = 100; diff --git a/core/test/tracking_basics/expand_to_function_tree_test.cpp b/core/test/tracking_basics/expand_to_function_tree_test.cpp new file mode 100644 index 000000000..11bfd3ed2 --- /dev/null +++ b/core/test/tracking_basics/expand_to_function_tree_test.cpp @@ -0,0 +1,207 @@ +//This file is part of Bertini 2. +// +//expand_to_function_tree_test.cpp is free software: you can redistribute it and/or modify +//it under the terms of the GNU General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//expand_to_function_tree_test.cpp is distributed in the hope that it will be useful, +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU General Public License for more details. +// +//You should have received a copy of the GNU General Public License +//along with expand_to_function_tree_test.cpp. If not, see . +// +// Copyright(C) Bertini2 Development Team +// +// See for a copy of the license, +// as well as COPYING. Bertini2 is provided with permitted +// additional terms in the b2/licenses/ directory. + +/** +\file expand_to_function_tree_test.cpp + +\brief Tests for System::ExpandToFunctionTree() -- the pure-function-tree twin of a +block-composed system. The contract is that the expanded system evaluates and differentiates +identically to the block system, at every point. This is the verification oracle used by the +AMP precision-escalation investigation to compare the block-composed homotopy's Jacobian against +an identical function-tree homotopy. +*/ + +#include + +#include "bertini2/system/system.hpp" +#include "bertini2/system/blocks/products_of_linears_block.hpp" +#include "bertini2/system/blocks/blend_block.hpp" +#include "bertini2/function_tree.hpp" + +BOOST_AUTO_TEST_SUITE(expand_to_function_tree) + +using System = bertini::System; +using Variable = bertini::node::Variable; +using VariableGroup = bertini::VariableGroup; +using dbl = bertini::dbl; +using mpfr_complex = bertini::mpfr_complex; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; + +namespace { + + // scale-relative comparison: |a-b| <= tol * (1 + |a|), entrywise. + template + void CheckClose(Eigen::MatrixBase const& a, Eigen::MatrixBase const& b, double tol) + { + BOOST_REQUIRE_EQUAL(a.rows(), b.rows()); + BOOST_REQUIRE_EQUAL(a.cols(), b.cols()); + for (Eigen::Index i = 0; i < a.rows(); ++i) + for (Eigen::Index j = 0; j < a.cols(); ++j) + { + double err = static_cast(abs(a(i, j) - b(i, j))); + double scale = 1.0 + static_cast(abs(a(i, j))); + BOOST_CHECK_SMALL(err / scale, tol); + } + } + + Vec RandomVecD(int n) + { + return Vec::Random(n); + } + + // compare block vs expanded system on eval + Jacobian at a point (autonomous overload) + template + void CompareEvalJac(System const& blk, System const& exp, Vec const& p, double tol) + { + CheckClose(blk.Eval(p), exp.Eval(p), tol); + CheckClose(blk.Jacobian(p), exp.Jacobian(p), tol); + } + + // ... and with a path-variable value + template + void CompareEvalJac(System const& blk, System const& exp, Vec const& p, T const& t, double tol) + { + CheckClose(blk.Eval(p, t), exp.Eval(p, t), tol); + CheckClose(blk.Jacobian(p, t), exp.Jacobian(p, t), tol); + } + +} // anonymous namespace + + +// A plain polynomial system is already a function tree; expansion must be a numeric identity. +BOOST_AUTO_TEST_CASE(polynomial_block_roundtrip) +{ + using namespace bertini; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + + System sys; + sys.AddVariableGroup(VariableGroup{x, y}); + sys.AddFunction(x * y - 1); + sys.AddFunction(pow(x, 2) + y - 3); + + System expanded = sys.ExpandToFunctionTree(); + + for (int trial = 0; trial < 8; ++trial) + CompareEvalJac(sys, expanded, RandomVecD(2), 1e-13); + + DefaultPrecision(50); + sys.precision(50); + expanded.precision(50); + for (int trial = 0; trial < 4; ++trial) + CompareEvalJac(sys, expanded, RandomOfUnits(2), 1e-40); +} + + +// A products-of-linears block: f_i = prod_r (linear form). The expansion rebuilds those +// products as nodes; block and expanded must agree on eval and Jacobian. +BOOST_AUTO_TEST_CASE(products_of_linears_block_matches) +{ + using namespace bertini; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + + System sys; + sys.AddVariableGroup(VariableGroup{x, y}); + + // f0 = (2x + 3y - 1)(x - y + 4); f1 = (x + 5)( -y + 2)( x + y ) (3 factors) + std::vector> factors; + { + Mat M0(2, 3); // rows = factors, cols = (x, y, const) + M0(0,0) = 2; M0(0,1) = 3; M0(0,2) = -1; + M0(1,0) = 1; M0(1,1) = -1; M0(1,2) = 4; + factors.push_back(M0); + + Mat M1(3, 3); + M1(0,0) = 1; M1(0,1) = 0; M1(0,2) = 5; + M1(1,0) = 0; M1(1,1) = -1; M1(1,2) = 2; + M1(2,0) = 1; M1(2,1) = 1; M1(2,2) = 0; + factors.push_back(M1); + } + sys.AddBlock(blocks::ProductsOfLinearsBlock(2, factors)); + + System expanded = sys.ExpandToFunctionTree(); + + for (int trial = 0; trial < 8; ++trial) + CompareEvalJac(sys, expanded, RandomVecD(2), 1e-12); + + DefaultPrecision(50); + sys.precision(50); + expanded.precision(50); + for (int trial = 0; trial < 4; ++trial) + CompareEvalJac(sys, expanded, RandomOfUnits(2), 1e-40); +} + + +// A blend block H = (1-t)*A + gamma*t*B. The expansion recurses into the operand systems and +// rebuilds the t-weighted sum; block and expanded must agree on eval and Jacobian (and the +// path-variable dependence). +BOOST_AUTO_TEST_CASE(blend_block_matches) +{ + using namespace bertini; + auto x = Variable::Make("x"); + auto y = Variable::Make("y"); + auto t = Variable::Make("t"); + + // operand A and operand B share the SAME variable nodes (x,y). + System A; + A.AddVariableGroup(VariableGroup{x, y}); + A.AddFunction(x * y - 1); + A.AddFunction(x + y); + + System B; + B.AddVariableGroup(VariableGroup{x, y}); + B.AddFunction(pow(x, 2) - 1); + B.AddFunction(pow(y, 2) - 1); + + auto gamma = node::Rational::Make(node::Rational::Rand()); + + System H; + H.AddVariableGroup(VariableGroup{x, y}); + H.AddPathVariable(t); + std::vector> coeffs{ 1 - t, gamma * t }; + std::vector> operands{ + std::make_shared(A), + std::make_shared(B) }; + H.AddBlock(blocks::BlendBlock(t, std::move(coeffs), std::move(operands))); + + System expanded = H.ExpandToFunctionTree(); + + for (int trial = 0; trial < 8; ++trial) + { + dbl tau = dbl(0.3, -0.2) * dbl(trial + 1); + CompareEvalJac(H, expanded, RandomVecD(2), tau, 1e-12); + } + + DefaultPrecision(50); + H.precision(50); + expanded.precision(50); + for (int trial = 0; trial < 4; ++trial) + { + Vec p = RandomOfUnits(2); + mpfr_complex tau = RandomOfUnits(1)(0); + CompareEvalJac(H, expanded, p, tau, 1e-40); + } +} + + +BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/tracking_basics/fixed_precision_tracker_test.cpp b/core/test/tracking_basics/fixed_precision_tracker_test.cpp index 14575401f..f46df6aa5 100644 --- a/core/test/tracking_basics/fixed_precision_tracker_test.cpp +++ b/core/test/tracking_basics/fixed_precision_tracker_test.cpp @@ -52,8 +52,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using bertini::DefaultPrecision; BOOST_AUTO_TEST_CASE(double_tracker_track_linear) { diff --git a/core/test/tracking_basics/heun_test.cpp b/core/test/tracking_basics/heun_test.cpp index e7650cb0a..1dede7d4e 100644 --- a/core/test/tracking_basics/heun_test.cpp +++ b/core/test/tracking_basics/heun_test.cpp @@ -59,8 +59,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; BOOST_AUTO_TEST_CASE(circle_line_heun_double) @@ -269,7 +269,7 @@ BOOST_AUTO_TEST_CASE(circle_line_heun_double) double predicted_error = 0.00544428757292458409463632380167773; Vec heun_prediction_result; - double next_time; + [[maybe_unused]] double next_time; double tracking_tolerance(1e-5); double condition_number_estimate; diff --git a/core/test/tracking_basics/higher_predictor_test.cpp b/core/test/tracking_basics/higher_predictor_test.cpp index 08cbbdb6f..1299e2210 100644 --- a/core/test/tracking_basics/higher_predictor_test.cpp +++ b/core/test/tracking_basics/higher_predictor_test.cpp @@ -59,8 +59,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using bertini::Precision; @@ -265,7 +265,7 @@ BOOST_AUTO_TEST_CASE(monodromy_RK4_d) dbl(0.731436941908745436117221142349986); Vec RK4_prediction_result; - double next_time; + [[maybe_unused]] double next_time; double tracking_tolerance(1e-5); double condition_number_estimate; @@ -595,7 +595,7 @@ BOOST_AUTO_TEST_CASE(monodromy_RKF45_d) double predicted_error = 7.17724133646795598396247354053062e-8; Vec RKF45_prediction_result; - double next_time; + [[maybe_unused]] double next_time; double tracking_tolerance(1e-5); double condition_number_estimate; @@ -945,7 +945,7 @@ BOOST_AUTO_TEST_CASE(monodromy_RKCK45_d) double predicted_error = 4.51352044466211707817977052519894e-9; Vec RKCK45_prediction_result; - double next_time; + [[maybe_unused]] double next_time; double tracking_tolerance(1e-5); double condition_number_estimate; @@ -1563,7 +1563,7 @@ BOOST_AUTO_TEST_CASE(monodromy_RKDP56_d) double predicted_error = 3.85904197101299548102733617445410e-9; Vec RKDP56_prediction_result; - double next_time; + [[maybe_unused]] double next_time; double tracking_tolerance(1e-5); double condition_number_estimate; @@ -1900,7 +1900,7 @@ BOOST_AUTO_TEST_CASE(monodromy_RKV67_d) double predicted_error = 1.42794733055750714441060080061e-8; Vec RKV67_prediction_result; - double next_time; + [[maybe_unused]] double next_time; double tracking_tolerance(1e-5); double condition_number_estimate; @@ -2012,6 +2012,124 @@ BOOST_AUTO_TEST_CASE(monodromy_RKV67_mp) } +// --- size_proportion / error_estimate behaviour (AMP precision-escalation work) ------------------- +// The default predictor was Euler (no error estimate); its size_proportion fallback +// maxCoeff(K)/|delta_t|^p blows up as the step shrinks (~1/|delta_t|), spuriously inflating AMP +// precision (DigitsB). RKF45 has an embedded error estimate, so size_proportion = +// err_est/|delta_t|^(p+1) is a bounded, stable "a" coefficient. These tests pin that and guard the +// default. (Previously size_proportion / error_estimate were computed in tests but never asserted.) + +namespace { + // build the smooth test homotopy + a generic on-path point used by the size_proportion tests. + struct PredFixture { + bertini::System sys; + Vec current_space{2}; + dbl current_time{0.9}; + bertini::tracking::AdaptiveMultiplePrecisionConfig AMP; + PredFixture() { + Var x = Variable::Make("x"), y = Variable::Make("y"), t = Variable::Make("t"); + sys.AddVariableGroup(VariableGroup{x,y}); + sys.AddPathVariable(t); + sys.AddFunction( t*(pow(x,2)-1) + (1-t)*(pow(x,2) + pow(y,2) - 4) ); + sys.AddFunction( t*(y-1) + (1-t)*(2*x + 5*y) ); + current_space << dbl(2.3,0.2), dbl(1.1,1.87); + AMP = bertini::tracking::AMPConfigFrom(sys); + AMP.coefficient_bound = 5; + } + }; +} + +BOOST_AUTO_TEST_CASE(default_predictor_is_rkf45) +{ + // guards the decision to default to an error-estimate predictor (was Euler, atrocious for + // performance and the cause of the size_proportion blow-up). + BOOST_CHECK(bertini::tracking::predict::DefaultPredictor() == bertini::tracking::Predictor::RKF45); +} + +BOOST_AUTO_TEST_CASE(rkf45_size_proportion_stays_bounded_as_step_shrinks) +{ + PredFixture f; + auto predictor = std::make_shared(bertini::tracking::Predictor::RKF45, f.sys); + + double tracking_tolerance(1e-5), cn(0); unsigned nsc(1), freq(1); + Vec result; double err_est(0), size_prop(0), nJ(0), nJinv(0); + + double sp_max = 0.0, sp_min = 1e300; + for (double h : {-0.1, -0.05, -0.025, -0.0125}) // step shrinks 8x; err_est stays above roundoff + { + auto code = predictor->Predict(result, err_est, size_prop, nJ, nJinv, f.sys, + f.current_space, f.current_time, dbl(h), + cn, nsc, freq, tracking_tolerance, f.AMP); + BOOST_REQUIRE(code == bertini::SuccessCode::Success); + BOOST_REQUIRE(size_prop > 0.0); + sp_max = std::max(sp_max, size_prop); + sp_min = std::min(sp_min, size_prop); + } + // RKF45's size_proportion ~ constant: it stays within a small factor as the step shrinks 8x. + // Euler's fallback maxCoeff(K)/|delta_t| would vary by ~8x over the same range (and unboundedly + // as the step keeps shrinking) -- which is what spuriously escalated precision. + BOOST_CHECK_LT(sp_max / sp_min, 5.0); +} + +BOOST_AUTO_TEST_CASE(rkf45_error_estimate_has_order_p_plus_1) +{ + PredFixture f; + auto predictor = std::make_shared(bertini::tracking::Predictor::RKF45, f.sys); + const unsigned p = bertini::tracking::predict::Order(bertini::tracking::Predictor::RKF45); // 4 + + double tracking_tolerance(1e-5), cn(0); unsigned nsc(1), freq(1); + Vec result; double size_prop(0), nJ(0), nJinv(0); + + auto err_at = [&](double h) { + double err_est(0); + auto code = predictor->Predict(result, err_est, size_prop, nJ, nJinv, f.sys, + f.current_space, f.current_time, dbl(h), + cn, nsc, freq, tracking_tolerance, f.AMP); + BOOST_REQUIRE(code == bertini::SuccessCode::Success); + return err_est; + }; + + // error_estimate ~ C * h^(p+1): halving the step divides the estimate by 2^(p+1). + const double expected_ratio = std::pow(2.0, double(p + 1)); // 2^5 = 32 for RKF45 + for (double h : {-0.1, -0.05, -0.025}) // above the roundoff floor in double + { + double ratio = err_at(h) / err_at(h / 2.0); + // within 2x of the asymptotic ratio (higher-order terms perturb it a little) + BOOST_CHECK_GT(ratio, expected_ratio / 2.0); + BOOST_CHECK_LT(ratio, expected_ratio * 2.0); + } +} + + +BOOST_AUTO_TEST_CASE(euler_size_proportion_stays_bounded_as_step_shrinks) +{ + // Regression for the non-error-estimate (Euler) size_proportion bug: AMP2 (Eqs 9-10) gives + // a = ||d||/|s| = ||K|| ~ maxCoeff(|K|), an O(1) step-independent constant. The old fallback + // maxCoeff(K)/|delta_t|^p over-divided by the step, so $a$ grew ~1/|delta_t| as the step shrank + // and spuriously escalated AMP precision. The corrected formula must stay (nearly) constant. + PredFixture f; + auto predictor = std::make_shared(bertini::tracking::Predictor::Euler, f.sys); + + double tracking_tolerance(1e-5), cn(0); unsigned nsc(1), freq(1); + Vec result; double size_prop(0), nJ(0), nJinv(0); + + double sp_max = 0.0, sp_min = 1e300; + for (double h : {-0.1, -0.05, -0.025, -0.0125}) // step shrinks 8x + { + // Euler has no error estimate, so use the size_proportion-only Predict overload. + auto code = predictor->Predict(result, size_prop, nJ, nJinv, f.sys, + f.current_space, f.current_time, dbl(h), + cn, nsc, freq, tracking_tolerance, f.AMP); + BOOST_REQUIRE(code == bertini::SuccessCode::Success); + BOOST_REQUIRE(size_prop > 0.0); + sp_max = std::max(sp_max, size_prop); + sp_min = std::min(sp_min, size_prop); + } + // ||K|| (the tangent magnitude) barely moves over a smooth path as the step shrinks 8x; the + // buggy maxCoeff(K)/|delta_t| would vary by ~8x over the same range (and unboundedly as h->0). + BOOST_CHECK_LT(sp_max / sp_min, 2.0); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/core/test/tracking_basics/newton_correct_test.cpp b/core/test/tracking_basics/newton_correct_test.cpp index 4ae7ffabb..689d38aa5 100644 --- a/core/test/tracking_basics/newton_correct_test.cpp +++ b/core/test/tracking_basics/newton_correct_test.cpp @@ -56,8 +56,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; using mpq_rational = bertini::mpq_rational; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using bertini::DefaultPrecision; diff --git a/core/test/tracking_basics/path_observers.cpp b/core/test/tracking_basics/path_observers.cpp index b0eed97cf..e916af402 100644 --- a/core/test/tracking_basics/path_observers.cpp +++ b/core/test/tracking_basics/path_observers.cpp @@ -27,6 +27,8 @@ #include +#include + #include "bertini2/trackers/amp_tracker.hpp" #include "bertini2/trackers/observers.hpp" @@ -57,8 +59,8 @@ using mpfr = bertini::mpfr_complex; using mpfr_float = bertini::mpfr_float; -template using Vec = bertini::Vec; -template using Mat = bertini::Mat; +template using Vec = bertini::Vec; +template using Mat = bertini::Mat; using bertini::DefaultPrecision; @@ -111,7 +113,7 @@ BOOST_AUTO_TEST_CASE(accumulate_single_path_square_root) tracker.AddObserver(precision_accumulator); start_point << mpfr(1), mpfr(1); - bertini::SuccessCode tracking_success = tracker.TrackPath(end_point, + [[maybe_unused]] bertini::SuccessCode tracking_success = tracker.TrackPath(end_point, t_start, t_end, start_point); } @@ -159,7 +161,7 @@ BOOST_AUTO_TEST_CASE(some_other_thing_square_root) Vec start_point(2); Vec end_point; - bertini::SuccessCode tracking_success; + [[maybe_unused]] bertini::SuccessCode tracking_success; GoryDetailLogger tons_of_detail; @@ -220,7 +222,7 @@ BOOST_AUTO_TEST_CASE(union_of_observers) tracker.AddObserver(agglomeration); - bertini::SuccessCode tracking_success = tracker.TrackPath(end_point, + [[maybe_unused]] bertini::SuccessCode tracking_success = tracker.TrackPath(end_point, t_start, t_end, start_point); } @@ -230,6 +232,189 @@ BOOST_AUTO_TEST_CASE(union_of_observers) +// --------------------------------------------------------------------------- +// Observer-dispatch refactor (#255): self-unsubscribe via return value, and +// subscribing a new observer from inside Observe() (the "meta-observer" trick). +// --------------------------------------------------------------------------- + +using bertini::AnyEvent; +using bertini::ObserveResult; +using bertini::tracking::AMPTracker; +using EmitterT = bertini::tracking::TrackerTraits::EventEmitterType; + +// Counts every event it receives, and keeps observing forever. +template +struct CountingObserver : public bertini::Observer +{ + int count = 0; + ObserveResult Observe(AnyEvent const&) override + { + ++count; + return ObserveResult::KeepObserving; + } +}; + +// Counts events, but asks to be unsubscribed after the very first one. +template +struct OneShotObserver : public bertini::Observer +{ + int count = 0; + ObserveResult Observe(AnyEvent const&) override + { + ++count; + return ObserveResult::Unsubscribe; + } +}; + +// On TrackingStarted, attaches `child` to the emitting tracker; on TrackingEnded, +// detaches it. Both happen from inside Observe(), exercising deferred mutation. +template +struct MetaObserver : public bertini::Observer +{ + CountingObserver child; + bool saw_started = false; + bool saw_ended = false; + + ObserveResult Observe(AnyEvent const& e) override + { + if (auto p = dynamic_cast*>(&e)) + { + saw_started = true; + p->Get().AddObserver(child); // deferred: child won't see THIS event + } + else if (auto p = dynamic_cast*>(&e)) + { + saw_ended = true; + p->Get().RemoveObserver(child); + } + return ObserveResult::KeepObserving; + } +}; + + +// builds the square-root system into `sys`, ready for a tracker. +static void BuildSquareRootSystem(System& sys) +{ + Var x = Variable::Make("x"); + Var y = Variable::Make("y"); + Var t = Variable::Make("t"); + + VariableGroup v{x,y}; + sys.AddFunction(x-t); + sys.AddFunction(pow(y,2)-x); + sys.AddPathVariable(t); + sys.AddVariableGroup(v); +} + + +BOOST_AUTO_TEST_CASE(self_unsubscribe_via_return_value) +{ + DefaultPrecision(16); + using namespace bertini::tracking; + + System sys; + BuildSquareRootSystem(sys); + AMPTracker tracker(sys); + tracker.Setup(Predictor::Euler, 1e-5, 1e5, SteppingConfig(), NewtonConfig()); + tracker.PrecisionSetup(AMPConfigFrom(sys)); + + OneShotObserver one_shot; + tracker.AddObserver(one_shot); + + Vec start_point(2); + start_point << mpfr(1), mpfr(1); + Vec end_point; + [[maybe_unused]] auto code = tracker.TrackPath(end_point, mpfr(1), mpfr(0), start_point); + + // it returned Unsubscribe on the first event, so it must have been dropped + // before the second event was ever emitted. + BOOST_CHECK_EQUAL(one_shot.count, 1); +} + + +BOOST_AUTO_TEST_CASE(meta_observer_attaches_child_mid_dispatch) +{ + DefaultPrecision(16); + using namespace bertini::tracking; + + System sys; + BuildSquareRootSystem(sys); + AMPTracker tracker(sys); + tracker.Setup(Predictor::Euler, 1e-5, 1e5, SteppingConfig(), NewtonConfig()); + tracker.PrecisionSetup(AMPConfigFrom(sys)); + + MetaObserver meta; + tracker.AddObserver(meta); + + Vec start_point(2); + start_point << mpfr(1), mpfr(1); + Vec end_point; + [[maybe_unused]] auto code = tracker.TrackPath(end_point, mpfr(1), mpfr(0), start_point); + + BOOST_CHECK(meta.saw_started); + BOOST_CHECK(meta.saw_ended); + // the child was attached during TrackingStarted and saw the events that + // followed (steps, precision changes, ...) up to and including TrackingEnded. + BOOST_CHECK_GT(meta.child.count, 0); +} + + +// an observer that declares it observes some unrelated type +struct ForeignThing {}; + + +BOOST_AUTO_TEST_CASE(incompatible_observer_is_rejected) +{ + DefaultPrecision(16); + using namespace bertini::tracking; + + System sys; + BuildSquareRootSystem(sys); + AMPTracker tracker(sys); + + // an observer whose ObservedKind() is not the tracker (nor the wildcard) + CountingObserver wrong; + BOOST_CHECK_THROW(tracker.AddObserver(wrong), bertini::IncompatibleObserver); + + // the right kind attaches fine + CountingObserver right; + BOOST_CHECK_NO_THROW(tracker.AddObserver(right)); +} + + +BOOST_AUTO_TEST_CASE(owning_observer_outlives_caller_reference) +{ + DefaultPrecision(16); + using namespace bertini::tracking; + + System sys; + BuildSquareRootSystem(sys); + AMPTracker tracker(sys); + tracker.Setup(Predictor::Euler, 1e-5, 1e5, SteppingConfig(), NewtonConfig()); + tracker.PrecisionSetup(AMPConfigFrom(sys)); + + auto obs = std::make_shared>(); + std::weak_ptr> weak = obs; + + tracker.AddObserver(std::static_pointer_cast(obs)); + + // caller forgets its reference: the tracker co-owns the observer, so it must + // stay alive and keep observing -- "attach it and forget it", no dangling. + obs.reset(); + BOOST_CHECK(!weak.expired()); + + Vec start_point(2); + start_point << mpfr(1), mpfr(1); + Vec end_point; + BOOST_CHECK_NO_THROW(tracker.TrackPath(end_point, mpfr(1), mpfr(0), start_point)); + BOOST_CHECK_GT(weak.lock()->count, 0); + + // detaching releases the tracker's ownership -> the observer is destroyed + tracker.RemoveObserver(*weak.lock()); + BOOST_CHECK(weak.expired()); +} + + BOOST_AUTO_TEST_SUITE_END() diff --git a/doc_resources/landing/cli/index.html b/doc_resources/landing/cli/index.html new file mode 100644 index 000000000..e9a559210 --- /dev/null +++ b/doc_resources/landing/cli/index.html @@ -0,0 +1,83 @@ + + + + + + Bertini 2 — Command-line executable + + + +
+
+

Bertini 2 — CLI

+

The bertini2 command-line executable.

+
+ + + +
+

Basic usage

+

+ bertini2 reads the classic Bertini input-file format and solves + zero-dimensional polynomial systems. +

+
bertini2 [options] [input_file]   # input_file defaults to "input"
+bertini2 --help                   # full usage
+bertini2 --version                # library and dependency versions
+ +

Parallelism

+

+ Two orthogonal parallelism layers are available when the library is built with MPI. + MPI is auto-detected at configure time and enabled by default when found. +

+ +

MPI ranks

+

+ Paths are distributed across multiple processes. Pass --bind-to none + so each rank can use all its assigned cores. +

+
mpirun --bind-to none -n N bertini2 input
+ +

Threads per rank

+

+ Each MPI rank tracks multiple paths concurrently using + std::thread — there is no OpenMP dependency. + The thread count is controlled by the environment variable + OMP_NUM_THREADS. +

+

+ Despite the name, OMP_NUM_THREADS here has nothing to do with OpenMP. + It was chosen because HPC schedulers such as SLURM set it automatically from + --cpus-per-task, so jobs running under a scheduler require no extra + configuration. When running outside a scheduler, set it manually: +

+
OMP_NUM_THREADS=T mpirun --bind-to none -n N bertini2 input
+

+ Total parallel capacity = N ranks × T threads/rank. + OMP_NUM_THREADS defaults to 1 when unset. +

+ +

Serial build

+

+ If the library was built without MPI (e.g. on a workstation where MPI was not + found at configure time), only single-process execution is available. Thread count + is still controlled by OMP_NUM_THREADS. + Rebuild with MPI present to unlock multi-rank parallelism. +

+
+ + +
+ + diff --git a/doc_resources/landing/index.html b/doc_resources/landing/index.html index d6d41793c..caf1d83c0 100644 --- a/doc_resources/landing/index.html +++ b/doc_resources/landing/index.html @@ -27,6 +27,13 @@

C++

Doxygen-generated API reference for embedders and contributors.

Open C++ docs → + + +

CLI

+

The bertini2 executable.

+

Usage, input format, MPI and thread parallelism.

+ Open CLI docs → +