thermometers-mip-solver


Namethermometers-mip-solver JSON
Version 0.2.0 PyPI version JSON
download
home_pageNone
SummaryA Thermometers puzzle solver using Mixed Integer Programming (MIP)
upload_time2025-08-26 18:47:18
maintainerNone
docs_urlNone
authorRasmus Ørnstrup Mikkelsen
requires_python>=3.9
licenseNone
keywords puzzle solver mixed-integer-programming optimization thermometers
VCS
bugtrack_url
requirements pytest pytest-cov ortools
Travis-CI No Travis.
coveralls test coverage No coveralls.
            # Thermometers MIP Solver

[![CI](https://github.com/DenHvideDvaerg/thermometers-mip-solver/actions/workflows/CI.yml/badge.svg)](https://github.com/DenHvideDvaerg/thermometers-mip-solver/actions/workflows/CI.yml)
[![Code Coverage](https://img.shields.io/codecov/c/github/DenHvideDvaerg/thermometers-mip-solver?color=blue)](https://codecov.io/gh/DenHvideDvaerg/thermometers-mip-solver)
[![PyPI version](https://img.shields.io/pypi/v/thermometers-mip-solver?color=green)](https://pypi.org/project/thermometers-mip-solver/)
[![Python](https://img.shields.io/pypi/pyversions/thermometers-mip-solver?color=blue)](https://pypi.org/project/thermometers-mip-solver/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

A Thermometers puzzle solver using mathematical programming.

## Overview

Thermometers is a logic puzzle where you must fill thermometers on a grid with mercury according to these rules:

- **Continuous filling from bulb** - thermometers fill from bulb end without gaps
- **Row and column constraints** - each row/column must have a specific number of filled cells
  - **Missing constraints variant** - supports puzzles where some row/column constraints are unknown (specified as `None`)

This solver models the puzzle as a **Mixed Integer Programming (MIP)** problem to find solutions.

## Installation

```bash
pip install thermometers-mip-solver
```

## Requirements

- Python 3.9+
- Google OR-Tools
- pytest (for testing)

## Example Puzzles

### 6x6 Puzzle with Straight Thermometers

This 6x6 puzzle demonstrates the solver with straight thermometers of various lengths and orientations:

| Puzzle | Solution |
|--------|----------|
| <img src="https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/6x6_14,708,221.png" width="200"> | <img src="https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/6x6_14,708,221_solution.png" width="200"> |

```python
def example_6x6():
    """6x6 Thermometers Puzzle ID: 14,708,221 from puzzle-thermometers.com"""
    puzzle = ThermometerPuzzle(
        row_sums=[3, 2, 1, 2, 5, 4],
        col_sums=[3, 2, 2, 4, 4, 2],
        thermometer_waypoints=[
            [(0, 0), (1, 0)],               # Vertical thermometer starting in row 0
            [(0, 2), (0, 1)],               # Horizontal thermometer starting in row 0
            [(1, 2), (1, 1)],               # Horizontal thermometer starting in row 1
            [(1, 3), (0, 3)],               # Vertical thermometer starting in row 1
            [(2, 0), (2, 2)],               # Horizontal thermometer starting in row 2
            [(3, 2), (3, 1)],               # Horizontal thermometer starting in row 3
            [(3, 3), (2, 3)],               # Vertical thermometer starting in row 3
            [(3, 4), (0, 4)],               # Long vertical thermometer starting in row 3
            [(3, 5), (0, 5)],               # Long vertical thermometer starting in row 3
            [(4, 0), (3, 0)],               # Vertical thermometer starting in row 4
            [(4, 1), (4, 3)],               # Horizontal thermometer starting in row 4
            [(4, 5), (4, 4)],               # Horizontal thermometer starting in row 4
            [(5, 0), (5, 5)],               # Long horizontal thermometer starting in row 5
        ]
    )
    return puzzle
```

### 5x5 Puzzle with Curved Thermometers and Missing Constraints

This 5x5 puzzle demonstrates advanced features: curved thermometers with multiple waypoints and missing row/column constraints (shown as `None`):

| Puzzle | Solution |
|--------|----------|
| <img src="https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/5x5_curved_missing_values.png" width="200"> | <img src="https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/5x5_curved_missing_values.png_solution.png" width="200"> |

```python
def example_5x5_curved_missing_values():
    """5x5 'Evil' Thermometers Puzzle from https://en.gridpuzzle.com/thermometers/evil-5"""
    puzzle = ThermometerPuzzle(
        row_sums=[2, 3, None, 5, None],         # Rows 2 and 4 have no constraint
        col_sums=[None, None, 1, 4, 4],         # Columns 0 and 1 have no constraint
        thermometer_waypoints=[
            [(0, 0), (0, 2), (2, 2)],            # L-shaped thermometer
            [(2, 0), (1, 0), (1, 1), (2, 1)],    # ∩-shaped thermometer
            [(2, 3), (0, 3), (0, 4)],            # L-shaped thermometer
            [(3, 0), (3, 3)],                    # Straight thermometer
            [(3, 4), (1, 4)],                    # Straight thermometer
            [(4, 0), (4, 1)],                    # Straight thermometer
            [(4, 2), (4, 4)],                    # Straight thermometer
        ]
    )
    return puzzle
```

## Usage

```python
from thermometers_mip_solver import ThermometerPuzzle, ThermometersSolver
import time

def solve_puzzle(puzzle, name):
    """Solve a thermometer puzzle and display results"""
    print(f"\n" + "="*60)
    print(f"SOLVING {name.upper()}")
    print("="*60)
    
    # Create and use the solver
    solver = ThermometersSolver(puzzle)
    
    print("Solver information:")
    info = solver.get_solver_info()
    for key, value in info.items():
        print(f"  {key}: {value}")
    
    print("\nSolving...")
    start_time = time.time()
    solution = solver.solve(verbose=False)
    solve_time = time.time() - start_time
    
    if solution:
        print(f"\nSolution found in {solve_time:.3f} seconds!")
        print(f"Solution has {len(solution)} filled cells")
        print(f"Solution: {sorted(list(solution))}")
    else:
        print("No solution found by solver!")

# Load and solve example puzzles
puzzle_6x6 = example_6x6()
solve_puzzle(puzzle_6x6, "6x6")

puzzle_5x5_curved_missing = example_5x5_curved_missing_values()
solve_puzzle(puzzle_5x5_curved_missing, "5x5 Curved Missing Values")
```

### Output

```
============================================================
SOLVING 6X6
============================================================
Solver information:
  solver_type: SCIP 9.2.2 [LP solver: SoPlex 7.1.3]
  num_variables: 36
  num_constraints: 35
  grid_size: 6x6
  num_thermometers: 13
  total_cells: 36

Solving...

Solution found in 0.002 seconds!
Solution has 17 filled cells
Solution: [(0, 0), (0, 3), (0, 4), (1, 3), (1, 4), (2, 4), (3, 4), (3, 5), (4, 0), (4, 1), (4, 2), (4, 3), (4, 5), (5, 0), (5, 1), (5, 2), (5, 3)]

============================================================
SOLVING 5X5 CURVED MISSING VALUES
============================================================
Solver information:
  solver_type: SCIP 9.2.2 [LP solver: SoPlex 7.1.3]
  num_variables: 25
  num_constraints: 24
  grid_size: 5x5
  num_thermometers: 7
  total_cells: 25

Solving...

Solution found in 0.002 seconds!
Solution has 14 filled cells
Solution: [(0, 3), (0, 4), (1, 0), (1, 3), (1, 4), (2, 0), (2, 3), (2, 4), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (4, 0)]
```

## Waypoint System

The solver uses a **waypoint-based approach** to define thermometers. You only need to specify key turning points, and the system automatically expands them into complete thermometer paths:

- **Straight thermometers**: Define with start and end points: `[(0, 0), (0, 3)]`
- **Curved thermometers**: Add waypoints at each turn: `[(0, 0), (1, 0), (1, 1), (0, 1)]`
- **Path expansion**: Automatically fills in all cells between waypoints using horizontal/vertical segments
- **Validation**: Ensures all segments are properly aligned and thermometers have minimum 2 cells

## Testing

The project uses pytest for testing:

```bash
pytest                                          # Run all tests
pytest --cov=thermometers_mip_solver           # Run with coverage
```

## Mathematical Model

The solver uses **Mixed Integer Programming (MIP)** to model the puzzle constraints. Google OR-Tools provides the optimization framework, with SCIP as the default solver.

See the complete formulation in **[Complete Mathematical Model Documentation](https://github.com/DenHvideDvaerg/thermometers-mip-solver/blob/main/model.md)**

The model uses only three essential constraint types:
- **Row sum constraints** - ensure each row has the required number of filled cells
- **Column sum constraints** - ensure each column has the required number of filled cells  
- **Thermometer continuity constraints** - ensure mercury fills continuously from bulb without gaps

## License

This project is open source and available under the [MIT License](LICENSE.txt).

            

Raw data

            {
    "_id": null,
    "home_page": null,
    "name": "thermometers-mip-solver",
    "maintainer": null,
    "docs_url": null,
    "requires_python": ">=3.9",
    "maintainer_email": null,
    "keywords": "puzzle, solver, mixed-integer-programming, optimization, thermometers",
    "author": "Rasmus \u00d8rnstrup Mikkelsen",
    "author_email": null,
    "download_url": "https://files.pythonhosted.org/packages/96/a0/c24156bed3549632ad26e3ce5ac3955fbf047cb514e17b04ffaeac940eaf/thermometers_mip_solver-0.2.0.tar.gz",
    "platform": null,
    "description": "# Thermometers MIP Solver\r\n\r\n[![CI](https://github.com/DenHvideDvaerg/thermometers-mip-solver/actions/workflows/CI.yml/badge.svg)](https://github.com/DenHvideDvaerg/thermometers-mip-solver/actions/workflows/CI.yml)\r\n[![Code Coverage](https://img.shields.io/codecov/c/github/DenHvideDvaerg/thermometers-mip-solver?color=blue)](https://codecov.io/gh/DenHvideDvaerg/thermometers-mip-solver)\r\n[![PyPI version](https://img.shields.io/pypi/v/thermometers-mip-solver?color=green)](https://pypi.org/project/thermometers-mip-solver/)\r\n[![Python](https://img.shields.io/pypi/pyversions/thermometers-mip-solver?color=blue)](https://pypi.org/project/thermometers-mip-solver/)\r\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\r\n\r\nA Thermometers puzzle solver using mathematical programming.\r\n\r\n## Overview\r\n\r\nThermometers is a logic puzzle where you must fill thermometers on a grid with mercury according to these rules:\r\n\r\n- **Continuous filling from bulb** - thermometers fill from bulb end without gaps\r\n- **Row and column constraints** - each row/column must have a specific number of filled cells\r\n  - **Missing constraints variant** - supports puzzles where some row/column constraints are unknown (specified as `None`)\r\n\r\nThis solver models the puzzle as a **Mixed Integer Programming (MIP)** problem to find solutions.\r\n\r\n## Installation\r\n\r\n```bash\r\npip install thermometers-mip-solver\r\n```\r\n\r\n## Requirements\r\n\r\n- Python 3.9+\r\n- Google OR-Tools\r\n- pytest (for testing)\r\n\r\n## Example Puzzles\r\n\r\n### 6x6 Puzzle with Straight Thermometers\r\n\r\nThis 6x6 puzzle demonstrates the solver with straight thermometers of various lengths and orientations:\r\n\r\n| Puzzle | Solution |\r\n|--------|----------|\r\n| <img src=\"https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/6x6_14,708,221.png\" width=\"200\"> | <img src=\"https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/6x6_14,708,221_solution.png\" width=\"200\"> |\r\n\r\n```python\r\ndef example_6x6():\r\n    \"\"\"6x6 Thermometers Puzzle ID: 14,708,221 from puzzle-thermometers.com\"\"\"\r\n    puzzle = ThermometerPuzzle(\r\n        row_sums=[3, 2, 1, 2, 5, 4],\r\n        col_sums=[3, 2, 2, 4, 4, 2],\r\n        thermometer_waypoints=[\r\n            [(0, 0), (1, 0)],               # Vertical thermometer starting in row 0\r\n            [(0, 2), (0, 1)],               # Horizontal thermometer starting in row 0\r\n            [(1, 2), (1, 1)],               # Horizontal thermometer starting in row 1\r\n            [(1, 3), (0, 3)],               # Vertical thermometer starting in row 1\r\n            [(2, 0), (2, 2)],               # Horizontal thermometer starting in row 2\r\n            [(3, 2), (3, 1)],               # Horizontal thermometer starting in row 3\r\n            [(3, 3), (2, 3)],               # Vertical thermometer starting in row 3\r\n            [(3, 4), (0, 4)],               # Long vertical thermometer starting in row 3\r\n            [(3, 5), (0, 5)],               # Long vertical thermometer starting in row 3\r\n            [(4, 0), (3, 0)],               # Vertical thermometer starting in row 4\r\n            [(4, 1), (4, 3)],               # Horizontal thermometer starting in row 4\r\n            [(4, 5), (4, 4)],               # Horizontal thermometer starting in row 4\r\n            [(5, 0), (5, 5)],               # Long horizontal thermometer starting in row 5\r\n        ]\r\n    )\r\n    return puzzle\r\n```\r\n\r\n### 5x5 Puzzle with Curved Thermometers and Missing Constraints\r\n\r\nThis 5x5 puzzle demonstrates advanced features: curved thermometers with multiple waypoints and missing row/column constraints (shown as `None`):\r\n\r\n| Puzzle | Solution |\r\n|--------|----------|\r\n| <img src=\"https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/5x5_curved_missing_values.png\" width=\"200\"> | <img src=\"https://github.com/DenHvideDvaerg/thermometers-mip-solver/raw/main/images/5x5_curved_missing_values.png_solution.png\" width=\"200\"> |\r\n\r\n```python\r\ndef example_5x5_curved_missing_values():\r\n    \"\"\"5x5 'Evil' Thermometers Puzzle from https://en.gridpuzzle.com/thermometers/evil-5\"\"\"\r\n    puzzle = ThermometerPuzzle(\r\n        row_sums=[2, 3, None, 5, None],         # Rows 2 and 4 have no constraint\r\n        col_sums=[None, None, 1, 4, 4],         # Columns 0 and 1 have no constraint\r\n        thermometer_waypoints=[\r\n            [(0, 0), (0, 2), (2, 2)],            # L-shaped thermometer\r\n            [(2, 0), (1, 0), (1, 1), (2, 1)],    # \u2229-shaped thermometer\r\n            [(2, 3), (0, 3), (0, 4)],            # L-shaped thermometer\r\n            [(3, 0), (3, 3)],                    # Straight thermometer\r\n            [(3, 4), (1, 4)],                    # Straight thermometer\r\n            [(4, 0), (4, 1)],                    # Straight thermometer\r\n            [(4, 2), (4, 4)],                    # Straight thermometer\r\n        ]\r\n    )\r\n    return puzzle\r\n```\r\n\r\n## Usage\r\n\r\n```python\r\nfrom thermometers_mip_solver import ThermometerPuzzle, ThermometersSolver\r\nimport time\r\n\r\ndef solve_puzzle(puzzle, name):\r\n    \"\"\"Solve a thermometer puzzle and display results\"\"\"\r\n    print(f\"\\n\" + \"=\"*60)\r\n    print(f\"SOLVING {name.upper()}\")\r\n    print(\"=\"*60)\r\n    \r\n    # Create and use the solver\r\n    solver = ThermometersSolver(puzzle)\r\n    \r\n    print(\"Solver information:\")\r\n    info = solver.get_solver_info()\r\n    for key, value in info.items():\r\n        print(f\"  {key}: {value}\")\r\n    \r\n    print(\"\\nSolving...\")\r\n    start_time = time.time()\r\n    solution = solver.solve(verbose=False)\r\n    solve_time = time.time() - start_time\r\n    \r\n    if solution:\r\n        print(f\"\\nSolution found in {solve_time:.3f} seconds!\")\r\n        print(f\"Solution has {len(solution)} filled cells\")\r\n        print(f\"Solution: {sorted(list(solution))}\")\r\n    else:\r\n        print(\"No solution found by solver!\")\r\n\r\n# Load and solve example puzzles\r\npuzzle_6x6 = example_6x6()\r\nsolve_puzzle(puzzle_6x6, \"6x6\")\r\n\r\npuzzle_5x5_curved_missing = example_5x5_curved_missing_values()\r\nsolve_puzzle(puzzle_5x5_curved_missing, \"5x5 Curved Missing Values\")\r\n```\r\n\r\n### Output\r\n\r\n```\r\n============================================================\r\nSOLVING 6X6\r\n============================================================\r\nSolver information:\r\n  solver_type: SCIP 9.2.2 [LP solver: SoPlex 7.1.3]\r\n  num_variables: 36\r\n  num_constraints: 35\r\n  grid_size: 6x6\r\n  num_thermometers: 13\r\n  total_cells: 36\r\n\r\nSolving...\r\n\r\nSolution found in 0.002 seconds!\r\nSolution has 17 filled cells\r\nSolution: [(0, 0), (0, 3), (0, 4), (1, 3), (1, 4), (2, 4), (3, 4), (3, 5), (4, 0), (4, 1), (4, 2), (4, 3), (4, 5), (5, 0), (5, 1), (5, 2), (5, 3)]\r\n\r\n============================================================\r\nSOLVING 5X5 CURVED MISSING VALUES\r\n============================================================\r\nSolver information:\r\n  solver_type: SCIP 9.2.2 [LP solver: SoPlex 7.1.3]\r\n  num_variables: 25\r\n  num_constraints: 24\r\n  grid_size: 5x5\r\n  num_thermometers: 7\r\n  total_cells: 25\r\n\r\nSolving...\r\n\r\nSolution found in 0.002 seconds!\r\nSolution has 14 filled cells\r\nSolution: [(0, 3), (0, 4), (1, 0), (1, 3), (1, 4), (2, 0), (2, 3), (2, 4), (3, 0), (3, 1), (3, 2), (3, 3), (3, 4), (4, 0)]\r\n```\r\n\r\n## Waypoint System\r\n\r\nThe solver uses a **waypoint-based approach** to define thermometers. You only need to specify key turning points, and the system automatically expands them into complete thermometer paths:\r\n\r\n- **Straight thermometers**: Define with start and end points: `[(0, 0), (0, 3)]`\r\n- **Curved thermometers**: Add waypoints at each turn: `[(0, 0), (1, 0), (1, 1), (0, 1)]`\r\n- **Path expansion**: Automatically fills in all cells between waypoints using horizontal/vertical segments\r\n- **Validation**: Ensures all segments are properly aligned and thermometers have minimum 2 cells\r\n\r\n## Testing\r\n\r\nThe project uses pytest for testing:\r\n\r\n```bash\r\npytest                                          # Run all tests\r\npytest --cov=thermometers_mip_solver           # Run with coverage\r\n```\r\n\r\n## Mathematical Model\r\n\r\nThe solver uses **Mixed Integer Programming (MIP)** to model the puzzle constraints. Google OR-Tools provides the optimization framework, with SCIP as the default solver.\r\n\r\nSee the complete formulation in **[Complete Mathematical Model Documentation](https://github.com/DenHvideDvaerg/thermometers-mip-solver/blob/main/model.md)**\r\n\r\nThe model uses only three essential constraint types:\r\n- **Row sum constraints** - ensure each row has the required number of filled cells\r\n- **Column sum constraints** - ensure each column has the required number of filled cells  \r\n- **Thermometer continuity constraints** - ensure mercury fills continuously from bulb without gaps\r\n\r\n## License\r\n\r\nThis project is open source and available under the [MIT License](LICENSE.txt).\r\n",
    "bugtrack_url": null,
    "license": null,
    "summary": "A Thermometers puzzle solver using Mixed Integer Programming (MIP)",
    "version": "0.2.0",
    "project_urls": {
        "Bug Reports": "https://github.com/DenHvideDvaerg/thermometers-mip-solver/issues",
        "Documentation": "https://github.com/DenHvideDvaerg/thermometers-mip-solver/blob/main/model.md",
        "Homepage": "https://github.com/DenHvideDvaerg/thermometers-mip-solver",
        "Source": "https://github.com/DenHvideDvaerg/thermometers-mip-solver"
    },
    "split_keywords": [
        "puzzle",
        " solver",
        " mixed-integer-programming",
        " optimization",
        " thermometers"
    ],
    "urls": [
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "3902d85aa8020dec2d2d72171703f650523dc3466396bde5b3a7177a58925241",
                "md5": "503a91da75e6b659198bd6f82f34e2d0",
                "sha256": "9f16fcbcb13a2175034e70ae5acafcf63de37e5001cd524f843ec7fc1082936d"
            },
            "downloads": -1,
            "filename": "thermometers_mip_solver-0.2.0-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "503a91da75e6b659198bd6f82f34e2d0",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": ">=3.9",
            "size": 10543,
            "upload_time": "2025-08-26T18:47:17",
            "upload_time_iso_8601": "2025-08-26T18:47:17.003722Z",
            "url": "https://files.pythonhosted.org/packages/39/02/d85aa8020dec2d2d72171703f650523dc3466396bde5b3a7177a58925241/thermometers_mip_solver-0.2.0-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": null,
            "digests": {
                "blake2b_256": "96a0c24156bed3549632ad26e3ce5ac3955fbf047cb514e17b04ffaeac940eaf",
                "md5": "578b3672e8f3aa360e03b70c7a1b20a9",
                "sha256": "4edb6462cbdd50317adcf7e870dad71323edec7af6eae91be8885d556077d8b8"
            },
            "downloads": -1,
            "filename": "thermometers_mip_solver-0.2.0.tar.gz",
            "has_sig": false,
            "md5_digest": "578b3672e8f3aa360e03b70c7a1b20a9",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": ">=3.9",
            "size": 15331,
            "upload_time": "2025-08-26T18:47:18",
            "upload_time_iso_8601": "2025-08-26T18:47:18.019323Z",
            "url": "https://files.pythonhosted.org/packages/96/a0/c24156bed3549632ad26e3ce5ac3955fbf047cb514e17b04ffaeac940eaf/thermometers_mip_solver-0.2.0.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2025-08-26 18:47:18",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "DenHvideDvaerg",
    "github_project": "thermometers-mip-solver",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "requirements": [
        {
            "name": "pytest",
            "specs": []
        },
        {
            "name": "pytest-cov",
            "specs": []
        },
        {
            "name": "ortools",
            "specs": []
        }
    ],
    "lcname": "thermometers-mip-solver"
}
        
Elapsed time: 0.44392s