# Thermometers MIP Solver
[](https://github.com/DenHvideDvaerg/thermometers-mip-solver/actions/workflows/CI.yml)
[](https://codecov.io/gh/DenHvideDvaerg/thermometers-mip-solver)
[](https://pypi.org/project/thermometers-mip-solver/)
[](https://pypi.org/project/thermometers-mip-solver/)
[](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[](https://github.com/DenHvideDvaerg/thermometers-mip-solver/actions/workflows/CI.yml)\r\n[](https://codecov.io/gh/DenHvideDvaerg/thermometers-mip-solver)\r\n[](https://pypi.org/project/thermometers-mip-solver/)\r\n[](https://pypi.org/project/thermometers-mip-solver/)\r\n[](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"
}