kiwisolver Documentation

repository·main·Indexed 20 days ago

https://github.com/nucleic/kiwi

A fast, lightweight C++ implementation of the Cassowary constraint solving algorithm with hand-rolled Python bindings. It provides a C++11 header-only library and Python 3.7+ support for defining variables and constraints with configurable strengths (weak, medium, strong, required). Key features include edit variables for suggesting values, solver state inspection via dump/dumps, and integration via CMake's FetchContent.

Tokens
4.8K
Snippets
17
Records
32
Agent score
73%

What's inside kiwisolver

  1. Overview of Kiwisolver

    main

    Kiwisolver is a high-performance C++ implementation of the Cassowary constraint solving algorithm. It is designed to be lightweight and fast, offering significant performance improvements over the original Cassowary solver:

    • Speed: 10x to 500x faster than the original solver (typically ~40x improvement).
    • Memory: Consistently > 5x memory savings.

    Kiwi provides a native C++ solver and includes hand-rolled Python bindings for use in Python environments.

  2. Python bindings requirements and architecture

    main

    Kiwi provides Python bindings that target Python 3.7 and above. The bindings are hand-written using cppy.

    To support sub-interpreters and maintain a modern C API, the implementation follows these architectural constraints:

    • Uses the multi-phase extension module initialization mechanism (PEP 489).
    • Non-exported symbols are enclosed in anonymous namespaces.
    • Relies on dynamic types rather than static types (though type slots and related structures are stored in a static variable).
    • Use of static variables is strictly limited to type slots and method def.
  3. Emulate stay constraints in Kiwi

    main

    Kiwi does not natively support 'stay constraints' (constraints used to keep non-modified variables close to their original positions in under-constrained scenarios).

    If your application requires this behavior, you can use one of these two workarounds:

    1. Equality constraints: Add or remove non-required equality constraints to mimic the behavior. Note that you must manually remove these constraints once they are no longer relevant.
    2. Edit variables: Use edit-variables to mimic stay constraints by updating the suggested value.
  4. Constraint generation using Enaml helpers (hbox/vbox)

    main

    Manually writing constraints for layouts like horizontal or vertical boxes is complex. Enaml provides helper functions like hbox to simplify this.

    How hbox works

    • Input: Accepts a list of widgets and spacers.
    • Spacers: These are objects with a specific size and policy (minimum, maximum, or strength). hbox inserts spacers between each widget and between widgets and the parent boundaries.
    • Mechanism: The hbox helper uses the container to generate constraints by "glueing" the anchors of surrounding widgets together using the spacers.
    • Flexibility: Spacers can have zero size (meaning widgets are in contact) and can generate multiple constraints to allow for flexible positioning.

    For implementation details, refer to the Enaml source for spacers, layout_helpers, linear_box_helper, and sequence_helper.

  5. Understand Term and Expression internal classes

    main

    While not typically used directly by end-users, Term and Expression are core classes used to build constraints:

    • Term: Represents a variable or symbol and its coefficient.
    • Expression: Represents a sum of terms and a constant value (used as the left-hand side of a constraint).
  6. Manage constraint strength

    main

    By default, constraints are 'required' (they must be satisfied). To express preferences that should only be respected on a best-effort basis, assign them a strength.

    Kiwi provides three standard non-required strengths:

    1. strong
    2. medium
    3. weak

    A stronger constraint will always override a weaker one. In Python, strength is specified using the bitwise OR operator | with a string literal. In C++, use the strength:: namespace.

    # Python: Assigning a weak strength
    solver.addConstraint((x1 == 40) | "weak")
    // C++: Assigning a weak strength
    solver.addConstraint(x1 == 40 | strength::weak);
  7. How Enaml uses Kiwi for widget layout

    main

    Enaml uses Kiwi to implement a nestable layout model. The system works through a bottom-up approach:

    1. Leaf Components: Provide a "preferred size" (size hint).
    2. Containers: Generate constraints to layout their children and pass a bounding box representation to those children so they can position themselves.
    3. Solving: The system solves from the bottom up, setting parent sizes based on the required space of children. If a parent is resized, the new dimensions are used to re-solve the layout.

    This architecture allows for efficient updates and handles complex hierarchies by propagating size requests from top-level parents down to leaf widgets and propagating solved sizes back up.

  8. Setting up and managing the Enaml solver

    main

    Enaml manages Kiwi solvers to compute widget layouts.

    Solver Lifecycle

    • Independence: By default, each container manages its own independent solver. This keeps the system small and ensures faster updates.
    • Setup: When a container is set up, it adds:
      1. Constraints reflecting the widget's size preferences.
      2. Constraints defining the layout of its children.
      3. Two edit variables representing the container's own width and height.
    • Resizing: When a parent is resized, the solver is invoked again using the new width and height as suggestions.
    • Constraint Changes: If widgets are added/removed or constraints are modified, the solver is reset and all constraints are rebuilt from scratch. This prevents memory leaks from unused variables.

    Computing Sizes

    Once set up, the solver can be used to request different size profiles:

    • Best size: Request a size of 0 with a 0.1*weak strength.
    • Min size: Request a size of 0 with medium strength.
    • Max size: Request the max size with medium strength.
  9. Build Kiwisolver from source

    main

    To build Kiwisolver from scratch, you need Python and a C++ compiler. On Windows (Python 3.6+), the free version of the Microsoft toolchain should work out of the box. On macOS Mojave, you must set MACOSX_DEPLOYMENT_TARGET to a value higher than 10.9 to ensure the compiler uses the correct C++ stdlib.

    # For macOS Mojave users
    $ export MACOSX_DEPLOYMENT_TARGET=10.10
    
    # Build from source
    $ pip install .
  10. Define variables and constraints in Kiwi

    main

    To build a system in Kiwi, you first define Variable objects and then create Constraint objects using standard mathematical operators (e.g., ==, >=, <=, +, /). Constraints can be added to a Solver instance. While naming variables is not mandatory, it is recommended for better error messages.

    Note that Kiwi supports redundant constraints (e.g., adding x == 10, x + y == 30, and y == 20 simultaneously), but you should avoid adding the exact same constraint multiple times.

    from kiwisolver import Variable, Solver
    
    # Define variables
    x1 = Variable('x1')
    x2 = Variable('x2')
    
    # Define constraints
    constraints = [x1 >= 0, x2 <= 100, x2 >= x1 + 10]
    
    # Add to solver
    solver = Solver()
    for cn in constraints:
        solver.addConstraint(cn)
  11. Inspect the solver state using dump and dumps

    main

    You can inspect the internal state of a Kiwi solver by generating a text representation. This is useful for debugging and analyzing the current objective, tableau, infeasible constraints, and variables.

    Use the following methods:

    • dump(): Outputs the state to stdout.
    • dumps(): Returns the state as a string.

    Understanding the Dump Output

    In the generated text, symbols are prefixed with specific letters:

    • v: External variable (created by the user).
    • s: Slack symbol (used to represent inequalities).
    • e: Error symbol (used to represent non-required constraints).
    • d: Dummy variable (always zero, tracks the impact of external variables in the tableau).
    • i: Invalid symbol (returned when no valid symbol can be found).
    // Example conceptual usage (API names provided in documentation)
    solver.dump() // to stdout
    state_string = solver.dumps()