commit 81396a5322a7a48764fcf254d5d933ba1e57bdc5 Author: John K. Luebs Date: Sun Feb 11 15:32:50 2024 -0600 Squashed 'kiwi/' content from commit 268028e git-subtree-dir: kiwi git-subtree-split: 268028ee4a694dcd89e4b1e683bf2f9ac48c08d9 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..db00e4e --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: [MatthieuDartiailh] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d49f0be --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,116 @@ +name: Continuous Integration +on: + schedule: + - cron: '0 0 * * 2' + push: + branches: + - main + pull_request: + branches: + - main + paths: + - .github/workflows/ci.yml + - "benchmarks/**" + - "kiwi/**" + - "py/**" + - setup.py + - pyproject.toml + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ['3.9'] + steps: + - uses: actions/checkout@v3 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r lint_requirements.txt + pip install -e . + - name: Formatting + if: always() + run: | + ruff format py --check + - name: Linting + if: always() + run: | + ruff py + - name: Typing + if: always() + # We test twice to ensure the type annotations are properly installed + run: | + mypy py + cd py/tests + mypy . + benchmark: + name: C++ Benchmark + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + steps: + - uses: actions/checkout@v3 + - name: Install dependencies + run: | + sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test + sudo apt-get install -y g++-11 + - name: Build and run benchmark (C++11) + run: cd benchmarks && ./build_and_run_bench.sh + - name: Build and run benchmark (C++20) + env: + CXX_COMPILER: g++-11 + CXX_FLAGS: -std=c++20 + run: cd benchmarks && ./build_and_run_bench.sh + tests: + name: Unit tests + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12-dev', 'pypy-3.7', 'pypy-3.8'] + steps: + - uses: actions/checkout@v3 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + - name: Install project + env: + CPPFLAGS: --coverage + KIWI_DISABLE_FH4: 1 + run: | + pip install . + - name: Test with pytest + run: | + pip install pytest + python -X dev -m pytest py -W error + - name: Generate C++ coverage reports + if: (github.event_name != 'schedule' && matrix.os != 'windows-latest') + run: | + bash -c "find . -type f -name '*.gcno' -exec gcov -pb --all-blocks {} +" || true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v3 + if: (github.event_name != 'schedule' && matrix.os != 'windows-latest') + with: + token: ${{ secrets.CODECOV_TOKEN }} + flags: unittests + name: codecov-umbrella + fail_ci_if_error: true diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..fb45cdc --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,45 @@ +name: Documentation building +on: + schedule: + - cron: '0 0 * * 2' + push: + branches: + - main + pull_request: + branches: + - main + paths: + - .github/workflows/docs.yml + - "kiwi/**" + - "py/**" + - "docs/**" + - setup.py + - pyproject.toml + +jobs: + docs: + name: Docs building + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r docs/requirements.txt + - name: Install project + run: | + pip install . + - name: Install graphviz + uses: ts-graphviz/setup-graphviz@v1 + - name: Build documentation + run: | + mkdir docs_output; + sphinx-build docs/source docs_output -W -b html; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..051bd70 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,220 @@ +name: Build and upload wheels +on: + workflow_dispatch: + schedule: + - cron: '0 0 * * 3' + push: + tags: + - '*' + +jobs: + build_sdist: + name: Build sdist + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Build sdist + run: | + pip install --upgrade pip + pip install wheel build + python -m build . -s + - name: Test sdist + run: | + pip install pytest + pip install dist/*.tar.gz + cd .. + pytest kiwi/py/tests -v -W error + - name: Store artifacts + uses: actions/upload-artifact@v3 + with: + name: artifact + path: dist/* + + build_wheels: + name: Build wheels on ${{ matrix.os }} for ${{ matrix.archs }} using ${{ matrix.manylinux_version }}+ + runs-on: ${{ matrix.os }} + env: + BUILD_COMMIT: main + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + # We build separately 3.7 and 3.8 using manylinux1 + manylinux_version: [manylinux1, manylinux2010, manylinux2014] + archs: [auto] + include: + - os: ubuntu-latest + archs: aarch64 + manylinux_version: manylinux1 + - os: ubuntu-latest + archs: ppc64le + manylinux_version: manylinux1 + - os: ubuntu-latest + archs: s390x + manylinux_version: manylinux1 + - os: ubuntu-latest + archs: aarch64 + manylinux_version: manylinux2010 + - os: ubuntu-latest + archs: ppc64le + manylinux_version: manylinux2010 + - os: ubuntu-latest + archs: s390x + manylinux_version: manylinux2010 + - os: ubuntu-latest + archs: aarch64 + manylinux_version: manylinux2014 + - os: ubuntu-latest + archs: ppc64le + manylinux_version: manylinux2014 + - os: ubuntu-latest + archs: s390x + manylinux_version: manylinux2014 + - os: windows-latest + archs: ARM64 + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Get history and tags for SCM versioning to work + run: | + git fetch --prune --unshallow + git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: '3.x' + - name: Set up QEMU + if: runner.os == 'Linux' && matrix.archs != 'auto' + uses: docker/setup-qemu-action@v2 + with: + platforms: all + - name: Install cibuildwheel + run: | + python -m pip install --upgrade pip + python -m pip install wheel cibuildwheel + - name: Build wheels + if: matrix.manylinux_version == 'manylinux1' + env: + CIBW_BUILD: "cp37-* cp38-*" + CIBW_ARCHS_MACOS: x86_64 universal2 arm64 + CIBW_ARCHS_LINUX: ${{ matrix.archs }} + CIBW_MANYLINUX_X86_64_IMAGE: manylinux1 + CIBW_MANYLINUX_I686_IMAGE: manylinux1 + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_COMMAND: pytest {package}/py/tests -v -W error + # Do not link against VC2014_1 on Windows + KIWI_DISABLE_FH4: 1 + run: | + python -m cibuildwheel . --output-dir dist + - name: Build wheels + if: matrix.manylinux_version == 'manylinux2010' + env: + CIBW_BUILD: "cp39-* cp310-* pp37-* pp38-*" + CIBW_ARCHS_MACOS: x86_64 universal2 arm64 + CIBW_ARCHS_LINUX: ${{ matrix.archs }} + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2010 + CIBW_MANYLINUX_I686_IMAGE: manylinux2010 + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_COMMAND: python -m pytest {package}/py/tests -v + # Do not link against VC2014_1 on Windows + KIWI_DISABLE_FH4: 1 + run: | + python -m cibuildwheel . --output-dir dist + - name: Build wheels + if: matrix.manylinux_version == 'manylinux2014' + env: + CIBW_BUILD: "cp312-* cp311-* pp39-*" + CIBW_ARCHS_MACOS: x86_64 universal2 arm64 + CIBW_ARCHS_LINUX: ${{ matrix.archs }} + CIBW_MANYLINUX_X86_64_IMAGE: manylinux2014 + CIBW_MANYLINUX_I686_IMAGE: manylinux2014 + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_COMMAND: python -m pytest {package}/py/tests -v + # Do not link against VC2014_1 on Windows + KIWI_DISABLE_FH4: 1 + run: | + python -m cibuildwheel . --output-dir dist + - name: Build wheels + if: runner.os == 'Windows' && matrix.archs != 'auto' + env: + CIBW_BUILD: "cp39-* cp310-* cp311-* cp312-*" + CIBW_ARCHS_WINDOWS: ${{ matrix.archs }} + # It is not yet possible to run ARM64 tests, only cross-compile them. + CIBW_TEST_SKIP: "*-win_arm64" + CIBW_TEST_REQUIRES: pytest + CIBW_TEST_COMMAND: python -m pytest {package}/py/tests -v + # Do not link against VC2014_1 on Windows. + KIWI_DISABLE_FH4: 1 + run: | + python -m cibuildwheel . --output-dir dist + - name: Store artifacts + uses: actions/upload-artifact@v3 + with: + name: artifact + path: dist/*.whl + + publish: + if: github.event_name == 'push' + needs: [build_wheels, build_sdist] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + + - uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.pypi_password }} + # To test: + # repository_url: https://test.pypi.org/legacy/ + + github-release: + name: >- + Sign the Python 🐍 distribution 📦 with Sigstore + and create a GitHub Release + runs-on: ubuntu-latest + needs: + - publish + + permissions: + contents: write + id-token: write + + steps: + - name: Download all the dists + uses: actions/download-artifact@v3 + with: + name: artifact + path: dist + - name: Sign the dists with Sigstore + uses: sigstore/gh-action-sigstore-python@v2.1.0 + with: + password: ${{ secrets.pypi_password }} + inputs: >- + ./dist/*.tar.gz + ./dist/*.whl + - name: Create GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release create + '${{ github.ref_name }}' + --repo '${{ github.repository }}' + --generate-notes + - name: Upload artifact signatures to GitHub Release + env: + GITHUB_TOKEN: ${{ github.token }} + run: >- + gh release upload + '${{ github.ref_name }}' dist/** + --repo '${{ github.repository }}' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df038af --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.pyc +*.pyd +*.so + +# auto-generated by setuptools-scm +py/src/version.h + +benchmarks/run_bench +build/ +dist/ +kiwisolver.egg-info/ +.eggs +.vscode +.mypy_cache diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b6a54b3 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: +- repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.1.6 + hooks: + # Run the linter. + - id: ruff + # Run the formatter. + - id: ruff-format +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.7.1 + hooks: + - id: mypy + additional_dependencies: [] \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..17a318d --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,27 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.9" + +# Build documentation in the docs/source directory with Sphinx +sphinx: + configuration: docs/source/conf.py + +# Enable epub output +formats: + - epub + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt + - method: pip + path: . diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c34aff7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,71 @@ +========================= + The Kiwi licensing terms +========================= +Kiwi is licensed under the terms of the Modified BSD License (also known as +New or Revised BSD), as follows: + +Copyright (c) 2013, Nucleic Development Team + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this +list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this +list of conditions and the following disclaimer in the documentation and/or +other materials provided with the distribution. + +Neither the name of the Nucleic Development Team nor the names of its +contributors may be used to endorse or promote products derived from this +software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +About Kiwi +---------- +Chris Colbert began the Kiwi project in December 2013 in an effort to +create a blisteringly fast UI constraint solver. Chris is still the +project lead. + +The Nucleic Development Team is the set of all contributors to the Nucleic +project and its subprojects. + +The core team that coordinates development on GitHub can be found here: +http://github.com/nucleic. The current team consists of: + +* Chris Colbert + +Our Copyright Policy +-------------------- +Nucleic uses a shared copyright model. Each contributor maintains copyright +over their contributions to Nucleic. But, it is important to note that these +contributions are typically only changes to the repositories. Thus, the Nucleic +source code, in its entirety is not the copyright of any single person or +institution. Instead, it is the collective copyright of the entire Nucleic +Development Team. If individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should indicate +their copyright in the commit message of the change, when they commit the +change to one of the Nucleic repositories. + +With this in mind, the following banner should be used in any source code file +to indicate the copyright and license terms: + +#------------------------------------------------------------------------------ +# Copyright (c) 2013, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +#------------------------------------------------------------------------------ diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..885ae2b --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,17 @@ +include MANIFEST.in +include LICENSE +include README.rst +include releasenotes.rst +recursive-include kiwi *.h +recursive-include py *.cpp *.h *.py *.pyi +include py/kiwisolver/py.typed +recursive-include docs/source *.rst +recursive-include docs/source *.png +recursive-include docs/source *.py +recursive-include docs/source *.svg +include docs/make.bat +include docs/Makefile +prune .git +prune dist +prune build +prune docs/build diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..33089f5 --- /dev/null +++ b/README.rst @@ -0,0 +1,23 @@ +Welcome to Kiwi +=============== + +.. image:: https://github.com/nucleic/kiwi/workflows/Continuous%20Integration/badge.svg + :target: https://github.com/nucleic/kiwi/actions +.. image:: https://github.com/nucleic/kiwi/workflows/Documentation%20building/badge.svg + :target: https://github.com/nucleic/kiwi/actions +.. image:: https://codecov.io/gh/nucleic/kiwi/branch/main/graph/badge.svg + :target: https://codecov.io/gh/nucleic/kiwi +.. image:: https://readthedocs.org/projects/kiwisolver/badge/?version=latest + :target: https://kiwisolver.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +Kiwi is an efficient C++ implementation of the Cassowary constraint solving +algorithm. Kiwi is an implementation of the algorithm based on the +`seminal Cassowary paper `_. +It is *not* a refactoring of the original C++ solver. Kiwi has been designed +from the ground up to be lightweight and fast. Kiwi ranges from 10x to 500x +faster than the original Cassowary solver with typical use cases gaining a 40x +improvement. Memory savings are consistently > 5x. + +In addition to the C++ solver, Kiwi ships with hand-rolled Python bindings for +Python 3.7+. diff --git a/benchmarks/README.rst b/benchmarks/README.rst new file mode 100644 index 0000000..aa84b01 --- /dev/null +++ b/benchmarks/README.rst @@ -0,0 +1,17 @@ +Benchmarks for Kiwi +------------------- + +Those benchmarks are mostly used to check the performance of kiwi depending on +different c++ data structure. + +# C++ + +GCC must be installed first on your system (`build-essential` package with apt) + + >>> ./build_and_run_bench.sh + +# Python + +Running these benchmarks require to install the perf module:: + + >>> python enaml_like_benchmarks.py diff --git a/benchmarks/build_and_run_bench.sh b/benchmarks/build_and_run_bench.sh new file mode 100755 index 0000000..ff42884 --- /dev/null +++ b/benchmarks/build_and_run_bench.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -o errexit -o nounset # fail on error or on unset variables + +# set default values if variables are unset +: "${CXX_COMPILER:=g++}" +: "${CXX_FLAGS:=-std=c++11}" + +"$CXX_COMPILER" ${CXX_FLAGS} -O2 -Wall -pedantic -I.. enaml_like_benchmark.cpp -o run_bench + +./run_bench diff --git a/benchmarks/enaml_like_benchmark.cpp b/benchmarks/enaml_like_benchmark.cpp new file mode 100644 index 0000000..4bd2fbe --- /dev/null +++ b/benchmarks/enaml_like_benchmark.cpp @@ -0,0 +1,224 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2020, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ + +// Time updating an EditVariable in a set of constraints typical of enaml use. + +#include +#define ANKERL_NANOBENCH_IMPLEMENT +#include "nanobench.h" + +using namespace kiwi; + +void build_solver(Solver& solver, Variable& width, Variable& height) +{ + // Create custom strength + double mmedium = strength::create(0.0, 1.0, 0.0, 1.25); + double smedium = strength::create(0.0, 100, 0.0); + + // Create the variable + Variable left("left"); + Variable top("top"); + Variable contents_top("contents_top"); + Variable contents_bottom("contents_bottom"); + Variable contents_left("contents_left"); + Variable contents_right("contents_right"); + Variable midline("midline"); + Variable ctleft("ctleft"); + Variable ctheight("ctheight"); + Variable cttop("cttop"); + Variable ctwidth("ctwidth"); + Variable lb1left("lb1left"); + Variable lb1height("lb1height"); + Variable lb1top("lb1top"); + Variable lb1width("lb1width"); + Variable lb2left("lb2left"); + Variable lb2height("lb2height"); + Variable lb2top("lb2top"); + Variable lb2width("lb2width"); + Variable lb3left("lb3left"); + Variable lb3height("lb3height"); + Variable lb3top("lb3top"); + Variable lb3width("lb3width"); + Variable fl1left("fl1left"); + Variable fl1height("fl1height"); + Variable fl1top("fl1top"); + Variable fl1width("fl1width"); + Variable fl2left("fl2left"); + Variable fl2height("fl2height"); + Variable fl2top("fl2top"); + Variable fl2width("fl2width"); + Variable fl3left("fl3left"); + Variable fl3height("fl3height"); + Variable fl3top("fl3top"); + Variable fl3width("fl3width"); + + // Add the edit variables + solver.addEditVariable(width, strength::strong); + solver.addEditVariable(height, strength::strong); + + // Add the constraints + Constraint constraints[] = { + (left + -0 >= 0) | strength::required, + (height + 0 == 0) | strength::medium, + (top + -0 >= 0) | strength::required, + (width + -0 >= 0) | strength::required, + (height + -0 >= 0) | strength::required, + (-top + contents_top + -10 == 0) | strength::required, + (lb3height + -16 == 0) | strength::strong, + (lb3height + -16 >= 0) | strength::strong, + (ctleft + -0 >= 0) | strength::required, + (cttop + -0 >= 0) | strength::required, + (ctwidth + -0 >= 0) | strength::required, + (ctheight + -0 >= 0) | strength::required, + (fl3left + -0 >= 0) | strength::required, + (ctheight + -24 >= 0) | smedium, + (ctwidth + -1.67772e+07 <= 0) | smedium, + (ctheight + -24 <= 0) | smedium, + (fl3top + -0 >= 0) | strength::required, + (fl3width + -0 >= 0) | strength::required, + (fl3height + -0 >= 0) | strength::required, + (lb1width + -67 == 0) | strength::weak, + (lb2width + -0 >= 0) | strength::required, + (lb2height + -0 >= 0) | strength::required, + (fl2height + -0 >= 0) | strength::required, + (lb3left + -0 >= 0) | strength::required, + (fl2width + -125 >= 0) | strength::strong, + (fl2height + -21 == 0) | strength::strong, + (fl2height + -21 >= 0) | strength::strong, + (lb3top + -0 >= 0) | strength::required, + (lb3width + -0 >= 0) | strength::required, + (fl1left + -0 >= 0) | strength::required, + (fl1width + -0 >= 0) | strength::required, + (lb1width + -67 >= 0) | strength::strong, + (fl2left + -0 >= 0) | strength::required, + (lb2width + -66 == 0) | strength::weak, + (lb2width + -66 >= 0) | strength::strong, + (lb2height + -16 == 0) | strength::strong, + (fl1height + -0 >= 0) | strength::required, + (fl1top + -0 >= 0) | strength::required, + (lb2top + -0 >= 0) | strength::required, + (-lb2top + lb3top + -lb2height + -10 == 0) | mmedium, + (-lb3top + -lb3height + fl3top + -10 >= 0) | strength::required, + (-lb3top + -lb3height + fl3top + -10 == 0) | mmedium, + (contents_bottom + -fl3height + -fl3top + -0 == 0) | mmedium, + (fl1top + -contents_top + 0 >= 0) | strength::required, + (fl1top + -contents_top + 0 == 0) | mmedium, + (contents_bottom + -fl3height + -fl3top + -0 >= 0) | strength::required, + (-left + -width + contents_right + 10 == 0) | strength::required, + (-top + -height + contents_bottom + 10 == 0) | strength::required, + (-left + contents_left + -10 == 0) | strength::required, + (lb3left + -contents_left + 0 == 0) | mmedium, + (fl1left + -midline + 0 == 0) | strength::strong, + (fl2left + -midline + 0 == 0) | strength::strong, + (ctleft + -midline + 0 == 0) | strength::strong, + (fl1top + 0.5 * fl1height + -lb1top + -0.5 * lb1height + 0 == 0) | strength::strong, + (lb1left + -contents_left + 0 >= 0) | strength::required, + (lb1left + -contents_left + 0 == 0) | mmedium, + (-lb1left + fl1left + -lb1width + -10 >= 0) | strength::required, + (-lb1left + fl1left + -lb1width + -10 == 0) | mmedium, + (-fl1left + contents_right + -fl1width + -0 >= 0) | strength::required, + (width + 0 == 0) | strength::medium, + (-fl1top + fl2top + -fl1height + -10 >= 0) | strength::required, + (-fl1top + fl2top + -fl1height + -10 == 0) | mmedium, + (cttop + -fl2top + -fl2height + -10 >= 0) | strength::required, + (-ctheight + -cttop + fl3top + -10 >= 0) | strength::required, + (contents_bottom + -fl3height + -fl3top + -0 >= 0) | strength::required, + (cttop + -fl2top + -fl2height + -10 == 0) | mmedium, + (-fl1left + contents_right + -fl1width + -0 == 0) | mmedium, + (-lb2top + -0.5 * lb2height + fl2top + 0.5 * fl2height + 0 == 0) | strength::strong, + (-contents_left + lb2left + 0 >= 0) | strength::required, + (-contents_left + lb2left + 0 == 0) | mmedium, + (fl2left + -lb2width + -lb2left + -10 >= 0) | strength::required, + (-ctheight + -cttop + fl3top + -10 == 0) | mmedium, + (contents_bottom + -fl3height + -fl3top + -0 == 0) | mmedium, + (lb1top + -0 >= 0) | strength::required, + (lb1width + -0 >= 0) | strength::required, + (lb1height + -0 >= 0) | strength::required, + (fl2left + -lb2width + -lb2left + -10 == 0) | mmedium, + (-fl2left + -fl2width + contents_right + -0 == 0) | mmedium, + (-fl2left + -fl2width + contents_right + -0 >= 0) | strength::required, + (lb3left + -contents_left + 0 >= 0) | strength::required, + (lb1left + -0 >= 0) | strength::required, + (0.5 * ctheight + cttop + -lb3top + -0.5 * lb3height + 0 == 0) | strength::strong, + (ctleft + -lb3left + -lb3width + -10 >= 0) | strength::required, + (-ctwidth + -ctleft + contents_right + -0 >= 0) | strength::required, + (ctleft + -lb3left + -lb3width + -10 == 0) | mmedium, + (fl3left + -contents_left + 0 >= 0) | strength::required, + (fl3left + -contents_left + 0 == 0) | mmedium, + (-ctwidth + -ctleft + contents_right + -0 == 0) | mmedium, + (-fl3left + contents_right + -fl3width + -0 == 0) | mmedium, + (-contents_top + lb1top + 0 >= 0) | strength::required, + (-contents_top + lb1top + 0 == 0) | mmedium, + (-fl3left + contents_right + -fl3width + -0 >= 0) | strength::required, + (lb2top + -lb1top + -lb1height + -10 >= 0) | strength::required, + (-lb2top + lb3top + -lb2height + -10 >= 0) | strength::required, + (lb2top + -lb1top + -lb1height + -10 == 0) | mmedium, + (fl1height + -21 == 0) | strength::strong, + (fl1height + -21 >= 0) | strength::strong, + (lb2left + -0 >= 0) | strength::required, + (lb2height + -16 >= 0) | strength::strong, + (fl2top + -0 >= 0) | strength::required, + (fl2width + -0 >= 0) | strength::required, + (lb1height + -16 >= 0) | strength::strong, + (lb1height + -16 == 0) | strength::strong, + (fl3width + -125 >= 0) | strength::strong, + (fl3height + -21 == 0) | strength::strong, + (fl3height + -21 >= 0) | strength::strong, + (lb3height + -0 >= 0) | strength::required, + (ctwidth + -119 >= 0) | smedium, + (lb3width + -24 == 0) | strength::weak, + (lb3width + -24 >= 0) | strength::strong, + (fl1width + -125 >= 0) | strength::strong, + }; + + for (const auto& constraint : constraints) + solver.addConstraint(constraint); +} + +int main() +{ + ankerl::nanobench::Bench().run("building solver", [&] { + Solver solver; + Variable width("width"); + Variable height("height"); + build_solver(solver, width, height); + ankerl::nanobench::doNotOptimizeAway(solver); //< prevent the compiler to optimize away the solver + }); + + struct Size + { + int width; + int height; + }; + + Size sizes[] = { + { 400, 600 }, + { 600, 400 }, + { 800, 1200 }, + { 1200, 800 }, + { 400, 800 }, + { 800, 400 } + }; + + Solver solver; + Variable widthVar("width"); + Variable heightVar("height"); + build_solver(solver, widthVar, heightVar); + + for (const Size& size : sizes) + { + double width = size.width; + double height = size.height; + + ankerl::nanobench::Bench().minEpochIterations(10).run("suggest value " + std::to_string(size.width) + "x" + std::to_string(size.height), [&] { + solver.suggestValue(widthVar, width); + solver.suggestValue(heightVar, height); + solver.updateVariables(); + }); + } +} diff --git a/benchmarks/enaml_like_benchmark.py b/benchmarks/enaml_like_benchmark.py new file mode 100644 index 0000000..e845a3c --- /dev/null +++ b/benchmarks/enaml_like_benchmark.py @@ -0,0 +1,206 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2019, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +"""Time updating an EditVariable in a set of constraints typical of enaml use. + +""" +import perf + +from kiwisolver import Solver, Variable, strength + +solver = Solver() + +# Create custom strength +mmedium = strength.create(0, 1, 0, 1.25) +smedium = strength.create(0, 100, 0) + +# Create the variable +left = Variable("left") +height = Variable("height") +top = Variable("top") +width = Variable("width") +contents_top = Variable("contents_top") +contents_bottom = Variable("contents_bottom") +contents_left = Variable("contents_left") +contents_right = Variable("contents_right") +midline = Variable("midline") +ctleft = Variable("ctleft") +ctheight = Variable("ctheight") +cttop = Variable("cttop") +ctwidth = Variable("ctwidth") +lb1left = Variable("lb1left") +lb1height = Variable("lb1height") +lb1top = Variable("lb1top") +lb1width = Variable("lb1width") +lb2left = Variable("lb2left") +lb2height = Variable("lb2height") +lb2top = Variable("lb2top") +lb2width = Variable("lb2width") +lb3left = Variable("lb3left") +lb3height = Variable("lb3height") +lb3top = Variable("lb3top") +lb3width = Variable("lb3width") +fl1left = Variable("fl1left") +fl1height = Variable("fl1height") +fl1top = Variable("fl1top") +fl1width = Variable("fl1width") +fl2left = Variable("fl2left") +fl2height = Variable("fl2height") +fl2top = Variable("fl2top") +fl2width = Variable("fl2width") +fl3left = Variable("fl3left") +fl3height = Variable("fl3height") +fl3top = Variable("fl3top") +fl3width = Variable("fl3width") + +# Add the edit variables +solver.addEditVariable(width, "strong") +solver.addEditVariable(height, "strong") + +# Add the constraints +for c in [ + (left + -0 >= 0) | "required", + (height + 0 == 0) | "medium", + (top + -0 >= 0) | "required", + (width + -0 >= 0) | "required", + (height + -0 >= 0) | "required", + (-top + contents_top + -10 == 0) | "required", + (lb3height + -16 == 0) | "strong", + (lb3height + -16 >= 0) | "strong", + (ctleft + -0 >= 0) | "required", + (cttop + -0 >= 0) | "required", + (ctwidth + -0 >= 0) | "required", + (ctheight + -0 >= 0) | "required", + (fl3left + -0 >= 0) | "required", + (ctheight + -24 >= 0) | smedium, + (ctwidth + -1.67772e07 <= 0) | smedium, + (ctheight + -24 <= 0) | smedium, + (fl3top + -0 >= 0) | "required", + (fl3width + -0 >= 0) | "required", + (fl3height + -0 >= 0) | "required", + (lb1width + -67 == 0) | "weak", + (lb2width + -0 >= 0) | "required", + (lb2height + -0 >= 0) | "required", + (fl2height + -0 >= 0) | "required", + (lb3left + -0 >= 0) | "required", + (fl2width + -125 >= 0) | "strong", + (fl2height + -21 == 0) | "strong", + (fl2height + -21 >= 0) | "strong", + (lb3top + -0 >= 0) | "required", + (lb3width + -0 >= 0) | "required", + (fl1left + -0 >= 0) | "required", + (fl1width + -0 >= 0) | "required", + (lb1width + -67 >= 0) | "strong", + (fl2left + -0 >= 0) | "required", + (lb2width + -66 == 0) | "weak", + (lb2width + -66 >= 0) | "strong", + (lb2height + -16 == 0) | "strong", + (fl1height + -0 >= 0) | "required", + (fl1top + -0 >= 0) | "required", + (lb2top + -0 >= 0) | "required", + (-lb2top + lb3top + -lb2height + -10 == 0) | mmedium, + (-lb3top + -lb3height + fl3top + -10 >= 0) | "required", + (-lb3top + -lb3height + fl3top + -10 == 0) | mmedium, + (contents_bottom + -fl3height + -fl3top + -0 == 0) | mmedium, + (fl1top + -contents_top + 0 >= 0) | "required", + (fl1top + -contents_top + 0 == 0) | mmedium, + (contents_bottom + -fl3height + -fl3top + -0 >= 0) | "required", + (-left + -width + contents_right + 10 == 0) | "required", + (-top + -height + contents_bottom + 10 == 0) | "required", + (-left + contents_left + -10 == 0) | "required", + (lb3left + -contents_left + 0 == 0) | mmedium, + (fl1left + -midline + 0 == 0) | "strong", + (fl2left + -midline + 0 == 0) | "strong", + (ctleft + -midline + 0 == 0) | "strong", + (fl1top + 0.5 * fl1height + -lb1top + -0.5 * lb1height + 0 == 0) | "strong", + (lb1left + -contents_left + 0 >= 0) | "required", + (lb1left + -contents_left + 0 == 0) | mmedium, + (-lb1left + fl1left + -lb1width + -10 >= 0) | "required", + (-lb1left + fl1left + -lb1width + -10 == 0) | mmedium, + (-fl1left + contents_right + -fl1width + -0 >= 0) | "required", + (width + 0 == 0) | "medium", + (-fl1top + fl2top + -fl1height + -10 >= 0) | "required", + (-fl1top + fl2top + -fl1height + -10 == 0) | mmedium, + (cttop + -fl2top + -fl2height + -10 >= 0) | "required", + (-ctheight + -cttop + fl3top + -10 >= 0) | "required", + (contents_bottom + -fl3height + -fl3top + -0 >= 0) | "required", + (cttop + -fl2top + -fl2height + -10 == 0) | mmedium, + (-fl1left + contents_right + -fl1width + -0 == 0) | mmedium, + (-lb2top + -0.5 * lb2height + fl2top + 0.5 * fl2height + 0 == 0) | "strong", + (-contents_left + lb2left + 0 >= 0) | "required", + (-contents_left + lb2left + 0 == 0) | mmedium, + (fl2left + -lb2width + -lb2left + -10 >= 0) | "required", + (-ctheight + -cttop + fl3top + -10 == 0) | mmedium, + (contents_bottom + -fl3height + -fl3top + -0 == 0) | mmedium, + (lb1top + -0 >= 0) | "required", + (lb1width + -0 >= 0) | "required", + (lb1height + -0 >= 0) | "required", + (fl2left + -lb2width + -lb2left + -10 == 0) | mmedium, + (-fl2left + -fl2width + contents_right + -0 == 0) | mmedium, + (-fl2left + -fl2width + contents_right + -0 >= 0) | "required", + (lb3left + -contents_left + 0 >= 0) | "required", + (lb1left + -0 >= 0) | "required", + (0.5 * ctheight + cttop + -lb3top + -0.5 * lb3height + 0 == 0) | "strong", + (ctleft + -lb3left + -lb3width + -10 >= 0) | "required", + (-ctwidth + -ctleft + contents_right + -0 >= 0) | "required", + (ctleft + -lb3left + -lb3width + -10 == 0) | mmedium, + (fl3left + -contents_left + 0 >= 0) | "required", + (fl3left + -contents_left + 0 == 0) | mmedium, + (-ctwidth + -ctleft + contents_right + -0 == 0) | mmedium, + (-fl3left + contents_right + -fl3width + -0 == 0) | mmedium, + (-contents_top + lb1top + 0 >= 0) | "required", + (-contents_top + lb1top + 0 == 0) | mmedium, + (-fl3left + contents_right + -fl3width + -0 >= 0) | "required", + (lb2top + -lb1top + -lb1height + -10 >= 0) | "required", + (-lb2top + lb3top + -lb2height + -10 >= 0) | "required", + (lb2top + -lb1top + -lb1height + -10 == 0) | mmedium, + (fl1height + -21 == 0) | "strong", + (fl1height + -21 >= 0) | "strong", + (lb2left + -0 >= 0) | "required", + (lb2height + -16 >= 0) | "strong", + (fl2top + -0 >= 0) | "required", + (fl2width + -0 >= 0) | "required", + (lb1height + -16 >= 0) | "strong", + (lb1height + -16 == 0) | "strong", + (fl3width + -125 >= 0) | "strong", + (fl3height + -21 == 0) | "strong", + (fl3height + -21 >= 0) | "strong", + (lb3height + -0 >= 0) | "required", + (ctwidth + -119 >= 0) | smedium, + (lb3width + -24 == 0) | "weak", + (lb3width + -24 >= 0) | "strong", + (fl1width + -125 >= 0) | "strong", +]: + solver.addConstraint(c) + + +def bench_update_variables(loops, solver): + """Suggest new values and update variables. + + This mimic the use of kiwi in enaml in the case of a resizing. + + """ + t0 = perf.perf_counter() + for w, h in [ + (400, 600), + (600, 400), + (800, 1200), + (1200, 800), + (400, 800), + (800, 400), + ] * loops: + solver.suggestValue(width, w) + solver.suggestValue(height, h) + solver.updateVariables() + + return perf.perf_counter() - t0 + + +runner = perf.Runner() +runner.bench_time_func( + "kiwi.suggestValue", bench_update_variables, solver, inner_loops=1 +) diff --git a/benchmarks/nanobench.h b/benchmarks/nanobench.h new file mode 100644 index 0000000..9daaabb --- /dev/null +++ b/benchmarks/nanobench.h @@ -0,0 +1,3359 @@ +// __ _ _______ __ _ _____ ______ _______ __ _ _______ _ _ +// | \ | |_____| | \ | | | |_____] |______ | \ | | |_____| +// | \_| | | | \_| |_____| |_____] |______ | \_| |_____ | | +// +// Microbenchmark framework for C++11/14/17/20 +// https://github.com/martinus/nanobench +// +// Licensed under the MIT License . +// SPDX-License-Identifier: MIT +// Copyright (c) 2019-2021 Martin Ankerl +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +#ifndef ANKERL_NANOBENCH_H_INCLUDED +#define ANKERL_NANOBENCH_H_INCLUDED + +// see https://semver.org/ +#define ANKERL_NANOBENCH_VERSION_MAJOR 4 // incompatible API changes +#define ANKERL_NANOBENCH_VERSION_MINOR 3 // backwards-compatible changes +#define ANKERL_NANOBENCH_VERSION_PATCH 4 // backwards-compatible bug fixes + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// public facing api - as minimal as possible +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#include // high_resolution_clock +#include // memcpy +#include // for std::ostream* custom output target in Config +#include // all names +#include // holds all results + +#define ANKERL_NANOBENCH(x) ANKERL_NANOBENCH_PRIVATE_##x() + +#define ANKERL_NANOBENCH_PRIVATE_CXX() __cplusplus +#define ANKERL_NANOBENCH_PRIVATE_CXX98() 199711L +#define ANKERL_NANOBENCH_PRIVATE_CXX11() 201103L +#define ANKERL_NANOBENCH_PRIVATE_CXX14() 201402L +#define ANKERL_NANOBENCH_PRIVATE_CXX17() 201703L + +#if ANKERL_NANOBENCH(CXX) >= ANKERL_NANOBENCH(CXX17) +# define ANKERL_NANOBENCH_PRIVATE_NODISCARD() [[nodiscard]] +#else +# define ANKERL_NANOBENCH_PRIVATE_NODISCARD() +#endif + +#if defined(__clang__) +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_PADDED_PUSH() \ + _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wpadded\"") +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_PADDED_POP() _Pragma("clang diagnostic pop") +#else +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_PADDED_PUSH() +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_PADDED_POP() +#endif + +#if defined(__GNUC__) +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_EFFCPP_PUSH() _Pragma("GCC diagnostic push") _Pragma("GCC diagnostic ignored \"-Weffc++\"") +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_EFFCPP_POP() _Pragma("GCC diagnostic pop") +#else +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_EFFCPP_PUSH() +# define ANKERL_NANOBENCH_PRIVATE_IGNORE_EFFCPP_POP() +#endif + +#if defined(ANKERL_NANOBENCH_LOG_ENABLED) +# include +# define ANKERL_NANOBENCH_LOG(x) \ + do { \ + std::cout << __FUNCTION__ << "@" << __LINE__ << ": " << x << std::endl; \ + } while (0) +#else +# define ANKERL_NANOBENCH_LOG(x) \ + do { \ + } while (0) +#endif + +#if defined(__linux__) && defined(PERF_EVENT_IOC_ID) && defined(PERF_COUNT_HW_REF_CPU_CYCLES) && defined(PERF_FLAG_FD_CLOEXEC) && \ + !defined(ANKERL_NANOBENCH_DISABLE_PERF_COUNTERS) +// only enable perf counters on kernel 3.14 which seems to have all the necessary defines. The three PERF_... defines are not in +// kernel 2.6.32 (all others are). +# define ANKERL_NANOBENCH_PRIVATE_PERF_COUNTERS() 1 +#else +# define ANKERL_NANOBENCH_PRIVATE_PERF_COUNTERS() 0 +#endif + +#if defined(__clang__) +# define ANKERL_NANOBENCH_NO_SANITIZE(...) __attribute__((no_sanitize(__VA_ARGS__))) +#else +# define ANKERL_NANOBENCH_NO_SANITIZE(...) +#endif + +#if defined(_MSC_VER) +# define ANKERL_NANOBENCH_PRIVATE_NOINLINE() __declspec(noinline) +#else +# define ANKERL_NANOBENCH_PRIVATE_NOINLINE() __attribute__((noinline)) +#endif + +// workaround missing "is_trivially_copyable" in g++ < 5.0 +// See https://stackoverflow.com/a/31798726/48181 +#if defined(__GNUC__) && __GNUC__ < 5 +# define ANKERL_NANOBENCH_IS_TRIVIALLY_COPYABLE(...) __has_trivial_copy(__VA_ARGS__) +#else +# define ANKERL_NANOBENCH_IS_TRIVIALLY_COPYABLE(...) std::is_trivially_copyable<__VA_ARGS__>::value +#endif + +// declarations /////////////////////////////////////////////////////////////////////////////////// + +namespace ankerl { +namespace nanobench { + +using Clock = std::conditional::type; +class Bench; +struct Config; +class Result; +class Rng; +class BigO; + +/** + * @brief Renders output from a mustache-like template and benchmark results. + * + * The templating facility here is heavily inspired by [mustache - logic-less templates](https://mustache.github.io/). + * It adds a few more features that are necessary to get all of the captured data out of nanobench. Please read the + * excellent [mustache manual](https://mustache.github.io/mustache.5.html) to see what this is all about. + * + * nanobench output has two nested layers, *result* and *measurement*. Here is a hierarchy of the allowed tags: + * + * * `{{#result}}` Marks the begin of the result layer. Whatever comes after this will be instantiated as often as + * a benchmark result is available. Within it, you can use these tags: + * + * * `{{title}}` See Bench::title(). + * + * * `{{name}}` Benchmark name, usually directly provided with Bench::run(), but can also be set with Bench::name(). + * + * * `{{unit}}` Unit, e.g. `byte`. Defaults to `op`, see Bench::title(). + * + * * `{{batch}}` Batch size, see Bench::batch(). + * + * * `{{complexityN}}` Value used for asymptotic complexity calculation. See Bench::complexityN(). + * + * * `{{epochs}}` Number of epochs, see Bench::epochs(). + * + * * `{{clockResolution}}` Accuracy of the clock, i.e. what's the smallest time possible to measure with the clock. + * For modern systems, this can be around 20 ns. This value is automatically determined by nanobench at the first + * benchmark that is run, and used as a static variable throughout the application's runtime. + * + * * `{{clockResolutionMultiple}}` Configuration multiplier for `clockResolution`. See Bench::clockResolutionMultiple(). + * This is the target runtime for each measurement (epoch). That means the more accurate your clock is, the faster + * will be the benchmark. Basing the measurement's runtime on the clock resolution is the main reason why nanobench is so fast. + * + * * `{{maxEpochTime}}` Configuration for a maximum time each measurement (epoch) is allowed to take. Note that at least + * a single iteration will be performed, even when that takes longer than maxEpochTime. See Bench::maxEpochTime(). + * + * * `{{minEpochTime}}` Minimum epoch time, usually not set. See Bench::minEpochTime(). + * + * * `{{minEpochIterations}}` See Bench::minEpochIterations(). + * + * * `{{epochIterations}}` See Bench::epochIterations(). + * + * * `{{warmup}}` Number of iterations used before measuring starts. See Bench::warmup(). + * + * * `{{relative}}` True or false, depending on the setting you have used. See Bench::relative(). + * + * Apart from these tags, it is also possible to use some mathematical operations on the measurement data. The operations + * are of the form `{{command(name)}}`. Currently `name` can be one of `elapsed`, `iterations`. If performance counters + * are available (currently only on current Linux systems), you also have `pagefaults`, `cpucycles`, + * `contextswitches`, `instructions`, `branchinstructions`, and `branchmisses`. All the measuers (except `iterations`) are + * provided for a single iteration (so `elapsed` is the time a single iteration took). The following tags are available: + * + * * `{{median()}}` Calculate median of a measurement data set, e.g. `{{median(elapsed)}}`. + * + * * `{{average()}}` Average (mean) calculation. + * + * * `{{medianAbsolutePercentError()}}` Calculates MdAPE, the Median Absolute Percentage Error. The MdAPE is an excellent + * metric for the variation of measurements. It is more robust to outliers than the + * [Mean absolute percentage error (M-APE)](https://en.wikipedia.org/wiki/Mean_absolute_percentage_error). + * @f[ + * \mathrm{MdAPE}(e) = \mathrm{med}\{| \frac{e_i - \mathrm{med}\{e\}}{e_i}| \} + * @f] + * E.g. for *elapsed*: First, @f$ \mathrm{med}\{e\} @f$ calculates the median by sorting and then taking the middle element + * of all *elapsed* measurements. This is used to calculate the absolute percentage + * error to this median for each measurement, as in @f$ | \frac{e_i - \mathrm{med}\{e\}}{e_i}| @f$. All these results + * are sorted, and the middle value is chosen as the median absolute percent error. + * + * This measurement is a bit hard to interpret, but it is very robust against outliers. E.g. a value of 5% means that half of the + * measurements deviate less than 5% from the median, and the other deviate more than 5% from the median. + * + * * `{{sum()}}` Sums of all the measurements. E.g. `{{sum(iterations)}}` will give you the total number of iterations +* measured in this benchmark. + * + * * `{{minimum()}}` Minimum of all measurements. + * + * * `{{maximum()}}` Maximum of all measurements. + * + * * `{{sumProduct(, )}}` Calculates the sum of the products of corresponding measures: + * @f[ + * \mathrm{sumProduct}(a,b) = \sum_{i=1}^{n}a_i\cdot b_i + * @f] + * E.g. to calculate total runtime of the benchmark, you multiply iterations with elapsed time for each measurement, and + * sum these results up: + * `{{sumProduct(iterations, elapsed)}}`. + * + * * `{{#measurement}}` To access individual measurement results, open the begin tag for measurements. + * + * * `{{elapsed}}` Average elapsed wall clock time per iteration, in seconds. + * + * * `{{iterations}}` Number of iterations in the measurement. The number of iterations will fluctuate due + * to some applied randomness, to enhance accuracy. + * + * * `{{pagefaults}}` Average number of pagefaults per iteration. + * + * * `{{cpucycles}}` Average number of CPU cycles processed per iteration. + * + * * `{{contextswitches}}` Average number of context switches per iteration. + * + * * `{{instructions}}` Average number of retired instructions per iteration. + * + * * `{{branchinstructions}}` Average number of branches executed per iteration. + * + * * `{{branchmisses}}` Average number of branches that were missed per iteration. + * + * * `{{/measurement}}` Ends the measurement tag. + * + * * `{{/result}}` Marks the end of the result layer. This is the end marker for the template part that will be instantiated + * for each benchmark result. + * + * + * For the layer tags *result* and *measurement* you additionally can use these special markers: + * + * * ``{{#-first}}`` - Begin marker of a template that will be instantiated *only for the first* entry in the layer. Use is only + * allowed between the begin and end marker of the layer allowed. So between ``{{#result}}`` and ``{{/result}}``, or between + * ``{{#measurement}}`` and ``{{/measurement}}``. Finish the template with ``{{/-first}}``. + * + * * ``{{^-first}}`` - Begin marker of a template that will be instantiated *for each except the first* entry in the layer. This, + * this is basically the inversion of ``{{#-first}}``. Use is only allowed between the begin and end marker of the layer allowed. + * So between ``{{#result}}`` and ``{{/result}}``, or between ``{{#measurement}}`` and ``{{/measurement}}``. + * + * * ``{{/-first}}`` - End marker for either ``{{#-first}}`` or ``{{^-first}}``. + * + * * ``{{#-last}}`` - Begin marker of a template that will be instantiated *only for the last* entry in the layer. Use is only + * allowed between the begin and end marker of the layer allowed. So between ``{{#result}}`` and ``{{/result}}``, or between + * ``{{#measurement}}`` and ``{{/measurement}}``. Finish the template with ``{{/-last}}``. + * + * * ``{{^-last}}`` - Begin marker of a template that will be instantiated *for each except the last* entry in the layer. This, + * this is basically the inversion of ``{{#-last}}``. Use is only allowed between the begin and end marker of the layer allowed. + * So between ``{{#result}}`` and ``{{/result}}``, or between ``{{#measurement}}`` and ``{{/measurement}}``. + * + * * ``{{/-last}}`` - End marker for either ``{{#-last}}`` or ``{{^-last}}``. + * + @verbatim embed:rst + + For an overview of all the possible data you can get out of nanobench, please see the tutorial at :ref:`tutorial-template-json`. + + The templates that ship with nanobench are: + + * :cpp:func:`templates::csv() ` + * :cpp:func:`templates::json() ` + * :cpp:func:`templates::htmlBoxplot() ` + * :cpp:func:`templates::pyperf() ` + + @endverbatim + * + * @param mustacheTemplate The template. + * @param bench Benchmark, containing all the results. + * @param out Output for the generated output. + */ +void render(char const* mustacheTemplate, Bench const& bench, std::ostream& out); +void render(std::string const& mustacheTemplate, Bench const& bench, std::ostream& out); + +/** + * Same as render(char const* mustacheTemplate, Bench const& bench, std::ostream& out), but for when + * you only have results available. + * + * @param mustacheTemplate The template. + * @param results All the results to be used for rendering. + * @param out Output for the generated output. + */ +void render(char const* mustacheTemplate, std::vector const& results, std::ostream& out); +void render(std::string const& mustacheTemplate, std::vector const& results, std::ostream& out); + +// Contains mustache-like templates +namespace templates { + +/*! + @brief CSV data for the benchmark results. + + Generates a comma-separated values dataset. First line is the header, each following line is a summary of each benchmark run. + + @verbatim embed:rst + See the tutorial at :ref:`tutorial-template-csv` for an example. + @endverbatim + */ +char const* csv() noexcept; + +/*! + @brief HTML output that uses plotly to generate an interactive boxplot chart. See the tutorial for an example output. + + The output uses only the elapsed wall clock time, and displays each epoch as a single dot. + @verbatim embed:rst + See the tutorial at :ref:`tutorial-template-html` for an example. + @endverbatim + + @see ankerl::nanobench::render() + */ +char const* htmlBoxplot() noexcept; + +/*! + @brief Output in pyperf compatible JSON format, which can be used for more analyzations. + @verbatim embed:rst + See the tutorial at :ref:`tutorial-template-pyperf` for an example how to further analyze the output. + @endverbatim + */ +char const* pyperf() noexcept; + +/*! + @brief Template to generate JSON data. + + The generated JSON data contains *all* data that has been generated. All times are as double values, in seconds. The output can get + quite large. + @verbatim embed:rst + See the tutorial at :ref:`tutorial-template-json` for an example. + @endverbatim + */ +char const* json() noexcept; + +} // namespace templates + +namespace detail { + +template +struct PerfCountSet; + +class IterationLogic; +class PerformanceCounters; + +#if ANKERL_NANOBENCH(PERF_COUNTERS) +class LinuxPerformanceCounters; +#endif + +} // namespace detail +} // namespace nanobench +} // namespace ankerl + +// definitions //////////////////////////////////////////////////////////////////////////////////// + +namespace ankerl { +namespace nanobench { +namespace detail { + +template +struct PerfCountSet { + T pageFaults{}; + T cpuCycles{}; + T contextSwitches{}; + T instructions{}; + T branchInstructions{}; + T branchMisses{}; +}; + +} // namespace detail + +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +struct Config { + // actual benchmark config + std::string mBenchmarkTitle = "benchmark"; + std::string mBenchmarkName = "noname"; + std::string mUnit = "op"; + double mBatch = 1.0; + double mComplexityN = -1.0; + size_t mNumEpochs = 11; + size_t mClockResolutionMultiple = static_cast(1000); + std::chrono::nanoseconds mMaxEpochTime = std::chrono::milliseconds(100); + std::chrono::nanoseconds mMinEpochTime{}; + uint64_t mMinEpochIterations{1}; + uint64_t mEpochIterations{0}; // If not 0, run *exactly* these number of iterations per epoch. + uint64_t mWarmup = 0; + std::ostream* mOut = nullptr; + std::chrono::duration mTimeUnit = std::chrono::nanoseconds{1}; + std::string mTimeUnitName = "ns"; + bool mShowPerformanceCounters = true; + bool mIsRelative = false; + + Config(); + ~Config(); + Config& operator=(Config const&); + Config& operator=(Config&&); + Config(Config const&); + Config(Config&&) noexcept; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +// Result returned after a benchmark has finished. Can be used as a baseline for relative(). +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +class Result { +public: + enum class Measure : size_t { + elapsed, + iterations, + pagefaults, + cpucycles, + contextswitches, + instructions, + branchinstructions, + branchmisses, + _size + }; + + explicit Result(Config const& benchmarkConfig); + + ~Result(); + Result& operator=(Result const&); + Result& operator=(Result&&); + Result(Result const&); + Result(Result&&) noexcept; + + // adds new measurement results + // all values are scaled by iters (except iters...) + void add(Clock::duration totalElapsed, uint64_t iters, detail::PerformanceCounters const& pc); + + ANKERL_NANOBENCH(NODISCARD) Config const& config() const noexcept; + + ANKERL_NANOBENCH(NODISCARD) double median(Measure m) const; + ANKERL_NANOBENCH(NODISCARD) double medianAbsolutePercentError(Measure m) const; + ANKERL_NANOBENCH(NODISCARD) double average(Measure m) const; + ANKERL_NANOBENCH(NODISCARD) double sum(Measure m) const noexcept; + ANKERL_NANOBENCH(NODISCARD) double sumProduct(Measure m1, Measure m2) const noexcept; + ANKERL_NANOBENCH(NODISCARD) double minimum(Measure m) const noexcept; + ANKERL_NANOBENCH(NODISCARD) double maximum(Measure m) const noexcept; + + ANKERL_NANOBENCH(NODISCARD) bool has(Measure m) const noexcept; + ANKERL_NANOBENCH(NODISCARD) double get(size_t idx, Measure m) const; + ANKERL_NANOBENCH(NODISCARD) bool empty() const noexcept; + ANKERL_NANOBENCH(NODISCARD) size_t size() const noexcept; + + // Finds string, if not found, returns _size. + static Measure fromString(std::string const& str); + +private: + Config mConfig{}; + std::vector> mNameToMeasurements{}; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +/** + * An extremely fast random generator. Currently, this implements *RomuDuoJr*, developed by Mark Overton. Source: + * http://www.romu-random.org/ + * + * RomuDuoJr is extremely fast and provides reasonable good randomness. Not enough for large jobs, but definitely + * good enough for a benchmarking framework. + * + * * Estimated capacity: @f$ 2^{51} @f$ bytes + * * Register pressure: 4 + * * State size: 128 bits + * + * This random generator is a drop-in replacement for the generators supplied by ````. It is not + * cryptographically secure. It's intended purpose is to be very fast so that benchmarks that make use + * of randomness are not distorted too much by the random generator. + * + * Rng also provides a few non-standard helpers, optimized for speed. + */ +class Rng final { +public: + /** + * @brief This RNG provides 64bit randomness. + */ + using result_type = uint64_t; + + static constexpr uint64_t(min)(); + static constexpr uint64_t(max)(); + + /** + * As a safety precausion, we don't allow copying. Copying a PRNG would mean you would have two random generators that produce the + * same sequence, which is generally not what one wants. Instead create a new rng with the default constructor Rng(), which is + * automatically seeded from `std::random_device`. If you really need a copy, use copy(). + */ + Rng(Rng const&) = delete; + + /** + * Same as Rng(Rng const&), we don't allow assignment. If you need a new Rng create one with the default constructor Rng(). + */ + Rng& operator=(Rng const&) = delete; + + // moving is ok + Rng(Rng&&) noexcept = default; + Rng& operator=(Rng&&) noexcept = default; + ~Rng() noexcept = default; + + /** + * @brief Creates a new Random generator with random seed. + * + * Instead of a default seed (as the random generators from the STD), this properly seeds the random generator from + * `std::random_device`. It guarantees correct seeding. Note that seeding can be relatively slow, depending on the source of + * randomness used. So it is best to create a Rng once and use it for all your randomness purposes. + */ + Rng(); + + /*! + Creates a new Rng that is seeded with a specific seed. Each Rng created from the same seed will produce the same randomness + sequence. This can be useful for deterministic behavior. + + @verbatim embed:rst + .. note:: + + The random algorithm might change between nanobench releases. Whenever a faster and/or better random + generator becomes available, I will switch the implementation. + @endverbatim + + As per the Romu paper, this seeds the Rng with splitMix64 algorithm and performs 10 initial rounds for further mixing up of the + internal state. + + @param seed The 64bit seed. All values are allowed, even 0. + */ + explicit Rng(uint64_t seed) noexcept; + Rng(uint64_t x, uint64_t y) noexcept; + Rng(std::vector const& data); + + /** + * Creates a copy of the Rng, thus the copy provides exactly the same random sequence as the original. + */ + ANKERL_NANOBENCH(NODISCARD) Rng copy() const noexcept; + + /** + * @brief Produces a 64bit random value. This should be very fast, thus it is marked as inline. In my benchmark, this is ~46 times + * faster than `std::default_random_engine` for producing 64bit random values. It seems that the fastest std contender is + * `std::mt19937_64`. Still, this RNG is 2-3 times as fast. + * + * @return uint64_t The next 64 bit random value. + */ + inline uint64_t operator()() noexcept; + + // This is slightly biased. See + + /** + * Generates a random number between 0 and range (excluding range). + * + * The algorithm only produces 32bit numbers, and is slightly biased. The effect is quite small unless your range is close to the + * maximum value of an integer. It is possible to correct the bias with rejection sampling (see + * [here](https://lemire.me/blog/2016/06/30/fast-random-shuffling/), but this is most likely irrelevant in practices for the + * purposes of this Rng. + * + * See Daniel Lemire's blog post [A fast alternative to the modulo + * reduction](https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/) + * + * @param range Upper exclusive range. E.g a value of 3 will generate random numbers 0, 1, 2. + * @return uint32_t Generated random values in range [0, range(. + */ + inline uint32_t bounded(uint32_t range) noexcept; + + // random double in range [0, 1( + // see http://prng.di.unimi.it/ + + /** + * Provides a random uniform double value between 0 and 1. This uses the method described in [Generating uniform doubles in the + * unit interval](http://prng.di.unimi.it/), and is extremely fast. + * + * @return double Uniformly distributed double value in range [0,1(, excluding 1. + */ + inline double uniform01() noexcept; + + /** + * Shuffles all entries in the given container. Although this has a slight bias due to the implementation of bounded(), this is + * preferable to `std::shuffle` because it is over 5 times faster. See Daniel Lemire's blog post [Fast random + * shuffling](https://lemire.me/blog/2016/06/30/fast-random-shuffling/). + * + * @param container The whole container will be shuffled. + */ + template + void shuffle(Container& container) noexcept; + + /** + * Extracts the full state of the generator, e.g. for serialization. For this RNG this is just 2 values, but to stay API compatible + * with future implementations that potentially use more state, we use a vector. + * + * @return Vector containing the full state: + */ + std::vector state() const; + +private: + static constexpr uint64_t rotl(uint64_t x, unsigned k) noexcept; + + uint64_t mX; + uint64_t mY; +}; + +/** + * @brief Main entry point to nanobench's benchmarking facility. + * + * It holds configuration and results from one or more benchmark runs. Usually it is used in a single line, where the object is + * constructed, configured, and then a benchmark is run. E.g. like this: + * + * ankerl::nanobench::Bench().unit("byte").batch(1000).run("random fluctuations", [&] { + * // here be the benchmark code + * }); + * + * In that example Bench() constructs the benchmark, it is then configured with unit() and batch(), and after configuration a + * benchmark is executed with run(). Once run() has finished, it prints the result to `std::cout`. It would also store the results + * in the Bench instance, but in this case the object is immediately destroyed so it's not available any more. + */ +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +class Bench { +public: + /** + * @brief Creates a new benchmark for configuration and running of benchmarks. + */ + Bench(); + + Bench(Bench&& other); + Bench& operator=(Bench&& other); + Bench(Bench const& other); + Bench& operator=(Bench const& other); + ~Bench() noexcept; + + /*! + @brief Repeatedly calls `op()` based on the configuration, and performs measurements. + + This call is marked with `noinline` to prevent the compiler to optimize beyond different benchmarks. This can have quite a big + effect on benchmark accuracy. + + @verbatim embed:rst + .. note:: + + Each call to your lambda must have a side effect that the compiler can't possibly optimize it away. E.g. add a result to an + externally defined number (like `x` in the above example), and finally call `doNotOptimizeAway` on the variables the compiler + must not remove. You can also use :cpp:func:`ankerl::nanobench::doNotOptimizeAway` directly in the lambda, but be aware that + this has a small overhead. + + @endverbatim + + @tparam Op The code to benchmark. + */ + template + ANKERL_NANOBENCH(NOINLINE) + Bench& run(char const* benchmarkName, Op&& op); + + template + ANKERL_NANOBENCH(NOINLINE) + Bench& run(std::string const& benchmarkName, Op&& op); + + /** + * @brief Same as run(char const* benchmarkName, Op op), but instead uses the previously set name. + * @tparam Op The code to benchmark. + */ + template + ANKERL_NANOBENCH(NOINLINE) + Bench& run(Op&& op); + + /** + * @brief Title of the benchmark, will be shown in the table header. Changing the title will start a new markdown table. + * + * @param benchmarkTitle The title of the benchmark. + */ + Bench& title(char const* benchmarkTitle); + Bench& title(std::string const& benchmarkTitle); + ANKERL_NANOBENCH(NODISCARD) std::string const& title() const noexcept; + + /// Name of the benchmark, will be shown in the table row. + Bench& name(char const* benchmarkName); + Bench& name(std::string const& benchmarkName); + ANKERL_NANOBENCH(NODISCARD) std::string const& name() const noexcept; + + /** + * @brief Sets the batch size. + * + * E.g. number of processed byte, or some other metric for the size of the processed data in each iteration. If you benchmark + * hashing of a 1000 byte long string and want byte/sec as a result, you can specify 1000 as the batch size. + * + * @tparam T Any input type is internally cast to `double`. + * @param b batch size + */ + template + Bench& batch(T b) noexcept; + ANKERL_NANOBENCH(NODISCARD) double batch() const noexcept; + + /** + * @brief Sets the operation unit. + * + * Defaults to "op". Could be e.g. "byte" for string processing. This is used for the table header, e.g. to show `ns/byte`. Use + * singular (*byte*, not *bytes*). A change clears the currently collected results. + * + * @param unit The unit name. + */ + Bench& unit(char const* unit); + Bench& unit(std::string const& unit); + ANKERL_NANOBENCH(NODISCARD) std::string const& unit() const noexcept; + + /** + * @brief Sets the time unit to be used for the default output. + * + * Nanobench defaults to using ns (nanoseconds) as output in the markdown. For some benchmarks this is too coarse, so it is + * possible to configure this. E.g. use `timeUnit(1ms, "ms")` to show `ms/op` instead of `ns/op`. + * + * @param tu Time unit to display the results in, default is 1ns. + * @param tuName Name for the time unit, default is "ns" + */ + Bench& timeUnit(std::chrono::duration const& tu, std::string const& tuName); + ANKERL_NANOBENCH(NODISCARD) std::string const& timeUnitName() const noexcept; + ANKERL_NANOBENCH(NODISCARD) std::chrono::duration const& timeUnit() const noexcept; + + /** + * @brief Set the output stream where the resulting markdown table will be printed to. + * + * The default is `&std::cout`. You can disable all output by setting `nullptr`. + * + * @param outstream Pointer to output stream, can be `nullptr`. + */ + Bench& output(std::ostream* outstream) noexcept; + ANKERL_NANOBENCH(NODISCARD) std::ostream* output() const noexcept; + + /** + * Modern processors have a very accurate clock, being able to measure as low as 20 nanoseconds. This is the main trick nanobech to + * be so fast: we find out how accurate the clock is, then run the benchmark only so often that the clock's accuracy is good enough + * for accurate measurements. + * + * The default is to run one epoch for 1000 times the clock resolution. So for 20ns resolution and 11 epochs, this gives a total + * runtime of + * + * @f[ + * 20ns * 1000 * 11 \approx 0.2ms + * @f] + * + * To be precise, nanobench adds a 0-20% random noise to each evaluation. This is to prevent any aliasing effects, and further + * improves accuracy. + * + * Total runtime will be higher though: Some initial time is needed to find out the target number of iterations for each epoch, and + * there is some overhead involved to start & stop timers and calculate resulting statistics and writing the output. + * + * @param multiple Target number of times of clock resolution. Usually 1000 is a good compromise between runtime and accuracy. + */ + Bench& clockResolutionMultiple(size_t multiple) noexcept; + ANKERL_NANOBENCH(NODISCARD) size_t clockResolutionMultiple() const noexcept; + + /** + * @brief Controls number of epochs, the number of measurements to perform. + * + * The reported result will be the median of evaluation of each epoch. The higher you choose this, the more + * deterministic the result be and outliers will be more easily removed. Also the `err%` will be more accurate the higher this + * number is. Note that the `err%` will not necessarily decrease when number of epochs is increased. But it will be a more accurate + * representation of the benchmarked code's runtime stability. + * + * Choose the value wisely. In practice, 11 has been shown to be a reasonable choice between runtime performance and accuracy. + * This setting goes hand in hand with minEpocIterations() (or minEpochTime()). If you are more interested in *median* runtime, you + * might want to increase epochs(). If you are more interested in *mean* runtime, you might want to increase minEpochIterations() + * instead. + * + * @param numEpochs Number of epochs. + */ + Bench& epochs(size_t numEpochs) noexcept; + ANKERL_NANOBENCH(NODISCARD) size_t epochs() const noexcept; + + /** + * @brief Upper limit for the runtime of each epoch. + * + * As a safety precausion if the clock is not very accurate, we can set an upper limit for the maximum evaluation time per + * epoch. Default is 100ms. At least a single evaluation of the benchmark is performed. + * + * @see minEpochTime(), minEpochIterations() + * + * @param t Maximum target runtime for a single epoch. + */ + Bench& maxEpochTime(std::chrono::nanoseconds t) noexcept; + ANKERL_NANOBENCH(NODISCARD) std::chrono::nanoseconds maxEpochTime() const noexcept; + + /** + * @brief Minimum time each epoch should take. + * + * Default is zero, so we are fully relying on clockResolutionMultiple(). In most cases this is exactly what you want. If you see + * that the evaluation is unreliable with a high `err%`, you can increase either minEpochTime() or minEpochIterations(). + * + * @see maxEpochTime(), minEpochIterations() + * + * @param t Minimum time each epoch should take. + */ + Bench& minEpochTime(std::chrono::nanoseconds t) noexcept; + ANKERL_NANOBENCH(NODISCARD) std::chrono::nanoseconds minEpochTime() const noexcept; + + /** + * @brief Sets the minimum number of iterations each epoch should take. + * + * Default is 1, and we rely on clockResolutionMultiple(). If the `err%` is high and you want a more smooth result, you might want + * to increase the minimum number or iterations, or increase the minEpochTime(). + * + * @see minEpochTime(), maxEpochTime(), minEpochIterations() + * + * @param numIters Minimum number of iterations per epoch. + */ + Bench& minEpochIterations(uint64_t numIters) noexcept; + ANKERL_NANOBENCH(NODISCARD) uint64_t minEpochIterations() const noexcept; + + /** + * Sets exactly the number of iterations for each epoch. Ignores all other epoch limits. This forces nanobench to use exactly + * the given number of iterations for each epoch, not more and not less. Default is 0 (disabled). + * + * @param numIters Exact number of iterations to use. Set to 0 to disable. + */ + Bench& epochIterations(uint64_t numIters) noexcept; + ANKERL_NANOBENCH(NODISCARD) uint64_t epochIterations() const noexcept; + + /** + * @brief Sets a number of iterations that are initially performed without any measurements. + * + * Some benchmarks need a few evaluations to warm up caches / database / whatever access. Normally this should not be needed, since + * we show the median result so initial outliers will be filtered away automatically. If the warmup effect is large though, you + * might want to set it. Default is 0. + * + * @param numWarmupIters Number of warmup iterations. + */ + Bench& warmup(uint64_t numWarmupIters) noexcept; + ANKERL_NANOBENCH(NODISCARD) uint64_t warmup() const noexcept; + + /** + * @brief Marks the next run as the baseline. + * + * Call `relative(true)` to mark the run as the baseline. Successive runs will be compared to this run. It is calculated by + * + * @f[ + * 100\% * \frac{baseline}{runtime} + * @f] + * + * * 100% means it is exactly as fast as the baseline + * * >100% means it is faster than the baseline. E.g. 200% means the current run is twice as fast as the baseline. + * * <100% means it is slower than the baseline. E.g. 50% means it is twice as slow as the baseline. + * + * See the tutorial section "Comparing Results" for example usage. + * + * @param isRelativeEnabled True to enable processing + */ + Bench& relative(bool isRelativeEnabled) noexcept; + ANKERL_NANOBENCH(NODISCARD) bool relative() const noexcept; + + /** + * @brief Enables/disables performance counters. + * + * On Linux nanobench has a powerful feature to use performance counters. This enables counting of retired instructions, count + * number of branches, missed branches, etc. On default this is enabled, but you can disable it if you don't need that feature. + * + * @param showPerformanceCounters True to enable, false to disable. + */ + Bench& performanceCounters(bool showPerformanceCounters) noexcept; + ANKERL_NANOBENCH(NODISCARD) bool performanceCounters() const noexcept; + + /** + * @brief Retrieves all benchmark results collected by the bench object so far. + * + * Each call to run() generates a Result that is stored within the Bench instance. This is mostly for advanced users who want to + * see all the nitty gritty detials. + * + * @return All results collected so far. + */ + ANKERL_NANOBENCH(NODISCARD) std::vector const& results() const noexcept; + + /*! + @verbatim embed:rst + + Convenience shortcut to :cpp:func:`ankerl::nanobench::doNotOptimizeAway`. + + @endverbatim + */ + template + Bench& doNotOptimizeAway(Arg&& arg); + + /*! + @verbatim embed:rst + + Sets N for asymptotic complexity calculation, so it becomes possible to calculate `Big O + `_ from multiple benchmark evaluations. + + Use :cpp:func:`ankerl::nanobench::Bench::complexityBigO` when the evaluation has finished. See the tutorial + :ref:`asymptotic-complexity` for details. + + @endverbatim + + @tparam T Any type is cast to `double`. + @param b Length of N for the next benchmark run, so it is possible to calculate `bigO`. + */ + template + Bench& complexityN(T b) noexcept; + ANKERL_NANOBENCH(NODISCARD) double complexityN() const noexcept; + + /*! + Calculates [Big O](https://en.wikipedia.org/wiki/Big_O_notation>) of the results with all preconfigured complexity functions. + Currently these complexity functions are fitted into the benchmark results: + + @f$ \mathcal{O}(1) @f$, + @f$ \mathcal{O}(n) @f$, + @f$ \mathcal{O}(\log{}n) @f$, + @f$ \mathcal{O}(n\log{}n) @f$, + @f$ \mathcal{O}(n^2) @f$, + @f$ \mathcal{O}(n^3) @f$. + + If we e.g. evaluate the complexity of `std::sort`, this is the result of `std::cout << bench.complexityBigO()`: + + ``` + | coefficient | err% | complexity + |--------------:|-------:|------------ + | 5.08935e-09 | 2.6% | O(n log n) + | 6.10608e-08 | 8.0% | O(n) + | 1.29307e-11 | 47.2% | O(n^2) + | 2.48677e-15 | 69.6% | O(n^3) + | 9.88133e-06 | 132.3% | O(log n) + | 5.98793e-05 | 162.5% | O(1) + ``` + + So in this case @f$ \mathcal{O}(n\log{}n) @f$ provides the best approximation. + + @verbatim embed:rst + See the tutorial :ref:`asymptotic-complexity` for details. + @endverbatim + @return Evaluation results, which can be printed or otherwise inspected. + */ + std::vector complexityBigO() const; + + /** + * @brief Calculates bigO for a custom function. + * + * E.g. to calculate the mean squared error for @f$ \mathcal{O}(\log{}\log{}n) @f$, which is not part of the default set of + * complexityBigO(), you can do this: + * + * ``` + * auto logLogN = bench.complexityBigO("O(log log n)", [](double n) { + * return std::log2(std::log2(n)); + * }); + * ``` + * + * The resulting mean squared error can be printed with `std::cout << logLogN`. E.g. it prints something like this: + * + * ```text + * 2.46985e-05 * O(log log n), rms=1.48121 + * ``` + * + * @tparam Op Type of mapping operation. + * @param name Name for the function, e.g. "O(log log n)" + * @param op Op's operator() maps a `double` with the desired complexity function, e.g. `log2(log2(n))`. + * @return BigO Error calculation, which is streamable to std::cout. + */ + template + BigO complexityBigO(char const* name, Op op) const; + + template + BigO complexityBigO(std::string const& name, Op op) const; + + /*! + @verbatim embed:rst + + Convenience shortcut to :cpp:func:`ankerl::nanobench::render`. + + @endverbatim + */ + Bench& render(char const* templateContent, std::ostream& os); + Bench& render(std::string const& templateContent, std::ostream& os); + + Bench& config(Config const& benchmarkConfig); + ANKERL_NANOBENCH(NODISCARD) Config const& config() const noexcept; + +private: + Config mConfig{}; + std::vector mResults{}; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +/** + * @brief Makes sure none of the given arguments are optimized away by the compiler. + * + * @tparam Arg Type of the argument that shouldn't be optimized away. + * @param arg The input that we mark as being used, even though we don't do anything with it. + */ +template +void doNotOptimizeAway(Arg&& arg); + +namespace detail { + +#if defined(_MSC_VER) +void doNotOptimizeAwaySink(void const*); + +template +void doNotOptimizeAway(T const& val); + +#else + +// These assembly magic is directly from what Google Benchmark is doing. I have previously used what facebook's folly was doing, but +// this seemd to have compilation problems in some cases. Google Benchmark seemed to be the most well tested anyways. +// see https://github.com/google/benchmark/blob/master/include/benchmark/benchmark.h#L307 +template +void doNotOptimizeAway(T const& val) { + // NOLINTNEXTLINE(hicpp-no-assembler) + asm volatile("" : : "r,m"(val) : "memory"); +} + +template +void doNotOptimizeAway(T& val) { +# if defined(__clang__) + // NOLINTNEXTLINE(hicpp-no-assembler) + asm volatile("" : "+r,m"(val) : : "memory"); +# else + // NOLINTNEXTLINE(hicpp-no-assembler) + asm volatile("" : "+m,r"(val) : : "memory"); +# endif +} +#endif + +// internally used, but visible because run() is templated. +// Not movable/copy-able, so we simply use a pointer instead of unique_ptr. This saves us from +// having to include , and the template instantiation overhead of unique_ptr which is unfortunately quite significant. +ANKERL_NANOBENCH(IGNORE_EFFCPP_PUSH) +class IterationLogic { +public: + explicit IterationLogic(Bench const& config) noexcept; + ~IterationLogic(); + + ANKERL_NANOBENCH(NODISCARD) uint64_t numIters() const noexcept; + void add(std::chrono::nanoseconds elapsed, PerformanceCounters const& pc) noexcept; + void moveResultTo(std::vector& results) noexcept; + +private: + struct Impl; + Impl* mPimpl; +}; +ANKERL_NANOBENCH(IGNORE_EFFCPP_POP) + +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +class PerformanceCounters { +public: + PerformanceCounters(PerformanceCounters const&) = delete; + PerformanceCounters& operator=(PerformanceCounters const&) = delete; + + PerformanceCounters(); + ~PerformanceCounters(); + + void beginMeasure(); + void endMeasure(); + void updateResults(uint64_t numIters); + + ANKERL_NANOBENCH(NODISCARD) PerfCountSet const& val() const noexcept; + ANKERL_NANOBENCH(NODISCARD) PerfCountSet const& has() const noexcept; + +private: +#if ANKERL_NANOBENCH(PERF_COUNTERS) + LinuxPerformanceCounters* mPc = nullptr; +#endif + PerfCountSet mVal{}; + PerfCountSet mHas{}; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +// Gets the singleton +PerformanceCounters& performanceCounters(); + +} // namespace detail + +class BigO { +public: + using RangeMeasure = std::vector>; + + template + static RangeMeasure mapRangeMeasure(RangeMeasure data, Op op) { + for (auto& rangeMeasure : data) { + rangeMeasure.first = op(rangeMeasure.first); + } + return data; + } + + static RangeMeasure collectRangeMeasure(std::vector const& results); + + template + BigO(char const* bigOName, RangeMeasure const& rangeMeasure, Op rangeToN) + : BigO(bigOName, mapRangeMeasure(rangeMeasure, rangeToN)) {} + + template + BigO(std::string const& bigOName, RangeMeasure const& rangeMeasure, Op rangeToN) + : BigO(bigOName, mapRangeMeasure(rangeMeasure, rangeToN)) {} + + BigO(char const* bigOName, RangeMeasure const& scaledRangeMeasure); + BigO(std::string const& bigOName, RangeMeasure const& scaledRangeMeasure); + ANKERL_NANOBENCH(NODISCARD) std::string const& name() const noexcept; + ANKERL_NANOBENCH(NODISCARD) double constant() const noexcept; + ANKERL_NANOBENCH(NODISCARD) double normalizedRootMeanSquare() const noexcept; + ANKERL_NANOBENCH(NODISCARD) bool operator<(BigO const& other) const noexcept; + +private: + std::string mName{}; + double mConstant{}; + double mNormalizedRootMeanSquare{}; +}; +std::ostream& operator<<(std::ostream& os, BigO const& bigO); +std::ostream& operator<<(std::ostream& os, std::vector const& bigOs); + +} // namespace nanobench +} // namespace ankerl + +// implementation ///////////////////////////////////////////////////////////////////////////////// + +namespace ankerl { +namespace nanobench { + +constexpr uint64_t(Rng::min)() { + return 0; +} + +constexpr uint64_t(Rng::max)() { + return (std::numeric_limits::max)(); +} + +ANKERL_NANOBENCH_NO_SANITIZE("integer", "undefined") +uint64_t Rng::operator()() noexcept { + auto x = mX; + + mX = UINT64_C(15241094284759029579) * mY; + mY = rotl(mY - x, 27); + + return x; +} + +ANKERL_NANOBENCH_NO_SANITIZE("integer", "undefined") +uint32_t Rng::bounded(uint32_t range) noexcept { + uint64_t r32 = static_cast(operator()()); + auto multiresult = r32 * range; + return static_cast(multiresult >> 32U); +} + +double Rng::uniform01() noexcept { + auto i = (UINT64_C(0x3ff) << 52U) | (operator()() >> 12U); + // can't use union in c++ here for type puning, it's undefined behavior. + // std::memcpy is optimized anyways. + double d; + std::memcpy(&d, &i, sizeof(double)); + return d - 1.0; +} + +template +void Rng::shuffle(Container& container) noexcept { + auto size = static_cast(container.size()); + for (auto i = size; i > 1U; --i) { + using std::swap; + auto p = bounded(i); // number in [0, i) + swap(container[i - 1], container[p]); + } +} + +constexpr uint64_t Rng::rotl(uint64_t x, unsigned k) noexcept { + return (x << k) | (x >> (64U - k)); +} + +template +ANKERL_NANOBENCH_NO_SANITIZE("integer") +Bench& Bench::run(Op&& op) { + // It is important that this method is kept short so the compiler can do better optimizations/ inlining of op() + detail::IterationLogic iterationLogic(*this); + auto& pc = detail::performanceCounters(); + + while (auto n = iterationLogic.numIters()) { + pc.beginMeasure(); + Clock::time_point before = Clock::now(); + while (n-- > 0) { + op(); + } + Clock::time_point after = Clock::now(); + pc.endMeasure(); + pc.updateResults(iterationLogic.numIters()); + iterationLogic.add(after - before, pc); + } + iterationLogic.moveResultTo(mResults); + return *this; +} + +// Performs all evaluations. +template +Bench& Bench::run(char const* benchmarkName, Op&& op) { + name(benchmarkName); + return run(std::forward(op)); +} + +template +Bench& Bench::run(std::string const& benchmarkName, Op&& op) { + name(benchmarkName); + return run(std::forward(op)); +} + +template +BigO Bench::complexityBigO(char const* benchmarkName, Op op) const { + return BigO(benchmarkName, BigO::collectRangeMeasure(mResults), op); +} + +template +BigO Bench::complexityBigO(std::string const& benchmarkName, Op op) const { + return BigO(benchmarkName, BigO::collectRangeMeasure(mResults), op); +} + +// Set the batch size, e.g. number of processed bytes, or some other metric for the size of the processed data in each iteration. +// Any argument is cast to double. +template +Bench& Bench::batch(T b) noexcept { + mConfig.mBatch = static_cast(b); + return *this; +} + +// Sets the computation complexity of the next run. Any argument is cast to double. +template +Bench& Bench::complexityN(T n) noexcept { + mConfig.mComplexityN = static_cast(n); + return *this; +} + +// Convenience: makes sure none of the given arguments are optimized away by the compiler. +template +Bench& Bench::doNotOptimizeAway(Arg&& arg) { + detail::doNotOptimizeAway(std::forward(arg)); + return *this; +} + +// Makes sure none of the given arguments are optimized away by the compiler. +template +void doNotOptimizeAway(Arg&& arg) { + detail::doNotOptimizeAway(std::forward(arg)); +} + +namespace detail { + +#if defined(_MSC_VER) +template +void doNotOptimizeAway(T const& val) { + doNotOptimizeAwaySink(&val); +} + +#endif + +} // namespace detail +} // namespace nanobench +} // namespace ankerl + +#if defined(ANKERL_NANOBENCH_IMPLEMENT) + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// implementation part - only visible in .cpp +/////////////////////////////////////////////////////////////////////////////////////////////////// + +# include // sort, reverse +# include // compare_exchange_strong in loop overhead +# include // getenv +# include // strstr, strncmp +# include // ifstream to parse proc files +# include // setw, setprecision +# include // cout +# include // accumulate +# include // random_device +# include // to_s in Number +# include // throw for rendering templates +# include // std::tie +# if defined(__linux__) +# include //sysconf +# endif +# if ANKERL_NANOBENCH(PERF_COUNTERS) +# include // map + +# include +# include +# include +# include +# endif + +// declarations /////////////////////////////////////////////////////////////////////////////////// + +namespace ankerl { +namespace nanobench { + +// helper stuff that is only intended to be used internally +namespace detail { + +struct TableInfo; + +// formatting utilities +namespace fmt { + +class NumSep; +class StreamStateRestorer; +class Number; +class MarkDownColumn; +class MarkDownCode; + +} // namespace fmt +} // namespace detail +} // namespace nanobench +} // namespace ankerl + +// definitions //////////////////////////////////////////////////////////////////////////////////// + +namespace ankerl { +namespace nanobench { + +uint64_t splitMix64(uint64_t& state) noexcept; + +namespace detail { + +// helpers to get double values +template +inline double d(T t) noexcept { + return static_cast(t); +} +inline double d(Clock::duration duration) noexcept { + return std::chrono::duration_cast>(duration).count(); +} + +// Calculates clock resolution once, and remembers the result +inline Clock::duration clockResolution() noexcept; + +} // namespace detail + +namespace templates { + +char const* csv() noexcept { + return R"DELIM("title";"name";"unit";"batch";"elapsed";"error %";"instructions";"branches";"branch misses";"total" +{{#result}}"{{title}}";"{{name}}";"{{unit}}";{{batch}};{{median(elapsed)}};{{medianAbsolutePercentError(elapsed)}};{{median(instructions)}};{{median(branchinstructions)}};{{median(branchmisses)}};{{sumProduct(iterations, elapsed)}} +{{/result}})DELIM"; +} + +char const* htmlBoxplot() noexcept { + return R"DELIM( + + + + + + +
+ + + +)DELIM"; +} + +char const* pyperf() noexcept { + return R"DELIM({ + "benchmarks": [ + { + "runs": [ + { + "values": [ +{{#measurement}} {{elapsed}}{{^-last}}, +{{/last}}{{/measurement}} + ] + } + ] + } + ], + "metadata": { + "loops": {{sum(iterations)}}, + "inner_loops": {{batch}}, + "name": "{{title}}", + "unit": "second" + }, + "version": "1.0" +})DELIM"; +} + +char const* json() noexcept { + return R"DELIM({ + "results": [ +{{#result}} { + "title": "{{title}}", + "name": "{{name}}", + "unit": "{{unit}}", + "batch": {{batch}}, + "complexityN": {{complexityN}}, + "epochs": {{epochs}}, + "clockResolution": {{clockResolution}}, + "clockResolutionMultiple": {{clockResolutionMultiple}}, + "maxEpochTime": {{maxEpochTime}}, + "minEpochTime": {{minEpochTime}}, + "minEpochIterations": {{minEpochIterations}}, + "epochIterations": {{epochIterations}}, + "warmup": {{warmup}}, + "relative": {{relative}}, + "median(elapsed)": {{median(elapsed)}}, + "medianAbsolutePercentError(elapsed)": {{medianAbsolutePercentError(elapsed)}}, + "median(instructions)": {{median(instructions)}}, + "medianAbsolutePercentError(instructions)": {{medianAbsolutePercentError(instructions)}}, + "median(cpucycles)": {{median(cpucycles)}}, + "median(contextswitches)": {{median(contextswitches)}}, + "median(pagefaults)": {{median(pagefaults)}}, + "median(branchinstructions)": {{median(branchinstructions)}}, + "median(branchmisses)": {{median(branchmisses)}}, + "totalTime": {{sumProduct(iterations, elapsed)}}, + "measurements": [ +{{#measurement}} { + "iterations": {{iterations}}, + "elapsed": {{elapsed}}, + "pagefaults": {{pagefaults}}, + "cpucycles": {{cpucycles}}, + "contextswitches": {{contextswitches}}, + "instructions": {{instructions}}, + "branchinstructions": {{branchinstructions}}, + "branchmisses": {{branchmisses}} + }{{^-last}},{{/-last}} +{{/measurement}} ] + }{{^-last}},{{/-last}} +{{/result}} ] +})DELIM"; +} + +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +struct Node { + enum class Type { tag, content, section, inverted_section }; + + char const* begin; + char const* end; + std::vector children; + Type type; + + template + // NOLINTNEXTLINE(hicpp-avoid-c-arrays,modernize-avoid-c-arrays,cppcoreguidelines-avoid-c-arrays) + bool operator==(char const (&str)[N]) const noexcept { + return static_cast(std::distance(begin, end) + 1) == N && 0 == strncmp(str, begin, N - 1); + } +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +static std::vector parseMustacheTemplate(char const** tpl) { + std::vector nodes; + + while (true) { + auto begin = std::strstr(*tpl, "{{"); + auto end = begin; + if (begin != nullptr) { + begin += 2; + end = std::strstr(begin, "}}"); + } + + if (begin == nullptr || end == nullptr) { + // nothing found, finish node + nodes.emplace_back(Node{*tpl, *tpl + std::strlen(*tpl), std::vector{}, Node::Type::content}); + return nodes; + } + + nodes.emplace_back(Node{*tpl, begin - 2, std::vector{}, Node::Type::content}); + + // we found a tag + *tpl = end + 2; + switch (*begin) { + case '/': + // finished! bail out + return nodes; + + case '#': + nodes.emplace_back(Node{begin + 1, end, parseMustacheTemplate(tpl), Node::Type::section}); + break; + + case '^': + nodes.emplace_back(Node{begin + 1, end, parseMustacheTemplate(tpl), Node::Type::inverted_section}); + break; + + default: + nodes.emplace_back(Node{begin, end, std::vector{}, Node::Type::tag}); + break; + } + } +} + +static bool generateFirstLast(Node const& n, size_t idx, size_t size, std::ostream& out) { + ANKERL_NANOBENCH_LOG("n.type=" << static_cast(n.type)); + bool matchFirst = n == "-first"; + bool matchLast = n == "-last"; + if (!matchFirst && !matchLast) { + return false; + } + + bool doWrite = false; + if (n.type == Node::Type::section) { + doWrite = (matchFirst && idx == 0) || (matchLast && idx == size - 1); + } else if (n.type == Node::Type::inverted_section) { + doWrite = (matchFirst && idx != 0) || (matchLast && idx != size - 1); + } + + if (doWrite) { + for (auto const& child : n.children) { + if (child.type == Node::Type::content) { + out.write(child.begin, std::distance(child.begin, child.end)); + } + } + } + return true; +} + +static bool matchCmdArgs(std::string const& str, std::vector& matchResult) { + matchResult.clear(); + auto idxOpen = str.find('('); + auto idxClose = str.find(')', idxOpen); + if (idxClose == std::string::npos) { + return false; + } + + matchResult.emplace_back(str.substr(0, idxOpen)); + + // split by comma + matchResult.emplace_back(std::string{}); + for (size_t i = idxOpen + 1; i != idxClose; ++i) { + if (str[i] == ' ' || str[i] == '\t') { + // skip whitespace + continue; + } + if (str[i] == ',') { + // got a comma => new string + matchResult.emplace_back(std::string{}); + continue; + } + // no whitespace no comma, append + matchResult.back() += str[i]; + } + return true; +} + +static bool generateConfigTag(Node const& n, Config const& config, std::ostream& out) { + using detail::d; + + if (n == "title") { + out << config.mBenchmarkTitle; + return true; + } else if (n == "name") { + out << config.mBenchmarkName; + return true; + } else if (n == "unit") { + out << config.mUnit; + return true; + } else if (n == "batch") { + out << config.mBatch; + return true; + } else if (n == "complexityN") { + out << config.mComplexityN; + return true; + } else if (n == "epochs") { + out << config.mNumEpochs; + return true; + } else if (n == "clockResolution") { + out << d(detail::clockResolution()); + return true; + } else if (n == "clockResolutionMultiple") { + out << config.mClockResolutionMultiple; + return true; + } else if (n == "maxEpochTime") { + out << d(config.mMaxEpochTime); + return true; + } else if (n == "minEpochTime") { + out << d(config.mMinEpochTime); + return true; + } else if (n == "minEpochIterations") { + out << config.mMinEpochIterations; + return true; + } else if (n == "epochIterations") { + out << config.mEpochIterations; + return true; + } else if (n == "warmup") { + out << config.mWarmup; + return true; + } else if (n == "relative") { + out << config.mIsRelative; + return true; + } + return false; +} + +static std::ostream& generateResultTag(Node const& n, Result const& r, std::ostream& out) { + if (generateConfigTag(n, r.config(), out)) { + return out; + } + // match e.g. "median(elapsed)" + // g++ 4.8 doesn't implement std::regex :( + // static std::regex const regOpArg1("^([a-zA-Z]+)\\(([a-zA-Z]*)\\)$"); + // std::cmatch matchResult; + // if (std::regex_match(n.begin, n.end, matchResult, regOpArg1)) { + std::vector matchResult; + if (matchCmdArgs(std::string(n.begin, n.end), matchResult)) { + if (matchResult.size() == 2) { + auto m = Result::fromString(matchResult[1]); + if (m == Result::Measure::_size) { + return out << 0.0; + } + + if (matchResult[0] == "median") { + return out << r.median(m); + } + if (matchResult[0] == "average") { + return out << r.average(m); + } + if (matchResult[0] == "medianAbsolutePercentError") { + return out << r.medianAbsolutePercentError(m); + } + if (matchResult[0] == "sum") { + return out << r.sum(m); + } + if (matchResult[0] == "minimum") { + return out << r.minimum(m); + } + if (matchResult[0] == "maximum") { + return out << r.maximum(m); + } + } else if (matchResult.size() == 3) { + auto m1 = Result::fromString(matchResult[1]); + auto m2 = Result::fromString(matchResult[2]); + if (m1 == Result::Measure::_size || m2 == Result::Measure::_size) { + return out << 0.0; + } + + if (matchResult[0] == "sumProduct") { + return out << r.sumProduct(m1, m2); + } + } + } + + // match e.g. "sumProduct(elapsed, iterations)" + // static std::regex const regOpArg2("^([a-zA-Z]+)\\(([a-zA-Z]*)\\s*,\\s+([a-zA-Z]*)\\)$"); + + // nothing matches :( + throw std::runtime_error("command '" + std::string(n.begin, n.end) + "' not understood"); +} + +static void generateResultMeasurement(std::vector const& nodes, size_t idx, Result const& r, std::ostream& out) { + for (auto const& n : nodes) { + if (!generateFirstLast(n, idx, r.size(), out)) { + ANKERL_NANOBENCH_LOG("n.type=" << static_cast(n.type)); + switch (n.type) { + case Node::Type::content: + out.write(n.begin, std::distance(n.begin, n.end)); + break; + + case Node::Type::inverted_section: + throw std::runtime_error("got a inverted section inside measurement"); + + case Node::Type::section: + throw std::runtime_error("got a section inside measurement"); + + case Node::Type::tag: { + auto m = Result::fromString(std::string(n.begin, n.end)); + if (m == Result::Measure::_size || !r.has(m)) { + out << 0.0; + } else { + out << r.get(idx, m); + } + break; + } + } + } + } +} + +static void generateResult(std::vector const& nodes, size_t idx, std::vector const& results, std::ostream& out) { + auto const& r = results[idx]; + for (auto const& n : nodes) { + if (!generateFirstLast(n, idx, results.size(), out)) { + ANKERL_NANOBENCH_LOG("n.type=" << static_cast(n.type)); + switch (n.type) { + case Node::Type::content: + out.write(n.begin, std::distance(n.begin, n.end)); + break; + + case Node::Type::inverted_section: + throw std::runtime_error("got a inverted section inside result"); + + case Node::Type::section: + if (n == "measurement") { + for (size_t i = 0; i < r.size(); ++i) { + generateResultMeasurement(n.children, i, r, out); + } + } else { + throw std::runtime_error("got a section inside result"); + } + break; + + case Node::Type::tag: + generateResultTag(n, r, out); + break; + } + } + } +} + +} // namespace templates + +// helper stuff that only intended to be used internally +namespace detail { + +char const* getEnv(char const* name); +bool isEndlessRunning(std::string const& name); +bool isWarningsEnabled(); + +template +T parseFile(std::string const& filename); + +void gatherStabilityInformation(std::vector& warnings, std::vector& recommendations); +void printStabilityInformationOnce(std::ostream* os); + +// remembers the last table settings used. When it changes, a new table header is automatically written for the new entry. +uint64_t& singletonHeaderHash() noexcept; + +// determines resolution of the given clock. This is done by measuring multiple times and returning the minimum time difference. +Clock::duration calcClockResolution(size_t numEvaluations) noexcept; + +// formatting utilities +namespace fmt { + +// adds thousands separator to numbers +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +class NumSep : public std::numpunct { +public: + explicit NumSep(char sep); + char do_thousands_sep() const override; + std::string do_grouping() const override; + +private: + char mSep; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +// RAII to save & restore a stream's state +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +class StreamStateRestorer { +public: + explicit StreamStateRestorer(std::ostream& s); + ~StreamStateRestorer(); + + // sets back all stream info that we remembered at construction + void restore(); + + // don't allow copying / moving + StreamStateRestorer(StreamStateRestorer const&) = delete; + StreamStateRestorer& operator=(StreamStateRestorer const&) = delete; + StreamStateRestorer(StreamStateRestorer&&) = delete; + StreamStateRestorer& operator=(StreamStateRestorer&&) = delete; + +private: + std::ostream& mStream; + std::locale mLocale; + std::streamsize const mPrecision; + std::streamsize const mWidth; + std::ostream::char_type const mFill; + std::ostream::fmtflags const mFmtFlags; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +// Number formatter +class Number { +public: + Number(int width, int precision, double value); + Number(int width, int precision, int64_t value); + std::string to_s() const; + +private: + friend std::ostream& operator<<(std::ostream& os, Number const& n); + std::ostream& write(std::ostream& os) const; + + int mWidth; + int mPrecision; + double mValue; +}; + +// helper replacement for std::to_string of signed/unsigned numbers so we are locale independent +std::string to_s(uint64_t s); + +std::ostream& operator<<(std::ostream& os, Number const& n); + +class MarkDownColumn { +public: + MarkDownColumn(int w, int prec, std::string const& tit, std::string const& suff, double val); + std::string title() const; + std::string separator() const; + std::string invalid() const; + std::string value() const; + +private: + int mWidth; + int mPrecision; + std::string mTitle; + std::string mSuffix; + double mValue; +}; + +// Formats any text as markdown code, escaping backticks. +class MarkDownCode { +public: + explicit MarkDownCode(std::string const& what); + +private: + friend std::ostream& operator<<(std::ostream& os, MarkDownCode const& mdCode); + std::ostream& write(std::ostream& os) const; + + std::string mWhat{}; +}; + +std::ostream& operator<<(std::ostream& os, MarkDownCode const& mdCode); + +} // namespace fmt +} // namespace detail +} // namespace nanobench +} // namespace ankerl + +// implementation ///////////////////////////////////////////////////////////////////////////////// + +namespace ankerl { +namespace nanobench { + +void render(char const* mustacheTemplate, std::vector const& results, std::ostream& out) { + detail::fmt::StreamStateRestorer restorer(out); + + out.precision(std::numeric_limits::digits10); + auto nodes = templates::parseMustacheTemplate(&mustacheTemplate); + + for (auto const& n : nodes) { + ANKERL_NANOBENCH_LOG("n.type=" << static_cast(n.type)); + switch (n.type) { + case templates::Node::Type::content: + out.write(n.begin, std::distance(n.begin, n.end)); + break; + + case templates::Node::Type::inverted_section: + throw std::runtime_error("unknown list '" + std::string(n.begin, n.end) + "'"); + + case templates::Node::Type::section: + if (n == "result") { + const size_t nbResults = results.size(); + for (size_t i = 0; i < nbResults; ++i) { + generateResult(n.children, i, results, out); + } + } else if (n == "measurement") { + if (results.size() != 1) { + throw std::runtime_error( + "render: can only use section 'measurement' here if there is a single result, but there are " + + detail::fmt::to_s(results.size())); + } + // when we only have a single result, we can immediately go into its measurement. + auto const& r = results.front(); + for (size_t i = 0; i < r.size(); ++i) { + generateResultMeasurement(n.children, i, r, out); + } + } else { + throw std::runtime_error("render: unknown section '" + std::string(n.begin, n.end) + "'"); + } + break; + + case templates::Node::Type::tag: + if (results.size() == 1) { + // result & config are both supported there + generateResultTag(n, results.front(), out); + } else { + // This just uses the last result's config. + if (!generateConfigTag(n, results.back().config(), out)) { + throw std::runtime_error("unknown tag '" + std::string(n.begin, n.end) + "'"); + } + } + break; + } + } +} + +void render(std::string const& mustacheTemplate, std::vector const& results, std::ostream& out) { + render(mustacheTemplate.c_str(), results, out); +} + +void render(char const* mustacheTemplate, const Bench& bench, std::ostream& out) { + render(mustacheTemplate, bench.results(), out); +} + +void render(std::string const& mustacheTemplate, const Bench& bench, std::ostream& out) { + render(mustacheTemplate.c_str(), bench.results(), out); +} + +namespace detail { + +PerformanceCounters& performanceCounters() { +# if defined(__clang__) +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wexit-time-destructors" +# endif + static PerformanceCounters pc; +# if defined(__clang__) +# pragma clang diagnostic pop +# endif + return pc; +} + +// Windows version of doNotOptimizeAway +// see https://github.com/google/benchmark/blob/master/include/benchmark/benchmark.h#L307 +// see https://github.com/facebook/folly/blob/master/folly/Benchmark.h#L280 +// see https://docs.microsoft.com/en-us/cpp/preprocessor/optimize +# if defined(_MSC_VER) +# pragma optimize("", off) +void doNotOptimizeAwaySink(void const*) {} +# pragma optimize("", on) +# endif + +template +T parseFile(std::string const& filename) { + std::ifstream fin(filename); + T num{}; + fin >> num; + return num; +} + +char const* getEnv(char const* name) { +# if defined(_MSC_VER) +# pragma warning(push) +# pragma warning(disable : 4996) // getenv': This function or variable may be unsafe. +# endif + return std::getenv(name); +# if defined(_MSC_VER) +# pragma warning(pop) +# endif +} + +bool isEndlessRunning(std::string const& name) { + auto endless = getEnv("NANOBENCH_ENDLESS"); + return nullptr != endless && endless == name; +} + +// True when environment variable NANOBENCH_SUPPRESS_WARNINGS is either not set at all, or set to "0" +bool isWarningsEnabled() { + auto suppression = getEnv("NANOBENCH_SUPPRESS_WARNINGS"); + return nullptr == suppression || suppression == std::string("0"); +} + +void gatherStabilityInformation(std::vector& warnings, std::vector& recommendations) { + warnings.clear(); + recommendations.clear(); + + bool recommendCheckFlags = false; + +# if defined(DEBUG) + warnings.emplace_back("DEBUG defined"); + recommendCheckFlags = true; +# endif + + bool recommendPyPerf = false; +# if defined(__linux__) + auto nprocs = sysconf(_SC_NPROCESSORS_CONF); + if (nprocs <= 0) { + warnings.emplace_back("couldn't figure out number of processors - no governor, turbo check possible"); + } else { + + // check frequency scaling + for (long id = 0; id < nprocs; ++id) { + auto idStr = detail::fmt::to_s(static_cast(id)); + auto sysCpu = "/sys/devices/system/cpu/cpu" + idStr; + auto minFreq = parseFile(sysCpu + "/cpufreq/scaling_min_freq"); + auto maxFreq = parseFile(sysCpu + "/cpufreq/scaling_max_freq"); + if (minFreq != maxFreq) { + auto minMHz = static_cast(minFreq) / 1000.0; + auto maxMHz = static_cast(maxFreq) / 1000.0; + warnings.emplace_back("CPU frequency scaling enabled: CPU " + idStr + " between " + + detail::fmt::Number(1, 1, minMHz).to_s() + " and " + detail::fmt::Number(1, 1, maxMHz).to_s() + + " MHz"); + recommendPyPerf = true; + break; + } + } + + auto currentGovernor = parseFile("/sys/devices/system/cpu/cpu0/cpufreq/scaling_governor"); + if ("performance" != currentGovernor) { + warnings.emplace_back("CPU governor is '" + currentGovernor + "' but should be 'performance'"); + recommendPyPerf = true; + } + + if (0 == parseFile("/sys/devices/system/cpu/intel_pstate/no_turbo")) { + warnings.emplace_back("Turbo is enabled, CPU frequency will fluctuate"); + recommendPyPerf = true; + } + } +# endif + + if (recommendCheckFlags) { + recommendations.emplace_back("Make sure you compile for Release"); + } + if (recommendPyPerf) { + recommendations.emplace_back("Use 'pyperf system tune' before benchmarking. See https://github.com/psf/pyperf"); + } +} + +void printStabilityInformationOnce(std::ostream* outStream) { + static bool shouldPrint = true; + if (shouldPrint && outStream && isWarningsEnabled()) { + auto& os = *outStream; + shouldPrint = false; + std::vector warnings; + std::vector recommendations; + gatherStabilityInformation(warnings, recommendations); + if (warnings.empty()) { + return; + } + + os << "Warning, results might be unstable:" << std::endl; + for (auto const& w : warnings) { + os << "* " << w << std::endl; + } + + os << std::endl << "Recommendations" << std::endl; + for (auto const& r : recommendations) { + os << "* " << r << std::endl; + } + } +} + +// remembers the last table settings used. When it changes, a new table header is automatically written for the new entry. +uint64_t& singletonHeaderHash() noexcept { + static uint64_t sHeaderHash{}; + return sHeaderHash; +} + +ANKERL_NANOBENCH_NO_SANITIZE("integer", "undefined") +inline uint64_t hash_combine(uint64_t seed, uint64_t val) { + return seed ^ (val + UINT64_C(0x9e3779b9) + (seed << 6U) + (seed >> 2U)); +} + +// determines resolution of the given clock. This is done by measuring multiple times and returning the minimum time difference. +Clock::duration calcClockResolution(size_t numEvaluations) noexcept { + auto bestDuration = Clock::duration::max(); + Clock::time_point tBegin; + Clock::time_point tEnd; + for (size_t i = 0; i < numEvaluations; ++i) { + tBegin = Clock::now(); + do { + tEnd = Clock::now(); + } while (tBegin == tEnd); + bestDuration = (std::min)(bestDuration, tEnd - tBegin); + } + return bestDuration; +} + +// Calculates clock resolution once, and remembers the result +Clock::duration clockResolution() noexcept { + static Clock::duration sResolution = calcClockResolution(20); + return sResolution; +} + +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +struct IterationLogic::Impl { + enum class State { warmup, upscaling_runtime, measuring, endless }; + + explicit Impl(Bench const& bench) + : mBench(bench) + , mResult(bench.config()) { + printStabilityInformationOnce(mBench.output()); + + // determine target runtime per epoch + mTargetRuntimePerEpoch = detail::clockResolution() * mBench.clockResolutionMultiple(); + if (mTargetRuntimePerEpoch > mBench.maxEpochTime()) { + mTargetRuntimePerEpoch = mBench.maxEpochTime(); + } + if (mTargetRuntimePerEpoch < mBench.minEpochTime()) { + mTargetRuntimePerEpoch = mBench.minEpochTime(); + } + + if (isEndlessRunning(mBench.name())) { + std::cerr << "NANOBENCH_ENDLESS set: running '" << mBench.name() << "' endlessly" << std::endl; + mNumIters = (std::numeric_limits::max)(); + mState = State::endless; + } else if (0 != mBench.warmup()) { + mNumIters = mBench.warmup(); + mState = State::warmup; + } else if (0 != mBench.epochIterations()) { + // exact number of iterations + mNumIters = mBench.epochIterations(); + mState = State::measuring; + } else { + mNumIters = mBench.minEpochIterations(); + mState = State::upscaling_runtime; + } + } + + // directly calculates new iters based on elapsed&iters, and adds a 10% noise. Makes sure we don't underflow. + ANKERL_NANOBENCH(NODISCARD) uint64_t calcBestNumIters(std::chrono::nanoseconds elapsed, uint64_t iters) noexcept { + auto doubleElapsed = d(elapsed); + auto doubleTargetRuntimePerEpoch = d(mTargetRuntimePerEpoch); + auto doubleNewIters = doubleTargetRuntimePerEpoch / doubleElapsed * d(iters); + + auto doubleMinEpochIters = d(mBench.minEpochIterations()); + if (doubleNewIters < doubleMinEpochIters) { + doubleNewIters = doubleMinEpochIters; + } + doubleNewIters *= 1.0 + 0.2 * mRng.uniform01(); + + // +0.5 for correct rounding when casting + // NOLINTNEXTLINE(bugprone-incorrect-roundings) + return static_cast(doubleNewIters + 0.5); + } + + ANKERL_NANOBENCH_NO_SANITIZE("integer", "undefined") void upscale(std::chrono::nanoseconds elapsed) { + if (elapsed * 10 < mTargetRuntimePerEpoch) { + // we are far below the target runtime. Multiply iterations by 10 (with overflow check) + if (mNumIters * 10 < mNumIters) { + // overflow :-( + showResult("iterations overflow. Maybe your code got optimized away?"); + mNumIters = 0; + return; + } + mNumIters *= 10; + } else { + mNumIters = calcBestNumIters(elapsed, mNumIters); + } + } + + void add(std::chrono::nanoseconds elapsed, PerformanceCounters const& pc) noexcept { +# if defined(ANKERL_NANOBENCH_LOG_ENABLED) + auto oldIters = mNumIters; +# endif + + switch (mState) { + case State::warmup: + if (isCloseEnoughForMeasurements(elapsed)) { + // if elapsed is close enough, we can skip upscaling and go right to measurements + // still, we don't add the result to the measurements. + mState = State::measuring; + mNumIters = calcBestNumIters(elapsed, mNumIters); + } else { + // not close enough: switch to upscaling + mState = State::upscaling_runtime; + upscale(elapsed); + } + break; + + case State::upscaling_runtime: + if (isCloseEnoughForMeasurements(elapsed)) { + // if we are close enough, add measurement and switch to always measuring + mState = State::measuring; + mTotalElapsed += elapsed; + mTotalNumIters += mNumIters; + mResult.add(elapsed, mNumIters, pc); + mNumIters = calcBestNumIters(mTotalElapsed, mTotalNumIters); + } else { + upscale(elapsed); + } + break; + + case State::measuring: + // just add measurements - no questions asked. Even when runtime is low. But we can't ignore + // that fluctuation, or else we would bias the result + mTotalElapsed += elapsed; + mTotalNumIters += mNumIters; + mResult.add(elapsed, mNumIters, pc); + if (0 != mBench.epochIterations()) { + mNumIters = mBench.epochIterations(); + } else { + mNumIters = calcBestNumIters(mTotalElapsed, mTotalNumIters); + } + break; + + case State::endless: + mNumIters = (std::numeric_limits::max)(); + break; + } + + if (static_cast(mResult.size()) == mBench.epochs()) { + // we got all the results that we need, finish it + showResult(""); + mNumIters = 0; + } + + ANKERL_NANOBENCH_LOG(mBench.name() << ": " << detail::fmt::Number(20, 3, static_cast(elapsed.count())) << " elapsed, " + << detail::fmt::Number(20, 3, static_cast(mTargetRuntimePerEpoch.count())) + << " target. oldIters=" << oldIters << ", mNumIters=" << mNumIters + << ", mState=" << static_cast(mState)); + } + + void showResult(std::string const& errorMessage) const { + ANKERL_NANOBENCH_LOG(errorMessage); + + if (mBench.output() != nullptr) { + // prepare column data /////// + std::vector columns; + + auto rMedian = mResult.median(Result::Measure::elapsed); + + if (mBench.relative()) { + double d = 100.0; + if (!mBench.results().empty()) { + d = rMedian <= 0.0 ? 0.0 : mBench.results().front().median(Result::Measure::elapsed) / rMedian * 100.0; + } + columns.emplace_back(11, 1, "relative", "%", d); + } + + if (mBench.complexityN() > 0) { + columns.emplace_back(14, 0, "complexityN", "", mBench.complexityN()); + } + + columns.emplace_back(22, 2, mBench.timeUnitName() + "/" + mBench.unit(), "", + rMedian / (mBench.timeUnit().count() * mBench.batch())); + columns.emplace_back(22, 2, mBench.unit() + "/s", "", rMedian <= 0.0 ? 0.0 : mBench.batch() / rMedian); + + double rErrorMedian = mResult.medianAbsolutePercentError(Result::Measure::elapsed); + columns.emplace_back(10, 1, "err%", "%", rErrorMedian * 100.0); + + double rInsMedian = -1.0; + if (mResult.has(Result::Measure::instructions)) { + rInsMedian = mResult.median(Result::Measure::instructions); + columns.emplace_back(18, 2, "ins/" + mBench.unit(), "", rInsMedian / mBench.batch()); + } + + double rCycMedian = -1.0; + if (mResult.has(Result::Measure::cpucycles)) { + rCycMedian = mResult.median(Result::Measure::cpucycles); + columns.emplace_back(18, 2, "cyc/" + mBench.unit(), "", rCycMedian / mBench.batch()); + } + if (rInsMedian > 0.0 && rCycMedian > 0.0) { + columns.emplace_back(9, 3, "IPC", "", rCycMedian <= 0.0 ? 0.0 : rInsMedian / rCycMedian); + } + if (mResult.has(Result::Measure::branchinstructions)) { + double rBraMedian = mResult.median(Result::Measure::branchinstructions); + columns.emplace_back(17, 2, "bra/" + mBench.unit(), "", rBraMedian / mBench.batch()); + if (mResult.has(Result::Measure::branchmisses)) { + double p = 0.0; + if (rBraMedian >= 1e-9) { + p = 100.0 * mResult.median(Result::Measure::branchmisses) / rBraMedian; + } + columns.emplace_back(10, 1, "miss%", "%", p); + } + } + + columns.emplace_back(12, 2, "total", "", mResult.sumProduct(Result::Measure::iterations, Result::Measure::elapsed)); + + // write everything + auto& os = *mBench.output(); + + // combine all elements that are relevant for printing the header + uint64_t hash = 0; + hash = hash_combine(std::hash{}(mBench.unit()), hash); + hash = hash_combine(std::hash{}(mBench.title()), hash); + hash = hash_combine(std::hash{}(mBench.timeUnitName()), hash); + hash = hash_combine(std::hash{}(mBench.timeUnit().count()), hash); + hash = hash_combine(std::hash{}(mBench.relative()), hash); + hash = hash_combine(std::hash{}(mBench.performanceCounters()), hash); + + if (hash != singletonHeaderHash()) { + singletonHeaderHash() = hash; + + // no result yet, print header + os << std::endl; + for (auto const& col : columns) { + os << col.title(); + } + os << "| " << mBench.title() << std::endl; + + for (auto const& col : columns) { + os << col.separator(); + } + os << "|:" << std::string(mBench.title().size() + 1U, '-') << std::endl; + } + + if (!errorMessage.empty()) { + for (auto const& col : columns) { + os << col.invalid(); + } + os << "| :boom: " << fmt::MarkDownCode(mBench.name()) << " (" << errorMessage << ')' << std::endl; + } else { + for (auto const& col : columns) { + os << col.value(); + } + os << "| "; + auto showUnstable = isWarningsEnabled() && rErrorMedian >= 0.05; + if (showUnstable) { + os << ":wavy_dash: "; + } + os << fmt::MarkDownCode(mBench.name()); + if (showUnstable) { + auto avgIters = static_cast(mTotalNumIters) / static_cast(mBench.epochs()); + // NOLINTNEXTLINE(bugprone-incorrect-roundings) + auto suggestedIters = static_cast(avgIters * 10 + 0.5); + + os << " (Unstable with ~" << detail::fmt::Number(1, 1, avgIters) + << " iters. Increase `minEpochIterations` to e.g. " << suggestedIters << ")"; + } + os << std::endl; + } + } + } + + ANKERL_NANOBENCH(NODISCARD) bool isCloseEnoughForMeasurements(std::chrono::nanoseconds elapsed) const noexcept { + return elapsed * 3 >= mTargetRuntimePerEpoch * 2; + } + + uint64_t mNumIters = 1; + Bench const& mBench; + std::chrono::nanoseconds mTargetRuntimePerEpoch{}; + Result mResult; + Rng mRng{123}; + std::chrono::nanoseconds mTotalElapsed{}; + uint64_t mTotalNumIters = 0; + + State mState = State::upscaling_runtime; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +IterationLogic::IterationLogic(Bench const& bench) noexcept + : mPimpl(new Impl(bench)) {} + +IterationLogic::~IterationLogic() { + if (mPimpl) { + delete mPimpl; + } +} + +uint64_t IterationLogic::numIters() const noexcept { + ANKERL_NANOBENCH_LOG(mPimpl->mBench.name() << ": mNumIters=" << mPimpl->mNumIters); + return mPimpl->mNumIters; +} + +void IterationLogic::add(std::chrono::nanoseconds elapsed, PerformanceCounters const& pc) noexcept { + mPimpl->add(elapsed, pc); +} + +void IterationLogic::moveResultTo(std::vector& results) noexcept { + results.emplace_back(std::move(mPimpl->mResult)); +} + +# if ANKERL_NANOBENCH(PERF_COUNTERS) + +ANKERL_NANOBENCH(IGNORE_PADDED_PUSH) +class LinuxPerformanceCounters { +public: + struct Target { + Target(uint64_t* targetValue_, bool correctMeasuringOverhead_, bool correctLoopOverhead_) + : targetValue(targetValue_) + , correctMeasuringOverhead(correctMeasuringOverhead_) + , correctLoopOverhead(correctLoopOverhead_) {} + + uint64_t* targetValue{}; + bool correctMeasuringOverhead{}; + bool correctLoopOverhead{}; + }; + + ~LinuxPerformanceCounters(); + + // quick operation + inline void start() {} + + inline void stop() {} + + bool monitor(perf_sw_ids swId, Target target); + bool monitor(perf_hw_id hwId, Target target); + + bool hasError() const noexcept { + return mHasError; + } + + // Just reading data is faster than enable & disabling. + // we subtract data ourselves. + inline void beginMeasure() { + if (mHasError) { + return; + } + + // NOLINTNEXTLINE(hicpp-signed-bitwise) + mHasError = -1 == ioctl(mFd, PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP); + if (mHasError) { + return; + } + + // NOLINTNEXTLINE(hicpp-signed-bitwise) + mHasError = -1 == ioctl(mFd, PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP); + } + + inline void endMeasure() { + if (mHasError) { + return; + } + + // NOLINTNEXTLINE(hicpp-signed-bitwise) + mHasError = (-1 == ioctl(mFd, PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP)); + if (mHasError) { + return; + } + + auto const numBytes = sizeof(uint64_t) * mCounters.size(); + auto ret = read(mFd, mCounters.data(), numBytes); + mHasError = ret != static_cast(numBytes); + } + + void updateResults(uint64_t numIters); + + // rounded integer division + template + static inline T divRounded(T a, T divisor) { + return (a + divisor / 2) / divisor; + } + + template + ANKERL_NANOBENCH_NO_SANITIZE("integer", "undefined") + void calibrate(Op&& op) { + // clear current calibration data, + for (auto& v : mCalibratedOverhead) { + v = UINT64_C(0); + } + + // create new calibration data + auto newCalibration = mCalibratedOverhead; + for (auto& v : newCalibration) { + v = (std::numeric_limits::max)(); + } + for (size_t iter = 0; iter < 100; ++iter) { + beginMeasure(); + op(); + endMeasure(); + if (mHasError) { + return; + } + + for (size_t i = 0; i < newCalibration.size(); ++i) { + auto diff = mCounters[i]; + if (newCalibration[i] > diff) { + newCalibration[i] = diff; + } + } + } + + mCalibratedOverhead = std::move(newCalibration); + + { + // calibrate loop overhead. For branches & instructions this makes sense, not so much for everything else like cycles. + // marsaglia's xorshift: mov, sal/shr, xor. Times 3. + // This has the nice property that the compiler doesn't seem to be able to optimize multiple calls any further. + // see https://godbolt.org/z/49RVQ5 + uint64_t const numIters = 100000U + (std::random_device{}() & 3); + uint64_t n = numIters; + uint32_t x = 1234567; + auto fn = [&]() { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + }; + + beginMeasure(); + while (n-- > 0) { + fn(); + } + endMeasure(); + detail::doNotOptimizeAway(x); + auto measure1 = mCounters; + + n = numIters; + beginMeasure(); + while (n-- > 0) { + // we now run *twice* so we can easily calculate the overhead + fn(); + fn(); + } + endMeasure(); + detail::doNotOptimizeAway(x); + auto measure2 = mCounters; + + for (size_t i = 0; i < mCounters.size(); ++i) { + // factor 2 because we have two instructions per loop + auto m1 = measure1[i] > mCalibratedOverhead[i] ? measure1[i] - mCalibratedOverhead[i] : 0; + auto m2 = measure2[i] > mCalibratedOverhead[i] ? measure2[i] - mCalibratedOverhead[i] : 0; + auto overhead = m1 * 2 > m2 ? m1 * 2 - m2 : 0; + + mLoopOverhead[i] = divRounded(overhead, numIters); + } + } + } + +private: + bool monitor(uint32_t type, uint64_t eventid, Target target); + + std::map mIdToTarget{}; + + // start with minimum size of 3 for read_format + std::vector mCounters{3}; + std::vector mCalibratedOverhead{3}; + std::vector mLoopOverhead{3}; + + uint64_t mTimeEnabledNanos = 0; + uint64_t mTimeRunningNanos = 0; + int mFd = -1; + bool mHasError = false; +}; +ANKERL_NANOBENCH(IGNORE_PADDED_POP) + +LinuxPerformanceCounters::~LinuxPerformanceCounters() { + if (-1 != mFd) { + close(mFd); + } +} + +bool LinuxPerformanceCounters::monitor(perf_sw_ids swId, LinuxPerformanceCounters::Target target) { + return monitor(PERF_TYPE_SOFTWARE, swId, target); +} + +bool LinuxPerformanceCounters::monitor(perf_hw_id hwId, LinuxPerformanceCounters::Target target) { + return monitor(PERF_TYPE_HARDWARE, hwId, target); +} + +// overflow is ok, it's checked +ANKERL_NANOBENCH_NO_SANITIZE("integer", "undefined") +void LinuxPerformanceCounters::updateResults(uint64_t numIters) { + // clear old data + for (auto& id_value : mIdToTarget) { + *id_value.second.targetValue = UINT64_C(0); + } + + if (mHasError) { + return; + } + + mTimeEnabledNanos = mCounters[1] - mCalibratedOverhead[1]; + mTimeRunningNanos = mCounters[2] - mCalibratedOverhead[2]; + + for (uint64_t i = 0; i < mCounters[0]; ++i) { + auto idx = static_cast(3 + i * 2 + 0); + auto id = mCounters[idx + 1U]; + + auto it = mIdToTarget.find(id); + if (it != mIdToTarget.end()) { + + auto& tgt = it->second; + *tgt.targetValue = mCounters[idx]; + if (tgt.correctMeasuringOverhead) { + if (*tgt.targetValue >= mCalibratedOverhead[idx]) { + *tgt.targetValue -= mCalibratedOverhead[idx]; + } else { + *tgt.targetValue = 0U; + } + } + if (tgt.correctLoopOverhead) { + auto correctionVal = mLoopOverhead[idx] * numIters; + if (*tgt.targetValue >= correctionVal) { + *tgt.targetValue -= correctionVal; + } else { + *tgt.targetValue = 0U; + } + } + } + } +} + +bool LinuxPerformanceCounters::monitor(uint32_t type, uint64_t eventid, Target target) { + *target.targetValue = (std::numeric_limits::max)(); + if (mHasError) { + return false; + } + + auto pea = perf_event_attr(); + std::memset(&pea, 0, sizeof(perf_event_attr)); + pea.type = type; + pea.size = sizeof(perf_event_attr); + pea.config = eventid; + pea.disabled = 1; // start counter as disabled + pea.exclude_kernel = 1; + pea.exclude_hv = 1; + + // NOLINTNEXTLINE(hicpp-signed-bitwise) + pea.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID | PERF_FORMAT_TOTAL_TIME_ENABLED | PERF_FORMAT_TOTAL_TIME_RUNNING; + + const int pid = 0; // the current process + const int cpu = -1; // all CPUs +# if defined(PERF_FLAG_FD_CLOEXEC) // since Linux 3.14 + const unsigned long flags = PERF_FLAG_FD_CLOEXEC; +# else + const unsigned long flags = 0; +# endif + + auto fd = static_cast(syscall(__NR_perf_event_open, &pea, pid, cpu, mFd, flags)); + if (-1 == fd) { + return false; + } + if (-1 == mFd) { + // first call: set to fd, and use this from now on + mFd = fd; + } + uint64_t id = 0; + // NOLINTNEXTLINE(hicpp-signed-bitwise) + if (-1 == ioctl(fd, PERF_EVENT_IOC_ID, &id)) { + // couldn't get id + return false; + } + + // insert into map, rely on the fact that map's references are constant. + mIdToTarget.emplace(id, target); + + // prepare readformat with the correct size (after the insert) + auto size = 3 + 2 * mIdToTarget.size(); + mCounters.resize(size); + mCalibratedOverhead.resize(size); + mLoopOverhead.resize(size); + + return true; +} + +PerformanceCounters::PerformanceCounters() + : mPc(new LinuxPerformanceCounters()) + , mVal() + , mHas() { + + mHas.pageFaults = mPc->monitor(PERF_COUNT_SW_PAGE_FAULTS, LinuxPerformanceCounters::Target(&mVal.pageFaults, true, false)); + mHas.cpuCycles = mPc->monitor(PERF_COUNT_HW_REF_CPU_CYCLES, LinuxPerformanceCounters::Target(&mVal.cpuCycles, true, false)); + mHas.contextSwitches = + mPc->monitor(PERF_COUNT_SW_CONTEXT_SWITCHES, LinuxPerformanceCounters::Target(&mVal.contextSwitches, true, false)); + mHas.instructions = mPc->monitor(PERF_COUNT_HW_INSTRUCTIONS, LinuxPerformanceCounters::Target(&mVal.instructions, true, true)); + mHas.branchInstructions = + mPc->monitor(PERF_COUNT_HW_BRANCH_INSTRUCTIONS, LinuxPerformanceCounters::Target(&mVal.branchInstructions, true, false)); + mHas.branchMisses = mPc->monitor(PERF_COUNT_HW_BRANCH_MISSES, LinuxPerformanceCounters::Target(&mVal.branchMisses, true, false)); + // mHas.branchMisses = false; + + mPc->start(); + mPc->calibrate([] { + auto before = ankerl::nanobench::Clock::now(); + auto after = ankerl::nanobench::Clock::now(); + (void)before; + (void)after; + }); + + if (mPc->hasError()) { + // something failed, don't monitor anything. + mHas = PerfCountSet{}; + } +} + +PerformanceCounters::~PerformanceCounters() { + if (nullptr != mPc) { + delete mPc; + } +} + +void PerformanceCounters::beginMeasure() { + mPc->beginMeasure(); +} + +void PerformanceCounters::endMeasure() { + mPc->endMeasure(); +} + +void PerformanceCounters::updateResults(uint64_t numIters) { + mPc->updateResults(numIters); +} + +# else + +PerformanceCounters::PerformanceCounters() = default; +PerformanceCounters::~PerformanceCounters() = default; +void PerformanceCounters::beginMeasure() {} +void PerformanceCounters::endMeasure() {} +void PerformanceCounters::updateResults(uint64_t) {} + +# endif + +ANKERL_NANOBENCH(NODISCARD) PerfCountSet const& PerformanceCounters::val() const noexcept { + return mVal; +} +ANKERL_NANOBENCH(NODISCARD) PerfCountSet const& PerformanceCounters::has() const noexcept { + return mHas; +} + +// formatting utilities +namespace fmt { + +// adds thousands separator to numbers +NumSep::NumSep(char sep) + : mSep(sep) {} + +char NumSep::do_thousands_sep() const { + return mSep; +} + +std::string NumSep::do_grouping() const { + return "\003"; +} + +// RAII to save & restore a stream's state +StreamStateRestorer::StreamStateRestorer(std::ostream& s) + : mStream(s) + , mLocale(s.getloc()) + , mPrecision(s.precision()) + , mWidth(s.width()) + , mFill(s.fill()) + , mFmtFlags(s.flags()) {} + +StreamStateRestorer::~StreamStateRestorer() { + restore(); +} + +// sets back all stream info that we remembered at construction +void StreamStateRestorer::restore() { + mStream.imbue(mLocale); + mStream.precision(mPrecision); + mStream.width(mWidth); + mStream.fill(mFill); + mStream.flags(mFmtFlags); +} + +Number::Number(int width, int precision, int64_t value) + : mWidth(width) + , mPrecision(precision) + , mValue(static_cast(value)) {} + +Number::Number(int width, int precision, double value) + : mWidth(width) + , mPrecision(precision) + , mValue(value) {} + +std::ostream& Number::write(std::ostream& os) const { + StreamStateRestorer restorer(os); + os.imbue(std::locale(os.getloc(), new NumSep(','))); + os << std::setw(mWidth) << std::setprecision(mPrecision) << std::fixed << mValue; + return os; +} + +std::string Number::to_s() const { + std::stringstream ss; + write(ss); + return ss.str(); +} + +std::string to_s(uint64_t n) { + std::string str; + do { + str += static_cast('0' + static_cast(n % 10)); + n /= 10; + } while (n != 0); + std::reverse(str.begin(), str.end()); + return str; +} + +std::ostream& operator<<(std::ostream& os, Number const& n) { + return n.write(os); +} + +MarkDownColumn::MarkDownColumn(int w, int prec, std::string const& tit, std::string const& suff, double val) + : mWidth(w) + , mPrecision(prec) + , mTitle(tit) + , mSuffix(suff) + , mValue(val) {} + +std::string MarkDownColumn::title() const { + std::stringstream ss; + ss << '|' << std::setw(mWidth - 2) << std::right << mTitle << ' '; + return ss.str(); +} + +std::string MarkDownColumn::separator() const { + std::string sep(static_cast(mWidth), '-'); + sep.front() = '|'; + sep.back() = ':'; + return sep; +} + +std::string MarkDownColumn::invalid() const { + std::string sep(static_cast(mWidth), ' '); + sep.front() = '|'; + sep[sep.size() - 2] = '-'; + return sep; +} + +std::string MarkDownColumn::value() const { + std::stringstream ss; + auto width = mWidth - 2 - static_cast(mSuffix.size()); + ss << '|' << Number(width, mPrecision, mValue) << mSuffix << ' '; + return ss.str(); +} + +// Formats any text as markdown code, escaping backticks. +MarkDownCode::MarkDownCode(std::string const& what) { + mWhat.reserve(what.size() + 2); + mWhat.push_back('`'); + for (char c : what) { + mWhat.push_back(c); + if ('`' == c) { + mWhat.push_back('`'); + } + } + mWhat.push_back('`'); +} + +std::ostream& MarkDownCode::write(std::ostream& os) const { + return os << mWhat; +} + +std::ostream& operator<<(std::ostream& os, MarkDownCode const& mdCode) { + return mdCode.write(os); +} +} // namespace fmt +} // namespace detail + +// provide implementation here so it's only generated once +Config::Config() = default; +Config::~Config() = default; +Config& Config::operator=(Config const&) = default; +Config& Config::operator=(Config&&) = default; +Config::Config(Config const&) = default; +Config::Config(Config&&) noexcept = default; + +// provide implementation here so it's only generated once +Result::~Result() = default; +Result& Result::operator=(Result const&) = default; +Result& Result::operator=(Result&&) = default; +Result::Result(Result const&) = default; +Result::Result(Result&&) noexcept = default; + +namespace detail { +template +inline constexpr typename std::underlying_type::type u(T val) noexcept { + return static_cast::type>(val); +} +} // namespace detail + +// Result returned after a benchmark has finished. Can be used as a baseline for relative(). +Result::Result(Config const& benchmarkConfig) + : mConfig(benchmarkConfig) + , mNameToMeasurements{detail::u(Result::Measure::_size)} {} + +void Result::add(Clock::duration totalElapsed, uint64_t iters, detail::PerformanceCounters const& pc) { + using detail::d; + using detail::u; + + double dIters = d(iters); + mNameToMeasurements[u(Result::Measure::iterations)].push_back(dIters); + + mNameToMeasurements[u(Result::Measure::elapsed)].push_back(d(totalElapsed) / dIters); + if (pc.has().pageFaults) { + mNameToMeasurements[u(Result::Measure::pagefaults)].push_back(d(pc.val().pageFaults) / dIters); + } + if (pc.has().cpuCycles) { + mNameToMeasurements[u(Result::Measure::cpucycles)].push_back(d(pc.val().cpuCycles) / dIters); + } + if (pc.has().contextSwitches) { + mNameToMeasurements[u(Result::Measure::contextswitches)].push_back(d(pc.val().contextSwitches) / dIters); + } + if (pc.has().instructions) { + mNameToMeasurements[u(Result::Measure::instructions)].push_back(d(pc.val().instructions) / dIters); + } + if (pc.has().branchInstructions) { + double branchInstructions = 0.0; + // correcting branches: remove branch introduced by the while (...) loop for each iteration. + if (pc.val().branchInstructions > iters + 1U) { + branchInstructions = d(pc.val().branchInstructions - (iters + 1U)); + } + mNameToMeasurements[u(Result::Measure::branchinstructions)].push_back(branchInstructions / dIters); + + if (pc.has().branchMisses) { + // correcting branch misses + double branchMisses = d(pc.val().branchMisses); + if (branchMisses > branchInstructions) { + // can't have branch misses when there were branches... + branchMisses = branchInstructions; + } + + // assuming at least one missed branch for the loop + branchMisses -= 1.0; + if (branchMisses < 1.0) { + branchMisses = 1.0; + } + mNameToMeasurements[u(Result::Measure::branchmisses)].push_back(branchMisses / dIters); + } + } +} + +Config const& Result::config() const noexcept { + return mConfig; +} + +inline double calcMedian(std::vector& data) { + if (data.empty()) { + return 0.0; + } + std::sort(data.begin(), data.end()); + + auto midIdx = data.size() / 2U; + if (1U == (data.size() & 1U)) { + return data[midIdx]; + } + return (data[midIdx - 1U] + data[midIdx]) / 2U; +} + +double Result::median(Measure m) const { + // create a copy so we can sort + auto data = mNameToMeasurements[detail::u(m)]; + return calcMedian(data); +} + +double Result::average(Measure m) const { + using detail::d; + auto const& data = mNameToMeasurements[detail::u(m)]; + if (data.empty()) { + return 0.0; + } + + // create a copy so we can sort + return sum(m) / d(data.size()); +} + +double Result::medianAbsolutePercentError(Measure m) const { + // create copy + auto data = mNameToMeasurements[detail::u(m)]; + + // calculates MdAPE which is the median of percentage error + // see https://www.spiderfinancial.com/support/documentation/numxl/reference-manual/forecasting-performance/mdape + auto med = calcMedian(data); + + // transform the data to absolute error + for (auto& x : data) { + x = (x - med) / x; + if (x < 0) { + x = -x; + } + } + return calcMedian(data); +} + +double Result::sum(Measure m) const noexcept { + auto const& data = mNameToMeasurements[detail::u(m)]; + return std::accumulate(data.begin(), data.end(), 0.0); +} + +double Result::sumProduct(Measure m1, Measure m2) const noexcept { + auto const& data1 = mNameToMeasurements[detail::u(m1)]; + auto const& data2 = mNameToMeasurements[detail::u(m2)]; + + if (data1.size() != data2.size()) { + return 0.0; + } + + double result = 0.0; + for (size_t i = 0, s = data1.size(); i != s; ++i) { + result += data1[i] * data2[i]; + } + return result; +} + +bool Result::has(Measure m) const noexcept { + return !mNameToMeasurements[detail::u(m)].empty(); +} + +double Result::get(size_t idx, Measure m) const { + auto const& data = mNameToMeasurements[detail::u(m)]; + return data.at(idx); +} + +bool Result::empty() const noexcept { + return 0U == size(); +} + +size_t Result::size() const noexcept { + auto const& data = mNameToMeasurements[detail::u(Measure::elapsed)]; + return data.size(); +} + +double Result::minimum(Measure m) const noexcept { + auto const& data = mNameToMeasurements[detail::u(m)]; + if (data.empty()) { + return 0.0; + } + + // here its save to assume that at least one element is there + return *std::min_element(data.begin(), data.end()); +} + +double Result::maximum(Measure m) const noexcept { + auto const& data = mNameToMeasurements[detail::u(m)]; + if (data.empty()) { + return 0.0; + } + + // here its save to assume that at least one element is there + return *std::max_element(data.begin(), data.end()); +} + +Result::Measure Result::fromString(std::string const& str) { + if (str == "elapsed") { + return Measure::elapsed; + } else if (str == "iterations") { + return Measure::iterations; + } else if (str == "pagefaults") { + return Measure::pagefaults; + } else if (str == "cpucycles") { + return Measure::cpucycles; + } else if (str == "contextswitches") { + return Measure::contextswitches; + } else if (str == "instructions") { + return Measure::instructions; + } else if (str == "branchinstructions") { + return Measure::branchinstructions; + } else if (str == "branchmisses") { + return Measure::branchmisses; + } else { + // not found, return _size + return Measure::_size; + } +} + +// Configuration of a microbenchmark. +Bench::Bench() { + mConfig.mOut = &std::cout; +} + +Bench::Bench(Bench&&) = default; +Bench& Bench::operator=(Bench&&) = default; +Bench::Bench(Bench const&) = default; +Bench& Bench::operator=(Bench const&) = default; +Bench::~Bench() noexcept = default; + +double Bench::batch() const noexcept { + return mConfig.mBatch; +} + +double Bench::complexityN() const noexcept { + return mConfig.mComplexityN; +} + +// Set a baseline to compare it to. 100% it is exactly as fast as the baseline, >100% means it is faster than the baseline, <100% +// means it is slower than the baseline. +Bench& Bench::relative(bool isRelativeEnabled) noexcept { + mConfig.mIsRelative = isRelativeEnabled; + return *this; +} +bool Bench::relative() const noexcept { + return mConfig.mIsRelative; +} + +Bench& Bench::performanceCounters(bool showPerformanceCounters) noexcept { + mConfig.mShowPerformanceCounters = showPerformanceCounters; + return *this; +} +bool Bench::performanceCounters() const noexcept { + return mConfig.mShowPerformanceCounters; +} + +// Operation unit. Defaults to "op", could be e.g. "byte" for string processing. +// If u differs from currently set unit, the stored results will be cleared. +// Use singular (byte, not bytes). +Bench& Bench::unit(char const* u) { + if (u != mConfig.mUnit) { + mResults.clear(); + } + mConfig.mUnit = u; + return *this; +} + +Bench& Bench::unit(std::string const& u) { + return unit(u.c_str()); +} + +std::string const& Bench::unit() const noexcept { + return mConfig.mUnit; +} + +Bench& Bench::timeUnit(std::chrono::duration const& tu, std::string const& tuName) { + mConfig.mTimeUnit = tu; + mConfig.mTimeUnitName = tuName; + return *this; +} + +std::string const& Bench::timeUnitName() const noexcept { + return mConfig.mTimeUnitName; +} + +std::chrono::duration const& Bench::timeUnit() const noexcept { + return mConfig.mTimeUnit; +} + +// If benchmarkTitle differs from currently set title, the stored results will be cleared. +Bench& Bench::title(const char* benchmarkTitle) { + if (benchmarkTitle != mConfig.mBenchmarkTitle) { + mResults.clear(); + } + mConfig.mBenchmarkTitle = benchmarkTitle; + return *this; +} +Bench& Bench::title(std::string const& benchmarkTitle) { + if (benchmarkTitle != mConfig.mBenchmarkTitle) { + mResults.clear(); + } + mConfig.mBenchmarkTitle = benchmarkTitle; + return *this; +} + +std::string const& Bench::title() const noexcept { + return mConfig.mBenchmarkTitle; +} + +Bench& Bench::name(const char* benchmarkName) { + mConfig.mBenchmarkName = benchmarkName; + return *this; +} + +Bench& Bench::name(std::string const& benchmarkName) { + mConfig.mBenchmarkName = benchmarkName; + return *this; +} + +std::string const& Bench::name() const noexcept { + return mConfig.mBenchmarkName; +} + +// Number of epochs to evaluate. The reported result will be the median of evaluation of each epoch. +Bench& Bench::epochs(size_t numEpochs) noexcept { + mConfig.mNumEpochs = numEpochs; + return *this; +} +size_t Bench::epochs() const noexcept { + return mConfig.mNumEpochs; +} + +// Desired evaluation time is a multiple of clock resolution. Default is to be 1000 times above this measurement precision. +Bench& Bench::clockResolutionMultiple(size_t multiple) noexcept { + mConfig.mClockResolutionMultiple = multiple; + return *this; +} +size_t Bench::clockResolutionMultiple() const noexcept { + return mConfig.mClockResolutionMultiple; +} + +// Sets the maximum time each epoch should take. Default is 100ms. +Bench& Bench::maxEpochTime(std::chrono::nanoseconds t) noexcept { + mConfig.mMaxEpochTime = t; + return *this; +} +std::chrono::nanoseconds Bench::maxEpochTime() const noexcept { + return mConfig.mMaxEpochTime; +} + +// Sets the maximum time each epoch should take. Default is 100ms. +Bench& Bench::minEpochTime(std::chrono::nanoseconds t) noexcept { + mConfig.mMinEpochTime = t; + return *this; +} +std::chrono::nanoseconds Bench::minEpochTime() const noexcept { + return mConfig.mMinEpochTime; +} + +Bench& Bench::minEpochIterations(uint64_t numIters) noexcept { + mConfig.mMinEpochIterations = (numIters == 0) ? 1 : numIters; + return *this; +} +uint64_t Bench::minEpochIterations() const noexcept { + return mConfig.mMinEpochIterations; +} + +Bench& Bench::epochIterations(uint64_t numIters) noexcept { + mConfig.mEpochIterations = numIters; + return *this; +} +uint64_t Bench::epochIterations() const noexcept { + return mConfig.mEpochIterations; +} + +Bench& Bench::warmup(uint64_t numWarmupIters) noexcept { + mConfig.mWarmup = numWarmupIters; + return *this; +} +uint64_t Bench::warmup() const noexcept { + return mConfig.mWarmup; +} + +Bench& Bench::config(Config const& benchmarkConfig) { + mConfig = benchmarkConfig; + return *this; +} +Config const& Bench::config() const noexcept { + return mConfig; +} + +Bench& Bench::output(std::ostream* outstream) noexcept { + mConfig.mOut = outstream; + return *this; +} + +ANKERL_NANOBENCH(NODISCARD) std::ostream* Bench::output() const noexcept { + return mConfig.mOut; +} + +std::vector const& Bench::results() const noexcept { + return mResults; +} + +Bench& Bench::render(char const* templateContent, std::ostream& os) { + ::ankerl::nanobench::render(templateContent, *this, os); + return *this; +} + +Bench& Bench::render(std::string const& templateContent, std::ostream& os) { + ::ankerl::nanobench::render(templateContent, *this, os); + return *this; +} + +std::vector Bench::complexityBigO() const { + std::vector bigOs; + auto rangeMeasure = BigO::collectRangeMeasure(mResults); + bigOs.emplace_back("O(1)", rangeMeasure, [](double) { + return 1.0; + }); + bigOs.emplace_back("O(n)", rangeMeasure, [](double n) { + return n; + }); + bigOs.emplace_back("O(log n)", rangeMeasure, [](double n) { + return std::log2(n); + }); + bigOs.emplace_back("O(n log n)", rangeMeasure, [](double n) { + return n * std::log2(n); + }); + bigOs.emplace_back("O(n^2)", rangeMeasure, [](double n) { + return n * n; + }); + bigOs.emplace_back("O(n^3)", rangeMeasure, [](double n) { + return n * n * n; + }); + std::sort(bigOs.begin(), bigOs.end()); + return bigOs; +} + +Rng::Rng() + : mX(0) + , mY(0) { + std::random_device rd; + std::uniform_int_distribution dist; + do { + mX = dist(rd); + mY = dist(rd); + } while (mX == 0 && mY == 0); +} + +ANKERL_NANOBENCH_NO_SANITIZE("integer", "undefined") +uint64_t splitMix64(uint64_t& state) noexcept { + uint64_t z = (state += UINT64_C(0x9e3779b97f4a7c15)); + z = (z ^ (z >> 30U)) * UINT64_C(0xbf58476d1ce4e5b9); + z = (z ^ (z >> 27U)) * UINT64_C(0x94d049bb133111eb); + return z ^ (z >> 31U); +} + +// Seeded as described in romu paper (update april 2020) +Rng::Rng(uint64_t seed) noexcept + : mX(splitMix64(seed)) + , mY(splitMix64(seed)) { + for (size_t i = 0; i < 10; ++i) { + operator()(); + } +} + +// only internally used to copy the RNG. +Rng::Rng(uint64_t x, uint64_t y) noexcept + : mX(x) + , mY(y) {} + +Rng Rng::copy() const noexcept { + return Rng{mX, mY}; +} + +Rng::Rng(std::vector const& data) + : mX(0) + , mY(0) { + if (data.size() != 2) { + throw std::runtime_error("ankerl::nanobench::Rng::Rng: needed exactly 2 entries in data, but got " + + std::to_string(data.size())); + } + mX = data[0]; + mY = data[1]; +} + +std::vector Rng::state() const { + std::vector data(2); + data[0] = mX; + data[1] = mY; + return data; +} + +BigO::RangeMeasure BigO::collectRangeMeasure(std::vector const& results) { + BigO::RangeMeasure rangeMeasure; + for (auto const& result : results) { + if (result.config().mComplexityN > 0.0) { + rangeMeasure.emplace_back(result.config().mComplexityN, result.median(Result::Measure::elapsed)); + } + } + return rangeMeasure; +} + +BigO::BigO(std::string const& bigOName, RangeMeasure const& rangeMeasure) + : mName(bigOName) { + + // estimate the constant factor + double sumRangeMeasure = 0.0; + double sumRangeRange = 0.0; + + for (size_t i = 0; i < rangeMeasure.size(); ++i) { + sumRangeMeasure += rangeMeasure[i].first * rangeMeasure[i].second; + sumRangeRange += rangeMeasure[i].first * rangeMeasure[i].first; + } + mConstant = sumRangeMeasure / sumRangeRange; + + // calculate root mean square + double err = 0.0; + double sumMeasure = 0.0; + for (size_t i = 0; i < rangeMeasure.size(); ++i) { + auto diff = mConstant * rangeMeasure[i].first - rangeMeasure[i].second; + err += diff * diff; + + sumMeasure += rangeMeasure[i].second; + } + + auto n = static_cast(rangeMeasure.size()); + auto mean = sumMeasure / n; + mNormalizedRootMeanSquare = std::sqrt(err / n) / mean; +} + +BigO::BigO(const char* bigOName, RangeMeasure const& rangeMeasure) + : BigO(std::string(bigOName), rangeMeasure) {} + +std::string const& BigO::name() const noexcept { + return mName; +} + +double BigO::constant() const noexcept { + return mConstant; +} + +double BigO::normalizedRootMeanSquare() const noexcept { + return mNormalizedRootMeanSquare; +} + +bool BigO::operator<(BigO const& other) const noexcept { + return std::tie(mNormalizedRootMeanSquare, mName) < std::tie(other.mNormalizedRootMeanSquare, other.mName); +} + +std::ostream& operator<<(std::ostream& os, BigO const& bigO) { + return os << bigO.constant() << " * " << bigO.name() << ", rms=" << bigO.normalizedRootMeanSquare(); +} + +std::ostream& operator<<(std::ostream& os, std::vector const& bigOs) { + detail::fmt::StreamStateRestorer restorer(os); + os << std::endl << "| coefficient | err% | complexity" << std::endl << "|--------------:|-------:|------------" << std::endl; + for (auto const& bigO : bigOs) { + os << "|" << std::setw(14) << std::setprecision(7) << std::scientific << bigO.constant() << " "; + os << "|" << detail::fmt::Number(6, 1, bigO.normalizedRootMeanSquare() * 100.0) << "% "; + os << "| " << bigO.name(); + os << std::endl; + } + return os; +} + +} // namespace nanobench +} // namespace ankerl + +#endif // ANKERL_NANOBENCH_IMPLEMENT +#endif // ANKERL_NANOBENCH_H_INCLUDED diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..2113022 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,24 @@ +codecov: + notify: + require_ci_to_pass: yes + +coverage: + precision: 2 + + status: + project: yes + patch: yes + changes: yes + +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +comment: + layout: "header, diff" + behavior: default + require_changes: no diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..176ad28 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = kiwi +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..d887c22 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,6 @@ +Building the documentation +========================== + +The documentation is built using Sphinx and requires, the Read the Docs +theme (`pip install sphinx-rtd-theme`) and the sphinxtabs sphinx extension +(`pip install sphinx-tabs`). diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..daa679f --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build +set SPHINXPROJ=kiwi + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..309e439 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +sphinx>=4 +sphinx-rtd-theme>=1 +sphinx-tabs diff --git a/docs/source/api/cpp.rst b/docs/source/api/cpp.rst new file mode 100644 index 0000000..470dce8 --- /dev/null +++ b/docs/source/api/cpp.rst @@ -0,0 +1,4 @@ +Kiwisolver C++ API +================== + +Under construction diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst new file mode 100644 index 0000000..d2ef7bd --- /dev/null +++ b/docs/source/api/index.rst @@ -0,0 +1,8 @@ +kiwisolver +========== + +.. toctree:: + :maxdepth: 2 + + Python API + C++ API diff --git a/docs/source/api/python.rst b/docs/source/api/python.rst new file mode 100644 index 0000000..c07bb6b --- /dev/null +++ b/docs/source/api/python.rst @@ -0,0 +1,7 @@ +Kiwisolver Python API +===================== + +.. automodule:: kiwisolver + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/basis/basic_systems.rst b/docs/source/basis/basic_systems.rst new file mode 100644 index 0000000..4c2d537 --- /dev/null +++ b/docs/source/basis/basic_systems.rst @@ -0,0 +1,213 @@ +.. _basis-basic-systems: + +Constraints definition and system solving +========================================= + +.. include:: ../substitutions.sub + +A system within Kiwi is defined by a set of constraints, which may +be either equalities or inequalities (limited to >= and <=, as strict inequalities +are not supported). Each constraint can be assigned a specific 'strength', +indicating its relative importance in the problem-solving process. The subsequent +sections will delve into the methods of defining these constraints and extracting +results from the solver. + +Defining variables and constraints +---------------------------------- + +The initial step involves defining variables, which represent +the values that the solver aims to determine. These variables are +encapsulated by |Variable| objects. The creation of these objects +can be accomplished as follows: + +.. tabs:: + + .. code-tab:: python + + from kiwisolver import Variable + + x1 = Variable('x1') + x2 = Variable('x2') + xm = Variable('xm') + + .. code-tab:: c++ + + #include + + using namespace kiwi + + Variable x1("x1"); + Variable x2("x2"); + Variable xm("xm"); + +.. note:: + + Naming your variables is not mandatory but it is recommended since it will + help the solver in providing more meaningful error messages. + +Now that we have some variables we can define our constraints. + +.. tabs:: + + .. code-tab:: python + + constraints = [x1 >= 0, x2 <= 100, x2 >= x1 + 10, xm == (x1 + x2) / 2] + + .. code-tab:: c++ + + Constraint constraints[] = { Constraint {x1 >= 0}, + Constraint {x2 <= 100}, + Constraint {x2 >= x1 + 20}, + Constraint {xm == (x1 + x2) / 2} + }; + +Next, add these variables to the solver, an instance of |Solver|: + +.. tabs:: + + .. code-tab:: python + + from kiwisolver import Solver + + solver = Solver() + + for cn in constraints: + solver.addConstraint(cn) + + .. code-tab:: c++ + + Solver solver; + + for(auto& constraint : constraints) + { + solver.addConstraint(constraint); + } + +.. note:: + + You can start adding constraints to the solver without creating all your + variables first. + + +So far, we have defined a system representing three points on the segment +[0, 100], with one of them being the middle of the others, which cannot get +closer than 10. All those constraints have to be satisfied; in the context +of Cassowary, they are required constraints. + +.. note:: + + Cassowary (and Kiwi) allows for redundant constraints, which means + even with two constraints (x == 10, x + y == 30) being equivalent to a + third one (y == 20), all three can be added to the solver without issues. + + However, it is advisable not to add the same constraint multiple times + in the same form to the solver. + + +Managing constraints strength +----------------------------- + +Cassowary also supports constraints that are not required. Those are only +respected on a best effort basis. To express that a constraint is not required +we need to assign it a *strength*. Kiwi specifies three standard strengths +besides the "required" strength: strong, medium, weak. A strong constraint +will always win over a medium constraint, which in turn will always override +a weak constraint [#f1]_ . + +In our example, let's assume x1 would like to be at 40, without this being a +requirement. This is translated as follows: + +.. tabs:: + + .. code-tab:: python + + solver.addConstraint((x1 == 40) | "weak") + + .. code-tab:: c++ + + solver.addConstraint(x1 == 40 | strength::weak); + + +Adding edit variables +--------------------- + +So far our system is pretty static; we have no way of trying to find solutions +for a particular value of `xm`, let's say. This is a problem. In a real +application (e.g. a GUI layout), we would like to find the size of the widgets +based on the top window but also react to the window resizing, so actually +adding and removing constraints all the time wouldn't be optimal. And there is +a better way: edit variables. + +Edit variables are variables for which you can suggest values. Edit variables +have a strength which can be at most strong (the value of a edit variable can +never be required). + +For the sake of our example, we will make "xm" editable: + +.. tabs:: + + .. code-tab:: python + + solver.addEditVariable(xm, 'strong') + + .. code-tab:: c++ + + solver.addEditVariable(xm, strength::strong); + +Once a variable has been added as an edit variable, you can suggest a value for +it and the solver will try to solve the system with it. + +.. tabs:: + + .. code-tab:: python + + solver.suggestValue(xm, 60) + + .. code-tab:: c++ + + solver.suggestValue(xm, 60); + +This would give the following solution: ``xm == 60, x1 == 40, x2 == 80``. + + +Solving and updating variables +------------------------------ + +Kiwi solves the system each time a constraint is added or removed, or a new +value is suggested for an edit variable. Solving the system each time makes for +faster updates and allows to keep the solver in a consinstent state. However, +the variable values are not updated automatically, and you need to ask +the solver to perform this operation before reading the values, as illustrated +below: + +.. tabs:: + + .. code-tab:: python + + solver.suggestValue(xm, 90) + solver.updateVariables() + print(xm.value(), x1.value(), x2.value()) + + .. code-tab:: c++ + + solver.suggestValue(xm, 90); + solver.updateVariables(); + std::cout << xm.value() << ", " << x1.value() << ", " << x2.value(); + +This last update creates an infeasible situation by pushing x2 further than +100, if we keep x1 where it would like to be. As a consequence, we get the +following solution: ``xm == 90, x1 == 80, x2 == 100`` + + +.. note:: + + To determine if a non-required constraint was violated when solving the system, + you can use the constraint's ``violated`` method. + + .. versionadded:: 1.4 + +Footnotes +--------- + +.. [#f1] Actually, there are some corner cases in which this can be violated. + See :ref:`basics-internals` diff --git a/docs/source/basis/index.rst b/docs/source/basis/index.rst new file mode 100644 index 0000000..3025972 --- /dev/null +++ b/docs/source/basis/index.rst @@ -0,0 +1,22 @@ +.. _basis: + +Kiwisolver usage +================ + +.. include:: ../substitutions.sub + +This section of the docs aims at getting you up and running with Kiwi. You will +in particular learn how to install Kiwi, create a system of constraints, solve, +update it etc... By the end of it you will know how to use the solver. + +However if you are not familiar with Cassowary (or constraints solver in +general) it may not be enough to get you started using it in your project. +Hopefully the real world use cases described in :ref:`uses` will shed +more light on how to use it in real applications. + +.. toctree:: + :maxdepth: 2 + + installation.rst + basic_systems.rst + solver_internals.rst diff --git a/docs/source/basis/installation.rst b/docs/source/basis/installation.rst new file mode 100644 index 0000000..7f35ddd --- /dev/null +++ b/docs/source/basis/installation.rst @@ -0,0 +1,87 @@ +.. _basis-installation: + +Installing Kiwisolver on Python +=============================== + +.. include:: ../substitutions.sub + +Kiwisolver is supported on Python 3.7+. Installing it is a straight-forward +process. There are three approaches to choose from. + +The easy way: Pre-compiled packages +----------------------------------- + +The easiest way to install atom is through pre-compiled packages. Kiwisolver is +distributed pre-compiled in two-forms. + +Conda packages +^^^^^^^^^^^^^^ + +If you use the `Anaconda`_ Python distribution platform (or `Miniconda`_, its +lighter-weight companion), the latest release of Kiwisolver can be installed +using conda from the default channel or the conda-forge channel:: + + $ conda install kiwisolver + + $ conda install kiwisolver -c conda-forge + +.. _Anaconda: https://store.continuum.io/cshop/anaconda +.. _Miniconda: https://conda.io/miniconda.html + +Wheels +^^^^^^ + +If you don't use Anaconda, you can install Kiwisolver pre-compiled, +through PIP, for most common platforms:: + + $ pip install kiwisolver + +Compiling it yourself: The Hard Way +----------------------------------- + +Building Kiwisolver from scratch requires Python and a C++ compiler. On Unix +platform getting a C++ compiler properly configured is generally +straighforward. On Windows, starting with Python 3.6 the free version of the +Microsoft toolchain should work out of the box. Installing Kiwisolver is then +as simple as:: + + $ pip install . + +.. note:: + + For MacOSX users on OSX Mojave, one needs to set MACOSX_DEPLOYMENT_TARGET + to higher than 10.9 to force the compiler to use the new C++ stdlib:: + + $ export MACOSX_DEPLOYMENT_TARGET=10.10 + + +Supported Platforms +------------------- + +Kiwisolver is known to run on Windows, OSX, and Linux; and compiles cleanly +with MSVC, Clang, GCC, and MinGW. If you encounter a bug, please report +it on the `Issue Tracker`_. + +.. _Issue Tracker: http://github.com/nucleic/enaml/issues + + +Checking your install +--------------------- + +Once you installed kiwisolver you should be able to import it as follows: + +.. code:: python + + import kiwisolver + +.. note:: + + On Windows, the import may fail with `ImportError: DLL load failed`. If it + does, it means your system is missing the Microsoft Visual C++ + redistributable matching your Python version. To fix the issue download + and install the package corresponding to your Python version + (https://support.microsoft.com/en-us/help/2977003/the-latest-supported-visual-c-downloads): + + - Python 2.7: Visual C++ Redistributable 2008 + - Python 3.4: Visual C++ Redistributable 2010 + - Python 3.5+: Visual C++ Redistributable 2015 or more recent diff --git a/docs/source/basis/solver_internals.rst b/docs/source/basis/solver_internals.rst new file mode 100644 index 0000000..ba5eda4 --- /dev/null +++ b/docs/source/basis/solver_internals.rst @@ -0,0 +1,196 @@ +.. _basics-internals: + +Solver internals and tips +========================= + +.. include:: ../substitutions.sub + + +Kiwi is not a mere rewriting of Cassowary, and due to this, it does not always +perfectly reflect the original implementation. The following sections point out +these discrepancies and provide tips on how to work effectively with Kiwi. + + +Inspecting the solver state +--------------------------- + +The state of the solver can be inspected by dumping a text representation of +its state either to stdout using the ``dump`` method of the solver, or to a +string using the ``dumps`` method. Typically, at least a basic understanding of +the Cassowary algorithm is necessary to analyse the output. + +A typical output is reproduced below: + + +.. code:: + + Objective + --------- + -2 + 2 * e2 + 1 * s8 + -2 * s10 + + Tableau + ------- + v1 | 1 + 1 * s10 + e3 | -1 + 1 * e2 + -1 * s10 + v4 | -1 + -1 * d5 + -1 * s10 + s6 | -2 + -1 * s10 + e9 | -1 + 1 * s8 + -1 * s10 + + Infeasible + ---------- + e3 + e9 + + Variables + --------- + bar = v1 + foo = v4 + + Edit Variables + -------------- + bar + + Constraints + ----------- + 1 * bar + -0 >= 0 | strength = 1 + 1 * bar + 1 <= 0 | strength = 1.001e+09 + 1 * foo + 1 * bar + 0 == 0 | strength = 1.001e+09 + 1 * bar + 0 == 0 | strength = 1 + +In the dump, the letters have the following meaning: + +- v: external variable, corresponds to the variable created by you, the user +- s: slack symbol, used to represent inequalities +- e: error symbol, used to represent non-required constraints +- d: dummy variable, always zero, used to keep track of the impact of an + external variable in the tableau. +- i: invalid symbol, returned when no valid symbol can be found. + + +Stay constraints emulation +-------------------------- + +One feature of Cassowary that Kiwi abandoned is the notion of stay +constraints. Stay constraints are typically used in under-constrained +situations (drag and drop) to allow the solver to find a solution by keeping +non-modified variable close to their original position. A typical example is +a rectangle whose one corner is being dragged in a drawing application. + +Kiwi does not have stay constraints mostly because in the context of widget +placement, the system is typically well constrained, rendering stay constraints +unnecessary. + +If your application requires them, several workarounds can be considered: + +- adding/removing non-required equality constraints to mimic stay constraints +- using edit-variables to mimic stay constraints + +The first method will require to remove the old constraints as soon as they +stop making sense, while the second will require to update the suggested value. + + +Creating strengths and their internal representation +---------------------------------------------------- + +Kiwi provides three strengths in addition to the required strength by default: +"weak", "medium", and "strong". Contrary to Cassowary, which uses lexicographic +ordering to ensure that strength are always respected, Kiwi strives for speed +and uses simple floating point numbers. + +.. note:: + + Using simple floating point, means that in some rare corner cases, a large + number of weak constraints may outweigh a medium constraint. However, in + practice, this rarely happens. + +Kiwi allows to create custom strengths in the following manner: + +.. tabs:: + + .. code-tab:: python + + from kiwisolver import strength + + my_strength = strength.create(1, 1, 1) + my_strength2 = strength.create(1, 1, 1, 2) + + .. code-tab:: c++ + + double my_strength = strength::create(1, 1, 1); + double my_strength = strength::create(1, 1, 1, 2); + +The first argument is multiplied by 1 000 000, the second argument by 1 000, +and the third by 1. No strength can be create larger than the required +strength. The default strengths in Kiwi correspond to: + +.. code:: python + + weak = strength.create(0, 0, 1) + medium = strength.create(0, 1, 0) + strong = strength.create(1, 0, 0) + required = strength.create(1000, 1000, 1000) + +While Cassowary differentiates between strength and weight, those two concepts +are fused in Kiwi: when creating a strength, one can apply a weight (the fourth +argument) that will multiply it. + +.. note:: + + Because strengths are implemented as floating point numbers, in order to be + effective, strengths must be different enough from one another. The + following is unlikely to produce any really useful result. + + .. code:: python + + weak1 = strength.create(0, 0, 1) + weak2 = strength.create(0, 0, 2) + weak3 = strength.create(0, 0, 3) + + +Managing memory +--------------- + +When removing a constraint, Kiwi does not check whether or not the variables +used in the constraint are still in use in other constraints. This is mostly +because such checks could be quite expensive. However, this means the map of +variables can grow over time. + +To avoid this possibly causing large memory leaks, it is recommended to reset +the solver state (using the method of the same name) and to add back the +constraints, that are still valid at this point. + + +Representation of constraints +----------------------------- + +If you browse through the API documentation you may notice a number of classes +that do not appear anywhere in this documentation: Term and Expression. + +Those classes are used internally in constraints and are created automatically +by the library. A |Term| represents a variable/symbol and the coefficient that +multiplies it, |Expression| represents a sum of terms and a constant value and +is used as the left hand side of a constraint. + + +Performance implementation tricks +--------------------------------- + +Map type +^^^^^^^^ + +Kiwi uses maps to represent the state of the solver and to manipulate it. As a +consequence, the map type should be fast, with a particular emphasis on +iteration. The C++ standard library provides unordered_map and map that could +be used in kiwi, but none of those are very friendly to the CPU cache. For +this reason, Kiwi uses the AssocVector class implemented in Loki (slightly +updated to respect c++11 standards). The use of this class provides a 2x +speedup over std::map. + + +Symbol representation +^^^^^^^^^^^^^^^^^^^^^ + +Symbols are used in Kiwi to represent the state of the solver. Since solving the +system requires a large number of manipulations of the symbols, the operations +have to compile down to an efficient representation. In Kiwi, symbols compile +down to long long meaning that a vector of them fits in a CPU cache line. diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..f122dfd --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/main/config + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + +# NOTE the project needs to be installed for the docs to be properly built. +import kiwisolver + +# -- Project information ----------------------------------------------------- + +project = "kiwisolver" +copyright = "2018-2021, Nucleic team" +author = "Nucleic team" + +# The short X.Y version +version = ".".join(kiwisolver.__version__.split(".")[:2]) +# The full version, including alpha/beta/rc tags +release = kiwisolver.__version__ + + +# -- General configuration --------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.todo", + "sphinx.ext.coverage", + "sphinx.ext.imgmath", + "sphinx.ext.ifconfig", + "sphinx.ext.viewcode", + "sphinx.ext.graphviz", + "sphinx.ext.inheritance_diagram", + "sphinx.ext.autosummary", + "sphinx.ext.napoleon", + "sphinx_tabs.tabs", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = ".rst" + +# The main toctree document. +main_doc = "index" + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path . +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +# html_static_path = ['_static'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = "kiwidoc" + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (main_doc, "kiwi.tex", "kiwi Documentation", "Nucleic team", "manual"), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [(main_doc, "kiwi", "kiwi Documentation", [author], 1)] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + ( + main_doc, + "kiwi", + "kiwi Documentation", + author, + "kiwi", + "One line description of project.", + "Miscellaneous", + ), +] diff --git a/docs/source/developer_notes/index.rst b/docs/source/developer_notes/index.rst new file mode 100644 index 0000000..f183723 --- /dev/null +++ b/docs/source/developer_notes/index.rst @@ -0,0 +1,29 @@ +.. _developer: + +Developer notes +================ + +These notes are meant to help developers and contributors with regards to some +details of the implementation and coding style of the project. + +C++ codebase +------------ + +The C++ codebase currently targets C++11 compliance. It is header-only since +one of the focus of the library is speed. + + +Python bindings +--------------- + +Python bindings targets Python 3.7 and above. The bindings are hand-written and +relies on cppy (https://github.com/nucleic/cppy). Kiwisolver tries to use a +reasonably modern C API and to support sub-interpreter, this has a couple of +consequences: + +- all the non exported symbol are enclosed in anonymous namespaces +- kiwisolver does not use static types and only dynamical types (note that the + type slots and related structures are stored in a static variable) +- modules use the multi-phases initialization mechanism as defined in + PEP 489 -- Multi-phase extension module initialization +- static variables use is limited to type slots, method def diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..6b066c1 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,32 @@ +.. kiwi documentation main file, created by + sphinx-quickstart on Mon Oct 29 21:48:45 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to Kiwisolver's documentation! +====================================== + +Kiwisolver is an efficient C++ implementation of the Cassowary constraint +solving algorithm. Kiwi is an implementation of the algorithm based on the +seminal Cassowary paper. It is *not* a refactoring of the original C++ solver. +Kiwisolver has been designed from the ground up to be lightweight and fast. +Kiwisolver range from 10x to 500x faster than the original Cassowary solver +with typical use cases gaining a 40x improvement. Memory savings are +consistently > 5x. + +In addition to the C++ solver, Kiwi ships with hand-rolled Python bindings. + +.. toctree:: + :maxdepth: 2 + + Getting started + Use cases + Developer notes + API Documentation + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/substitutions.sub b/docs/source/substitutions.sub new file mode 100644 index 0000000..0843947 --- /dev/null +++ b/docs/source/substitutions.sub @@ -0,0 +1,16 @@ +.. + This file holds some common substitutions (mainly for referencing code). + To use it add the directive include:: substitutions.rst at the beginning of + the file. + +.. ============================================================================ +.. Kiwi substitutions +.. ============================================================================ + +.. |Variable| replace:: :py:class:`~kiwisolver.Variable` + +.. |Solver| replace:: :py:class:`~kiwisolver.Solver` + +.. |Term| replace:: :py:class:`~kiwisolver.Term` + +.. |Expression| replace:: :py:class:`~kiwisolver.Expression` diff --git a/docs/source/use_cases/enaml.rst b/docs/source/use_cases/enaml.rst new file mode 100644 index 0000000..71aeb70 --- /dev/null +++ b/docs/source/use_cases/enaml.rst @@ -0,0 +1,142 @@ +.. _uses-enaml: + +Enaml +===== + +.. include:: ../substitutions.sub + +Enaml is a programming language and framework for creating professional-quality +user interfaces with minimal effort. It relies on Kiwi to layout widgets. + +To implement its layout, Enaml uses a nestable model. Containers widgets +handle the constraints generation used to layout their children. Furthermore, +they pass to their their children a representation of the bounding box in +which they should live which allows the widgets to position themselves inside +their parent. Since each leaf component has a "preferred size", the system can +be solved from the bottom-up and set the size the parents based on the required +space of the children. If at a later time the parent is resized, this new input +can be used to solve the layout problem. + +The following sections will describe in more details how the constraints are +generated and the preferred size estimated. + +Widget variables +---------------- + +In Enaml, each widget that can be constrained defines a bunch of Kiwi +|Variable|: ``left``, ``top``, ``width`` and ``height``. In addition, +it provides easy access to combination of those: ``right``, ``bottom``, +``h_center``, ``v_center``. Those variables will be used to define the layout. + +In addition, because each widget has a preferred size, it defines a set of +constraints related to that preferred size on which the user can act upon by +modifying their strength: +- hug_width: equivalent to (width == hint) | hug_width +- hug_height: equivalent to (height == hint) | hug_height +- resist_width: equivalent to (width >= hint) | resist_width +- resist_height: equivalent to (height >= hint) | resist_height +- limit_width: equivalent to (width <= hint) | limit_width +- limit_height: equivalent to (height <= hint) | limit_height + +Finally, widget that can contain other widgets define a set of variables that +they expose to their children to allow to place themselves relative to their +parents. Those are: ``contents_left``, ``contents_top``, ``contents_width``, +``contents_height``, ``contents_right``, ``contents_bottom``, +``contents_h_center``, ``contents_v_center``. Those are usually not equivalent +to the non-prefixed variables (even-though they are related) because of the +container margins. + +The base classes used a mixin to implement those behaviors are defined in: +https://github.com/nucleic/enaml/blob/main/enaml/layout/constrainable.py + +Constraints definition +---------------------- + +Using the above variable, one can express any constraints. However, even for +simple vertical or horizontal boxes, the constraints to define, in +particular if one needs to introduce some spacing around the objects, become +quite painful to write by hand. + +To make constraints definition easier, Enaml relies on helpers function and +classes. In the following, we will focus on how horizontal and vertical boxes +constraints are handled, by studying the following example in details: + +.. image:: enaml_hbox.svg + +Here we consider a container widget with three child widgets. The outer black +frame represents the limit of the container. The dashed frame represents the +contents visible to children when defining their constraint. The container uses +the margin definition to relate the outer left, top, width and height to their +'contents' equivalent. + +The three widgets are arranged according to a horizontal box for which the +constraints are created using the `hbox` helper function which simply accepts +a list of widgets and spacers . To define the constraints from that list, Enaml +relies on the spacers represented here in orange. Each spacer has a +given size and a policy regarding that size (is it a minimum value, maximum, +how strongly to enforce that size). For each orientation, `hbox` add spacers so +that there is a spacer between each widget and between the widgets and the +parent boundaries. Some spacers can have a zero size, simply meaning that +widgets should be in contact. + +When generating the constraints, `hbox` will be passed the container and use +the spacers to generate the constraints by simply glueing the anchors of +surrounding widgets. Each spacer can generate multiple constraints which gives +this process a lot of flexibility. Furthermore, those helpers define the same +variable as the widgets allowing for to position groups with respect to one +another. + +.. note:: + + In practice, `hbox` itself relies on some helpers but the above gives you + the general idea. + +For further details you can have a look at the source of the helpers described +in this section which can be found in the Enaml source: + +- spacers: https://github.com/nucleic/enaml/blob/main/enaml/layout/spacers.py +- helpers: + + - https://github.com/nucleic/enaml/blob/main/enaml/layout/layout_helpers.py + - https://github.com/nucleic/enaml/blob/main/enaml/layout/linear_box_helper.py + - https://github.com/nucleic/enaml/blob/main/enaml/layout/sequence_helper.py + + +Setting up the solver +--------------------- + +So far we have only defined the constraints that represent the layout, we will +now turn to how Enaml pass those to the solver and how it handle updates and +solver resets. + +By default, each container manages its own solver independently. This has +the advantage of keeping the system relatively smalls and hence allow for +faster updates. When setting up the solver, the container will add for each +widget a set of constraints reflecting the preference of the widget regarding +its size as reported by the widget, and add to those the constraints defining +the layout. It will also add two edit variable representing the width and +height of the container. + +Once the solver has been set up it can be used to compute different values, +such as the best size for the container (requesting a size of 0 with a 0.1*weak +strength), its min size (0 size, medium strength) and max size (max size, +medium strength). + +When the parent is resized, the solver is invoked again with the new width and +height as suggestion. On the other hand, if the constraints change either +because widgets have been added or removed or because the users modified them, +the solver is reset and the constraints are rebuilt-from scratch. This means +that we never keep the solver around long enough to have to worry about memory +consumption due to unused variables in the solver. + +In a complex hierarchy, the top parent will request the sizes of the nested +containers which will trigger the solving of their constraints. At some point +in the nested structure, we will only find widgets which provides a size hint +without requiring to solve constraints (ex: a button). This will allow to solve +the system and then propagate back upward. + + +Hopefully this brief introduction will have clarified how Enaml make use of +kiwi to layout its widgets. Some fine mechanics have been simplified for the +sake of this description but you can check Enaml sources for a more in depth +description. diff --git a/docs/source/use_cases/enaml_hbox.svg b/docs/source/use_cases/enaml_hbox.svg new file mode 100644 index 0000000..c2f205b --- /dev/null +++ b/docs/source/use_cases/enaml_hbox.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Widget 1 + + + + Widget 2 + + + + Widget 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +top margin +bottom margin +left margin +right margin +Container + diff --git a/docs/source/use_cases/index.rst b/docs/source/use_cases/index.rst new file mode 100644 index 0000000..6bde241 --- /dev/null +++ b/docs/source/use_cases/index.rst @@ -0,0 +1,13 @@ +.. _uses: + +Kiwisolver real uses +==================== + +The following sections describe how kiwisolver is used in third-party project, +and aim at providing more involved example of how kiwisolver can be used in +real life project. + +.. toctree:: + :maxdepth: 2 + + enaml.rst diff --git a/kiwi/AssocVector.h b/kiwi/AssocVector.h new file mode 100644 index 0000000..47a6823 --- /dev/null +++ b/kiwi/AssocVector.h @@ -0,0 +1,352 @@ +//////////////////////////////////////////////////////////////////////////////// +// The Loki Library +// Copyright (c) 2001 by Andrei Alexandrescu +// This code accompanies the book: +// Alexandrescu, Andrei. "Modern C++ Design: Generic Programming and Design +// Patterns Applied". Copyright (c) 2001. Addison-Wesley. +// Permission to use, copy, modify, distribute and sell this software for any +// purpose is hereby granted without fee, provided that the above copyright +// notice appear in all copies and that both that copyright notice and this +// permission notice appear in supporting documentation. +// The author or Addison-Wesley Longman make no representations about the +// suitability of this software for any purpose. It is provided "as is" +// without express or implied warranty. +//////////////////////////////////////////////////////////////////////////////// +// Updated 2019 by Matthieu Dartiailh for C++11 compliancy +//////////////////////////////////////////////////////////////////////////////// +#pragma once + +// $Id: AssocVector.h 765 2006-10-18 13:55:32Z syntheticpp $ + + +#include +#include +#include +#include + +namespace Loki +{ +//////////////////////////////////////////////////////////////////////////////// +// class template AssocVectorCompare +// Used by AssocVector +//////////////////////////////////////////////////////////////////////////////// + + namespace Private + { + template + class AssocVectorCompare : public C + { + typedef std::pair + Data; + typedef K first_argument_type; + + public: + AssocVectorCompare() + {} + + AssocVectorCompare(const C& src) : C(src) + {} + + bool operator()(const first_argument_type& lhs, + const first_argument_type& rhs) const + { return C::operator()(lhs, rhs); } + + bool operator()(const Data& lhs, const Data& rhs) const + { return operator()(lhs.first, rhs.first); } + + bool operator()(const Data& lhs, + const first_argument_type& rhs) const + { return operator()(lhs.first, rhs); } + + bool operator()(const first_argument_type& lhs, + const Data& rhs) const + { return operator()(lhs, rhs.first); } + }; + } + +//////////////////////////////////////////////////////////////////////////////// +// class template AssocVector +// An associative vector built as a syntactic drop-in replacement for std::map +// BEWARE: AssocVector doesn't respect all map's guarantees, the most important +// being: +// * iterators are invalidated by insert and erase operations +// * the complexity of insert/erase is O(N) not O(log N) +// * value_type is std::pair not std::pair +// * iterators are random +//////////////////////////////////////////////////////////////////////////////// + + + template + < + class K, + class V, + class C = std::less, + class A = std::allocator< std::pair > + > + class AssocVector + : private std::vector< std::pair, A > + , private Private::AssocVectorCompare + { + typedef std::vector, A> Base; + typedef Private::AssocVectorCompare MyCompare; + + public: + typedef K key_type; + typedef V mapped_type; + typedef typename Base::value_type value_type; + + typedef C key_compare; + typedef A allocator_type; + typedef typename Base::iterator iterator; + typedef typename Base::const_iterator const_iterator; + typedef typename Base::size_type size_type; + typedef typename Base::difference_type difference_type; + typedef typename Base::reverse_iterator reverse_iterator; + typedef typename Base::const_reverse_iterator const_reverse_iterator; + + class value_compare + : public std::function + , private key_compare + { + friend class AssocVector; + + protected: + value_compare(key_compare pred) : key_compare(pred) + {} + + public: + bool operator()(const value_type& lhs, const value_type& rhs) const + { return key_compare::operator()(lhs.first, rhs.first); } + }; + + // 23.3.1.1 construct/copy/destroy + + explicit AssocVector(const key_compare& comp = key_compare(), + const A& alloc = A()) + : Base(alloc), MyCompare(comp) + {} + + template + AssocVector(InputIterator first, InputIterator last, + const key_compare& comp = key_compare(), + const A& alloc = A()) + : Base(first, last, alloc), MyCompare(comp) + { + MyCompare& me = *this; + std::sort(begin(), end(), me); + } + + AssocVector& operator=(const AssocVector& rhs) + { + AssocVector(rhs).swap(*this); + return *this; + } + + // iterators: + // The following are here because MWCW gets 'using' wrong + iterator begin() { return Base::begin(); } + const_iterator begin() const { return Base::begin(); } + iterator end() { return Base::end(); } + const_iterator end() const { return Base::end(); } + reverse_iterator rbegin() { return Base::rbegin(); } + const_reverse_iterator rbegin() const { return Base::rbegin(); } + reverse_iterator rend() { return Base::rend(); } + const_reverse_iterator rend() const { return Base::rend(); } + + // capacity: + bool empty() const { return Base::empty(); } + size_type size() const { return Base::size(); } + size_type max_size() { return Base::max_size(); } + + // 23.3.1.2 element access: + mapped_type& operator[](const key_type& key) + { return insert(value_type(key, mapped_type())).first->second; } + + // modifiers: + std::pair insert(const value_type& val) + { + bool found(true); + iterator i(lower_bound(val.first)); + + if (i == end() || this->operator()(val.first, i->first)) + { + i = Base::insert(i, val); + found = false; + } + return std::make_pair(i, !found); + } + //Section [23.1.2], Table 69 + //http://developer.apple.com/documentation/DeveloperTools/gcc-3.3/libstdc++/23_containers/howto.html#4 + iterator insert(iterator pos, const value_type& val) + { + if( (pos == begin() || this->operator()(*(pos-1),val)) && + (pos == end() || this->operator()(val, *pos)) ) + { + return Base::insert(pos, val); + } + return insert(val).first; + } + + template + void insert(InputIterator first, InputIterator last) + { for (; first != last; ++first) insert(*first); } + + void erase(iterator pos) + { Base::erase(pos); } + + size_type erase(const key_type& k) + { + iterator i(find(k)); + if (i == end()) return 0; + erase(i); + return 1; + } + + void erase(iterator first, iterator last) + { Base::erase(first, last); } + + void swap(AssocVector& other) + { + Base::swap(other); + MyCompare& me = *this; + MyCompare& rhs = other; + std::swap(me, rhs); + } + + void clear() + { Base::clear(); } + + // observers: + key_compare key_comp() const + { return *this; } + + value_compare value_comp() const + { + const key_compare& comp = *this; + return value_compare(comp); + } + + // 23.3.1.3 map operations: + iterator find(const key_type& k) + { + iterator i(lower_bound(k)); + if (i != end() && this->operator()(k, i->first)) + { + i = end(); + } + return i; + } + + const_iterator find(const key_type& k) const + { + const_iterator i(lower_bound(k)); + if (i != end() && this->operator()(k, i->first)) + { + i = end(); + } + return i; + } + + size_type count(const key_type& k) const + { return find(k) != end(); } + + iterator lower_bound(const key_type& k) + { + MyCompare& me = *this; + return std::lower_bound(begin(), end(), k, me); + } + + const_iterator lower_bound(const key_type& k) const + { + const MyCompare& me = *this; + return std::lower_bound(begin(), end(), k, me); + } + + iterator upper_bound(const key_type& k) + { + MyCompare& me = *this; + return std::upper_bound(begin(), end(), k, me); + } + + const_iterator upper_bound(const key_type& k) const + { + const MyCompare& me = *this; + return std::upper_bound(begin(), end(), k, me); + } + + std::pair equal_range(const key_type& k) + { + MyCompare& me = *this; + return std::equal_range(begin(), end(), k, me); + } + + std::pair equal_range( + const key_type& k) const + { + const MyCompare& me = *this; + return std::equal_range(begin(), end(), k, me); + } + + template + friend bool operator==(const AssocVector& lhs, + const AssocVector& rhs); + + bool operator<(const AssocVector& rhs) const + { + const Base& me = *this; + const Base& yo = rhs; + return me < yo; + } + + template + friend bool operator!=(const AssocVector& lhs, + const AssocVector& rhs); + + template + friend bool operator>(const AssocVector& lhs, + const AssocVector& rhs); + + template + friend bool operator>=(const AssocVector& lhs, + const AssocVector& rhs); + + template + friend bool operator<=(const AssocVector& lhs, + const AssocVector& rhs); + }; + + template + inline bool operator==(const AssocVector& lhs, + const AssocVector& rhs) + { + const std::vector, A>& me = lhs; + return me == rhs; + } + + template + inline bool operator!=(const AssocVector& lhs, + const AssocVector& rhs) + { return !(lhs == rhs); } + + template + inline bool operator>(const AssocVector& lhs, + const AssocVector& rhs) + { return rhs < lhs; } + + template + inline bool operator>=(const AssocVector& lhs, + const AssocVector& rhs) + { return !(lhs < rhs); } + + template + inline bool operator<=(const AssocVector& lhs, + const AssocVector& rhs) + { return !(rhs < lhs); } + + + // specialized algorithms: + template + void swap(AssocVector& lhs, AssocVector& rhs) + { lhs.swap(rhs); } + +} // namespace Loki diff --git a/kiwi/constraint.h b/kiwi/constraint.h new file mode 100644 index 0000000..513548a --- /dev/null +++ b/kiwi/constraint.h @@ -0,0 +1,140 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include +#include +#include "expression.h" +#include "shareddata.h" +#include "strength.h" +#include "term.h" +#include "variable.h" +#include "util.h" + +namespace kiwi +{ + +enum RelationalOperator +{ + OP_LE, + OP_GE, + OP_EQ +}; + +class Constraint +{ + +public: + Constraint() = default; + + Constraint(const Expression &expr, + RelationalOperator op, + double strength = strength::required) : m_data(new ConstraintData(expr, op, strength)) {} + + Constraint(const Constraint &other, double strength) : m_data(new ConstraintData(other, strength)) {} + + Constraint(const Constraint &) = default; + + Constraint(Constraint &&) noexcept = default; + + ~Constraint() = default; + + const Expression &expression() const + { + return m_data->m_expression; + } + + RelationalOperator op() const + { + return m_data->m_op; + } + + double strength() const + { + return m_data->m_strength; + } + + bool violated() const + { + switch (m_data->m_op) + { + case OP_EQ: return !impl::nearZero(m_data->m_expression.value()); + case OP_GE: return m_data->m_expression.value() < 0.0; + case OP_LE: return m_data->m_expression.value() > 0.0; + } + + std::abort(); + } + + bool operator!() const + { + return !m_data; + } + + Constraint& operator=(const Constraint &) = default; + + Constraint& operator=(Constraint &&) noexcept = default; + +private: + static Expression reduce(const Expression &expr) + { + std::map vars; + for (const auto & term : expr.terms()) + vars[term.variable()] += term.coefficient(); + + std::vector terms(vars.begin(), vars.end()); + return Expression(std::move(terms), expr.constant()); + } + + class ConstraintData : public SharedData + { + + public: + ConstraintData(const Expression &expr, + RelationalOperator op, + double strength) : SharedData(), + m_expression(reduce(expr)), + m_strength(strength::clip(strength)), + m_op(op) {} + + ConstraintData(const Constraint &other, double strength) : SharedData(), + m_expression(other.expression()), + m_strength(strength::clip(strength)), + m_op(other.op()) {} + + ~ConstraintData() = default; + + Expression m_expression; + double m_strength; + RelationalOperator m_op; + + private: + ConstraintData(const ConstraintData &other); + + ConstraintData &operator=(const ConstraintData &other); + }; + + SharedDataPtr m_data; + + friend bool operator<(const Constraint &lhs, const Constraint &rhs) + { + return lhs.m_data < rhs.m_data; + } + + friend bool operator==(const Constraint &lhs, const Constraint &rhs) + { + return lhs.m_data == rhs.m_data; + } + + friend bool operator!=(const Constraint &lhs, const Constraint &rhs) + { + return lhs.m_data != rhs.m_data; + } +}; + +} // namespace kiwi diff --git a/kiwi/debug.h b/kiwi/debug.h new file mode 100644 index 0000000..f2b29b4 --- /dev/null +++ b/kiwi/debug.h @@ -0,0 +1,184 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include +#include +#include "constraint.h" +#include "solverimpl.h" +#include "term.h" + +namespace kiwi +{ + +namespace impl +{ + +class DebugHelper +{ + +public: + static void dump(const SolverImpl &solver, std::ostream &out) + { + out << "Objective" << std::endl; + out << "---------" << std::endl; + dump(*solver.m_objective, out); + out << std::endl; + out << "Tableau" << std::endl; + out << "-------" << std::endl; + dump(solver.m_rows, out); + out << std::endl; + out << "Infeasible" << std::endl; + out << "----------" << std::endl; + dump(solver.m_infeasible_rows, out); + out << std::endl; + out << "Variables" << std::endl; + out << "---------" << std::endl; + dump(solver.m_vars, out); + out << std::endl; + out << "Edit Variables" << std::endl; + out << "--------------" << std::endl; + dump(solver.m_edits, out); + out << std::endl; + out << "Constraints" << std::endl; + out << "-----------" << std::endl; + dump(solver.m_cns, out); + out << std::endl; + out << std::endl; + } + + static void dump(const SolverImpl::RowMap &rows, std::ostream &out) + { + for (const auto &rowPair : rows) + { + dump(rowPair.first, out); + out << " | "; + dump(*rowPair.second, out); + } + } + + static void dump(const std::vector &symbols, std::ostream &out) + { + for (const auto &symbol : symbols) + { + dump(symbol, out); + out << std::endl; + } + } + + static void dump(const SolverImpl::VarMap &vars, std::ostream &out) + { + for (const auto &varPair : vars) + { + out << varPair.first.name() << " = "; + dump(varPair.second, out); + out << std::endl; + } + } + + static void dump(const SolverImpl::CnMap &cns, std::ostream &out) + { + for (const auto &cnPair : cns) + dump(cnPair.first, out); + } + + static void dump(const SolverImpl::EditMap &edits, std::ostream &out) + { + for (const auto &editPair : edits) + out << editPair.first.name() << std::endl; + } + + static void dump(const Row &row, std::ostream &out) + { + for (const auto &rowPair : row.cells()) + { + out << " + " << rowPair.second << " * "; + dump(rowPair.first, out); + } + out << std::endl; + } + + static void dump(const Symbol &symbol, std::ostream &out) + { + switch (symbol.type()) + { + case Symbol::Invalid: + out << "i"; + break; + case Symbol::External: + out << "v"; + break; + case Symbol::Slack: + out << "s"; + break; + case Symbol::Error: + out << "e"; + break; + case Symbol::Dummy: + out << "d"; + break; + default: + break; + } + out << symbol.id(); + } + + static void dump(const Constraint &cn, std::ostream &out) + { + for (const auto &term : cn.expression().terms()) + { + out << term.coefficient() << " * "; + out << term.variable().name() << " + "; + } + out << cn.expression().constant(); + switch (cn.op()) + { + case OP_LE: + out << " <= 0 "; + break; + case OP_GE: + out << " >= 0 "; + break; + case OP_EQ: + out << " == 0 "; + break; + default: + break; + } + out << " | strength = " << cn.strength() << std::endl; + } +}; + +} // namespace impl + +namespace debug +{ + +template +void dump(const T &value) +{ + impl::DebugHelper::dump(value, std::cout); +} + +template +void dump(const T &value, std::ostream &out) +{ + impl::DebugHelper::dump(value, out); +} + +template +std::string dumps(const T &value) +{ + std::stringstream stream; + impl::DebugHelper::dump(value, stream); + return stream.str(); +} + +} // namespace debug + +} // namespace kiwi diff --git a/kiwi/errors.h b/kiwi/errors.h new file mode 100644 index 0000000..fd3919f --- /dev/null +++ b/kiwi/errors.h @@ -0,0 +1,162 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include +#include "constraint.h" +#include "variable.h" + +namespace kiwi +{ + +class UnsatisfiableConstraint : public std::exception +{ + +public: + UnsatisfiableConstraint(Constraint constraint) : m_constraint(std::move(constraint)) {} + + ~UnsatisfiableConstraint() noexcept {} + + const char *what() const noexcept + { + return "The constraint can not be satisfied."; + } + + const Constraint &constraint() const + { + return m_constraint; + } + +private: + Constraint m_constraint; +}; + +class UnknownConstraint : public std::exception +{ + +public: + UnknownConstraint(Constraint constraint) : m_constraint(std::move(constraint)) {} + + ~UnknownConstraint() noexcept {} + + const char *what() const noexcept + { + return "The constraint has not been added to the solver."; + } + + const Constraint &constraint() const + { + return m_constraint; + } + +private: + Constraint m_constraint; +}; + +class DuplicateConstraint : public std::exception +{ + +public: + DuplicateConstraint(Constraint constraint) : m_constraint(std::move(constraint)) {} + + ~DuplicateConstraint() noexcept {} + + const char *what() const noexcept + { + return "The constraint has already been added to the solver."; + } + + const Constraint &constraint() const + { + return m_constraint; + } + +private: + Constraint m_constraint; +}; + +class UnknownEditVariable : public std::exception +{ + +public: + UnknownEditVariable(Variable variable) : m_variable(std::move(variable)) {} + + ~UnknownEditVariable() noexcept {} + + const char *what() const noexcept + { + return "The edit variable has not been added to the solver."; + } + + const Variable &variable() const + { + return m_variable; + } + +private: + Variable m_variable; +}; + +class DuplicateEditVariable : public std::exception +{ + +public: + DuplicateEditVariable(Variable variable) : m_variable(std::move(variable)) {} + + ~DuplicateEditVariable() noexcept {} + + const char *what() const noexcept + { + return "The edit variable has already been added to the solver."; + } + + const Variable &variable() const + { + return m_variable; + } + +private: + Variable m_variable; +}; + +class BadRequiredStrength : public std::exception +{ + +public: + BadRequiredStrength() {} + + ~BadRequiredStrength() noexcept {} + + const char *what() const noexcept + { + return "A required strength cannot be used in this context."; + } +}; + +class InternalSolverError : public std::exception +{ + +public: + InternalSolverError() : m_msg("An internal solver error ocurred.") {} + + InternalSolverError(const char *msg) : m_msg(msg) {} + + InternalSolverError(std::string msg) : m_msg(std::move(msg)) {} + + ~InternalSolverError() noexcept {} + + const char *what() const noexcept + { + return m_msg.c_str(); + } + +private: + std::string m_msg; +}; + +} // namespace kiwi diff --git a/kiwi/expression.h b/kiwi/expression.h new file mode 100644 index 0000000..20c8fd3 --- /dev/null +++ b/kiwi/expression.h @@ -0,0 +1,62 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include "term.h" + +namespace kiwi +{ + +class Expression +{ + +public: + Expression(double constant = 0.0) : m_constant(constant) {} + + Expression(const Term &term, double constant = 0.0) : m_terms(1, term), m_constant(constant) {} + + Expression(std::vector terms, double constant = 0.0) : m_terms(std::move(terms)), m_constant(constant) {} + + Expression(const Expression&) = default; + + // Could be marked noexcept but for a bug in the GCC of the manylinux1 image + Expression(Expression&&) = default; + + ~Expression() = default; + + const std::vector &terms() const + { + return m_terms; + } + + double constant() const + { + return m_constant; + } + + double value() const + { + double result = m_constant; + + for (const Term &term : m_terms) + result += term.value(); + + return result; + } + + Expression& operator=(const Expression&) = default; + + // Could be marked noexcept but for a bug in the GCC of the manylinux1 image + Expression& operator=(Expression&&) = default; + +private: + std::vector m_terms; + double m_constant; +}; + +} // namespace kiwi diff --git a/kiwi/kiwi.h b/kiwi/kiwi.h new file mode 100644 index 0000000..77bb6a8 --- /dev/null +++ b/kiwi/kiwi.h @@ -0,0 +1,19 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include "constraint.h" +#include "debug.h" +#include "errors.h" +#include "expression.h" +#include "shareddata.h" +#include "solver.h" +#include "strength.h" +#include "symbolics.h" +#include "term.h" +#include "variable.h" +#include "version.h" diff --git a/kiwi/maptype.h b/kiwi/maptype.h new file mode 100644 index 0000000..9b19a21 --- /dev/null +++ b/kiwi/maptype.h @@ -0,0 +1,37 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include +#include +#include +#include "AssocVector.h" + +namespace kiwi +{ + +namespace impl +{ + +template < + typename K, + typename V, + typename C = std::less, + typename A = std::allocator>> +using MapType = Loki::AssocVector; + +// template< +// typename K, +// typename V, +// typename C = std::less, +// typename A = std::allocator< std::pair > > +// using MapType = std::map; + +} // namespace impl + +} // namespace kiwi diff --git a/kiwi/row.h b/kiwi/row.h new file mode 100644 index 0000000..b7da02f --- /dev/null +++ b/kiwi/row.h @@ -0,0 +1,182 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include "maptype.h" +#include "symbol.h" +#include "util.h" + +namespace kiwi +{ + +namespace impl +{ + +class Row +{ + +public: + using CellMap = MapType; + + Row() : Row(0.0) {} + + Row(double constant) : m_constant(constant) {} + + Row(const Row &other) = default; + + ~Row() = default; + + const CellMap &cells() const + { + return m_cells; + } + + double constant() const + { + return m_constant; + } + + /* Add a constant value to the row constant. + + The new value of the constant is returned. + + */ + double add(double value) + { + return m_constant += value; + } + + /* Insert a symbol into the row with a given coefficient. + + If the symbol already exists in the row, the coefficient will be + added to the existing coefficient. If the resulting coefficient + is zero, the symbol will be removed from the row. + + */ + void insert(const Symbol &symbol, double coefficient = 1.0) + { + if (nearZero(m_cells[symbol] += coefficient)) + m_cells.erase(symbol); + } + + /* Insert a row into this row with a given coefficient. + + The constant and the cells of the other row will be multiplied by + the coefficient and added to this row. Any cell with a resulting + coefficient of zero will be removed from the row. + + */ + void insert(const Row &other, double coefficient = 1.0) + { + m_constant += other.m_constant * coefficient; + + for (const auto & cellPair : other.m_cells) + { + double coeff = cellPair.second * coefficient; + if (nearZero(m_cells[cellPair.first] += coeff)) + m_cells.erase(cellPair.first); + } + } + + /* Remove the given symbol from the row. + + */ + void remove(const Symbol &symbol) + { + auto it = m_cells.find(symbol); + if (it != m_cells.end()) + m_cells.erase(it); + } + + /* Reverse the sign of the constant and all cells in the row. + + */ + void reverseSign() + { + m_constant = -m_constant; + for (auto &cellPair : m_cells) + cellPair.second = -cellPair.second; + } + + /* Solve the row for the given symbol. + + This method assumes the row is of the form a * x + b * y + c = 0 + and (assuming solve for x) will modify the row to represent the + right hand side of x = -b/a * y - c / a. The target symbol will + be removed from the row, and the constant and other cells will + be multiplied by the negative inverse of the target coefficient. + + The given symbol *must* exist in the row. + + */ + void solveFor(const Symbol &symbol) + { + double coeff = -1.0 / m_cells[symbol]; + m_cells.erase(symbol); + m_constant *= coeff; + for (auto &cellPair : m_cells) + cellPair.second *= coeff; + } + + /* Solve the row for the given symbols. + + This method assumes the row is of the form x = b * y + c and will + solve the row such that y = x / b - c / b. The rhs symbol will be + removed from the row, the lhs added, and the result divided by the + negative inverse of the rhs coefficient. + + The lhs symbol *must not* exist in the row, and the rhs symbol + *must* exist in the row. + + */ + void solveFor(const Symbol &lhs, const Symbol &rhs) + { + insert(lhs, -1.0); + solveFor(rhs); + } + + /* Get the coefficient for the given symbol. + + If the symbol does not exist in the row, zero will be returned. + + */ + double coefficientFor(const Symbol &symbol) const + { + CellMap::const_iterator it = m_cells.find(symbol); + if (it == m_cells.end()) + return 0.0; + return it->second; + } + + /* Substitute a symbol with the data from another row. + + Given a row of the form a * x + b and a substitution of the + form x = 3 * y + c the row will be updated to reflect the + expression 3 * a * y + a * c + b. + + If the symbol does not exist in the row, this is a no-op. + + */ + void substitute(const Symbol &symbol, const Row &row) + { + auto it = m_cells.find(symbol); + if (it != m_cells.end()) + { + double coefficient = it->second; + m_cells.erase(it); + insert(row, coefficient); + } + } + +private: + CellMap m_cells; + double m_constant; +}; + +} // namespace impl + +} // namespace kiwi diff --git a/kiwi/shareddata.h b/kiwi/shareddata.h new file mode 100644 index 0000000..0160cf1 --- /dev/null +++ b/kiwi/shareddata.h @@ -0,0 +1,181 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once + +/* +Implementation note +=================== +SharedDataPtr/SharedData offer the same basic functionality as std::shared_ptr, +but do not use atomic counters under the hood. +Since kiwi operates within a single thread context, atomic counters are not necessary, +especially given the extra CPU cost. +Therefore the use of SharedDataPtr/SharedData is preferred over std::shared_ptr. +*/ + +namespace kiwi +{ + +class SharedData +{ + +public: + SharedData() : m_refcount(0) {} + + SharedData(const SharedData &other) = delete; + + SharedData(SharedData&& other) = delete; + + int m_refcount; + + SharedData &operator=(const SharedData &other) = delete; + + SharedData &operator=(SharedData&& other) = delete; +}; + +template +class SharedDataPtr +{ + +public: + using Type = T; + + SharedDataPtr() : m_data(nullptr) {} + + explicit SharedDataPtr(T *data) : m_data(data) + { + incref(m_data); + } + + ~SharedDataPtr() + { + decref(m_data); + } + + T *data() + { + return m_data; + } + + const T *data() const + { + return m_data; + } + + operator T *() + { + return m_data; + } + + operator const T *() const + { + return m_data; + } + + T *operator->() + { + return m_data; + } + + const T *operator->() const + { + return m_data; + } + + T &operator*() + { + return *m_data; + } + + const T &operator*() const + { + return *m_data; + } + + bool operator!() const + { + return !m_data; + } + + bool operator<(const SharedDataPtr &other) const + { + return m_data < other.m_data; + } + + bool operator==(const SharedDataPtr &other) const + { + return m_data == other.m_data; + } + + bool operator!=(const SharedDataPtr &other) const + { + return m_data != other.m_data; + } + + SharedDataPtr(const SharedDataPtr &other) : m_data(other.m_data) + { + incref(m_data); + } + + SharedDataPtr(SharedDataPtr&& other) noexcept : m_data(other.m_data) + { + other.m_data = nullptr; + } + + SharedDataPtr &operator=(const SharedDataPtr &other) + { + if (m_data != other.m_data) + { + T *temp = m_data; + m_data = other.m_data; + incref(m_data); + decref(temp); + } + return *this; + } + + SharedDataPtr& operator=(SharedDataPtr&& other) noexcept + { + if (m_data != other.m_data) + { + T *temp = m_data; + m_data = other.m_data; + other.m_data = nullptr; + decref(temp); + } + return *this; + } + + SharedDataPtr &operator=(T *other) + { + if (m_data != other) + { + T *temp = m_data; + m_data = other; + incref(m_data); + decref(temp); + } + return *this; + } + +private: + static void incref(T *data) + { + if (data) + ++data->m_refcount; + } + + static void decref(T *data) + { + if (data && --data->m_refcount == 0) + delete data; + } + + T *m_data; +}; + +} // namespace kiwi diff --git a/kiwi/solver.h b/kiwi/solver.h new file mode 100644 index 0000000..8ff2dbb --- /dev/null +++ b/kiwi/solver.h @@ -0,0 +1,178 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include "constraint.h" +#include "debug.h" +#include "solverimpl.h" +#include "strength.h" +#include "variable.h" + + +namespace kiwi +{ + +class Solver +{ + +public: + + Solver() = default; + + ~Solver() = default; + + /* Add a constraint to the solver. + + Throws + ------ + DuplicateConstraint + The given constraint has already been added to the solver. + + UnsatisfiableConstraint + The given constraint is required and cannot be satisfied. + + */ + void addConstraint( const Constraint& constraint ) + { + m_impl.addConstraint( constraint ); + } + + /* Remove a constraint from the solver. + + Throws + ------ + UnknownConstraint + The given constraint has not been added to the solver. + + */ + void removeConstraint( const Constraint& constraint ) + { + m_impl.removeConstraint( constraint ); + } + + /* Test whether a constraint has been added to the solver. + + */ + bool hasConstraint( const Constraint& constraint ) const + { + return m_impl.hasConstraint( constraint ); + } + + /* Add an edit variable to the solver. + + This method should be called before the `suggestValue` method is + used to supply a suggested value for the given edit variable. + + Throws + ------ + DuplicateEditVariable + The given edit variable has already been added to the solver. + + BadRequiredStrength + The given strength is >= required. + + */ + void addEditVariable( const Variable& variable, double strength ) + { + m_impl.addEditVariable( variable, strength ); + } + + /* Remove an edit variable from the solver. + + Throws + ------ + UnknownEditVariable + The given edit variable has not been added to the solver. + + */ + void removeEditVariable( const Variable& variable ) + { + m_impl.removeEditVariable( variable ); + } + + /* Test whether an edit variable has been added to the solver. + + */ + bool hasEditVariable( const Variable& variable ) const + { + return m_impl.hasEditVariable( variable ); + } + + /* Suggest a value for the given edit variable. + + This method should be used after an edit variable as been added to + the solver in order to suggest the value for that variable. After + all suggestions have been made, the `solve` method can be used to + update the values of all variables. + + Throws + ------ + UnknownEditVariable + The given edit variable has not been added to the solver. + + */ + void suggestValue( const Variable& variable, double value ) + { + m_impl.suggestValue( variable, value ); + } + + /* Update the values of the external solver variables. + + */ + void updateVariables() + { + m_impl.updateVariables(); + } + + /* Reset the solver to the empty starting condition. + + This method resets the internal solver state to the empty starting + condition, as if no constraints or edit variables have been added. + This can be faster than deleting the solver and creating a new one + when the entire system must change, since it can avoid unecessary + heap (de)allocations. + + */ + void reset() + { + m_impl.reset(); + } + + /* Dump a representation of the solver internals to stdout. + + */ + void dump() + { + debug::dump( m_impl ); + } + + /* Dump a representation of the solver internals to a stream. + + */ + void dump( std::ostream& out ) + { + debug::dump( m_impl, out ); + } + + /* Dump a representation of the solver internals to a string. + + */ + std::string dumps() + { + return debug::dumps( m_impl ); + } + +private: + + Solver( const Solver& ); + + Solver& operator=( const Solver& ); + + impl::SolverImpl m_impl; +}; + +} // namespace kiwi diff --git a/kiwi/solverimpl.h b/kiwi/solverimpl.h new file mode 100644 index 0000000..c1960fc --- /dev/null +++ b/kiwi/solverimpl.h @@ -0,0 +1,825 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include +#include +#include +#include "constraint.h" +#include "errors.h" +#include "expression.h" +#include "maptype.h" +#include "row.h" +#include "symbol.h" +#include "term.h" +#include "util.h" +#include "variable.h" + + +namespace kiwi +{ + +namespace impl +{ + +class SolverImpl +{ + friend class DebugHelper; + + struct Tag + { + Symbol marker; + Symbol other; + }; + + struct EditInfo + { + Tag tag; + Constraint constraint; + double constant; + }; + + using VarMap = MapType; + + using RowMap = MapType; + + using CnMap = MapType; + + using EditMap = MapType; + + struct DualOptimizeGuard + { + DualOptimizeGuard( SolverImpl& impl ) : m_impl( impl ) {} + ~DualOptimizeGuard() { m_impl.dualOptimize(); } + SolverImpl& m_impl; + }; + +public: + + SolverImpl() : m_objective( new Row() ), m_id_tick( 1 ) {} + + SolverImpl( const SolverImpl& ) = delete; + + SolverImpl( SolverImpl&& ) = delete; + + ~SolverImpl() { clearRows(); } + + /* Add a constraint to the solver. + + Throws + ------ + DuplicateConstraint + The given constraint has already been added to the solver. + + UnsatisfiableConstraint + The given constraint is required and cannot be satisfied. + + */ + void addConstraint( const Constraint& constraint ) + { + if( m_cns.find( constraint ) != m_cns.end() ) + throw DuplicateConstraint( constraint ); + + // Creating a row causes symbols to be reserved for the variables + // in the constraint. If this method exits with an exception, + // then its possible those variables will linger in the var map. + // Since its likely that those variables will be used in other + // constraints and since exceptional conditions are uncommon, + // i'm not too worried about aggressive cleanup of the var map. + Tag tag; + std::unique_ptr rowptr( createRow( constraint, tag ) ); + Symbol subject( chooseSubject( *rowptr, tag ) ); + + // If chooseSubject could not find a valid entering symbol, one + // last option is available if the entire row is composed of + // dummy variables. If the constant of the row is zero, then + // this represents redundant constraints and the new dummy + // marker can enter the basis. If the constant is non-zero, + // then it represents an unsatisfiable constraint. + if( subject.type() == Symbol::Invalid && allDummies( *rowptr ) ) + { + if( !nearZero( rowptr->constant() ) ) + throw UnsatisfiableConstraint( constraint ); + else + subject = tag.marker; + } + + // If an entering symbol still isn't found, then the row must + // be added using an artificial variable. If that fails, then + // the row represents an unsatisfiable constraint. + if( subject.type() == Symbol::Invalid ) + { + if( !addWithArtificialVariable( *rowptr ) ) + throw UnsatisfiableConstraint( constraint ); + } + else + { + rowptr->solveFor( subject ); + substitute( subject, *rowptr ); + m_rows[ subject ] = rowptr.release(); + } + + m_cns[ constraint ] = tag; + + // Optimizing after each constraint is added performs less + // aggregate work due to a smaller average system size. It + // also ensures the solver remains in a consistent state. + optimize( *m_objective ); + } + + /* Remove a constraint from the solver. + + Throws + ------ + UnknownConstraint + The given constraint has not been added to the solver. + + */ + void removeConstraint( const Constraint& constraint ) + { + auto cn_it = m_cns.find( constraint ); + if( cn_it == m_cns.end() ) + throw UnknownConstraint( constraint ); + + Tag tag( cn_it->second ); + m_cns.erase( cn_it ); + + // Remove the error effects from the objective function + // *before* pivoting, or substitutions into the objective + // will lead to incorrect solver results. + removeConstraintEffects( constraint, tag ); + + // If the marker is basic, simply drop the row. Otherwise, + // pivot the marker into the basis and then drop the row. + auto row_it = m_rows.find( tag.marker ); + if( row_it != m_rows.end() ) + { + std::unique_ptr rowptr( row_it->second ); + m_rows.erase( row_it ); + } + else + { + row_it = getMarkerLeavingRow( tag.marker ); + if( row_it == m_rows.end() ) + throw InternalSolverError( "failed to find leaving row" ); + Symbol leaving( row_it->first ); + std::unique_ptr rowptr( row_it->second ); + m_rows.erase( row_it ); + rowptr->solveFor( leaving, tag.marker ); + substitute( tag.marker, *rowptr ); + } + + // Optimizing after each constraint is removed ensures that the + // solver remains consistent. It makes the solver api easier to + // use at a small tradeoff for speed. + optimize( *m_objective ); + } + + /* Test whether a constraint has been added to the solver. + + */ + bool hasConstraint( const Constraint& constraint ) const + { + return m_cns.find( constraint ) != m_cns.end(); + } + + /* Add an edit variable to the solver. + + This method should be called before the `suggestValue` method is + used to supply a suggested value for the given edit variable. + + Throws + ------ + DuplicateEditVariable + The given edit variable has already been added to the solver. + + BadRequiredStrength + The given strength is >= required. + + */ + void addEditVariable( const Variable& variable, double strength ) + { + if( m_edits.find( variable ) != m_edits.end() ) + throw DuplicateEditVariable( variable ); + strength = strength::clip( strength ); + if( strength == strength::required ) + throw BadRequiredStrength(); + Constraint cn( Expression( variable ), OP_EQ, strength ); + addConstraint( cn ); + EditInfo info; + info.tag = m_cns[ cn ]; + info.constraint = cn; + info.constant = 0.0; + m_edits[ variable ] = info; + } + + /* Remove an edit variable from the solver. + + Throws + ------ + UnknownEditVariable + The given edit variable has not been added to the solver. + + */ + void removeEditVariable( const Variable& variable ) + { + auto it = m_edits.find( variable ); + if( it == m_edits.end() ) + throw UnknownEditVariable( variable ); + removeConstraint( it->second.constraint ); + m_edits.erase( it ); + } + + /* Test whether an edit variable has been added to the solver. + + */ + bool hasEditVariable( const Variable& variable ) const + { + return m_edits.find( variable ) != m_edits.end(); + } + + /* Suggest a value for the given edit variable. + + This method should be used after an edit variable as been added to + the solver in order to suggest the value for that variable. + + Throws + ------ + UnknownEditVariable + The given edit variable has not been added to the solver. + + */ + void suggestValue( const Variable& variable, double value ) + { + auto it = m_edits.find( variable ); + if( it == m_edits.end() ) + throw UnknownEditVariable( variable ); + + DualOptimizeGuard guard( *this ); + EditInfo& info = it->second; + double delta = value - info.constant; + info.constant = value; + + // Check first if the positive error variable is basic. + auto row_it = m_rows.find( info.tag.marker ); + if( row_it != m_rows.end() ) + { + if( row_it->second->add( -delta ) < 0.0 ) + m_infeasible_rows.push_back( row_it->first ); + return; + } + + // Check next if the negative error variable is basic. + row_it = m_rows.find( info.tag.other ); + if( row_it != m_rows.end() ) + { + if( row_it->second->add( delta ) < 0.0 ) + m_infeasible_rows.push_back( row_it->first ); + return; + } + + // Otherwise update each row where the error variables exist. + for (const auto & rowPair : m_rows) + { + double coeff = rowPair.second->coefficientFor( info.tag.marker ); + if( coeff != 0.0 && + rowPair.second->add( delta * coeff ) < 0.0 && + rowPair.first.type() != Symbol::External ) + m_infeasible_rows.push_back( rowPair.first ); + } + } + + /* Update the values of the external solver variables. + + */ + void updateVariables() + { + auto row_end = m_rows.end(); + + for (auto &varPair : m_vars) + { + Variable& var = varPair.first; + auto row_it = m_rows.find( varPair.second ); + if( row_it == row_end ) + var.setValue( 0.0 ); + else + var.setValue( row_it->second->constant() ); + } + } + + /* Reset the solver to the empty starting condition. + + This method resets the internal solver state to the empty starting + condition, as if no constraints or edit variables have been added. + This can be faster than deleting the solver and creating a new one + when the entire system must change, since it can avoid unecessary + heap (de)allocations. + + */ + void reset() + { + clearRows(); + m_cns.clear(); + m_vars.clear(); + m_edits.clear(); + m_infeasible_rows.clear(); + m_objective.reset( new Row() ); + m_artificial.reset(); + m_id_tick = 1; + } + + SolverImpl& operator=( const SolverImpl& ) = delete; + + SolverImpl& operator=( SolverImpl&& ) = delete; + +private: + + struct RowDeleter + { + template + void operator()( T& pair ) { delete pair.second; } + }; + + void clearRows() + { + std::for_each( m_rows.begin(), m_rows.end(), RowDeleter() ); + m_rows.clear(); + } + + /* Get the symbol for the given variable. + + If a symbol does not exist for the variable, one will be created. + + */ + Symbol getVarSymbol( const Variable& variable ) + { + auto it = m_vars.find( variable ); + if( it != m_vars.end() ) + return it->second; + Symbol symbol( Symbol::External, m_id_tick++ ); + m_vars[ variable ] = symbol; + return symbol; + } + + /* Create a new Row object for the given constraint. + + The terms in the constraint will be converted to cells in the row. + Any term in the constraint with a coefficient of zero is ignored. + This method uses the `getVarSymbol` method to get the symbol for + the variables added to the row. If the symbol for a given cell + variable is basic, the cell variable will be substituted with the + basic row. + + The necessary slack and error variables will be added to the row. + If the constant for the row is negative, the sign for the row + will be inverted so the constant becomes positive. + + The tag will be updated with the marker and error symbols to use + for tracking the movement of the constraint in the tableau. + + */ + std::unique_ptr createRow( const Constraint& constraint, Tag& tag ) + { + const Expression& expr( constraint.expression() ); + std::unique_ptr row( new Row( expr.constant() ) ); + + // Substitute the current basic variables into the row. + for (const auto &term : expr.terms()) + { + if( !nearZero( term.coefficient() ) ) + { + Symbol symbol( getVarSymbol( term.variable() ) ); + auto row_it = m_rows.find( symbol ); + if( row_it != m_rows.end() ) + row->insert( *row_it->second, term.coefficient() ); + else + row->insert( symbol, term.coefficient() ); + } + } + + // Add the necessary slack, error, and dummy variables. + switch( constraint.op() ) + { + case OP_LE: + case OP_GE: + { + double coeff = constraint.op() == OP_LE ? 1.0 : -1.0; + Symbol slack( Symbol::Slack, m_id_tick++ ); + tag.marker = slack; + row->insert( slack, coeff ); + if( constraint.strength() < strength::required ) + { + Symbol error( Symbol::Error, m_id_tick++ ); + tag.other = error; + row->insert( error, -coeff ); + m_objective->insert( error, constraint.strength() ); + } + break; + } + case OP_EQ: + { + if( constraint.strength() < strength::required ) + { + Symbol errplus( Symbol::Error, m_id_tick++ ); + Symbol errminus( Symbol::Error, m_id_tick++ ); + tag.marker = errplus; + tag.other = errminus; + row->insert( errplus, -1.0 ); // v = eplus - eminus + row->insert( errminus, 1.0 ); // v - eplus + eminus = 0 + m_objective->insert( errplus, constraint.strength() ); + m_objective->insert( errminus, constraint.strength() ); + } + else + { + Symbol dummy( Symbol::Dummy, m_id_tick++ ); + tag.marker = dummy; + row->insert( dummy ); + } + break; + } + } + + // Ensure the row as a positive constant. + if( row->constant() < 0.0 ) + row->reverseSign(); + + return row; + } + + /* Choose the subject for solving for the row. + + This method will choose the best subject for using as the solve + target for the row. An invalid symbol will be returned if there + is no valid target. + + The symbols are chosen according to the following precedence: + + 1) The first symbol representing an external variable. + 2) A negative slack or error tag variable. + + If a subject cannot be found, an invalid symbol will be returned. + + */ + Symbol chooseSubject( const Row& row, const Tag& tag ) const + { + for (const auto &cellPair : row.cells()) + { + if( cellPair.first.type() == Symbol::External ) + return cellPair.first; + } + if( tag.marker.type() == Symbol::Slack || tag.marker.type() == Symbol::Error ) + { + if( row.coefficientFor( tag.marker ) < 0.0 ) + return tag.marker; + } + if( tag.other.type() == Symbol::Slack || tag.other.type() == Symbol::Error ) + { + if( row.coefficientFor( tag.other ) < 0.0 ) + return tag.other; + } + return Symbol(); + } + + /* Add the row to the tableau using an artificial variable. + + This will return false if the constraint cannot be satisfied. + + */ + bool addWithArtificialVariable( const Row& row ) + { + // Create and add the artificial variable to the tableau + Symbol art( Symbol::Slack, m_id_tick++ ); + m_rows[ art ] = new Row( row ); + m_artificial.reset( new Row( row ) ); + + // Optimize the artificial objective. This is successful + // only if the artificial objective is optimized to zero. + optimize( *m_artificial ); + bool success = nearZero( m_artificial->constant() ); + m_artificial.reset(); + + // If the artificial variable is not basic, pivot the row so that + // it becomes basic. If the row is constant, exit early. + auto it = m_rows.find( art ); + if( it != m_rows.end() ) + { + std::unique_ptr rowptr( it->second ); + m_rows.erase( it ); + if( rowptr->cells().empty() ) + return success; + Symbol entering( anyPivotableSymbol( *rowptr ) ); + if( entering.type() == Symbol::Invalid ) + return false; // unsatisfiable (will this ever happen?) + rowptr->solveFor( art, entering ); + substitute( entering, *rowptr ); + m_rows[ entering ] = rowptr.release(); + } + + // Remove the artificial variable from the tableau. + for (auto &rowPair : m_rows) + rowPair.second->remove(art); + + m_objective->remove( art ); + return success; + } + + /* Substitute the parametric symbol with the given row. + + This method will substitute all instances of the parametric symbol + in the tableau and the objective function with the given row. + + */ + void substitute( const Symbol& symbol, const Row& row ) + { + for( auto& rowPair : m_rows ) + { + rowPair.second->substitute( symbol, row ); + if( rowPair.first.type() != Symbol::External && + rowPair.second->constant() < 0.0 ) + m_infeasible_rows.push_back( rowPair.first ); + } + m_objective->substitute( symbol, row ); + if( m_artificial.get() ) + m_artificial->substitute( symbol, row ); + } + + /* Optimize the system for the given objective function. + + This method performs iterations of Phase 2 of the simplex method + until the objective function reaches a minimum. + + Throws + ------ + InternalSolverError + The value of the objective function is unbounded. + + */ + void optimize( const Row& objective ) + { + while( true ) + { + Symbol entering( getEnteringSymbol( objective ) ); + if( entering.type() == Symbol::Invalid ) + return; + auto it = getLeavingRow( entering ); + if( it == m_rows.end() ) + throw InternalSolverError( "The objective is unbounded." ); + // pivot the entering symbol into the basis + Symbol leaving( it->first ); + Row* row = it->second; + m_rows.erase( it ); + row->solveFor( leaving, entering ); + substitute( entering, *row ); + m_rows[ entering ] = row; + } + } + + /* Optimize the system using the dual of the simplex method. + + The current state of the system should be such that the objective + function is optimal, but not feasible. This method will perform + an iteration of the dual simplex method to make the solution both + optimal and feasible. + + Throws + ------ + InternalSolverError + The system cannot be dual optimized. + + */ + void dualOptimize() + { + while( !m_infeasible_rows.empty() ) + { + + Symbol leaving( m_infeasible_rows.back() ); + m_infeasible_rows.pop_back(); + auto it = m_rows.find( leaving ); + if( it != m_rows.end() && !nearZero( it->second->constant() ) && + it->second->constant() < 0.0 ) + { + Symbol entering( getDualEnteringSymbol( *it->second ) ); + if( entering.type() == Symbol::Invalid ) + throw InternalSolverError( "Dual optimize failed." ); + // pivot the entering symbol into the basis + Row* row = it->second; + m_rows.erase( it ); + row->solveFor( leaving, entering ); + substitute( entering, *row ); + m_rows[ entering ] = row; + } + } + } + + /* Compute the entering variable for a pivot operation. + + This method will return first symbol in the objective function which + is non-dummy and has a coefficient less than zero. If no symbol meets + the criteria, it means the objective function is at a minimum, and an + invalid symbol is returned. + + */ + Symbol getEnteringSymbol( const Row& objective ) const + { + for (const auto &cellPair : objective.cells()) + { + if( cellPair.first.type() != Symbol::Dummy && cellPair.second < 0.0 ) + return cellPair.first; + } + return Symbol(); + } + + /* Compute the entering symbol for the dual optimize operation. + + This method will return the symbol in the row which has a positive + coefficient and yields the minimum ratio for its respective symbol + in the objective function. The provided row *must* be infeasible. + If no symbol is found which meats the criteria, an invalid symbol + is returned. + + */ + Symbol getDualEnteringSymbol( const Row& row ) const + { + Symbol entering; + double ratio = std::numeric_limits::max(); + for (const auto &cellPair : row.cells()) + { + if( cellPair.second > 0.0 && cellPair.first.type() != Symbol::Dummy ) + { + double coeff = m_objective->coefficientFor( cellPair.first ); + double r = coeff / cellPair.second; + if( r < ratio ) + { + ratio = r; + entering = cellPair.first; + } + } + } + return entering; + } + + /* Get the first Slack or Error symbol in the row. + + If no such symbol is present, and Invalid symbol will be returned. + + */ + Symbol anyPivotableSymbol( const Row& row ) const + { + for (const auto &cellPair : row.cells()) + { + const Symbol& sym( cellPair.first ); + if( sym.type() == Symbol::Slack || sym.type() == Symbol::Error ) + return sym; + } + return Symbol(); + } + + /* Compute the row which holds the exit symbol for a pivot. + + This method will return an iterator to the row in the row map + which holds the exit symbol. If no appropriate exit symbol is + found, the end() iterator will be returned. This indicates that + the objective function is unbounded. + + */ + RowMap::iterator getLeavingRow( const Symbol& entering ) + { + double ratio = std::numeric_limits::max(); + auto end = m_rows.end(); + auto found = m_rows.end(); + for( auto it = m_rows.begin(); it != end; ++it ) + { + if( it->first.type() != Symbol::External ) + { + double temp = it->second->coefficientFor( entering ); + if( temp < 0.0 ) + { + double temp_ratio = -it->second->constant() / temp; + if( temp_ratio < ratio ) + { + ratio = temp_ratio; + found = it; + } + } + } + } + return found; + } + + /* Compute the leaving row for a marker variable. + + This method will return an iterator to the row in the row map + which holds the given marker variable. The row will be chosen + according to the following precedence: + + 1) The row with a restricted basic varible and a negative coefficient + for the marker with the smallest ratio of -constant / coefficient. + + 2) The row with a restricted basic variable and the smallest ratio + of constant / coefficient. + + 3) The last unrestricted row which contains the marker. + + If the marker does not exist in any row, the row map end() iterator + will be returned. This indicates an internal solver error since + the marker *should* exist somewhere in the tableau. + + */ + RowMap::iterator getMarkerLeavingRow( const Symbol& marker ) + { + const double dmax = std::numeric_limits::max(); + double r1 = dmax; + double r2 = dmax; + auto end = m_rows.end(); + auto first = end; + auto second = end; + auto third = end; + for( auto it = m_rows.begin(); it != end; ++it ) + { + double c = it->second->coefficientFor( marker ); + if( c == 0.0 ) + continue; + if( it->first.type() == Symbol::External ) + { + third = it; + } + else if( c < 0.0 ) + { + double r = -it->second->constant() / c; + if( r < r1 ) + { + r1 = r; + first = it; + } + } + else + { + double r = it->second->constant() / c; + if( r < r2 ) + { + r2 = r; + second = it; + } + } + } + if( first != end ) + return first; + if( second != end ) + return second; + return third; + } + + /* Remove the effects of a constraint on the objective function. + + */ + void removeConstraintEffects( const Constraint& cn, const Tag& tag ) + { + if( tag.marker.type() == Symbol::Error ) + removeMarkerEffects( tag.marker, cn.strength() ); + if( tag.other.type() == Symbol::Error ) + removeMarkerEffects( tag.other, cn.strength() ); + } + + /* Remove the effects of an error marker on the objective function. + + */ + void removeMarkerEffects( const Symbol& marker, double strength ) + { + auto row_it = m_rows.find( marker ); + if( row_it != m_rows.end() ) + m_objective->insert( *row_it->second, -strength ); + else + m_objective->insert( marker, -strength ); + } + + /* Test whether a row is composed of all dummy variables. + + */ + bool allDummies( const Row& row ) const + { + for (const auto &rowPair : row.cells()) + { + if( rowPair.first.type() != Symbol::Dummy ) + return false; + } + return true; + } + + CnMap m_cns; + RowMap m_rows; + VarMap m_vars; + EditMap m_edits; + std::vector m_infeasible_rows; + std::unique_ptr m_objective; + std::unique_ptr m_artificial; + Symbol::Id m_id_tick; +}; + +} // namespace impl + +} // namespace kiwi diff --git a/kiwi/strength.h b/kiwi/strength.h new file mode 100644 index 0000000..11732e9 --- /dev/null +++ b/kiwi/strength.h @@ -0,0 +1,44 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include + + +namespace kiwi +{ + +namespace strength +{ + +inline double create( double a, double b, double c, double w = 1.0 ) +{ + double result = 0.0; + result += std::max( 0.0, std::min( 1000.0, a * w ) ) * 1000000.0; + result += std::max( 0.0, std::min( 1000.0, b * w ) ) * 1000.0; + result += std::max( 0.0, std::min( 1000.0, c * w ) ); + return result; +} + + +const double required = create( 1000.0, 1000.0, 1000.0 ); + +const double strong = create( 1.0, 0.0, 0.0 ); + +const double medium = create( 0.0, 1.0, 0.0 ); + +const double weak = create( 0.0, 0.0, 1.0 ); + + +inline double clip( double value ) +{ + return std::max( 0.0, std::min( required, value ) ); +} + +} // namespace strength + +} // namespace kiwi diff --git a/kiwi/symbol.h b/kiwi/symbol.h new file mode 100644 index 0000000..2c49a5c --- /dev/null +++ b/kiwi/symbol.h @@ -0,0 +1,68 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once + + +namespace kiwi +{ + +namespace impl +{ + +class Symbol +{ + +public: + + using Id = unsigned long long; + + enum Type + { + Invalid, + External, + Slack, + Error, + Dummy + }; + + Symbol() : m_id( 0 ), m_type( Invalid ) {} + + Symbol( Type type, Id id ) : m_id( id ), m_type( type ) {} + + ~Symbol() = default; + + Id id() const + { + return m_id; + } + + Type type() const + { + return m_type; + } + +private: + + Id m_id; + Type m_type; + + friend bool operator<( const Symbol& lhs, const Symbol& rhs ) + { + return lhs.m_id < rhs.m_id; + } + + friend bool operator==( const Symbol& lhs, const Symbol& rhs ) + { + return lhs.m_id == rhs.m_id; + } + +}; + +} // namespace impl + +} // namespace kiwi diff --git a/kiwi/symbolics.h b/kiwi/symbolics.h new file mode 100644 index 0000000..c665860 --- /dev/null +++ b/kiwi/symbolics.h @@ -0,0 +1,680 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include "constraint.h" +#include "expression.h" +#include "term.h" +#include "variable.h" + + +namespace kiwi +{ + +// Variable multiply, divide, and unary invert + +inline +Term operator*( const Variable& variable, double coefficient ) +{ + return Term( variable, coefficient ); +} + + +inline +Term operator/( const Variable& variable, double denominator ) +{ + return variable * ( 1.0 / denominator ); +} + + +inline +Term operator-( const Variable& variable ) +{ + return variable * -1.0; +} + + +// Term multiply, divide, and unary invert + +inline +Term operator*( const Term& term, double coefficient ) +{ + return Term( term.variable(), term.coefficient() * coefficient ); +} + + +inline +Term operator/( const Term& term, double denominator ) +{ + return term * ( 1.0 / denominator ); +} + + +inline +Term operator-( const Term& term ) +{ + return term * -1.0; +} + + +// Expression multiply, divide, and unary invert + +inline +Expression operator*( const Expression& expression, double coefficient ) +{ + std::vector terms; + terms.reserve( expression.terms().size() ); + + for (const Term &term : expression.terms()) + terms.push_back(term * coefficient); + + return Expression( std::move(terms), expression.constant() * coefficient ); +} + + +inline +Expression operator/( const Expression& expression, double denominator ) +{ + return expression * ( 1.0 / denominator ); +} + + +inline +Expression operator-( const Expression& expression ) +{ + return expression * -1.0; +} + + +// Double multiply + +inline +Expression operator*( double coefficient, const Expression& expression ) +{ + return expression * coefficient; +} + + +inline +Term operator*( double coefficient, const Term& term ) +{ + return term * coefficient; +} + + +inline +Term operator*( double coefficient, const Variable& variable ) +{ + return variable * coefficient; +} + + +// Expression add and subtract + +inline +Expression operator+( const Expression& first, const Expression& second ) +{ + std::vector terms; + terms.reserve( first.terms().size() + second.terms().size() ); + terms.insert( terms.begin(), first.terms().begin(), first.terms().end() ); + terms.insert( terms.end(), second.terms().begin(), second.terms().end() ); + return Expression( std::move(terms), first.constant() + second.constant() ); +} + + +inline +Expression operator+( const Expression& first, const Term& second ) +{ + std::vector terms; + terms.reserve( first.terms().size() + 1 ); + terms.insert( terms.begin(), first.terms().begin(), first.terms().end() ); + terms.push_back( second ); + return Expression( std::move(terms), first.constant() ); +} + + +inline +Expression operator+( const Expression& expression, const Variable& variable ) +{ + return expression + Term( variable ); +} + + +inline +Expression operator+( const Expression& expression, double constant ) +{ + return Expression( expression.terms(), expression.constant() + constant ); +} + + +inline +Expression operator-( const Expression& first, const Expression& second ) +{ + return first + -second; +} + + +inline +Expression operator-( const Expression& expression, const Term& term ) +{ + return expression + -term; +} + + +inline +Expression operator-( const Expression& expression, const Variable& variable ) +{ + return expression + -variable; +} + + +inline +Expression operator-( const Expression& expression, double constant ) +{ + return expression + -constant; +} + + +// Term add and subtract + +inline +Expression operator+( const Term& term, const Expression& expression ) +{ + return expression + term; +} + + +inline +Expression operator+( const Term& first, const Term& second ) +{ + return Expression( { first, second } ); +} + + +inline +Expression operator+( const Term& term, const Variable& variable ) +{ + return term + Term( variable ); +} + + +inline +Expression operator+( const Term& term, double constant ) +{ + return Expression( term, constant ); +} + + +inline +Expression operator-( const Term& term, const Expression& expression ) +{ + return -expression + term; +} + + +inline +Expression operator-( const Term& first, const Term& second ) +{ + return first + -second; +} + + +inline +Expression operator-( const Term& term, const Variable& variable ) +{ + return term + -variable; +} + + +inline +Expression operator-( const Term& term, double constant ) +{ + return term + -constant; +} + + +// Variable add and subtract + +inline +Expression operator+( const Variable& variable, const Expression& expression ) +{ + return expression + variable; +} + + +inline +Expression operator+( const Variable& variable, const Term& term ) +{ + return term + variable; +} + + +inline +Expression operator+( const Variable& first, const Variable& second ) +{ + return Term( first ) + second; +} + + +inline +Expression operator+( const Variable& variable, double constant ) +{ + return Term( variable ) + constant; +} + + +inline +Expression operator-( const Variable& variable, const Expression& expression ) +{ + return variable + -expression; +} + + +inline +Expression operator-( const Variable& variable, const Term& term ) +{ + return variable + -term; +} + + +inline +Expression operator-( const Variable& first, const Variable& second ) +{ + return first + -second; +} + + +inline +Expression operator-( const Variable& variable, double constant ) +{ + return variable + -constant; +} + + +// Double add and subtract + +inline +Expression operator+( double constant, const Expression& expression ) +{ + return expression + constant; +} + + +inline +Expression operator+( double constant, const Term& term ) +{ + return term + constant; +} + + +inline +Expression operator+( double constant, const Variable& variable ) +{ + return variable + constant; +} + + +inline +Expression operator-( double constant, const Expression& expression ) +{ + return -expression + constant; +} + + +inline +Expression operator-( double constant, const Term& term ) +{ + return -term + constant; +} + + +inline +Expression operator-( double constant, const Variable& variable ) +{ + return -variable + constant; +} + + +// Expression relations + +inline +Constraint operator==( const Expression& first, const Expression& second ) +{ + return Constraint( first - second, OP_EQ ); +} + + +inline +Constraint operator==( const Expression& expression, const Term& term ) +{ + return expression == Expression( term ); +} + + +inline +Constraint operator==( const Expression& expression, const Variable& variable ) +{ + return expression == Term( variable ); +} + + +inline +Constraint operator==( const Expression& expression, double constant ) +{ + return expression == Expression( constant ); +} + + +inline +Constraint operator<=( const Expression& first, const Expression& second ) +{ + return Constraint( first - second, OP_LE ); +} + + +inline +Constraint operator<=( const Expression& expression, const Term& term ) +{ + return expression <= Expression( term ); +} + + +inline +Constraint operator<=( const Expression& expression, const Variable& variable ) +{ + return expression <= Term( variable ); +} + + +inline +Constraint operator<=( const Expression& expression, double constant ) +{ + return expression <= Expression( constant ); +} + + +inline +Constraint operator>=( const Expression& first, const Expression& second ) +{ + return Constraint( first - second, OP_GE ); +} + + +inline +Constraint operator>=( const Expression& expression, const Term& term ) +{ + return expression >= Expression( term ); +} + + +inline +Constraint operator>=( const Expression& expression, const Variable& variable ) +{ + return expression >= Term( variable ); +} + + +inline +Constraint operator>=( const Expression& expression, double constant ) +{ + return expression >= Expression( constant ); +} + + +// Term relations + +inline +Constraint operator==( const Term& term, const Expression& expression ) +{ + return expression == term; +} + + +inline +Constraint operator==( const Term& first, const Term& second ) +{ + return Expression( first ) == second; +} + + +inline +Constraint operator==( const Term& term, const Variable& variable ) +{ + return Expression( term ) == variable; +} + + +inline +Constraint operator==( const Term& term, double constant ) +{ + return Expression( term ) == constant; +} + + +inline +Constraint operator<=( const Term& term, const Expression& expression ) +{ + return expression >= term; +} + + +inline +Constraint operator<=( const Term& first, const Term& second ) +{ + return Expression( first ) <= second; +} + + +inline +Constraint operator<=( const Term& term, const Variable& variable ) +{ + return Expression( term ) <= variable; +} + + +inline +Constraint operator<=( const Term& term, double constant ) +{ + return Expression( term ) <= constant; +} + + +inline +Constraint operator>=( const Term& term, const Expression& expression ) +{ + return expression <= term; +} + + +inline +Constraint operator>=( const Term& first, const Term& second ) +{ + return Expression( first ) >= second; +} + + +inline +Constraint operator>=( const Term& term, const Variable& variable ) +{ + return Expression( term ) >= variable; +} + + +inline +Constraint operator>=( const Term& term, double constant ) +{ + return Expression( term ) >= constant; +} + + +// Variable relations +inline +Constraint operator==( const Variable& variable, const Expression& expression ) +{ + return expression == variable; +} + + +inline +Constraint operator==( const Variable& variable, const Term& term ) +{ + return term == variable; +} + + +inline +Constraint operator==( const Variable& first, const Variable& second ) +{ + return Term( first ) == second; +} + + +inline +Constraint operator==( const Variable& variable, double constant ) +{ + return Term( variable ) == constant; +} + + +inline +Constraint operator<=( const Variable& variable, const Expression& expression ) +{ + return expression >= variable; +} + + +inline +Constraint operator<=( const Variable& variable, const Term& term ) +{ + return term >= variable; +} + + +inline +Constraint operator<=( const Variable& first, const Variable& second ) +{ + return Term( first ) <= second; +} + + +inline +Constraint operator<=( const Variable& variable, double constant ) +{ + return Term( variable ) <= constant; +} + + +inline +Constraint operator>=( const Variable& variable, const Expression& expression ) +{ + return expression <= variable; +} + + +inline +Constraint operator>=( const Variable& variable, const Term& term ) +{ + return term <= variable; +} + + +inline +Constraint operator>=( const Variable& first, const Variable& second ) +{ + return Term( first ) >= second; +} + + +inline +Constraint operator>=( const Variable& variable, double constant ) +{ + return Term( variable ) >= constant; +} + + +// Double relations + +inline +Constraint operator==( double constant, const Expression& expression ) +{ + return expression == constant; +} + + +inline +Constraint operator==( double constant, const Term& term ) +{ + return term == constant; +} + + +inline +Constraint operator==( double constant, const Variable& variable ) +{ + return variable == constant; +} + + +inline +Constraint operator<=( double constant, const Expression& expression ) +{ + return expression >= constant; +} + + +inline +Constraint operator<=( double constant, const Term& term ) +{ + return term >= constant; +} + + +inline +Constraint operator<=( double constant, const Variable& variable ) +{ + return variable >= constant; +} + + +inline +Constraint operator>=( double constant, const Expression& expression ) +{ + return expression <= constant; +} + + +inline +Constraint operator>=( double constant, const Term& term ) +{ + return term <= constant; +} + + +inline +Constraint operator>=( double constant, const Variable& variable ) +{ + return variable <= constant; +} + + +// Constraint strength modifier + +inline +Constraint operator|( const Constraint& constraint, double strength ) +{ + return Constraint( constraint, strength ); +} + + +inline +Constraint operator|( double strength, const Constraint& constraint ) +{ + return constraint | strength; +} + +} // namespace kiwi diff --git a/kiwi/term.h b/kiwi/term.h new file mode 100644 index 0000000..85e00b1 --- /dev/null +++ b/kiwi/term.h @@ -0,0 +1,59 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include "variable.h" + + +namespace kiwi +{ + +class Term +{ + +public: + + Term( Variable variable, double coefficient = 1.0 ) : + m_variable( std::move(variable) ), m_coefficient( coefficient ) {} + + // to facilitate efficient map -> vector copies + Term( const std::pair& pair ) : + m_variable( pair.first ), m_coefficient( pair.second ) {} + + Term(const Term&) = default; + + Term(Term&&) noexcept = default; + + ~Term() = default; + + const Variable& variable() const + { + return m_variable; + } + + double coefficient() const + { + return m_coefficient; + } + + double value() const + { + return m_coefficient * m_variable.value(); + } + + Term& operator=(const Term&) = default; + + Term& operator=(Term&&) noexcept = default; + +private: + + Variable m_variable; + double m_coefficient; +}; + +} // namespace kiwi diff --git a/kiwi/util.h b/kiwi/util.h new file mode 100644 index 0000000..560a43a --- /dev/null +++ b/kiwi/util.h @@ -0,0 +1,24 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once + +namespace kiwi +{ + +namespace impl +{ + +inline bool nearZero(double value) +{ + const double eps = 1.0e-8; + return value < 0.0 ? -value < eps : value < eps; +} + +} // namespace impl + +} // namespace kiwi diff --git a/kiwi/variable.h b/kiwi/variable.h new file mode 100644 index 0000000..98d7499 --- /dev/null +++ b/kiwi/variable.h @@ -0,0 +1,119 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2017, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include +#include "shareddata.h" + +namespace kiwi +{ + +class Variable +{ + +public: + class Context + { + public: + Context() = default; + virtual ~Context() {} // LCOV_EXCL_LINE + }; + + Variable(Context *context = 0) : m_data(new VariableData("", context)) {} + + Variable(std::string name, Context *context = 0) : m_data(new VariableData(std::move(name), context)) {} + + Variable(const char *name, Context *context = 0) : m_data(new VariableData(name, context)) {} + + Variable(const Variable&) = default; + + Variable(Variable&&) noexcept = default; + + ~Variable() = default; + + const std::string &name() const + { + return m_data->m_name; + } + + void setName(const char *name) + { + m_data->m_name = name; + } + + void setName(const std::string &name) + { + m_data->m_name = name; + } + + Context *context() const + { + return m_data->m_context.get(); + } + + void setContext(Context *context) + { + m_data->m_context.reset(context); + } + + double value() const + { + return m_data->m_value; + } + + void setValue(double value) + { + m_data->m_value = value; + } + + // operator== is used for symbolics + bool equals(const Variable &other) + { + return m_data == other.m_data; + } + + Variable& operator=(const Variable&) = default; + + Variable& operator=(Variable&&) noexcept = default; + +private: + class VariableData : public SharedData + { + + public: + VariableData(std::string name, Context *context) : SharedData(), + m_name(std::move(name)), + m_context(context), + m_value(0.0) {} + + VariableData(const char *name, Context *context) : SharedData(), + m_name(name), + m_context(context), + m_value(0.0) {} + + ~VariableData() = default; + + std::string m_name; + std::unique_ptr m_context; + double m_value; + + private: + VariableData(const VariableData &other); + + VariableData &operator=(const VariableData &other); + }; + + SharedDataPtr m_data; + + friend bool operator<(const Variable &lhs, const Variable &rhs) + { + return lhs.m_data < rhs.m_data; + } +}; + +} // namespace kiwi diff --git a/kiwi/version.h b/kiwi/version.h new file mode 100644 index 0000000..562d447 --- /dev/null +++ b/kiwi/version.h @@ -0,0 +1,14 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2022, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once + +#define KIWI_MAJOR_VERSION 1 +#define KIWI_MINOR_VERSION 4 +#define KIWI_MICRO_VERSION 2 +#define KIWI_VERSION_HEX 0x010402 +#define KIWI_VERSION "1.4.2" diff --git a/lint_requirements.txt b/lint_requirements.txt new file mode 100644 index 0000000..152b65b --- /dev/null +++ b/lint_requirements.txt @@ -0,0 +1,4 @@ +ruff +mypy +# Allow to lint tests using mypy +pytest \ No newline at end of file diff --git a/py/kiwisolver/__init__.py b/py/kiwisolver/__init__.py new file mode 100644 index 0000000..f4e1753 --- /dev/null +++ b/py/kiwisolver/__init__.py @@ -0,0 +1,42 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2013-2022, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +from ._cext import ( + Constraint, + Expression, + Solver, + Term, + Variable, + __kiwi_version__, + __version__, + strength, +) +from .exceptions import ( + BadRequiredStrength, + DuplicateConstraint, + DuplicateEditVariable, + UnknownConstraint, + UnknownEditVariable, + UnsatisfiableConstraint, +) + +__all__ = [ + "BadRequiredStrength", + "DuplicateConstraint", + "DuplicateEditVariable", + "UnknownConstraint", + "UnknownEditVariable", + "UnsatisfiableConstraint", + "strength", + "Variable", + "Term", + "Expression", + "Constraint", + "Solver", + "__version__", + "__kiwi_version__", +] diff --git a/py/kiwisolver/_cext.pyi b/py/kiwisolver/_cext.pyi new file mode 100644 index 0000000..c786c7e --- /dev/null +++ b/py/kiwisolver/_cext.pyi @@ -0,0 +1,228 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2021, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- + +from typing import Any, Iterable, NoReturn, Tuple, type_check_only + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal # type: ignore + +__version__: str +__kiwi_version__: str + +# Types +@type_check_only +class Strength: + @property + def weak(self) -> float: ... + @property + def medium(self) -> float: ... + @property + def strong(self) -> float: ... + @property + def required(self) -> float: ... + def create( + self, + a: int | float, + b: int | float, + c: int | float, + weight: int | float = 1.0, + /, + ) -> float: ... + +# This is meant as a singleton and users should not access the Strength type. +strength: Strength + +class Variable: + """Variable to express a constraint in a solver.""" + + __hash__: None # type: ignore + def __init__(self, name: str = "", context: Any = None, /) -> None: ... + def name(self) -> str: + """Get the name of the variable.""" + ... + def setName(self, name: str, /) -> Any: + """Set the name of the variable.""" + ... + def value(self) -> float: + """Get the current value of the variable.""" + ... + def context(self) -> Any: + """Get the context object associated with the variable.""" + ... + def setContext(self, context: Any, /) -> Any: + """Set the context object associated with the variable.""" + ... + def __neg__(self) -> Term: ... + def __add__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __radd__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __sub__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __rsub__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __mul__(self, other: float) -> Term: ... + def __rmul__(self, other: float) -> Term: ... + def __truediv__(self, other: float) -> Term: ... + def __rtruediv__(self, other: float) -> Term: ... + def __eq__(self, other: float | Variable | Term | Expression) -> Constraint: ... # type: ignore + def __ge__(self, other: float | Variable | Term | Expression) -> Constraint: ... + def __le__(self, other: float | Variable | Term | Expression) -> Constraint: ... + def __ne__(self, other: Any) -> NoReturn: ... + def __gt__(self, other: Any) -> NoReturn: ... + def __lt__(self, other: Any) -> NoReturn: ... + +class Term: + """Product of a variable by a constant pre-factor.""" + + __hash__: None # type: ignore + def __init__( + self, variable: Variable, coefficient: int | float = 1.0, / + ) -> None: ... + def coefficient(self) -> float: + """Get the coefficient for the term.""" + ... + def variable(self) -> Variable: + """Get the variable for the term.""" + ... + def value(self) -> float: + """Get the value for the term.""" + ... + def __neg__(self) -> Term: ... + def __add__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __radd__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __sub__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __rsub__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __mul__(self, other: float) -> Term: ... + def __rmul__(self, other: float) -> Term: ... + def __truediv__(self, other: float) -> Term: ... + def __rtruediv__(self, other: float) -> Term: ... + def __eq__(self, other: float | Variable | Term | Expression) -> Constraint: ... # type: ignore + def __ge__(self, other: float | Variable | Term | Expression) -> Constraint: ... + def __le__(self, other: float | Variable | Term | Expression) -> Constraint: ... + def __ne__(self, other: Any) -> NoReturn: ... + def __gt__(self, other: Any) -> NoReturn: ... + def __lt__(self, other: Any) -> NoReturn: ... + +class Expression: + """Sum of terms and an additional constant.""" + + __hash__: None # type: ignore + def __init__( + self, terms: Iterable[Term], constant: int | float = 0.0, / + ) -> None: ... + def constant(self) -> float: + "" "Get the constant for the expression." "" + ... + def terms(self) -> Tuple[Term, ...]: + """Get the tuple of terms for the expression.""" + ... + def value(self) -> float: + """Get the value for the expression.""" + ... + def __neg__(self) -> Expression: ... + def __add__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __radd__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __sub__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __rsub__(self, other: float | Variable | Term | Expression) -> Expression: ... + def __mul__(self, other: float) -> Expression: ... + def __rmul__(self, other: float) -> Expression: ... + def __truediv__(self, other: float) -> Expression: ... + def __rtruediv__(self, other: float) -> Expression: ... + def __eq__(self, other: float | Variable | Term | Expression) -> Constraint: ... # type: ignore + def __ge__(self, other: float | Variable | Term | Expression) -> Constraint: ... + def __le__(self, other: float | Variable | Term | Expression) -> Constraint: ... + def __ne__(self, other: Any) -> NoReturn: ... + def __gt__(self, other: Any) -> NoReturn: ... + def __lt__(self, other: Any) -> NoReturn: ... + +class Constraint: + def __init__( + self, + expression: Expression, + op: Literal["=="] | Literal["<="] | Literal[">="], + strength: float + | Literal["weak"] + | Literal["medium"] + | Literal["strong"] + | Literal["required"] = "required", + /, + ) -> None: ... + def expression(self) -> Expression: + """Get the expression object for the constraint.""" + ... + def op(self) -> Literal["=="] | Literal["<="] | Literal[">="]: + """Get the relational operator for the constraint.""" + ... + def strength(self) -> float: + """Get the strength for the constraint.""" + ... + def violated(self) -> bool: + """Indicate if the constraint is violated in teh current state of the solver.""" + ... + def __or__( + self, + other: float + | Literal["weak"] + | Literal["medium"] + | Literal["strong"] + | Literal["required"], + ) -> Constraint: ... + def __ror__( + self, + other: float + | Literal["weak"] + | Literal["medium"] + | Literal["strong"] + | Literal["required"], + ) -> Constraint: ... + +class Solver: + """Kiwi solver class.""" + + def __init__(self) -> None: ... + def addConstraint(self, constraint: Constraint, /) -> None: + """Add a constraint to the solver.""" + ... + def removeConstraint(self, constraint: Constraint, /) -> None: + """Remove a constraint from the solver.""" + ... + def hasConstraint(self, constraint: Constraint, /) -> bool: + """Check whether the solver contains a constraint.""" + ... + def addEditVariable( + self, + variable: Variable, + strength: float + | Literal["weak"] + | Literal["medium"] + | Literal["strong"] + | Literal["required"], + /, + ) -> None: + """Add an edit variable to the solver.""" + ... + def removeEditVariable(self, variable: Variable, /) -> None: + """Remove an edit variable from the solver.""" + ... + def hasEditVariable(self, variable: Variable, /) -> bool: + """Check whether the solver contains an edit variable.""" + ... + def suggestValue(self, variable: Variable, value: int | float, /) -> None: + """Suggest a desired value for an edit variable.""" + ... + def updateVariables(self) -> None: + """Update the values of the solver variables.""" + ... + def reset(self) -> None: + """Reset the solver to the initial empty starting condition.""" + ... + def dump(self) -> None: + """Dump a representation of the solver internals to stdout.""" + ... + def dumps(self) -> str: + """Dump a representation of the solver internals to a string.""" + ... diff --git a/py/kiwisolver/exceptions.py b/py/kiwisolver/exceptions.py new file mode 100644 index 0000000..f982ee4 --- /dev/null +++ b/py/kiwisolver/exceptions.py @@ -0,0 +1,51 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2023, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +"""Kiwi exceptions. + +Imported by the kiwisolver C extension. + +""" + + +class BadRequiredStrength(Exception): + pass + + +class DuplicateConstraint(Exception): + __slots__ = ("constraint",) + + def __init__(self, constraint): + self.constraint = constraint + + +class DuplicateEditVariable(Exception): + __slots__ = ("edit_variable",) + + def __init__(self, edit_variable): + self.edit_variable = edit_variable + + +class UnknownConstraint(Exception): + __slots__ = ("constraint",) + + def __init__(self, constraint): + self.constraint = constraint + + +class UnknownEditVariable(Exception): + __slots__ = ("edit_variable",) + + def __init__(self, edit_variable): + self.edit_variable = edit_variable + + +class UnsatisfiableConstraint(Exception): + __slots__ = ("constraint",) + + def __init__(self, constraint): + self.constraint = constraint diff --git a/py/kiwisolver/py.typed b/py/kiwisolver/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/py/src/constraint.cpp b/py/src/constraint.cpp new file mode 100644 index 0000000..14fb62a --- /dev/null +++ b/py/src/constraint.cpp @@ -0,0 +1,222 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#include +#include +#include +#include +#include "types.h" +#include "util.h" + +namespace kiwisolver +{ + +namespace +{ + +PyObject * +Constraint_new(PyTypeObject *type, PyObject *args, PyObject *kwargs) +{ + static const char *kwlist[] = {"expression", "op", "strength", 0}; + PyObject *pyexpr; + PyObject *pyop; + PyObject *pystrength = 0; + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "OO|O:__new__", const_cast(kwlist), + &pyexpr, &pyop, &pystrength)) + return 0; + if (!Expression::TypeCheck(pyexpr)) + return cppy::type_error(pyexpr, "Expression"); + kiwi::RelationalOperator op; + if (!convert_to_relational_op(pyop, op)) + return 0; + double strength = kiwi::strength::required; + if (pystrength && !convert_to_strength(pystrength, strength)) + return 0; + cppy::ptr pycn(PyType_GenericNew(type, args, kwargs)); + if (!pycn) + return 0; + Constraint *cn = reinterpret_cast(pycn.get()); + cn->expression = reduce_expression(pyexpr); + if (!cn->expression) + return 0; + kiwi::Expression expr(convert_to_kiwi_expression(cn->expression)); + new (&cn->constraint) kiwi::Constraint(expr, op, strength); + return pycn.release(); +} + +void Constraint_clear(Constraint *self) +{ + Py_CLEAR(self->expression); +} + +int Constraint_traverse(Constraint *self, visitproc visit, void *arg) +{ + Py_VISIT(self->expression); +#if PY_VERSION_HEX >= 0x03090000 + // This was not needed before Python 3.9 (Python issue 35810 and 40217) + Py_VISIT(Py_TYPE(self)); +#endif + return 0; +} + +void Constraint_dealloc(Constraint *self) +{ + PyObject_GC_UnTrack(self); + Constraint_clear(self); + self->constraint.~Constraint(); + Py_TYPE(self)->tp_free(pyobject_cast(self)); +} + +PyObject * +Constraint_repr(Constraint *self) +{ + std::stringstream stream; + Expression *expr = reinterpret_cast(self->expression); + Py_ssize_t size = PyTuple_GET_SIZE(expr->terms); + for (Py_ssize_t i = 0; i < size; ++i) + { + PyObject *item = PyTuple_GET_ITEM(expr->terms, i); + Term *term = reinterpret_cast(item); + stream << term->coefficient << " * "; + stream << reinterpret_cast(term->variable)->variable.name(); + stream << " + "; + } + stream << expr->constant; + switch (self->constraint.op()) + { + case kiwi::OP_EQ: + stream << " == 0"; + break; + case kiwi::OP_LE: + stream << " <= 0"; + break; + case kiwi::OP_GE: + stream << " >= 0"; + break; + } + stream << " | strength = " << self->constraint.strength(); + if (self->constraint.violated()) + { + stream << " (VIOLATED)"; + } + return PyUnicode_FromString(stream.str().c_str()); +} + +PyObject * +Constraint_expression(Constraint *self) +{ + return cppy::incref(self->expression); +} + +PyObject * +Constraint_op(Constraint *self) +{ + PyObject *res = 0; + switch (self->constraint.op()) + { + case kiwi::OP_EQ: + res = PyUnicode_FromString("=="); + break; + case kiwi::OP_LE: + res = PyUnicode_FromString("<="); + break; + case kiwi::OP_GE: + res = PyUnicode_FromString(">="); + break; + } + return res; +} + +PyObject * +Constraint_strength(Constraint *self) +{ + return PyFloat_FromDouble(self->constraint.strength()); +} + +PyObject * +Constraint_violated(Constraint *self) +{ + if (self->constraint.violated()) { + Py_RETURN_TRUE; + } else { + Py_RETURN_FALSE; + } +} + +PyObject * +Constraint_or(PyObject *pyoldcn, PyObject *value) +{ + if (!Constraint::TypeCheck(pyoldcn)) + std::swap(pyoldcn, value); + double strength; + if (!convert_to_strength(value, strength)) + return 0; + PyObject *pynewcn = PyType_GenericNew(Constraint::TypeObject, 0, 0); + if (!pynewcn) + return 0; + Constraint *oldcn = reinterpret_cast(pyoldcn); + Constraint *newcn = reinterpret_cast(pynewcn); + newcn->expression = cppy::incref(oldcn->expression); + new (&newcn->constraint) kiwi::Constraint(oldcn->constraint, strength); + return pynewcn; +} + +static PyMethodDef + Constraint_methods[] = { + {"expression", (PyCFunction)Constraint_expression, METH_NOARGS, + "Get the expression object for the constraint."}, + {"op", (PyCFunction)Constraint_op, METH_NOARGS, + "Get the relational operator for the constraint."}, + {"strength", (PyCFunction)Constraint_strength, METH_NOARGS, + "Get the strength for the constraint."}, + {"violated", (PyCFunction)Constraint_violated, METH_NOARGS, + "Return whether or not the constraint was violated " + "during the last solver pass."}, + {0} // sentinel +}; + +static PyType_Slot Constraint_Type_slots[] = { + {Py_tp_dealloc, void_cast(Constraint_dealloc)}, /* tp_dealloc */ + {Py_tp_traverse, void_cast(Constraint_traverse)}, /* tp_traverse */ + {Py_tp_clear, void_cast(Constraint_clear)}, /* tp_clear */ + {Py_tp_repr, void_cast(Constraint_repr)}, /* tp_repr */ + {Py_tp_methods, void_cast(Constraint_methods)}, /* tp_methods */ + {Py_tp_new, void_cast(Constraint_new)}, /* tp_new */ + {Py_tp_alloc, void_cast(PyType_GenericAlloc)}, /* tp_alloc */ + {Py_tp_free, void_cast(PyObject_GC_Del)}, /* tp_free */ + {Py_nb_or, void_cast(Constraint_or)}, /* nb_or */ + {0, 0}, +}; + +} // namespace + +// Initialize static variables (otherwise the compiler eliminates them) +PyTypeObject *Constraint::TypeObject = NULL; + +PyType_Spec Constraint::TypeObject_Spec = { + "kiwisolver.Constraint", /* tp_name */ + sizeof(Constraint), /* tp_basicsize */ + 0, /* tp_itemsize */ + Py_TPFLAGS_DEFAULT | + Py_TPFLAGS_HAVE_GC | + Py_TPFLAGS_BASETYPE, /* tp_flags */ + Constraint_Type_slots /* slots */ +}; + +bool Constraint::Ready() +{ + // The reference will be handled by the module to which we will add the type + TypeObject = pytype_cast(PyType_FromSpec(&TypeObject_Spec)); + if (!TypeObject) + { + return false; + } + return true; +} + +} // namespace kiwisolver diff --git a/py/src/expression.cpp b/py/src/expression.cpp new file mode 100644 index 0000000..3840048 --- /dev/null +++ b/py/src/expression.cpp @@ -0,0 +1,251 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#include +#include +#include "symbolics.h" +#include "types.h" +#include "util.h" + + +namespace kiwisolver +{ + +namespace +{ + +PyObject* +Expression_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) +{ + static const char *kwlist[] = { "terms", "constant", 0 }; + PyObject* pyterms; + PyObject* pyconstant = 0; + if( !PyArg_ParseTupleAndKeywords( + args, kwargs, "O|O:__new__", const_cast( kwlist ), + &pyterms, &pyconstant ) ) + return 0; + cppy::ptr terms( PySequence_Tuple( pyterms ) ); + if( !terms ) + return 0; + Py_ssize_t end = PyTuple_GET_SIZE( terms.get() ); + for( Py_ssize_t i = 0; i < end; ++i ) + { + PyObject* item = PyTuple_GET_ITEM( terms.get(), i ); + if( !Term::TypeCheck( item ) ) + return cppy::type_error( item, "Term" ); + } + double constant = 0.0; + if( pyconstant && !convert_to_double( pyconstant, constant ) ) + return 0; + PyObject* pyexpr = PyType_GenericNew( type, args, kwargs ); + if( !pyexpr ) + return 0; + Expression* self = reinterpret_cast( pyexpr ); + self->terms = terms.release(); + self->constant = constant; + return pyexpr; +} + + +void +Expression_clear( Expression* self ) +{ + Py_CLEAR( self->terms ); +} + + +int +Expression_traverse( Expression* self, visitproc visit, void* arg ) +{ + Py_VISIT( self->terms ); +#if PY_VERSION_HEX >= 0x03090000 + // This was not needed before Python 3.9 (Python issue 35810 and 40217) + Py_VISIT(Py_TYPE(self)); +#endif + return 0; +} + + +void +Expression_dealloc( Expression* self ) +{ + PyObject_GC_UnTrack( self ); + Expression_clear( self ); + Py_TYPE( self )->tp_free( pyobject_cast( self ) ); +} + + +PyObject* +Expression_repr( Expression* self ) +{ + std::stringstream stream; + Py_ssize_t end = PyTuple_GET_SIZE( self->terms ); + for( Py_ssize_t i = 0; i < end; ++i ) + { + PyObject* item = PyTuple_GET_ITEM( self->terms, i ); + Term* term = reinterpret_cast( item ); + stream << term->coefficient << " * "; + stream << reinterpret_cast( term->variable )->variable.name(); + stream << " + "; + } + stream << self->constant; + return PyUnicode_FromString( stream.str().c_str() ); +} + + +PyObject* +Expression_terms( Expression* self ) +{ + return cppy::incref( self->terms ); +} + + +PyObject* +Expression_constant( Expression* self ) +{ + return PyFloat_FromDouble( self->constant ); +} + + +PyObject* +Expression_value( Expression* self ) +{ + double result = self->constant; + Py_ssize_t size = PyTuple_GET_SIZE( self->terms ); + for( Py_ssize_t i = 0; i < size; ++i ) + { + PyObject* item = PyTuple_GET_ITEM( self->terms, i ); + Term* term = reinterpret_cast( item ); + Variable* pyvar = reinterpret_cast( term->variable ); + result += term->coefficient * pyvar->variable.value(); + } + return PyFloat_FromDouble( result ); +} + + +PyObject* +Expression_add( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Expression_sub( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Expression_mul( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Expression_div( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Expression_neg( PyObject* value ) +{ + return UnaryInvoke()( value ); +} + + +PyObject* +Expression_richcmp( PyObject* first, PyObject* second, int op ) +{ + switch( op ) + { + case Py_EQ: + return BinaryInvoke()( first, second ); + case Py_LE: + return BinaryInvoke()( first, second ); + case Py_GE: + return BinaryInvoke()( first, second ); + default: + break; + } + PyErr_Format( + PyExc_TypeError, + "unsupported operand type(s) for %s: " + "'%.100s' and '%.100s'", + pyop_str( op ), + Py_TYPE( first )->tp_name, + Py_TYPE( second )->tp_name + ); + return 0; +} + + +static PyMethodDef +Expression_methods[] = { + { "terms", ( PyCFunction )Expression_terms, METH_NOARGS, + "Get the tuple of terms for the expression." }, + { "constant", ( PyCFunction )Expression_constant, METH_NOARGS, + "Get the constant for the expression." }, + { "value", ( PyCFunction )Expression_value, METH_NOARGS, + "Get the value for the expression." }, + { 0 } // sentinel +}; + + +static PyType_Slot Expression_Type_slots[] = { + { Py_tp_dealloc, void_cast( Expression_dealloc ) }, /* tp_dealloc */ + { Py_tp_traverse, void_cast( Expression_traverse ) }, /* tp_traverse */ + { Py_tp_clear, void_cast( Expression_clear ) }, /* tp_clear */ + { Py_tp_repr, void_cast( Expression_repr ) }, /* tp_repr */ + { Py_tp_richcompare, void_cast( Expression_richcmp ) }, /* tp_richcompare */ + { Py_tp_methods, void_cast( Expression_methods ) }, /* tp_methods */ + { Py_tp_new, void_cast( Expression_new ) }, /* tp_new */ + { Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */ + { Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */ + { Py_nb_add, void_cast( Expression_add ) }, /* nb_add */ + { Py_nb_subtract, void_cast( Expression_sub ) }, /* nb_sub */ + { Py_nb_multiply, void_cast( Expression_mul ) }, /* nb_mul */ + { Py_nb_negative, void_cast( Expression_neg ) }, /* nb_neg */ + { Py_nb_true_divide, void_cast( Expression_div ) }, /* nb_div */ + { 0, 0 }, +}; + + +} // namespace + + +// Initialize static variables (otherwise the compiler eliminates them) +PyTypeObject* Expression::TypeObject = NULL; + + +PyType_Spec Expression::TypeObject_Spec = { + "kiwisolver.Expression", /* tp_name */ + sizeof( Expression ), /* tp_basicsize */ + 0, /* tp_itemsize */ + Py_TPFLAGS_DEFAULT| + Py_TPFLAGS_HAVE_GC| + Py_TPFLAGS_BASETYPE, /* tp_flags */ + Expression_Type_slots /* slots */ +}; + + +bool Expression::Ready() +{ + // The reference will be handled by the module to which we will add the type + TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) ); + if( !TypeObject ) + { + return false; + } + return true; +} + +} // namesapce kiwisolver diff --git a/py/src/kiwisolver.cpp b/py/src/kiwisolver.cpp new file mode 100644 index 0000000..47a8e06 --- /dev/null +++ b/py/src/kiwisolver.cpp @@ -0,0 +1,187 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2021, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#include +#include +#include "types.h" +#include "version.h" + + +namespace +{ + + +bool ready_types() +{ + using namespace kiwisolver; + if( !Variable::Ready() ) + { + return false; + } + if( !Term::Ready() ) + { + return false; + } + if( !Expression::Ready() ) + { + return false; + } + if( !Constraint::Ready() ) + { + return false; + } + if( !strength::Ready() ) + { + return false; + } + if( !Solver::Ready() ) + { + return false; + } + return true; +} + +bool add_objects( PyObject* mod ) +{ + using namespace kiwisolver; + + cppy::ptr kiwiversion( PyUnicode_FromString( KIWI_VERSION ) ); + if( !kiwiversion ) + { + return false; + } + cppy::ptr pyversion( PyUnicode_FromString( PY_KIWI_VERSION ) ); + if( !pyversion ) + { + return false; + } + cppy::ptr pystrength( PyType_GenericNew( strength::TypeObject, 0, 0 ) ); + if( !pystrength ) + { + return false; + } + + if( PyModule_AddObject( mod, "__version__", pyversion.get() ) < 0 ) + { + return false; + } + pyversion.release(); + + if( PyModule_AddObject( mod, "__kiwi_version__", kiwiversion.get() ) < 0 ) + { + return false; + } + kiwiversion.release(); + + if( PyModule_AddObject( mod, "strength", pystrength.get() ) < 0 ) + { + return false; + } + pystrength.release(); + + // Variable + cppy::ptr var( pyobject_cast( Variable::TypeObject ) ); + if( PyModule_AddObject( mod, "Variable", var.get() ) < 0 ) + { + return false; + } + var.release(); + + // Term + cppy::ptr term( pyobject_cast( Term::TypeObject ) ); + if( PyModule_AddObject( mod, "Term", term.get() ) < 0 ) + { + return false; + } + term.release(); + + // Expression + cppy::ptr expr( pyobject_cast( Expression::TypeObject ) ); + if( PyModule_AddObject( mod, "Expression", expr.get() ) < 0 ) + { + return false; + } + expr.release(); + + // Constraint + cppy::ptr cons( pyobject_cast( Constraint::TypeObject ) ); + if( PyModule_AddObject( mod, "Constraint", cons.get() ) < 0 ) + { + return false; + } + cons.release(); + + cppy::ptr solver( pyobject_cast( Solver::TypeObject ) ); + if( PyModule_AddObject( mod, "Solver", solver.get() ) < 0 ) + { + return false; + } + solver.release(); + + PyModule_AddObject( mod, "DuplicateConstraint", DuplicateConstraint ); + PyModule_AddObject( mod, "UnsatisfiableConstraint", UnsatisfiableConstraint ); + PyModule_AddObject( mod, "UnknownConstraint", UnknownConstraint ); + PyModule_AddObject( mod, "DuplicateEditVariable", DuplicateEditVariable ); + PyModule_AddObject( mod, "UnknownEditVariable", UnknownEditVariable ); + PyModule_AddObject( mod, "BadRequiredStrength", BadRequiredStrength ); + + return true; +} + + +int +kiwi_modexec( PyObject *mod ) +{ + if( !ready_types() ) + { + return -1; + } + if( !kiwisolver::init_exceptions() ) + { + return -1; + } + if( !add_objects( mod ) ) + { + return -1; + } + + + return 0; +} + + +static PyMethodDef +kiwisolver_methods[] = { + { 0 } // Sentinel +}; + + +PyModuleDef_Slot kiwisolver_slots[] = { + {Py_mod_exec, reinterpret_cast( kiwi_modexec ) }, + {0, NULL} +}; + + +struct PyModuleDef moduledef = { + PyModuleDef_HEAD_INIT, + "_cext", + "kiwisolver extension module", + 0, + kiwisolver_methods, + kiwisolver_slots, + NULL, + NULL, + NULL +}; + +} // namespace + + +PyMODINIT_FUNC PyInit__cext( void ) +{ + return PyModuleDef_Init( &moduledef ); +} diff --git a/py/src/solver.cpp b/py/src/solver.cpp new file mode 100644 index 0000000..38a941b --- /dev/null +++ b/py/src/solver.cpp @@ -0,0 +1,338 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#include +#include +#include "types.h" +#include "util.h" + + +namespace kiwisolver +{ + +namespace +{ + +PyObject* +Solver_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) +{ + if( PyTuple_GET_SIZE( args ) != 0 || ( kwargs && PyDict_Size( kwargs ) != 0 ) ) + return cppy::type_error( "Solver.__new__ takes no arguments" ); + PyObject* pysolver = PyType_GenericNew( type, args, kwargs ); + if( !pysolver ) + return 0; + Solver* self = reinterpret_cast( pysolver ); + new( &self->solver ) kiwi::Solver(); + return pysolver; +} + + +void +Solver_dealloc( Solver* self ) +{ + self->solver.~Solver(); + Py_TYPE( self )->tp_free( pyobject_cast( self ) ); +} + + +PyObject* +Solver_addConstraint( Solver* self, PyObject* other ) +{ + if( !Constraint::TypeCheck( other ) ) + return cppy::type_error( other, "Constraint" ); + Constraint* cn = reinterpret_cast( other ); + try + { + self->solver.addConstraint( cn->constraint ); + } + catch( const kiwi::DuplicateConstraint& ) + { + PyErr_SetObject( DuplicateConstraint, other ); + return 0; + } + catch( const kiwi::UnsatisfiableConstraint& ) + { + PyErr_SetObject( UnsatisfiableConstraint, other ); + return 0; + } + Py_RETURN_NONE; +} + + +PyObject* +Solver_removeConstraint( Solver* self, PyObject* other ) +{ + if( !Constraint::TypeCheck( other ) ) + return cppy::type_error( other, "Constraint" ); + Constraint* cn = reinterpret_cast( other ); + try + { + self->solver.removeConstraint( cn->constraint ); + } + catch( const kiwi::UnknownConstraint& ) + { + PyErr_SetObject( UnknownConstraint, other ); + return 0; + } + Py_RETURN_NONE; +} + + +PyObject* +Solver_hasConstraint( Solver* self, PyObject* other ) +{ + if( !Constraint::TypeCheck( other ) ) + return cppy::type_error( other, "Constraint" ); + Constraint* cn = reinterpret_cast( other ); + return cppy::incref( self->solver.hasConstraint( cn->constraint ) ? Py_True : Py_False ); +} + + +PyObject* +Solver_addEditVariable( Solver* self, PyObject* args ) +{ + PyObject* pyvar; + PyObject* pystrength; + if( !PyArg_ParseTuple( args, "OO", &pyvar, &pystrength ) ) + return 0; + if( !Variable::TypeCheck( pyvar ) ) + return cppy::type_error( pyvar, "Variable" ); + double strength; + if( !convert_to_strength( pystrength, strength ) ) + return 0; + Variable* var = reinterpret_cast( pyvar ); + try + { + self->solver.addEditVariable( var->variable, strength ); + } + catch( const kiwi::DuplicateEditVariable& ) + { + PyErr_SetObject( DuplicateEditVariable, pyvar ); + return 0; + } + catch( const kiwi::BadRequiredStrength& e ) + { + PyErr_SetString( BadRequiredStrength, e.what() ); + return 0; + } + Py_RETURN_NONE; +} + + +PyObject* +Solver_removeEditVariable( Solver* self, PyObject* other ) +{ + if( !Variable::TypeCheck( other ) ) + return cppy::type_error( other, "Variable" ); + Variable* var = reinterpret_cast( other ); + try + { + self->solver.removeEditVariable( var->variable ); + } + catch( const kiwi::UnknownEditVariable& ) + { + PyErr_SetObject( UnknownEditVariable, other ); + return 0; + } + Py_RETURN_NONE; +} + + +PyObject* +Solver_hasEditVariable( Solver* self, PyObject* other ) +{ + if( !Variable::TypeCheck( other ) ) + return cppy::type_error( other, "Variable" ); + Variable* var = reinterpret_cast( other ); + return cppy::incref( self->solver.hasEditVariable( var->variable ) ? Py_True : Py_False ); +} + + +PyObject* +Solver_suggestValue( Solver* self, PyObject* args ) +{ + PyObject* pyvar; + PyObject* pyvalue; + if( !PyArg_ParseTuple( args, "OO", &pyvar, &pyvalue ) ) + return 0; + if( !Variable::TypeCheck( pyvar ) ) + return cppy::type_error( pyvar, "Variable" ); + double value; + if( !convert_to_double( pyvalue, value ) ) + return 0; + Variable* var = reinterpret_cast( pyvar ); + try + { + self->solver.suggestValue( var->variable, value ); + } + catch( const kiwi::UnknownEditVariable& ) + { + PyErr_SetObject( UnknownEditVariable, pyvar ); + return 0; + } + Py_RETURN_NONE; +} + + +PyObject* +Solver_updateVariables( Solver* self ) +{ + self->solver.updateVariables(); + Py_RETURN_NONE; +} + + +PyObject* +Solver_reset( Solver* self ) +{ + self->solver.reset(); + Py_RETURN_NONE; +} + + +PyObject* +Solver_dump( Solver* self ) +{ + cppy::ptr dump_str( PyUnicode_FromString( self->solver.dumps().c_str() ) ); + PyObject_Print( dump_str.get(), stdout, 0 ); + Py_RETURN_NONE; +} + +PyObject* +Solver_dumps( Solver* self ) +{ + return PyUnicode_FromString( self->solver.dumps().c_str() ); +} + +static PyMethodDef +Solver_methods[] = { + { "addConstraint", ( PyCFunction )Solver_addConstraint, METH_O, + "Add a constraint to the solver." }, + { "removeConstraint", ( PyCFunction )Solver_removeConstraint, METH_O, + "Remove a constraint from the solver." }, + { "hasConstraint", ( PyCFunction )Solver_hasConstraint, METH_O, + "Check whether the solver contains a constraint." }, + { "addEditVariable", ( PyCFunction )Solver_addEditVariable, METH_VARARGS, + "Add an edit variable to the solver." }, + { "removeEditVariable", ( PyCFunction )Solver_removeEditVariable, METH_O, + "Remove an edit variable from the solver." }, + { "hasEditVariable", ( PyCFunction )Solver_hasEditVariable, METH_O, + "Check whether the solver contains an edit variable." }, + { "suggestValue", ( PyCFunction )Solver_suggestValue, METH_VARARGS, + "Suggest a desired value for an edit variable." }, + { "updateVariables", ( PyCFunction )Solver_updateVariables, METH_NOARGS, + "Update the values of the solver variables." }, + { "reset", ( PyCFunction )Solver_reset, METH_NOARGS, + "Reset the solver to the initial empty starting condition." }, + { "dump", ( PyCFunction )Solver_dump, METH_NOARGS, + "Dump a representation of the solver internals to stdout." }, + { "dumps", ( PyCFunction )Solver_dumps, METH_NOARGS, + "Dump a representation of the solver internals to a string." }, + { 0 } // sentinel +}; + + +static PyType_Slot Solver_Type_slots[] = { + { Py_tp_dealloc, void_cast( Solver_dealloc ) }, /* tp_dealloc */ + { Py_tp_methods, void_cast( Solver_methods ) }, /* tp_methods */ + { Py_tp_new, void_cast( Solver_new ) }, /* tp_new */ + { Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */ + { Py_tp_free, void_cast( PyObject_Del ) }, /* tp_free */ + { 0, 0 }, +}; + + +} // namespace + + +// Initialize static variables (otherwise the compiler eliminates them) +PyTypeObject* Solver::TypeObject = NULL; + + +PyType_Spec Solver::TypeObject_Spec = { + "kiwisolver.Solver", /* tp_name */ + sizeof( Solver ), /* tp_basicsize */ + 0, /* tp_itemsize */ + Py_TPFLAGS_DEFAULT| + Py_TPFLAGS_BASETYPE, /* tp_flags */ + Solver_Type_slots /* slots */ +}; + + +bool Solver::Ready() +{ + // The reference will be handled by the module to which we will add the type + TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) ); + if( !TypeObject ) + { + return false; + } + return true; +} + + +PyObject* DuplicateConstraint; + +PyObject* UnsatisfiableConstraint; + +PyObject* UnknownConstraint; + +PyObject* DuplicateEditVariable; + +PyObject* UnknownEditVariable; + +PyObject* BadRequiredStrength; + + +bool init_exceptions() +{ + cppy::ptr mod( PyImport_ImportModule( "kiwisolver.exceptions" ) ); + if( !mod ) + { + return false; + } + + DuplicateConstraint = mod.getattr( "DuplicateConstraint" ); + if( !DuplicateConstraint ) + { + return false; + } + + UnsatisfiableConstraint = mod.getattr( "UnsatisfiableConstraint" ); + if( !UnsatisfiableConstraint ) + { + return false; + } + + UnknownConstraint = mod.getattr( "UnknownConstraint" ); + if( !UnknownConstraint ) + { + return false; + } + + DuplicateEditVariable = mod.getattr( "DuplicateEditVariable" ); + if( !DuplicateEditVariable ) + { + return false; + } + + UnknownEditVariable = mod.getattr( "UnknownEditVariable" ); + if( !UnknownEditVariable ) + { + return false; + } + + BadRequiredStrength = mod.getattr( "BadRequiredStrength" ); + if( !BadRequiredStrength ) + { + return false; + } + + return true; +} + +} // namespace diff --git a/py/src/strength.cpp b/py/src/strength.cpp new file mode 100644 index 0000000..e77ba2d --- /dev/null +++ b/py/src/strength.cpp @@ -0,0 +1,149 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#include +#include +#include "util.h" + + +#ifdef __clang__ +#pragma clang diagnostic ignored "-Wdeprecated-writable-strings" +#endif + +#ifdef __GNUC__ +#pragma GCC diagnostic ignored "-Wwrite-strings" +#endif + + +namespace kiwisolver +{ + + +namespace +{ + + +void +strength_dealloc( PyObject* self ) +{ + Py_TYPE( self )->tp_free( self ); +} + + +PyObject* +strength_weak( strength* self ) +{ + return PyFloat_FromDouble( kiwi::strength::weak ); +} + + +PyObject* +strength_medium( strength* self ) +{ + return PyFloat_FromDouble( kiwi::strength::medium ); +} + + +PyObject* +strength_strong( strength* self ) +{ + return PyFloat_FromDouble( kiwi::strength::strong ); +} + + +PyObject* +strength_required( strength* self ) +{ + return PyFloat_FromDouble( kiwi::strength::required ); +} + + +PyObject* +strength_create( strength* self, PyObject* args ) +{ + PyObject* pya; + PyObject* pyb; + PyObject* pyc; + PyObject* pyw = 0; + if( !PyArg_ParseTuple( args, "OOO|O", &pya, &pyb, &pyc, &pyw ) ) + return 0; + double a, b, c; + double w = 1.0; + if( !convert_to_double( pya, a ) ) + return 0; + if( !convert_to_double( pyb, b ) ) + return 0; + if( !convert_to_double( pyc, c ) ) + return 0; + if( pyw && !convert_to_double( pyw, w ) ) + return 0; + return PyFloat_FromDouble( kiwi::strength::create( a, b, c, w ) ); +} + + +static PyGetSetDef +strength_getset[] = { + { "weak", ( getter )strength_weak, 0, + "The predefined weak strength." }, + { "medium", ( getter )strength_medium, 0, + "The predefined medium strength." }, + { "strong", ( getter )strength_strong, 0, + "The predefined strong strength." }, + { "required", ( getter )strength_required, 0, + "The predefined required strength." }, + { 0 } // sentinel +}; + + +static PyMethodDef +strength_methods[] = { + { "create", ( PyCFunction )strength_create, METH_VARARGS, + "Create a strength from constituent values and optional weight." }, + { 0 } // sentinel +}; + + + +static PyType_Slot strength_Type_slots[] = { + { Py_tp_dealloc, void_cast( strength_dealloc ) }, /* tp_dealloc */ + { Py_tp_getset, void_cast( strength_getset ) }, /* tp_getset */ + { Py_tp_methods, void_cast( strength_methods ) }, /* tp_methods */ + { Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */ + { Py_tp_free, void_cast( PyObject_Del ) }, /* tp_free */ + { 0, 0 }, +}; + + +} // namespace + + +// Initialize static variables (otherwise the compiler eliminates them) +PyTypeObject* strength::TypeObject = NULL; + + +PyType_Spec strength::TypeObject_Spec = { + "kiwisolver.Strength", /* tp_name */ + sizeof( strength ), /* tp_basicsize */ + 0, /* tp_itemsize */ + Py_TPFLAGS_DEFAULT, /* tp_flags */ + strength_Type_slots /* slots */ +}; + + +bool strength::Ready() +{ + // The reference will be handled by the module to which we will add the type + TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) ); + if( !TypeObject ) + { + return false; + } + return true; +} + + +} // namespace diff --git a/py/src/symbolics.h b/py/src/symbolics.h new file mode 100644 index 0000000..69ea540 --- /dev/null +++ b/py/src/symbolics.h @@ -0,0 +1,618 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include "types.h" +#include "util.h" + + +namespace kiwisolver +{ + +template +struct UnaryInvoke +{ + PyObject* operator()( PyObject* value ) + { + return Op()( reinterpret_cast( value ) ); + } +}; + + +template +struct BinaryInvoke +{ + PyObject* operator()( PyObject* first, PyObject* second ) + { + if( T::TypeCheck( first ) ) + return invoke( reinterpret_cast( first ), second ); + return invoke( reinterpret_cast( second ), first ); + } + + struct Normal + { + template + PyObject* operator()( T* primary, U secondary ) + { + return Op()( primary, secondary ); + } + }; + + struct Reverse + { + template + PyObject* operator()( T* primary, U secondary ) + { + return Op()( secondary, primary ); + } + }; + + template + PyObject* invoke( T* primary, PyObject* secondary ) + { + if( Expression::TypeCheck( secondary ) ) + return Invk()( primary, reinterpret_cast( secondary ) ); + if( Term::TypeCheck( secondary ) ) + return Invk()( primary, reinterpret_cast( secondary ) ); + if( Variable::TypeCheck( secondary ) ) + return Invk()( primary, reinterpret_cast( secondary ) ); + if( PyFloat_Check( secondary ) ) + return Invk()( primary, PyFloat_AS_DOUBLE( secondary ) ); + if( PyLong_Check( secondary ) ) + { + double v = PyLong_AsDouble( secondary ); + if( v == -1 && PyErr_Occurred() ) + return 0; + return Invk()( primary, v ); + } + Py_RETURN_NOTIMPLEMENTED; + } +}; + + +struct BinaryMul +{ + template + PyObject* operator()( T first, U second ) + { + Py_RETURN_NOTIMPLEMENTED; + } +}; + + +template<> inline +PyObject* BinaryMul::operator()( Variable* first, double second ) +{ + PyObject* pyterm = PyType_GenericNew( Term::TypeObject, 0, 0 ); + if( !pyterm ) + return 0; + Term* term = reinterpret_cast( pyterm ); + term->variable = cppy::incref( pyobject_cast( first ) ); + term->coefficient = second; + return pyterm; +} + + +template<> inline +PyObject* BinaryMul::operator()( Term* first, double second ) +{ + PyObject* pyterm = PyType_GenericNew( Term::TypeObject, 0, 0 ); + if( !pyterm ) + return 0; + Term* term = reinterpret_cast( pyterm ); + term->variable = cppy::incref( first->variable ); + term->coefficient = first->coefficient * second; + return pyterm; +} + + +template<> inline +PyObject* BinaryMul::operator()( Expression* first, double second ) +{ + cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) ); + if( !pyexpr ) + return 0; + Expression* expr = reinterpret_cast( pyexpr.get() ); + cppy::ptr terms( PyTuple_New( PyTuple_GET_SIZE( first->terms ) ) ); + if( !terms ) + return 0; + Py_ssize_t end = PyTuple_GET_SIZE( first->terms ); + for( Py_ssize_t i = 0; i < end; ++i ) // memset 0 for safe error return + PyTuple_SET_ITEM( terms.get(), i, 0 ); + for( Py_ssize_t i = 0; i < end; ++i ) + { + PyObject* item = PyTuple_GET_ITEM( first->terms, i ); + PyObject* term = BinaryMul()( reinterpret_cast( item ), second ); + if( !term ) + return 0; + PyTuple_SET_ITEM( terms.get(), i, term ); + } + expr->terms = terms.release(); + expr->constant = first->constant * second; + return pyexpr.release(); +} + + +template<> inline +PyObject* BinaryMul::operator()( double first, Variable* second ) +{ + return operator()( second, first ); +} + + +template<> inline +PyObject* BinaryMul::operator()( double first, Term* second ) +{ + return operator()( second, first ); +} + + +template<> inline +PyObject* BinaryMul::operator()( double first, Expression* second ) +{ + return operator()( second, first ); +} + + +struct BinaryDiv +{ + template + PyObject* operator()( T first, U second ) + { + Py_RETURN_NOTIMPLEMENTED; + } +}; + + +template<> inline +PyObject* BinaryDiv::operator()( Variable* first, double second ) +{ + if( second == 0.0 ) + { + PyErr_SetString( PyExc_ZeroDivisionError, "float division by zero" ); + return 0; + } + return BinaryMul()( first, 1.0 / second ); +} + + +template<> inline +PyObject* BinaryDiv::operator()( Term* first, double second ) +{ + if( second == 0.0 ) + { + PyErr_SetString( PyExc_ZeroDivisionError, "float division by zero" ); + return 0; + } + return BinaryMul()( first, 1.0 / second ); +} + + +template<> inline +PyObject* BinaryDiv::operator()( Expression* first, double second ) +{ + if( second == 0.0 ) + { + PyErr_SetString( PyExc_ZeroDivisionError, "float division by zero" ); + return 0; + } + return BinaryMul()( first, 1.0 / second ); +} + + +struct UnaryNeg +{ + template + PyObject* operator()( T value ) + { + Py_RETURN_NOTIMPLEMENTED; + } +}; + + +template<> inline +PyObject* UnaryNeg::operator()( Variable* value ) +{ + return BinaryMul()( value, -1.0 ); +} + + +template<> inline +PyObject* UnaryNeg::operator()( Term* value ) +{ + return BinaryMul()( value, -1.0 ); +} + + +template<> inline +PyObject* UnaryNeg::operator()( Expression* value ) +{ + return BinaryMul()( value, -1.0 ); +} + + +struct BinaryAdd +{ + template + PyObject* operator()( T first, U second ) + { + Py_RETURN_NOTIMPLEMENTED; + } +}; + + +template<> inline +PyObject* BinaryAdd::operator()( Expression* first, Expression* second ) +{ + cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) ); + if( !pyexpr ) + return 0; + Expression* expr = reinterpret_cast( pyexpr.get() ); + expr->constant = first->constant + second->constant; + expr->terms = PySequence_Concat( first->terms, second->terms ); + if( !expr->terms ) + return 0; + return pyexpr.release(); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Expression* first, Term* second ) +{ + cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) ); + if( !pyexpr ) + return 0; + PyObject* terms = PyTuple_New( PyTuple_GET_SIZE( first->terms ) + 1 ); + if( !terms ) + return 0; + Py_ssize_t end = PyTuple_GET_SIZE( first->terms ); + for( Py_ssize_t i = 0; i < end; ++i ) + { + PyObject* item = PyTuple_GET_ITEM( first->terms, i ); + PyTuple_SET_ITEM( terms, i, cppy::incref( item ) ); + } + PyTuple_SET_ITEM( terms, end, cppy::incref( pyobject_cast( second ) ) ); + Expression* expr = reinterpret_cast( pyexpr.get() ); + expr->terms = terms; + expr->constant = first->constant; + return pyexpr.release(); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Expression* first, Variable* second ) +{ + cppy::ptr temp( BinaryMul()( second, 1.0 ) ); + if( !temp ) + return 0; + return operator()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Expression* first, double second ) +{ + cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) ); + if( !pyexpr ) + return 0; + Expression* expr = reinterpret_cast( pyexpr.get() ); + expr->terms = cppy::incref( first->terms ); + expr->constant = first->constant + second; + return pyexpr.release(); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Term* first, double second ) +{ + cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) ); + if( !pyexpr ) + return 0; + Expression* expr = reinterpret_cast( pyexpr.get() ); + expr->constant = second; + expr->terms = PyTuple_Pack( 1, first ); + if( !expr->terms ) + return 0; + return pyexpr.release(); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Term* first, Expression* second ) +{ + return operator()( second, first ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Term* first, Term* second ) +{ + cppy::ptr pyexpr( PyType_GenericNew( Expression::TypeObject, 0, 0 ) ); + if( !pyexpr ) + return 0; + Expression* expr = reinterpret_cast( pyexpr.get() ); + expr->constant = 0.0; + expr->terms = PyTuple_Pack( 2, first, second ); + if( !expr->terms ) + return 0; + return pyexpr.release(); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Term* first, Variable* second ) +{ + cppy::ptr temp( BinaryMul()( second, 1.0 ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Variable* first, double second ) +{ + cppy::ptr temp( BinaryMul()( first, 1.0 ) ); + if( !temp ) + return 0; + return operator()( reinterpret_cast( temp.get() ), second ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Variable* first, Variable* second ) +{ + cppy::ptr temp( BinaryMul()( first, 1.0 ) ); + if( !temp ) + return 0; + return operator()( reinterpret_cast( temp.get() ), second ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Variable* first, Term* second ) +{ + cppy::ptr temp( BinaryMul()( first, 1.0 ) ); + if( !temp ) + return 0; + return operator()( reinterpret_cast( temp.get() ), second ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( Variable* first, Expression* second ) +{ + cppy::ptr temp( BinaryMul()( first, 1.0 ) ); + if( !temp ) + return 0; + return operator()( reinterpret_cast( temp.get() ), second ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( double first, Variable* second ) +{ + return operator()( second, first ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( double first, Term* second ) +{ + return operator()( second, first ); +} + + +template<> inline +PyObject* BinaryAdd::operator()( double first, Expression* second ) +{ + return operator()( second, first ); +} + + +struct BinarySub +{ + template + PyObject* operator()( T first, U second ) + { + Py_RETURN_NOTIMPLEMENTED; + } +}; + + +template<> inline +PyObject* BinarySub::operator()( Variable* first, double second ) +{ + return BinaryAdd()( first, -second ); +} + + +template<> inline +PyObject* BinarySub::operator()( Variable* first, Variable* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Variable* first, Term* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Variable* first, Expression* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Term* first, double second ) +{ + return BinaryAdd()( first, -second ); +} + + +template<> inline +PyObject* BinarySub::operator()( Term* first, Variable* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Term* first, Term* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Term* first, Expression* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Expression* first, double second ) +{ + return BinaryAdd()( first, -second ); +} + + +template<> inline +PyObject* BinarySub::operator()( Expression* first, Variable* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Expression* first, Term* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( Expression* first, Expression* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( double first, Variable* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( double first, Term* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template<> inline +PyObject* BinarySub::operator()( double first, Expression* second ) +{ + cppy::ptr temp( UnaryNeg()( second ) ); + if( !temp ) + return 0; + return BinaryAdd()( first, reinterpret_cast( temp.get() ) ); +} + + +template +PyObject* makecn( T first, U second, kiwi::RelationalOperator op ) +{ + cppy::ptr pyexpr( BinarySub()( first, second ) ); + if( !pyexpr ) + return 0; + cppy::ptr pycn( PyType_GenericNew( Constraint::TypeObject, 0, 0 ) ); + if( !pycn ) + return 0; + Constraint* cn = reinterpret_cast( pycn.get() ); + cn->expression = reduce_expression( pyexpr.get() ); + if( !cn->expression ) + return 0; + kiwi::Expression expr( convert_to_kiwi_expression( cn->expression ) ); + new( &cn->constraint ) kiwi::Constraint( expr, op, kiwi::strength::required ); + return pycn.release(); +} + + +struct CmpEQ +{ + template + PyObject* operator()( T first, U second ) + { + return makecn( first, second, kiwi::OP_EQ ); + } +}; + + +struct CmpLE +{ + template + PyObject* operator()( T first, U second ) + { + return makecn( first, second, kiwi::OP_LE ); + } +}; + + +struct CmpGE +{ + template + PyObject* operator()( T first, U second ) + { + return makecn( first, second, kiwi::OP_GE ); + } +}; + + +} // namespace kiwisolver diff --git a/py/src/term.cpp b/py/src/term.cpp new file mode 100644 index 0000000..35b0a0e --- /dev/null +++ b/py/src/term.cpp @@ -0,0 +1,229 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#include +#include +#include "symbolics.h" +#include "types.h" +#include "util.h" + + +namespace kiwisolver +{ + + +namespace +{ + + +PyObject* +Term_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) +{ + static const char *kwlist[] = { "variable", "coefficient", 0 }; + PyObject* pyvar; + PyObject* pycoeff = 0; + if( !PyArg_ParseTupleAndKeywords( + args, kwargs, "O|O:__new__", const_cast( kwlist ), + &pyvar, &pycoeff ) ) + return 0; + if( !Variable::TypeCheck( pyvar ) ) + return cppy::type_error( pyvar, "Variable" ); + double coefficient = 1.0; + if( pycoeff && !convert_to_double( pycoeff, coefficient ) ) + return 0; + PyObject* pyterm = PyType_GenericNew( type, args, kwargs ); + if( !pyterm ) + return 0; + Term* self = reinterpret_cast( pyterm ); + self->variable = cppy::incref( pyvar ); + self->coefficient = coefficient; + return pyterm; +} + + +void +Term_clear( Term* self ) +{ + Py_CLEAR( self->variable ); +} + + +int +Term_traverse( Term* self, visitproc visit, void* arg ) +{ + Py_VISIT( self->variable ); +#if PY_VERSION_HEX >= 0x03090000 + // This was not needed before Python 3.9 (Python issue 35810 and 40217) + Py_VISIT(Py_TYPE(self)); +#endif + return 0; +} + + +void +Term_dealloc( Term* self ) +{ + PyObject_GC_UnTrack( self ); + Term_clear( self ); + Py_TYPE( self )->tp_free( pyobject_cast( self ) ); +} + + +PyObject* +Term_repr( Term* self ) +{ + std::stringstream stream; + stream << self->coefficient << " * "; + stream << reinterpret_cast( self->variable )->variable.name(); + return PyUnicode_FromString( stream.str().c_str() ); +} + + +PyObject* +Term_variable( Term* self ) +{ + return cppy::incref( self->variable ); +} + + +PyObject* +Term_coefficient( Term* self ) +{ + return PyFloat_FromDouble( self->coefficient ); +} + + +PyObject* +Term_value( Term* self ) +{ + Variable* pyvar = reinterpret_cast( self->variable ); + return PyFloat_FromDouble( self->coefficient * pyvar->variable.value() ); +} + + +PyObject* +Term_add( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Term_sub( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Term_mul( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Term_div( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Term_neg( PyObject* value ) +{ + return UnaryInvoke()( value ); +} + + +PyObject* +Term_richcmp( PyObject* first, PyObject* second, int op ) +{ + switch( op ) + { + case Py_EQ: + return BinaryInvoke()( first, second ); + case Py_LE: + return BinaryInvoke()( first, second ); + case Py_GE: + return BinaryInvoke()( first, second ); + default: + break; + } + PyErr_Format( + PyExc_TypeError, + "unsupported operand type(s) for %s: " + "'%.100s' and '%.100s'", + pyop_str( op ), + Py_TYPE( first )->tp_name, + Py_TYPE( second )->tp_name + ); + return 0; +} + + +static PyMethodDef +Term_methods[] = { + { "variable", ( PyCFunction )Term_variable, METH_NOARGS, + "Get the variable for the term." }, + { "coefficient", ( PyCFunction )Term_coefficient, METH_NOARGS, + "Get the coefficient for the term." }, + { "value", ( PyCFunction )Term_value, METH_NOARGS, + "Get the value for the term." }, + { 0 } // sentinel +}; + + +static PyType_Slot Term_Type_slots[] = { + { Py_tp_dealloc, void_cast( Term_dealloc ) }, /* tp_dealloc */ + { Py_tp_traverse, void_cast( Term_traverse ) }, /* tp_traverse */ + { Py_tp_clear, void_cast( Term_clear ) }, /* tp_clear */ + { Py_tp_repr, void_cast( Term_repr ) }, /* tp_repr */ + { Py_tp_richcompare, void_cast( Term_richcmp ) }, /* tp_richcompare */ + { Py_tp_methods, void_cast( Term_methods ) }, /* tp_methods */ + { Py_tp_new, void_cast( Term_new ) }, /* tp_new */ + { Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */ + { Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */ + { Py_nb_add, void_cast( Term_add ) }, /* nb_add */ + { Py_nb_subtract, void_cast( Term_sub ) }, /* nb_subatract */ + { Py_nb_multiply, void_cast( Term_mul ) }, /* nb_multiply */ + { Py_nb_negative, void_cast( Term_neg ) }, /* nb_negative */ + { Py_nb_true_divide, void_cast( Term_div ) }, /* nb_true_divide */ + { 0, 0 }, +}; + + +} // namespace + + +// Initialize static variables (otherwise the compiler eliminates them) +PyTypeObject* Term::TypeObject = NULL; + + +PyType_Spec Term::TypeObject_Spec = { + "kiwisolver.Term", /* tp_name */ + sizeof( Term ), /* tp_basicsize */ + 0, /* tp_itemsize */ + Py_TPFLAGS_DEFAULT| + Py_TPFLAGS_HAVE_GC| + Py_TPFLAGS_BASETYPE, /* tp_flags */ + Term_Type_slots /* slots */ +}; + + +bool Term::Ready() +{ + // The reference will be handled by the module to which we will add the type + TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) ); + if( !TypeObject ) + { + return false; + } + return true; +} + +} // namespace kiwisolver diff --git a/py/src/types.h b/py/src/types.h new file mode 100644 index 0000000..afd5b8d --- /dev/null +++ b/py/src/types.h @@ -0,0 +1,138 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include + + +namespace kiwisolver +{ + +extern PyObject* DuplicateConstraint; + +extern PyObject* UnsatisfiableConstraint; + +extern PyObject* UnknownConstraint; + +extern PyObject* DuplicateEditVariable; + +extern PyObject* UnknownEditVariable; + +extern PyObject* BadRequiredStrength; + + +struct strength +{ + PyObject_HEAD; + + static PyType_Spec TypeObject_Spec; + + static PyTypeObject* TypeObject; + + static bool Ready(); +}; + + +struct Variable +{ + PyObject_HEAD + PyObject* context; + kiwi::Variable variable; + + static PyType_Spec TypeObject_Spec; + + static PyTypeObject* TypeObject; + + static bool Ready(); + + static bool TypeCheck( PyObject* obj ) + { + return PyObject_TypeCheck( obj, TypeObject ) != 0; + } +}; + + +struct Term +{ + PyObject_HEAD + PyObject* variable; + double coefficient; + + static PyType_Spec TypeObject_Spec; + + static PyTypeObject* TypeObject; + + static bool Ready(); + + static bool TypeCheck( PyObject* obj ) + { + return PyObject_TypeCheck( obj, TypeObject ) != 0; + } +}; + + +struct Expression +{ + PyObject_HEAD + PyObject* terms; + double constant; + + static PyType_Spec TypeObject_Spec; + + static PyTypeObject* TypeObject; + + static bool Ready(); + + static bool TypeCheck( PyObject* obj ) + { + return PyObject_TypeCheck( obj, TypeObject ) != 0; + } +}; + + +struct Constraint +{ + PyObject_HEAD + PyObject* expression; + kiwi::Constraint constraint; + + static PyType_Spec TypeObject_Spec; + + static PyTypeObject* TypeObject; + + static bool Ready(); + + static bool TypeCheck( PyObject* obj ) + { + return PyObject_TypeCheck( obj, TypeObject ) != 0; + } +}; + + +struct Solver +{ + PyObject_HEAD + kiwi::Solver solver; + + static PyType_Spec TypeObject_Spec; + + static PyTypeObject* TypeObject; + + static bool Ready(); + + static bool TypeCheck( PyObject* obj ) + { + return PyObject_TypeCheck( obj, TypeObject ) != 0; + } +}; + + +bool init_exceptions(); + + +} // namespace kiwisolver diff --git a/py/src/util.h b/py/src/util.h new file mode 100644 index 0000000..4b00fa7 --- /dev/null +++ b/py/src/util.h @@ -0,0 +1,203 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#pragma once +#include +#include +#include +#include +#include "types.h" + + +namespace kiwisolver +{ + +inline bool +convert_to_double( PyObject* obj, double& out ) +{ + if( PyFloat_Check( obj ) ) + { + out = PyFloat_AS_DOUBLE( obj ); + return true; + } + if( PyLong_Check( obj ) ) + { + out = PyLong_AsDouble( obj ); + if( out == -1.0 && PyErr_Occurred() ) + return false; + return true; + } + cppy::type_error( obj, "float, int, or long" ); + return false; +} + + +inline bool +convert_pystr_to_str( PyObject* value, std::string& out ) +{ + out = PyUnicode_AsUTF8( value ); + return true; +} + + +inline bool +convert_to_strength( PyObject* value, double& out ) +{ + if( PyUnicode_Check( value ) ) + { + std::string str; + if( !convert_pystr_to_str( value, str ) ) + return false; + if( str == "required" ) + out = kiwi::strength::required; + else if( str == "strong" ) + out = kiwi::strength::strong; + else if( str == "medium" ) + out = kiwi::strength::medium; + else if( str == "weak" ) + out = kiwi::strength::weak; + else + { + PyErr_Format( + PyExc_ValueError, + "string strength must be 'required', 'strong', 'medium', " + "or 'weak', not '%s'", + str.c_str() + ); + return false; + } + return true; + } + if( !convert_to_double( value, out ) ) + return false; + return true; +} + + +inline bool +convert_to_relational_op( PyObject* value, kiwi::RelationalOperator& out ) +{ + if( !PyUnicode_Check( value ) ) + { + cppy::type_error( value, "str" ); + return false; + } + std::string str; + if( !convert_pystr_to_str( value, str ) ) + return false; + if( str == "==" ) + out = kiwi::OP_EQ; + else if( str == "<=" ) + out = kiwi::OP_LE; + else if( str == ">=" ) + out = kiwi::OP_GE; + else + { + PyErr_Format( + PyExc_ValueError, + "relational operator must be '==', '<=', or '>=', not '%s'", + str.c_str() + ); + return false; + } + return true; +} + + +inline PyObject* +make_terms( const std::map& coeffs ) +{ + typedef std::map::const_iterator iter_t; + cppy::ptr terms( PyTuple_New( coeffs.size() ) ); + if( !terms ) + return 0; + Py_ssize_t size = PyTuple_GET_SIZE( terms.get() ); + for( Py_ssize_t i = 0; i < size; ++i ) // zero tuple for safe early return + PyTuple_SET_ITEM( terms.get(), i, 0 ); + Py_ssize_t i = 0; + iter_t it = coeffs.begin(); + iter_t end = coeffs.end(); + for( ; it != end; ++it, ++i ) + { + PyObject* pyterm = PyType_GenericNew( Term::TypeObject, 0, 0 ); + if( !pyterm ) + return 0; + Term* term = reinterpret_cast( pyterm ); + term->variable = cppy::incref( it->first ); + term->coefficient = it->second; + PyTuple_SET_ITEM( terms.get(), i, pyterm ); + } + return terms.release(); +} + + +inline PyObject* +reduce_expression( PyObject* pyexpr ) // pyexpr must be an Expression +{ + Expression* expr = reinterpret_cast( pyexpr ); + std::map coeffs; + Py_ssize_t size = PyTuple_GET_SIZE( expr->terms ); + for( Py_ssize_t i = 0; i < size; ++i ) + { + PyObject* item = PyTuple_GET_ITEM( expr->terms, i ); + Term* term = reinterpret_cast( item ); + coeffs[ term->variable ] += term->coefficient; + } + cppy::ptr terms( make_terms( coeffs ) ); + if( !terms ) + return 0; + PyObject* pynewexpr = PyType_GenericNew( Expression::TypeObject, 0, 0 ); + if( !pynewexpr ) + return 0; + Expression* newexpr = reinterpret_cast( pynewexpr ); + newexpr->terms = terms.release(); + newexpr->constant = expr->constant; + return pynewexpr; +} + + +inline kiwi::Expression +convert_to_kiwi_expression( PyObject* pyexpr ) // pyexpr must be an Expression +{ + Expression* expr = reinterpret_cast( pyexpr ); + std::vector kterms; + Py_ssize_t size = PyTuple_GET_SIZE( expr->terms ); + for( Py_ssize_t i = 0; i < size; ++i ) + { + PyObject* item = PyTuple_GET_ITEM( expr->terms, i ); + Term* term = reinterpret_cast( item ); + Variable* var = reinterpret_cast( term->variable ); + kterms.push_back( kiwi::Term( var->variable, term->coefficient ) ); + } + return kiwi::Expression( kterms, expr->constant ); +} + + +inline const char* +pyop_str( int op ) +{ + switch( op ) + { + case Py_LT: + return "<"; + case Py_LE: + return "<="; + case Py_EQ: + return "=="; + case Py_NE: + return "!="; + case Py_GT: + return ">"; + case Py_GE: + return ">="; + default: + return ""; + } +} + + +} // namespace kiwisolver diff --git a/py/src/variable.cpp b/py/src/variable.cpp new file mode 100644 index 0000000..b00f809 --- /dev/null +++ b/py/src/variable.cpp @@ -0,0 +1,270 @@ +/*----------------------------------------------------------------------------- +| Copyright (c) 2013-2019, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +|----------------------------------------------------------------------------*/ +#include +#include +#include "symbolics.h" +#include "types.h" +#include "util.h" + + +namespace kiwisolver +{ + + +namespace +{ + + +PyObject* +Variable_new( PyTypeObject* type, PyObject* args, PyObject* kwargs ) +{ + static const char *kwlist[] = { "name", "context", 0 }; + PyObject* context = 0; + PyObject* name = 0; + + if( !PyArg_ParseTupleAndKeywords( + args, kwargs, "|OO:__new__", const_cast( kwlist ), + &name, &context ) ) + return 0; + + cppy::ptr pyvar( PyType_GenericNew( type, args, kwargs ) ); + if( !pyvar ) + return 0; + + Variable* self = reinterpret_cast( pyvar.get() ); + self->context = cppy::xincref( context ); + + if( name != 0 ) + { + if( !PyUnicode_Check( name ) ) + return cppy::type_error( name, "str" ); + std::string c_name; + if( !convert_pystr_to_str(name, c_name) ) + return 0; // LCOV_EXCL_LINE + new( &self->variable ) kiwi::Variable( c_name ); + } + else + { + new( &self->variable ) kiwi::Variable(); + } + + return pyvar.release(); +} + + +void +Variable_clear( Variable* self ) +{ + Py_CLEAR( self->context ); +} + + +int +Variable_traverse( Variable* self, visitproc visit, void* arg ) +{ + Py_VISIT( self->context ); +#if PY_VERSION_HEX >= 0x03090000 + // This was not needed before Python 3.9 (Python issue 35810 and 40217) + Py_VISIT(Py_TYPE(self)); +#endif + return 0; +} + + +void +Variable_dealloc( Variable* self ) +{ + PyObject_GC_UnTrack( self ); + Variable_clear( self ); + self->variable.~Variable(); + Py_TYPE( self )->tp_free( pyobject_cast( self ) ); +} + + +PyObject* +Variable_repr( Variable* self ) +{ + return PyUnicode_FromString( self->variable.name().c_str() ); +} + + +PyObject* +Variable_name( Variable* self ) +{ + return PyUnicode_FromString( self->variable.name().c_str() ); +} + + +PyObject* +Variable_setName( Variable* self, PyObject* pystr ) +{ + if( !PyUnicode_Check( pystr ) ) + return cppy::type_error( pystr, "str" ); + std::string str; + if( !convert_pystr_to_str( pystr, str ) ) + return 0; + self->variable.setName( str ); + Py_RETURN_NONE; +} + + +PyObject* +Variable_context( Variable* self ) +{ + if( self->context ) + return cppy::incref( self->context ); + Py_RETURN_NONE; +} + + +PyObject* +Variable_setContext( Variable* self, PyObject* value ) +{ + if( value != self->context ) + { + PyObject* temp = self->context; + self->context = cppy::incref( value ); + Py_XDECREF( temp ); + } + Py_RETURN_NONE; +} + + +PyObject* +Variable_value( Variable* self ) +{ + return PyFloat_FromDouble( self->variable.value() ); +} + + +PyObject* +Variable_add( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Variable_sub( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Variable_mul( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Variable_div( PyObject* first, PyObject* second ) +{ + return BinaryInvoke()( first, second ); +} + + +PyObject* +Variable_neg( PyObject* value ) +{ + return UnaryInvoke()( value ); +} + + +PyObject* +Variable_richcmp( PyObject* first, PyObject* second, int op ) +{ + switch( op ) + { + case Py_EQ: + return BinaryInvoke()( first, second ); + case Py_LE: + return BinaryInvoke()( first, second ); + case Py_GE: + return BinaryInvoke()( first, second ); + default: + break; + } + PyErr_Format( + PyExc_TypeError, + "unsupported operand type(s) for %s: " + "'%.100s' and '%.100s'", + pyop_str( op ), + Py_TYPE( first )->tp_name, + Py_TYPE( second )->tp_name + ); + return 0; +} + + +static PyMethodDef +Variable_methods[] = { + { "name", ( PyCFunction )Variable_name, METH_NOARGS, + "Get the name of the variable." }, + { "setName", ( PyCFunction )Variable_setName, METH_O, + "Set the name of the variable." }, + { "context", ( PyCFunction )Variable_context, METH_NOARGS, + "Get the context object associated with the variable." }, + { "setContext", ( PyCFunction )Variable_setContext, METH_O, + "Set the context object associated with the variable." }, + { "value", ( PyCFunction )Variable_value, METH_NOARGS, + "Get the current value of the variable." }, + { 0 } // sentinel +}; + + +static PyType_Slot Variable_Type_slots[] = { + { Py_tp_dealloc, void_cast( Variable_dealloc ) }, /* tp_dealloc */ + { Py_tp_traverse, void_cast( Variable_traverse ) }, /* tp_traverse */ + { Py_tp_clear, void_cast( Variable_clear ) }, /* tp_clear */ + { Py_tp_repr, void_cast( Variable_repr ) }, /* tp_repr */ + { Py_tp_richcompare, void_cast( Variable_richcmp ) }, /* tp_richcompare */ + { Py_tp_methods, void_cast( Variable_methods ) }, /* tp_methods */ + { Py_tp_new, void_cast( Variable_new ) }, /* tp_new */ + { Py_tp_alloc, void_cast( PyType_GenericAlloc ) }, /* tp_alloc */ + { Py_tp_free, void_cast( PyObject_GC_Del ) }, /* tp_free */ + { Py_nb_add, void_cast( Variable_add ) }, /* nb_add */ + { Py_nb_subtract, void_cast( Variable_sub ) }, /* nb_subtract */ + { Py_nb_multiply, void_cast( Variable_mul ) }, /* nb_multiply */ + { Py_nb_negative, void_cast( Variable_neg ) }, /* nb_negative */ + { Py_nb_true_divide, void_cast( Variable_div ) }, /* nb_true_divide */ + { 0, 0 }, +}; + + +} // namespace + + +// Initialize static variables (otherwise the compiler eliminates them) +PyTypeObject* Variable::TypeObject = NULL; + + +PyType_Spec Variable::TypeObject_Spec = { + "kiwisolver.Variable", /* tp_name */ + sizeof( Variable ), /* tp_basicsize */ + 0, /* tp_itemsize */ + Py_TPFLAGS_DEFAULT| + Py_TPFLAGS_HAVE_GC| + Py_TPFLAGS_BASETYPE, /* tp_flags */ + Variable_Type_slots /* slots */ +}; + + +bool Variable::Ready() +{ + // The reference will be handled by the module to which we will add the type + TypeObject = pytype_cast( PyType_FromSpec( &TypeObject_Spec ) ); + if( !TypeObject ) + { + return false; + } + return true; +} + +} // namespace kiwisolver diff --git a/py/tests/test_constraint.py b/py/tests/test_constraint.py new file mode 100644 index 0000000..f4fe288 --- /dev/null +++ b/py/tests/test_constraint.py @@ -0,0 +1,84 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2014-2021, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +import gc +import re + +import pytest + +from kiwisolver import Constraint, Variable, strength + + +@pytest.mark.parametrize("op", ("==", "<=", ">=")) +def test_constraint_creation(op) -> None: + """Test constraints creation and methods.""" + v = Variable("foo") + c = Constraint(v + 1, op) + + assert c.strength() == strength.required and c.op() == op + e = c.expression() + t = e.terms() + assert ( + e.constant() == 1 + and len(t) == 1 + and t[0].variable() is v + and t[0].coefficient() == 1 + ) + + constraint_format = r"1 \* foo \+ 1 %s 0 | strength = 1.001e\+[0]+9" % op + assert re.match(constraint_format, str(c)) + + for s in ("weak", "medium", "strong", "required"): + # Not an exact literal... + c = Constraint(v + 1, op, s) # type: ignore + assert c.strength() == getattr(strength, s) + + # Ensure we test garbage collection. + del c + gc.collect() + + +def test_constraint_creation2() -> None: + """Test for errors in Constraints creation.""" + v = Variable("foo") + + with pytest.raises(TypeError) as excinfo: + Constraint(1, "==") # type: ignore + assert "Expression" in excinfo.exconly() + + with pytest.raises(TypeError) as excinfo: + Constraint(v + 1, 1) # type: ignore + assert "str" in excinfo.exconly() + + with pytest.raises(ValueError) as excinfo2: + Constraint(v + 1, "!=") # type: ignore + assert "relational operator" in excinfo2.exconly() + + +@pytest.mark.parametrize("op", ("==", "<=", ">=")) +def test_constraint_repr(op) -> None: + """Test the repr method of a constraint object.""" + v = Variable("foo") + c = Constraint(v + 1, op) + + assert op in repr(c) + + +def test_constraint_or_operator() -> None: + """Test modifying a constraint strength using the | operator.""" + v = Variable("foo") + c = Constraint(v + 1, "==") + + for s in ("weak", "medium", "strong", "required", strength.create(1, 1, 0)): + c2 = c | s # type: ignore + if isinstance(s, str): + assert c2.strength() == getattr(strength, s) + else: + assert c2.strength() == s + + with pytest.raises(ValueError): + c | "unknown" # type: ignore diff --git a/py/tests/test_expression.py b/py/tests/test_expression.py new file mode 100644 index 0000000..28115d3 --- /dev/null +++ b/py/tests/test_expression.py @@ -0,0 +1,271 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2014-2021, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +import gc +import math +import operator +import sys +from typing import Tuple + +import pytest + +from kiwisolver import Constraint, Expression, Term, Variable, strength + + +def test_expression_creation() -> None: + """Test the Term constructor.""" + v = Variable("foo") + v2 = Variable("bar") + v3 = Variable("aux") + e1 = Expression((v * 1, v2 * 2, v3 * 3)) + e2 = Expression((v * 1, v2 * 2, v3 * 3), 10) + + for e, val in ((e1, 0), (e2, 10)): + t = e.terms() + assert ( + len(t) == 3 + and t[0].variable() is v + and t[0].coefficient() == 1 + and t[1].variable() is v2 + and t[1].coefficient() == 2 + and t[2].variable() is v3 + and t[2].coefficient() == 3 + ) + assert e.constant() == val + + assert str(e2) == "1 * foo + 2 * bar + 3 * aux + 10" + + with pytest.raises(TypeError) as excinfo: + Expression((1, v2 * 2, v3 * 3)) # type: ignore + assert "Term" in excinfo.exconly() + + # ensure we test garbage collection. + del e2 + gc.collect() + + +@pytest.fixture() +def expressions(): + """Build expressions, terms and variables to test operations.""" + v = Variable("foo") + v2 = Variable("bar") + t = Term(v, 10) + t2 = Term(v2) + e = t + 5 + e2 = v2 - 10 + return e, e2, t, t2, v, v2 + + +def test_expression_neg( + expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable], +): + """Test neg on an expression.""" + e, _, _, _, v, _ = expressions + + neg = -e + assert isinstance(neg, Expression) + neg_t = neg.terms() + assert ( + len(neg_t) == 1 + and neg_t[0].variable() is v + and neg_t[0].coefficient() == -10 + and neg.constant() == -5 + ) + + +def test_expression_mul( + expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable], +): + """Test expresion multiplication.""" + e, _, _, _, v, _ = expressions + + for mul in (e * 2.0, 2.0 * e): + assert isinstance(mul, Expression) + mul_t = mul.terms() + assert ( + len(mul_t) == 1 + and mul_t[0].variable() is v + and mul_t[0].coefficient() == 20 + and mul.constant() == 10 + ) + + with pytest.raises(TypeError): + e * v # type: ignore + + +def test_expression_div( + expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable], +): + """Test expression divisions.""" + e, _, _, _, v, v2 = expressions + + div = e / 2 + assert isinstance(div, Expression) + div_t = div.terms() + assert ( + len(div_t) == 1 + and div_t[0].variable() is v + and div_t[0].coefficient() == 5 + and div.constant() == 2.5 + ) + + with pytest.raises(TypeError): + e / v2 # type: ignore + + with pytest.raises(ZeroDivisionError): + e / 0 + + +def test_expression_addition( + expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable], +): + """Test expressions additions.""" + e, e2, _, t2, v, v2 = expressions + + for add in (e + 2, 2.0 + e): + assert isinstance(add, Expression) + assert add.constant() == 7 + terms = add.terms() + assert ( + len(terms) == 1 + and terms[0].variable() is v + and terms[0].coefficient() == 10 + ) + + add2 = e + v2 + assert isinstance(add2, Expression) + assert add2.constant() == 5 + terms = add2.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == 10 + and terms[1].variable() is v2 + and terms[1].coefficient() == 1 + ) + + add3 = e + t2 + assert isinstance(add3, Expression) + assert add3.constant() == 5 + terms = add3.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == 10 + and terms[1].variable() is v2 + and terms[1].coefficient() == 1 + ) + + add4 = e + e2 + assert isinstance(add4, Expression) + assert add4.constant() == -5 + terms = add4.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == 10 + and terms[1].variable() is v2 + and terms[1].coefficient() == 1 + ) + + +def test_expressions_substraction( + expressions: Tuple[Expression, Expression, Term, Term, Variable, Variable], +): + """Test expression substraction.""" + e, e2, _, t2, v, v2 = expressions + + for sub, diff in zip((e - 2, 2.0 - e), (3, -3)): + assert isinstance(sub, Expression) + assert sub.constant() == diff + terms = sub.terms() + assert ( + len(terms) == 1 + and terms[0].variable() is v + and terms[0].coefficient() == math.copysign(10, diff) + ) + + for sub2, diff in zip((e - v2, v2 - e), (5, -5)): + assert isinstance(sub2, Expression) + assert sub2.constant() == diff + terms = sub2.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == math.copysign(10, diff) + and terms[1].variable() is v2 + and terms[1].coefficient() == -math.copysign(1, diff) + ) + + for sub3, diff in zip((e - t2, t2 - e), (5, -5)): + assert isinstance(sub3, Expression) + assert sub3.constant() == diff + terms = sub3.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == math.copysign(10, diff) + and terms[1].variable() is v2 + and terms[1].coefficient() == -math.copysign(1, diff) + ) + + sub4 = e - e2 + assert isinstance(sub3, Expression) + assert sub4.constant() == 15 + terms = sub4.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == 10 + and terms[1].variable() is v2 + and terms[1].coefficient() == -1 + ) + + +@pytest.mark.parametrize( + "op, symbol", + [ + (operator.le, "<="), + (operator.eq, "=="), + (operator.ge, ">="), + (operator.lt, None), + (operator.ne, None), + (operator.gt, None), + ], +) +def test_expression_rich_compare_operations(op, symbol) -> None: + """Test using comparison on variables.""" + v1 = Variable("foo") + v2 = Variable("bar") + t1 = Term(v1, 10) + e1 = t1 + 5 + e2 = v2 - 10 + + if symbol is not None: + c = op(e1, e2) + assert isinstance(c, Constraint) + e = c.expression() + t = e.terms() + assert len(t) == 2 + if t[0].variable() is not v1: + t = (t[1], t[0]) + assert ( + t[0].variable() is v1 + and t[0].coefficient() == 10 + and t[1].variable() is v2 + and t[1].coefficient() == -1 + ) + assert e.constant() == 15 + assert c.op() == symbol and c.strength() == strength.required + + else: + with pytest.raises(TypeError) as excinfo: + op(e1, e2) + if "PyPy" in sys.version: + assert "Expression" in excinfo.exconly() + else: + assert "kiwisolver.Expression" in excinfo.exconly() diff --git a/py/tests/test_solver.py b/py/tests/test_solver.py new file mode 100644 index 0000000..1125913 --- /dev/null +++ b/py/tests/test_solver.py @@ -0,0 +1,295 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2014-2022, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +import pytest + +from kiwisolver import ( + BadRequiredStrength, + DuplicateConstraint, + DuplicateEditVariable, + Solver, + UnknownConstraint, + UnknownEditVariable, + UnsatisfiableConstraint, + Variable, +) + + +def test_solver_creation() -> None: + """Test initializing a solver.""" + s = Solver() + assert isinstance(s, Solver) + + with pytest.raises(TypeError): + Solver(Variable()) # type: ignore + + +def test_managing_edit_variable() -> None: + """Test adding/removing edit variables.""" + s = Solver() + v1 = Variable("foo") + v2 = Variable("bar") + + with pytest.raises(TypeError): + s.hasEditVariable(object()) # type: ignore + with pytest.raises(TypeError): + s.addEditVariable(object(), "weak") # type: ignore + with pytest.raises(TypeError): + s.removeEditVariable(object()) # type: ignore + with pytest.raises(TypeError): + s.suggestValue(object(), 10) # type: ignore + + assert not s.hasEditVariable(v1) + s.addEditVariable(v1, "weak") + assert s.hasEditVariable(v1) + with pytest.raises(DuplicateEditVariable) as e: + s.addEditVariable(v1, "medium") + assert e.value.edit_variable is v1 + with pytest.raises(UnknownEditVariable) as e2: + s.removeEditVariable(v2) + assert e2.value.edit_variable is v2 + s.removeEditVariable(v1) + assert not s.hasEditVariable(v1) + + with pytest.raises(BadRequiredStrength): + s.addEditVariable(v1, "required") + + s.addEditVariable(v2, "strong") + assert s.hasEditVariable(v2) + with pytest.raises(UnknownEditVariable) as e3: + s.suggestValue(v1, 10) + assert e3.value.edit_variable is v1 + + s.reset() + assert not s.hasEditVariable(v2) + + +def test_suggesting_values_for_edit_variables() -> None: + """Test suggesting values in different situations.""" + # Suggest value for an edit variable entering a weak equality + s = Solver() + v1 = Variable("foo") + + s.addEditVariable(v1, "medium") + s.addConstraint((v1 == 1) | "weak") + s.suggestValue(v1, 2) + s.updateVariables() + assert v1.value() == 2 + + # Suggest a value for an edit variable entering multiple solver rows + s.reset() + v1 = Variable("foo") + v2 = Variable("bar") + s = Solver() + + s.addEditVariable(v2, "weak") + s.addConstraint(v1 + v2 == 0) + s.addConstraint((v2 <= -1)) + s.addConstraint((v2 >= 0) | "weak") + s.suggestValue(v2, 0) + s.updateVariables() + assert v2.value() <= -1 + + +def test_managing_constraints() -> None: + """Test adding/removing constraints.""" + s = Solver() + v = Variable("foo") + c1 = v >= 1 + c2 = v <= 0 + + with pytest.raises(TypeError): + s.hasConstraint(object()) # type: ignore + with pytest.raises(TypeError): + s.addConstraint(object()) # type: ignore + with pytest.raises(TypeError): + s.removeConstraint(object()) # type: ignore + + assert not s.hasConstraint(c1) + s.addConstraint(c1) + assert s.hasConstraint(c1) + with pytest.raises(DuplicateConstraint) as e: + s.addConstraint(c1) + assert e.value.constraint is c1 + with pytest.raises(UnknownConstraint) as e2: + s.removeConstraint(c2) + assert e2.value.constraint is c2 + with pytest.raises(UnsatisfiableConstraint) as e3: + s.addConstraint(c2) + assert e3.value.constraint is c2 + # XXX need to find how to get an invalid symbol from choose subject + # with pytest.raises(UnsatisfiableConstraint): + # s.addConstraint(c3) + s.removeConstraint(c1) + assert not s.hasConstraint(c1) + + s.addConstraint(c2) + assert s.hasConstraint(c2) + s.reset() + assert not s.hasConstraint(c2) + + +def test_solving_under_constrained_system() -> None: + """Test solving an under constrained system.""" + s = Solver() + v = Variable("foo") + c = 2 * v + 1 >= 0 + s.addEditVariable(v, "weak") + s.addConstraint(c) + s.suggestValue(v, 10) + s.updateVariables() + + assert c.expression().value() == 21 + assert c.expression().terms()[0].value() == 20 + assert c.expression().terms()[0].variable().value() == 10 + + +def test_solving_with_strength() -> None: + """Test solving a system with unsatisfiable non-required constraint.""" + v1 = Variable("foo") + v2 = Variable("bar") + s = Solver() + + s.addConstraint(v1 + v2 == 0) + s.addConstraint(v1 == 10) + s.addConstraint((v2 >= 0) | "weak") + s.updateVariables() + assert v1.value() == 10 and v2.value() == -10 + + s.reset() + + s.addConstraint(v1 + v2 == 0) + s.addConstraint((v1 >= 10) | "medium") + s.addConstraint((v2 == 2) | "strong") + s.updateVariables() + assert v1.value() == -2 and v2.value() == 2 + + +# Typical output solver.dump in the following function. +# the order is not stable. +# """Objective +# --------- +# -2 + 2 * e2 + 1 * s8 + -2 * s10 + +# Tableau +# ------- +# v1 | 1 + 1 * s10 +# e3 | -1 + 1 * e2 + -1 * s10 +# v4 | -1 + -1 * d5 + -1 * s10 +# s6 | -2 + -1 * s10 +# e9 | -1 + 1 * s8 + -1 * s10 + +# Infeasible +# ---------- +# e3 +# e9 + +# Variables +# --------- +# bar = v1 +# foo = v4 + +# Edit Variables +# -------------- +# bar + +# Constraints +# ----------- +# 1 * bar + -0 >= 0 | strength = 1 +# 1 * bar + 1 <= 0 | strength = 1.001e+09 +# 1 * foo + 1 * bar + 0 == 0 | strength = 1.001e+09 +# 1 * bar + 0 == 0 | strength = 1 + +# """ + + +def test_dumping_solver(capsys) -> None: + """Test dumping the solver internal to stdout.""" + v1 = Variable("foo") + v2 = Variable("bar") + s = Solver() + + s.addEditVariable(v2, "weak") + s.addConstraint(v1 + v2 == 0) + s.addConstraint((v2 <= -1)) + s.addConstraint((v2 >= 0) | "weak") + s.updateVariables() + try: + s.addConstraint((v2 >= 1)) + except Exception: + pass + + # Print the solver state + s.dump() + + state = s.dumps() + for header in ( + "Objective", + "Tableau", + "Infeasible", + "Variables", + "Edit Variables", + "Constraints", + ): + assert header in state + + +def test_handling_infeasible_constraints() -> None: + """Test that we properly handle infeasible constraints. + + We use the example of the cassowary paper to generate an infeasible + situation after updating an edit variable which causes the solver to use + the dual optimization. + + """ + xm = Variable("xm") + xl = Variable("xl") + xr = Variable("xr") + s = Solver() + + s.addEditVariable(xm, "strong") + s.addEditVariable(xl, "weak") + s.addEditVariable(xr, "weak") + s.addConstraint(2 * xm == xl + xr) + s.addConstraint(xl + 20 <= xr) + s.addConstraint(xl >= -10) + s.addConstraint(xr <= 100) + + s.suggestValue(xm, 40) + s.suggestValue(xr, 50) + s.suggestValue(xl, 30) + + # First update causing a normal update. + s.suggestValue(xm, 60) + + # Create an infeasible condition triggering a dual optimization + s.suggestValue(xm, 90) + s.updateVariables() + assert xl.value() + xr.value() == 2 * xm.value() + assert xl.value() == 80 + assert xr.value() == 100 + + +def test_constraint_violated(): + """Test running a solver and check that constraints + report they've been violated + + """ + s = Solver() + v = Variable("foo") + + c1 = (v >= 10) | "required" + c2 = (v <= -5) | "weak" + + s.addConstraint(c1) + s.addConstraint(c2) + + s.updateVariables() + + assert v.value() >= 10 + assert c1.violated() is False + assert c2.violated() is True diff --git a/py/tests/test_strength.py b/py/tests/test_strength.py new file mode 100644 index 0000000..b00d31c --- /dev/null +++ b/py/tests/test_strength.py @@ -0,0 +1,27 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2014-2021, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +import pytest + +from kiwisolver import strength + + +def test_accessing_predefined_strength() -> None: + """Test getting the default values for the strength.""" + assert strength.weak < strength.medium + assert strength.medium < strength.strong + assert strength.strong < strength.required + + +def test_creating_strength() -> None: + """Test creating strength from constituent values.""" + assert strength.create(0, 0, 1) < strength.create(0, 1, 0) + assert strength.create(0, 1, 0) < strength.create(1, 0, 0) + assert strength.create(1, 0, 0, 1) < strength.create(1, 0, 0, 4) + + with pytest.raises(TypeError): + strength.create("", "", "") # type: ignore diff --git a/py/tests/test_term.py b/py/tests/test_term.py new file mode 100644 index 0000000..69b33fe --- /dev/null +++ b/py/tests/test_term.py @@ -0,0 +1,202 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2014-2021, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +import gc +import math +import operator +from typing import Tuple + +import pytest + +from kiwisolver import Constraint, Expression, Term, Variable, strength + + +def test_term_creation() -> None: + """Test the Term constructor.""" + v = Variable("foo") + t = Term(v) + assert t.variable() is v + assert t.coefficient() == 1 + + t = Term(v, 100) + assert t.variable() is v + assert t.coefficient() == 100 + + assert str(t) == "100 * foo" + + with pytest.raises(TypeError) as excinfo: + Term("") # type: ignore + assert "Variable" in excinfo.exconly() + + # ensure we test garbage collection + del t + gc.collect() + + +@pytest.fixture() +def terms(): + """Terms for testing.""" + v = Variable("foo") + v2 = Variable("bar") + t = Term(v, 10) + t2 = Term(v2) + + return t, t2, v, v2 + + +def test_term_neg(terms: Tuple[Term, Term, Variable, Variable]) -> None: + """Test neg on a term.""" + t, _, v, _ = terms + + neg = -t + assert isinstance(neg, Term) + assert neg.variable() is v and neg.coefficient() == -10 + + +def test_term_mul(terms: Tuple[Term, Term, Variable, Variable]) -> None: + """Test term multiplications""" + t, _, v, _ = terms + + for mul in (t * 2, 2.0 * t): + assert isinstance(mul, Term) + assert mul.variable() is v and mul.coefficient() == 20 + + with pytest.raises(TypeError): + t * v # type: ignore + + +def test_term_div(terms: Tuple[Term, Term, Variable, Variable]) -> None: + """Test term divisions.""" + t, _, v, v2 = terms + + div = t / 2 + assert isinstance(div, Term) + assert div.variable() is v and div.coefficient() == 5 + + with pytest.raises(TypeError): + t / v2 # type: ignore + + with pytest.raises(ZeroDivisionError): + t / 0 + + +def test_term_add(terms: Tuple[Term, Term, Variable, Variable]) -> None: + """Test term additions.""" + t, t2, v, v2 = terms + + for add in (t + 2, 2.0 + t): + assert isinstance(add, Expression) + assert add.constant() == 2 + terms_ = add.terms() + assert ( + len(terms_) == 1 + and terms[0].variable() is v + and terms_[0].coefficient() == 10 + ) + + for add2, order in zip((t + v2, v2 + t), ((0, 1), (1, 0))): + assert isinstance(add2, Expression) + assert add2.constant() == 0 + terms_ = add2.terms() + assert ( + len(terms_) == 2 + and terms_[order[0]].variable() is v + and terms_[order[0]].coefficient() == 10 + and terms_[order[1]].variable() is v2 + and terms_[order[1]].coefficient() == 1 + ) + + add2 = t + t2 + assert isinstance(add2, Expression) + assert add2.constant() == 0 + terms_ = add2.terms() + assert ( + len(terms_) == 2 + and terms_[0].variable() is v + and terms_[0].coefficient() == 10 + and terms_[1].variable() is v2 + and terms_[1].coefficient() == 1 + ) + + +def test_term_sub(terms: Tuple[Term, Term, Variable, Variable]) -> None: + """Test term substractions.""" + t, t2, v, v2 = terms + + for sub, diff in zip((t - 2, 2.0 - t), (-2, 2)): + assert isinstance(sub, Expression) + assert sub.constant() == diff + terms_ = sub.terms() + assert ( + len(terms_) == 1 + and terms[0].variable() is v + and terms_[0].coefficient() == -math.copysign(10, diff) + ) + + for sub2, order in zip((t - v2, v2 - t), ((0, 1), (1, 0))): + assert isinstance(sub2, Expression) + assert sub2.constant() == 0 + terms_ = sub2.terms() + assert ( + len(terms_) == 2 + and terms_[order[0]].variable() is v + and terms_[order[0]].coefficient() == 10 * (-1) ** order[0] + and terms_[order[1]].variable() is v2 + and terms_[order[1]].coefficient() == -1 * (-1) ** order[0] + ) + + sub2 = t - t2 + assert isinstance(sub2, Expression) + assert sub2.constant() == 0 + terms_ = sub2.terms() + assert ( + len(terms_) == 2 + and terms_[0].variable() is v + and terms_[0].coefficient() == 10 + and terms_[1].variable() is v2 + and terms_[1].coefficient() == -1 + ) + + +@pytest.mark.parametrize( + "op, symbol", + [ + (operator.le, "<="), + (operator.eq, "=="), + (operator.ge, ">="), + (operator.lt, None), + (operator.ne, None), + (operator.gt, None), + ], +) +def test_term_rich_compare_operations(op, symbol): + """Test using comparison on variables.""" + v = Variable("foo") + v2 = Variable("bar") + t1 = Term(v, 10) + t2 = Term(v2, 20) + + if symbol is not None: + c = op(t1, t2 + 1) + assert isinstance(c, Constraint) + e = c.expression() + t = e.terms() + assert len(t) == 2 + if t[0].variable() is not v: + t = (t[1], t[0]) + assert ( + t[0].variable() is v + and t[0].coefficient() == 10 + and t[1].variable() is v2 + and t[1].coefficient() == -20 + ) + assert e.constant() == -1 + assert c.op() == symbol and c.strength() == strength.required + + else: + with pytest.raises(TypeError): + op(t1, t2) diff --git a/py/tests/test_variable.py b/py/tests/test_variable.py new file mode 100644 index 0000000..345c451 --- /dev/null +++ b/py/tests/test_variable.py @@ -0,0 +1,163 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2014-2022, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +import math +import operator +import sys + +import pytest + +from kiwisolver import Constraint, Expression, Term, Variable, strength + + +def test_variable_methods() -> None: + """Test the variable modification methods.""" + v = Variable() + assert v.name() == "" + v.setName("Δ") + assert v.name() == "Δ" + v.setName("foo") + assert v.name() == "foo" + with pytest.raises(TypeError): + v.setName(1) # type: ignore + if sys.version_info >= (3,): + with pytest.raises(TypeError): + v.setName(b"r") # type: ignore + + assert v.value() == 0.0 + + assert v.context() is None + ctx = object() + v.setContext(ctx) + assert v.context() is ctx + + assert str(v) == "foo" + + with pytest.raises(TypeError): + Variable(1) # type: ignore + + +def test_variable_neg() -> None: + """Test neg on a variable.""" + v = Variable("foo") + + neg = -v + assert isinstance(neg, Term) + assert neg.variable() is v and neg.coefficient() == -1 + + +def test_variable_mul() -> None: + """Test variable multiplications.""" + v = Variable("foo") + v2 = Variable("bar") + + for mul in (v * 2.0, 2 * v): + assert isinstance(mul, Term) + assert mul.variable() is v and mul.coefficient() == 2 + + with pytest.raises(TypeError): + v * v2 # type: ignore + + +def test_variable_division() -> None: + """Test variable divisions.""" + v = Variable("foo") + v2 = Variable("bar") + + div = v / 2.0 + assert isinstance(div, Term) + assert div.variable() is v and div.coefficient() == 0.5 + + with pytest.raises(TypeError): + v / v2 # type: ignore + + with pytest.raises(ZeroDivisionError): + v / 0 + + +def test_variable_addition() -> None: + """Test variable additions.""" + v = Variable("foo") + v2 = Variable("bar") + + for add in (v + 2, 2.0 + v): + assert isinstance(add, Expression) + assert add.constant() == 2 + terms = add.terms() + assert ( + len(terms) == 1 and terms[0].variable() is v and terms[0].coefficient() == 1 + ) + + add2 = v + v2 + assert isinstance(add2, Expression) + assert add2.constant() == 0 + terms = add2.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == 1 + and terms[1].variable() is v2 + and terms[1].coefficient() == 1 + ) + + with pytest.raises(TypeError): + v + "" # type: ignore + + +def test_variable_sub() -> None: + """Test variable substractions.""" + v = Variable("foo") + v2 = Variable("bar") + + for sub, diff in zip((v - 2, 2 - v), (-2, 2)): + assert isinstance(sub, Expression) + assert sub.constant() == diff + terms = sub.terms() + assert ( + len(terms) == 1 + and terms[0].variable() is v + and terms[0].coefficient() == -math.copysign(1, diff) + ) + + sub2 = v - v2 + assert isinstance(sub2, Expression) + assert sub2.constant() == 0 + terms = sub2.terms() + assert ( + len(terms) == 2 + and terms[0].variable() is v + and terms[0].coefficient() == 1 + and terms[1].variable() is v2 + and terms[1].coefficient() == -1 + ) + + +def test_variable_rich_compare_operations() -> None: + """Test using comparison on variables.""" + v = Variable("foo") + v2 = Variable("Δ") + + for op, symbol in ((operator.le, "<="), (operator.eq, "=="), (operator.ge, ">=")): + c = op(v, v2 + 1) + assert isinstance(c, Constraint) + e = c.expression() + t = e.terms() + assert len(t) == 2 + if t[0].variable() is not v: + t = (t[1], t[0]) + assert ( + t[0].variable() is v + and t[0].coefficient() == 1 + and t[1].variable() is v2 + and t[1].coefficient() == -1 + ) + assert e.constant() == -1 + assert c.op() == symbol and c.strength() == strength.required + + for op in (operator.lt, operator.ne, operator.gt): + with pytest.raises(TypeError): + op(v, v2) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..fbd0d47 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2013-2022, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- + +[project] + name = "kiwisolver" + description = "A fast implementation of the Cassowary constraint solver" + readme = "README.rst" + requires-python = ">=3.7" + license = { file = "LICENSE" } + authors = [{ name = "The Nucleic Development Team", email = "sccolbert@gmail.com" }] + maintainers = [{ name = "Matthieu C. Dartiailh", email = "m.dartiailh@gmail.com" }] + classifiers = [ + "License :: OSI Approved :: BSD License", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", + ] + dependencies = ["typing_extensions;python_version<'3.8'"] + dynamic = ["version"] + + [project.urls] + homepage = "https://github.com/nucleic/kiwi" + documentation = "https://kiwisolver.readthedocs.io/en/latest/" + repository = "https://github.com/nucleic/kiwi" + changelog = "https://github.com/nucleic/kiwi/blob/main/releasenotes.rst" + +[build-system] + requires = ["setuptools>=61.2", "wheel", "setuptools_scm[toml]>=3.4.3", "cppy>=1.2.0"] + build-backend = "setuptools.build_meta" + +[tool.setuptools] + include-package-data = false + package-data = { kiwisolver = ["py.typed", "*.pyi"] } + + [tool.setuptools.packages.find] + where = ["py"] + include = ["kiwisolver"] + +[tool.setuptools_scm] + write_to = "py/src/version.h" + write_to_template = """ +/* ---------------------------------------------------------------------------- +| Copyright (c) 2013-2023, Nucleic Development Team. +| +| Distributed under the terms of the Modified BSD License. +| +| The full license is in the file LICENSE, distributed with this software. +| ---------------------------------------------------------------------------*/ +// This file is auto-generated by setuptools-scm do NOT edit it. + +#pragma once + +#define PY_KIWI_VERSION "{version}" + +""" + +[tool.ruff] + src = ["src"] + extend-exclude = ["tests/instruments/hardware/nifpga/scope_based"] + line-length = 88 + [tool.ruff.lint] + select = ["C", "E", "F", "W", "I", "C90", "RUF"] + extend-ignore = ["E501", "RUF012"] + + [tool.ruff.lint.isort] + combine-as-imports = true + known-first-party = ["kiwisolver"] + + [tool.ruff.lint.mccabe] + max-complexity = 20 + +[tool.pytest.ini_options] + minversion = "6.0" + +[tool.mypy] + follow_imports = "normal" + strict_optional = true diff --git a/releasenotes.rst b/releasenotes.rst new file mode 100644 index 0000000..2668b5e --- /dev/null +++ b/releasenotes.rst @@ -0,0 +1,97 @@ +Kiwi Release Notes +================== + +Wrappers 1.4.5 | Solver 1.4.2 | 24/08/2023 +------------------------------------------ +- implement exceptions in Python PR #162 + This allows to expose in a natural manner the object relevant to the exception: + constraint or edit_variable +- add missing signature of Constraint.violated for Python wrapper PR #166 +- add support for Python 3.12 + +Wrappers 1.4.4 | Solver 1.4.2 | 15/07/2022 +------------------------------------------ +- fix timing in shared data release procedure PR #149 +- revert use of nullpointer introduced in #142 + Its use is not necessary anymore in 3.11.0-beta.4 and used to cause issues on + some platforms (see #144 ) PR #145 + +Wrappers 1.4.3 | Solver 1.4.1 | 13/06/2022 +------------------------------------------ +- add support for Python 3.11 PR #142 +- do not install tests PR #143 +- fix packaging for latest setuptools PR #140 + +Wrappers 1.4.2 | Solver 1.4.1 | 28/03/2022 +------------------------------------------ +- fix an issue with setuptools configuration PR #134 + +Wrappers 1.4.1 | Solver 1.4.1 | 27/03/2022 +------------------------------------------ +- add missing include PR #129 +- re-organize the Python binding sources to properly ship type hints PR #131 + +Wrappers 1.4.0 | Solver 1.4.0 | 14/03/2022 +------------------------------------------ +- make installation PEP517 compliant PR #125 +- add type hints PR #125 +- add Constraint::violated() method PR #128 +- make the the c++ part of the code c++20 compliant PR #120 +- test with c++11 and c++20 PR #120 + +Wrappers 1.3.2 | Solver 1.3.1 | 31/08/2021 +------------------------------------------ +- Add support for Python 3.10, drop official support Python 3.6 PR #103 +- Remove direct accesses to ob_type in C-API use Py_TYPE instead PR #103 + +Wrappers 1.3.1 | Solver 1.3.1 | 11/01/2020 +------------------------------------------ +- allow to avoid linking against VC2014_1 on windows PR #97 +- do not mark move constructor / assignment operator of expression as noexcept PR #97 + This is to circumvent a suspected bug in the GCC compiler in the manylinux1 + image. + +Wrappers 1.3.0 | Solver 1.3.0 | 10/21/2020 +------------------------------------------ +- add c++ benchmarks and run them on CIs PR #91 +- modernize the c++ code by using more c++11 features PR #90 +- introduce move semantic in some c++ constructors to improve performances PR #89 +- add support for Python 3.9 PR #88 + +Wrappers 1.2.0 | Solver 1.2.0 | 03/26/2020 +------------------------------------------ +- make the the c++ part of the code c++11 compliant PR #55 +- use cppy for Python/C bindings PR #55 + +Wrappers 1.1.0 | Solver 1.1.0 | 04/24/2019 +------------------------------------------ +- prevent attempting a dual optimize on a dummy row PR #56 closes #15 +- add ``dump`` and ``dumps`` methods to inspect the internal state of the + solver PR #56 +- test on Python 3.7 PR #51 +- improvements to setup.py and tests PR #46 #50 + +Wrappers 1.0.1 | Solver 1.0.0 | 10/24/2017 +------------------------------------------ +- allow unicode strings for variable name in Python 2 +- allow unicode strings as strength specifiers in Python 2 + +Wrappers 1.0.0 | Solver 1.0.0 | 09/06/2017 +------------------------------------------ +- Allow anonymous variables (solver PR #32, wrappers PR #22) +- Solver: Define binary operators as free functions (PR #23) +- Wrappers: support for Python 3 (PR #13) +- Wrappers: drop distribute dependency in favor of setuptools (PR #22) +- Wrappers: add a comprehensive test suite + +Wrappers 0.1.3 | Solver 0.1.1 | 07/12/2013 +------------------------------------------ +- Update the build script to remove the need for build.py + +Wrappers 0.1.2 | Solver 0.1.1 | 01/15/2013 +------------------------------------------ +- Fix issue #2. Bad handling of zero-size constraints. + +Wrappers 0.1.1 | Solver 0.1.0 | 01/13/2013 +------------------------------------------ +- Initial public release. diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..d430756 --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +# -------------------------------------------------------------------------------------- +# Copyright (c) 2013-2022, Nucleic Development Team. +# +# Distributed under the terms of the Modified BSD License. +# +# The full license is in the file LICENSE, distributed with this software. +# -------------------------------------------------------------------------------------- +import os + +from setuptools import Extension, setup + +try: + from cppy import CppyBuildExt +except ImportError as e: + raise RuntimeError( + "Missing setup required dependencies: cppy. " + "Installing through pip as recommended ensure one never hits this issue." + ) from e + +# Before releasing the version needs to be updated in kiwi/version.h, if the changes +# are not limited to the solver. + +# Use the env var KIWI_DISABLE_FH4 to disable linking against VCRUNTIME140_1.dll + +if "KIWI_DISABLE_FH4" in os.environ: + os.environ.setdefault("CPPY_DISABLE_FH4", "1") + +ext_modules = [ + Extension( + "kiwisolver._cext", + [ + "py/src/kiwisolver.cpp", + "py/src/constraint.cpp", + "py/src/expression.cpp", + "py/src/solver.cpp", + "py/src/strength.cpp", + "py/src/term.cpp", + "py/src/variable.cpp", + ], + include_dirs=["."], + language="c++", + ), +] + + +setup( + ext_modules=ext_modules, + cmdclass={"build_ext": CppyBuildExt}, +)