├── pychoco ├── py.typed ├── objects │ ├── __init__.py │ ├── automaton │ │ ├── __init__.py │ │ └── cost_automaton.py │ └── graphs │ │ ├── __init__.py │ │ └── multivalued_decision_diagram.py ├── search │ └── __init__.py ├── constraints │ ├── __init__.py │ ├── cnf │ │ ├── __init__.py │ │ └── log_op.py │ ├── extension │ │ ├── __init__.py │ │ └── hybrid │ │ │ └── __init__.py │ └── constraint.py ├── variables │ ├── __init__.py │ ├── graphvar.py │ ├── setvar.py │ ├── directed_graphvar.py │ ├── variable.py │ ├── undirected_graphvar.py │ ├── boolvar.py │ └── task.py ├── utils.py ├── _handle_wrapper.py ├── __init__.py ├── solution.py └── model.py ├── tests ├── __init__.py ├── int_constraints │ ├── __init__.py │ ├── test_not_.py │ ├── test_square.py │ ├── test_max.py │ ├── test_min.py │ ├── test_sort.py │ ├── test_lex_less.py │ ├── test_div.py │ ├── test_lex_less_eq.py │ ├── test_cumulative.py │ ├── test_mod.py │ ├── test_tree.py │ ├── test_clauses_int_channeling.py │ ├── test_diff_n.py │ ├── test_path.py │ ├── test_not_all_equal.py │ ├── test_sub_circuit.py │ ├── test_lex_chain_less.py │ ├── test_inverse_channeling.py │ ├── test_lex_chain_less_eq.py │ ├── test_decreasing.py │ ├── test_increasing.py │ ├── test_absolute.py │ ├── test_n_values.py │ ├── test_member.py │ ├── test_argmax.py │ ├── test_argmin.py │ ├── test_at_least_n_values.py │ ├── test_at_most_n_values.py │ ├── test_not_member.py │ ├── test_bin_packing.py │ ├── test_knapsack.py │ ├── test_circuit.py │ ├── test_among.py │ ├── test_global_cardinality.py │ ├── test_mddc.py │ ├── test_all_equal.py │ ├── test_all_different.py │ ├── test_int_value_precede_chain.py │ ├── test_bools_int_channeling.py │ ├── test_pow.py │ ├── test_times.py │ ├── test_count.py │ ├── test_or.py │ ├── test_table.py │ ├── test_hybrid_table.py │ ├── test_sub_path.py │ ├── test_element.py │ ├── test_sum.py │ ├── test_distance.py │ ├── test_and.py │ ├── test_scalar.py │ ├── test_bits_int_channeling.py │ ├── test_keysort.py │ ├── test_regular.py │ ├── test_multi_cost_regular.py │ └── test_cost_regular.py ├── reification │ ├── __init__.py │ └── test_reify.py ├── sat_constraints │ └── __init__.py ├── set_constraints │ ├── __init__.py │ ├── test_set_not_empty.py │ ├── test_set_max.py │ ├── test_set_min.py │ ├── test_set_sum.py │ ├── test_set_member_int.py │ ├── test_set_bools_channeling.py │ ├── test_set_not_member_int.py │ ├── test_set_offset.py │ ├── test_set_subseteq.py │ ├── test_set_disjoint.py │ ├── test_set_symmetric.py │ ├── test_set_nb_empty.py │ ├── test_set_sum_element.py │ ├── test_set_max_indices.py │ ├── test_set_min_indices.py │ ├── test_set_element.py │ ├── test_set_member_set.py │ ├── test_set_inverse_set.py │ ├── test_set_all_equal.py │ ├── test_set_all_different.py │ ├── test_set_ints_channeling.py │ ├── test_set_union.py │ ├── test_set_all_disjoint.py │ ├── test_set_intersection.py │ ├── test_set_le.py │ ├── test_set_union_indices.py │ ├── test_set_lt.py │ └── test_set_partition.py ├── graph_constraints │ ├── __init__.py │ ├── test_graph_node_channeling.py │ ├── test_graph_max_degree.py │ ├── test_graph_min_degree.py │ ├── test_graph_no_cycle.py │ ├── test_graph_max_in_degree.py │ ├── test_graph_max_out_degree.py │ ├── test_graph_min_in_degree.py │ ├── test_graph_min_out_degree.py │ ├── test_graph_biconnected.py │ ├── test_graph_no_circuit.py │ ├── test_graph_tree.py │ ├── test_graph_directed_tree.py │ ├── test_graph_forest.py │ ├── test_graph_symmetric.py │ ├── test_graph_directed_forest.py │ ├── test_graph_anti_symmetric.py │ ├── test_graph_degrees.py │ ├── test_graph_max_degrees.py │ ├── test_graph_max_in_degrees.py │ ├── test_graph_max_out_degrees.py │ ├── test_graph_min_degrees.py │ ├── test_graph_min_in_degrees.py │ ├── test_graph_min_out_degrees.py │ ├── test_graph_in_degrees.py │ ├── test_graph_out_degrees.py │ ├── test_graph_reachability.py │ ├── test_graph_diameter.py │ ├── test_graph_nb_loops.py │ ├── test_graph_connected.py │ ├── test_graph_loop_set.py │ ├── test_graph_size_max_connected_components.py │ ├── test_graph_size_min_connected_components.py │ ├── test_graph_strongly_connected.py │ ├── test_graph_nb_strongly_connected_components.py │ ├── test_graph_cycle.py │ ├── test_graph_nb_edges.py │ ├── test_graph_nb_nodes.py │ ├── test_graph_nb_cliques.py │ ├── test_graph_size_connected_components.py │ ├── test_graph_nb_connected_components.py │ ├── test_graph_edge_channeling.py │ ├── test_graph_node_successors_channeling.py │ ├── test_graph_node_neighbors_channeling.py │ ├── test_graph_node_predecessors_channeling.py │ ├── test_graph_nodes_channeling.py │ ├── test_graph_neighbors_channeling.py │ ├── test_graph_successors_channeling.py │ ├── test_graph_transitivity.py │ └── test_graph_subgraph.py ├── test_model.py ├── test_mdd.py ├── test_setvar.py ├── test_parallel_portfolio.py ├── test_task.py ├── test_search_strategies.py ├── test_graphvar.py └── test_graph.py ├── requirements_tests.txt ├── docs ├── pychoco-cheatsheet.pdf ├── _static │ ├── ChocoLogo-150x135.png │ └── ChocoLogo-300x345.png ├── api │ ├── index.rst │ ├── model.rst │ ├── pychoco.search.rst │ ├── pychoco.constraints.rst │ ├── pychoco.rst │ ├── pychoco.variables.rst │ └── views.rst ├── Makefile ├── make.bat ├── index.rst ├── conf.py ├── quickstart.rst └── installation.rst ├── paper └── pychoco_cheat_sheet.png ├── examples └── notebooks │ └── data │ ├── forest_kaala.tif │ └── africa_no_islands.gpkg ├── .gitmodules ├── requirements_docs.txt ├── .github ├── workflows │ ├── draft-pdf.yml │ ├── ubuntu.yml │ ├── windows.yml │ └── macos.yml └── actions │ └── build-choco-solver-capi │ └── action.yml ├── CITATION.cff ├── NEWS.md ├── LICENSE └── .gitignore /pychoco/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/objects/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/search/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/constraints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/variables/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/int_constraints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/reification/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/sat_constraints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/set_constraints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/constraints/cnf/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/objects/automaton/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/objects/graphs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements_tests.txt: -------------------------------------------------------------------------------- 1 | networkx >= 2 -------------------------------------------------------------------------------- /tests/graph_constraints/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/constraints/extension/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pychoco/constraints/extension/hybrid/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/pychoco-cheatsheet.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chocoteam/pychoco/HEAD/docs/pychoco-cheatsheet.pdf -------------------------------------------------------------------------------- /paper/pychoco_cheat_sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chocoteam/pychoco/HEAD/paper/pychoco_cheat_sheet.png -------------------------------------------------------------------------------- /docs/_static/ChocoLogo-150x135.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chocoteam/pychoco/HEAD/docs/_static/ChocoLogo-150x135.png -------------------------------------------------------------------------------- /docs/_static/ChocoLogo-300x345.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chocoteam/pychoco/HEAD/docs/_static/ChocoLogo-300x345.png -------------------------------------------------------------------------------- /examples/notebooks/data/forest_kaala.tif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chocoteam/pychoco/HEAD/examples/notebooks/data/forest_kaala.tif -------------------------------------------------------------------------------- /pychoco/utils.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class ESat(Enum): 5 | FALSE = 0 6 | TRUE = 1 7 | UNDEFINED = 2 8 | -------------------------------------------------------------------------------- /examples/notebooks/data/africa_no_islands.gpkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chocoteam/pychoco/HEAD/examples/notebooks/data/africa_no_islands.gpkg -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "choco-solver-capi"] 2 | path = choco-solver-capi 3 | url = https://github.com/chocoteam/choco-solver-capi.git 4 | branch = master 5 | -------------------------------------------------------------------------------- /docs/api/index.rst: -------------------------------------------------------------------------------- 1 | .. _api: 2 | 3 | API 4 | === 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | :caption: Contents: 9 | 10 | model 11 | variables 12 | constraints 13 | views -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestModel(unittest.TestCase): 7 | 8 | def test_create_model(self): 9 | model = Model("MyModel") 10 | self.assertEqual(model.name, "MyModel") 11 | -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | nbsphinx==0.9.2 2 | Sphinx==7.0.1 3 | sphinx-rtd-theme==1.2.2 4 | sphinx_gallery==0.13.0 5 | pychoco~=0.1.1 6 | networkx~=3.1 7 | setuptools>=70.0.0 8 | matplotlib~=3.7.2 9 | numpy~=1.25.1 10 | libpysal~=4.7.0 11 | geopandas~=0.13.2 12 | rasterio==1.3.8 13 | shapely==2.0.1 -------------------------------------------------------------------------------- /docs/api/model.rst: -------------------------------------------------------------------------------- 1 | .. _model: 2 | 3 | Model 4 | ===== 5 | 6 | The model is the core component of PyChoco. A model is created using the `Model()` constructor, 7 | and it is the entry point to create variables, constraints, and solve problems. 8 | 9 | .. autoclass:: pychoco.model.Model 10 | :members: 11 | :undoc-members: 12 | :noindex: 13 | 14 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_not_empty.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetNbEmpty(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | setvar = m.setvar([], range(0, 10)) 11 | m.set_not_empty(setvar).post() 12 | while m.get_solver().solve(): 13 | self.assertTrue(len(setvar.get_value()) > 0) 14 | -------------------------------------------------------------------------------- /tests/test_mdd.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.multivalued_decision_diagram import MultivaluedDecisionDiagram 5 | 6 | 7 | class TestMDD(unittest.TestCase): 8 | 9 | def test_mdd_1(self): 10 | model = Model("MyModel") 11 | v = model.intvars(4, 0, 10) 12 | tuples = [[0, 1, 1, 1], [1, 1, 1, 2]] 13 | mdd = MultivaluedDecisionDiagram(v, tuples) 14 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_max.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestMax(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 10) 11 | setvar = m.setvar([], range(0, 11)) 12 | m.set_max(setvar, intvar, True).post() 13 | while m.get_solver().solve(): 14 | self.assertEqual(intvar.get_value(), max(setvar.get_value())) 15 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_min.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestMin(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 10) 11 | setvar = m.setvar([], range(0, 11)) 12 | m.set_min(setvar, intvar, True).post() 13 | while m.get_solver().solve(): 14 | self.assertEqual(intvar.get_value(), min(setvar.get_value())) 15 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_sum.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetSum(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 100) 11 | setvar = m.setvar([], range(0, 10)) 12 | m.set_sum(setvar, intvar).post() 13 | while m.get_solver().solve(): 14 | self.assertEqual(sum(setvar.get_value()), intvar.get_value()) 15 | -------------------------------------------------------------------------------- /tests/int_constraints/test_not_.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestNot(unittest.TestCase): 7 | 8 | def testNot1(self): 9 | m = Model() 10 | a = m.intvar(0, 10) 11 | b = m.intvar(0, 10) 12 | c = m.arithm(a, ">", b) 13 | m.not_(c).post() 14 | sols = m.get_solver().find_all_solutions() 15 | for s in sols: 16 | self.assertTrue(s.get_int_val(a) <= s.get_int_val(b)) 17 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_member_int.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetMemberInt(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 100) 11 | setvar = m.setvar([], [1, 10, 20, 50, 77, 92]) 12 | m.set_member_int(intvar, setvar).post() 13 | while m.get_solver().solve(): 14 | self.assertTrue(intvar.get_value() in setvar.get_value()) 15 | -------------------------------------------------------------------------------- /docs/api/pychoco.search.rst: -------------------------------------------------------------------------------- 1 | pychoco.search package 2 | ====================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pychoco.search.search\_strategies module 8 | ---------------------------------------- 9 | 10 | .. automodule:: pychoco.search.search_strategies 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | Module contents 16 | --------------- 17 | 18 | .. automodule:: pychoco.search 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | -------------------------------------------------------------------------------- /tests/int_constraints/test_square.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from pychoco.model import Model 5 | 6 | 7 | class TestSquare(unittest.TestCase): 8 | 9 | def testSquare1(self): 10 | m = Model() 11 | x2 = m.intvar(0, 100) 12 | x = m.intvar(0, 10) 13 | m.square(x2, x).post() 14 | sols = m.get_solver().find_all_solutions() 15 | for s in sols: 16 | self.assertEqual(math.pow(s.get_int_val(x), 2), s.get_int_val(x2)) 17 | -------------------------------------------------------------------------------- /tests/int_constraints/test_max.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestMax(unittest.TestCase): 7 | 8 | def testMax1(self): 9 | m = Model() 10 | a = m.intvar(0, 5) 11 | b = m.intvars(5, 0, 5) 12 | m.max(a, b).post() 13 | sols = m.get_solver().find_all_solutions() 14 | for s in sols: 15 | vals = [s.get_int_val(i) for i in b] 16 | self.assertEqual(max(vals), s.get_int_val(a)) 17 | -------------------------------------------------------------------------------- /tests/int_constraints/test_min.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestMin(unittest.TestCase): 7 | 8 | def testMin(self): 9 | m = Model() 10 | a = m.intvar(0, 5) 11 | b = m.intvars(5, 0, 5) 12 | m.min(a, b).post() 13 | sols = m.get_solver().find_all_solutions() 14 | for s in sols: 15 | vals = [s.get_int_val(i) for i in b] 16 | self.assertEqual(min(vals), s.get_int_val(a)) 17 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_bools_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetBoolsChanneling(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.boolvars(5) 12 | m.set_bools_channeling(b, a).post() 13 | while m.get_solver().solve(): 14 | for i in a.get_value(): 15 | self.assertTrue(b[i].get_value()) 16 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_not_member_int.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetNotMemberInt(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 100) 11 | setvar = m.setvar([], [1, 10, 20, 50, 77, 92]) 12 | m.set_not_member_int(intvar, setvar).post() 13 | while m.get_solver().solve(): 14 | self.assertTrue(intvar.get_value() not in setvar.get_value()) 15 | -------------------------------------------------------------------------------- /tests/int_constraints/test_sort.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSort(unittest.TestCase): 7 | 8 | def testSort1(self): 9 | m = Model() 10 | iv1 = m.intvars(6, 0, 3) 11 | iv2 = m.intvars(6, 0, 3) 12 | sort = m.sort(iv1, iv2) 13 | sort.post() 14 | while m.get_solver().solve(): 15 | self.assertTrue(sort.is_satisfied()) 16 | self.assertTrue(m.get_solver().get_solution_count() > 0) 17 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_offset.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetOffset(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | s1 = m.setvar([], range(0, 10)) 11 | s2 = m.setvar([], range(0, 10)) 12 | m.set_offset(s1, s2, 2).post() 13 | while m.get_solver().solve(): 14 | value = [v - 2 for v in s2.get_value()] 15 | self.assertSetEqual(s1.get_value(), set(value)) 16 | -------------------------------------------------------------------------------- /tests/int_constraints/test_lex_less.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestLexLess(unittest.TestCase): 7 | 8 | def testLexLess1(self): 9 | m = Model() 10 | ar1 = m.intvars(3, 0, 5) 11 | ar2 = m.intvars(3, -1, 4) 12 | c = m.lex_less(ar1, ar2) 13 | c.post() 14 | while m.get_solver().solve(): 15 | self.assertTrue(c.is_satisfied()) 16 | self.assertTrue(m.get_solver().get_solution_count() > 0) 17 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_subseteq.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetSubsetEq(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | setvars = [m.setvar([], range(0, 5)) for i in range(0, 4)] 11 | m.set_subset_eq(setvars).post() 12 | while m.get_solver().solve(): 13 | for i in range(0, 3): 14 | self.assertTrue(setvars[i].get_value().issubset(setvars[i + 1].get_value())) 15 | -------------------------------------------------------------------------------- /tests/int_constraints/test_div.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestDiv(unittest.TestCase): 7 | 8 | def testDiv1(self): 9 | m = Model() 10 | a = m.intvar(0, 10) 11 | b = m.intvar(-10, 40) 12 | c = m.intvar(-1, 12) 13 | m.div(a, b, c).post() 14 | sols = m.get_solver().find_all_solutions() 15 | for s in sols: 16 | self.assertEqual(int(s.get_int_val(a) / s.get_int_val(b)), s.get_int_val(c)) 17 | -------------------------------------------------------------------------------- /tests/int_constraints/test_lex_less_eq.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestLexLessEq(unittest.TestCase): 7 | 8 | def testLexLessEq1(self): 9 | m = Model() 10 | ar1 = m.intvars(3, 0, 5) 11 | ar2 = m.intvars(3, -1, 4) 12 | c = m.lex_less_eq(ar1, ar2) 13 | c.post() 14 | while m.get_solver().solve(): 15 | self.assertTrue(c.is_satisfied()) 16 | self.assertTrue(m.get_solver().get_solution_count() > 0) 17 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_disjoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetDisjoint(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.setvar(set([]), set(range(0, 5))) 12 | m.set_disjoint(a, b).post() 13 | while m.get_solver().solve(): 14 | aa = a.get_value() 15 | bb = b.get_value() 16 | self.assertTrue(aa.isdisjoint(bb)) 17 | -------------------------------------------------------------------------------- /tests/int_constraints/test_cumulative.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestCumulative(unittest.TestCase): 7 | 8 | def testCumulative1(self): 9 | m = Model() 10 | t1 = m.task(m.intvar(9), m.intvar(6), m.intvar(15)) 11 | t2 = m.task(m.intvar(8), m.intvar(0, 6), m.intvar(8, 14)) 12 | m.cumulative([t1, t2], [m.intvar(1), m.intvar(1)], m.intvar(1)).post() 13 | m.get_solver().solve() 14 | self.assertEqual(t2.duration.get_value(), 0) 15 | -------------------------------------------------------------------------------- /tests/int_constraints/test_mod.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from pychoco.model import Model 5 | 6 | 7 | class TestMod(unittest.TestCase): 8 | 9 | def testMod1(self): 10 | m = Model() 11 | a = m.intvar(0, 10) 12 | b = m.intvar(-10, 40) 13 | c = m.intvar(-1, 12) 14 | m.mod(a, b, c).post() 15 | sols = m.get_solver().find_all_solutions() 16 | for s in sols: 17 | self.assertEqual(math.fmod(s.get_int_val(a), s.get_int_val(b)), s.get_int_val(c)) 18 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_symmetric.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetSymmetric(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | setvars = [m.setvar([], range(0, 10)) for i in range(0, 3)] 11 | m.set_symmetric(setvars).post() 12 | while m.get_solver().solve(): 13 | for y in range(0, len(setvars)): 14 | for x in setvars[y].get_value(): 15 | self.assertTrue(y in setvars[x].get_value()) 16 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_nb_empty.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetNbEmpty(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | setvars = [m.setvar([], range(0, 3)) for i in range(0, 4)] 11 | intvar = m.intvar(0, 3) 12 | m.set_nb_empty(setvars, intvar).post() 13 | while m.get_solver().solve(): 14 | nb_empty = sum([1 if len(s.get_value()) == 0 else 0 for s in setvars]) 15 | self.assertEqual(nb_empty, intvar.get_value()) 16 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_sum_element.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetSum(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 100) 11 | setvar = m.setvar([], range(0, 10)) 12 | weights = [1, 10, 15, 20, 2, 1, 4, 8, 65, 0] 13 | m.set_sum_element(setvar, weights, intvar).post() 14 | while m.get_solver().solve(): 15 | self.assertEqual(sum([weights[i] for i in setvar.get_value()]), intvar.get_value()) 16 | -------------------------------------------------------------------------------- /.github/workflows/draft-pdf.yml: -------------------------------------------------------------------------------- 1 | name: Draft PDF 2 | on: [push] 3 | 4 | jobs: 5 | paper: 6 | runs-on: ubuntu-latest 7 | name: Paper Draft 8 | steps: 9 | - name: Checkout 10 | uses: actions/checkout@v4 11 | - name: Build draft PDF 12 | uses: openjournals/openjournals-draft-action@master 13 | with: 14 | journal: joss 15 | paper-path: paper/paper.md 16 | - name: Upload 17 | uses: actions/upload-artifact@v4 18 | with: 19 | name: paper 20 | path: paper/paper.pdf -------------------------------------------------------------------------------- /tests/set_constraints/test_set_max_indices.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestMaxIndices(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 10) 11 | indices = m.setvar([], range(0, 11)) 12 | weights = [2, 3, 1, 5, 6, 8, 9, 0, 11, 1, 21] 13 | m.set_max_indices(indices, weights, intvar, True).post() 14 | while m.get_solver().solve(): 15 | self.assertEqual(intvar.get_value(), max([weights[i] for i in indices.get_value()])) 16 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_min_indices.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestMinIndices(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | intvar = m.intvar(0, 10) 11 | indices = m.setvar([], range(0, 11)) 12 | weights = [2, 3, 1, 5, 6, 8, 9, 0, 11, 1, 21] 13 | m.set_min_indices(indices, weights, intvar, True).post() 14 | while m.get_solver().solve(): 15 | self.assertEqual(intvar.get_value(), min([weights[i] for i in indices.get_value()])) 16 | -------------------------------------------------------------------------------- /tests/int_constraints/test_tree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestTree(unittest.TestCase): 7 | 8 | def testTree1(self): 9 | model = Model() 10 | vs = model.intvars(6, -1, 6) 11 | nt = model.intvar(2, 3) 12 | tree = model.tree(vs, nt) 13 | tree.post() 14 | model.get_solver().set_random_search(*vs) 15 | while model.get_solver().solve(): 16 | self.assertTrue(tree.is_satisfied()) 17 | self.assertTrue(model.get_solver().get_solution_count() > 0) 18 | -------------------------------------------------------------------------------- /tests/int_constraints/test_clauses_int_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAllDifferent(unittest.TestCase): 7 | 8 | def testClausesIntChanneling1(self): 9 | model = Model() 10 | iv = model.intvar(1, 50) 11 | eqs = model.boolvars(50) 12 | lqs = model.boolvars(50) 13 | model.clauses_int_channeling(iv, eqs, lqs).post() 14 | s = model.get_solver() 15 | while model.get_solver().solve(): 16 | pass 17 | self.assertEqual(s.get_solution_count(), 50) 18 | -------------------------------------------------------------------------------- /tests/int_constraints/test_diff_n.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestDiffN(unittest.TestCase): 7 | 8 | def testDiffN1(self): 9 | m = Model() 10 | x = m.intvars(2, 0, 2) 11 | y = m.intvars(2, 0, 2) 12 | width = m.intvars(2, 0, 2) 13 | height = m.intvars(2, 0, 2) 14 | diff_n = m.diff_n(x, y, width, height) 15 | diff_n.post() 16 | while m.get_solver().solve(): 17 | self.assertTrue(diff_n.is_satisfied()) 18 | self.assertTrue(m.get_solver().get_solution_count() > 0) 19 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_element.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetElement(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | setvars = [m.setvar(set([]), set(range(0, 5))) for i in range(0, 3)] 11 | s = m.setvar(set(range(0, 5))) 12 | i = m.intvar(0, 3) 13 | m.set_all_different(setvars).post() 14 | m.set_element(i, setvars, s).post() 15 | while m.get_solver().solve(): 16 | set_value = setvars[i.get_value()].get_value() 17 | self.assertSetEqual(set_value, s.get_value()) 18 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_member_set.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetMemberSet(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar([1, 2, 3]) 11 | b = m.setvar([2, 3, 4]) 12 | c = m.setvar([3, 4, 5]) 13 | d = m.setvar([6, 7, 8]) 14 | setvars = [a, b, c, d] 15 | setvar = m.setvar([], range(0, 1000)) 16 | m.set_member_set(setvars, setvar).post() 17 | while m.get_solver().solve(): 18 | self.assertTrue(setvar.get_value() in [s.get_value() for s in setvars]) 19 | -------------------------------------------------------------------------------- /tests/int_constraints/test_path.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestPath(unittest.TestCase): 7 | 8 | def testPath1(self): 9 | model = Model() 10 | x = model.intvars(10, 0, 20) 11 | model.path(x, model.intvar(0), model.intvar(1)).post() 12 | model.get_solver().solve() 13 | self.assertEqual(1, model.get_solver().get_solution_count()) 14 | 15 | def testPathFail(self): 16 | model = Model() 17 | x = model.intvars(10, 0, 9) 18 | model.path(x, model.intvar(0), model.intvar(1)).post() 19 | self.assertFalse(model.get_solver().solve()) 20 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_inverse_set.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetInverseSet(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | setvars = [m.setvar(set([]), set(range(0, 4))) for i in range(0, 3)] 11 | inv_setvars = [m.setvar(set([]), set(range(0, 4))) for i in range(0, 3)] 12 | m.set_inverse_set(setvars, inv_setvars).post() 13 | while m.get_solver().solve(): 14 | for y in range(0, len(setvars)): 15 | for x in setvars[y].get_value(): 16 | self.assertTrue(y in inv_setvars[x].get_value()) 17 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_node_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphNodesChanneling(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | b = m.boolvar() 15 | m.graph_node_channeling(g, b, 2).post() 16 | while m.get_solver().solve(): 17 | self.assertEqual(b.get_value(), g.get_value().contains_node(2)) 18 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_all_equal.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetAllEqual(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.setvar(set([]), set(range(0, 5))) 12 | c = m.setvar(set([]), set(range(0, 5))) 13 | m.set_all_equal([a, b, c]).post() 14 | while m.get_solver().solve(): 15 | aa = a.get_value() 16 | bb = b.get_value() 17 | cc = c.get_value() 18 | self.assertSetEqual(aa, bb) 19 | self.assertSetEqual(aa, cc) 20 | self.assertSetEqual(bb, cc) 21 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_all_different.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetAllDiff(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.setvar(set([]), set(range(0, 5))) 12 | c = m.setvar(set([]), set(range(0, 5))) 13 | m.set_all_different([a, b, c]).post() 14 | while m.get_solver().solve(): 15 | aa = a.get_value() 16 | bb = b.get_value() 17 | cc = c.get_value() 18 | self.assertNotEqual(aa, bb) 19 | self.assertNotEqual(aa, cc) 20 | self.assertNotEqual(bb, cc) 21 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_max_degree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphMaxDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | m.graph_max_degree(g, 2).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | self.assertTrue(len(val.get_neighbors_of(i)) <= 2) 19 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_min_degree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphMaxDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | m.graph_min_degree(g, 2).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | self.assertTrue(len(val.get_neighbors_of(i)) >= 2) 19 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_no_cycle.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import cycle_basis 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphCycle(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | m.graph_no_cycle(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | self.assertEqual(len(cycle_basis(val)), 0) 20 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_ints_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetIntsChanneling(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.setvar(set([]), set(range(5, 10))) 12 | c = m.setvar(set([]), set(range(0, 10))) 13 | setvars = [a, b, c] 14 | intvars = m.intvars(10, 0, 2) 15 | m.set_ints_channeling(setvars, intvars).post() 16 | while m.get_solver().solve(): 17 | for i in range(0, 10): 18 | v = intvars[i].get_value() 19 | self.assertTrue(i in setvars[v].get_value()) 20 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_union.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetUnion(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.setvar(set([]), set(range(5, 10))) 12 | c = m.setvar(set([]), set(range(10, 15))) 13 | d = m.setvar(set([]), set(range(0, 15))) 14 | m.set_union([a, b, c], d).post() 15 | while m.get_solver().solve(): 16 | aa = a.get_value() 17 | bb = b.get_value() 18 | cc = c.get_value() 19 | dd = d.get_value() 20 | self.assertSetEqual(aa.union(bb).union(cc), dd) 21 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_max_in_degree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMaxInDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | m.graph_max_in_degree(g, 2).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | self.assertTrue(len(val.get_predecessors_of(i)) <= 2) 19 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_max_out_degree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMaxOutDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | m.graph_max_out_degree(g, 2).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | self.assertTrue(len(val.get_successors_of(i)) <= 2) 19 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_min_in_degree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMinInDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | m.graph_min_in_degree(g, 2).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | self.assertTrue(len(val.get_predecessors_of(i)) >= 2) 19 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_min_out_degree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMinOutDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | m.graph_min_out_degree(g, 2).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | self.assertTrue(len(val.get_successors_of(i)) >= 2) 19 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_biconnected.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import is_biconnected 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphBiconnected(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | m.graph_biconnected(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | self.assertTrue(is_biconnected(val)) 20 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_all_disjoint.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetAllDisjoint(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.setvar(set([]), set(range(0, 5))) 12 | c = m.setvar(set([]), set(range(0, 5))) 13 | m.set_all_disjoint([a, b, c]).post() 14 | while m.get_solver().solve(): 15 | aa = a.get_value() 16 | bb = b.get_value() 17 | cc = c.get_value() 18 | self.assertTrue(aa.isdisjoint(bb)) 19 | self.assertTrue(aa.isdisjoint(cc)) 20 | self.assertTrue(bb.isdisjoint(cc)) 21 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_no_circuit.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import recursive_simple_cycles 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 7 | 8 | 9 | class TestGraphMinOutDegree(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_directed_graph(m, 4) 14 | ub = create_complete_directed_graph(m, 4) 15 | g = m.digraphvar(lb, ub, "g") 16 | m.graph_no_circuit(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | self.assertEqual(len(recursive_simple_cycles(val)), 0) 20 | -------------------------------------------------------------------------------- /tests/test_setvar.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetVar(unittest.TestCase): 7 | 8 | def test_create_setvar(self): 9 | model = Model("MyModel") 10 | lb = set([0, 1, 2]) 11 | ub = set([0, 1, 2, 3, 4]) 12 | s = model.setvar(lb, ub) 13 | llb = s.get_lb() 14 | uub = s.get_ub() 15 | self.assertEqual(lb, llb) 16 | self.assertEqual(ub, uub) 17 | ss = model.setvar(lb, name="ss") 18 | self.assertEqual(ss.name, "ss") 19 | self.assertEqual(ss.get_value(), lb) 20 | sol = model.get_solver().find_solution() 21 | val = sol.get_set_val(s) 22 | s1 = model.setvar({}, {1, 2, 3, 5, 12}, name="y") -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_tree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import is_tree 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphUnDirectedTree(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | m.graph_tree(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | if val.number_of_nodes() > 0: 20 | self.assertTrue(is_tree(val)) 21 | -------------------------------------------------------------------------------- /tests/int_constraints/test_not_all_equal.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestNotAllEqual(unittest.TestCase): 7 | 8 | def testNotAllEqual1(self): 9 | m = Model() 10 | variables = m.intvars(3, 0, 2) 11 | m.not_all_equal(variables).post() 12 | while m.get_solver().solve(): 13 | vals = set([v.get_value() for v in variables]) 14 | self.assertTrue(len(vals) > 1) 15 | 16 | def testNotAllEqualFail(self): 17 | m = Model() 18 | variables = [m.intvar(1, 1), m.intvar(1, 1), m.intvar(1)] 19 | m.not_all_equal(variables).post() 20 | solution = m.get_solver().find_solution() 21 | self.assertIsNone(solution) 22 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_intersection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetIntersection(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set([]), set(range(0, 5))) 11 | b = m.setvar(set([]), set(range(5, 10))) 12 | c = m.setvar(set([]), set(range(10, 15))) 13 | d = m.setvar(set([]), set(range(0, 15))) 14 | m.set_intersection([a, b, c], d).post() 15 | while m.get_solver().solve(): 16 | aa = a.get_value() 17 | bb = b.get_value() 18 | cc = c.get_value() 19 | dd = d.get_value() 20 | self.assertSetEqual(aa.intersection(bb).intersection(cc), dd) 21 | -------------------------------------------------------------------------------- /pychoco/_handle_wrapper.py: -------------------------------------------------------------------------------- 1 | from pychoco import backend 2 | 3 | 4 | class _HandleWrapper: 5 | """ 6 | A C Object handle wrapper (through SWIG). Keeps a handle to a backend object and 7 | cleans up on deletion. Inspired from https://github.com/d-michail/python-jgrapht/. 8 | """ 9 | 10 | def __init__(self, handle): 11 | self._handle_ = handle 12 | 13 | @property 14 | def _handle(self): 15 | return self._handle_ 16 | 17 | def __del__(self): 18 | if backend.chocosolver_is_initialized(): 19 | if self._handle_ is not None: 20 | backend.chocosolver_handles_destroy(self._handle_) 21 | 22 | def __repr__(self): 23 | return "_HandleWrapper(%r)" % self._handle_ 24 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_directed_tree.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import is_tree 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 7 | 8 | 9 | class TestGraphDirectedTree(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_directed_graph(m, 5) 14 | ub = create_complete_directed_graph(m, 5) 15 | g = m.digraphvar(lb, ub, "g") 16 | m.graph_directed_tree(g, 0).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | if val.number_of_nodes() > 0: 20 | self.assertTrue(is_tree(val)) 21 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_forest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import is_forest 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphDirectedForest(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 4) 14 | ub = create_complete_undirected_graph(m, 4) 15 | g = m.graphvar(lb, ub, "g") 16 | m.graph_forest(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | if val.number_of_nodes() > 0: 20 | self.assertTrue(is_forest(val)) 21 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_symmetric.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphSymmetric(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4, [], []) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | m.graph_symmetric(g).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | for j in val.get_successors_of(i): 19 | self.assertTrue(val.contains_edge(j, i)) 20 | -------------------------------------------------------------------------------- /tests/int_constraints/test_sub_circuit.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSubCircuit(unittest.TestCase): 7 | 8 | def testSubCircuit1(self): 9 | model = Model() 10 | x = model.intvars(10, 0, 20) 11 | model.sub_circuit(x, 0, model.intvar(0, len(x) - 1)).post() 12 | model.get_solver().solve(); 13 | self.assertEqual(1, model.get_solver().get_solution_count()) 14 | 15 | def testSubCircuit2(self): 16 | model = Model() 17 | x = model.intvars(5, 0, 8) 18 | model.sub_circuit(x, 0, model.intvar(0, len(x) - 1)).post() 19 | model.get_solver().find_all_solutions(); 20 | self.assertEqual(61, model.get_solver().get_solution_count()) 21 | -------------------------------------------------------------------------------- /docs/api/pychoco.constraints.rst: -------------------------------------------------------------------------------- 1 | pychoco.constraints package 2 | =========================== 3 | 4 | Submodules 5 | ---------- 6 | 7 | pychoco.constraints.constraint module 8 | ------------------------------------- 9 | 10 | .. automodule:: pychoco.constraints.constraint 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pychoco.constraints.int\_constraint\_factory module 16 | --------------------------------------------------- 17 | 18 | .. automodule:: pychoco.constraints.int_constraint_factory 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | Module contents 24 | --------------- 25 | 26 | .. automodule:: pychoco.constraints 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | -------------------------------------------------------------------------------- /tests/int_constraints/test_lex_chain_less.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestLexChainLess(unittest.TestCase): 7 | 8 | def testLexChainLess1(self): 9 | m = Model() 10 | ar1 = m.intvars(3, 0, 5) 11 | ar2 = m.intvars(3, -1, 4) 12 | c = m.lex_chain_less([ar1, ar2]) 13 | c.post() 14 | while m.get_solver().solve(): 15 | self.assertTrue(c.is_satisfied()) 16 | 17 | def testLexChainLess2(self): 18 | m = Model() 19 | ar1 = m.intvars(3, 0, 5) 20 | ar2 = m.intvars(3, -1, 4) 21 | c = m.lex_chain_less(ar1, ar2) 22 | c.post() 23 | while m.get_solver().solve(): 24 | self.assertTrue(c.is_satisfied()) 25 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_le.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetLe(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set(), set(range(0, 5))) 11 | b = m.setvar(set(), set(range(0, 5))) 12 | m.set_le(a, b).post() 13 | while m.get_solver().solve(): 14 | aa = [str(i) if i in a.get_value() else "" for i in range(0, 5)] 15 | bb = [str(i) if i in b.get_value() else "" for i in range(0, 5)] 16 | sa = "".join(aa) 17 | sb = "".join(bb) 18 | words = [sa, sb] 19 | words.sort() 20 | self.assertEqual(words[0], sa) 21 | self.assertEqual(words[1], sb) 22 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_directed_forest.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import is_forest 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 7 | 8 | 9 | class TestGraphDirectedForest(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_directed_graph(m, 4) 14 | ub = create_complete_directed_graph(m, 4) 15 | g = m.digraphvar(lb, ub, "g") 16 | m.graph_directed_forest(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | if val.number_of_nodes() > 0: 20 | self.assertTrue(is_forest(val)) 21 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_union_indices.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetUnion(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar([], range(0, 5)) 11 | b = m.setvar([], range(5, 10)) 12 | c = m.setvar([], range(5, 7)) 13 | setvars = [a, b, c] 14 | d = m.setvar([], range(0, 10)) 15 | indices = m.setvar([], range(0, 2)) 16 | m.set_union_indices(setvars, indices, d).post() 17 | while m.get_solver().solve(): 18 | value = set() 19 | for i in indices.get_value(): 20 | value = value.union(setvars[i].get_value()) 21 | self.assertSetEqual(value, d.get_value()) 22 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_anti_symmetric.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphAntiSymmetric(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4, [], []) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | m.graph_anti_symmetric(g).post() 15 | while m.get_solver().solve(): 16 | val = g.get_value() 17 | for i in val.get_nodes(): 18 | for j in val.get_successors_of(i): 19 | self.assertFalse(val.contains_edge(j, i)) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphDegrees(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | degrees = m.intvars(5, 0, 5) 15 | m.graph_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertEqual(len(val.get_neighbors_of(i)), degrees[i].get_value()) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_max_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphMaxDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | degrees = [1, 2, 3, 4, 5] 15 | m.graph_max_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertTrue(len(val.get_neighbors_of(i)) <= degrees[i]) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_max_in_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMaxInDegrees(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | degrees = [0, 1, 2, 3] 15 | m.graph_max_in_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertTrue(len(val.get_predecessors_of(i)) <= degrees[i]) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_max_out_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMaxOutDegrees(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | degrees = [0, 1, 2, 3] 15 | m.graph_max_out_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertTrue(len(val.get_successors_of(i)) <= degrees[i]) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_min_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphMaxDegree(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | degrees = [1, 2, 3, 4, 5] 15 | m.graph_min_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertTrue(len(val.get_neighbors_of(i)) >= degrees[i]) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_min_in_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMinInDegrees(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | degrees = [0, 1, 2, 3] 15 | m.graph_min_in_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertTrue(len(val.get_predecessors_of(i)) >= degrees[i]) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_min_out_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphMinInDegrees(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | degrees = [0, 1, 2, 3] 15 | m.graph_min_out_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertTrue(len(val.get_successors_of(i)) >= degrees[i]) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_in_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphInDegrees(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | degrees = m.intvars(5, 0, 5) 15 | m.graph_in_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertEqual(len(val.get_predecessors_of(i)), degrees[i].get_value()) 20 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_out_degrees.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphOutDegrees(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | g = m.digraphvar(lb, ub, "g") 14 | degrees = m.intvars(5, 0, 5) 15 | m.graph_out_degrees(g, degrees).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertEqual(len(val.get_successors_of(i)), degrees[i].get_value()) 20 | -------------------------------------------------------------------------------- /tests/int_constraints/test_inverse_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestInverseChanneling(unittest.TestCase): 7 | 8 | def testInverseChanneling1(self): 9 | m = Model() 10 | iv1 = m.intvars(5, 0, 5) 11 | iv2 = m.intvars(5, 0, 5) 12 | m.inverse_channeling(iv1, iv2).post() 13 | while m.get_solver().solve(): 14 | for i in range(0, 5): 15 | self.assertEqual(iv2[iv1[i].get_value()].get_value(), i) 16 | 17 | def testInverseChannelingFail(self): 18 | m = Model() 19 | iv1 = m.intvars(5, 0, 5) 20 | iv2 = m.intvars(5, 1, 6) 21 | m.inverse_channeling(iv1, iv2).post() 22 | self.assertFalse(m.get_solver().solve()) 23 | -------------------------------------------------------------------------------- /tests/int_constraints/test_lex_chain_less_eq.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestLexChainLessEq(unittest.TestCase): 7 | 8 | def testLexChainLessEq1(self): 9 | m = Model() 10 | ar1 = m.intvars(3, 0, 5) 11 | ar2 = m.intvars(3, -1, 4) 12 | c = m.lex_chain_less_eq([ar1, ar2]) 13 | c.post() 14 | while m.get_solver().solve(): 15 | self.assertTrue(c.is_satisfied()) 16 | 17 | def testLexChainLessEq2(self): 18 | m = Model() 19 | ar1 = m.intvars(3, 0, 5) 20 | ar2 = m.intvars(3, -1, 4) 21 | c = m.lex_chain_less_eq(ar1, ar2) 22 | c.post() 23 | while m.get_solver().solve(): 24 | self.assertTrue(c.is_satisfied()) 25 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_reachability.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx.algorithms.tournament import is_reachable 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 7 | 8 | 9 | class TestGraphReachability(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_directed_graph(m, 4) 14 | ub = create_complete_directed_graph(m, 4) 15 | g = m.digraphvar(lb, ub, "g") 16 | m.graph_reachability(g, 0).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | for i in g.get_value().get_nodes(): 20 | is_reachable(val, i, 0) 21 | -------------------------------------------------------------------------------- /tests/int_constraints/test_decreasing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestDecreasing(unittest.TestCase): 7 | 8 | def testDecreasing1(self): 9 | m = Model() 10 | intvars = m.intvars(10, 0, 10) 11 | m.decreasing(intvars).post() 12 | for i in range(0, 10): 13 | m.get_solver().solve() 14 | sol = [v.get_value() for v in intvars] 15 | for j in range(0, 9): 16 | self.assertGreaterEqual(sol[j], sol[j + 1]) 17 | 18 | def testDecreasing2(self): 19 | m = Model() 20 | intvars = m.intvars(10, 0, 0) 21 | m.increasing(intvars).post() 22 | sols = m.get_solver().find_all_solutions() 23 | self.assertEqual(len(sols), 1) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_increasing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestIncreasing(unittest.TestCase): 7 | 8 | def testIncreasing1(self): 9 | m = Model() 10 | intvars = m.intvars(10, 0, 10) 11 | m.increasing(intvars).post() 12 | for i in range(0, 10): 13 | m.get_solver().solve() 14 | sol = [v.get_value() for v in intvars] 15 | for j in range(0, 9): 16 | self.assertGreaterEqual(sol[j + 1], sol[j]) 17 | 18 | def testIncreasing2(self): 19 | m = Model() 20 | intvars = m.intvars(10, 0, 0) 21 | m.increasing(intvars).post() 22 | sols = m.get_solver().find_all_solutions() 23 | self.assertEqual(len(sols), 1) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_absolute.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAbsolute(unittest.TestCase): 7 | 8 | def testAbsolute1(self): 9 | m = Model() 10 | x = m.intvar(0, 20) 11 | y = m.intvar(-10, 10) 12 | m.absolute(x, y).post() 13 | solutions = m.get_solver().find_all_solutions() 14 | for s in solutions: 15 | self.assertEqual(s.get_int_val(x), abs(s.get_int_val(y))) 16 | self.assertEqual(len(solutions), 21) 17 | 18 | def testFail(self): 19 | m = Model() 20 | x = m.intvar(-10, -4) 21 | y = m.intvar(-10, 10) 22 | m.absolute(x, y).post() 23 | solution = m.get_solver().find_solution() 24 | self.assertIsNone(solution) 25 | -------------------------------------------------------------------------------- /tests/int_constraints/test_n_values.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestNValues(unittest.TestCase): 7 | 8 | def testNValues1(self): 9 | m = Model() 10 | variables = m.intvars(5, 0, 5) 11 | n_values = m.intvar(0, 2) 12 | m.n_values(variables, n_values).post() 13 | while m.get_solver().solve(): 14 | vals = set([v.get_value() for v in variables]) 15 | self.assertTrue(len(vals) == n_values.get_value()) 16 | 17 | def testNValuesFail(self): 18 | m = Model() 19 | variables = m.intvars(5, 0, 5) 20 | n_values = m.intvar(2) 21 | m.n_values(variables, n_values).post() 22 | m.all_equal(variables).post() 23 | self.assertFalse(m.get_solver().solve()) 24 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_lt.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetLt(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | a = m.setvar(set(), set(range(0, 5))) 11 | b = m.setvar(set(), set(range(0, 5))) 12 | m.set_lt(a, b).post() 13 | while m.get_solver().solve(): 14 | aa = [str(i) if i in a.get_value() else "" for i in range(0, 5)] 15 | bb = [str(i) if i in b.get_value() else "" for i in range(0, 5)] 16 | sa = "".join(aa) 17 | sb = "".join(bb) 18 | words = [sa, sb] 19 | words.sort() 20 | self.assertEqual(words[0], sa) 21 | self.assertEqual(words[1], sb) 22 | self.assertNotEqual(sa, sb) 23 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_diameter.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import diameter 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphDiameter(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | diam = m.intvar(0, 10) 17 | m.graph_diameter(g, diam).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | if val.number_of_nodes() > 0: 21 | self.assertEqual(diameter(val), diam.get_value()) 22 | -------------------------------------------------------------------------------- /tests/int_constraints/test_member.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestMember(unittest.TestCase): 7 | 8 | def testMember1(self): 9 | m = Model() 10 | x = m.intvar(-1000, 1000) 11 | m.member(x, [0, 1, 3, 5]).post() 12 | sols = m.get_solver().find_all_solutions() 13 | self.assertEqual(len(sols), 4) 14 | for s in sols: 15 | self.assertTrue(s.get_int_val(x) in [0, 1, 3, 5]) 16 | 17 | def testMember2(self): 18 | m = Model() 19 | x = m.intvar(-1000, 1000) 20 | m.member(x, lb=-10, ub=10).post() 21 | sols = m.get_solver().find_all_solutions() 22 | self.assertEqual(len(sols), 21) 23 | for s in sols: 24 | self.assertTrue(s.get_int_val(x) in range(-10, 11)) 25 | -------------------------------------------------------------------------------- /tests/int_constraints/test_argmax.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestArgmax(unittest.TestCase): 7 | 8 | def testArgmax1(self): 9 | m = Model() 10 | variables = m.intvars(5, 0, 10) 11 | m.all_different(variables).post() 12 | idx = m.intvar(1, 5) 13 | m.argmax(idx, 1, variables).post() 14 | while m.get_solver().solve(): 15 | vals = [v.get_value() for v in variables] 16 | self.assertEqual(variables[idx.get_value() - 1].get_value(), max(vals)) 17 | 18 | def testArgmaxFail(self): 19 | m = Model() 20 | variables = [m.intvar(10, 11), m.intvar(0, 1), m.intvar(2, 3)] 21 | idx = m.intvar(1, 8) 22 | m.argmax(idx, 0, variables).post() 23 | self.assertFalse(m.get_solver().solve()) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_argmin.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestArgmin(unittest.TestCase): 7 | 8 | def testArgmin1(self): 9 | m = Model() 10 | variables = m.intvars(5, 0, 10) 11 | m.all_different(variables).post() 12 | idx = m.intvar(1, 5) 13 | m.argmin(idx, 1, variables).post() 14 | while m.get_solver().solve(): 15 | vals = [v.get_value() for v in variables] 16 | self.assertEqual(variables[idx.get_value() - 1].get_value(), min(vals)) 17 | 18 | def testArgminFail(self): 19 | m = Model() 20 | variables = [m.intvar(0, 1), m.intvar(2, 4), m.intvar(2, 3)] 21 | idx = m.intvar(1, 8) 22 | m.argmin(idx, 0, variables).post() 23 | self.assertFalse(m.get_solver().solve()) 24 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_nb_loops.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphNbCliques(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | nb_loops = m.intvar(0, 10) 15 | m.graph_nb_loops(g, nb_loops).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | nb = 0 19 | for i in val.get_nodes(): 20 | if val.contains_edge(i, i): 21 | nb += 1 22 | self.assertEqual(nb, nb_loops.get_value()) 23 | -------------------------------------------------------------------------------- /tests/int_constraints/test_at_least_n_values.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAllDifferent(unittest.TestCase): 7 | 8 | def testAtLeastNValues1(self): 9 | m = Model() 10 | variables = m.intvars(5, 0, 5) 11 | n_values = m.intvar(0, 2) 12 | m.at_least_n_values(variables, n_values).post() 13 | while m.get_solver().solve(): 14 | vals = set([v.get_value() for v in variables]) 15 | self.assertTrue(len(vals) >= n_values.get_value()) 16 | 17 | def testAtLeastNValuesFail(self): 18 | m = Model() 19 | variables = m.intvars(5, 0, 5) 20 | n_values = m.intvar(2) 21 | m.at_least_n_values(variables, n_values).post() 22 | m.all_equal(variables).post() 23 | self.assertFalse(m.get_solver().solve()) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_at_most_n_values.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAtMostNValues(unittest.TestCase): 7 | 8 | def testAtMostNValues1(self): 9 | m = Model() 10 | variables = m.intvars(5, 0, 5) 11 | n_values = m.intvar(0, 2) 12 | m.at_most_n_values(variables, n_values).post() 13 | while m.get_solver().solve(): 14 | vals = set([v.get_value() for v in variables]) 15 | self.assertTrue(len(vals) <= n_values.get_value()) 16 | 17 | def testAtMostNValuesFail(self): 18 | m = Model() 19 | variables = m.intvars(5, 0, 5) 20 | n_values = m.intvar(2) 21 | m.at_most_n_values(variables, n_values).post() 22 | m.all_different(variables).post() 23 | self.assertFalse(m.get_solver().solve()) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_not_member.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestNotMember(unittest.TestCase): 7 | 8 | def testMember1(self): 9 | m = Model() 10 | x = m.intvar(-1000, 1000) 11 | m.not_member(x, [0, 1, 3, 5]).post() 12 | sols = m.get_solver().find_all_solutions() 13 | self.assertEqual(len(sols), 2001 - 4) 14 | for s in sols: 15 | self.assertFalse(s.get_int_val(x) in [0, 1, 3, 5]) 16 | 17 | def testMember2(self): 18 | m = Model() 19 | x = m.intvar(-1000, 1000) 20 | m.not_member(x, lb=-10, ub=10).post() 21 | sols = m.get_solver().find_all_solutions() 22 | self.assertEqual(len(sols), 2001 - 21) 23 | for s in sols: 24 | self.assertFalse(s.get_int_val(x) in range(-10, 11)) 25 | -------------------------------------------------------------------------------- /tests/set_constraints/test_set_partition.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSetPartition(unittest.TestCase): 7 | 8 | def test1(self): 9 | m = Model() 10 | universe = m.setvar([], range(0, 10)) 11 | a = m.setvar([], [1, 2, 3, 4]) 12 | b = m.setvar([], [0, 1, 2, 5, 6, 7, 8]) 13 | c = m.setvar([], [5, 6, 7, 8, 9]) 14 | m.set_partition([a, b, c], universe).post() 15 | while m.get_solver().solve(): 16 | aa = a.get_value() 17 | bb = b.get_value() 18 | cc = c.get_value() 19 | self.assertTrue(aa.isdisjoint(bb)) 20 | self.assertTrue(aa.isdisjoint(cc)) 21 | self.assertTrue(bb.isdisjoint(cc)) 22 | union = aa.union(bb).union(cc) 23 | self.assertSetEqual(union, universe.get_value()) 24 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_connected.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import is_connected 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphConnected(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | m.graph_connected(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | # The connected constraint of Choco allows empty graphs, 20 | # whereas networkx does not. 21 | if val.number_of_nodes() > 0: 22 | self.assertTrue(is_connected(val)) 23 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_loop_set.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphDirectedForest(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | g = m.graphvar(lb, ub, "g") 14 | s = m.setvar([], [0, 1, 2, 3, 4]) 15 | m.graph_loop_set(g, s).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | if val.contains_edge(i, i): 20 | self.assertTrue(i in s.get_value()) 21 | else: 22 | self.assertFalse(i in s.get_value()) 23 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_size_max_connected_components.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import connected_components 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphNbCliques(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | min_cc = m.intvar(1, 10) 17 | m.graph_size_min_connected_components(g, min_cc).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | ccs = [len(cc) for cc in connected_components(val)] 21 | self.assertEqual(min(ccs), min_cc.get_value()) 22 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_size_min_connected_components.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import connected_components 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphNbCliques(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | max_cc = m.intvar(1, 10) 17 | m.graph_size_max_connected_components(g, max_cc).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | ccs = [len(cc) for cc in connected_components(val)] 21 | self.assertEqual(max(ccs), max_cc.get_value()) 22 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_strongly_connected.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx.algorithms.tournament import is_strongly_connected 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 7 | 8 | 9 | class TestGraphNbStronglyConnectedComponents(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_directed_graph(m, 3) 14 | ub = create_complete_directed_graph(m, 3) # TODO: there may be a bug in Choco strongly connected constraint 15 | g = m.digraphvar(lb, ub, "g") 16 | m.graph_strongly_connected(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | if val.number_of_nodes() > 0: 20 | self.assertTrue(is_strongly_connected(val)) 21 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_nb_strongly_connected_components.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import number_strongly_connected_components 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 7 | 8 | 9 | class TestGraphNbStronglyConnectedComponents(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_directed_graph(m, 4) 14 | ub = create_complete_directed_graph(m, 4) 15 | g = m.digraphvar(lb, ub, "g") 16 | nb = m.intvar(0, 3) 17 | m.graph_nb_strongly_connected_components(g, nb).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | if val.number_of_nodes() > 0: 21 | self.assertEqual(number_strongly_connected_components(val), nb.get_value()) 22 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_cycle.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import cycle_basis 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphCycle(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | m.graph_cycle(g).post() 17 | while m.get_solver().solve(): 18 | val = g.get_value().to_networkx_graph() 19 | # The cycle constraint of Choco allows empty graphs, 20 | # whereas networkx does not return cycle for empty graphs. 21 | if val.number_of_nodes() > 0: 22 | self.assertEqual(len(cycle_basis(val)[0]), val.number_of_nodes()) 23 | -------------------------------------------------------------------------------- /tests/int_constraints/test_bin_packing.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestBinPacking(unittest.TestCase): 7 | 8 | def testBinPacking1(self): 9 | model = Model(); 10 | item_size = [2, 3, 1]; 11 | item_bin = model.intvars(3, -1, 1); 12 | bin_load = model.intvars(2, 3, 3); 13 | model.bin_packing(item_bin, item_size, bin_load).post() 14 | self.assertTrue(model.get_solver().solve()) 15 | 16 | def testBinPacking2(self): 17 | model = Model() 18 | item_size = [2, 2, 2] 19 | item_bin = model.intvars(3, 0, 2) 20 | bin_load = model.intvars(3, 0, 5) 21 | model.arithm(item_bin[0], "!=", 0).post() 22 | model.bin_packing(item_bin, item_size, bin_load, 0).post() 23 | model.get_solver().find_all_solutions() 24 | self.assertEqual(model.get_solver().get_solution_count(), 16) 25 | -------------------------------------------------------------------------------- /tests/int_constraints/test_knapsack.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestKnapsack(unittest.TestCase): 7 | 8 | def testKnapsack1(self): 9 | for seed in range(0, 200): 10 | m = Model() 11 | occs = m.boolvars(35) 12 | es = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 8, 8, 8, 8, 4, 4, 4, 4, 13 | 4, 7, 7, 7, 3, 3, 3, 3, 3, 3, 3, 3] 14 | ws = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 4, 4, 4, 4, 3, 3, 3, 3, 15 | 3, 5, 5, 5, 2, 2, 2, 2, 2, 2, 2, 2] 16 | capa = m.intvar(0, 15) 17 | power = m.intvar(0, 999); 18 | m.knapsack(occs, capa, power, ws, es).post() 19 | m.get_solver().set_random_search(*occs, seed=seed) 20 | s = m.get_solver().find_optimal_solution(power, True) 21 | self.assertEqual(s.get_int_val(power), 28) 22 | -------------------------------------------------------------------------------- /pychoco/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pychoco - Python API for the Choco Constraint Programming solver 3 | """ 4 | 5 | # Implementation inspired by https://github.com/d-michail/python-jgrapht 6 | 7 | import atexit 8 | 9 | from . import backend 10 | 11 | backend.chocosolver_init() 12 | del backend 13 | 14 | 15 | def _module_cleanup_function(): 16 | from . import backend 17 | backend.chocosolver_init() 18 | 19 | 20 | atexit.register(_module_cleanup_function) 21 | del atexit 22 | 23 | from .model import Model 24 | from .objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 25 | from .objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 26 | from .objects.automaton.finite_automaton import FiniteAutomaton 27 | from .objects.automaton.cost_automaton import CostAutomaton 28 | from .objects.graphs.multivalued_decision_diagram import MultivaluedDecisionDiagram 29 | -------------------------------------------------------------------------------- /tests/int_constraints/test_circuit.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestCircuit(unittest.TestCase): 7 | 8 | def testCircuit1(self): 9 | m = Model() 10 | intvars = m.intvars(7, 0, 6) 11 | m.circuit(intvars).post() 12 | all_diff = m.all_different(intvars) 13 | while m.get_solver().solve(): 14 | self.assertTrue(all_diff.is_satisfied()) 15 | i = intvars[0].get_value() 16 | n = 1 17 | while i != 0 and n < len(intvars): 18 | i = intvars[i].get_value() 19 | n += 1 20 | self.assertTrue(i == 0 and n == len(intvars)) 21 | 22 | def testCircuitFail(self): 23 | m = Model() 24 | intvars = m.intvars(7, 0, 6) 25 | m.circuit(intvars).post() 26 | m.at_most_n_values(intvars, m.intvar(3)).post() 27 | self.assertFalse(m.get_solver().solve()) 28 | -------------------------------------------------------------------------------- /tests/int_constraints/test_among.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAmong(unittest.TestCase): 7 | 8 | def testAmong1(self): 9 | m = Model() 10 | variables = m.intvars(4, 0, 10) 11 | values = [1, 2, 3] 12 | nb_var = m.intvar(2, 3) 13 | m.among(nb_var, variables, values).post() 14 | while m.get_solver().solve(): 15 | nb_in = 0 16 | for v in variables: 17 | if v.get_value() in values: 18 | nb_in += 1 19 | self.assertTrue(2 <= nb_in <= 3) 20 | self.assertEqual(nb_in, nb_var.get_value()) 21 | 22 | def testAmongFail(self): 23 | m = Model() 24 | variables = m.intvars(4, 0, 10) 25 | values = [11, 12, 13] 26 | nb_var = m.intvar(2, 3) 27 | m.among(nb_var, variables, values).post() 28 | self.assertFalse(m.get_solver().solve()) 29 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_nb_edges.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import number_of_edges 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphNbCliques(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | i = m.intvar(0, 10) 17 | m.graph_nb_edges(g, i).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | # The cycle constraint of Choco allows empty graphs, 21 | # whereas networkx does not return cycle for empty graphs. 22 | if val.number_of_nodes() > 0: 23 | self.assertEqual(number_of_edges(val), i.get_value()) 24 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_nb_nodes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import number_of_nodes 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphNbCliques(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | i = m.intvar(0, 10) 17 | m.graph_nb_nodes(g, i).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | # The cycle constraint of Choco allows empty graphs, 21 | # whereas networkx does not return cycle for empty graphs. 22 | if val.number_of_nodes() > 0: 23 | self.assertEqual(number_of_nodes(val), i.get_value()) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_global_cardinality.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestGlobalCardinality(unittest.TestCase): 7 | 8 | def testGlobalCardinality1(self): 9 | m = Model() 10 | intvars = m.intvars(5, 0, 5) 11 | values = [1, 2] 12 | occurrences = m.intvars(2, 2) 13 | gcc = m.global_cardinality(intvars, values, occurrences, False) 14 | gcc.post() 15 | while m.get_solver().solve(): 16 | self.assertTrue(gcc.is_satisfied()) 17 | self.assertTrue(m.get_solver().get_solution_count() > 0) 18 | 19 | def testGlobalCardinalityFail(self): 20 | m = Model() 21 | intvars = m.intvars(5, 3, 5) 22 | values = [1, 2] 23 | occurrences = m.intvars(2, 2) 24 | gcc = m.global_cardinality(intvars, values, occurrences, False) 25 | gcc.post() 26 | self.assertFalse(m.get_solver().solve()) 27 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_nb_cliques.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import networkx as nx 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphNbCliques(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 7) 14 | ub = create_complete_undirected_graph(m, 7) 15 | g = m.graphvar(lb, ub, "g") 16 | i = m.intvar(0, 10) 17 | m.graph_nb_cliques(g, i).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | # The cycle constraint of Choco allows empty graphs, 21 | # whereas networkx does not return cycle for empty graphs. 22 | if val.number_of_nodes() > 0: 23 | self.assertEqual(sum(1 for _ in nx.find_cliques(val)), i.get_value()) 24 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /tests/int_constraints/test_mddc.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.multivalued_decision_diagram import MultivaluedDecisionDiagram 5 | 6 | 7 | class TestMddc(unittest.TestCase): 8 | 9 | def testMddc1(self): 10 | m = Model() 11 | intvars = m.intvars(3, 0, 1) 12 | tuples = [[0, 0, 0], [1, 1, 1]] 13 | m.mddc(intvars, MultivaluedDecisionDiagram(intvars, tuples)).post() 14 | while m.get_solver().solve(): 15 | pass 16 | self.assertEqual(m.get_solver().get_solution_count(), 2) 17 | 18 | def testMddc2(self): 19 | model = Model() 20 | intvars = model.intvars(3, 0, 2) 21 | tuples = [[0, 1, 2], [2, 1, 0]] 22 | model.mddc(intvars, MultivaluedDecisionDiagram(intvars, tuples)).post(); 23 | while model.get_solver().solve(): 24 | pass 25 | self.assertEqual(model.get_solver().get_solution_count(), 2) 26 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_size_connected_components.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import connected_components 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphSizeCC(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | min_cc = m.intvar(1, 10) 17 | max_cc = m.intvar(1, 10) 18 | m.graph_size_connected_components(g, min_cc, max_cc).post() 19 | while m.get_solver().solve(): 20 | val = g.get_value().to_networkx_graph() 21 | ccs = [len(cc) for cc in connected_components(val)] 22 | self.assertEqual(min(ccs), min_cc.get_value()) 23 | self.assertEqual(max(ccs), max_cc.get_value()) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_all_equal.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAllEqual(unittest.TestCase): 7 | 8 | def testAllEqual1(self): 9 | m = Model() 10 | variables = m.intvars(3, 0, 2) 11 | m.all_equal(variables).post() 12 | solutions = m.get_solver().find_all_solutions() 13 | self.assertEqual(len(solutions), 3) 14 | for s in solutions: 15 | self.assertEqual(s.get_int_val(variables[0]), s.get_int_val(variables[1])) 16 | self.assertEqual(s.get_int_val(variables[0]), s.get_int_val(variables[2])) 17 | self.assertEqual(s.get_int_val(variables[1]), s.get_int_val(variables[2])) 18 | 19 | def testAllEqualFail(self): 20 | m = Model() 21 | variables = [m.intvar(0, 1), m.intvar(1, 2), m.intvar(3, 4)] 22 | m.all_equal(variables).post() 23 | solution = m.get_solver().find_solution() 24 | self.assertIsNone(solution) 25 | -------------------------------------------------------------------------------- /tests/reification/test_reify.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestReify(unittest.TestCase): 7 | 8 | def testReify1(self): 9 | m = Model() 10 | a = m.intvar(0, 10) 11 | b = m.intvar(0, 10) 12 | 13 | cons = m.arithm(a, "+", b, '=', 10) 14 | bv = cons.reify() 15 | 16 | solutions = m.get_solver().find_all_solutions() 17 | for s in solutions: 18 | self.assertEqual(s.get_int_val(a) + s.get_int_val(b) == 10, s.get_int_val(bv)) 19 | 20 | def testReifyWith1(self): 21 | m = Model() 22 | a = m.intvar(0, 10) 23 | b = m.intvar(0, 10) 24 | bv = m.boolvar() 25 | 26 | cons = m.arithm(a, "+", b, '=', 10) 27 | cons.reify_with(bv) 28 | 29 | solutions = m.get_solver().find_all_solutions() 30 | for s in solutions: 31 | self.assertEqual(s.get_int_val(a) + s.get_int_val(b) == 10, s.get_int_val(bv)) 32 | 33 | -------------------------------------------------------------------------------- /tests/int_constraints/test_all_different.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAllDifferent(unittest.TestCase): 7 | 8 | def testAllDifferent1(self): 9 | m = Model() 10 | variables = m.intvars(3, 0, 2) 11 | m.all_different(variables).post() 12 | solutions = m.get_solver().find_all_solutions() 13 | self.assertEqual(len(solutions), 6) 14 | for s in solutions: 15 | self.assertNotEqual(s.get_int_val(variables[0]), s.get_int_val(variables[1])) 16 | self.assertNotEqual(s.get_int_val(variables[0]), s.get_int_val(variables[2])) 17 | self.assertNotEqual(s.get_int_val(variables[1]), s.get_int_val(variables[2])) 18 | 19 | def testAllDifferentFail(self): 20 | m = Model() 21 | variables = m.intvars(3, 0, 1) 22 | m.all_different(variables).post() 23 | solution = m.get_solver().find_solution() 24 | self.assertIsNone(solution) 25 | -------------------------------------------------------------------------------- /tests/int_constraints/test_int_value_precede_chain.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestIntValuePrecedeChain(unittest.TestCase): 7 | 8 | def testIntValuePrecedeChain1(self): 9 | for i in range(0, 10): 10 | model = Model() 11 | intvars = model.intvars(5, 0, 5) 12 | model.int_value_precede_chain(intvars, [1, 2]).post() 13 | model.get_solver().set_random_search(*intvars) 14 | while model.get_solver().solve(): 15 | pass 16 | s1 = model.get_solver().get_solution_count() 17 | model = Model() 18 | intvars = model.intvars(5, 0, 5) 19 | model.int_value_precede_chain(intvars, [1, 2]).post() 20 | model.get_solver().set_random_search(*intvars) 21 | while model.get_solver().solve(): 22 | pass 23 | s2 = model.get_solver().get_solution_count() 24 | self.assertEqual(s1, s2) 25 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_nb_connected_components.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from networkx import number_connected_components 4 | 5 | from pychoco.model import Model 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 7 | 8 | 9 | class TestGraphNbCliques(unittest.TestCase): 10 | 11 | def test1(self): 12 | m = Model() 13 | lb = create_undirected_graph(m, 5) 14 | ub = create_complete_undirected_graph(m, 5) 15 | g = m.graphvar(lb, ub, "g") 16 | i = m.intvar(0, 10) 17 | m.graph_nb_connected_components(g, i).post() 18 | while m.get_solver().solve(): 19 | val = g.get_value().to_networkx_graph() 20 | # The cycle constraint of Choco allows empty graphs, 21 | # whereas networkx does not return cycle for empty graphs. 22 | if val.number_of_nodes() > 0: 23 | self.assertEqual(number_connected_components(val), i.get_value()) 24 | -------------------------------------------------------------------------------- /tests/int_constraints/test_bools_int_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestBoolsIntChanneling(unittest.TestCase): 7 | 8 | def testBoolsIntChanneling1(self): 9 | m = Model() 10 | bools = m.boolvars(10) 11 | intvar = m.intvar(0, 9) 12 | m.bools_int_channeling(bools, intvar).post() 13 | while m.get_solver().solve(): 14 | for b in range(0, 10): 15 | if b == intvar.get_value(): 16 | self.assertTrue(bools[b].get_value()) 17 | else: 18 | self.assertFalse(bools[b].get_value()) 19 | self.assertTrue(m.get_solver().get_solution_count(), 10) 20 | 21 | def testBoolsIntChannelingFail(self): 22 | m = Model() 23 | bools = m.boolvars(10) 24 | intvar = m.intvar(10, 11) 25 | m.or_(bools).post() 26 | m.bools_int_channeling(bools, intvar).post() 27 | self.assertFalse(m.get_solver().solve()) 28 | -------------------------------------------------------------------------------- /tests/int_constraints/test_pow.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestPow(unittest.TestCase): 7 | 8 | def testPow1(self): 9 | m = Model() 10 | dividend = m.intvar(2, 3, "dividend") 11 | divisor = 1 12 | remainder = m.intvar(1, 2, "remainder") 13 | m.pow(dividend, divisor, remainder).post() 14 | s = m.get_solver() 15 | s.set_input_order_lb_search(dividend, remainder) 16 | s.solve() 17 | 18 | def testPow2(self): 19 | model = Model("model"); 20 | a = model.intvar(2, 6); 21 | b = 2; 22 | c = model.intvar(5, 30); 23 | model.pow(a, b, c).post() 24 | self.assertEqual(len(model.get_solver().find_all_solutions()), 3) 25 | 26 | def testPow3(self): 27 | model = Model("model") 28 | x = model.intvar(-5, 5) 29 | z = model.intvar(-5, 5) 30 | model.pow(x, 3, z).post() 31 | self.assertEqual(len(model.get_solver().find_all_solutions()), 3) 32 | -------------------------------------------------------------------------------- /docs/api/pychoco.rst: -------------------------------------------------------------------------------- 1 | pychoco package 2 | =============== 3 | 4 | Subpackages 5 | ----------- 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | pychoco.constraints 11 | pychoco.search 12 | pychoco.variables 13 | 14 | Submodules 15 | ---------- 16 | 17 | 18 | pychoco.model module 19 | -------------------- 20 | 21 | .. automodule:: pychoco.model 22 | :members: 23 | :undoc-members: 24 | :show-inheritance: 25 | 26 | pychoco.solution module 27 | ----------------------- 28 | 29 | .. automodule:: pychoco.solution 30 | :members: 31 | :undoc-members: 32 | :show-inheritance: 33 | 34 | pychoco.solver module 35 | --------------------- 36 | 37 | .. automodule:: pychoco.solver 38 | :members: 39 | :undoc-members: 40 | :show-inheritance: 41 | 42 | pychoco.utils module 43 | -------------------- 44 | 45 | .. automodule:: pychoco.utils 46 | :members: 47 | :undoc-members: 48 | :show-inheritance: 49 | 50 | Module contents 51 | --------------- 52 | 53 | .. automodule:: pychoco 54 | :members: 55 | :undoc-members: 56 | :show-inheritance: 57 | -------------------------------------------------------------------------------- /docs/api/pychoco.variables.rst: -------------------------------------------------------------------------------- 1 | pychoco.variables package 2 | ========================= 3 | 4 | Submodules 5 | ---------- 6 | 7 | pychoco.variables.boolvar module 8 | -------------------------------- 9 | 10 | .. automodule:: pychoco.variables.boolvar 11 | :members: 12 | :undoc-members: 13 | :show-inheritance: 14 | 15 | pychoco.variables.intvar module 16 | ------------------------------- 17 | 18 | .. automodule:: pychoco.variables.intvar 19 | :members: 20 | :undoc-members: 21 | :show-inheritance: 22 | 23 | pychoco.variables.variable module 24 | --------------------------------- 25 | 26 | .. automodule:: pychoco.variables.variable 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | pychoco.variables.variable\_factory module 32 | ------------------------------------------ 33 | 34 | .. automodule:: pychoco.variables.variable_factory 35 | :members: 36 | :undoc-members: 37 | :show-inheritance: 38 | 39 | Module contents 40 | --------------- 41 | 42 | .. automodule:: pychoco.variables 43 | :members: 44 | :undoc-members: 45 | :show-inheritance: 46 | -------------------------------------------------------------------------------- /tests/int_constraints/test_times.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestTimes(unittest.TestCase): 7 | 8 | def testTimes1(self): 9 | m = Model() 10 | x = m.intvar(-10, 10) 11 | y = m.intvar(4, 8) 12 | z = m.intvar(-3, 12) 13 | m.times(x, y, z).post() 14 | sols = m.get_solver().find_all_solutions() 15 | for s in sols: 16 | self.assertEqual(s.get_int_val(x) * s.get_int_val(y), s.get_int_val(z)) 17 | 18 | def testTimes2(self): 19 | m = Model() 20 | x = m.intvar(-10, 10) 21 | y = m.intvar(4, 8) 22 | m.times(x, y, -6).post() 23 | sols = m.get_solver().find_all_solutions() 24 | for s in sols: 25 | self.assertEqual(s.get_int_val(x) * s.get_int_val(y), -6) 26 | 27 | def testTimes3(self): 28 | m = Model() 29 | x = m.intvar(-10, 10) 30 | z = m.intvar(-3, 12) 31 | m.times(x, 3, z).post() 32 | sols = m.get_solver().find_all_solutions() 33 | for s in sols: 34 | self.assertEqual(s.get_int_val(x) * 3, s.get_int_val(z)) 35 | -------------------------------------------------------------------------------- /pychoco/objects/graphs/multivalued_decision_diagram.py: -------------------------------------------------------------------------------- 1 | from typing import List 2 | 3 | from pychoco import backend 4 | from pychoco._handle_wrapper import _HandleWrapper 5 | from pychoco._utils import make_intvar_array, make_int_2d_array 6 | 7 | 8 | class MultivaluedDecisionDiagram(_HandleWrapper): 9 | """ 10 | Multi-valued Decision Diagram (MDD) 11 | """ 12 | 13 | def __init__(self, intvars: List["IntVar"], tuples: List[List[int]], compact: str = "NEVER", sort_tuple=False): 14 | """ 15 | Create a MDD 16 | 17 | :param intvars: A list of IntVars. 18 | :param tuples: A List[List[int]] either tuples (allowed). 19 | :param compact: Either "NEVER", "ONCE", or "EACH". 20 | :param sort_tuple: A bool. 21 | :return: A MDD. 22 | """ 23 | assert len(tuples) > 0 24 | for r in tuples: 25 | assert len(r) == len(intvars) 26 | handle = backend.create_mdd_tuples( 27 | make_intvar_array(intvars), 28 | make_int_2d_array(tuples), 29 | compact, 30 | sort_tuple 31 | ) 32 | super().__init__(handle) 33 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_edge_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 6 | 7 | 8 | class TestGraphEdgeChanneling(unittest.TestCase): 9 | 10 | def test1(self): 11 | m = Model() 12 | lb = create_undirected_graph(m, 5) 13 | ub = create_complete_undirected_graph(m, 5) 14 | g = m.graphvar(lb, ub, "g") 15 | b = m.boolvar() 16 | m.graph_edge_channeling(g, b, 0, 2).post() 17 | while m.get_solver().solve(): 18 | self.assertEqual(b.get_value(), g.get_value().contains_edge(0, 2)) 19 | 20 | def test2(self): 21 | m = Model() 22 | lb = create_directed_graph(m, 4) 23 | ub = create_complete_directed_graph(m, 4) 24 | g = m.digraphvar(lb, ub, "g") 25 | b = m.boolvar() 26 | m.graph_edge_channeling(g, b, 0, 2).post() 27 | while m.get_solver().solve(): 28 | self.assertEqual(b.get_value(), g.get_value().contains_edge(0, 2)) 29 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_node_successors_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphNodeSuccessorsChanneling(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | s = m.setvar([], [0, 1, 2, 3]) 14 | g = m.digraphvar(lb, ub, "g") 15 | m.graph_node_successors_channeling(g, s, 2).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | self.assertSetEqual(set(val.get_successors_of(2)), s.get_value()) 19 | 20 | def test2(self): 21 | m = Model() 22 | lb = create_directed_graph(m, 4) 23 | ub = create_complete_directed_graph(m, 4) 24 | bools = m.boolvars(4) 25 | g = m.digraphvar(lb, ub, "g") 26 | m.graph_node_successors_channeling(g, bools, 3).post() 27 | while m.get_solver().solve(): 28 | val = g.get_value() 29 | for j in val.get_successors_of(3): 30 | self.assertTrue(bools[j].get_value()) 31 | -------------------------------------------------------------------------------- /tests/test_parallel_portfolio.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from pychoco.model import Model 5 | from pychoco.parallel_portfolio import ParallelPortfolio 6 | 7 | 8 | class TestParallelPortfolio(unittest.TestCase): 9 | 10 | def test_simple_solve(self): 11 | pf = ParallelPortfolio() 12 | for i in range(0, 5): 13 | m = Model() 14 | vars = m.intvars(10, 0, 20) 15 | nv = m.intvar(3, 6) 16 | m.n_values(vars, nv).post() 17 | s = m.intvar(0, 100) 18 | m.sum(vars, "=", s).post() 19 | pf.add_model(m) 20 | self.assertTrue(pf.solve()) 21 | 22 | def test_optimize(self): 23 | pf = ParallelPortfolio() 24 | pf.steal_nogoods_on_restarts() 25 | for i in range(0, 5): 26 | m = Model() 27 | vars = m.intvars(10, 0, 100) 28 | nv = m.intvar(3, 4) 29 | m.n_values(vars, nv).post() 30 | s = m.intvar(0, 1000) 31 | m.sum(vars, "=", s).post() 32 | m.set_objective(s, True) 33 | pf.add_model(m) 34 | sol = pf.find_best_solution() 35 | best_val = sol.get_int_val(s) 36 | self.assertEqual(best_val, 997) 37 | 38 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_node_neighbors_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphNeighborsChanneling(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | s = m.setvar([], [0, 1, 2, 3, 4]) 14 | g = m.graphvar(lb, ub, "g") 15 | m.graph_node_neighbors_channeling(g, s, 2).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | self.assertSetEqual(set(val.get_neighbors_of(2)), s.get_value()) 19 | 20 | def test2(self): 21 | m = Model() 22 | lb = create_undirected_graph(m, 5) 23 | ub = create_complete_undirected_graph(m, 5) 24 | bools = m.boolvars(5) 25 | g = m.graphvar(lb, ub, "g") 26 | m.graph_node_neighbors_channeling(g, bools, 3).post() 27 | while m.get_solver().solve(): 28 | val = g.get_value() 29 | for j in val.get_neighbors_of(3): 30 | self.assertTrue(bools[j].get_value()) 31 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_node_predecessors_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphNodePredecessorsChanneling(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | s = m.setvar([], [0, 1, 2, 3]) 14 | g = m.digraphvar(lb, ub, "g") 15 | m.graph_node_predecessors_channeling(g, s, 2).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | self.assertSetEqual(set(val.get_predecessors_of(2)), s.get_value()) 19 | 20 | def test2(self): 21 | m = Model() 22 | lb = create_directed_graph(m, 4) 23 | ub = create_complete_directed_graph(m, 4) 24 | bools = m.boolvars(4) 25 | g = m.digraphvar(lb, ub, "g") 26 | m.graph_node_predecessors_channeling(g, bools, 3).post() 27 | while m.get_solver().solve(): 28 | val = g.get_value() 29 | for j in val.get_predecessors_of(3): 30 | self.assertTrue(bools[j].get_value()) 31 | -------------------------------------------------------------------------------- /tests/int_constraints/test_count.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestCount(unittest.TestCase): 7 | 8 | def testCount1(self): 9 | m = Model() 10 | intvars = m.intvars(5, 0, 5) 11 | count = m.intvar(0, 3) 12 | m.count(1, intvars, count).post() 13 | while m.get_solver().solve(): 14 | s = sum([1 for v in intvars if v.get_value() == 1]) 15 | self.assertEqual(s, count.get_value()) 16 | self.assertTrue(0 <= s <= 3) 17 | 18 | def testCount2(self): 19 | m = Model() 20 | intvars = m.intvars(5, 0, 5) 21 | count = m.intvar(0, 3) 22 | value = m.intvar(0, 5) 23 | m.count(value, intvars, count).post() 24 | while m.get_solver().solve(): 25 | s = sum([1 for v in intvars if v.get_value() == value.get_value()]) 26 | self.assertEqual(s, count.get_value()) 27 | self.assertTrue(0 <= s <= 3) 28 | 29 | def testCountFail(self): 30 | m = Model() 31 | intvars = [m.intvar(0, 5), m.intvar(1), m.intvar(1), m.intvar(1, 3)] 32 | count = m.intvar(2, 3) 33 | m.count(4, intvars, count).post() 34 | self.assertFalse(m.get_solver().solve()) 35 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Pychoco 2 | ======= 3 | 4 | Python bindings for the Choco Constraint programming solver (https://choco-solver.org/). 5 | 6 | Choco-solver is an open-source Java library for Constraint Programming (see https://choco-solver.org/). 7 | It comes with many features such as various types of variables, various state-of-the-art constraint, 8 | various search strategies, etc. 9 | 10 | The PyChoco library uses a *native-build* of the original Java Choco-solver library, in the form 11 | of a shared library, which means that it can be used without any JVM. This native-build is created 12 | with GraalVM (https://www.graalvm.org/) native-image tool. 13 | 14 | We heavily relied on JGraphT Python bindings (https://python-jgrapht.readthedocs.io/) source code to 15 | understand how such a thing could be achieved, so many thanks to JGraphT authors! 16 | 17 | Documentation 18 | ============= 19 | 20 | .. toctree:: 21 | :maxdepth: 2 22 | :caption: Contents: 23 | 24 | installation 25 | quickstart 26 | api/index 27 | 28 | Notebooks 29 | ========= 30 | 31 | .. nbgallery:: 32 | notebooks/graph_colouring 33 | notebooks/coloured_nonogram 34 | 35 | Indices and tables 36 | ================== 37 | 38 | * :ref:`genindex` 39 | * :ref:`modindex` 40 | * :ref:`search` 41 | -------------------------------------------------------------------------------- /pychoco/solution.py: -------------------------------------------------------------------------------- 1 | from pychoco import backend 2 | from pychoco._handle_wrapper import _HandleWrapper 3 | from pychoco._utils import get_int_array 4 | 5 | 6 | class Solution(_HandleWrapper): 7 | """ 8 | Solution to a Choco problem. This object can be used to retrieve the value of variables in the solution. 9 | """ 10 | 11 | def __init__(self, handle): 12 | """ 13 | Warning: Not intended to be used by users, use a Model object to instantiate constraints instead. 14 | """ 15 | super().__init__(handle) 16 | 17 | def get_int_val(self, x: "IntVar"): 18 | """ 19 | The value of the IntVar `x` in this solution. 20 | :param x: An IntVar. 21 | :return: The value of `x` in this solution. 22 | """ 23 | return backend.get_int_val(self._handle, x._handle) 24 | 25 | def get_set_val(self, s: "SetVar"): 26 | """ 27 | The value of the SetVar `s` in this solution. 28 | :param s: A SetVar. 29 | :return: The value of `s` in this solution. 30 | """ 31 | val_handle = backend.get_set_val(self._handle, s._handle) 32 | val = get_int_array(val_handle) 33 | return set(val) 34 | 35 | def __repr__(self): 36 | return "Choco Solution" 37 | -------------------------------------------------------------------------------- /pychoco/variables/graphvar.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from pychoco.variables.variable import Variable 4 | 5 | 6 | class GraphVar(Variable, ABC): 7 | """ 8 | A Graph Variable is defined by a domain which is a graph interval [LB, UB]. 9 | An instantiation of a graph variable is a graph composed of nodes and edges (directed or not). 10 | LB is the kernel graph (or lower bound), that must be a subgraph of any instantiation. 11 | UB is the envelope graph (or upper bound), such that any instantiation is a subgraph of it. 12 | """ 13 | 14 | def __init__(self, handle, model: "Model", lb: "Graph", ub: "Graph"): 15 | self._lb = lb 16 | self._ub = ub 17 | super().__init__(handle, model) 18 | 19 | @abstractmethod 20 | def is_directed(self): 21 | """ 22 | :return: True if the graphvar is directed, False otherwise. 23 | """ 24 | pass 25 | 26 | @abstractmethod 27 | def get_lb(self): 28 | pass 29 | 30 | @abstractmethod 31 | def get_ub(self): 32 | pass 33 | 34 | def get_nb_max_nodes(self): 35 | """ 36 | :return: The maximum number of node the graph variable may have. 37 | """ 38 | return self.get_ub().get_nb_max_nodes() 39 | -------------------------------------------------------------------------------- /tests/int_constraints/test_or.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestOr(unittest.TestCase): 7 | 8 | def testOr1(self): 9 | m = Model() 10 | variables = m.intvars(3, 0, 4) 11 | c1 = m.arithm(variables[0], ">", variables[1]) 12 | c2 = m.arithm(variables[1], ">", variables[2]) 13 | m.or_([c1, c2]).post() 14 | while m.get_solver().solve(): 15 | self.assertTrue(c1.is_satisfied() or c2.is_satisfied()) 16 | 17 | def testOr2(self): 18 | m = Model() 19 | variables = m.boolvars(3) 20 | m.or_(variables).post() 21 | while m.get_solver().solve(): 22 | self.assertTrue(sum([1 for v in variables if v.get_value() == 1]) > 0) 23 | 24 | def testOrFail1(self): 25 | m = Model() 26 | variables = m.boolvars(3) 27 | m.or_(variables).post() 28 | m.sum(variables, "=", 0).post() 29 | self.assertFalse(m.get_solver().solve()) 30 | 31 | def testOrFail2(self): 32 | m = Model() 33 | variables = m.intvars(3, 0, 1) 34 | all_diff = m.all_different(variables) 35 | sum_ = m.sum(variables, ">", 10) 36 | m.or_([all_diff, sum_]).post() 37 | self.assertFalse(m.get_solver().solve()) 38 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_nodes_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphNodesChanneling(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | s = m.setvar([], [0, 1, 2, 3, 4]) 14 | g = m.graphvar(lb, ub, "g") 15 | m.graph_nodes_channeling(g, s).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | self.assertSetEqual(set(val.get_nodes()), s.get_value()) 19 | 20 | def test2(self): 21 | m = Model() 22 | lb = create_undirected_graph(m, 5) 23 | ub = create_complete_undirected_graph(m, 5) 24 | bools = m.boolvars(5) 25 | g = m.graphvar(lb, ub, "g") 26 | m.graph_nodes_channeling(g, bools).post() 27 | while m.get_solver().solve(): 28 | val = g.get_value() 29 | for i in range(0, len(bools)): 30 | if i in val.get_nodes(): 31 | self.assertTrue(bools[i].get_value()) 32 | else: 33 | self.assertFalse(bools[i].get_value()) 34 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | cff-version: "1.2.0" 2 | authors: 3 | - family-names: Justeau-Allaire 4 | given-names: Dimitri 5 | orcid: "https://orcid.org/0000-0003-4129-0764" 6 | - family-names: Prud'homme 7 | given-names: Charles 8 | orcid: "https://orcid.org/0000-0002-4546-9027" 9 | contact: 10 | - family-names: Prud'homme 11 | given-names: Charles 12 | orcid: "https://orcid.org/0000-0002-4546-9027" 13 | doi: 10.5281/zenodo.17219306 14 | message: If you use this software, please cite our article in the 15 | Journal of Open Source Software. 16 | preferred-citation: 17 | authors: 18 | - family-names: Justeau-Allaire 19 | given-names: Dimitri 20 | orcid: "https://orcid.org/0000-0003-4129-0764" 21 | - family-names: Prud'homme 22 | given-names: Charles 23 | orcid: "https://orcid.org/0000-0002-4546-9027" 24 | date-published: 2025-09-29 25 | doi: 10.21105/joss.08847 26 | issn: 2475-9066 27 | issue: 113 28 | journal: Journal of Open Source Software 29 | publisher: 30 | name: Open Journals 31 | start: 8847 32 | title: "pychoco: all-inclusive Python bindings for the Choco-solver 33 | constraint programming library" 34 | type: article 35 | url: "https://joss.theoj.org/papers/10.21105/joss.08847" 36 | volume: 10 37 | title: "pychoco: all-inclusive Python bindings for the Choco-solver 38 | constraint programming library" 39 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_neighbors_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 5 | 6 | 7 | class TestGraphNeighborsChanneling(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_undirected_graph(m, 5) 12 | ub = create_complete_undirected_graph(m, 5) 13 | sets = [m.setvar([], [0, 1, 2, 3, 4]) for i in range(0, 5)] 14 | g = m.graphvar(lb, ub, "g") 15 | m.graph_neighbors_channeling(g, sets).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertSetEqual(set(val.get_neighbors_of(i)), sets[i].get_value()) 20 | 21 | def test2(self): 22 | m = Model() 23 | lb = create_undirected_graph(m, 5) 24 | ub = create_complete_undirected_graph(m, 5) 25 | bools = [m.boolvars(5) for i in range(0, 5)] 26 | g = m.graphvar(lb, ub, "g") 27 | m.graph_neighbors_channeling(g, bools).post() 28 | while m.get_solver().solve(): 29 | val = g.get_value() 30 | for i in val.get_nodes(): 31 | for j in val.get_neighbors_of(i): 32 | self.assertTrue(bools[i][j].get_value()) 33 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_successors_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | 6 | 7 | class TestGraphSuccessorsChanneling(unittest.TestCase): 8 | 9 | def test1(self): 10 | m = Model() 11 | lb = create_directed_graph(m, 4) 12 | ub = create_complete_directed_graph(m, 4) 13 | sets = [m.setvar([], [0, 1, 2, 3, 4]) for i in range(0, 4)] 14 | g = m.digraphvar(lb, ub, "g") 15 | m.graph_successors_channeling(g, sets).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | self.assertSetEqual(set(val.get_successors_of(i)), sets[i].get_value()) 20 | 21 | def test2(self): 22 | m = Model() 23 | lb = create_directed_graph(m, 4) 24 | ub = create_complete_directed_graph(m, 4) 25 | bools = [m.boolvars(4) for i in range(0, 4)] 26 | g = m.digraphvar(lb, ub, "g") 27 | m.graph_successors_channeling(g, bools).post() 28 | while m.get_solver().solve(): 29 | val = g.get_value() 30 | for i in val.get_nodes(): 31 | for j in val.get_successors_of(i): 32 | self.assertTrue(bools[i][j].get_value()) 33 | -------------------------------------------------------------------------------- /tests/int_constraints/test_table.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestTable(unittest.TestCase): 7 | 8 | def testTable1(self): 9 | m = Model() 10 | intvars = m.intvars(3, 1, 2) 11 | tuples = [ 12 | [0, 0, 0], 13 | [1, 1, 1], 14 | [2, 2, 2], 15 | [3, 3, 3] 16 | ] 17 | table_constraint = m.table(intvars, tuples, False) 18 | table_constraint.post() 19 | m.get_solver().solve() 20 | self.assertEqual(m.get_solver().get_solution_count(), 1) 21 | 22 | def testTable2(self): 23 | m = Model() 24 | x = m.intvar(0, 4) 25 | y = m.boolvar() 26 | z = m.boolvar() 27 | tuples = [ 28 | [0, -1, 1], 29 | [0, 0, 1], 30 | [5, -1, 1], 31 | [1, 0, 1] 32 | ] 33 | m.table([x, y, z], tuples, algo="CT+").post() 34 | m.get_solver().find_all_solutions() 35 | self.assertEqual(m.get_solver().get_solution_count(), 2) 36 | 37 | def testTable3(self): 38 | m = Model() 39 | x = m.intvar(0, 3) 40 | y = m.boolvar() 41 | tuples = [ 42 | [-1, 1], 43 | ] 44 | m.table([x, y], tuples, algo="CT+", universal_value=-1).post() 45 | m.get_solver().find_all_solutions() 46 | self.assertEqual(m.get_solver().get_solution_count(), 4) -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # pychoco 0.2.4 2 | 3 | - Update to choco-solver 4.10.18 4 | - Add JOSS paper 5 | 6 | # pychoco 0.2.3 7 | 8 | Rename `handle` property to `_handle` to avoid including it in autocompletion for IDE users. 9 | Also introduce minor fixes. 10 | 11 | # pychoco 0.2.2 12 | 13 | Add accessors to solver statistics: 14 | 15 | - `get_time_count()` 16 | - `get_node_count()` 17 | - `get_backtrack_count()` 18 | - `get_fail_count()` 19 | - `get_restart_count()` 20 | - `is_objective_optimal()` 21 | - `get_search_state()` 22 | 23 | # pychoco 0.2.1 24 | 25 | Same as 0.2.0 but fixing a wheel distribution issue. 26 | 27 | # pychoco 0.2.0 28 | 29 | - Update to choco-solver 4.10.16 30 | - Add `bounded_domain` option in intvar 31 | - Add reification constraints 32 | - Add `pick_on_dom` and `pick_on_fil` search strategies 33 | - Add `show_restarts` in solver 34 | - Add hybrid table constraint 35 | - Add universal value in table constraint 36 | - Add 2D shape intvars and boolvars constructor 37 | - Add Sat API (clauses) 38 | - Fix `lex_chain_less` and `lex_chain_less_eq 39 | - Add interface to parallel portfolio 40 | 41 | # pychoco 0.1.2 42 | 43 | Fix a few bugs and includes the `solver.limit_time(time_limit_string)` function. We also illustrated a few use cases in `docs/notebooks`. 44 | 45 | # pychoco 0.1.1 46 | 47 | First release of pychoco, includes most features of Choco-solver. Extensively tested but still a beta release, we are open to feedbacks ! -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Project information ----------------------------------------------------- 7 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 8 | 9 | import os 10 | import sys 11 | 12 | sys.path.insert(0, os.path.abspath('../pychoco')) 13 | 14 | project = 'pychoco' 15 | copyright = "2022, Dimitri Justeau-Allaire & Charles Prud'homme" 16 | author = "Dimitri Justeau-Allaire, Charles Prud'homme" 17 | release = '0.1.1' 18 | 19 | # -- General configuration --------------------------------------------------- 20 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 21 | 22 | extensions = [ 23 | 'sphinx.ext.autodoc', 24 | 'sphinx.ext.napoleon', 25 | 'nbsphinx', 26 | 'sphinx_gallery.load_style', 27 | ] 28 | 29 | templates_path = ['_templates'] 30 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 31 | 32 | # -- Options for HTML output ------------------------------------------------- 33 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 34 | 35 | html_theme = 'sphinx_rtd_theme' 36 | html_static_path = ['_static'] 37 | html_logo = "_static/ChocoLogo-150x135.png" 38 | 39 | add_module_names = False 40 | -------------------------------------------------------------------------------- /pychoco/variables/setvar.py: -------------------------------------------------------------------------------- 1 | from pychoco import backend 2 | from pychoco._utils import get_int_array 3 | from pychoco.variables.variable import Variable 4 | 5 | 6 | class SetVar(Variable): 7 | """ 8 | A Set Variable is defined by a domain which is a set interval [lb, ub], where: 9 | lb is the set of integers that must belong to every single solution. 10 | ub is the set of integers that may belong to at least one solution. 11 | In the context of SetVars, a value of the variable is a set of integers. 12 | """ 13 | 14 | def get_lb(self): 15 | """ 16 | :return: The lower bound of this setvar (a set of integers). 17 | """ 18 | return set(get_int_array(backend.get_setvar_lb(self._handle))) 19 | 20 | def get_ub(self): 21 | """ 22 | :return: The upper bound of this setvar (a set of integers). 23 | """ 24 | return set(get_int_array(backend.get_setvar_ub(self._handle))) 25 | 26 | def get_value(self): 27 | """ 28 | :return: The value of this set variable (only valid if it is instantiated). 29 | """ 30 | assert self.is_instantiated(), "{} is not instantiated".format(self.name) 31 | return set(get_int_array(backend.get_setvar_value(self._handle))) 32 | 33 | def get_type(self): 34 | return "SetVar" 35 | 36 | def __repr__(self): 37 | return super().__repr__() + " = [{}, {}]".format(self.get_lb(), self.get_ub()) -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | .. _quickstart: 2 | 3 | Quickstart 4 | ========== 5 | 6 | Pychoco's API is quite close to Choco's Java API. The first thing to do is to import the 7 | library and create a model object: 8 | 9 | .. code-block:: python 10 | 11 | from pychoco import Model 12 | 13 | model = Model("My Choco Model") 14 | 15 | Then, you can use this model object to create variables: 16 | 17 | .. code-block:: python 18 | 19 | intvars = model.intvars(10, 0, 10) 20 | sum_var = model.intvar(0, 100) 21 | 22 | 23 | You can also create views from this Model object: 24 | 25 | .. code-block:: python 26 | 27 | b6 = model.int_ge_view(intvars[6], 6) 28 | 29 | 30 | Create and post (or reify) constraints: 31 | 32 | .. code-block:: python 33 | 34 | model.all_different(intvars).post() 35 | model.sum(intvars, "=", sum_var).post() 36 | b7 = model.arithm(intvars[7], ">=", 7).reify() 37 | 38 | Solve your problem: 39 | 40 | .. code-block:: python 41 | 42 | model.get_solver().solve() 43 | 44 | 45 | And retrieve the solution: 46 | 47 | .. code-block:: python 48 | 49 | print("intvars = {}".format([i.get_value() for i in intvars])) 50 | print("sum = {}".format(sum_var.get_value())) 51 | print("intvar[6] >= 6 ? {}".format(b6.get_value())) 52 | print("intvar[7] >= 7 ? {}".format(b7.get_value())) 53 | 54 | > "intvars = [3, 5, 9, 6, 7, 2, 0, 1, 4, 8]" 55 | > "sum = 45" 56 | > "intvar[6] >= 6 ? False" 57 | > "intvar[7] >= 7 ? False" 58 | -------------------------------------------------------------------------------- /pychoco/variables/directed_graphvar.py: -------------------------------------------------------------------------------- 1 | from pychoco.variables.graphvar import GraphVar 2 | 3 | 4 | class DirectedGraphVar(GraphVar): 5 | """ 6 | A Directed Graph Variable is defined by a domain which is a graph interval [LB, UB]. 7 | An instantiation of a directed graph variable is a directed graph composed of nodes and edges. 8 | LB is the kernel graph (or lower bound), that must be a subgraph of any instantiation. 9 | UB is the envelope graph (or upper bound), such that any instantiation is a subgraph of it. 10 | """ 11 | 12 | def __init__(self, handle, model: "Model", lb: "DirectedGraph", ub: "DirectedGraph"): 13 | self._lb = lb 14 | self._ub = ub 15 | super().__init__(handle, model, lb, ub) 16 | 17 | def is_directed(self): 18 | return True 19 | 20 | def get_lb(self): 21 | """ 22 | :return: The lower bound of this directed graph variable (a DirectedGraph). 23 | """ 24 | return self._lb 25 | 26 | def get_ub(self): 27 | """ 28 | :return: The upper bound of this directed graph variable (a DirectedGraph). 29 | """ 30 | return self._ub 31 | 32 | def get_value(self): 33 | """ 34 | :return: The value of this variable, if it is instantiated. 35 | """ 36 | assert self.is_instantiated(), "{} is not instantiated".format(self.name) 37 | return self._lb 38 | 39 | def get_type(self): 40 | return "DirectedGraphVar" 41 | -------------------------------------------------------------------------------- /pychoco/variables/variable.py: -------------------------------------------------------------------------------- 1 | from abc import ABC, abstractmethod 2 | 3 | from pychoco import backend 4 | from pychoco._handle_wrapper import _HandleWrapper 5 | 6 | 7 | class Variable(_HandleWrapper, ABC): 8 | """ 9 | A variable (IntVar) is an unknown whose value of a constraint satisfaction (or optimization) 10 | problem. It is instantiated to a single value in any solution of the problem. 11 | """ 12 | 13 | def __init__(self, handle, model): 14 | super().__init__(handle) 15 | self._model = model 16 | 17 | @property 18 | def name(self): 19 | """ 20 | The name of the variable. 21 | """ 22 | return backend.get_variable_name(self._handle) 23 | 24 | @property 25 | def model(self): 26 | """ 27 | The model in which the variable was declared. 28 | """ 29 | return self._model 30 | 31 | def is_instantiated(self): 32 | """ 33 | :return: True if the variable is instantiated. 34 | """ 35 | return bool(backend.is_instantiated(self._handle)) 36 | 37 | def is_view(self): 38 | """ 39 | :return: True if this variable is a view 40 | """ 41 | return backend.is_view(self._handle) 42 | 43 | @abstractmethod 44 | def get_type(self): 45 | """ 46 | :return: The type of this variable. 47 | """ 48 | pass 49 | 50 | def __repr__(self): 51 | return "{} '{}'".format(self.get_type(), self.name) 52 | -------------------------------------------------------------------------------- /tests/int_constraints/test_hybrid_table.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.constraints.extension.hybrid.supportable import * 4 | from pychoco.model import Model 5 | 6 | 7 | class TestHybridTable(unittest.TestCase): 8 | 9 | def testHTable1(self): 10 | m = Model() 11 | intvars = m.intvars(3, 0, 5) 12 | htuples = [ 13 | [eq(1), gt(2), le(col(0))], 14 | [eq(2), le(2), ne(col(1))], 15 | [eq(3), eq(0), any_val()], 16 | [eq(4), ne(0), eq(col(1))] 17 | ] 18 | htable_constraint = m.hybrid_table(intvars, htuples) 19 | htable_constraint.post() 20 | sols = m.get_solver().find_all_solutions() 21 | self.assertTrue(len(sols) > 4) 22 | for s in sols: 23 | if s.get_int_val(intvars[0]) == 1: 24 | self.assertTrue(s.get_int_val(intvars[1]) > 2) 25 | self.assertTrue(s.get_int_val(intvars[2]) <= s.get_int_val(intvars[0])) 26 | elif s.get_int_val(intvars[0]) == 2: 27 | self.assertTrue(s.get_int_val(intvars[1]) <= 2) 28 | self.assertTrue(s.get_int_val(intvars[2]) != s.get_int_val(intvars[1])) 29 | elif s.get_int_val(intvars[0]) == 3: 30 | self.assertTrue(s.get_int_val(intvars[1]) == 0) 31 | elif s.get_int_val(intvars[0]) == 4: 32 | self.assertTrue(s.get_int_val(intvars[1]) != 0) 33 | self.assertTrue(s.get_int_val(intvars[2]) == s.get_int_val(intvars[1])) 34 | -------------------------------------------------------------------------------- /pychoco/variables/undirected_graphvar.py: -------------------------------------------------------------------------------- 1 | from pychoco.variables.graphvar import GraphVar 2 | 3 | 4 | class UndirectedGraphVar(GraphVar): 5 | """ 6 | An Undirected Graph Variable is defined by a domain which is a graph interval [LB, UB]. 7 | An instantiation of an undirected graph variable is an undirected graph composed of nodes and edges. 8 | LB is the kernel graph (or lower bound), that must be a subgraph of any instantiation. 9 | UB is the envelope graph (or upper bound), such that any instantiation is a subgraph of it. 10 | """ 11 | 12 | def __init__(self, handle, model: "Model", lb: "UndirectedGraph", ub: "UndirectedGraph"): 13 | self._lb = lb 14 | self._ub = ub 15 | super().__init__(handle, model, lb, ub) 16 | 17 | def is_directed(self): 18 | return False 19 | 20 | def get_lb(self): 21 | """ 22 | :return: The lower bound of this undirected graph variable (an UndirectedGraph). 23 | """ 24 | return self._lb 25 | 26 | def get_ub(self): 27 | """ 28 | :return: The upper bound of this undirected graph variable (an UndirectedGraph). 29 | """ 30 | return self._ub 31 | 32 | def get_value(self): 33 | """ 34 | :return: The value of this variable, if it is instantiated. 35 | """ 36 | assert self.is_instantiated(), "{} is not instantiated".format(self.name) 37 | return self._lb 38 | 39 | def get_type(self): 40 | return "UndirectedGraphVar" 41 | -------------------------------------------------------------------------------- /tests/int_constraints/test_sub_path.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSubPath(unittest.TestCase): 7 | 8 | def testSubPath1(self): 9 | model = Model() 10 | x = [ 11 | model.intvar(4), 12 | model.intvar(1), 13 | model.intvar(0), 14 | model.intvar(2), 15 | ] 16 | start = model.intvar(-3, 10) 17 | end = model.intvar(-3, 10) 18 | size = model.intvar(-3, 10) 19 | model.sub_path(x, start, end, 0, size).post() 20 | model.get_solver().solve(); 21 | self.assertEqual(1, model.get_solver().get_solution_count()) 22 | self.assertEqual(3, size.get_value()) 23 | self.assertEqual(3, start.get_value()) 24 | self.assertEqual(0, end.get_value()) 25 | 26 | def testSubPath2(self): 27 | model = Model() 28 | x = [ 29 | model.intvar(4), 30 | model.intvar(-3, 6), 31 | model.intvar(0), 32 | model.intvar(2), 33 | ] 34 | start = model.intvar(-3, 10) 35 | end = model.intvar(-3, 10) 36 | size = model.intvar(3) 37 | model.sub_path(x, start, end, 0, size).post() 38 | model.get_solver().solve() 39 | self.assertEqual(1, model.get_solver().get_solution_count()) 40 | self.assertEqual(3, size.get_value()) 41 | self.assertEqual(3, start.get_value()) 42 | self.assertEqual(1, x[1].get_value()) 43 | self.assertEqual(0, end.get_value()) 44 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_transitivity.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 6 | 7 | 8 | class TestGraphTransitivity(unittest.TestCase): 9 | 10 | def test1(self): 11 | m = Model() 12 | lb = create_undirected_graph(m, 5) 13 | ub = create_complete_undirected_graph(m, 5) 14 | g = m.graphvar(lb, ub, "g") 15 | m.graph_transitivity(g).post() 16 | while m.get_solver().solve(): 17 | val = g.get_value() 18 | for i in val.get_nodes(): 19 | for j in val.get_neighbors_of(i): 20 | for k in val.get_neighbors_of(j): 21 | if k != i: 22 | self.assertTrue(val.contains_edge(i, k)) 23 | 24 | def test2(self): 25 | m = Model() 26 | lb = create_directed_graph(m, 4) 27 | ub = create_complete_directed_graph(m, 4) 28 | g = m.digraphvar(lb, ub, "g") 29 | m.graph_transitivity(g).post() 30 | while m.get_solver().solve(): 31 | val = g.get_value() 32 | for i in val.get_nodes(): 33 | for j in val.get_successors_of(i): 34 | for k in val.get_successors_of(j): 35 | if k != i: 36 | self.assertTrue(val.contains_edge(i, k)) 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2025, IMT Atlantique 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /tests/int_constraints/test_element.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.utils import ESat 5 | 6 | 7 | class TestElement(unittest.TestCase): 8 | 9 | def genericTest(self, model, x, index, values, offset, nb_sols): 10 | model.element(x, values, index, offset=offset).post() 11 | solutions = model.get_solver().find_all_solutions() 12 | self.assertEqual(len(solutions), nb_sols) 13 | 14 | def testAllSame(self): 15 | m = Model("m") 16 | values = [1, 1, 1, 1] 17 | x = m.intvar(0, 1) 18 | index = m.intvar(20, 22) 19 | self.genericTest(m, x, index, values, 20, 3) 20 | 21 | def test1(self): 22 | m = Model("m") 23 | values = [1, 2, 0, 4, 3] 24 | index = m.intvar(-3, 10) 25 | var = m.intvar(-20, 20) 26 | self.genericTest(m, var, index, values, 0, 5) 27 | 28 | def testNeg(self): 29 | m = Model("m") 30 | values = [1, 2, 0, 4, ] 31 | index = m.intvar(-3, 10) 32 | var = m.intvar(-20, 20); 33 | m.element(var, values, index).reify() 34 | m.get_solver().find_all_solutions() 35 | 36 | def testProp1(self): 37 | m = Model() 38 | values = [1, 2, 0, 4, 3] 39 | index = m.intvar(0) 40 | var = m.intvar(1) 41 | c = m.element(var, values, index) 42 | self.assertEqual(ESat.TRUE, c.is_satisfied()) 43 | 44 | def testProp2(self): 45 | m = Model() 46 | values = [1, 2, 0, 4, 3] 47 | index = m.intvar(0) 48 | var = m.intvar(2) 49 | c = m.element(var, values, index); 50 | self.assertEqual(ESat.FALSE, c.is_satisfied()) 51 | -------------------------------------------------------------------------------- /tests/int_constraints/test_sum.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestSum(unittest.TestCase): 7 | 8 | def testSum1(self): 9 | model = Model() 10 | intvars = model.intvars(5, 0, 5) 11 | sumvar = model.intvar(15, 20) 12 | model.sum(intvars, "=", sumvar).post() 13 | nb_sol = self._check_solutions("=", intvars, sumvar) 14 | # compare to scalar 15 | coeffs = [1, 1, 1, 1, 1] 16 | model = Model() 17 | intvars = model.intvars(5, 0, 5) 18 | sumvar = model.intvar(15, 20) 19 | model.scalar(intvars, coeffs, "=", sumvar).post() 20 | nb_sol2 = 0; 21 | while model.get_solver().solve(): 22 | nb_sol2 += 1 23 | self.assertEqual(nb_sol, nb_sol2) 24 | 25 | def testSumFail(self): 26 | model = Model() 27 | intvars = model.intvars(5, 0, 5) 28 | sumvar = model.intvar(26, 30) 29 | model.sum(intvars, "=", sumvar).post() 30 | self.assertFalse(model.get_solver().solve()) 31 | 32 | def _check_solutions(self, operator, intvars, sumvar): 33 | model = sumvar.model 34 | nb_sol = 0 35 | while model.get_solver().solve(): 36 | nb_sol += 1 37 | computed = sum([v.get_value() for v in intvars]) 38 | if operator == "=": 39 | self.assertEqual(computed, sumvar.get_value()) 40 | elif operator == ">=": 41 | self.assertTrue(computed >= sumvar.get_value()) 42 | elif operator == "<=": 43 | self.assertTrue(computed <= sumvar.getValue()) 44 | self.assertTrue(nb_sol > 0) 45 | return nb_sol 46 | -------------------------------------------------------------------------------- /tests/int_constraints/test_distance.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestDistance(unittest.TestCase): 7 | 8 | def testDistance1(self): 9 | m = Model() 10 | a = m.intvar(0, 10) 11 | b = m.intvar(0, 20) 12 | m.distance(a, b, ">", 10).post() 13 | sols = m.get_solver().find_all_solutions() 14 | for s in sols: 15 | self.assertTrue(abs(s.get_int_val(a) - s.get_int_val(b)) >= 10) 16 | 17 | def testDistance2(self): 18 | m = Model() 19 | a = m.intvar(0, 10) 20 | b = m.intvar(0, 20) 21 | c = m.intvar(0, 20) 22 | m.distance(a, b, "=", c).post() 23 | sols = m.get_solver().find_all_solutions() 24 | for s in sols: 25 | self.assertEqual(abs(s.get_int_val(a) - s.get_int_val(b)), s.get_int_val(c)) 26 | 27 | def testDistance3(self): 28 | m = Model() 29 | a = m.intvar(0, 10) 30 | b = m.intvar(0, 20) 31 | c = m.intvar(0, 20) 32 | m.distance(a, b, ">", c).post() 33 | sols = m.get_solver().find_all_solutions() 34 | for s in sols: 35 | self.assertTrue(abs(s.get_int_val(a) - s.get_int_val(b)) > s.get_int_val(c)) 36 | 37 | def testDistanceError(self): 38 | m = Model() 39 | a = m.intvar(0, 10) 40 | b = m.intvar(0, 20) 41 | c = m.intvar(0, 20) 42 | self.assertRaises(AssertionError, m.distance, *[a, b, "!=", c]) 43 | self.assertRaises(AssertionError, m.distance, *[a, b, ">=", 10]) 44 | self.assertRaises(AssertionError, m.distance, *[a, b, "pp", c]) 45 | self.assertRaises(AssertionError, m.distance, *[a, b, "pp", 2]) 46 | -------------------------------------------------------------------------------- /tests/test_task.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestTask(unittest.TestCase): 7 | 8 | def test_create_task_iv_iv_iv(self): 9 | model = Model("MyModel") 10 | s = model.intvar(0, 10) 11 | d = model.intvar(0, 10) 12 | e = model.intvar(0, 20) 13 | t = model.task(s, d, e) 14 | self.assertEqual(t.start.get_ub(), 10) 15 | self.assertEqual(t.end.get_lb(), 0) 16 | self.assertEqual(t.duration.get_lb(), 0) 17 | t.ensure_bound_consistency() 18 | 19 | def test_create_task_iv_i(self): 20 | model = Model("MyModel") 21 | s = model.intvar(0, 10) 22 | d = 10 23 | t = model.task(s, d) 24 | self.assertEqual(t.start.get_ub(), 10) 25 | self.assertEqual(t.end.get_ub(), 20) 26 | self.assertEqual(t.duration.get_lb(), 10) 27 | t.ensure_bound_consistency() 28 | 29 | def test_create_task_iv_iv(self): 30 | model = Model("MyModel") 31 | s = model.intvar(0, 10) 32 | d = model.intvar(0, 10) 33 | t = model.task(s, d) 34 | self.assertEqual(t.start.get_ub(), 10) 35 | self.assertEqual(t.end.get_ub(), 20) 36 | self.assertEqual(t.duration.get_lb(), 0) 37 | t.ensure_bound_consistency() 38 | 39 | def test_create_task_iv_i_iv(self): 40 | model = Model("MyModel") 41 | s = model.intvar(0, 10) 42 | d = 10 43 | e = model.intvar(0, 20) 44 | t = model.task(s, d, e) 45 | t.ensure_bound_consistency() 46 | self.assertEqual(t.start.get_ub(), 10) 47 | self.assertEqual(t.end.get_lb(), 10) 48 | self.assertEqual(t.duration.get_lb(), 10) 49 | -------------------------------------------------------------------------------- /tests/int_constraints/test_and.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestAnd(unittest.TestCase): 7 | 8 | def testAnd1(self): 9 | m = Model() 10 | variables = m.intvars(3, 0, 4) 11 | all_diff = m.all_different(variables) 12 | sum_ = m.sum(variables, "<=", 3) 13 | m.and_([all_diff, sum_]).post() 14 | while m.get_solver().solve(): 15 | self.assertNotEqual(variables[0].get_value(), variables[1].get_value()) 16 | self.assertNotEqual(variables[0].get_value(), variables[2].get_value()) 17 | self.assertNotEqual(variables[1].get_value(), variables[2].get_value()) 18 | self.assertTrue(all_diff.is_satisfied()) 19 | self.assertTrue(sum_.is_satisfied()) 20 | self.assertEqual(m.get_solver().get_solution_count(), 6) 21 | 22 | def testAnd2(self): 23 | m = Model() 24 | variables = m.boolvars(3) 25 | m.and_(variables).post() 26 | sols = m.get_solver().find_all_solutions() 27 | self.assertEqual(len(sols), 1) 28 | for v in variables: 29 | self.assertEqual(sols[0].get_int_val(v), 1) 30 | 31 | def testAndFail1(self): 32 | m = Model() 33 | variables = m.boolvars(3) 34 | m.and_(variables).post() 35 | m.sum(variables, "<", 3).post() 36 | self.assertFalse(m.get_solver().solve()) 37 | 38 | def testAndFail2(self): 39 | m = Model() 40 | variables = m.intvars(3, 0, 3) 41 | all_diff = m.all_different(variables) 42 | sum_ = m.sum(variables, "<=", 2) 43 | m.and_([all_diff, sum_]).post() 44 | self.assertFalse(m.get_solver().solve()) 45 | -------------------------------------------------------------------------------- /tests/graph_constraints/test_graph_subgraph.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import create_directed_graph, create_complete_directed_graph 5 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, create_complete_undirected_graph 6 | 7 | 8 | class TestGraphSubGraph(unittest.TestCase): 9 | 10 | def test1(self): 11 | m = Model() 12 | lb1 = create_undirected_graph(m, 4) 13 | lb2 = create_undirected_graph(m, 4) 14 | ub1 = create_complete_undirected_graph(m, 4) 15 | ub2 = create_complete_undirected_graph(m, 4) 16 | g1 = m.graphvar(lb1, ub1, "g") 17 | g2 = m.graphvar(lb2, ub2, "g") 18 | m.graph_subgraph(g1, g2).post() 19 | while m.get_solver().solve(): 20 | val1 = g1.get_value() 21 | val2 = g2.get_value() 22 | for i in val1.get_nodes(): 23 | self.assertTrue(i in val2.get_nodes()) 24 | for j in val1.get_neighbors_of(i): 25 | self.assertTrue(val2.contains_edge(i, j)) 26 | 27 | def test2(self): 28 | m = Model() 29 | lb1 = create_directed_graph(m, 3) 30 | lb2 = create_directed_graph(m, 3) 31 | ub1 = create_complete_directed_graph(m, 3) 32 | ub2 = create_complete_directed_graph(m, 3) 33 | g1 = m.digraphvar(lb1, ub1, "g") 34 | g2 = m.digraphvar(lb2, ub2, "g") 35 | m.graph_subgraph(g1, g2).post() 36 | while m.get_solver().solve(): 37 | val1 = g1.get_value() 38 | val2 = g2.get_value() 39 | for i in val1.get_nodes(): 40 | self.assertTrue(i in val2.get_nodes()) 41 | for j in val1.get_successors_of(i): 42 | self.assertTrue(val2.contains_edge(i, j)) 43 | -------------------------------------------------------------------------------- /tests/int_constraints/test_scalar.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestScalar(unittest.TestCase): 7 | 8 | def testScalar1(self): 9 | model = Model() 10 | coeffs = [1, 5, 7, 8] 11 | intvars = model.intvars(4, 1, 5); 12 | model.scalar(intvars, coeffs, "=", 35).post() 13 | self._check_solutions(coeffs, intvars, model.intvar(35), "=") 14 | 15 | def testScalar2(self): 16 | model = Model() 17 | coeffs = [5, 6, 7, 9] 18 | vars = model.intvars(4, -5, 5) 19 | model.scalar(vars, coeffs, "<=", 0).post() 20 | self._check_solutions(coeffs, vars, model.intvar(0), "<=") 21 | 22 | def testScalar3(self): 23 | model = Model() 24 | coeffs = [1] 25 | intvars = [model.intvar(1, 100)] 26 | sumvar = model.intvar(1, 100) 27 | model.scalar(intvars, coeffs, "=", sumvar).post() 28 | self.assertEqual(self._check_solutions(coeffs, intvars, sumvar, "="), 100) 29 | 30 | def testScalarFail(self): 31 | model = Model() 32 | coeffs = [0] 33 | intvars = [model.intvar(1, 10)] 34 | model.scalar(intvars, coeffs, ">=", 1).post() 35 | self.assertFalse(model.get_solver().solve()) 36 | 37 | def _check_solutions(self, coeffs, intvars, sumvar, operator): 38 | model = intvars[0].model 39 | nb_sol = 0 40 | while model.get_solver().solve(): 41 | nb_sol += 1 42 | computed = 0 43 | for i in range(0, len(intvars)): 44 | computed += coeffs[i] * intvars[i].get_value() 45 | if operator == "=": 46 | self.assertEqual(sumvar.get_value(), computed) 47 | elif operator == "<=": 48 | self.assertTrue(computed <= sumvar.get_value()) 49 | self.assertTrue(nb_sol > 0) 50 | return nb_sol 51 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | .. _install: 2 | 3 | Installation 4 | ============ 5 | 6 | We are still in the process of implementing and releasing PyChoco. So currently the only way to install 7 | it and try it is to follow the entire build-from-source process. However, we plan to release pre-built 8 | Python wheels for various operating systems. Stay tuned! 9 | 10 | Installation from PyPI 11 | ---------------------- 12 | 13 | We automatically build 64-bit wheels for Python versions 3.6, 3.7, 3.8, 3.9, and 3.10 on Linux, Windows and 14 | MacOSX. They can be directly downloaded from PyPI (https://pypi.org/project/pychoco/) or using pip:: 15 | 16 | $ pip install pychoco 17 | 18 | Build from source 19 | ----------------- 20 | 21 | The following system dependencies are required to build PyChco from sources: 22 | 23 | - GraalVM >= 20 (see https://www.graalvm.org/) 24 | - Native Image component for GraalVM (see https://www.graalvm.org/22.1/reference-manual/native-image/) 25 | - Apache Maven (see https://maven.apache.org/) 26 | - Python >= 3.6 (see https://www.python.org/) 27 | - SWIG >= 3 (see https://www.swig.org/) 28 | 29 | Once these dependencies are satisfied, clone the current repository:: 30 | 31 | $ git clone --recurse-submodules https://github.com/dimitri-justeau/pychoco.git 32 | 33 | The `--recurse-submodules` is necessary as the `choco-solver-capi` is a separate git project included 34 | as a submodule (see https://github.com/dimitri-justeau/choco-solver-capi). It contains all the necessary 35 | to compile Choco-solver as a shared native library using GraalVM native-image. 36 | 37 | Ensure that the `$JAVA_HOME` environment variable is pointing to GraalVM, and from the cloned repository 38 | execute the following command:: 39 | 40 | $ sh build.sh 41 | 42 | This command will compile Choco-solver into a shared native library and compile the Python bindings 43 | to this native API using SWIG. 44 | 45 | Finally, run:: 46 | 47 | $ pip install . 48 | 49 | And voilà ! 50 | -------------------------------------------------------------------------------- /.github/actions/build-choco-solver-capi/action.yml: -------------------------------------------------------------------------------- 1 | name: "Build choco-solver-capi" 2 | 3 | description: "build choco-solver-capi and caches it" 4 | 5 | inputs: 6 | os: 7 | description: target OS 8 | required: true 9 | type: string 10 | arch: 11 | description: target arch 12 | required: true 13 | type: string 14 | cache: 15 | description: if true, caches the result 16 | required: false 17 | default: true 18 | type: string 19 | 20 | runs: 21 | using: composite 22 | steps: 23 | - name: Check out repository 24 | uses: actions/checkout@v4 25 | with: 26 | submodules: recursive 27 | - name: Check choco-solver-capi latest commit 28 | shell: bash 29 | run: echo "CHOCO_CAPI_LATEST_HASH=$(git -C choco-solver-capi log HEAD -n 1 --pretty=format:%h)" >> "$GITHUB_ENV" 30 | - if: ${{ inputs.cache == 'true' }} 31 | name: Cache choco-solver-capi build 32 | id: cache-choco-solver-capi 33 | uses: actions/cache@v4 34 | env: 35 | cache-name: cache-choco-solver-capi-build 36 | with: 37 | path: choco-solver-capi 38 | key: ${{ inputs.os }}-${{ inputs.arch }}-build-${{ env.cache-name }}-${{ env.CHOCO_CAPI_LATEST_HASH }} 39 | - if: ${{ (inputs.cache != 'true') || (steps.cache-choco-solver-capi.outputs.cache-hit != 'true') }} 40 | name: Set up Visual Studio shell (only for Windows) 41 | uses: ilammy/msvc-dev-cmd@v1 42 | with: 43 | arch: x64 44 | - if: ${{ (inputs.cache != 'true') || (steps.cache-choco-solver-capi.outputs.cache-hit != 'true') }} 45 | name: Set up GraalVM Native Image toolchain 46 | uses: graalvm/setup-graalvm@v1 47 | with: 48 | java-version: '22' 49 | distribution: 'graalvm' 50 | cache: 'maven' 51 | - if: ${{ (inputs.cache != 'true') || (steps.cache-choco-solver-capi.outputs.cache-hit != 'true') }} 52 | name: Build choco-solver-capi 53 | shell: bash 54 | run: (cd choco-solver-capi ; sh build.sh) 55 | -------------------------------------------------------------------------------- /pychoco/variables/boolvar.py: -------------------------------------------------------------------------------- 1 | from pychoco.variables.intvar import IntVar 2 | 3 | 4 | class BoolVar(IntVar): 5 | """ 6 | A boolean variable (BoolVar) is an unknown whose value should be a boolean (0 / 1, 7 | or False / True). Therefore, the domain of an integer variable is [0, 1]. 8 | """ 9 | 10 | def get_type(self): 11 | return "BoolVar" 12 | 13 | def get_value(self): 14 | return bool(super().get_value()) 15 | 16 | def __and__(self, other): 17 | if isinstance(other, BoolVar): 18 | return self.model.and_([self, other]).reify() 19 | elif isinstance(other, bool): 20 | return self.model.and_([self, self.model.boolvar(other)]).reify() 21 | else: 22 | raise NotImplementedError("Unsupported operation between BoolVar and {}".format(other.__class__)) 23 | 24 | def __rand__(self, other): 25 | return self.__and__(other) 26 | 27 | def __or__(self, other): 28 | if isinstance(other, BoolVar): 29 | return self.model.or_([self, other]).reify() 30 | elif isinstance(other, bool): 31 | return self.model.or_([self, self.model.boolvar(other)]).reify() 32 | else: 33 | raise NotImplementedError("Unsupported operation between BoolVar and {}".format(other.__class__)) 34 | 35 | def __ror__(self, other): 36 | return self.__or__(other) 37 | 38 | def __invert__(self): 39 | return self.model.bool_not_view(self) 40 | 41 | def __eq__(self, other): 42 | if isinstance(other, (IntVar, BoolVar)): 43 | return self.model.arithm(self, "=", other).reify() 44 | elif isinstance(other, bool): 45 | return self.model.int_eq_view(self, other) 46 | else: 47 | raise NotImplementedError("Unsupported operation between BoolVar and {}".format(other.__class__)) 48 | 49 | def __ne__(self, other): 50 | if isinstance(other, (IntVar, BoolVar)): 51 | return self.model.arithm(self, "!=", other).reify() 52 | elif isinstance(other, bool): 53 | return self.model.int_ne_view(self, other) 54 | else: 55 | raise NotImplementedError("Unsupported operation between BoolVar and {}".format(other.__class__)) 56 | -------------------------------------------------------------------------------- /tests/int_constraints/test_bits_int_channeling.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestBitsIntChanneling(unittest.TestCase): 7 | 8 | def testBitsIntChanneling1(self): 9 | model = Model() 10 | bits = [ 11 | model.boolvar(False), 12 | model.boolvar(True), 13 | model.boolvar(False), 14 | model.boolvar(True), 15 | model.boolvar(False) 16 | ] 17 | intvar = model.intvar(0, 100) 18 | model.bits_int_channeling(bits, intvar).post() 19 | self.assertTrue(model.get_solver().solve()) 20 | self.assertEqual(intvar.get_value(), 10) 21 | self.assertFalse(model.get_solver().solve()) 22 | 23 | def testBitsIntChanneling2(self): 24 | model = Model() 25 | bits = [] 26 | intvar = model.intvar(0, 100) 27 | model.bits_int_channeling(bits, intvar).post() 28 | self.assertTrue(model.get_solver().solve()) 29 | self.assertEqual(intvar.get_value(), 0) 30 | self.assertFalse(model.get_solver().solve()) 31 | 32 | def testBitsIntChanneling3(self): 33 | model = Model() 34 | bits = model.boolvars(7) 35 | intvar = model.intvar(10) 36 | model.bits_int_channeling(bits, intvar).post() 37 | self._check_solutions(model, bits, intvar) 38 | 39 | def testBitsIntChanneling4(self): 40 | model = Model() 41 | bits = model.boolvars(10) 42 | var = model.intvar(0, 1000) 43 | model.bits_int_channeling(bits, var).post() 44 | self._check_solutions(model, bits, var) 45 | 46 | def testBitsIntChannelingFail(self): 47 | model = Model() 48 | bits = model.boolvars(7) 49 | intvar = model.intvar(128, 500) 50 | model.bits_int_channeling(bits, intvar).post() 51 | self.assertFalse(model.get_solver().solve()) 52 | 53 | def _check_solutions(self, model, bits, var): 54 | nb_sol = 0 55 | while model.get_solver().solve(): 56 | nb_sol += 1 57 | exp = 1 58 | number = 0 59 | for bit in bits: 60 | number += bit.get_value() * exp 61 | exp *= 2 62 | self.assertEqual(number, var.get_value()) 63 | self.assertTrue(nb_sol > 0) 64 | -------------------------------------------------------------------------------- /tests/int_constraints/test_keysort.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | 5 | 6 | class TestKeysort(unittest.TestCase): 7 | 8 | def testKeysort1(self): 9 | model = Model() 10 | x = [ 11 | [model.intvar(2), model.intvar(3), model.intvar(1001)], 12 | [model.intvar(2), model.intvar(4), model.intvar(1002)], 13 | [model.intvar(1), model.intvar(5), model.intvar(1003)], 14 | [model.intvar(2), model.intvar(3), model.intvar(1004)] 15 | ] 16 | y = [ 17 | [model.intvar(0, 3), model.intvar(2, 6), model.intvar(1000, 10006)], 18 | [model.intvar(0, 3), model.intvar(2, 6), model.intvar(1000, 10006)], 19 | [model.intvar(0, 3), model.intvar(2, 6), model.intvar(1000, 10006)], 20 | [model.intvar(0, 3), model.intvar(2, 6), model.intvar(1000, 10006)] 21 | ] 22 | model.keysort(x, None, y, 2).post() 23 | model.get_solver().solve() 24 | self.assertEqual(y[0][0].get_value(), 1) 25 | self.assertEqual(y[0][1].get_value(), 5) 26 | self.assertEqual(y[0][2].get_value(), 1003) 27 | self.assertEqual(y[1][0].get_value(), 2) 28 | self.assertEqual(y[1][1].get_value(), 3) 29 | self.assertEqual(y[1][2].get_value(), 1001) 30 | self.assertEqual(y[2][0].get_value(), 2) 31 | self.assertEqual(y[2][1].get_value(), 3) 32 | self.assertEqual(y[2][2].get_value(), 1004) 33 | self.assertEqual(y[3][0].get_value(), 2) 34 | self.assertEqual(y[3][1].get_value(), 4) 35 | self.assertEqual(y[3][2].get_value(), 1002) 36 | 37 | def testKeysort2(self): 38 | model = Model() 39 | x = [ 40 | [model.intvar(3, 3), model.intvar(1, 1)], 41 | [model.intvar(1, 4), model.intvar(2, 2)], 42 | [model.intvar(4, 4), model.intvar(3, 3)], 43 | [model.intvar(1, 4), model.intvar(4, 4)], 44 | ] 45 | y = [ 46 | [model.intvar(1, 4), model.intvar(1, 4)], 47 | [model.intvar(1, 4), model.intvar(1, 4)], 48 | [model.intvar(1, 4), model.intvar(1, 4)], 49 | [model.intvar(1, 4), model.intvar(1, 4)], 50 | ] 51 | model.keysort(x, None, y, 2).post() 52 | while model.get_solver().solve(): 53 | pass 54 | self.assertEqual(model.get_solver().get_solution_count(), 16) 55 | -------------------------------------------------------------------------------- /tests/int_constraints/test_regular.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.automaton.finite_automaton import FiniteAutomaton 5 | 6 | 7 | class TestRegular(unittest.TestCase): 8 | 9 | def testRegular1(self): 10 | model = Model() 11 | n = 10 12 | intvars = model.intvars(n, 0, 2) 13 | auto = FiniteAutomaton() 14 | start = auto.add_state() 15 | end = auto.add_state() 16 | auto.set_initial_state(start) 17 | auto.set_final(start) 18 | auto.set_final(end) 19 | auto.add_transition(start, start, 0, 1) 20 | auto.add_transition(start, end, 2) 21 | auto.add_transition(end, start, 2) 22 | auto.add_transition(end, start, 0, 1) 23 | model.regular(intvars, auto).post() 24 | model.get_solver().set_input_order_lb_search(*intvars) 25 | while model.get_solver().solve(): 26 | pass 27 | self.assertEqual(model.get_solver().get_solution_count(), 59049) 28 | 29 | def testRegular2(self): 30 | model = Model() 31 | n = 12 32 | intvars = model.intvars(n, 0, 2) 33 | # different rules are formulated as patterns that must NOT be matched by x 34 | forbidden_regexps = [ 35 | # do not end with '00' if start with '11' 36 | "11(0|1|2)*00", 37 | # at most three consecutive 0 38 | "(0|1|2)*0000(0|1|2)*", 39 | # no pattern '112' at position 5 40 | "(0|1|2){4}112(0|1|2)*", 41 | # pattern '12' after a 0 or a sequence of 0 42 | "(0|1|2)*02(0|1|2)*", 43 | "(0|1|2)*01(0|1)(0|1|2)*", 44 | # at most three 2 on consecutive even positions 45 | "(0|1|2)((0|1|2)(0|1|2))*2(0|1|2)2(0|1|2)2(0|1|2)*" 46 | ] 47 | # a unique automaton is built as the complement language composed of all the forbidden patterns 48 | auto = FiniteAutomaton() 49 | for reg in forbidden_regexps: 50 | a = FiniteAutomaton(reg) 51 | auto = auto.union(a) 52 | auto.minimize() 53 | auto = auto.complement() 54 | auto.minimize() 55 | self.assertEqual(auto.nb_states, 54) 56 | model.regular(intvars, auto).post() 57 | model.get_solver().set_input_order_lb_search(*intvars) 58 | while model.get_solver().solve(): 59 | pass 60 | self.assertEqual(model.get_solver().get_solution_count(), 25980) 61 | -------------------------------------------------------------------------------- /pychoco/model.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | 3 | from pychoco import backend 4 | from pychoco._handle_wrapper import _HandleWrapper 5 | from pychoco.constraints.graph_constraint_factory import GraphConstraintFactory 6 | from pychoco.constraints.int_constraint_factory import IntConstraintFactory 7 | from pychoco.constraints.sat_factory import SatFactory 8 | from pychoco.constraints.set_constraint_factory import SetConstraintFactory 9 | from pychoco.constraints.reification_factory import ReificationFactory 10 | from pychoco.solver import Solver 11 | from pychoco.variables.variable_factory import VariableFactory 12 | from pychoco.variables.view_factory import ViewFactory 13 | 14 | 15 | class Model(VariableFactory, ViewFactory, IntConstraintFactory, SetConstraintFactory, GraphConstraintFactory, 16 | ReificationFactory, SatFactory, _HandleWrapper): 17 | """ 18 | The Model is the header component of Constraint Programming. It embeds the list of 19 | Variable (and their Domain), the Constraint's network, and a propagation engine to 20 | pilot the propagation. 21 | """ 22 | 23 | def __init__(self, name: Optional[str] = None, **kwargs: Any) -> None: 24 | """ 25 | Choco Model constructor. 26 | 27 | :param name: The name of the model (optional). 28 | """ 29 | if "_handle" in kwargs: 30 | super(Model, self).__init__(kwargs["_handle"]) 31 | else: 32 | if name is not None: 33 | handle = backend.create_model_s(name) 34 | else: 35 | handle = backend.create_model() 36 | super(Model, self).__init__(handle) 37 | 38 | @property 39 | def _handle(self): 40 | return self._handle_ 41 | 42 | @property 43 | def name(self): 44 | """ 45 | :return: The name of the model. 46 | """ 47 | return backend.get_model_name(self._handle) 48 | 49 | def get_solver(self) -> Solver: 50 | """ 51 | :return: The solver associated with this model. 52 | """ 53 | solver_handler = backend.get_solver(self._handle) 54 | return Solver(solver_handler, self) 55 | 56 | def set_objective(self, objective: "Variable", maximize: bool = True): 57 | """ 58 | Define an optimization objective. 59 | :param objective: The optimization objective. 60 | :param maximize: if True, maximizes objective, otherwise minimizes it. 61 | """ 62 | backend.set_objective(self._handle, maximize, objective._handle) 63 | 64 | def __repr__(self): 65 | return "Choco Model ('" + self.name + "')" 66 | -------------------------------------------------------------------------------- /.github/workflows/ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu 2 | 3 | on: 4 | push: 5 | branches: [ "master", "dev_actions" ] 6 | tags: [ '**' ] 7 | 8 | jobs: 9 | build-ubuntu: 10 | runs-on: ${{ matrix.os }} 11 | timeout-minutes: 30 12 | strategy: 13 | fail-fast: true 14 | matrix: 15 | os : [ubuntu-22.04] 16 | arch: [x86_64] 17 | python-version: ["3.11"] 18 | #python-version: ["3.12", "3.11", "3.10", "3.9"] 19 | steps: 20 | # if matrix.force is not true or event is not tag, skip 21 | - if: ${{ matrix.force }} != true || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags')) 22 | run: 23 | echo "Skipping job for ${{ matrix.os }} ${{ matrix.python-version }}" 24 | exit 0 25 | - name: Check out repository 26 | uses: actions/checkout@v3 27 | - name: Setup python 28 | uses: actions/setup-python@v3 29 | with: 30 | python-version: ${{ matrix.python-version }} 31 | architecture: x64 32 | - name: Display Python version 33 | run: | 34 | python -c "import sys; print(sys.version)" 35 | - name: Install setuptools (needed from Python 3.12) 36 | run: pip install setuptools 37 | - name: Set up GraalVM Native Image toolchain 38 | uses: graalvm/setup-graalvm@v1 39 | with: 40 | java-version: '22' 41 | distribution: 'graalvm' 42 | - name: Install Swig 43 | run: sudo apt-get install swig 44 | - name: Update repository 45 | run: | 46 | git submodule update --init --recursive 47 | - name: Build 48 | run: | 49 | sh build.sh nowheel 50 | pip install pychoco -f dist/ 51 | - name: Build wheels 52 | uses: pypa/cibuildwheel@v3.0 53 | env: 54 | CIBW_SKIP: "*-musllinux*" 55 | CIBW_BUILD_FRONTEND: build 56 | CIBW_MANYLINUX_X86_64_IMAGE: "quay.io/pypa/manylinux_2_34_x86_64" 57 | CIBW_ARCHS: x86_64 58 | with: 59 | output-dir: dist 60 | - name: Test 61 | run: | 62 | pip install pytest 63 | pip install -r requirements_tests.txt 64 | pytest 65 | - name: Generate Report 66 | run: | 67 | pip install coverage 68 | coverage run -m unittest 69 | - name: Upload Coverage to Codecov 70 | uses: codecov/codecov-action@v3 71 | - name: Upload to PyPi 72 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 73 | uses: pypa/gh-action-pypi-publish@release/v1 74 | with: 75 | user: __token__ 76 | password: ${{ secrets.PYPI_API_TOKEN }} 77 | verbose: true 78 | skip-existing: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | pychoco/backend_wrap.c 11 | pychoco/backend.py 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | 141 | # Cython debug symbols 142 | cython_debug/ 143 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: 4 | push: 5 | branches: [ "master", "dev_actions" ] 6 | tags: [ '**' ] 7 | 8 | jobs: 9 | build-windows: 10 | runs-on: ${{ matrix.os }} 11 | timeout-minutes: 20 12 | strategy: 13 | matrix: 14 | os : [windows-2022] 15 | arch: [x86_64] 16 | python-version: ["3.11"] 17 | steps: 18 | - name: Print system info 19 | run: echo $(uname -o) $(uname -r) $(uname -m) 20 | - name: Check out repository 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: recursive 24 | - name: Setup python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | architecture: x64 29 | cache: 'pip' 30 | cache-dependency-path: '**/requirements*.txt' 31 | - name: Install setuptools (needed from Python 3.12) 32 | run: pip install setuptools 33 | - name: Install Swig 34 | run: choco install swig 35 | - name: Build choco-solver-capi 36 | uses: ./.github/actions/build-choco-solver-capi 37 | with: 38 | os: ${{ runner.os }} 39 | arch: ${{ matrix.arch }} 40 | - name: Build pychoco 41 | run: | 42 | sh build.sh nocapibuild nowheel 43 | pip install pychoco -f dist/ 44 | - name: Test 45 | run: | 46 | pip install -U pytest 47 | pip install -r requirements_tests.txt 48 | pytest 49 | - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 50 | name: Build wheels 51 | uses: pypa/cibuildwheel@v2.21.1 52 | env: 53 | CIBW_ARCHS: AMD64 54 | with: 55 | output-dir: dist 56 | - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 57 | name: Upload wheel artifacts 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: wheel-${{matrix.os}}-${{matrix.python-version}}-artifact 61 | path: dist/ 62 | if-no-files-found: error 63 | 64 | 65 | upload-pypi-windows: 66 | needs: build-windows 67 | runs-on: ubuntu-latest 68 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 69 | strategy: 70 | matrix: 71 | os : [windows-2022] 72 | python-version: ["3.11"] 73 | steps: 74 | - name: Download artifacts 75 | uses: actions/download-artifact@v4 76 | with: 77 | name: wheel-${{matrix.os}}-${{matrix.python-version}}-artifact 78 | path: dist 79 | - name: List files 80 | run: ls -R 81 | - name: Upload to PyPi 82 | uses: pypa/gh-action-pypi-publish@release/v1 83 | with: 84 | user: __token__ 85 | password: ${{ secrets.PYPI_API_TOKEN }} 86 | verbose: true 87 | skip-existing: true 88 | -------------------------------------------------------------------------------- /pychoco/variables/task.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from pychoco import backend 4 | from pychoco._handle_wrapper import _HandleWrapper 5 | from pychoco.variables.intvar import IntVar 6 | 7 | 8 | class Task(_HandleWrapper): 9 | """ 10 | Container representing a task: 11 | It ensures that: start + duration = end 12 | """ 13 | 14 | def __init__(self, model: "_Model", start: "IntVar", duration: Union[int, "IntVar"], 15 | end: Union[None, "IntVar"] = None): 16 | """ 17 | Task constructor. Based on a starting time `start`, a duration `duration`, and 18 | optionally an ending time `end`, such that: `start` + `duration` = `end`. 19 | 20 | A call to ensure_bound_consistency() is required before launching the resolution, 21 | this will not be done automatically. 22 | 23 | Warning: it is recommended to instantiate variables through a Model object. 24 | 25 | :param model: A Choco Model. 26 | :param start: The starting time (IntVar). 27 | :param duration: The duration (int or IntVar). 28 | :param end: The ending time (IntVar, or None). 29 | """ 30 | self._has_monitor = True 31 | self._start = start 32 | self._duration = duration 33 | if end is None: 34 | if isinstance(duration, IntVar): 35 | self._end = model.intvar(start.get_lb() + duration.get_lb(), start.get_ub() + duration.get_ub()) 36 | handle = backend.create_task_iv_iv_iv(start._handle, duration._handle, self._end._handle) 37 | else: 38 | handle = backend.create_task_iv_i(start._handle, duration) 39 | self._has_monitor = False 40 | self._end = IntVar(backend.task_get_end(handle), model) 41 | self._duration = IntVar(backend.task_get_duration(handle), model) 42 | else: 43 | self._end = end 44 | if isinstance(duration, IntVar): 45 | handle = backend.create_task_iv_iv_iv(start._handle, duration._handle, end._handle) 46 | else: 47 | handle = backend.create_task_iv_i_iv(start._handle, duration, end._handle) 48 | self._duration = IntVar(backend.task_get_duration(handle), model) 49 | self._model = model 50 | super().__init__(handle) 51 | 52 | @property 53 | def start(self): 54 | """ 55 | :return: The integer variable corresponding to the start of this task. 56 | """ 57 | return self._start 58 | 59 | @property 60 | def end(self): 61 | """ 62 | :return: The integer variable corresponding to the end of this task. 63 | """ 64 | return self._end 65 | 66 | @property 67 | def duration(self): 68 | """ 69 | :return: The integer variable corresponding to the duration of this task. 70 | """ 71 | return self._duration 72 | 73 | def ensure_bound_consistency(self): 74 | """ 75 | Apply supplementary filtering to ensure bound consistency. 76 | """ 77 | if self._has_monitor: 78 | backend.task_ensure_bound_consistency(self._handle) 79 | -------------------------------------------------------------------------------- /tests/int_constraints/test_multi_cost_regular.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.automaton.cost_automaton import make_multi_resources 5 | from pychoco.objects.automaton.finite_automaton import FiniteAutomaton 6 | 7 | 8 | def make(period, seed): 9 | model = Model() 10 | sequence = model.intvars(period, 0, 2) 11 | bounds = [model.intvar(0, 80), model.intvar(0, 28), model.intvar(0, 28), model.intvar(0, 28)] 12 | auto = FiniteAutomaton() 13 | idx = auto.add_state() 14 | auto.set_initial_state(idx) 15 | auto.set_final(idx) 16 | idx = auto.add_state() 17 | day = 0 18 | auto.add_transition(auto.initial_state, idx, day) 19 | next_ = auto.add_state() 20 | night = 1 21 | auto.add_transition(idx, next_, day, night) 22 | rest = 2 23 | auto.add_transition(next_, auto.initial_state, rest) 24 | auto.add_transition(auto.initial_state, next_, night) 25 | cost_matrix = [] 26 | for i in range(0, period): 27 | a = [] 28 | for j in range(0, 3): 29 | b = [] 30 | for k in range(0, 4): 31 | if k == 0: 32 | if j == day: 33 | b.append([3, 5, 0]) 34 | elif j == night: 35 | b.append([8, 9, 0]) 36 | else: 37 | b.append([0, 0, 2]) 38 | elif k == 1: 39 | if j == day: 40 | b.append([1, 1, 0]) 41 | else: 42 | b.append([0, 0, 0]) 43 | elif k == 2: 44 | if j == night: 45 | b.append([1, 1, 0]) 46 | else: 47 | b.append([0, 0, 0]) 48 | else: 49 | if j == rest: 50 | b.append([1, 1, 0]) 51 | else: 52 | b.append([0, 0, 0]) 53 | a.append(b) 54 | cost_matrix.append(a) 55 | cost_automaton = make_multi_resources(auto, cost_matrix, bounds) 56 | model.multi_cost_regular(sequence, bounds, cost_automaton).post() 57 | model.get_solver().set_random_search(*(sequence + bounds), seed=seed) 58 | return model 59 | 60 | 61 | class TestMultiCostRegular(unittest.TestCase): 62 | 63 | def testMultiCostRegular1(self): 64 | seed = 0 65 | for i in range(0, 200): 66 | model = make(5, i + seed) 67 | while model.get_solver().solve(): 68 | pass 69 | self.assertEqual(model.get_solver().get_solution_count(), 4) 70 | 71 | def testMultiCostRegular2(self): 72 | seed = 0 73 | for i in range(0, 200): 74 | model = make(7, i + seed) 75 | while model.get_solver().solve(): 76 | pass 77 | self.assertEqual(model.get_solver().get_solution_count(), 6) 78 | 79 | def testMultiCostRegular3(self): 80 | seed = 0 81 | for i in range(0, 20): 82 | model = make(21, i + seed) 83 | while model.get_solver().solve(): 84 | pass 85 | self.assertEqual(model.get_solver().get_solution_count(), 85) 86 | -------------------------------------------------------------------------------- /pychoco/objects/automaton/cost_automaton.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from pychoco import backend 4 | from pychoco._utils import make_int_3d_array, make_intvar_array, make_int_4d_array, make_int_2d_array 5 | from pychoco.objects.automaton.finite_automaton import FiniteAutomaton 6 | from pychoco.variables.intvar import IntVar 7 | 8 | 9 | class CostAutomaton(FiniteAutomaton): 10 | """ 11 | Cost automaton. 12 | """ 13 | 14 | def __init__(self, automaton: Union[FiniteAutomaton, None] = None, _handle=None): 15 | """ 16 | Cost automaton constructor. 17 | 18 | :param automaton: Another automaton (optional). 19 | """ 20 | if _handle is not None: 21 | super().__init__(_handle=_handle) 22 | else: 23 | if automaton is None: 24 | handle = backend.create_cost_fa() 25 | else: 26 | handle = backend.create_cost_fa_from_fa(automaton._handle) 27 | super().__init__(_handle=handle) 28 | 29 | def add_counter_state(self, layer_value_state: List[List[List[int]]], min_bound: int, max_bound: int): 30 | """ 31 | Add a counter state to this cost automaton. 32 | 33 | :param layer_value_state: 34 | :param min_bound: 35 | :param max_bound: 36 | """ 37 | layer_value_state_handle = make_int_3d_array(layer_value_state) 38 | counter_handle = backend.create_counter_state(layer_value_state_handle, min_bound, max_bound) 39 | backend.cost_fa_add_counter(self._handle, counter_handle) 40 | 41 | 42 | def make_single_resource(automaton: FiniteAutomaton, costs: Union[List[List[int]], List[List[List[int]]]], inf: int, 43 | sup: int): 44 | """ 45 | :param automaton: A finite automaton. 46 | :param costs: Costs (2 or 3 dimensional int matrix). 47 | :param inf: Lower bound. 48 | :param sup: Upper bound. 49 | :return: A cost automaton from a finite automaton and costs. 50 | """ 51 | assert len(costs) > 0 52 | c1 = costs[0] 53 | assert len(c1) > 0 54 | c2 = c1[0] 55 | if isinstance(c2, list): 56 | assert len(c2) > 0 57 | handle = backend.make_single_resource_iii(automaton._handle, make_int_3d_array(costs), inf, sup) 58 | else: 59 | handle = backend.make_single_resource_ii(automaton._handle, make_int_2d_array(costs), inf, sup) 60 | return CostAutomaton(_handle=handle) 61 | 62 | 63 | def make_multi_resources(automaton: FiniteAutomaton, costs: Union[List[List[List[int]]], List[List[List[List[int]]]]], 64 | bounds: List[IntVar]): 65 | """ 66 | :param automaton: A finite automaton. 67 | :param costs: Costs (3 or 4 dimensional int matrix). 68 | :param bounds: List of IntVars defining bounds. 69 | :return: A multi cost automaton from a finite automaton and costs. 70 | """ 71 | assert len(costs) > 0 72 | c1 = costs[0] 73 | assert len(c1) > 0 74 | c2 = c1[0] 75 | assert len(c2) > 0 76 | c3 = c2[0] 77 | if isinstance(c3, list): 78 | assert len(c3) > 0 79 | handle = backend.make_multi_resources_iiii(automaton._handle, make_int_4d_array(costs), 80 | make_intvar_array(bounds)) 81 | else: 82 | handle = backend.make_multi_resources_iii(automaton._handle, make_int_3d_array(costs), 83 | make_intvar_array(bounds)) 84 | return CostAutomaton(_handle=handle) 85 | -------------------------------------------------------------------------------- /tests/test_search_strategies.py: -------------------------------------------------------------------------------- 1 | import math 2 | import unittest 3 | 4 | from pychoco.model import Model 5 | 6 | 7 | class TestSearchStrategies(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.model = Model() 11 | self.vars = self.model.intvars(5, 0, 10) 12 | self.model.all_different(self.vars).post() 13 | self.obj = self.model.intvar(0, 5 * 10) 14 | self.model.sum(self.vars, "=", self.obj).post() 15 | 16 | def test_default_search(self): 17 | self.model.get_solver().set_default_search() 18 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 19 | 20 | def test_dom_over_w_deg_search(self): 21 | self.model.get_solver().set_dom_over_w_deg_search(self.vars) 22 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 23 | 24 | def test_dom_over_w_deg_ref_search(self): 25 | self.model.get_solver().set_dom_over_w_deg_ref_search(*self.vars) 26 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 27 | 28 | def test_dom_over_w_deg_ref_search(self): 29 | self.model.get_solver().set_dom_over_w_deg_ref_search(*self.vars) 30 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 31 | 32 | def test_activity_based_search(self): 33 | self.model.get_solver().show_restarts() 34 | self.model.get_solver().set_activity_based_search(*self.vars) 35 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 36 | 37 | def test_min_dom_lb_search(self): 38 | self.model.get_solver().set_min_dom_lb_search(*self.vars) 39 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 40 | 41 | def test_min_dom_ub_search(self): 42 | self.model.get_solver().set_min_dom_ub_search(*self.vars) 43 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 44 | 45 | def test_random_search(self): 46 | self.model.get_solver().set_random_search(*self.vars) 47 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 48 | 49 | def test_conflict_history_search(self): 50 | self.model.get_solver().set_conflict_history_search(*self.vars) 51 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 52 | 53 | def test_input_order_lb_search(self): 54 | self.model.get_solver().set_input_order_lb_search(*self.vars) 55 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 56 | 57 | def test_input_order_ub_search(self): 58 | self.model.get_solver().set_input_order_ub_search(*self.vars) 59 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 60 | 61 | def test_failure_length_based_search(self): 62 | self.model.get_solver().set_failure_length_based_search(*self.vars) 63 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 64 | 65 | def test_failure_rate_based_search(self): 66 | self.model.get_solver().set_failure_rate_based_search(*self.vars) 67 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 68 | 69 | def test_pick_on_dom_search(self): 70 | self.model.get_solver().set_pick_on_dom_search(*self.vars) 71 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) 72 | 73 | def test_pick_on_fil_search(self): 74 | self.model.get_solver().set_pick_on_fil_search(*self.vars) 75 | self.model.get_solver().find_optimal_solution(objective=self.obj, maximize=True) -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: 4 | push: 5 | branches: [ "master", "dev_actions" ] 6 | tags: [ '**' ] 7 | 8 | jobs: 9 | build-macos: 10 | runs-on: ${{ matrix.os }} 11 | timeout-minutes: 20 12 | strategy: 13 | matrix: 14 | include: 15 | - os: "macos-13" 16 | arch: x86_64 17 | python-version: "3.11" 18 | CIBW_ENVIRONMENT: MACOSX_DEPLOYMENT_TARGET=15.0 19 | - os: "macos-14" 20 | arch: arm64 21 | python-version: "3.11" 22 | CIBW_ENVIRONMENT: MACOSX_DEPLOYMENT_TARGET=15.0 23 | - os: "macos-15" 24 | arch: arm64 25 | python-version: "3.11" 26 | CIBW_ENVIRONMENT: MACOSX_DEPLOYMENT_TARGET=15.0 27 | steps: 28 | - name: Print system info 29 | run: echo $(uname -o) $(uname -r) $(uname -m) 30 | - name: Check out repository 31 | uses: actions/checkout@v4 32 | with: 33 | submodules: recursive 34 | - name: Setup python 35 | uses: actions/setup-python@v5 36 | with: 37 | python-version: ${{ matrix.python-version }} 38 | architecture: x64 39 | cache: 'pip' 40 | cache-dependency-path: '**/requirements*.txt' 41 | - name: Install setuptools (needed from Python 3.12) 42 | run: pip install setuptools 43 | - name: Install Swig 44 | run: brew install swig 45 | - name: Build choco-solver-capi 46 | uses: ./.github/actions/build-choco-solver-capi 47 | with: 48 | os: ${{ runner.os }} 49 | arch: ${{ matrix.arch }} 50 | - name: Build pychoco 51 | run: | 52 | sh build.sh nocapibuild nowheel 53 | pip install pychoco -f dist/ 54 | - name: Test 55 | run: | 56 | pip install pytest 57 | pip install -r requirements_tests.txt 58 | pytest 59 | - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 60 | name: Build wheels 61 | uses: pypa/cibuildwheel@v2.21.1 62 | env: 63 | CIBW_ARCHS: ${{ matrix.arch }} 64 | CIBW_ENVIRONMENT: ${{ matrix.CIBW_ENVIRONMENT }} 65 | with: 66 | output-dir: dist 67 | - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 68 | name: Upload wheel artifacts 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: wheel-${{matrix.os}}-${{matrix.python-version}}-${{matrix.arch}}-artifact 72 | path: dist/ 73 | if-no-files-found: error 74 | 75 | upload-pypi-macos: 76 | needs: build-macos 77 | runs-on: ubuntu-latest 78 | if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 79 | strategy: 80 | matrix: 81 | include: 82 | - os: "macos-13" 83 | arch: x86_64 84 | python-version: "3.11" 85 | - os: "macos-14" 86 | arch: arm64 87 | python-version: "3.11" 88 | - os: "macos-15" 89 | arch: arm64 90 | python-version: "3.11" 91 | steps: 92 | - name: Download artifacts 93 | uses: actions/download-artifact@v4 94 | with: 95 | name: wheel-${{matrix.os}}-${{matrix.python-version}}-${{matrix.arch}}-artifact 96 | path: dist 97 | - name: List files 98 | run: ls -R 99 | - name: Upload to PyPi 100 | uses: pypa/gh-action-pypi-publish@release/v1 101 | with: 102 | user: __token__ 103 | password: ${{ secrets.PYPI_API_TOKEN }} 104 | verbose: true 105 | skip-existing: true 106 | -------------------------------------------------------------------------------- /tests/test_graphvar.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import DirectedGraph 5 | from pychoco.objects.graphs.directed_graph import create_directed_graph 6 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph, UndirectedGraph 7 | 8 | 9 | class TestGraphVar(unittest.TestCase): 10 | 11 | def test_undirected_graph_var(self): 12 | model = Model("MyModel") 13 | lb = create_undirected_graph(model, 10, [], []) 14 | ub = create_undirected_graph(model, 10, [0, 1, 2, 3], [[0, 1], [1, 3], [3, 2]]) 15 | graphvar = model.graphvar(lb, ub, "g") 16 | llb = graphvar.get_lb() 17 | uub = graphvar.get_ub() 18 | self.assertIsInstance(llb, UndirectedGraph) 19 | self.assertIsInstance(uub, UndirectedGraph) 20 | self.assertFalse(graphvar.is_directed()) 21 | self.assertEqual(len(llb.get_nodes()), 0) 22 | self.assertEqual(len(uub.get_nodes()), 4) 23 | self.assertEqual(graphvar.get_type(), "UndirectedGraphVar") 24 | g2 = model.graphvar(lb, lb, "g2") 25 | self.assertTrue(g2.is_instantiated()) 26 | self.assertEqual(g2.get_value().get_nodes(), []) 27 | 28 | def test_node_induced_graph_var(self): 29 | model = Model("MyModel") 30 | lb = create_undirected_graph(model, 10, [], []) 31 | ub = create_undirected_graph(model, 10, [0, 1, 2, 3], [[0, 1], [1, 3], [3, 2]]) 32 | graphvar = model.node_induced_graphvar(lb, ub, "g") 33 | llb = graphvar.get_lb() 34 | uub = graphvar.get_ub() 35 | self.assertIsInstance(llb, UndirectedGraph) 36 | self.assertIsInstance(uub, UndirectedGraph) 37 | self.assertFalse(graphvar.is_directed()) 38 | self.assertEqual(len(llb.get_nodes()), 0) 39 | self.assertEqual(len(uub.get_nodes()), 4) 40 | self.assertEqual(graphvar.get_type(), "UndirectedGraphVar") 41 | g2 = model.node_induced_graphvar(lb, lb, "g2") 42 | self.assertTrue(g2.is_instantiated()) 43 | self.assertEqual(g2.get_value().get_nodes(), []) 44 | 45 | def test_directed_graph_var(self): 46 | model = Model("MyModel") 47 | lb = create_directed_graph(model, 10, [], []) 48 | ub = create_directed_graph(model, 10, [0, 1, 2, 3], [[0, 1], [1, 3], [3, 2]]) 49 | graphvar = model.digraphvar(lb, ub, "g") 50 | llb = graphvar.get_lb() 51 | uub = graphvar.get_ub() 52 | self.assertIsInstance(llb, DirectedGraph) 53 | self.assertIsInstance(uub, DirectedGraph) 54 | self.assertTrue(graphvar.is_directed()) 55 | self.assertEqual(len(llb.get_nodes()), 0) 56 | self.assertEqual(len(uub.get_nodes()), 4) 57 | self.assertEqual(graphvar.get_type(), "DirectedGraphVar") 58 | g2 = model.digraphvar(lb, lb, "g2") 59 | self.assertTrue(g2.is_instantiated()) 60 | self.assertEqual(g2.get_value().get_nodes(), []) 61 | 62 | def test_node_induced_digraph_var(self): 63 | model = Model("MyModel") 64 | lb = create_directed_graph(model, 10, [], []) 65 | ub = create_directed_graph(model, 10, [0, 1, 2, 3], [[0, 1], [1, 3], [3, 2]]) 66 | graphvar = model.node_induced_digraphvar(lb, ub, "g") 67 | llb = graphvar.get_lb() 68 | uub = graphvar.get_ub() 69 | self.assertIsInstance(llb, DirectedGraph) 70 | self.assertIsInstance(uub, DirectedGraph) 71 | self.assertTrue(graphvar.is_directed()) 72 | self.assertEqual(len(llb.get_nodes()), 0) 73 | self.assertEqual(len(uub.get_nodes()), 4) 74 | self.assertEqual(graphvar.get_type(), "DirectedGraphVar") 75 | g2 = model.node_induced_digraphvar(lb, lb, "g2") 76 | self.assertTrue(g2.is_instantiated()) 77 | self.assertEqual(g2.get_value().get_nodes(), []) 78 | -------------------------------------------------------------------------------- /tests/test_graph.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.graphs.directed_graph import DirectedGraph 5 | from pychoco.objects.graphs.directed_graph import create_directed_graph 6 | from pychoco.objects.graphs.undirected_graph import UndirectedGraph 7 | from pychoco.objects.graphs.undirected_graph import create_undirected_graph 8 | 9 | 10 | class TestGraph(unittest.TestCase): 11 | 12 | def test_undirected_graph(self): 13 | model = Model() 14 | g = UndirectedGraph(model, 10, "BITSET", "BIPARTITE_SET") 15 | self.assertFalse(g.is_directed()) 16 | self.assertEqual(g.get_nb_max_nodes(), 10) 17 | self.assertEqual(len(g.get_nodes()), 0) 18 | self.assertEqual(g.get_node_set_type(), "BITSET") 19 | self.assertEqual(g.get_edge_set_type(), "BIPARTITE_SET") 20 | self.assertTrue(g.add_node(0)) 21 | self.assertTrue(g.add_node(1)) 22 | self.assertTrue(g.add_node(2)) 23 | self.assertTrue(g.contains_node(0) and g.contains_node(1) and g.contains_node(2)) 24 | self.assertEqual(set(g.get_nodes()), set([0, 1, 2])) 25 | self.assertFalse(g.contains_node(4)) 26 | self.assertFalse(g.contains_edge(0, 1)) 27 | self.assertTrue(g.add_edge(0, 1)) 28 | self.assertTrue(g.contains_edge(0, 1)) 29 | self.assertTrue(g.remove_node(2)) 30 | self.assertFalse(g.remove_node(2)) 31 | self.assertFalse(g.contains_node(2)) 32 | self.assertEqual(g.get_neighbors_of(0), [1]) 33 | self.assertTrue(g.remove_edge(1, 0)) 34 | self.assertEqual(g.get_neighbors_of(0), []) 35 | g.graphviz_export() 36 | 37 | def test_directed_graph(self): 38 | model = Model() 39 | g = DirectedGraph(model, 10, "RANGE_SET", "BIPARTITE_SET") 40 | self.assertTrue(g.is_directed()) 41 | self.assertEqual(g.get_nb_max_nodes(), 10) 42 | self.assertEqual(len(g.get_nodes()), 0) 43 | self.assertEqual(g.get_node_set_type(), "RANGE_SET") 44 | self.assertEqual(g.get_edge_set_type(), "BIPARTITE_SET") 45 | self.assertTrue(g.add_node(0)) 46 | self.assertTrue(g.add_node(1)) 47 | self.assertTrue(g.add_node(2)) 48 | self.assertTrue(g.contains_node(0) and g.contains_node(1) and g.contains_node(2)) 49 | self.assertEqual(set(g.get_nodes()), set([0, 1, 2])) 50 | self.assertFalse(g.contains_node(4)) 51 | self.assertFalse(g.contains_edge(0, 1)) 52 | self.assertTrue(g.add_edge(0, 1)) 53 | self.assertTrue(g.contains_edge(0, 1)) 54 | self.assertTrue(g.remove_node(2)) 55 | self.assertFalse(g.remove_node(2)) 56 | self.assertFalse(g.contains_node(2)) 57 | self.assertEqual(g.get_successors_of(0), [1]) 58 | self.assertEqual(g.get_successors_of(1), []) 59 | self.assertEqual(g.get_predecessors_of(1), [0]) 60 | self.assertFalse(g.remove_edge(1, 0)) 61 | self.assertTrue(g.remove_edge(0, 1)) 62 | self.assertEqual(g.get_successors_of(0), []) 63 | g.graphviz_export() 64 | 65 | def test_factory_methods(self): 66 | m = Model() 67 | g1 = create_undirected_graph(m, 10, [0, 1, 2, 3], [[0, 1], [1, 3], [3, 2]]) 68 | self.assertFalse(g1.is_directed()) 69 | self.assertSetEqual(set(g1.get_nodes()), set([0, 1, 2, 3])) 70 | self.assertTrue(g1.contains_edge(0, 1)) 71 | self.assertTrue(g1.contains_edge(1, 3)) 72 | self.assertTrue(g1.contains_edge(3, 2)) 73 | g2 = create_directed_graph(m, 10, [0, 1, 2, 3], [[0, 1], [1, 3], [3, 2]]) 74 | self.assertTrue(g2.is_directed()) 75 | self.assertSetEqual(set(g1.get_nodes()), set([0, 1, 2, 3])) 76 | self.assertTrue(g1.contains_edge(0, 1)) 77 | self.assertTrue(g1.contains_edge(1, 3)) 78 | self.assertTrue(g1.contains_edge(3, 2)) 79 | -------------------------------------------------------------------------------- /tests/int_constraints/test_cost_regular.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from pychoco.model import Model 4 | from pychoco.objects.automaton.cost_automaton import CostAutomaton, make_single_resource 5 | from pychoco.objects.automaton.finite_automaton import FiniteAutomaton 6 | 7 | 8 | class TestCostRegular(unittest.TestCase): 9 | 10 | def testCostRegular1(self): 11 | m = Model() 12 | n = 10 13 | intvars = m.intvars(10, 0, 2) 14 | cost = m.intvar(3, 4) 15 | auto = CostAutomaton() 16 | start = auto.add_state() 17 | end = auto.add_state() 18 | auto.set_initial_state(start) 19 | auto.set_final(start) 20 | auto.set_final(end) 21 | auto.add_transition(start, start, 0, 1) 22 | auto.add_transition(start, end, 2) 23 | auto.add_transition(end, start, 2) 24 | auto.add_transition(end, start, 0, 1) 25 | costs = [] 26 | for i in range(0, n): 27 | costs.append([[0, 1], [0, 1], [0, 0]]) 28 | auto.add_counter_state(costs, 3, 4) 29 | m.cost_regular(intvars, cost, auto).post() 30 | while m.get_solver().solve(): 31 | pass 32 | self.assertEqual(m.get_solver().get_solution_count(), 9280) 33 | 34 | def testCostRegular2(self): 35 | model = Model() 36 | n = 10 37 | intvars = model.intvars(n, 0, 2) 38 | cost = model.intvar(3, 4) 39 | auto = FiniteAutomaton() 40 | start = auto.add_state() 41 | end = auto.add_state() 42 | auto.set_initial_state(start) 43 | auto.set_final(start) 44 | auto.set_final(end) 45 | auto.add_transition(start, start, 0, 1) 46 | auto.add_transition(start, end, 2) 47 | auto.add_transition(end, start, 2) 48 | auto.add_transition(end, start, 0, 1) 49 | costs = [] 50 | for i in range(0, n): 51 | costs.append([[0, 1], [0, 1], [0, 0]]) 52 | model.cost_regular(intvars, cost, make_single_resource(auto, costs, cost.get_lb(), cost.get_ub())).post() 53 | model.get_solver().set_input_order_lb_search(*intvars) 54 | while model.get_solver().solve(): 55 | pass 56 | self.assertEqual(model.get_solver().get_solution_count(), 9280) 57 | 58 | def testCostRegular3(self): 59 | model = Model() 60 | n = 28 61 | intvars = model.intvars(n, 0, 2) 62 | cost = model.intvar(0, 4) 63 | forbidden_regexps = [ 64 | # do not end with '00' if start with '11' 65 | "11(0|1|2)*00", 66 | # at most three consecutive 0 67 | "(0|1|2)*0000(0|1|2)*", 68 | # no pattern '112' at position 5 69 | "(0|1|2){4}112(0|1|2)*", 70 | # pattern '12' after a 0 or a sequence of 0 71 | "(0|1|2)*02(0|1|2)*", 72 | "(0|1|2)*01(0|1)(0|1|2)*", 73 | # at most three 2 on consecutive even positions 74 | "(0|1|2)((0|1|2)(0|1|2))*2(0|1|2)2(0|1|2)2(0|1|2)*" 75 | ] 76 | # a unique automaton is built as the complement language composed of all the forbidden patterns 77 | auto = FiniteAutomaton() 78 | for reg in forbidden_regexps: 79 | a = FiniteAutomaton(reg) 80 | auto = auto.union(a) 81 | auto.minimize() 82 | auto = auto.complement() 83 | auto.minimize() 84 | self.assertEqual(auto.nb_states, 54) 85 | # costs 86 | costs = [] 87 | for i in range(0, n): 88 | costs.append([0, 0, 0]) 89 | for j in range(1, n, 2): 90 | costs[j][0] = 1 91 | costs[j][1] = 1 92 | model.cost_regular(intvars, cost, make_single_resource(auto, costs, cost.get_lb(), cost.get_ub())).post() 93 | model.get_solver().set_input_order_lb_search(*intvars) 94 | while model.get_solver().solve(): 95 | pass 96 | self.assertEqual(model.get_solver().get_solution_count(), 229376) 97 | -------------------------------------------------------------------------------- /pychoco/constraints/constraint.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from pychoco import backend 4 | from pychoco._handle_wrapper import _HandleWrapper 5 | from pychoco.utils import ESat 6 | from pychoco.variables.boolvar import BoolVar 7 | 8 | 9 | class Constraint(_HandleWrapper): 10 | """ 11 | A constraint is a logic formula defining allowed combinations of values for a set of 12 | variables, i.e., restrictions over variables that must be respected in order to get 13 | a feasible solution. A constraint is equipped with a (set of) filtering algorithm(s), 14 | named propagator(s). A propagator removes, from the domains of the target variables, 15 | values that cannot correspond to a valid combination of values. A solution of a 16 | problem is a variable-value assignment verifying all the constraints. 17 | 18 | To be effective, a constraint must be either posted or reified. 19 | """ 20 | 21 | def __init__(self, handle: "SwigPyObject", model: "_Model"): 22 | """ 23 | Warning: Not intended to be used by users, use a Model object to instantiate constraints instead. 24 | """ 25 | super().__init__(handle) 26 | self._model = model 27 | 28 | def get_name(self): 29 | """ 30 | :return: The name of the constraint. 31 | """ 32 | return backend.get_constraint_name(self._handle) 33 | 34 | @property 35 | def model(self): 36 | """ 37 | :return: The model associated with this constraint. 38 | """ 39 | return self._model 40 | 41 | @abstractmethod 42 | def post(self): 43 | """ 44 | :return: Post the constraint. 45 | """ 46 | backend.post(self._handle) 47 | 48 | @abstractmethod 49 | def reify(self): 50 | """ 51 | Reifies the constraint, i.e. return a boolvar whose instantiation in a solution 52 | correspond to the satisfaction state of the constraint in this solution. 53 | 54 | :return: A BoolVar. 55 | """ 56 | var_handle = backend.reify(self._handle) 57 | return BoolVar(var_handle, self.model) 58 | 59 | @abstractmethod 60 | def reify_with(self, boolvar): 61 | """ 62 | Reifies the constraint with a given boolvar whose instantiation in a solution 63 | correspond to the satisfaction state of the constraint in this solution. 64 | """ 65 | backend.reify_with(self._handle, boolvar._handle) 66 | 67 | 68 | @abstractmethod 69 | def implies(self, boolvar): 70 | """ 71 | Encapsulate this constraint in an implication relationship. 72 | The truth value of this constraints implies the truth value of te boolvar. 73 | """ 74 | backend.implies(self._handle, boolvar._handle) 75 | 76 | @abstractmethod 77 | def implied_by(self, boolvar): 78 | """ 79 | Encapsulate this constraint in an implication relationship. 80 | Represents half-reification of the constraint. 81 | """ 82 | backend.implied_by(self._handle, boolvar._handle) 83 | 84 | @abstractmethod 85 | def is_satisfied(self): 86 | """ 87 | Check whether the constraint is satisfied (ESat.TRUE), not satisfied (ESat.FALSE), 88 | or if it is not yet possible to define whether it is satisfied or not (ESat.UNDEFINED). 89 | **Note:** this method is used internally by Choco, but it can be useful to verify whether 90 | a constraint is satisfied (or not) regardless of the variables' instantiation. 91 | :return: The satisfaction state of the constraint. 92 | """ 93 | state = backend.is_satisfied(self._handle) 94 | if state == 0: 95 | return ESat.FALSE 96 | if state == 1: 97 | return ESat.TRUE 98 | return ESat.UNDEFINED 99 | 100 | def __repr__(self): 101 | return "Choco Constraint ('" + self.get_name() + "')" 102 | -------------------------------------------------------------------------------- /docs/api/views.rst: -------------------------------------------------------------------------------- 1 | .. _views: 2 | 3 | Views 4 | ===== 5 | 6 | The concept of views in Constraint Programming is halfway between variables and constraints. 7 | Specifically, a view is a special kind of variable that does not declare any domain, but instead relies on one or 8 | several other variables through a logical relation. From a modelling perspective, a view can be manipulated exactly 9 | as any other variable. In pychoco, the only difference that you will notice is that the `is_view()` method will 10 | return True when a variable is actually a view. 11 | 12 | Views are directly declared from a `Model` object (see :ref:`model`). 13 | 14 | .. py:currentmodule:: pychoco.variables.view_factory.ViewFactory 15 | 16 | Boolean views 17 | ------------- 18 | 19 | Boolean view can be declared over several types of variables, and behave as Boolean variables. 20 | 21 | bool_not_view 22 | ^^^^^^^^^^^^^ 23 | 24 | .. autofunction:: bool_not_view 25 | :noindex: 26 | 27 | set_bool_view 28 | ^^^^^^^^^^^^^ 29 | 30 | .. autofunction:: set_bool_view 31 | :noindex: 32 | 33 | set_bools_view 34 | ^^^^^^^^^^^^^^ 35 | 36 | .. autofunction:: set_bools_view 37 | :noindex: 38 | 39 | Integer views 40 | ------------- 41 | 42 | Integer view can be declared over several types of variables, and behave as Integer variables. 43 | 44 | int_offset_view 45 | ^^^^^^^^^^^^^^^ 46 | 47 | .. autofunction:: int_offset_view 48 | :noindex: 49 | 50 | int_minus_view 51 | ^^^^^^^^^^^^^^ 52 | 53 | .. autofunction:: int_minus_view 54 | :noindex: 55 | 56 | int_scale_view 57 | ^^^^^^^^^^^^^^ 58 | 59 | .. autofunction:: int_scale_view 60 | :noindex: 61 | 62 | int_abs_view 63 | ^^^^^^^^^^^^ 64 | 65 | .. autofunction:: int_abs_view 66 | :noindex: 67 | 68 | int_affine_view 69 | ^^^^^^^^^^^^^^^ 70 | 71 | .. autofunction:: int_affine_view 72 | :noindex: 73 | 74 | int_eq_view 75 | ^^^^^^^^^^^ 76 | 77 | .. autofunction:: int_eq_view 78 | :noindex: 79 | 80 | int_ne_view 81 | ^^^^^^^^^^^ 82 | 83 | .. autofunction:: int_ne_view 84 | :noindex: 85 | 86 | int_le_view 87 | ^^^^^^^^^^^ 88 | 89 | .. autofunction:: int_le_view 90 | :noindex: 91 | 92 | int_ge_view 93 | ^^^^^^^^^^^ 94 | 95 | .. autofunction:: int_ge_view 96 | :noindex: 97 | 98 | Set views 99 | --------- 100 | 101 | Set view can be declared over several types of variables, and behave as Set variables. 102 | 103 | bools_set_view 104 | ^^^^^^^^^^^^^^ 105 | 106 | .. autofunction:: bools_set_view 107 | :noindex: 108 | 109 | 110 | ints_set_view 111 | ^^^^^^^^^^^^^ 112 | 113 | .. autofunction:: ints_set_view 114 | :noindex: 115 | 116 | set_union_view 117 | ^^^^^^^^^^^^^^ 118 | 119 | .. autofunction:: set_union_view 120 | :noindex: 121 | 122 | set_intersection_view 123 | ^^^^^^^^^^^^^^^^^^^^^ 124 | 125 | .. autofunction:: set_intersection_view 126 | :noindex: 127 | 128 | set_difference_view 129 | ^^^^^^^^^^^^^^^^^^^ 130 | 131 | .. autofunction:: set_difference_view 132 | :noindex: 133 | 134 | graph_node_set_view 135 | ^^^^^^^^^^^^^^^^^^^ 136 | 137 | .. autofunction:: graph_node_set_view 138 | :noindex: 139 | 140 | graph_successors_set_view 141 | ^^^^^^^^^^^^^^^^^^^^^^^^^ 142 | 143 | .. autofunction:: graph_successors_set_view 144 | :noindex: 145 | 146 | graph_predecessors_set_view 147 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 148 | 149 | .. autofunction:: graph_predecessors_set_view 150 | :noindex: 151 | 152 | graph_neighbors_set_view 153 | ^^^^^^^^^^^^^^^^^^^^^^^^ 154 | 155 | .. autofunction:: graph_neighbors_set_view 156 | :noindex: 157 | 158 | 159 | Graph views 160 | ----------- 161 | 162 | node_induced_subgraph_view 163 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 164 | 165 | .. autofunction:: node_induced_subgraph_view 166 | :noindex: 167 | 168 | edge_induced_subgraph_view 169 | ^^^^^^^^^^^^^^^^^^^^^^^^^^ 170 | 171 | .. autofunction:: edge_induced_subgraph_view 172 | :noindex: 173 | 174 | graph_union_view 175 | ^^^^^^^^^^^^^^^^ 176 | 177 | .. autofunction:: graph_union_view 178 | :noindex: 179 | -------------------------------------------------------------------------------- /pychoco/constraints/cnf/log_op.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from pychoco import backend 4 | from pychoco._handle_wrapper import _HandleWrapper 5 | from pychoco._utils import make_logical_array 6 | from pychoco.variables.boolvar import BoolVar 7 | 8 | 9 | class LogOp(_HandleWrapper): 10 | """ 11 | Logical operator, to ease clause definition 12 | """ 13 | 14 | def __init__(self, handle: "SwigPyObject"): 15 | super().__init__(handle) 16 | 17 | 18 | def and_op(*logops: List[Union[LogOp, BoolVar]]): 19 | """ 20 | Create a conjunction, results in true if all of its operands are true. 21 | :param logops: A list of LogOp/Boolvar. 22 | """ 23 | if len(logops) == 1 and isinstance(logops[0], list): 24 | logs = logops[0] 25 | else: 26 | logs = logops 27 | list_handle = make_logical_array(logs) 28 | handle = backend.and_op(list_handle) 29 | return LogOp(handle) 30 | 31 | 32 | def if_only_if_op(a: Union[LogOp, BoolVar], b: Union[LogOp, BoolVar]): 33 | """ 34 | Create a biconditional, results in true if and only if both operands are false or both operands are true. 35 | :param a: operand (LogOp/Boolvar). 36 | :param b: operand (LogOp/Boolvar). 37 | """ 38 | handle = backend.if_only_if_op(a._handle, b._handle) 39 | return LogOp(handle) 40 | 41 | 42 | def if_then_else_op(a: Union[LogOp, BoolVar], b: Union[LogOp, BoolVar], c: Union[LogOp, BoolVar]): 43 | """ 44 | Create an implication, results in true if a is true` and b is true or a is false and c is true. 45 | :param a: operand (LogOp/Boolvar). 46 | :param b: operand (LogOp/Boolvar). 47 | :param c: operand (LogOp/Boolvar). 48 | """ 49 | handle = backend.if_then_else_op(a._handle, b._handle, c._handle) 50 | return LogOp(handle) 51 | 52 | 53 | def implies_op(a: Union[LogOp, BoolVar], b: Union[LogOp, BoolVar]): 54 | """ 55 | Create an implication, results in true if a is false or b is true. 56 | :param a: operand (LogOp/Boolvar). 57 | :param b: operand (LogOp/Boolvar). 58 | """ 59 | handle = backend.implies_op(a._handle, b._handle) 60 | return LogOp(handle) 61 | 62 | 63 | def reified_op(b: BoolVar, tree: Union[LogOp, BoolVar]): 64 | """ 65 | create a logical connection between ``b`` and ``tree``. 66 | :param b: A BoolVar. 67 | :param tree: operand (LogOp/Boolvar). 68 | """ 69 | handle = backend.reified_op(b._handle, tree._handle) 70 | return LogOp(handle) 71 | 72 | 73 | def or_op(*logops: List[Union[LogOp, BoolVar]]): 74 | """ 75 | Create a disjunction, results in true whenever one or more of its operands are true. 76 | :param logops: A list of LogOp/Boolvar. 77 | """ 78 | if len(logops) == 1 and isinstance(logops[0], list): 79 | logs = logops[0] 80 | else: 81 | logs = logops 82 | list_handle = make_logical_array(logs) 83 | handle = backend.or_op(list_handle) 84 | return LogOp(handle) 85 | 86 | 87 | def nand_op(*logops: List[Union[LogOp, BoolVar]]): 88 | """ 89 | Create an alternative denial, results in if at least one of its operands is false. 90 | :param logops: A list of LogOp/Boolvar. 91 | """ 92 | if len(logops) == 1 and isinstance(logops[0], list): 93 | logs = logops[0] 94 | else: 95 | logs = logops 96 | list_handle = make_logical_array(logs) 97 | handle = backend.nand_op(list_handle) 98 | return LogOp(handle) 99 | 100 | 101 | def nor_op(*logops: List[Union[LogOp, BoolVar]]): 102 | """ 103 | Create a joint denial, results in `true` if all of its operands are false. 104 | :param logops: A list of LogOp/Boolvar. 105 | """ 106 | if len(logops) == 1 and isinstance(logops[0], list): 107 | logs = logops[0] 108 | else: 109 | logs = logops 110 | list_handle = make_logical_array(logs) 111 | handle = backend.nor_op(list_handle) 112 | return LogOp(handle) 113 | 114 | 115 | def xor_op(a: Union[LogOp, BoolVar], b: Union[LogOp, BoolVar]): 116 | """ 117 | Create an exclusive disjunction, results in true whenever both operands differ. 118 | :param a: operand (LogOp/Boolvar). 119 | :param b: operand (LogOp/Boolvar). 120 | """ 121 | handle = backend.xor_op(a._handle, b._handle) 122 | return LogOp(handle) 123 | --------------------------------------------------------------------------------