From 240005977d6500621fde8d7d4f671f623a92bab2 Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:07:49 +0100 Subject: [PATCH 1/9] add: dev-0.3.1 --- .github/workflows/ci.yml | 52 +++ README.md | 61 ++-- pyproject.toml | 67 ++++ qaekwy/__init__.py | 34 +- qaekwy/__main__.py | 12 +- qaekwy/__metadata__.py | 2 +- qaekwy/api/model.py | 158 ++++----- qaekwy/core/engine.py | 23 +- qaekwy/core/model/constraint/abs.py | 4 +- qaekwy/core/model/constraint/acos.py | 4 +- qaekwy/core/model/constraint/asin.py | 4 +- qaekwy/core/model/constraint/atan.py | 4 +- qaekwy/core/model/constraint/cos.py | 4 +- qaekwy/core/model/constraint/distinct.py | 4 +- qaekwy/core/model/constraint/divide.py | 4 +- qaekwy/core/model/constraint/element.py | 4 +- qaekwy/core/model/constraint/exponential.py | 4 +- qaekwy/core/model/constraint/if_then_else.py | 5 +- qaekwy/core/model/constraint/logarithm.py | 4 +- qaekwy/core/model/constraint/maximum.py | 4 +- qaekwy/core/model/constraint/member.py | 4 +- qaekwy/core/model/constraint/minimum.py | 4 +- qaekwy/core/model/constraint/modulo.py | 4 +- qaekwy/core/model/constraint/multiply.py | 4 +- qaekwy/core/model/constraint/nroot.py | 4 +- qaekwy/core/model/constraint/power.py | 4 +- qaekwy/core/model/constraint/relational.py | 4 +- qaekwy/core/model/constraint/sin.py | 4 +- qaekwy/core/model/constraint/sort.py | 4 +- qaekwy/core/model/constraint/tan.py | 4 +- qaekwy/core/model/function.py | 2 +- qaekwy/core/model/modeller.py | 227 ++++++------- qaekwy/core/model/relation.py | 52 --- qaekwy/core/model/specific.py | 4 +- qaekwy/core/model/variable/boolean.py | 18 +- qaekwy/core/model/variable/float.py | 12 +- qaekwy/core/model/variable/integer.py | 17 +- qaekwy/core/model/variable/variable.py | 131 +++----- qaekwy/core/response.py | 18 +- qaekwy/core/solution.py | 2 +- setup.py | 87 ----- tests/api/test_model.py | 316 ++++++++++++++++++ tests/{ => core}/model/constraint/test_abs.py | 0 .../{ => core}/model/constraint/test_acos.py | 0 .../{ => core}/model/constraint/test_asin.py | 0 .../{ => core}/model/constraint/test_atan.py | 0 tests/{ => core}/model/constraint/test_cos.py | 0 tests/core/model/constraint/test_distinct.py | 218 ++++++++++++ .../model/constraint/test_divide.py | 0 .../model/constraint/test_element.py | 0 .../model/constraint/test_exponential.py | 0 .../model/constraint/test_if_then_else.py | 0 .../model/constraint/test_logarithm.py | 0 .../model/constraint/test_maximum.py | 0 .../model/constraint/test_member.py | 0 .../model/constraint/test_minimum.py | 0 .../model/constraint/test_modulo.py | 0 .../model/constraint/test_multiply.py | 0 .../{ => core}/model/constraint/test_nroot.py | 0 .../{ => core}/model/constraint/test_power.py | 0 .../model/constraint/test_relational.py | 0 tests/{ => core}/model/constraint/test_sin.py | 0 .../{ => core}/model/constraint/test_sort.py | 0 tests/{ => core}/model/constraint/test_tan.py | 0 tests/core/model/test_cutoff.py | 164 +++++++++ tests/core/model/test_function.py | 62 ++++ tests/core/model/test_modeller.py | 243 ++++++++++++++ tests/core/model/variable/test_boolean.py | 138 ++++++++ .../model/variable/test_expression.py | 0 tests/core/model/variable/test_float.py | 180 ++++++++++ tests/core/model/variable/test_integer.py | 291 ++++++++++++++++ tests/{ => core}/test_explanation.py | 0 tests/core/test_response.py | 105 ++++++ tests/core/test_solution.py | 170 ++++++++++ tests/model/constraint/test_distinct.py | 151 --------- tests/model/test_modeller.py | 105 ------ tests/model/variable/test_integer.py | 102 ------ tests/test_solution.py | 65 ---- 78 files changed, 2339 insertions(+), 1039 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 pyproject.toml delete mode 100644 qaekwy/core/model/relation.py delete mode 100644 setup.py create mode 100644 tests/api/test_model.py rename tests/{ => core}/model/constraint/test_abs.py (100%) rename tests/{ => core}/model/constraint/test_acos.py (100%) rename tests/{ => core}/model/constraint/test_asin.py (100%) rename tests/{ => core}/model/constraint/test_atan.py (100%) rename tests/{ => core}/model/constraint/test_cos.py (100%) create mode 100644 tests/core/model/constraint/test_distinct.py rename tests/{ => core}/model/constraint/test_divide.py (100%) rename tests/{ => core}/model/constraint/test_element.py (100%) rename tests/{ => core}/model/constraint/test_exponential.py (100%) rename tests/{ => core}/model/constraint/test_if_then_else.py (100%) rename tests/{ => core}/model/constraint/test_logarithm.py (100%) rename tests/{ => core}/model/constraint/test_maximum.py (100%) rename tests/{ => core}/model/constraint/test_member.py (100%) rename tests/{ => core}/model/constraint/test_minimum.py (100%) rename tests/{ => core}/model/constraint/test_modulo.py (100%) rename tests/{ => core}/model/constraint/test_multiply.py (100%) rename tests/{ => core}/model/constraint/test_nroot.py (100%) rename tests/{ => core}/model/constraint/test_power.py (100%) rename tests/{ => core}/model/constraint/test_relational.py (100%) rename tests/{ => core}/model/constraint/test_sin.py (100%) rename tests/{ => core}/model/constraint/test_sort.py (100%) rename tests/{ => core}/model/constraint/test_tan.py (100%) create mode 100644 tests/core/model/test_cutoff.py create mode 100644 tests/core/model/test_function.py create mode 100644 tests/core/model/test_modeller.py create mode 100644 tests/core/model/variable/test_boolean.py rename tests/{ => core}/model/variable/test_expression.py (100%) create mode 100644 tests/core/model/variable/test_float.py create mode 100644 tests/core/model/variable/test_integer.py rename tests/{ => core}/test_explanation.py (100%) create mode 100644 tests/core/test_response.py create mode 100644 tests/core/test_solution.py delete mode 100644 tests/model/constraint/test_distinct.py delete mode 100644 tests/model/test_modeller.py delete mode 100644 tests/model/variable/test_integer.py delete mode 100644 tests/test_solution.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6eb27d0 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + pull_request: + branches: + - main + push: + branches: + - "dev-[0-9]*-[0-9]*-[0-9]*" + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: pip + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ".[dev]" + pip install requests types-requests + + - name: Lint with Black + run: black --check qaekwy + + - name: Lint with Pylint + run: pylint qaekwy + + - name: Type check with MyPy + run: mypy qaekwy + + - name: Test with Pytest + coverage + run: | + python -m pytest --cov=qaekwy --cov-report=xml --cov-fail-under=80 tests + +# - name: Upload coverage to Codecov +# uses: codecov/codecov-action@v4 +# with: +# files: coverage.xml +# fail_ci_if_error: true diff --git a/README.md b/README.md index 4406d70..bfb0a2f 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@ Qaekwy is a Python library designed for modeling and solving combinatorial optim It provides a clean, Pythonic interface for defining variables, constraints, and objectives, enabling a natural *define-and-solve* workflow. Qaekwy manages the interaction with the solver engine, allowing users to focus entirely on expressing the structure of their problems. -Perfect for: +#### Perfect for -* 🎓 **Learning**: Students can *quickly model* and solve problems. -* 👩‍🏫 **Teaching**: Instructors can *demo core CSP concepts* with **minimal setup**. -* 🌟 **Discovering**: Researchers can *explore* strategies, heuristics, and models. +* 🎓 **Learning** — Model real problems in minutes +* 👩‍🏫 **Teaching** — Demonstrate CSP concepts with no setup +* 🔬 **Research & Prototyping** — Explore models, heuristics, and ideas fast ## 🚀 Quick Start @@ -26,11 +26,11 @@ Perfect for: ### Installation -Install the production-ready client via PyPI: - -`pip install qaekwy` +```shell +pip install qaekwy +``` -### Your First Model +### 🌱 Your First Model ```python import qaekwy as qw @@ -42,12 +42,9 @@ y = m.integer_variable("y", (-10, 10)) z = m.integer_variable("z", (-10, 10)) m.constraint(x + 2*y + 3*z <= 15) -m.constraint(x + y >= 5) - -m.minimize(z) +m.maximize(x) -solution = m.solve_one() -solution.pretty_print() +m.solve_one().pretty_print() ``` *Output*: @@ -56,14 +53,12 @@ solution.pretty_print() ---------------------------------------- Solution: ---------------------------------------- -x: 6 -y: 8 -z: -3 +x: -3 +y: 2 +z: 4 ---------------------------------------- ``` -*That's it.* - ## Capabilities @@ -84,11 +79,14 @@ Transparent handling of model serialization and execution on the Qaekwy Cloud So Visit the [Qaekwy Documentation](https://docs.qaekwy.io/) for guides, teaching resources, and detailed examples. ---- +## Examples + +### 🔢 Constraint Programming -- Sudoku -### 🧩 Constraint Programming Example: Sudoku +Here is a complete example solving a [Sudoku](https://en.wikipedia.org/wiki/Sudoku) grid: -Here is a complete example solving a Sudoku grid: +> The objective is to fill a 9 × 9 grid with digits so that each column, each row, and each +> of the nine 3 × 3 subgrids that compose the grid contains all of the digits from 1 to 9. ```python import qaekwy as qw @@ -108,7 +106,7 @@ my_problem = [ [0, 2, 0, 0, 0, 0, 5, 7, 0] ] -# Initialize the model container +# Instantiate the model m = qw.Model() # Create a 9x9 matrix of integer variables @@ -122,7 +120,7 @@ for i in range(9): # Ensure all variables in column 'i' are unique m.constraint_distinct(grid.col(i)) -# Iterate in steps of 3 (0, 3, 6) to find the top-left corner of each block +# Iterate over 3x3 blocks for i in range(0, 9, 3): for j in range(0, 9, 3): # Extract the 3x3 block and enforce uniqueness @@ -161,7 +159,7 @@ grid: (9 x 9 matrix) ---------------------------------------- ``` -### 🎒 Optimization Example: Knapsack Problem +### 🎒 Optimization -- Knapsack Problem Here is a complete example solving a basic resource allocation problem ([The Knapsack Problem](https://en.wikipedia.org/wiki/Knapsack_problem)): @@ -207,18 +205,18 @@ print(f"Max Value: {solution.total_value}") # Output: Max Value: 9 ``` -#### 💡 Core Concepts +## 💡 Core Concepts

Qaekwy core concept

-##### The Model +### The Model The `qw.Model` acts as the container for your variables and constraints. It also manages the interaction with the underlying solver engine. -##### The Variables +#### The Variables Here are examples of variable creation in the model: @@ -230,7 +228,7 @@ capacity = m.integer_variable("capacity", domain=(0, 100)) grid = m.integer_matrix("grid", rows=9, cols=9, domain=(1, 9)) ``` -##### The Constraints +#### The Constraints Constraints are logical assertions that must be true in any valid solution. @@ -239,7 +237,7 @@ Constraints are logical assertions that must be true in any valid solution. m.constraint(x * 2 < qw.math.power(y, 2) + 5) ``` -#### Modeling Capabilities +### Modeling Capabilities Qaekwy supports: @@ -278,9 +276,9 @@ m.constraint(sum(mat.col(0)) > arr[2]) - `solve_one()` — find one feasible or optimal solution - `solve()` — returns a list of solutions -- minimize(...) / maximize(...) — Set one or more objectives on variables +- `minimize(...)` / `maximize(...)` — Set one or more objectives on variables - Searchers such as DFS, Branch-and-Bound, etc. -- Cloud-based Solver instance (please, refer to [Terms & Conditions](https://docs.qaekwy.io/docs/terms-and-conditions/)) +- Cloud-based Solver instance (*please, refer to [Terms & Conditions](https://docs.qaekwy.io/docs/terms-and-conditions/)*) #### Integration @@ -291,7 +289,6 @@ The model is then sent to the Qaekwy Cloud Engine through REST API. Qaekwy Integration

---- ## License diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3ddce64 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,67 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "qaekwy" +dynamic = ["version"] +description = "Qaekwy, a modern, open-source Python framework for declarative constraint programming and combinatorial optimization" +readme = "README.md" +requires-python = ">=3.9" +license = "EUPL-1.2" +authors = [ + {name = "Alexis LE GOADEC", email = "alex@qaekwy.io"}, +] +keywords = [ + "optimization", "constraint programming", "combinatorial optimization", + "operations research", "constraint satisfaction", "constraint solver", + "solver", "CSP", "optimization library", "optimization framework", + "mathematical optimization", "discrete optimization", "optimization modeling", + "constraint modeling", "declarative programming", "modeling language", + "DSL", "define-and-solve", "scheduling", "routing", "planning", + "resource allocation", "assignment", "decision support", "open source", "Python" +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development :: Libraries :: Python Modules", +] +dependencies = [ + "requests" +] + +[project.optional-dependencies] +types = [ + "types-requests", +] +dev = [ + "pytest>=7.0", + "pytest-cov", + "black", + "mypy", + "build", + "twine", +] + +[project.urls] +Homepage = "https://qaekwy.io" +Documentation = "https://docs.qaekwy.io" +"Issue Tracker" = "https://github.com/alex-87/qaekwy-python/issues" +Source = "https://github.com/alex-87/qaekwy-python" + +[tool.setuptools.dynamic] +version = {attr = "qaekwy.__metadata__.__version__"} + +[tool.setuptools.packages.find] +where = ["."] +include = ["qaekwy*"] + +[tool.pylint] +disable = ["R0801", "R0917", "R0913", "R0902", "R0903", "C0301", "R0911", "R0912"] diff --git a/qaekwy/__init__.py b/qaekwy/__init__.py index 22cf204..dcbefab 100644 --- a/qaekwy/__init__.py +++ b/qaekwy/__init__.py @@ -1,29 +1,15 @@ """QAekwy API Module Initialization.""" -from qaekwy.api.model import Model -from qaekwy.api.exceptions import SolverError -from qaekwy.core.model.variable.branch import ( - BranchBooleanVal, - BranchBooleanVar, - BranchIntegerVal, - BranchIntegerVar, - BranchFloatVal, - BranchFloatVar, -) -from qaekwy.core.model.cutoff import ( - Cutoff, - CutoffConstant, - CutoffFibonacci, - CutoffLinear, - CutoffLuby, - CutoffGeometric, - CutoffRandom, - MetaCutoffAppender, - MetaCutoffMerger, - MetaCutoffRepeater, -) - -import qaekwy.core.model.function as math +from .api.exceptions import SolverError +from .api.model import Model +from .core.model import function as math +from .core.model.cutoff import (Cutoff, CutoffConstant, CutoffFibonacci, + CutoffGeometric, CutoffLinear, CutoffLuby, + CutoffRandom, MetaCutoffAppender, + MetaCutoffMerger, MetaCutoffRepeater) +from .core.model.variable.branch import (BranchBooleanVal, BranchBooleanVar, + BranchFloatVal, BranchFloatVar, + BranchIntegerVal, BranchIntegerVar) __all__ = [ "Model", diff --git a/qaekwy/__main__.py b/qaekwy/__main__.py index b498e02..aa2407f 100644 --- a/qaekwy/__main__.py +++ b/qaekwy/__main__.py @@ -4,14 +4,8 @@ import argparse -from qaekwy.__metadata__ import ( - __author__, - __copyright__, - __license__, - __license_url__, - __software__, - __version__, -) +from .__metadata__ import (__author__, __copyright__, __license__, + __license_url__, __software__, __version__) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Qaekwy Python Library") @@ -29,5 +23,5 @@ print(f"Licensed under the {__license__}") print(f"You may obtain a copy of the License at {__license_url__}") print( - "You are free to use, modify, and redistribute this work under the terms of this licence." + "You are free to use, modify, and redistribute this work under the terms of this license." ) diff --git a/qaekwy/__metadata__.py b/qaekwy/__metadata__.py index 17e20b7..66f9975 100644 --- a/qaekwy/__metadata__.py +++ b/qaekwy/__metadata__.py @@ -8,4 +8,4 @@ __license_url__ = "https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12" __software__ = "Qaekwy" __author_email__ = "alex@qaekwy.io" -__version__ = "0.3.0" +__version__ = "0.3.1" diff --git a/qaekwy/api/model.py b/qaekwy/api/model.py index 26c3cf1..2867479 100644 --- a/qaekwy/api/model.py +++ b/qaekwy/api/model.py @@ -14,70 +14,55 @@ as well as for solving the model and retrieving solutions. """ -from typing import Optional - -from qaekwy.api.exceptions import SolverError -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.constraint.multiply import ConstraintMultiply -from qaekwy.core.solution import Solution - -from qaekwy.core.model.modeller import Modeller -from qaekwy.core.response import SolutionResponse - -from qaekwy.core.model import DIRECTENGINE_API_ENDPOINT - -from qaekwy.core.model.variable.variable import ( - MatrixVariable, - Variable, - ArrayVariable, - Expression, - VariableType, - VectorExpression, -) -from qaekwy.core.model.variable.integer import ( - IntegerVariable, - IntegerExpressionVariable, -) -from qaekwy.core.model.variable.integer import IntegerVariableArray -from qaekwy.core.model.variable.branch import BranchIntegerVal, BranchIntegerVar -from qaekwy.core.model.variable.float import FloatVariable, FloatExpressionVariable -from qaekwy.core.model.variable.float import FloatVariableArray -from qaekwy.core.model.variable.branch import BranchFloatVal, BranchFloatVar -from qaekwy.core.model.variable.boolean import BooleanVariable -from qaekwy.core.model.variable.boolean import BooleanVariableArray -from qaekwy.core.model.variable.branch import BranchBooleanVal, BranchBooleanVar - -from qaekwy.core.model.constraint.abs import ConstraintAbs -from qaekwy.core.model.constraint.acos import ConstraintACos -from qaekwy.core.model.constraint.asin import ConstraintASin -from qaekwy.core.model.constraint.atan import ConstraintATan -from qaekwy.core.model.constraint.cos import ConstraintCos -from qaekwy.core.model.constraint.distinct import ( - ConstraintDistinctArray, - ConstraintDistinctRow, - ConstraintDistinctCol, - ConstraintDistinctSlice, -) -from qaekwy.core.model.constraint.divide import ConstraintDivide -from qaekwy.core.model.constraint.element import ConstraintElement -from qaekwy.core.model.constraint.exponential import ConstraintExponential -from qaekwy.core.model.constraint.sin import ConstraintSin -from qaekwy.core.model.constraint.tan import ConstraintTan -from qaekwy.core.model.constraint.logarithm import ConstraintLogarithm -from qaekwy.core.model.constraint.maximum import ConstraintMaximum -from qaekwy.core.model.constraint.minimum import ConstraintMinimum -from qaekwy.core.model.constraint.member import ConstraintMember -from qaekwy.core.model.constraint.modulo import ConstraintModulo -from qaekwy.core.model.constraint.nroot import ConstraintNRoot -from qaekwy.core.model.constraint.power import ConstraintPower -from qaekwy.core.model.constraint.if_then_else import ConstraintIfThenElse -from qaekwy.core.model.constraint.sort import ConstraintSorted, ConstraintReverseSorted - -from qaekwy.core.model.specific import SpecificMaximum, SpecificMinimum - -from qaekwy.core.model.searcher import SearcherType -from qaekwy.core.model.cutoff import Cutoff -from qaekwy.core.engine import DirectEngine +from typing import Optional, Union + +from ..core.engine import DirectEngine +from ..core.model import DIRECTENGINE_API_ENDPOINT +from ..core.model.constraint.abs import ConstraintAbs +from ..core.model.constraint.abstract_constraint import AbstractConstraint +from ..core.model.constraint.acos import ConstraintACos +from ..core.model.constraint.asin import ConstraintASin +from ..core.model.constraint.atan import ConstraintATan +from ..core.model.constraint.cos import ConstraintCos +from ..core.model.constraint.distinct import (ConstraintDistinctArray, + ConstraintDistinctCol, + ConstraintDistinctRow, + ConstraintDistinctSlice) +from ..core.model.constraint.divide import ConstraintDivide +from ..core.model.constraint.element import ConstraintElement +from ..core.model.constraint.exponential import ConstraintExponential +from ..core.model.constraint.if_then_else import ConstraintIfThenElse +from ..core.model.constraint.logarithm import ConstraintLogarithm +from ..core.model.constraint.maximum import ConstraintMaximum +from ..core.model.constraint.member import ConstraintMember +from ..core.model.constraint.minimum import ConstraintMinimum +from ..core.model.constraint.modulo import ConstraintModulo +from ..core.model.constraint.multiply import ConstraintMultiply +from ..core.model.constraint.nroot import ConstraintNRoot +from ..core.model.constraint.power import ConstraintPower +from ..core.model.constraint.sin import ConstraintSin +from ..core.model.constraint.sort import (ConstraintReverseSorted, + ConstraintSorted) +from ..core.model.constraint.tan import ConstraintTan +from ..core.model.cutoff import Cutoff +from ..core.model.modeller import Modeller +from ..core.model.searcher import SearcherType +from ..core.model.specific import SpecificMaximum, SpecificMinimum +from ..core.model.variable.boolean import BooleanVariable, BooleanVariableArray, BooleanVariableMatrix +from ..core.model.variable.branch import (BranchBooleanVal, BranchBooleanVar, + BranchFloatVal, BranchFloatVar, + BranchIntegerVal, BranchIntegerVar) +from ..core.model.variable.float import (FloatExpressionVariable, + FloatVariable, FloatVariableArray, FloatVariableMatrix) +from ..core.model.variable.integer import (IntegerExpressionVariable, + IntegerVariable, + IntegerVariableArray, IntegerVariableMatrix) +from ..core.model.variable.variable import (ArrayVariable, Expression, + MatrixVariable, Variable, + VariableType, VectorExpression) +from ..core.response import SolutionResponse +from ..core.solution import Solution +from .exceptions import SolverError class Model: # pylint: disable=too-many-public-methods @@ -109,7 +94,7 @@ def integer_variable( expression: Optional[str] = None, branch_val: BranchIntegerVal = BranchIntegerVal.VAL_RND, branch_order: Optional[int] = -1, - ) -> IntegerVariable | IntegerExpressionVariable: + ) -> Union[IntegerVariable, IntegerExpressionVariable]: """ Creates an integer variable or an integer expression variable. @@ -195,7 +180,7 @@ def integer_matrix( branch_var: BranchIntegerVar = BranchIntegerVar.VAR_RND, branch_val: BranchIntegerVal = BranchIntegerVal.VAL_RND, branch_order: Optional[int] = -1, - ) -> MatrixVariable: + ) -> IntegerVariableMatrix: """ Creates a matrix of integer variables. @@ -210,10 +195,10 @@ def integer_matrix( branch_order (int, optional): The branching order. Returns: - MatrixVariable: The created variable matrix. + IntegerVariableMatrix: The created variable matrix. """ l, h = domain - v: MatrixVariable = MatrixVariable( + v: IntegerVariableMatrix = IntegerVariableMatrix( var_name=name, rows=rows, cols=cols, @@ -234,7 +219,7 @@ def float_variable( expression: Optional[str] = None, branch_val: BranchFloatVal = BranchFloatVal.VAL_RND, branch_order: int = -1, - ) -> FloatVariable | FloatExpressionVariable: + ) -> Union[FloatVariable, FloatExpressionVariable]: """ Creates a float variable or a float expression variable. @@ -310,11 +295,10 @@ def float_matrix( rows: int, cols: int, domain: tuple[float, float], - specific_domain: Optional[list[float]] = None, branch_var: BranchFloatVar = BranchFloatVar.VAR_RND, branch_val: BranchFloatVal = BranchFloatVal.VAL_RND, branch_order: Optional[int] = -1, - ) -> MatrixVariable: + ) -> FloatVariableMatrix: """ Creates a matrix of float variables. @@ -323,23 +307,20 @@ def float_matrix( rows (int): The number of rows in the matrix. cols (int): The number of columns in the matrix. domain (tuple[float, float]): A tuple representing the (low, high) domain of the variables. - specific_domain (list[float], optional): A specific domain for the variables. branch_var (BranchFloatVar, optional): The brancher variable strategy. branch_val (BranchFloatVal, optional): The brancher value strategy. branch_order (int, optional): The branching order. Returns: - MatrixVariable: The created variable matrix. + FloatVariableMatrix: The created variable matrix. """ l, h = domain - v: MatrixVariable = MatrixVariable( + v: FloatVariableMatrix = FloatVariableMatrix( var_name=name, rows=rows, cols=cols, - var_type=VariableType.FLOAT_ARRAY, domain_low=l, domain_high=h, - specific_domain=specific_domain, branch_var=branch_var, branch_val=branch_val, branch_order=branch_order, @@ -406,12 +387,10 @@ def boolean_matrix( name: str, rows: int, cols: int, - domain: tuple[bool, bool], - specific_domain: Optional[list[bool]] = None, branch_var: BranchBooleanVar = BranchBooleanVar.VAR_RND, branch_val: BranchBooleanVal = BranchBooleanVal.VAL_RND, branch_order: Optional[int] = -1, - ) -> MatrixVariable: + ) -> BooleanVariableMatrix: """ Creates a matrix of boolean variables. @@ -419,24 +398,17 @@ def boolean_matrix( name (str): The name of the variable matrix. rows (int): The number of rows in the matrix. cols (int): The number of columns in the matrix. - domain (tuple[boolean, boolean]): A tuple representing the (low, high) domain of the variables. - specific_domain (list[boolean], optional): A specific domain for the variables. branch_var (BranchBooleanVar, optional): The brancher variable strategy. branch_val (BranchBooleanVal, optional): The brancher value strategy. branch_order (int, optional): The branching order. Returns: - MatrixVariable: The created variable matrix. + BooleanVariableMatrix: The created variable matrix. """ - l, h = domain - v: MatrixVariable = MatrixVariable( + v: BooleanVariableMatrix = BooleanVariableMatrix( var_name=name, rows=rows, cols=cols, - var_type=VariableType.BOOLEAN_ARRAY, - domain_low=l, - domain_high=h, - specific_domain=specific_domain, branch_var=branch_var, branch_val=branch_val, branch_order=branch_order, @@ -509,7 +481,7 @@ def constraint_cos(self, var_1: Variable, var_2: Variable) -> None: constraint = ConstraintCos(var_1, var_2) self._modeller.add_constraint(constraint) - def constraint_distinct(self, var: ArrayVariable | VectorExpression) -> None: + def constraint_distinct(self, var: Union[ArrayVariable, VectorExpression]) -> None: """ Add a distinct constraint. @@ -808,8 +780,8 @@ def solve( self, searcher: str = "dfs", solution_limit: int = 1, - cutoff: Cutoff | None = None, - ) -> list[Solution] | None: + cutoff: Union[Cutoff, None] = None, + ) -> Union[list[Solution], None]: """ Solves the model. @@ -852,8 +824,8 @@ def solve( return solution_response.get_solutions() def solve_one( - self, searcher: str = "dfs", cutoff: Cutoff | None = None - ) -> Solution | None: + self, searcher: str = "dfs", cutoff: Union[Cutoff, None] = None + ) -> Union[Solution, None]: """ Solves the model and returns one solution. diff --git a/qaekwy/core/engine.py b/qaekwy/core/engine.py index 2d40413..3d684bf 100644 --- a/qaekwy/core/engine.py +++ b/qaekwy/core/engine.py @@ -78,23 +78,12 @@ import requests -from qaekwy.core.model import DIRECTENGINE_API_ENDPOINT -from qaekwy.core.model.modeller import Modeller -from qaekwy.core.response import ( - AbstractResponse, - ClusterStatusResponse, - EchoResponse, - ExplanationResponse, - ModelJSonResponse, - SolutionResponse, - StatusResponse, - VersionResponse, -) - -from qaekwy.__metadata__ import ( - __software__, - __version__, -) +from ..__metadata__ import __software__, __version__ +from .model import DIRECTENGINE_API_ENDPOINT +from .model.modeller import Modeller +from .response import (AbstractResponse, ClusterStatusResponse, EchoResponse, + ExplanationResponse, ModelJSonResponse, + SolutionResponse, StatusResponse, VersionResponse) class AbstractAction(ABC): diff --git a/qaekwy/core/model/constraint/abs.py b/qaekwy/core/model/constraint/abs.py index a47145f..ecaaf1d 100644 --- a/qaekwy/core/model/constraint/abs.py +++ b/qaekwy/core/model/constraint/abs.py @@ -2,8 +2,8 @@ This module defines the ConstraintAbs class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintAbs(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/acos.py b/qaekwy/core/model/constraint/acos.py index 4553b43..88c7a70 100644 --- a/qaekwy/core/model/constraint/acos.py +++ b/qaekwy/core/model/constraint/acos.py @@ -2,8 +2,8 @@ This module defines the ConstraintACos class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintACos(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/asin.py b/qaekwy/core/model/constraint/asin.py index 337ecd6..495a0ef 100644 --- a/qaekwy/core/model/constraint/asin.py +++ b/qaekwy/core/model/constraint/asin.py @@ -2,8 +2,8 @@ This module defines the ConstraintASin class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintASin(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/atan.py b/qaekwy/core/model/constraint/atan.py index 6f6e189..6dc2cb4 100644 --- a/qaekwy/core/model/constraint/atan.py +++ b/qaekwy/core/model/constraint/atan.py @@ -2,8 +2,8 @@ This module defines the ConstraintATan class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintATan(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/cos.py b/qaekwy/core/model/constraint/cos.py index a339c23..f233081 100644 --- a/qaekwy/core/model/constraint/cos.py +++ b/qaekwy/core/model/constraint/cos.py @@ -2,8 +2,8 @@ This module defines the ConstraintCos class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintCos(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/distinct.py b/qaekwy/core/model/constraint/distinct.py index 3007068..937ca87 100644 --- a/qaekwy/core/model/constraint/distinct.py +++ b/qaekwy/core/model/constraint/distinct.py @@ -2,8 +2,8 @@ This module defines constraints for enforcing distinctness in arrays. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import ArrayVariable, MatrixVariable +from ..variable.variable import ArrayVariable, MatrixVariable +from .abstract_constraint import AbstractConstraint class ConstraintDistinctArray(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/divide.py b/qaekwy/core/model/constraint/divide.py index f54785c..9f0e058 100644 --- a/qaekwy/core/model/constraint/divide.py +++ b/qaekwy/core/model/constraint/divide.py @@ -2,8 +2,8 @@ This module defines the ConstraintDivide class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintDivide(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/element.py b/qaekwy/core/model/constraint/element.py index 8118e01..92fe25e 100644 --- a/qaekwy/core/model/constraint/element.py +++ b/qaekwy/core/model/constraint/element.py @@ -2,8 +2,8 @@ This module defines the ConstraintElement class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import ArrayVariable, Variable +from ..variable.variable import ArrayVariable, Variable +from .abstract_constraint import AbstractConstraint class ConstraintElement(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/exponential.py b/qaekwy/core/model/constraint/exponential.py index cd41732..0736ed0 100644 --- a/qaekwy/core/model/constraint/exponential.py +++ b/qaekwy/core/model/constraint/exponential.py @@ -2,8 +2,8 @@ This module defines the ConstraintExponential class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintExponential(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/if_then_else.py b/qaekwy/core/model/constraint/if_then_else.py index ff9e034..e71912c 100644 --- a/qaekwy/core/model/constraint/if_then_else.py +++ b/qaekwy/core/model/constraint/if_then_else.py @@ -3,8 +3,9 @@ """ from typing import Optional -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Expression, VariableType + +from ..variable.variable import Expression, VariableType +from .abstract_constraint import AbstractConstraint class ConstraintIfThenElse(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/logarithm.py b/qaekwy/core/model/constraint/logarithm.py index 652f56a..0b40b0f 100644 --- a/qaekwy/core/model/constraint/logarithm.py +++ b/qaekwy/core/model/constraint/logarithm.py @@ -2,8 +2,8 @@ This module defines the ConstraintLogarithm class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintLogarithm(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/maximum.py b/qaekwy/core/model/constraint/maximum.py index 9b2c894..3beb5a9 100644 --- a/qaekwy/core/model/constraint/maximum.py +++ b/qaekwy/core/model/constraint/maximum.py @@ -2,8 +2,8 @@ This module defines the ConstraintMaximum class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintMaximum(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/member.py b/qaekwy/core/model/constraint/member.py index faa0953..e65d54e 100644 --- a/qaekwy/core/model/constraint/member.py +++ b/qaekwy/core/model/constraint/member.py @@ -2,8 +2,8 @@ This module defines the ConstraintMember class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import ArrayVariable, Variable +from ..variable.variable import ArrayVariable, Variable +from .abstract_constraint import AbstractConstraint class ConstraintMember(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/minimum.py b/qaekwy/core/model/constraint/minimum.py index d5bac20..9cbcb7e 100644 --- a/qaekwy/core/model/constraint/minimum.py +++ b/qaekwy/core/model/constraint/minimum.py @@ -2,8 +2,8 @@ This module defines the ConstraintMinimum class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintMinimum(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/modulo.py b/qaekwy/core/model/constraint/modulo.py index acf51b3..f876d74 100644 --- a/qaekwy/core/model/constraint/modulo.py +++ b/qaekwy/core/model/constraint/modulo.py @@ -2,8 +2,8 @@ This module defines the ConstraintModulo class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintModulo(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/multiply.py b/qaekwy/core/model/constraint/multiply.py index d4d8cf1..0ae0694 100644 --- a/qaekwy/core/model/constraint/multiply.py +++ b/qaekwy/core/model/constraint/multiply.py @@ -2,8 +2,8 @@ This module defines the ConstraintMultiply class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintMultiply(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/nroot.py b/qaekwy/core/model/constraint/nroot.py index 5c34d60..e054d77 100644 --- a/qaekwy/core/model/constraint/nroot.py +++ b/qaekwy/core/model/constraint/nroot.py @@ -2,8 +2,8 @@ This module defines the ConstraintNRoot class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintNRoot(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/power.py b/qaekwy/core/model/constraint/power.py index 2dc6ef5..bd386f2 100644 --- a/qaekwy/core/model/constraint/power.py +++ b/qaekwy/core/model/constraint/power.py @@ -2,8 +2,8 @@ This module defines the ConstraintPower class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintPower(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/relational.py b/qaekwy/core/model/constraint/relational.py index 3108304..4a7644b 100644 --- a/qaekwy/core/model/constraint/relational.py +++ b/qaekwy/core/model/constraint/relational.py @@ -2,8 +2,8 @@ This module defines the RelationalExpression class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Expression, VariableType +from ..variable.variable import Expression, VariableType +from .abstract_constraint import AbstractConstraint class RelationalExpression(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/sin.py b/qaekwy/core/model/constraint/sin.py index 221bdc0..3451ed0 100644 --- a/qaekwy/core/model/constraint/sin.py +++ b/qaekwy/core/model/constraint/sin.py @@ -2,8 +2,8 @@ This module defines the ConstraintSin class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintSin(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/sort.py b/qaekwy/core/model/constraint/sort.py index bfb28f4..d67ee9b 100644 --- a/qaekwy/core/model/constraint/sort.py +++ b/qaekwy/core/model/constraint/sort.py @@ -2,8 +2,8 @@ This module defines constraints for sorting arrays. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import ArrayVariable +from ..variable.variable import ArrayVariable +from .abstract_constraint import AbstractConstraint class ConstraintSorted(AbstractConstraint): diff --git a/qaekwy/core/model/constraint/tan.py b/qaekwy/core/model/constraint/tan.py index a7ec2ea..4281664 100644 --- a/qaekwy/core/model/constraint/tan.py +++ b/qaekwy/core/model/constraint/tan.py @@ -2,8 +2,8 @@ This module defines the ConstraintTan class. """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from ..variable.variable import Variable +from .abstract_constraint import AbstractConstraint class ConstraintTan(AbstractConstraint): diff --git a/qaekwy/core/model/function.py b/qaekwy/core/model/function.py index 55e30ba..45d0e1f 100644 --- a/qaekwy/core/model/function.py +++ b/qaekwy/core/model/function.py @@ -23,7 +23,7 @@ """ -from qaekwy.core.model.variable.variable import Expression, ExpressionArray +from .variable.variable import Expression, ExpressionArray def maximum(expr: ExpressionArray) -> Expression: diff --git a/qaekwy/core/model/modeller.py b/qaekwy/core/model/modeller.py index 5bc15e9..2d93d30 100644 --- a/qaekwy/core/model/modeller.py +++ b/qaekwy/core/model/modeller.py @@ -18,49 +18,44 @@ json_model = modeller.to_json() """ -from typing import Any, Optional, Union - -from qaekwy.core.exception.model_failure import ModelFailure - -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.constraint.abs import ConstraintAbs -from qaekwy.core.model.constraint.acos import ConstraintACos -from qaekwy.core.model.constraint.asin import ConstraintASin -from qaekwy.core.model.constraint.atan import ConstraintATan -from qaekwy.core.model.constraint.cos import ConstraintCos -from qaekwy.core.model.constraint.distinct import ( - ConstraintDistinctArray, - ConstraintDistinctCol, - ConstraintDistinctRow, - ConstraintDistinctSlice, -) -from qaekwy.core.model.constraint.divide import ConstraintDivide -from qaekwy.core.model.constraint.element import ConstraintElement -from qaekwy.core.model.constraint.exponential import ConstraintExponential -from qaekwy.core.model.constraint.logarithm import ConstraintLogarithm -from qaekwy.core.model.constraint.maximum import ConstraintMaximum -from qaekwy.core.model.constraint.minimum import ConstraintMinimum -from qaekwy.core.model.constraint.modulo import ConstraintModulo -from qaekwy.core.model.constraint.member import ConstraintMember -from qaekwy.core.model.constraint.multiply import ConstraintMultiply -from qaekwy.core.model.constraint.nroot import ConstraintNRoot -from qaekwy.core.model.constraint.power import ConstraintPower -from qaekwy.core.model.constraint.relational import RelationalExpression -from qaekwy.core.model.constraint.sin import ConstraintSin -from qaekwy.core.model.constraint.sort import ConstraintSorted, ConstraintReverseSorted -from qaekwy.core.model.constraint.tan import ConstraintTan -from qaekwy.core.model.constraint.if_then_else import ConstraintIfThenElse - -from qaekwy.core.model.cutoff import Cutoff -from qaekwy.core.model.searcher import SearcherType -from qaekwy.core.model.specific import SpecificMaximum, SpecificMinimum -from qaekwy.core.model.variable.variable import ( - ArrayVariable, - Expression, - MatrixVariable, - Variable, - VariableType, -) +from typing import Any, Callable, Dict, Optional, Union + +from ..exception.model_failure import ModelFailure +from .constraint.abs import ConstraintAbs +from .constraint.abstract_constraint import AbstractConstraint +from .constraint.acos import ConstraintACos +from .constraint.asin import ConstraintASin +from .constraint.atan import ConstraintATan +from .constraint.cos import ConstraintCos +from .constraint.distinct import (ConstraintDistinctArray, + ConstraintDistinctCol, ConstraintDistinctRow, + ConstraintDistinctSlice) +from .constraint.divide import ConstraintDivide +from .constraint.element import ConstraintElement +from .constraint.exponential import ConstraintExponential +from .constraint.if_then_else import ConstraintIfThenElse +from .constraint.logarithm import ConstraintLogarithm +from .constraint.maximum import ConstraintMaximum +from .constraint.member import ConstraintMember +from .constraint.minimum import ConstraintMinimum +from .constraint.modulo import ConstraintModulo +from .constraint.multiply import ConstraintMultiply +from .constraint.nroot import ConstraintNRoot +from .constraint.power import ConstraintPower +from .constraint.relational import RelationalExpression +from .constraint.sin import ConstraintSin +from .constraint.sort import ConstraintReverseSorted, ConstraintSorted +from .constraint.tan import ConstraintTan +from .cutoff import Cutoff +from .searcher import SearcherType +from .specific import SpecificMaximum, SpecificMinimum +from .variable.variable import (ArrayVariable, Expression, MatrixVariable, + Variable, VariableType) + +ConstraintFactory = Callable[ + [dict, list[Union[Variable, ArrayVariable, MatrixVariable]]], + AbstractConstraint, +] class Modeller: @@ -68,7 +63,7 @@ class Modeller: Constructs and configures optimization models. Attributes: - variable_list (list[Union[Variable, ArrayVariable]]): Collection of model variables. + variable_list (list[Union[Variable, ArrayVariable, MatrixVariable]]): Collection of model variables. constraint_list (list[Union[AbstractConstraint]]): Collection of model constraints. objective_list (list[Union[SpecificMinimum, SpecificMaximum]]): Optimization objectives (minimize or maximize). searcher (SearcherType): Strategy for searching solution space. @@ -77,11 +72,40 @@ class Modeller: solution_limit (int): Maximum number of solutions to return. """ + CONSTRAINT_REGISTRY: Dict[str, ConstraintFactory] = { + "abs": ConstraintAbs.from_json, + "acos": ConstraintACos.from_json, + "asin": ConstraintASin.from_json, + "atan": ConstraintATan.from_json, + "cos": ConstraintCos.from_json, + "distinct_array": ConstraintDistinctArray.from_json, + "distinct_col": ConstraintDistinctCol.from_json, + "distinct_row": ConstraintDistinctRow.from_json, + "distinct_slice": ConstraintDistinctSlice.from_json, + "divide": ConstraintDivide.from_json, + "element": ConstraintElement.from_json, + "exponential": ConstraintExponential.from_json, + "logarithm": ConstraintLogarithm.from_json, + "maximum": ConstraintMaximum.from_json, + "minimum": ConstraintMinimum.from_json, + "modulo": ConstraintModulo.from_json, + "member": ConstraintMember.from_json, + "multiply": ConstraintMultiply.from_json, + "nroot": ConstraintNRoot.from_json, + "power": ConstraintPower.from_json, + "rel": lambda data, _: RelationalExpression.from_json(data), + "sin": ConstraintSin.from_json, + "sorted": ConstraintSorted.from_json, + "rsorted": ConstraintReverseSorted.from_json, + "tan": ConstraintTan.from_json, + "if_then_else": lambda data, _: ConstraintIfThenElse.from_json(data), + } + def __init__(self) -> None: """ Initializes an empty Modeller instance. """ - self.variable_list: list[Union[Variable, ArrayVariable]] = [] + self.variable_list: list[Union[Variable, ArrayVariable, MatrixVariable]] = [] self.constraint_list: list[Union[AbstractConstraint]] = [] self.objective_list: list[Union[SpecificMinimum, SpecificMaximum]] = [] self.searcher: Optional[SearcherType] = None @@ -89,12 +113,14 @@ def __init__(self) -> None: self.callback_url: Optional[str] = None self.solution_limit: int = 1 - def add_variable(self, variable: Union[Variable, ArrayVariable]) -> "Modeller": + def add_variable( + self, variable: Union[Variable, ArrayVariable, MatrixVariable] + ) -> "Modeller": """ Adds a variable or array of variables to the model. Args: - variable: A single Variable or ArrayVariable instance. + variable: A single Variable, ArrayVariable or MatrixVariable instance. Returns: self: Enables method chaining. @@ -150,7 +176,7 @@ def set_searcher(self, searcher: SearcherType) -> "Modeller": self.searcher = searcher return self - def set_cutoff(self, cutoff: Cutoff | None) -> "Modeller": + def set_cutoff(self, cutoff: Union[Cutoff, None]) -> "Modeller": """ Sets a cutoff condition to terminate optimization early. @@ -229,72 +255,20 @@ def to_json(self, serialization: bool = False) -> dict: @staticmethod def _constraints_factory( - constraint_data: dict, variable_list: list[Union[Variable, ArrayVariable]] + constraint_data: dict, + variable_list: list[Union[Variable, ArrayVariable, MatrixVariable]], ) -> AbstractConstraint: """ Factory method to create a Constraint instance from JSON data. + """ + constraint_type: str = str(constraint_data.get("type")) - Args: - constraint_data (dict): A dictionary containing constraint information. - variable_list (list[Union[Variable, ArrayVariable]]): A list of Variable instances. + try: + factory = Modeller.CONSTRAINT_REGISTRY[constraint_type] + except KeyError as exc: + raise ValueError(f"Unknown constraint type: {constraint_type}") from exc - Returns: - AbstractConstraint: An AbstractConstraint instance. - """ - if constraint_data.get("type") == "abs": - return ConstraintAbs.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "acos": - return ConstraintACos.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "asin": - return ConstraintASin.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "atan": - return ConstraintATan.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "cos": - return ConstraintCos.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "distinct_array": - return ConstraintDistinctArray.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "distinct_col": - return ConstraintDistinctCol.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "distinct_row": - return ConstraintDistinctRow.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "distinct_slice": - return ConstraintDistinctSlice.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "divide": - return ConstraintDivide.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "element": - return ConstraintElement.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "exponential": - return ConstraintExponential.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "logarithm": - return ConstraintLogarithm.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "maximum": - return ConstraintMaximum.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "minimum": - return ConstraintMinimum.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "modulo": - return ConstraintModulo.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "member": - return ConstraintMember.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "multiply": - return ConstraintMultiply.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "nroot": - return ConstraintNRoot.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "power": - return ConstraintPower.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "rel": - return RelationalExpression.from_json(constraint_data) - if constraint_data.get("type") == "sin": - return ConstraintSin.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "sorted": - return ConstraintSorted.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "rsorted": - return ConstraintReverseSorted.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "tan": - return ConstraintTan.from_json(constraint_data, variable_list) - if constraint_data.get("type") == "if_then_else": - return ConstraintIfThenElse.from_json(constraint_data) - - raise ValueError(f"Unknown constraint type: {constraint_data.get('type')}") + return factory(constraint_data, variable_list) @staticmethod def from_json(json_data: dict) -> "Modeller": @@ -308,19 +282,32 @@ def from_json(json_data: dict) -> "Modeller": Modeller: A Modeller instance. """ modeller = Modeller() + + variable_list: list = [] + array_variable_list: list = [] + matrix_variable_list: list = [] + for v in json_data.get("var", []): + var_type = VariableType.from_json(v["type"]) + if var_type in [ + VariableType.INTEGER_ARRAY, + VariableType.FLOAT_ARRAY, + VariableType.BOOLEAN_ARRAY, + ]: + if v.get("subtype", "") == "matrix": + matrix_variable_list.append(MatrixVariable.from_json(v)) + else: + array_variable_list.append(ArrayVariable.from_json(v)) + elif var_type in [ + VariableType.INTEGER, + VariableType.FLOAT, + VariableType.BOOLEAN, + ]: + variable_list.append(Variable.from_json(v)) + modeller.variable_list = ( - [Variable.from_json(v) for v in json_data.get("var", []) if v is not None] - + [ - ArrayVariable.from_json(v) - for v in json_data.get("var", []) - if v is not None - ] - + [ - MatrixVariable.from_json(v) - for v in json_data.get("var", []) - if v is not None - ] + variable_list + array_variable_list + matrix_variable_list ) + modeller.constraint_list = [] for c in json_data.get("constraint", []): constraint = Modeller._constraints_factory(c, modeller.variable_list) diff --git a/qaekwy/core/model/relation.py b/qaekwy/core/model/relation.py deleted file mode 100644 index f61bf3a..0000000 --- a/qaekwy/core/model/relation.py +++ /dev/null @@ -1,52 +0,0 @@ -"""RelationType Module - -This module defines the RelationType enum, which represents different -types of relational comparisons. - -Enums: - RelationType: Represents different types of relational comparisons. - -""" - -from enum import Enum - - -class RelationType(Enum): - """ - Represents different types of relational comparisons. - - The RelationType enum defines symbolic representations of common relational comparisons - used in constraint-based modelling, such as greater than, greater than or equal to, - equal to, not equal to, less than or equal to, and less than. - - Enum Members: - GT (str): Greater than. - GE (str): Greater than or equal to. - EQ (str): Equal to. - NE (str): Not equal to. - LE (str): Less than or equal to. - LT (str): Less than. - - Example: - relation = RelationType.GE # Represents "greater than or equal to" - """ - - GT = "GT" - GE = "GE" - EQ = "EQ" - NE = "NE" - LE = "LE" - LT = "LT" - - @staticmethod - def from_json(json_data: str) -> "RelationType": - """ - Creates a RelationType instance from a string. - - Args: - json_data (str): The string representation of the relation type. - - Returns: - RelationType: An instance of the RelationType enum. - """ - return RelationType(json_data) diff --git a/qaekwy/core/model/specific.py b/qaekwy/core/model/specific.py index 3b3562e..92865ee 100644 --- a/qaekwy/core/model/specific.py +++ b/qaekwy/core/model/specific.py @@ -9,8 +9,8 @@ """ -from qaekwy.core.model.constraint.abstract_constraint import AbstractConstraint -from qaekwy.core.model.variable.variable import Variable +from .constraint.abstract_constraint import AbstractConstraint +from .variable.variable import Variable class SpecificMinimum(AbstractConstraint): diff --git a/qaekwy/core/model/variable/boolean.py b/qaekwy/core/model/variable/boolean.py index d4dc4ff..65d7033 100644 --- a/qaekwy/core/model/variable/boolean.py +++ b/qaekwy/core/model/variable/boolean.py @@ -10,20 +10,10 @@ """ from typing import Optional -from qaekwy.core.model.variable.branch import ( - BranchBooleanVal, - BranchBooleanVar, - BranchVal, - BranchVar, -) -from qaekwy.core.model.variable.variable import ( - ArrayVariable, - Expression, - ExpressionVariable, - MatrixVariable, - Variable, - VariableType, -) + +from .branch import BranchBooleanVal, BranchBooleanVar, BranchVal, BranchVar +from .variable import (ArrayVariable, Expression, ExpressionVariable, + MatrixVariable, Variable, VariableType) class BooleanVariable(Variable): diff --git a/qaekwy/core/model/variable/float.py b/qaekwy/core/model/variable/float.py index beea975..9f2c020 100644 --- a/qaekwy/core/model/variable/float.py +++ b/qaekwy/core/model/variable/float.py @@ -11,15 +11,9 @@ from typing import Optional -from qaekwy.core.model.variable.branch import BranchFloatVal, BranchFloatVar, BranchVal -from qaekwy.core.model.variable.variable import ( - ArrayVariable, - Expression, - ExpressionVariable, - MatrixVariable, - Variable, - VariableType, -) +from .branch import BranchFloatVal, BranchFloatVar, BranchVal +from .variable import (ArrayVariable, Expression, ExpressionVariable, + MatrixVariable, Variable, VariableType) class FloatVariable(Variable): diff --git a/qaekwy/core/model/variable/integer.py b/qaekwy/core/model/variable/integer.py index d86cefc..0f166d6 100644 --- a/qaekwy/core/model/variable/integer.py +++ b/qaekwy/core/model/variable/integer.py @@ -12,20 +12,9 @@ from typing import Optional -from qaekwy.core.model.variable.branch import ( - BranchIntegerVal, - BranchIntegerVar, - BranchVal, - BranchVar, -) -from qaekwy.core.model.variable.variable import ( - ArrayVariable, - Expression, - ExpressionVariable, - MatrixVariable, - Variable, - VariableType, -) +from .branch import BranchIntegerVal, BranchIntegerVar, BranchVal, BranchVar +from .variable import (ArrayVariable, Expression, ExpressionVariable, + MatrixVariable, Variable, VariableType) class IntegerVariable(Variable): diff --git a/qaekwy/core/model/variable/variable.py b/qaekwy/core/model/variable/variable.py index 1552cba..29a4f64 100644 --- a/qaekwy/core/model/variable/variable.py +++ b/qaekwy/core/model/variable/variable.py @@ -19,16 +19,9 @@ from enum import Enum from typing import Optional, Union -from qaekwy.core.model.variable.branch import ( - BranchBooleanVal, - BranchBooleanVar, - BranchFloatVal, - BranchFloatVar, - BranchIntegerVal, - BranchIntegerVar, - BranchVal, - BranchVar, -) +from .branch import (BranchBooleanVal, BranchBooleanVar, BranchFloatVal, + BranchFloatVar, BranchIntegerVal, BranchIntegerVar, + BranchVal, BranchVar) class VariableType(Enum): @@ -163,23 +156,10 @@ class ExpressionArray: array_name (str): The name of the array. Methods: - col(table_width: int, column: int) -> Expression: - Creates an Expression for accessing a column in the array-like structure. - - row(table_width: int, row: int) -> Expression: - Creates an Expression for accessing a row in the array-like structure. - - slice() -> Expression: - Creates an Expression for accessing a slice in the array-like structure. __getitem__(pos: int) -> Expression: Overloaded method to create an Expression for accessing a specific position in the array. - - Example: - col_expression = - my_array.col(table_width=4, column=2) # Creates an expression for column access. - """ def __init__(self, array_name: str) -> None: @@ -309,16 +289,6 @@ def from_json(json_data: dict) -> "ArrayVariable": ArrayVariable: An instance of the ArrayVariable class. """ var_type = VariableType.from_json(json_data["type"]) - if ( - var_type - not in [ - VariableType.INTEGER_ARRAY, - VariableType.FLOAT_ARRAY, - VariableType.BOOLEAN_ARRAY, - ] - or json_data.get("subtype", "") == "matrix" - ): - return None branch_var_enum: Union[ type[BranchIntegerVar], type[BranchFloatVar], type[BranchBooleanVar] @@ -405,7 +375,6 @@ def sum(self) -> Expression: def __str__(self): if self.kind == "row": return f"{self.matrix.var_name}[{self.matrix.rows}][{self.matrix.cols}][r][{self.params['row']}]" - if self.kind == "col": return f"{self.matrix.var_name}[{self.matrix.rows}][{self.matrix.cols}][c][{self.params['col']}]" if self.kind == "slice": @@ -413,19 +382,8 @@ def __str__(self): return "VectorExpression()" - def to_json(self): - """ - Converts the vector expression to a JSON representation. - """ - return { - "type": "vector", - "matrix": self.matrix.var_name, - "kind": self.kind, - **self.params, - } - -class MatrixVariable(ArrayVariable): +class MatrixVariable: """ Represents a matrix-type variable. @@ -463,27 +421,28 @@ def __init__( branch_val: BranchVal = BranchIntegerVal.VAL_RND, branch_order: Optional[int] = -1, ) -> None: - length = rows * cols - # Check if var_name is already in the form 'MATRIX_{rows}_{cols}_{var_name}' - expected_prefix = f"MATRIX${rows}${cols}$" - if var_name.startswith(expected_prefix): - raise ValueError( - f"var_name should not start with '{expected_prefix}'. Please provide a base variable name." - ) - - typed_var_name: str = f"MATRIX${rows}${cols}${var_name}" - super().__init__( - typed_var_name, - length, - var_type, - domain_low, - domain_high, - specific_domain, - branch_var, - branch_val, - branch_order, - ) + typed_var_name: str = var_name + if not var_name.startswith(f"MATRIX${rows}${cols}$"): + if var_name.startswith("MATRIX$"): + parts = var_name.split("$", 3) + part_col, part_row = int(parts[1]), int(parts[2]) + if rows != part_row or cols != part_col: + raise ValueError( + f"var_name '{var_name}' should not start with 'MATRIX$'. Please provide a base variable name." + ) + else: + typed_var_name = f"MATRIX${rows}${cols}${var_name}" + + self.var_name = typed_var_name + self.var_type = var_type + self.length = rows * cols + self.domain_low = domain_low + self.domain_high = domain_high + self.specific_domain = specific_domain + self.branch_var = branch_var + self.branch_val = branch_val + self.branching_order = branch_order self.rows = rows self.cols = cols @@ -497,16 +456,6 @@ def __getitem__(self, col: int) -> Expression: f"{self.matrix_var.var_name}[{self.row * (self.matrix_var.cols) + col}]" ) - class _MatrixCol: - def __init__(self, matrix_var: "MatrixVariable", col: int): - self.matrix_var = matrix_var - self.col = col - - def __getitem__(self, row: int) -> Expression: - return Expression( - f"{self.matrix_var.var_name}[{row * (self.matrix_var.cols) + self.col}]" - ) - def __getitem__(self, row: int) -> "_MatrixRow": return MatrixVariable._MatrixRow(self, row) @@ -548,7 +497,24 @@ def to_json(self): Returns: dict: A JSON representation of the matrix variable. """ - data_json = super().to_json() + data_json = { + "name": self.var_name, + "type": self.var_type.value, + "length": self.length, + "brancher_variable": self.branch_var.value, + "brancher_value": self.branch_val.value, + "branching_order": self.branching_order, + } + + if self.domain_low is not None: + data_json["domlow"] = self.domain_low + + if self.domain_high is not None: + data_json["domup"] = self.domain_high + + if self.specific_domain is not None: + data_json["specific_domain"] = self.specific_domain + data_json["rows"] = self.rows data_json["cols"] = self.cols data_json["subtype"] = "matrix" @@ -566,16 +532,6 @@ def from_json(json_data) -> "MatrixVariable": MatrixVariable: An instance of the MatrixVariable class. """ var_type = VariableType.from_json(json_data["type"]) - if ( - var_type - not in [ - VariableType.INTEGER_ARRAY, - VariableType.FLOAT_ARRAY, - VariableType.BOOLEAN_ARRAY, - ] - or json_data.get("subtype", "") != "matrix" - ): - return None branch_var_enum: Union[ type[BranchIntegerVar], type[BranchFloatVar], type[BranchBooleanVar] @@ -605,6 +561,7 @@ def from_json(json_data) -> "MatrixVariable": return MatrixVariable( var_name=json_data["name"], + var_type=var_type, rows=json_data["rows"], cols=json_data["cols"], domain_low=json_data.get("domlow"), diff --git a/qaekwy/core/response.py b/qaekwy/core/response.py index 5a6a40a..6f5a7a3 100644 --- a/qaekwy/core/response.py +++ b/qaekwy/core/response.py @@ -44,8 +44,8 @@ from abc import ABC from typing import Any, Optional -from qaekwy.core.explanation import Explanation -from qaekwy.core.solution import Solution +from .explanation import Explanation +from .solution import Solution class NodeStatus: @@ -348,7 +348,7 @@ def get_app(self) -> str: Returns: str: The name of the application. """ - return self.response_content["app"] + return str(self.response_content["app"]) def get_author(self) -> str: """ @@ -357,7 +357,7 @@ def get_author(self) -> str: Returns: str: The author of the application. """ - return self.response_content["author"] + return str(self.response_content["author"]) def get_version(self) -> str: """ @@ -366,7 +366,7 @@ def get_version(self) -> str: Returns: str: The version of the application. """ - return self.response_content["version"] + return str(self.response_content["version"]) def get_version_major(self) -> int: """ @@ -375,7 +375,7 @@ def get_version_major(self) -> int: Returns: int: The major version number of the application. """ - return self.response_content["version_major"] + return int(self.response_content["version_major"]) def get_version_minor(self) -> int: """ @@ -384,7 +384,7 @@ def get_version_minor(self) -> int: Returns: int: The minor version number of the application. """ - return self.response_content["version_minor"] + return int(self.response_content["version_minor"]) def get_version_build(self) -> int: """ @@ -393,7 +393,7 @@ def get_version_build(self) -> int: Returns: int: The build version number of the application. """ - return self.response_content["version_build"] + return int(self.response_content["version_build"]) def get_release(self) -> str: """ @@ -402,7 +402,7 @@ def get_release(self) -> str: Returns: str: The release information of the application. """ - return self.response_content["version_release"] + return str(self.response_content["version_release"]) class ClusterStatusResponse(AbstractResponse): diff --git a/qaekwy/core/solution.py b/qaekwy/core/solution.py index 67b4a78..11df6f3 100644 --- a/qaekwy/core/solution.py +++ b/qaekwy/core/solution.py @@ -36,7 +36,7 @@ class Solution(dict): z_value = solution.z # z_value is None """ - def __init__( # pylint: disable=too-many-locals + def __init__( # pylint: disable=too-many-locals self, solution_json_content: list[dict] ) -> None: self.solution_json_content = solution_json_content diff --git a/setup.py b/setup.py deleted file mode 100644 index 37d2a6b..0000000 --- a/setup.py +++ /dev/null @@ -1,87 +0,0 @@ -""" Installation """ - -import ast -from pathlib import Path -from setuptools import setup - - -metadata_file = Path("qaekwy/__metadata__.py") - -metadata = {} -with open(metadata_file, "r", encoding="utf-8") as f: - tree = ast.parse(f.read(), filename=str(metadata_file)) - for node in tree.body: - if isinstance(node, ast.Assign) and len(node.targets) == 1: - target = node.targets[0] - if isinstance(target, ast.Name) and isinstance(node.value, ast.Constant): - metadata[target.id] = node.value.value - -with open("README.md", "r", encoding="UTF-8") as fh: - long_description = fh.read() - - -setup( - name="qaekwy", - version=metadata["__version__"], - license=metadata["__license__"], - author=metadata["__author__"], - author_email=metadata["__author_email__"], - keywords=[ - "optimization", - "constraint programming", - "combinatorial optimization", - "operations research", - "constraint satisfaction", - "constraint solver", - "solver", - "CSP", - "optimization library", - "optimization framework", - "mathematical optimization", - "discrete optimization", - "optimization modeling", - "constraint modeling", - "declarative programming", - "modeling language", - "DSL", - "define-and-solve", - "scheduling", - "routing", - "planning", - "resource allocation", - "assignment", - "decision support", - "open source", - "Python", - ], - description="Qaekwy, a modern, open-source Python framework for declarative constraint programming and combinatorial optimization", - long_description_content_type = "text/markdown", - long_description=long_description, - url="https://qaekwy.io", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "Intended Audience :: Education", - "Intended Audience :: Science/Research", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Topic :: Scientific/Engineering", - "Topic :: Scientific/Engineering :: Mathematics", - "Topic :: Scientific/Engineering :: Artificial Intelligence", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - packages=[ - "qaekwy", - ], - project_urls={ - 'Homepage': 'https://qaekwy.io', - 'Documentation': 'https://docs.qaekwy.io', - 'Issue Tracker': 'https://github.com/alex-87/qaekwy-python/issues', - 'Source': 'https://github.com/alex-87/qaekwy-python', - }, - python_requires=">=3.9", - install_requires=[ - "requests", - "types_requests", - ], -) diff --git a/tests/api/test_model.py b/tests/api/test_model.py new file mode 100644 index 0000000..17f1cf4 --- /dev/null +++ b/tests/api/test_model.py @@ -0,0 +1,316 @@ +# pylint: skip-file + +import unittest +from unittest.mock import MagicMock, patch + +from qaekwy import Model +from qaekwy import SolverError + +from qaekwy.core.model.cutoff import Cutoff +from qaekwy.core.model.searcher import SearcherType +from qaekwy.core.response import SolutionResponse +from qaekwy.core.solution import Solution + +from qaekwy.core.model.variable.integer import IntegerVariable +from qaekwy.core.model.variable.integer import IntegerVariableArray +from qaekwy.core.model.variable.variable import VectorExpression +from qaekwy.core.model.variable.variable import MatrixVariable + + + + +class TestModelVariables(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + + def test_integer_variable(self): + var = self.model.integer_variable( + name="x", + domain=(0, 10) + ) + + self.assertIsInstance(var, IntegerVariable) + self.model._modeller.add_variable.assert_called_once_with(var) + + def test_integer_array(self): + arr = self.model.integer_array( + name="x", + length=5, + domain=(0, 9) + ) + + self.assertIsInstance(arr, IntegerVariableArray) + self.model._modeller.add_variable.assert_called_once_with(arr) + +class TestConstraintDistinct(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + + def test_constraint_distinct_array(self): + array = MagicMock(spec=IntegerVariableArray) + + self.model.constraint_distinct(array) + + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_distinct_row(self): + matrix = MagicMock(spec=MatrixVariable) + matrix.cols = 4 + + vec = MagicMock(spec=VectorExpression) + vec.matrix = matrix + vec.kind = "row" + vec.params = {"row": 1} + + self.model.constraint_distinct(vec) + + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_distinct_col(self): + matrix = MagicMock(spec=MatrixVariable) + matrix.rows = 3 + + vec = MagicMock(spec=VectorExpression) + vec.matrix = matrix + vec.kind = "col" + vec.params = {"col": 2} + + self.model.constraint_distinct(vec) + + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_distinct_slice(self): + matrix = MagicMock(spec=MatrixVariable) + matrix.cols = 5 + + vec = MagicMock(spec=VectorExpression) + vec.matrix = matrix + vec.kind = "slice" + vec.params = { + "row_start": 0, + "col_start": 0, + "row_end": 2, + "col_end": 2, + } + + self.model.constraint_distinct(vec) + + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_distinct_invalid_type(self): + with self.assertRaises(TypeError): + self.model.constraint_distinct(object()) + + +class TestSolve(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + self.model._engine = MagicMock() + + def test_invalid_searcher(self): + with self.assertRaises(ValueError): + self.model.solve(searcher="invalid") + + def test_invalid_solution_limit(self): + with self.assertRaises(ValueError): + self.model.solve(solution_limit=0) + + def test_solver_error(self): + response = MagicMock(spec=SolutionResponse) + response.is_status_ok.return_value = False + response.get_status.return_value = "ERROR" + response.get_message.return_value = "Failure" + response.get_content.return_value = {} + + self.model._engine.model.return_value = response + + with self.assertRaises(SolverError): + self.model.solve() + + def test_solve_success(self): + sol = MagicMock(spec=Solution) + + response = MagicMock(spec=SolutionResponse) + response.is_status_ok.return_value = True + response.get_solutions.return_value = [sol] + + self.model._engine.model.return_value = response + + solutions = self.model.solve() + + self.assertEqual(solutions, [sol]) + + def test_solve_one(self): + sol = MagicMock(spec=Solution) + + self.model.solve = MagicMock(return_value=[sol]) + + result = self.model.solve_one() + + self.assertEqual(result, sol) + +class TestModelExpressionVariables(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + + def test_integer_expression_variable(self): + v = self.model.integer_variable( + name="x", + expression="y + 1" + ) + + self.model._modeller.add_variable.assert_called_once_with(v) + self.assertEqual(v.var_name, "x") + + def test_float_expression_variable(self): + v = self.model.float_variable( + name="f", + domain=None, + expression="x * 0.5" + ) + + self.model._modeller.add_variable.assert_called_once_with(v) + self.assertEqual(v.var_name, "f") + +class TestModelBooleanVariables(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + + def test_boolean_variable(self): + v = self.model.boolean_variable("b") + + self.model._modeller.add_variable.assert_called_once_with(v) + + def test_boolean_array(self): + arr = self.model.boolean_array("b", length=3) + + self.model._modeller.add_variable.assert_called_once_with(arr) + +class TestModelFloatCollections(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + + def test_float_array(self): + arr = self.model.float_array("f", length=4, domain=(0.0, 1.0)) + self.model._modeller.add_variable.assert_called_once_with(arr) + + def test_float_matrix(self): + mat = self.model.float_matrix("m", rows=2, cols=2, domain=(0.0, 10.0)) + self.model._modeller.add_variable.assert_called_once_with(mat) + +class TestModelObjectives(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + + def test_minimize(self): + var = MagicMock() + self.model.minimize(var) + + self.model._modeller.add_objective.assert_called_once() + + def test_maximize(self): + var = MagicMock() + self.model.maximize(var) + + self.model._modeller.add_objective.assert_called_once() + +class TestModelConstraints(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + self.v1 = MagicMock() + self.v2 = MagicMock() + self.v3 = MagicMock() + + def test_constraint_abs(self): + self.model.constraint_abs(self.v1, self.v2) + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_multiply(self): + self.model.constraint_multiply(self.v1, self.v2, self.v3) + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_divide(self): + self.model.constraint_divide(self.v1, self.v2, self.v3) + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_power(self): + self.model.constraint_power(self.v1, 2, self.v3) + self.model._modeller.add_constraint.assert_called_once() + + def test_constraint_if_then_else(self): + cond = MagicMock() + then_c = MagicMock() + + self.model.constraint_if_then_else(cond, then_c) + + self.model._modeller.add_constraint.assert_called_once() + +class TestModelSearcherConfiguration(unittest.TestCase): + + def setUp(self): + self.model = Model() + self.model._modeller = MagicMock() + self.model._engine = MagicMock() + + def test_searcher_set_correctly(self): + response = MagicMock() + response.is_status_ok.return_value = True + response.get_solutions.return_value = [] + + self.model._engine.model.return_value = response + + self.model.solve(searcher="dfs") + + self.model._modeller.set_searcher.assert_called_once_with( + searcher=SearcherType.DFS + ) + + def test_cutoff_passed(self): + cutoff = MagicMock(spec=Cutoff) + + response = MagicMock() + response.is_status_ok.return_value = True + response.get_solutions.return_value = [] + + self.model._engine.model.return_value = response + + self.model.solve(cutoff=cutoff) + + self.model._modeller.set_cutoff.assert_called_once_with(cutoff=cutoff) + +class TestSolveOneEdgeCases(unittest.TestCase): + + def setUp(self): + self.model = Model() + + def test_solve_one_no_solution(self): + self.model.solve = MagicMock(return_value=[]) + + result = self.model.solve_one() + + self.assertIsNone(result) + +class TestModelJsonExtended(unittest.TestCase): + + def test_to_json_delegation(self): + model = Model() + model._modeller = MagicMock() + model._modeller.to_json.return_value = {"k": "v"} + + self.assertEqual(model.to_json(), {"k": "v"}) diff --git a/tests/model/constraint/test_abs.py b/tests/core/model/constraint/test_abs.py similarity index 100% rename from tests/model/constraint/test_abs.py rename to tests/core/model/constraint/test_abs.py diff --git a/tests/model/constraint/test_acos.py b/tests/core/model/constraint/test_acos.py similarity index 100% rename from tests/model/constraint/test_acos.py rename to tests/core/model/constraint/test_acos.py diff --git a/tests/model/constraint/test_asin.py b/tests/core/model/constraint/test_asin.py similarity index 100% rename from tests/model/constraint/test_asin.py rename to tests/core/model/constraint/test_asin.py diff --git a/tests/model/constraint/test_atan.py b/tests/core/model/constraint/test_atan.py similarity index 100% rename from tests/model/constraint/test_atan.py rename to tests/core/model/constraint/test_atan.py diff --git a/tests/model/constraint/test_cos.py b/tests/core/model/constraint/test_cos.py similarity index 100% rename from tests/model/constraint/test_cos.py rename to tests/core/model/constraint/test_cos.py diff --git a/tests/core/model/constraint/test_distinct.py b/tests/core/model/constraint/test_distinct.py new file mode 100644 index 0000000..16328d2 --- /dev/null +++ b/tests/core/model/constraint/test_distinct.py @@ -0,0 +1,218 @@ +# pylint: skip-file +import unittest + +# Import the classes under test +# Adjust the import path if needed for your project structure +from qaekwy.core.model.constraint.distinct import ( + ConstraintDistinctArray, + ConstraintDistinctRow, + ConstraintDistinctCol, + ConstraintDistinctSlice, +) + + +# --------------------------------------------------------------------------- +# Minimal mock classes to avoid dependency on the full variable implementation +# --------------------------------------------------------------------------- + +class MockArrayVariable: + def __init__(self, var_name): + self.var_name = var_name + + +class MockMatrixVariable: + def __init__(self, var_name): + self.var_name = var_name + + +# --------------------------------------------------------------------------- +# Test cases +# --------------------------------------------------------------------------- + +class TestConstraintDistinctArray(unittest.TestCase): + def setUp(self): + self.var = MockArrayVariable("x") + + def test_to_json(self): + constraint = ConstraintDistinctArray(self.var, "c1") + + expected = { + "name": "c1", + "type": "distinct", + "v1": "x", + "selection": "standard", + } + + self.assertEqual(constraint.to_json(), expected) + + def test_from_json(self): + json_data = { + "name": "c1", + "type": "distinct", + "v1": "x", + "selection": "standard", + } + + constraint = ConstraintDistinctArray.from_json(json_data, [self.var]) + + self.assertIsInstance(constraint, ConstraintDistinctArray) + self.assertEqual(constraint.var_1, self.var) + self.assertEqual(constraint.constraint_name, "c1") + + def test_from_json_variable_not_found(self): + json_data = {"v1": "missing"} + + with self.assertRaises(ValueError): + ConstraintDistinctArray.from_json(json_data, []) + + +class TestConstraintDistinctRow(unittest.TestCase): + def setUp(self): + self.var = MockMatrixVariable("m") + + def test_to_json(self): + constraint = ConstraintDistinctRow(self.var, size=4, idx=1, constraint_name="row1") + + expected = { + "name": "row1", + "type": "distinct", + "v1": "m", + "selection": "row", + "size": 4, + "index": 1, + } + + self.assertEqual(constraint.to_json(), expected) + + def test_from_json(self): + json_data = { + "name": "row1", + "v1": "m", + "selection": "row", + "size": 4, + "index": 1, + } + + constraint = ConstraintDistinctRow.from_json(json_data, [self.var]) + + self.assertIsInstance(constraint, ConstraintDistinctRow) + self.assertEqual(constraint.var_1, self.var) + self.assertEqual(constraint.size, 4) + self.assertEqual(constraint.idx, 1) + self.assertEqual(constraint.constraint_name, "row1") + + def test_from_json_variable_not_found(self): + json_data = {"v1": "missing", "size": 3, "index": 0} + + with self.assertRaises(ValueError): + ConstraintDistinctRow.from_json(json_data, []) + + +class TestConstraintDistinctCol(unittest.TestCase): + def setUp(self): + self.var = MockMatrixVariable("m") + + def test_to_json(self): + constraint = ConstraintDistinctCol(self.var, size=5, idx=2, constraint_name="col1") + + expected = { + "name": "col1", + "type": "distinct", + "v1": "m", + "selection": "col", + "size": 5, + "index": 2, + } + + self.assertEqual(constraint.to_json(), expected) + + def test_from_json(self): + json_data = { + "name": "col1", + "v1": "m", + "selection": "col", + "size": 5, + "index": 2, + } + + constraint = ConstraintDistinctCol.from_json(json_data, [self.var]) + + self.assertIsInstance(constraint, ConstraintDistinctCol) + self.assertEqual(constraint.size, 5) + self.assertEqual(constraint.idx, 2) + self.assertEqual(constraint.constraint_name, "col1") + + def test_from_json_variable_not_found(self): + json_data = {"v1": "missing", "size": 3, "index": 0} + + with self.assertRaises(ValueError): + ConstraintDistinctCol.from_json(json_data, []) + + +class TestConstraintDistinctSlice(unittest.TestCase): + def setUp(self): + self.var = MockMatrixVariable("m") + + def test_to_json(self): + constraint = ConstraintDistinctSlice( + self.var, + size=6, + offset_start_x=0, + offset_start_y=1, + offset_end_x=2, + offset_end_y=3, + constraint_name="slice1", + ) + + expected = { + "name": "slice1", + "type": "distinct", + "v1": "m", + "selection": "slice", + "size": 6, + "offset_start_x": 0, + "offset_start_y": 1, + "offset_end_x": 2, + "offset_end_y": 3, + } + + self.assertEqual(constraint.to_json(), expected) + + def test_from_json(self): + json_data = { + "name": "slice1", + "v1": "m", + "selection": "slice", + "size": 6, + "offset_start_x": 0, + "offset_start_y": 1, + "offset_end_x": 2, + "offset_end_y": 3, + } + + constraint = ConstraintDistinctSlice.from_json(json_data, [self.var]) + + self.assertIsInstance(constraint, ConstraintDistinctSlice) + self.assertEqual(constraint.size, 6) + self.assertEqual(constraint.offset_start_x, 0) + self.assertEqual(constraint.offset_start_y, 1) + self.assertEqual(constraint.offset_end_x, 2) + self.assertEqual(constraint.offset_end_y, 3) + self.assertEqual(constraint.constraint_name, "slice1") + + def test_from_json_variable_not_found(self): + json_data = { + "v1": "missing", + "size": 4, + "offset_start_x": 0, + "offset_start_y": 0, + "offset_end_x": 1, + "offset_end_y": 1, + } + + with self.assertRaises(ValueError): + ConstraintDistinctSlice.from_json(json_data, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/model/constraint/test_divide.py b/tests/core/model/constraint/test_divide.py similarity index 100% rename from tests/model/constraint/test_divide.py rename to tests/core/model/constraint/test_divide.py diff --git a/tests/model/constraint/test_element.py b/tests/core/model/constraint/test_element.py similarity index 100% rename from tests/model/constraint/test_element.py rename to tests/core/model/constraint/test_element.py diff --git a/tests/model/constraint/test_exponential.py b/tests/core/model/constraint/test_exponential.py similarity index 100% rename from tests/model/constraint/test_exponential.py rename to tests/core/model/constraint/test_exponential.py diff --git a/tests/model/constraint/test_if_then_else.py b/tests/core/model/constraint/test_if_then_else.py similarity index 100% rename from tests/model/constraint/test_if_then_else.py rename to tests/core/model/constraint/test_if_then_else.py diff --git a/tests/model/constraint/test_logarithm.py b/tests/core/model/constraint/test_logarithm.py similarity index 100% rename from tests/model/constraint/test_logarithm.py rename to tests/core/model/constraint/test_logarithm.py diff --git a/tests/model/constraint/test_maximum.py b/tests/core/model/constraint/test_maximum.py similarity index 100% rename from tests/model/constraint/test_maximum.py rename to tests/core/model/constraint/test_maximum.py diff --git a/tests/model/constraint/test_member.py b/tests/core/model/constraint/test_member.py similarity index 100% rename from tests/model/constraint/test_member.py rename to tests/core/model/constraint/test_member.py diff --git a/tests/model/constraint/test_minimum.py b/tests/core/model/constraint/test_minimum.py similarity index 100% rename from tests/model/constraint/test_minimum.py rename to tests/core/model/constraint/test_minimum.py diff --git a/tests/model/constraint/test_modulo.py b/tests/core/model/constraint/test_modulo.py similarity index 100% rename from tests/model/constraint/test_modulo.py rename to tests/core/model/constraint/test_modulo.py diff --git a/tests/model/constraint/test_multiply.py b/tests/core/model/constraint/test_multiply.py similarity index 100% rename from tests/model/constraint/test_multiply.py rename to tests/core/model/constraint/test_multiply.py diff --git a/tests/model/constraint/test_nroot.py b/tests/core/model/constraint/test_nroot.py similarity index 100% rename from tests/model/constraint/test_nroot.py rename to tests/core/model/constraint/test_nroot.py diff --git a/tests/model/constraint/test_power.py b/tests/core/model/constraint/test_power.py similarity index 100% rename from tests/model/constraint/test_power.py rename to tests/core/model/constraint/test_power.py diff --git a/tests/model/constraint/test_relational.py b/tests/core/model/constraint/test_relational.py similarity index 100% rename from tests/model/constraint/test_relational.py rename to tests/core/model/constraint/test_relational.py diff --git a/tests/model/constraint/test_sin.py b/tests/core/model/constraint/test_sin.py similarity index 100% rename from tests/model/constraint/test_sin.py rename to tests/core/model/constraint/test_sin.py diff --git a/tests/model/constraint/test_sort.py b/tests/core/model/constraint/test_sort.py similarity index 100% rename from tests/model/constraint/test_sort.py rename to tests/core/model/constraint/test_sort.py diff --git a/tests/model/constraint/test_tan.py b/tests/core/model/constraint/test_tan.py similarity index 100% rename from tests/model/constraint/test_tan.py rename to tests/core/model/constraint/test_tan.py diff --git a/tests/core/model/test_cutoff.py b/tests/core/model/test_cutoff.py new file mode 100644 index 0000000..25230b2 --- /dev/null +++ b/tests/core/model/test_cutoff.py @@ -0,0 +1,164 @@ +# pylint: skip-file + +import unittest + + +from qaekwy.core.model.cutoff import ( + Cutoff, + CutoffConstant, + CutoffFibonacci, + CutoffGeometric, + CutoffLuby, + CutoffLinear, + CutoffRandom, + MetaCutoffAppender, + MetaCutoffMerger, + MetaCutoffRepeater, +) + + + +class TestCutoff(unittest.TestCase): + + def test_cutoff_constant(self): + c = CutoffConstant(42) + + assert c.constant_value == 42 + assert c.is_meta() is False + assert c.to_json() == {"name": "constant", "value": 42} + + restored = CutoffConstant.from_json(c.to_json()) + assert restored.constant_value == 42 + + + def test_cutoff_fibonacci(self): + c = CutoffFibonacci() + + assert c.is_meta() is False + assert c.to_json() == {"name": "fibonacci"} + + restored = CutoffFibonacci.from_json({}) + assert isinstance(restored, CutoffFibonacci) + + + def test_cutoff_geometric(self): + c = CutoffGeometric(base=1.2, scale=3) + + assert c.is_meta() is False + assert c.base == 1.2 + assert c.scale == 3 + + data = c.to_json() + restored = CutoffGeometric.from_json(data) + + assert restored.base == 1.2 + assert restored.scale == 3 + + + def test_cutoff_luby(self): + c = CutoffLuby(scale=5) + + assert c.is_meta() is False + assert c.scale == 5 + + data = c.to_json() + restored = CutoffLuby.from_json(data) + + assert restored.scale == 5 + + + def test_cutoff_linear(self): + c = CutoffLinear(scale=7) + + assert c.is_meta() is False + assert c.scale == 7 + + data = c.to_json() + restored = CutoffLinear.from_json(data) + + assert restored.scale == 7 + + + def test_cutoff_random(self): + c = CutoffRandom(seed=123, minimum=10, maximum=50, round_value=5) + + assert c.is_meta() is False + assert c.seed == 123 + assert c.minimum == 10 + assert c.maximum == 50 + assert c.round_value == 5 + + data = c.to_json() + restored = CutoffRandom.from_json(data) + + assert restored.seed == 123 + assert restored.minimum == 10 + assert restored.maximum == 50 + assert restored.round_value == 5 + + def test_meta_cutoff_appender(self): + first = CutoffConstant(10) + second = CutoffLinear(3) + + meta = MetaCutoffAppender( + first_cutoff=first, + number_from_first=2, + second_cutoff=second, + ) + + assert meta.is_meta() is True + assert meta.number_from_first == 2 + + data = meta.to_json() + restored = MetaCutoffAppender.from_json(data) + + assert isinstance(restored.first_cutoff, CutoffConstant) + assert isinstance(restored.second_cutoff, CutoffLinear) + assert restored.number_from_first == 2 + + + def test_meta_cutoff_merger(self): + first = CutoffConstant(5) + second = CutoffFibonacci() + + meta = MetaCutoffMerger(first, second) + + assert meta.is_meta() is True + + data = meta.to_json() + restored = MetaCutoffMerger.from_json(data) + + assert isinstance(restored.first_cutoff, CutoffConstant) + assert isinstance(restored.second_cutoff, CutoffFibonacci) + + + def test_meta_cutoff_repeater(self): + sub = CutoffGeometric(base=2.0, scale=3) + meta = MetaCutoffRepeater(sub_cutoff=sub, repeat=4) + + assert meta.is_meta() is True + assert meta.repeat == 4 + + data = meta.to_json() + restored = MetaCutoffRepeater.from_json(data) + + assert isinstance(restored.sub_cutoff, CutoffGeometric) + assert restored.repeat == 4 + + def test_nested_meta_cutoffs_roundtrip(self): + base = CutoffConstant(10) + repeated = MetaCutoffRepeater(base, repeat=3) + merged = MetaCutoffMerger(repeated, CutoffLinear(2)) + + data = merged.to_json() + restored = Cutoff.from_json(data) + + assert isinstance(restored, MetaCutoffMerger) + assert isinstance(restored.first_cutoff, MetaCutoffRepeater) + assert isinstance(restored.first_cutoff.sub_cutoff, CutoffConstant) + assert restored.first_cutoff.repeat == 3 + assert isinstance(restored.second_cutoff, CutoffLinear) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/core/model/test_function.py b/tests/core/model/test_function.py new file mode 100644 index 0000000..41c056d --- /dev/null +++ b/tests/core/model/test_function.py @@ -0,0 +1,62 @@ +# pylint: skip-file + +import unittest + +import qaekwy as qw +from qaekwy.core.model.variable.variable import Expression, ExpressionArray + +class TestMathExpressions(unittest.TestCase): + + def setUp(self): + self.expr_a = Expression("x") + self.expr_b = Expression("y") + self.array = ExpressionArray([self.expr_a, self.expr_b]) + + def test_maximum(self): + result = qw.math.maximum(self.array) + self.assertEqual(result, "max(x,y)") + + def test_minimum(self): + result = qw.math.minimum(self.array) + self.assertEqual(result, "min(x,y)") + + def test_sum_of(self): + result = qw.math.sum_of(self.array) + self.assertEqual(result, "sum(x,y)") + + def test_absolute(self): + result = qw.math.absolute(self.expr_a) + self.assertEqual(result, "abs(x)") + + def test_power(self): + result = qw.math.power(self.expr_a, 3) + self.assertEqual(result, "(pow(x, 3))") + + def test_nroot(self): + result = qw.math.nroot(self.expr_a, 2) + self.assertEqual(result, "nroot(x, 2)") + + def test_sqr(self): + result = qw.math.sqr(self.expr_a) + self.assertEqual(result, "sqr(x)") + + def test_sqrt(self): + result = qw.math.sqrt(self.expr_a) + self.assertEqual(result, "sqrt(x)") + + def test_trigonometry(self): + self.assertEqual(qw.math.sin(self.expr_a), "sin(x)") + self.assertEqual(qw.math.cos(self.expr_a), "cos(x)") + self.assertEqual(qw.math.tan(self.expr_a), "tan(x)") + + def test_inverse_trigonometry(self): + self.assertEqual(qw.math.asin(self.expr_a), "asin(x)") + self.assertEqual(qw.math.acos(self.expr_a), "acos(x)") + self.assertEqual(qw.math.atan(self.expr_a), "atan(x)") + + def test_log_and_exp(self): + self.assertEqual(qw.math.log(self.expr_a), "log(x)") + self.assertEqual(qw.math.exp(self.expr_a), "exp(x)") + +if __name__ == "__main__": + unittest.main() diff --git a/tests/core/model/test_modeller.py b/tests/core/model/test_modeller.py new file mode 100644 index 0000000..7d2237e --- /dev/null +++ b/tests/core/model/test_modeller.py @@ -0,0 +1,243 @@ +# pylint: skip-file + +import unittest +from unittest.mock import MagicMock + +from qaekwy.core.model.constraint.abs import ConstraintAbs +from qaekwy.core.model.modeller import Modeller +from qaekwy.core.model.specific import SpecificMinimum +from qaekwy.core.model.searcher import SearcherType +from qaekwy.core.model.cutoff import CutoffFibonacci +from qaekwy.core.model.variable.integer import IntegerVariable +from qaekwy.core.model.cutoff import Cutoff +from qaekwy.core.model.variable.variable import Expression +from qaekwy.core.exception.model_failure import ModelFailure + + +class TestModeller(unittest.TestCase): + + def setUp(self): + self.modeller = Modeller() + self.var1 = IntegerVariable("var1", 0, 10) + self.var2 = IntegerVariable("var2", 0, 10) + self.constraint = ConstraintAbs( + var_1=self.var1, var_2=self.var2, constraint_name="abs" + ) + self.objective = SpecificMinimum(self.var1) + self.searcher = SearcherType.DFS + self.cutoff = CutoffFibonacci() + self.callback_url = "https://example.com/callback" + + def test_add_variable(self): + self.modeller.add_variable(self.var1).add_variable(self.var2) + self.assertEqual(self.modeller.variable_list, [self.var1, self.var2]) + + def test_add_constraint(self): + self.modeller.add_constraint(self.constraint) + self.assertEqual(self.modeller.constraint_list, [self.constraint]) + + def test_add_objective(self): + self.modeller.add_objective(self.objective) + self.assertEqual(self.modeller.objective_list, [self.objective]) + + def test_set_searcher(self): + self.modeller.set_searcher(self.searcher) + self.assertEqual(self.modeller.searcher, self.searcher) + + def test_set_cutoff(self): + self.modeller.set_cutoff(self.cutoff) + self.assertEqual(self.modeller.cutoff, self.cutoff) + + def test_set_callback_url(self): + self.modeller.set_callback_url(self.callback_url) + self.assertEqual(self.modeller.callback_url, self.callback_url) + + def test_to_json(self): + self.modeller.add_variable(self.var1).add_variable(self.var2).add_constraint( + self.constraint + ).add_objective(self.objective) + self.modeller.set_searcher(self.searcher).set_cutoff( + self.cutoff + ).set_callback_url(self.callback_url) + + expected_json = { + "callback_url": "https://example.com/callback", + "constraint": [{"name": "abs", "type": "abs", "v1": "var1", "v2": "var2"}], + "cutoff": {"name": "fibonacci"}, + "solution_limit": 1, + "specific": [{"type": "minimize", "var": "var1"}], + "var": [ + { + "brancher_value": "VAL_RND", + "branching_order": -1, + "domlow": 0, + "domup": 10, + "name": "var1", + "type": "integer", + }, + { + "brancher_value": "VAL_RND", + "branching_order": -1, + "domlow": 0, + "domup": 10, + "name": "var2", + "type": "integer", + }, + ], + } + + print( + self.modeller.from_json( + self.modeller.from_json(expected_json).to_json(serialization=True) + ).to_json(serialization=True) + ) + + self.assertDictEqual( + self.modeller.to_json(serialization=True), + self.modeller.from_json(expected_json).to_json(serialization=True), + self.modeller.from_json( + self.modeller.from_json(expected_json).to_json(serialization=True) + ).to_json(serialization=True), + ) + + self.assertDictEqual( + self.modeller.to_json(serialization=True), + expected_json, + ) + +class TestModellerBasic(unittest.TestCase): + + def test_add_variable(self): + m = Modeller() + v = MagicMock() + + result = m.add_variable(v) + + self.assertIs(result, m) + self.assertIn(v, m.variable_list) + + def test_add_constraint_expression_wrapped(self): + m = Modeller() + expr = MagicMock(spec=Expression) + + m.add_constraint(expr) + + self.assertEqual(len(m.constraint_list), 1) + self.assertNotEqual(m.constraint_list[0], expr) + +class TestModellerToJson(unittest.TestCase): + + def setUp(self): + self.m = Modeller() + self.m.variable_list = [] + self.m.constraint_list = [] + self.m.objective_list = [] + + def test_to_json_no_searcher_raises(self): + with self.assertRaises(ModelFailure): + self.m.to_json() + + def test_to_json_serialization_skips_searcher(self): + json_data = self.m.to_json(serialization=True) + self.assertNotIn("searcher", json_data) + + def test_to_json_with_searcher(self): + self.m.set_searcher(SearcherType.DFS) + + json_data = self.m.to_json() + self.assertEqual(json_data["searcher"], SearcherType.DFS.value) + + def test_to_json_meta_cutoff(self): + cutoff = MagicMock(spec=Cutoff) + cutoff.is_meta.return_value = True + cutoff.to_json.return_value = {"t": 1} + + self.m.set_searcher(SearcherType.DFS) + self.m.set_cutoff(cutoff) + + json_data = self.m.to_json() + self.assertIn("meta_cutoff", json_data) + + def test_to_json_callback_url(self): + self.m.set_searcher(SearcherType.DFS) + self.m.set_callback_url("https://callback") + + json_data = self.m.to_json() + self.assertEqual(json_data["callback_url"], "https://callback") + +class TestConstraintFactory(unittest.TestCase): + + def test_unknown_constraint_type(self): + with self.assertRaises(ValueError): + Modeller._constraints_factory( + {"type": "unknown"}, + [] + ) + +class TestModellerFromJsonExtras(unittest.TestCase): + + def test_solution_limit_default(self): + m = Modeller.from_json({}) + self.assertEqual(m.solution_limit, 1) + + def test_solution_limit_custom(self): + m = Modeller.from_json({"solution_limit": 5}) + self.assertEqual(m.solution_limit, 5) + + def test_cutoff_parsing(self): + with unittest.mock.patch( + "qaekwy.core.model.cutoff.Cutoff.from_json", + return_value="cutoff" + ): + m = Modeller.from_json({"cutoff": {"x": 1}}) + + self.assertEqual(m.cutoff, "cutoff") + + +class TestModellerFromJsonVariables(unittest.TestCase): + + def test_scalar_variable(self): + json_data = { + "var": [{"type": "integer", "name": "x", "brancher_value": "VAL_RND"}], + } + + m = Modeller.from_json(json_data) + self.assertEqual(len(m.variable_list), 1) + + def test_array_variable(self): + json_data = { + "var": [ + { + "type": "integer_array", + "name": "a", + "length": 5, + "brancher_variable": "VAR_RND", + "brancher_value": "VAL_RND" + } + ], + } + + m = Modeller.from_json(json_data) + self.assertEqual(len(m.variable_list), 1) + + def test_matrix_variable(self): + json_data = { + "var": [ + { + "type": "integer_array", + "subtype": "matrix", + "name": "m", + "cols": 2, + "rows": 3, + "brancher_variable": "VAR_RND", + "brancher_value": "VAL_RND" + } + ], + } + + m = Modeller.from_json(json_data) + self.assertEqual(len(m.variable_list), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/core/model/variable/test_boolean.py b/tests/core/model/variable/test_boolean.py new file mode 100644 index 0000000..05bb66a --- /dev/null +++ b/tests/core/model/variable/test_boolean.py @@ -0,0 +1,138 @@ +# pylint: skip-file + +import unittest + +from qaekwy.core.model.variable.boolean import ( + BooleanVariable, + BooleanExpressionVariable, + BooleanVariableArray, + BooleanVariableMatrix, +) + +from qaekwy.core.model.variable.variable import ( + Expression, + VariableType, +) + +from qaekwy.core.model.variable.branch import ( + BranchBooleanVal, + BranchBooleanVar, +) + + +class TestBooleanVariable(unittest.TestCase): + + def test_boolean_variable_basic_init(self): + v = BooleanVariable("b") + + self.assertEqual(v.var_name, "b") + self.assertEqual(v.var_type, VariableType.BOOLEAN) + self.assertEqual(v.domain_low, 0) + self.assertEqual(v.domain_high, 1) + self.assertEqual(v.branch_val, BranchBooleanVal.VAL_RND) + + def test_boolean_variable_to_json_and_from_json_roundtrip(self): + v = BooleanVariable( + "b1", + branch_val=BranchBooleanVal.VAL_RND, + branch_order=5, + ) + + data = v.to_json() + restored = BooleanVariable.from_json(data) + + self.assertEqual(restored.var_name, v.var_name) + self.assertEqual(restored.var_type, VariableType.BOOLEAN) + self.assertEqual(restored.domain_low, 0) + self.assertEqual(restored.domain_high, 1) + self.assertEqual(restored.branch_val, BranchBooleanVal.VAL_RND) + self.assertEqual(restored.branching_order, 5) + + def test_boolean_expression_variable_basic_init(self): + expr = "x > 5" + v = BooleanExpressionVariable("be", expression=expr) + + self.assertEqual(v.var_name, "be") + self.assertEqual(v.var_type, VariableType.BOOLEAN) + self.assertEqual(str(v.expression), "x > 5") + + def test_boolean_expression_variable_from_json(self): + json_data = { + "name": "be_json", + "expr": "y == 1", + "type": VariableType.BOOLEAN.value, + "brancher_value": BranchBooleanVal.VAL_RND.value, + "branching_order": 2, + } + + v = BooleanExpressionVariable.from_json(json_data) + + self.assertEqual(v.var_name, "be_json") + self.assertEqual(str(v.expression), "y == 1") + self.assertEqual(v.branch_val, BranchBooleanVal.VAL_RND) + self.assertEqual(v.branching_order, 2) + + def test_boolean_variable_array_basic_init(self): + arr = BooleanVariableArray("bool_arr", length=10) + + self.assertEqual(arr.var_name, "bool_arr") + self.assertEqual(arr.length, 10) + self.assertEqual(arr.var_type, VariableType.BOOLEAN_ARRAY) + self.assertEqual(arr.domain_low, 0) + self.assertEqual(arr.domain_high, 1) + self.assertEqual(arr.branch_var, BranchBooleanVar.VAR_RND) + + def test_boolean_variable_array_to_json_and_from_json_roundtrip(self): + arr = BooleanVariableArray( + "arr_json", + length=3, + branch_var=BranchBooleanVar.VAR_RND, + branch_val=BranchBooleanVal.VAL_RND, + branch_order=1, + ) + + data = arr.to_json() + restored = BooleanVariableArray.from_json(data) + + self.assertEqual(restored.var_name, arr.var_name) + self.assertEqual(restored.length, 3) + self.assertEqual(restored.domain_low, 0) + self.assertEqual(restored.domain_high, 1) + self.assertEqual(restored.branch_var, BranchBooleanVar.VAR_RND) + self.assertEqual(restored.branch_val, BranchBooleanVal.VAL_RND) + self.assertEqual(restored.branching_order, 1) + + def test_boolean_variable_matrix_basic_init(self): + m = BooleanVariableMatrix("M", rows=4, cols=5) + + self.assertEqual(m.rows, 4) + self.assertEqual(m.cols, 5) + self.assertEqual(m.length, 20) + self.assertEqual(m.var_type, VariableType.BOOLEAN_ARRAY) + self.assertEqual(m.domain_low, 0) + self.assertEqual(m.domain_high, 1) + + def test_boolean_variable_matrix_to_json_and_from_json_roundtrip(self): + m = BooleanVariableMatrix( + "mat_json", + rows=2, + cols=2, + branch_var=BranchBooleanVar.VAR_RND, + branch_val=BranchBooleanVal.VAL_RND, + branch_order=4, + ) + + data = m.to_json() + restored = BooleanVariableMatrix.from_json(data) + + self.assertEqual(restored.var_name, m.var_name) + self.assertEqual(restored.rows, 2) + self.assertEqual(restored.cols, 2) + self.assertEqual(restored.length, 4) + self.assertEqual(restored.branch_var, BranchBooleanVar.VAR_RND) + self.assertEqual(restored.branch_val, BranchBooleanVal.VAL_RND) + self.assertEqual(restored.branching_order, 4) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/model/variable/test_expression.py b/tests/core/model/variable/test_expression.py similarity index 100% rename from tests/model/variable/test_expression.py rename to tests/core/model/variable/test_expression.py diff --git a/tests/core/model/variable/test_float.py b/tests/core/model/variable/test_float.py new file mode 100644 index 0000000..0bb4ac8 --- /dev/null +++ b/tests/core/model/variable/test_float.py @@ -0,0 +1,180 @@ +# pylint: skip-file + +import unittest + +from qaekwy.core.model.variable.float import ( + FloatVariable, + FloatExpressionVariable, + FloatVariableArray, + FloatVariableMatrix, + Expression, + VariableType, +) +from qaekwy.core.model.variable.branch import ( + BranchFloatVal, + BranchFloatVar, +) + + +class TestFloatVariable(unittest.TestCase): + + def test_float_variable_basic_init(self): + v = FloatVariable("x") + + assert v.var_name == "x" + assert v.var_type == VariableType.FLOAT + assert v.domain_low is None + assert v.domain_high is None + assert v.branch_val == BranchFloatVal.VAL_RND + + + def test_float_variable_with_domain(self): + v = FloatVariable("x", domain_low=0.0, domain_high=1.0) + + assert v.domain_low == 0.0 + assert v.domain_high == 1.0 + + + def test_float_variable_to_json_and_from_json_roundtrip(self): + v = FloatVariable( + "x", + domain_low=-1.5, + domain_high=2.5, + branch_val=BranchFloatVal.VAL_RND, + branch_order=3, + ) + + data = v.to_json() + restored = FloatVariable.from_json(data) + + assert restored.var_name == v.var_name + assert restored.var_type == VariableType.FLOAT + assert restored.domain_low == -1.5 + assert restored.domain_high == 2.5 + assert restored.branch_val == BranchFloatVal.VAL_RND + assert restored.branching_order == 3 + + + def test_float_expression_variable_basic_init(self): + expr = Expression("x + 1.5") + v = FloatExpressionVariable("y", expression=expr.expr) + + assert v.var_name == "y" + assert v.var_type == VariableType.FLOAT + assert str(v.expression) == "x + 1.5" + + + def test_float_expression_variable_from_json(self): + json_data = { + "name": "z", + "expr": "x * 0.5", + "type": VariableType.FLOAT.value, + "brancher_value": BranchFloatVal.VAL_RND.value, + "branching_order": 2, + } + + v = FloatExpressionVariable.from_json(json_data) + + assert v.var_name == "z" + assert v.var_type == VariableType.FLOAT + assert str(v.expression) == "x * 0.5" + assert v.branch_val == BranchFloatVal.VAL_RND + assert v.branching_order == 2 + + + def test_float_variable_array_basic_init(self): + arr = FloatVariableArray("arr", length=5) + + assert arr.var_name == "arr" + assert arr.length == 5 + assert arr.var_type == VariableType.FLOAT_ARRAY + assert arr.domain_low is None + assert arr.domain_high is None + assert arr.branch_var == BranchFloatVar.VAR_RND + + + def test_float_variable_array_with_domain(self): + arr = FloatVariableArray( + "arr", + length=3, + domain_low=0.0, + domain_high=10.0, + ) + + assert arr.domain_low == 0.0 + assert arr.domain_high == 10.0 + + + def test_float_variable_array_to_json_and_from_json_roundtrip(self): + arr = FloatVariableArray( + "arr", + length=4, + domain_low=-2.0, + domain_high=2.0, + branch_var=BranchFloatVar.VAR_RND, + branch_val=BranchFloatVal.VAL_RND, + branch_order=1, + ) + + data = arr.to_json() + restored = FloatVariableArray.from_json(data) + + assert restored.var_name == arr.var_name + assert restored.length == 4 + assert restored.var_type == VariableType.FLOAT_ARRAY + assert restored.domain_low == -2.0 + assert restored.domain_high == 2.0 + assert restored.branch_var == BranchFloatVar.VAR_RND + assert restored.branch_val == BranchFloatVal.VAL_RND + assert restored.branching_order == 1 + + def test_float_variable_matrix_basic_init(self): + m = FloatVariableMatrix("M", rows=2, cols=3) + + assert m.rows == 2 + assert m.cols == 3 + assert m.length == 6 + assert m.var_type == VariableType.FLOAT_ARRAY + assert m.branch_var == BranchFloatVar.VAR_RND + + + def test_float_variable_matrix_with_domain(self): + m = FloatVariableMatrix( + "M", + rows=3, + cols=3, + domain_low=0.0, + domain_high=1.0, + ) + + assert m.domain_low == 0.0 + assert m.domain_high == 1.0 + + + def test_float_variable_matrix_to_json_and_from_json_roundtrip(self): + m = FloatVariableMatrix( + "M", + rows=2, + cols=2, + domain_low=-1.0, + domain_high=1.0, + branch_var=BranchFloatVar.VAR_RND, + branch_val=BranchFloatVal.VAL_RND, + branch_order=4, + ) + + data = m.to_json() + restored = FloatVariableMatrix.from_json(data) + + assert restored.var_name == m.var_name + assert restored.rows == 2 + assert restored.cols == 2 + assert restored.length == 4 + assert restored.domain_low == -1.0 + assert restored.domain_high == 1.0 + assert restored.branch_var == BranchFloatVar.VAR_RND + assert restored.branch_val == BranchFloatVal.VAL_RND + assert restored.branching_order == 4 + +if __name__ == "__main__": + unittest.main() diff --git a/tests/core/model/variable/test_integer.py b/tests/core/model/variable/test_integer.py new file mode 100644 index 0000000..ac25473 --- /dev/null +++ b/tests/core/model/variable/test_integer.py @@ -0,0 +1,291 @@ +# pylint: skip-file + +import unittest + +from qaekwy.core.model.variable.branch import BranchIntegerVal, BranchIntegerVar +from qaekwy.core.model.variable.integer import IntegerVariable, IntegerVariableMatrix +from qaekwy.core.model.variable.variable import Expression, VariableType, VectorExpression + + +class TestExpression(unittest.TestCase): + def test_arithmetic_operations(self): + expr = Expression("x") + expr_add = expr + 2 + self.assertEqual(str(expr_add), "(x + 2)") + + expr_sub = expr - 3 + self.assertEqual(str(expr_sub), "(x - 3)") + + expr_mul = expr * 4 + self.assertEqual(str(expr_mul), "x * 4") + + expr_div = expr / 5 + self.assertEqual(str(expr_div), "((x) / (5))") + + expr_mod = expr % 6 + self.assertEqual(str(expr_mod), "((x) % (6))") + + +class TestVariable(unittest.TestCase): + def test_variable_to_json(self): + var = IntegerVariable("x", domain_low=0, domain_high=10) + + var_json = var.to_json() + self.assertEqual(var_json["name"], "x") + self.assertEqual(var_json["type"], "integer") + self.assertEqual(var_json["brancher_value"], "VAL_RND") + self.assertEqual(var_json["domlow"], 0) + self.assertEqual(var_json["domup"], 10) + + +class TestSpecificDomainVariable(unittest.TestCase): + def test_variable_to_json(self): + var = IntegerVariable("x", specific_domain=[2, 4, 6]) + + var_json = var.to_json() + self.assertEqual(var_json["name"], "x") + self.assertEqual(var_json["type"], "integer") + self.assertEqual(var_json["brancher_value"], "VAL_RND") + self.assertEqual(var_json["specific_domain"], [2, 4, 6]) + + +class TestExprVariable(unittest.TestCase): + def test_variable_to_json(self): + var = IntegerVariable("x") + var_expr = var + 2 + var.expression = var_expr + + var_json = var.to_json() + self.assertEqual(var_json["name"], "x") + self.assertEqual(var_json["type"], "integer") + self.assertEqual(var_json["brancher_value"], "VAL_RND") + self.assertEqual(var_json["expr"], "(x + 2)") + + +class TestIntegerVariable(unittest.TestCase): + def test_integer_variable_to_json(self): + int_var = IntegerVariable("y", domain_low=1, domain_high=5) + int_var_json = int_var.to_json() + self.assertEqual(int_var_json["name"], "y") + self.assertEqual(int_var_json["type"], "integer") + self.assertEqual(int_var_json["brancher_value"], "VAL_RND") + self.assertEqual(int_var_json["domlow"], 1) + self.assertEqual(int_var_json["domup"], 5) + + def test_from_json(self): + json_data = { + "name": "i", + "type": "integer", + "brancher_value": "VAL_MAX", + "specific_domain": [1, 2, 3], + } + i = IntegerVariable.from_json(json_data) + self.assertEqual(i.var_name, "i") + self.assertEqual(i.specific_domain, [1, 2, 3]) + self.assertEqual(i.branch_val, BranchIntegerVal.VAL_MAX) + + json_data = { + "name": "j", + "type": "integer", + "brancher_value": "VAL_MAX", + "domlow": 0, + "domup": 100, + } + i = IntegerVariable.from_json(json_data) + self.assertEqual(i.var_name, "j") + self.assertEqual(i.domain_low, 0) + self.assertEqual(i.domain_high, 100) + self.assertEqual(i.branch_val, BranchIntegerVal.VAL_MAX) + + +class TestIntegerMatrix(unittest.TestCase): + + def test_matrix_variable_basic_init(self): + m = IntegerVariableMatrix("A", rows=2, cols=3) + + assert m.rows == 2 + assert m.cols == 3 + assert m.length == 6 + assert m.var_type == VariableType.INTEGER_ARRAY + assert m.var_name == "MATRIX$2$3$A" + + def test_matrix_variable_domain_fields(self): + m = IntegerVariableMatrix( + "B", + rows=2, + cols=2, + domain_low=0, + domain_high=10, + specific_domain=[1, 3, 5], + ) + + assert m.domain_low == 0 + assert m.domain_high == 10 + assert m.specific_domain == [1, 3, 5] + + def test_matrix_item_access_returns_expression(self): + m = IntegerVariableMatrix("A", rows=2, cols=3) + + expr = m[1][2] + assert isinstance(expr, Expression) + assert str(expr) == "MATRIX$2$3$A[5]" # 1 * 3 + 2 + + + def test_row_vector_expression_creation(self): + m = IntegerVariableMatrix("A", rows=2, cols=3) + + v = m.row(1) + assert isinstance(v, VectorExpression) + assert v.kind == "row" + assert v.params["row"] == 1 + + + def test_col_vector_expression_creation(self): + m = IntegerVariableMatrix("A", rows=2, cols=3) + + v = m.col(2) + assert isinstance(v, VectorExpression) + assert v.kind == "col" + assert v.params["col"] == 2 + + + def test_slice_vector_expression_creation(self): + m = IntegerVariableMatrix("A", rows=3, cols=3) + + v = m.slice(0, 1, 1, 2) + + assert v.kind == "slice" + assert v.params["row_start"] == 0 + assert v.params["col_start"] == 1 + # slice() adds +1 internally + assert v.params["row_end"] == 2 + assert v.params["col_end"] == 3 + + def test_iter_row_vector_expression(self): + m = IntegerVariableMatrix("A", rows=2, cols=3) + + v = m.row(0) + items = list(v) + + assert len(items) == 3 + assert [str(e) for e in items] == [ + "MATRIX$2$3$A[0]", + "MATRIX$2$3$A[1]", + "MATRIX$2$3$A[2]", + ] + + + def test_iter_col_vector_expression(self): + m = IntegerVariableMatrix("A", rows=3, cols=2) + + v = m.col(1) + items = list(v) + + assert len(items) == 3 + assert [str(e) for e in items] == [ + "MATRIX$3$2$A[1]", + "MATRIX$3$2$A[3]", + "MATRIX$3$2$A[5]", + ] + + + def test_iter_slice_vector_expression(self): + m = IntegerVariableMatrix("A", rows=3, cols=3) + + v = m.slice(0, 0, 1, 1) + items = list(v) + + assert len(items) == 4 + assert [str(e) for e in items] == [ + "MATRIX$3$3$A[0]", + "MATRIX$3$3$A[1]", + "MATRIX$3$3$A[3]", + "MATRIX$3$3$A[4]", + ] + + + def test_vector_expression_str_row(self): + m = IntegerVariableMatrix("A", rows=2, cols=3) + + v = m.row(1) + assert str(v) == "MATRIX$2$3$A[2][3][r][1]" + + + def test_vector_expression_str_col(self): + m = IntegerVariableMatrix("A", rows=2, cols=3) + + v = m.col(0) + assert str(v) == "MATRIX$2$3$A[2][3][c][0]" + + + def test_vector_expression_str_slice(self): + m = IntegerVariableMatrix("A", rows=3, cols=3) + + v = m.slice(0, 1, 1, 2) + assert str(v) == "MATRIX$3$3$A[3][3][s][0][1][2][3]" + + + def test_vector_expression_sum_method(self): + m = IntegerVariableMatrix("A", rows=2, cols=2) + + v = m.row(0) + s = v.sum() + + assert isinstance(s, Expression) + assert str(s) == f"sum({v})" + + + def test_vector_expression_radd_with_zero(self): + m = IntegerVariableMatrix("A", rows=2, cols=2) + + v = m.col(0) + result = sum([v]) # triggers __radd__ with 0 + + assert isinstance(result, Expression) + assert str(result) == f"sum({v})" + + + def test_matrix_variable_to_json(self): + m = IntegerVariableMatrix( + "A", + rows=2, + cols=3, + domain_low=0, + domain_high=5, + branch_var=BranchIntegerVar.VAR_RND, + branch_val=BranchIntegerVal.VAL_RND, + branch_order=7, + ) + + data = m.to_json() + + assert data["name"] == m.var_name + assert data["rows"] == 2 + assert data["cols"] == 3 + assert data["length"] == 6 + assert data["subtype"] == "matrix" + assert data["domlow"] == 0 + assert data["domup"] == 5 + assert data["branching_order"] == 7 + + + def test_matrix_variable_from_json_roundtrip(self): + m = IntegerVariableMatrix( + "A", + rows=2, + cols=2, + domain_low=1, + domain_high=9, + ) + + data = m.to_json() + restored = IntegerVariableMatrix.from_json(data) + + assert restored.var_name == m.var_name + assert restored.rows == 2 + assert restored.cols == 2 + assert restored.domain_low == 1 + assert restored.domain_high == 9 + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_explanation.py b/tests/core/test_explanation.py similarity index 100% rename from tests/test_explanation.py rename to tests/core/test_explanation.py diff --git a/tests/core/test_response.py b/tests/core/test_response.py new file mode 100644 index 0000000..aab581f --- /dev/null +++ b/tests/core/test_response.py @@ -0,0 +1,105 @@ +# pylint: skip-file + +import unittest +import json +# from unittest.mock import MagicMock + +from qaekwy.core.response import ( + AbstractResponse, EchoResponse, StatusResponse, + SolutionResponse, ExplanationResponse, VersionResponse, + ClusterStatusResponse +) + +class TestResponseClasses(unittest.TestCase): + + def test_abstract_response_default_ok(self): + resp = AbstractResponse({"content": "data"}) + self.assertEqual(resp.get_status(), "Ok") + self.assertTrue(resp.is_status_ok()) + self.assertEqual(resp.get_message(), "") + + def test_abstract_response_custom_status(self): + resp = AbstractResponse({"status": "Error", "message": "Failed"}) + self.assertEqual(resp.get_status(), "Error") + self.assertFalse(resp.is_status_ok()) + self.assertEqual(resp.get_message(), "Failed") + + def test_echo_response(self): + content = "hello world" + resp = EchoResponse(content) + self.assertEqual(resp.get_status(), "") + self.assertTrue(resp.is_status_ok()) + self.assertEqual(resp.get_message(), content) + self.assertEqual(resp.get_content(), content) + + def test_status_response_fields(self): + data = { + "type": "info", + "code": 200, + "busy_node": True, + "current_solution_found": 5 + } + resp = StatusResponse(data) + self.assertEqual(resp.get_type(), "info") + self.assertEqual(resp.get_code(), 200) + self.assertTrue(resp.is_busy()) + self.assertEqual(resp.get_number_of_solution_found(), 5) + + def test_status_response_defaults(self): + resp = StatusResponse({}) + self.assertEqual(resp.get_type(), "") + self.assertEqual(resp.get_code(), -1) + self.assertFalse(resp.is_busy()) + self.assertEqual(resp.get_number_of_solution_found(), -1) + + def test_version_response(self): + data = { + "app": "OptiEngine", + "author": "DevTeam", + "version": "1.2.3", + "version_major": 1, + "version_minor": 2, + "version_build": 3, + "version_release": "stable" + } + resp = VersionResponse(data) + self.assertEqual(resp.get_app(), "OptiEngine") + self.assertEqual(resp.get_version_major(), 1) + self.assertEqual(resp.get_release(), "stable") + + def test_cluster_status_parsing(self): + node_data = [ + { + "identifier": "node_01", + "url": "http://node1", + "enabled": True, + "message": "Healthy", + "busy_node": False, + "current_solution_found": 10, + "failure": False, + "awake": True + } + ] + resp = ClusterStatusResponse(json.dumps(node_data)) + nodes = resp.get_node_status_list() + + self.assertIsNotNone(nodes) + self.assertEqual(len(nodes), 1) + self.assertEqual(nodes[0].identifier, "node_01") + self.assertEqual(nodes[0].number_of_solutions, 10) + self.assertTrue(nodes[0].is_enabled) + + def test_cluster_status_empty(self): + resp = ClusterStatusResponse(None) + self.assertIsNone(resp.get_node_status_list()) + + def test_solution_response_error_status(self): + resp = SolutionResponse({"status": "Error", "content": []}) + self.assertIsNone(resp.get_solutions()) + + def test_explanation_response_error_status(self): + resp = ExplanationResponse({"status": "Error", "content": {}}) + self.assertIsNone(resp.get_explanation()) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/core/test_solution.py b/tests/core/test_solution.py new file mode 100644 index 0000000..1bef5fa --- /dev/null +++ b/tests/core/test_solution.py @@ -0,0 +1,170 @@ +# pylint: skip-file + +import unittest + +from io import StringIO +from contextlib import redirect_stdout + +from qaekwy.core.solution import Solution + +class TestSolutionScalars(unittest.TestCase): + + def test_scalar_assigned(self): + content = [ + {"name": "x", "assigned": True, "value": 5}, + {"name": "y", "assigned": True, "value": 10}, + ] + + sol = Solution(content) + + self.assertEqual(sol["x"], 5) + self.assertEqual(sol["y"], 10) + + self.assertEqual(sol.x, 5) + self.assertEqual(sol.y, 10) + + def test_scalar_unassigned(self): + content = [ + {"name": "x", "assigned": False, "value": None}, + ] + + sol = Solution(content) + + self.assertIsNone(sol["x"]) + self.assertIsNone(sol.x) + +class TestSolutionArrays(unittest.TestCase): + + def test_array_with_positions(self): + content = [ + {"name": "a", "assigned": True, "value": 1, "position": 0}, + {"name": "a", "assigned": True, "value": 3, "position": 2}, + {"name": "a", "assigned": False, "value": None, "position": 1}, + ] + + sol = Solution(content) + + self.assertEqual(sol["a"], [1, None, 3]) + self.assertEqual(sol.a, [1, None, 3]) + + def test_sparse_array_positions(self): + content = [ + {"name": "b", "assigned": True, "value": 7, "position": 3}, + ] + + sol = Solution(content) + + self.assertEqual(sol["b"], [None, None, None, 7]) + +class TestSolutionMatrices(unittest.TestCase): + + def test_matrix_reconstruction(self): + content = [ + {"name": "MATRIX$2$3$m", "assigned": True, "value": 1, "position": 0}, + {"name": "MATRIX$2$3$m", "assigned": True, "value": 2, "position": 1}, + {"name": "MATRIX$2$3$m", "assigned": True, "value": 3, "position": 2}, + {"name": "MATRIX$2$3$m", "assigned": True, "value": 4, "position": 3}, + {"name": "MATRIX$2$3$m", "assigned": True, "value": 5, "position": 4}, + {"name": "MATRIX$2$3$m", "assigned": True, "value": 6, "position": 5}, + ] + + sol = Solution(content) + + expected = [ + [1, 2, 3], + [4, 5, 6], + ] + + self.assertIn("m", sol) + self.assertNotIn("MATRIX$2$3$m", sol) + + self.assertEqual(sol["m"], expected) + self.assertEqual(sol.m, expected) + +class TestSolutionMixed(unittest.TestCase): + + def test_mixed_solution(self): + content = [ + {"name": "x", "assigned": True, "value": 9}, + {"name": "arr", "assigned": True, "value": 1, "position": 0}, + {"name": "arr", "assigned": True, "value": 2, "position": 1}, + {"name": "MATRIX$1$2$m", "assigned": True, "value": 4, "position": 0}, + {"name": "MATRIX$1$2$m", "assigned": True, "value": 5, "position": 1}, + ] + + sol = Solution(content) + + self.assertEqual(sol.x, 9) + self.assertEqual(sol.arr, [1, 2]) + self.assertEqual(sol.m, [[4, 5]]) + +class TestSolutionRepr(unittest.TestCase): + + def test_repr(self): + content = [{"name": "x", "assigned": True, "value": 1}] + sol = Solution(content) + + rep = repr(sol) + + self.assertTrue(rep.startswith("Solution(")) + self.assertIn("'x': 1", rep) + +class TestSolutionPrettyPrint(unittest.TestCase): + + def test_pretty_print_scalar(self): + content = [{"name": "x", "assigned": True, "value": 5}] + sol = Solution(content) + + buf = StringIO() + with redirect_stdout(buf): + sol.pretty_print() + + output = buf.getvalue() + + self.assertIn("Solution:", output) + self.assertIn("x", output) + self.assertIn("5", output) + + def test_pretty_print_array(self): + content = [ + {"name": "a", "assigned": True, "value": 1, "position": 0}, + {"name": "a", "assigned": False, "value": None, "position": 1}, + ] + sol = Solution(content) + + buf = StringIO() + with redirect_stdout(buf): + sol.pretty_print() + + output = buf.getvalue() + + self.assertIn("[1, -]", output) + + def test_pretty_print_matrix(self): + content = [ + {"name": "MATRIX$2$2$m", "assigned": True, "value": 1, "position": 0}, + {"name": "MATRIX$2$2$m", "assigned": True, "value": 2, "position": 1}, + {"name": "MATRIX$2$2$m", "assigned": True, "value": 3, "position": 2}, + {"name": "MATRIX$2$2$m", "assigned": True, "value": 4, "position": 3}, + ] + + sol = Solution(content) + + buf = StringIO() + with redirect_stdout(buf): + sol.pretty_print() + + output = buf.getvalue() + + self.assertIn("(2 x 2 matrix)", output) + self.assertIn("1 2", output) + self.assertIn("3 4", output) + + def test_pretty_print_empty(self): + sol = Solution([]) + + buf = StringIO() + with redirect_stdout(buf): + sol.pretty_print() + + self.assertIn("Empty solution", buf.getvalue()) diff --git a/tests/model/constraint/test_distinct.py b/tests/model/constraint/test_distinct.py deleted file mode 100644 index 21e9c59..0000000 --- a/tests/model/constraint/test_distinct.py +++ /dev/null @@ -1,151 +0,0 @@ -# pylint: skip-file - - -import unittest - -from qaekwy.core.model.variable.integer import IntegerVariableArray -from qaekwy.core.model.constraint.distinct import ( - ConstraintDistinctArray, - ConstraintDistinctCol, - ConstraintDistinctRow, - ConstraintDistinctSlice, -) - - -class TestConstraintDistinctArray(unittest.TestCase): - - def setUp(self): - self.array_var = IntegerVariableArray("array_var", 10, 0, 30) - - def test_constraint_array_creation(self): - constraint = ConstraintDistinctArray( - self.array_var, "distinct_array_constraint" - ) - self.assertEqual(constraint.var_1, self.array_var) - self.assertEqual(constraint.constraint_name, "distinct_array_constraint") - - def test_constraint_array_to_json(self): - constraint = ConstraintDistinctArray( - self.array_var, "distinct_array_constraint" - ) - expected_json = { - "name": "distinct_array_constraint", - "type": "distinct", - "v1": "array_var", - "selection": "standard", - } - - self.assertDictEqual(constraint.to_json(), expected_json) - - -class TestConstraintDistinctRow(unittest.TestCase): - - def setUp(self): - self.array_var = IntegerVariableArray("array_var", 10, 0, 30) - - def test_constraint_row_creation(self): - constraint = ConstraintDistinctRow( - self.array_var, size=3, idx=1, constraint_name="distinct_row_constraint" - ) - self.assertEqual(constraint.var_1, self.array_var) - self.assertEqual(constraint.size, 3) - self.assertEqual(constraint.idx, 1) - self.assertEqual(constraint.constraint_name, "distinct_row_constraint") - - def test_constraint_row_to_json(self): - constraint = ConstraintDistinctRow( - self.array_var, size=3, idx=1, constraint_name="distinct_row_constraint" - ) - expected_json = { - "name": "distinct_row_constraint", - "type": "distinct", - "v1": "array_var", - "selection": "row", - "size": 3, - "index": 1, - } - self.assertDictEqual(constraint.to_json(), expected_json) - - -class TestConstraintDistinctCol(unittest.TestCase): - - def setUp(self): - self.array_var = IntegerVariableArray("array_var", 10, 0, 30) - - def test_constraint_column_creation(self): - constraint = ConstraintDistinctCol( - self.array_var, size=3, idx=0, constraint_name="distinct_col_constraint" - ) - self.assertEqual(constraint.var_1, self.array_var) - self.assertEqual(constraint.size, 3) - self.assertEqual(constraint.idx, 0) - self.assertEqual(constraint.constraint_name, "distinct_col_constraint") - - def test_constraint_column_to_json(self): - constraint = ConstraintDistinctCol( - self.array_var, size=3, idx=0, constraint_name="distinct_col_constraint" - ) - expected_json = { - "name": "distinct_col_constraint", - "type": "distinct", - "v1": "array_var", - "selection": "col", - "size": 3, - "index": 0, - } - self.assertDictEqual(constraint.to_json(), expected_json) - - -class TestConstraintDistinctSlice(unittest.TestCase): - - def setUp(self): - self.array_var = IntegerVariableArray("array_var", 10, 0, 30) - - def test_constraint_slice_creation(self): - constraint = ConstraintDistinctSlice( - self.array_var, - size=6, - offset_start_x=1, - offset_start_y=1, - offset_end_x=3, - offset_end_y=2, - constraint_name="distinct_slice_constraint", - ) - self.assertEqual(constraint.var_1, self.array_var) - self.assertEqual(constraint.size, 6) - self.assertEqual(constraint.offset_start_x, 1) - self.assertEqual(constraint.offset_start_y, 1) - self.assertEqual(constraint.offset_end_x, 3) - self.assertEqual(constraint.offset_end_y, 2) - self.assertEqual(constraint.constraint_name, "distinct_slice_constraint") - - def test_constraint_slice_to_json(self): - constraint = ConstraintDistinctSlice( - self.array_var, - size=6, - offset_start_x=1, - offset_start_y=1, - offset_end_x=3, - offset_end_y=2, - constraint_name="distinct_slice_constraint", - ) - expected_json = { - "name": "distinct_slice_constraint", - "type": "distinct", - "v1": "array_var", - "selection": "slice", - "size": 6, - "offset_start_x": 1, - "offset_start_y": 1, - "offset_end_x": 3, - "offset_end_y": 2, - } - self.assertDictEqual( - constraint.to_json(), - constraint.from_json(expected_json, [self.array_var]).to_json(), - expected_json, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/model/test_modeller.py b/tests/model/test_modeller.py deleted file mode 100644 index 5065c84..0000000 --- a/tests/model/test_modeller.py +++ /dev/null @@ -1,105 +0,0 @@ -# pylint: skip-file - -import unittest -from qaekwy.core.model.constraint.abs import ConstraintAbs -from qaekwy.core.model.modeller import Modeller -from qaekwy.core.model.specific import SpecificMinimum -from qaekwy.core.model.searcher import SearcherType -from qaekwy.core.model.cutoff import CutoffFibonacci -from qaekwy.core.model.variable.integer import IntegerVariable - - -class TestModeller(unittest.TestCase): - - def setUp(self): - self.modeller = Modeller() - self.var1 = IntegerVariable("var1", 0, 10) - self.var2 = IntegerVariable("var2", 0, 10) - self.constraint = ConstraintAbs( - var_1=self.var1, var_2=self.var2, constraint_name="abs" - ) - self.objective = SpecificMinimum(self.var1) - self.searcher = SearcherType.DFS - self.cutoff = CutoffFibonacci() - self.callback_url = "https://example.com/callback" - - def test_add_variable(self): - self.modeller.add_variable(self.var1).add_variable(self.var2) - self.assertEqual(self.modeller.variable_list, [self.var1, self.var2]) - - def test_add_constraint(self): - self.modeller.add_constraint(self.constraint) - self.assertEqual(self.modeller.constraint_list, [self.constraint]) - - def test_add_objective(self): - self.modeller.add_objective(self.objective) - self.assertEqual(self.modeller.objective_list, [self.objective]) - - def test_set_searcher(self): - self.modeller.set_searcher(self.searcher) - self.assertEqual(self.modeller.searcher, self.searcher) - - def test_set_cutoff(self): - self.modeller.set_cutoff(self.cutoff) - self.assertEqual(self.modeller.cutoff, self.cutoff) - - def test_set_callback_url(self): - self.modeller.set_callback_url(self.callback_url) - self.assertEqual(self.modeller.callback_url, self.callback_url) - - def test_to_json(self): - self.modeller.add_variable(self.var1).add_variable(self.var2).add_constraint( - self.constraint - ).add_objective(self.objective) - self.modeller.set_searcher(self.searcher).set_cutoff( - self.cutoff - ).set_callback_url(self.callback_url) - - expected_json = { - "callback_url": "https://example.com/callback", - "constraint": [{"name": "abs", "type": "abs", "v1": "var1", "v2": "var2"}], - "cutoff": {"name": "fibonacci"}, - "solution_limit": 1, - "specific": [{"type": "minimize", "var": "var1"}], - "var": [ - { - "brancher_value": "VAL_RND", - "branching_order": -1, - "domlow": 0, - "domup": 10, - "name": "var1", - "type": "integer", - }, - { - "brancher_value": "VAL_RND", - "branching_order": -1, - "domlow": 0, - "domup": 10, - "name": "var2", - "type": "integer", - }, - ], - } - - print( - self.modeller.from_json( - self.modeller.from_json(expected_json).to_json(serialization=True) - ).to_json(serialization=True) - ) - - self.assertDictEqual( - self.modeller.to_json(serialization=True), - self.modeller.from_json(expected_json).to_json(serialization=True), - self.modeller.from_json( - self.modeller.from_json(expected_json).to_json(serialization=True) - ).to_json(serialization=True), - ) - - self.assertDictEqual( - self.modeller.to_json(serialization=True), - expected_json, - ) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/model/variable/test_integer.py b/tests/model/variable/test_integer.py deleted file mode 100644 index 2c55475..0000000 --- a/tests/model/variable/test_integer.py +++ /dev/null @@ -1,102 +0,0 @@ -# pylint: skip-file - -import unittest - -from qaekwy.core.model.variable.branch import BranchIntegerVal -from qaekwy.core.model.variable.integer import IntegerVariable -from qaekwy.core.model.variable.variable import Expression - - -class TestExpression(unittest.TestCase): - def test_arithmetic_operations(self): - expr = Expression("x") - expr_add = expr + 2 - self.assertEqual(str(expr_add), "(x + 2)") - - expr_sub = expr - 3 - self.assertEqual(str(expr_sub), "(x - 3)") - - expr_mul = expr * 4 - self.assertEqual(str(expr_mul), "x * 4") - - expr_div = expr / 5 - self.assertEqual(str(expr_div), "((x) / (5))") - - expr_mod = expr % 6 - self.assertEqual(str(expr_mod), "((x) % (6))") - - -class TestVariable(unittest.TestCase): - def test_variable_to_json(self): - var = IntegerVariable("x", domain_low=0, domain_high=10) - - var_json = var.to_json() - self.assertEqual(var_json["name"], "x") - self.assertEqual(var_json["type"], "integer") - self.assertEqual(var_json["brancher_value"], "VAL_RND") - self.assertEqual(var_json["domlow"], 0) - self.assertEqual(var_json["domup"], 10) - - -class TestSpecificDomainVariable(unittest.TestCase): - def test_variable_to_json(self): - var = IntegerVariable("x", specific_domain=[2, 4, 6]) - - var_json = var.to_json() - self.assertEqual(var_json["name"], "x") - self.assertEqual(var_json["type"], "integer") - self.assertEqual(var_json["brancher_value"], "VAL_RND") - self.assertEqual(var_json["specific_domain"], [2, 4, 6]) - - -class TestExprVariable(unittest.TestCase): - def test_variable_to_json(self): - var = IntegerVariable("x") - var_expr = var + 2 - var.expression = var_expr - - var_json = var.to_json() - self.assertEqual(var_json["name"], "x") - self.assertEqual(var_json["type"], "integer") - self.assertEqual(var_json["brancher_value"], "VAL_RND") - self.assertEqual(var_json["expr"], "(x + 2)") - - -class TestIntegerVariable(unittest.TestCase): - def test_integer_variable_to_json(self): - int_var = IntegerVariable("y", domain_low=1, domain_high=5) - int_var_json = int_var.to_json() - self.assertEqual(int_var_json["name"], "y") - self.assertEqual(int_var_json["type"], "integer") - self.assertEqual(int_var_json["brancher_value"], "VAL_RND") - self.assertEqual(int_var_json["domlow"], 1) - self.assertEqual(int_var_json["domup"], 5) - - def test_from_json(self): - json_data = { - "name": "i", - "type": "integer", - "brancher_value": "VAL_MAX", - "specific_domain": [1, 2, 3], - } - i = IntegerVariable.from_json(json_data) - self.assertEqual(i.var_name, "i") - self.assertEqual(i.specific_domain, [1, 2, 3]) - self.assertEqual(i.branch_val, BranchIntegerVal.VAL_MAX) - - json_data = { - "name": "j", - "type": "integer", - "brancher_value": "VAL_MAX", - "domlow": 0, - "domup": 100, - } - i = IntegerVariable.from_json(json_data) - self.assertEqual(i.var_name, "j") - self.assertEqual(i.domain_low, 0) - self.assertEqual(i.domain_high, 100) - self.assertEqual(i.branch_val, BranchIntegerVal.VAL_MAX) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/test_solution.py b/tests/test_solution.py deleted file mode 100644 index 81f7371..0000000 --- a/tests/test_solution.py +++ /dev/null @@ -1,65 +0,0 @@ -# pylint: skip-file - -import unittest - -from qaekwy.core.solution import Solution - - -class SolutionTest(unittest.TestCase): - - def test_init(self): - solution_json_content = [ - {"name": "x", "assigned": True, "value": 5}, - {"name": "y", "assigned": True, "value": 10}, - {"name": "z", "assigned": False, "value": None}, - ] - solution = Solution(solution_json_content) - - self.assertEqual(solution["x"], 5) - self.assertEqual(solution["y"], 10) - self.assertEqual(solution["z"], None) - - self.assertTrue(hasattr(solution, "x")) - self.assertTrue(hasattr(solution, "y")) - self.assertTrue(hasattr(solution, "z")) - - self.assertEqual(solution.x, 5) - self.assertEqual(solution.y, 10) - self.assertEqual(solution.z, None) - - def test_positional_assignment(self): - solution_json_content = [ - {"name": "x", "assigned": True, "value": 5, "position": 1}, - {"name": "y", "assigned": True, "value": 10, "position": 0}, - {"name": "z", "assigned": False, "value": None}, - ] - solution = Solution(solution_json_content) - - self.assertEqual(solution["x"], [None, 5]) - self.assertEqual(solution["y"][0], 10) - self.assertEqual(solution["z"], None) - - def test_missing_variable(self): - solution_json_content = [ - {"name": "x", "assigned": True, "value": 5}, - {"name": "y", "assigned": True, "value": 10}, - ] - solution = Solution(solution_json_content) - - with self.assertRaises(KeyError): - solution["z"] - - def test_invalid_position(self): - solution_json_content = [ - {"name": "x", "assigned": True, "value": 5, "position": 1}, - {"name": "y", "assigned": True, "value": 10, "position": 0}, - {"name": "z", "assigned": False, "value": None}, - ] - solution = Solution(solution_json_content) - - with self.assertRaises(IndexError): - solution["x"][2] - - -if __name__ == "__main__": - unittest.main() From bbbc24fa9fa69bf607951745c083056376250dd5 Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:16:26 +0100 Subject: [PATCH 2/9] dev-0-3-1 README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index bfb0a2f..eeb69c2 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ z = m.integer_variable("z", (-10, 10)) m.constraint(x + 2*y + 3*z <= 15) m.maximize(x) -m.solve_one().pretty_print() +m.solve_one(searcher="bab").pretty_print() ``` *Output*: @@ -53,9 +53,9 @@ m.solve_one().pretty_print() ---------------------------------------- Solution: ---------------------------------------- -x: -3 +x: 10 y: 2 -z: 4 +z: -4 ---------------------------------------- ``` From 7d5a9f435b67fa091ca9b44d44da91ca3a72e009 Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:20:11 +0100 Subject: [PATCH 3/9] add: coverage report --- .github/workflows/ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6eb27d0..5b16cdd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,8 +45,8 @@ jobs: run: | python -m pytest --cov=qaekwy --cov-report=xml --cov-fail-under=80 tests -# - name: Upload coverage to Codecov -# uses: codecov/codecov-action@v4 -# with: -# files: coverage.xml -# fail_ci_if_error: true + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4 + with: + files: coverage.xml + fail_ci_if_error: true From 4f1dc9a809d3b4ff556019188e7762527a27ce8f Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:22:17 +0100 Subject: [PATCH 4/9] fix: code linting --- qaekwy/__init__.py | 27 ++++++++---- qaekwy/__main__.py | 10 ++++- qaekwy/api/model.py | 57 ++++++++++++++++++-------- qaekwy/core/engine.py | 13 ++++-- qaekwy/core/model/modeller.py | 18 +++++--- qaekwy/core/model/variable/boolean.py | 10 ++++- qaekwy/core/model/variable/float.py | 10 ++++- qaekwy/core/model/variable/integer.py | 10 ++++- qaekwy/core/model/variable/variable.py | 13 ++++-- 9 files changed, 124 insertions(+), 44 deletions(-) diff --git a/qaekwy/__init__.py b/qaekwy/__init__.py index dcbefab..3104fd1 100644 --- a/qaekwy/__init__.py +++ b/qaekwy/__init__.py @@ -3,13 +3,26 @@ from .api.exceptions import SolverError from .api.model import Model from .core.model import function as math -from .core.model.cutoff import (Cutoff, CutoffConstant, CutoffFibonacci, - CutoffGeometric, CutoffLinear, CutoffLuby, - CutoffRandom, MetaCutoffAppender, - MetaCutoffMerger, MetaCutoffRepeater) -from .core.model.variable.branch import (BranchBooleanVal, BranchBooleanVar, - BranchFloatVal, BranchFloatVar, - BranchIntegerVal, BranchIntegerVar) +from .core.model.cutoff import ( + Cutoff, + CutoffConstant, + CutoffFibonacci, + CutoffGeometric, + CutoffLinear, + CutoffLuby, + CutoffRandom, + MetaCutoffAppender, + MetaCutoffMerger, + MetaCutoffRepeater, +) +from .core.model.variable.branch import ( + BranchBooleanVal, + BranchBooleanVar, + BranchFloatVal, + BranchFloatVar, + BranchIntegerVal, + BranchIntegerVar, +) __all__ = [ "Model", diff --git a/qaekwy/__main__.py b/qaekwy/__main__.py index aa2407f..d76f9f7 100644 --- a/qaekwy/__main__.py +++ b/qaekwy/__main__.py @@ -4,8 +4,14 @@ import argparse -from .__metadata__ import (__author__, __copyright__, __license__, - __license_url__, __software__, __version__) +from .__metadata__ import ( + __author__, + __copyright__, + __license__, + __license_url__, + __software__, + __version__, +) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Qaekwy Python Library") diff --git a/qaekwy/api/model.py b/qaekwy/api/model.py index 2867479..19d09e5 100644 --- a/qaekwy/api/model.py +++ b/qaekwy/api/model.py @@ -24,10 +24,12 @@ from ..core.model.constraint.asin import ConstraintASin from ..core.model.constraint.atan import ConstraintATan from ..core.model.constraint.cos import ConstraintCos -from ..core.model.constraint.distinct import (ConstraintDistinctArray, - ConstraintDistinctCol, - ConstraintDistinctRow, - ConstraintDistinctSlice) +from ..core.model.constraint.distinct import ( + ConstraintDistinctArray, + ConstraintDistinctCol, + ConstraintDistinctRow, + ConstraintDistinctSlice, +) from ..core.model.constraint.divide import ConstraintDivide from ..core.model.constraint.element import ConstraintElement from ..core.model.constraint.exponential import ConstraintExponential @@ -41,25 +43,44 @@ from ..core.model.constraint.nroot import ConstraintNRoot from ..core.model.constraint.power import ConstraintPower from ..core.model.constraint.sin import ConstraintSin -from ..core.model.constraint.sort import (ConstraintReverseSorted, - ConstraintSorted) +from ..core.model.constraint.sort import ConstraintReverseSorted, ConstraintSorted from ..core.model.constraint.tan import ConstraintTan from ..core.model.cutoff import Cutoff from ..core.model.modeller import Modeller from ..core.model.searcher import SearcherType from ..core.model.specific import SpecificMaximum, SpecificMinimum -from ..core.model.variable.boolean import BooleanVariable, BooleanVariableArray, BooleanVariableMatrix -from ..core.model.variable.branch import (BranchBooleanVal, BranchBooleanVar, - BranchFloatVal, BranchFloatVar, - BranchIntegerVal, BranchIntegerVar) -from ..core.model.variable.float import (FloatExpressionVariable, - FloatVariable, FloatVariableArray, FloatVariableMatrix) -from ..core.model.variable.integer import (IntegerExpressionVariable, - IntegerVariable, - IntegerVariableArray, IntegerVariableMatrix) -from ..core.model.variable.variable import (ArrayVariable, Expression, - MatrixVariable, Variable, - VariableType, VectorExpression) +from ..core.model.variable.boolean import ( + BooleanVariable, + BooleanVariableArray, + BooleanVariableMatrix, +) +from ..core.model.variable.branch import ( + BranchBooleanVal, + BranchBooleanVar, + BranchFloatVal, + BranchFloatVar, + BranchIntegerVal, + BranchIntegerVar, +) +from ..core.model.variable.float import ( + FloatExpressionVariable, + FloatVariable, + FloatVariableArray, + FloatVariableMatrix, +) +from ..core.model.variable.integer import ( + IntegerExpressionVariable, + IntegerVariable, + IntegerVariableArray, + IntegerVariableMatrix, +) +from ..core.model.variable.variable import ( + ArrayVariable, + Expression, + Variable, + VariableType, + VectorExpression, +) from ..core.response import SolutionResponse from ..core.solution import Solution from .exceptions import SolverError diff --git a/qaekwy/core/engine.py b/qaekwy/core/engine.py index 3d684bf..e6ad5e6 100644 --- a/qaekwy/core/engine.py +++ b/qaekwy/core/engine.py @@ -81,9 +81,16 @@ from ..__metadata__ import __software__, __version__ from .model import DIRECTENGINE_API_ENDPOINT from .model.modeller import Modeller -from .response import (AbstractResponse, ClusterStatusResponse, EchoResponse, - ExplanationResponse, ModelJSonResponse, - SolutionResponse, StatusResponse, VersionResponse) +from .response import ( + AbstractResponse, + ClusterStatusResponse, + EchoResponse, + ExplanationResponse, + ModelJSonResponse, + SolutionResponse, + StatusResponse, + VersionResponse, +) class AbstractAction(ABC): diff --git a/qaekwy/core/model/modeller.py b/qaekwy/core/model/modeller.py index 2d93d30..67b56f5 100644 --- a/qaekwy/core/model/modeller.py +++ b/qaekwy/core/model/modeller.py @@ -27,9 +27,12 @@ from .constraint.asin import ConstraintASin from .constraint.atan import ConstraintATan from .constraint.cos import ConstraintCos -from .constraint.distinct import (ConstraintDistinctArray, - ConstraintDistinctCol, ConstraintDistinctRow, - ConstraintDistinctSlice) +from .constraint.distinct import ( + ConstraintDistinctArray, + ConstraintDistinctCol, + ConstraintDistinctRow, + ConstraintDistinctSlice, +) from .constraint.divide import ConstraintDivide from .constraint.element import ConstraintElement from .constraint.exponential import ConstraintExponential @@ -49,8 +52,13 @@ from .cutoff import Cutoff from .searcher import SearcherType from .specific import SpecificMaximum, SpecificMinimum -from .variable.variable import (ArrayVariable, Expression, MatrixVariable, - Variable, VariableType) +from .variable.variable import ( + ArrayVariable, + Expression, + MatrixVariable, + Variable, + VariableType, +) ConstraintFactory = Callable[ [dict, list[Union[Variable, ArrayVariable, MatrixVariable]]], diff --git a/qaekwy/core/model/variable/boolean.py b/qaekwy/core/model/variable/boolean.py index 65d7033..75c3cf0 100644 --- a/qaekwy/core/model/variable/boolean.py +++ b/qaekwy/core/model/variable/boolean.py @@ -12,8 +12,14 @@ from typing import Optional from .branch import BranchBooleanVal, BranchBooleanVar, BranchVal, BranchVar -from .variable import (ArrayVariable, Expression, ExpressionVariable, - MatrixVariable, Variable, VariableType) +from .variable import ( + ArrayVariable, + Expression, + ExpressionVariable, + MatrixVariable, + Variable, + VariableType, +) class BooleanVariable(Variable): diff --git a/qaekwy/core/model/variable/float.py b/qaekwy/core/model/variable/float.py index 9f2c020..c25c36c 100644 --- a/qaekwy/core/model/variable/float.py +++ b/qaekwy/core/model/variable/float.py @@ -12,8 +12,14 @@ from typing import Optional from .branch import BranchFloatVal, BranchFloatVar, BranchVal -from .variable import (ArrayVariable, Expression, ExpressionVariable, - MatrixVariable, Variable, VariableType) +from .variable import ( + ArrayVariable, + Expression, + ExpressionVariable, + MatrixVariable, + Variable, + VariableType, +) class FloatVariable(Variable): diff --git a/qaekwy/core/model/variable/integer.py b/qaekwy/core/model/variable/integer.py index 0f166d6..e439678 100644 --- a/qaekwy/core/model/variable/integer.py +++ b/qaekwy/core/model/variable/integer.py @@ -13,8 +13,14 @@ from typing import Optional from .branch import BranchIntegerVal, BranchIntegerVar, BranchVal, BranchVar -from .variable import (ArrayVariable, Expression, ExpressionVariable, - MatrixVariable, Variable, VariableType) +from .variable import ( + ArrayVariable, + Expression, + ExpressionVariable, + MatrixVariable, + Variable, + VariableType, +) class IntegerVariable(Variable): diff --git a/qaekwy/core/model/variable/variable.py b/qaekwy/core/model/variable/variable.py index 29a4f64..8440e19 100644 --- a/qaekwy/core/model/variable/variable.py +++ b/qaekwy/core/model/variable/variable.py @@ -19,9 +19,16 @@ from enum import Enum from typing import Optional, Union -from .branch import (BranchBooleanVal, BranchBooleanVar, BranchFloatVal, - BranchFloatVar, BranchIntegerVal, BranchIntegerVar, - BranchVal, BranchVar) +from .branch import ( + BranchBooleanVal, + BranchBooleanVar, + BranchFloatVal, + BranchFloatVar, + BranchIntegerVal, + BranchIntegerVar, + BranchVal, + BranchVar, +) class VariableType(Enum): From aecc8bd7dc76c85bed4d2b4a4caf346ec7fbdf8a Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:24:24 +0100 Subject: [PATCH 5/9] add: dev deps --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 3ddce64..d382602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ dev = [ "pytest-cov", "black", "mypy", + "pylint", "build", "twine", ] From ccb316b6ae3d6578c5cafdfcd57efc858ded768f Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:27:01 +0100 Subject: [PATCH 6/9] fix: ci coverage rendering from xml to json --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5b16cdd..70bdb53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,10 @@ jobs: - name: Test with Pytest + coverage run: | - python -m pytest --cov=qaekwy --cov-report=xml --cov-fail-under=80 tests + python -m pytest --cov=qaekwy --cov-report=json --cov-fail-under=80 tests - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: - files: coverage.xml + files: coverage.json fail_ci_if_error: true From 2794da272a6ec78814f95de9b3f4f40f7664c65c Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:38:19 +0100 Subject: [PATCH 7/9] add: coverage badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eeb69c2..8d7d101 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ *A modern, open-source Python framework for declarative constraint programming and combinatorial optimization*. -![GitHub License](https://img.shields.io/github/license/alex-87/qaekwy-python) ![PyPI - Version](https://img.shields.io/pypi/v/qaekwy) +![GitHub License](https://img.shields.io/github/license/alex-87/qaekwy-python) ![PyPI - Version](https://img.shields.io/pypi/v/qaekwy) ![Coverage](https://codecov.io/gh/alex-87/qaekwy-python/branch/main/graph/badge.svg) + ## Overview From 56b66da9ef890b69d4f40cc93e7355916e10522a Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 18:53:33 +0100 Subject: [PATCH 8/9] fix: CI --- .github/workflows/ci.yml | 8 +------- README.md | 4 +--- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70bdb53..bf35e36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,10 +43,4 @@ jobs: - name: Test with Pytest + coverage run: | - python -m pytest --cov=qaekwy --cov-report=json --cov-fail-under=80 tests - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 - with: - files: coverage.json - fail_ci_if_error: true + python -m pytest --cov=qaekwy --cov-fail-under=80 tests diff --git a/README.md b/README.md index 8d7d101..007ab2f 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,7 @@ *A modern, open-source Python framework for declarative constraint programming and combinatorial optimization*. -![GitHub License](https://img.shields.io/github/license/alex-87/qaekwy-python) ![PyPI - Version](https://img.shields.io/pypi/v/qaekwy) ![Coverage](https://codecov.io/gh/alex-87/qaekwy-python/branch/main/graph/badge.svg) - - +![GitHub License](https://img.shields.io/github/license/alex-87/qaekwy-python) ![PyPI - Version](https://img.shields.io/pypi/v/qaekwy) ## Overview Qaekwy is a Python library designed for modeling and solving combinatorial optimization and constraint satisfaction problems. From bb3f7dd5682ef9bccc756d22fd289eec76109b44 Mon Sep 17 00:00:00 2001 From: alex-87 Date: Sat, 10 Jan 2026 19:04:50 +0100 Subject: [PATCH 9/9] fix: README --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 007ab2f..5d35438 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,9 @@ It provides a clean, Pythonic interface for defining variables, constraints, and * 👩‍🏫 **Teaching** — Demonstrate CSP concepts with no setup * 🔬 **Research & Prototyping** — Explore models, heuristics, and ideas fast +## 📚 Documentation + +Visit the [Qaekwy Documentation](https://docs.qaekwy.io/) for guides, teaching resources, and detailed examples. ## 🚀 Quick Start @@ -73,11 +76,6 @@ Configure solver behavior using explicit search strategies such as Depth-First S Transparent handling of model serialization and execution on the Qaekwy Cloud Solver instance. -## 📚 Documentation - -Visit the [Qaekwy Documentation](https://docs.qaekwy.io/) for guides, teaching resources, and detailed examples. - - ## Examples ### 🔢 Constraint Programming -- Sudoku