{"id":26187028,"url":"https://github.com/pjritee/pl_search","last_synced_at":"2025-07-03T12:04:40.176Z","repository":{"id":168080414,"uuid":"606606599","full_name":"pjritee/pl_search","owner":"pjritee","description":"A Python module that uses Prolog ideas for search and constraint programming","archived":false,"fork":false,"pushed_at":"2025-03-01T06:36:32.000Z","size":213,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-11T23:37:37.657Z","etag":null,"topics":["backtracking-search","constraint-logic-programming","constraint-programming","prolog","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pjritee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-02-26T01:41:02.000Z","updated_at":"2025-03-01T06:36:36.000Z","dependencies_parsed_at":null,"dependency_job_id":"44b047fd-22b1-4d24-8bae-0f882cd5bbb7","html_url":"https://github.com/pjritee/pl_search","commit_stats":null,"previous_names":["pjritee/pl_search"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/pjritee/pl_search","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjritee%2Fpl_search","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjritee%2Fpl_search/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjritee%2Fpl_search/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjritee%2Fpl_search/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pjritee","download_url":"https://codeload.github.com/pjritee/pl_search/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pjritee%2Fpl_search/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":263322786,"owners_count":23448712,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backtracking-search","constraint-logic-programming","constraint-programming","prolog","python"],"created_at":"2025-03-11T23:36:16.163Z","updated_at":"2025-07-03T12:04:40.154Z","avatar_url":"https://github.com/pjritee.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# A module for searching and constraint programming using Prolog ideas\n\nThere are several Python constraint solvers but it appears that they are tailored for specific domains such as integers. The aim of this module is to be quite generic and support searching and constraint solving over a wide range of domains including working with symbolic state and constraints. This module relies quite heavily on ideas from Prolog such as Prolog like variables, environments and trailing to support backtracking search.\n\nBeing more generic means that the programmer may have more work to do that in other constraint solvers.\n\nThe top-level docstring in `pl_search/__init__.py` gives details of the module and `examples/send_more_money.py` is an example of the use of this module. Further `test/test1.py` has a collection of various simple searches.\n\n## Installation\n\nThe simplest option is a direct installation from github:\n\n`python3 -m pip install -U git+https://github.com/pjritee/pl_search.git`\n\nThe other option is to clone the repository and then, from the top-level of the cloned repository, do a pip install:\n\n`python3 -m pip install -U .`\n\n## Example Usage\n\nTo illustrate the use of the module we work through a constraint handling solution to finding NxN magic squares.\nThe program described here is supplied in `examples/magic_squares.py`.\n\nThe first step is to import the module:\n\n```python\nimport pl_search as pls\n```\n\nWe first initialise some constants (for a 3x3 magic square) .\n\n```python\nN = 3\nN2 = N**2\n\n# each row, column, diagonal sum\nSUM = (N * (N2 + 1))//2\n\n# The square to be filled in with the numbers 1,2,3,.. N**2\nCHOICES = set(range(1,N2+1))\n\n```\nThe approach is to create a NxN array containing distinct variables and then use constraint programming and backtrack search to find appropriate values for these variables.\n\nWe could use `pls.Var()` but given we need to check that the variables\nhave to have disjoint values it's better to create a subclass as follows.\n\n```python\nclass MSVar(pls.Var):\n\n    def set_disjoint(self, disjoints):\n        self.disjoints = disjoints\n\n    def bind(self, n):\n        if n in self.disjoints or n not in CHOICES:\n            return False\n        return super().bind(n)\n\n    def get_choices(self):\n        known_disjoints = {pls.dereference(n) for n in self.disjoints\n                           if not pls.var(n)}\n        return CHOICES.difference(known_disjoints)\n```\nBy checking the value for the variable is a valid choice in `bind` we guarantee that the choice for the variable value satisfies the disjointness constraint and comes from the required set. We use `set_disjoint` to set the disjoint list to be all the variables (after all the variables have been created). Later when we start searching we will use `get_choices` to  an iterator to be used to backtrack through possible choices in the search predicate.\n\nWe can then create a list of variables, set their disjoints attribute and create a 3x3 array as follows.\n\n```python\nall_vars = [MSVar() for _ in range(N2)]\nfor v in all_vars:\n    v.set_disjoint(all_vars)\nsquare = [all_vars[i:i+N] for i in range(0, N2, N)]\n\n```\nIf we do that in the interpreter and then look at the value of square we get\n```python\n[[X01, X02, X03], [X04, X05, X06], [X07, X08, X09]]\n```\nwhere `X01` etc. are the string representations of the variables.\n\nThe next step is to consider how we represent the row, column and diagonal sum constraints. The approach taken here is to represent a constraint like\n\n`X01 + X02 + X03 = SUM`\n\nas the pair\n\n`([X01,X02, X03], SUM)`.\n\nAs we search for a solution, variables get bound to numbers and the constraints need to be checked and deductions made where possible. There are two approaches we could take. The first is to leave the constraints unchanged and check them and do deductions as variables get bound. The other approach is to simplify the constraints as we proceed. For this simple problem the first approach is viable but as the size and complexity of the constraints increase this approach will become very inefficient. We will take the second approach but we need to be careful because the simplifications that can be made are based on choices for variables and so when we backtrack we also have to undo simplifications to constraints. This can be managed using `UpdatableVar` to implement a form of backtrackable assignment.\n\nAgain, we have two approaches. The first is to have one `UpdatableVar` to store the entire list of constraints and the other is to use an `UpdatableVar` for each constraint. For the first approach, when we simplify the constraints we need to make a new list containing the update constraints.\nThis gives us a chance to remove solved constraints. The downside of this approach is that we end up with lots of near copies of the constraints list as we move forward in the search. The downside of the second approach is we don't get a chance to remove solved constraints.\n\nWe take the second approach and define\n\n```python\ndef generate_constraints(square):\n    \"\"\"Return the row, column and diagonal sum constraints.\"\"\"\n    constraints = \\\n        [pls.UpdatableVar(([square[i][j] for i in range(N)], SUM))\n         for j in range(N)] + \\\n        [pls.UpdatableVar(([square[j][i] for i in range(N)], SUM))\n         for j in range(N)] + \\\n        [pls.UpdatableVar(([square[i][i] for i in range(N)], SUM))] + \\\n        [pls.UpdatableVar(([square[i][N-1-i] for i in range(N)], SUM))]\n    return constraints\n```\nContinuing in the interpreter we get\n\n```\n\u003e\u003e\u003e generate_constraints(square)\n[UpdatableVar(([X01, X04, X07], 15)), UpdatableVar(([X02, X05, X08], 15)), UpdatableVar(([X03, X06, X09], 15)), UpdatableVar(([X01, X02, X03], 15)), UpdatableVar(([X04, X05, X06], 15)), UpdatableVar(([X07, X08, X09], 15)), UpdatableVar(([X01, X05, X09], 15)), UpdatableVar(([X03, X05, X07], 15))]\n\n```\nNow that we have generated the constraints we need to be able to test them and carry out any possible deductions and so we give the following definition.\n\n```python\ndef check_constraints(constraints):\n    \"\"\" check and simplify constraints.\n    Return True iff constraints are satisfiable.\n    \"\"\"\n    progress = True\n    while progress:\n        # keep repeating until no more 'useful' simplifications are possible\n        progress = False        \n        for c in constraints:\n            lhs, rhs = c.value\n            if lhs == [] and rhs == 0:\n                # solved constraint\n                continue\n            var_lhs = [x for x in lhs if  pls.var(x)]\n            ground_lhs = [pls.dereference(x) for x in lhs\n                          if not x in var_lhs]\n            new_rhs = rhs - sum(ground_lhs)\n            if var_lhs == []:\n                if new_rhs == 0:\n                    # newly solved constraint\n                    pls.engine.unify(c, ([], 0))\n                return False\n            if new_rhs \u003c 0:\n                # no solution is possible\n                return False\n            if len(var_lhs) == 1: # constraint is Var = new_rhs\n                progress = True\n                if not pls.engine.unify(var_lhs[0], new_rhs):\n                    # this fails when new_rhs is too big\n                    # or is already taken\n                    return False\n                # newly solved constraint\n                pls.engine.unify(c, ([], 0))\n            elif new_rhs != rhs:\n                # the constraint is simplified\n                progress = True\n                pls.engine.unify(c, (var_lhs, new_rhs))\n    return True\n\n```\n`pl_search` contains an `Engine` class and an instance `engine` of that class.\nThe `engine` object is responsible for executing predicates, including managing backtracking. Notice that we use `pls.engine.unify` rather than `bind`\nas `bind` does not trail the variable and so the binding would not be undone on backtracking. We also use `pls.dereference(x)` although in this case we could have used `x.deref()` because we know x is a variable (that might be bound). In general it's better to use `pls.dereference` as this also works as expected when the argument is not a variable.\n\nNote, above, that `c` is an `UpdatableVar` and `c.value` is the current values for `c` and `pls.engine.unify(c, (var_lhs, new_rhs))` assigns this new value to `c`. The old value of `c` is trailed so that, on backtracking, the old value will be restored.\n\nWe can test this in the interpreter as, for example:\n\n```python\n\u003e\u003e\u003e pls.engine.unify(all_vars[0], 8)\nTrue\n\u003e\u003e\u003e pls.engine.unify(all_vars[1], 1)\nTrue\n\u003e\u003e\u003e check_constraints(constraints)\nTrue\n\u003e\u003e\u003e constraints\n[UpdatableVar(([X04, X07], 7)), UpdatableVar(([X05, X08], 14)), UpdatableVar(([X06, X09], 9)), UpdatableVar(([], 0)), UpdatableVar(([X04, X05, X06], 15)), UpdatableVar(([X07, X08, X09], 15)), UpdatableVar(([X05, X09], 7)), UpdatableVar(([X05, X07], 9))]\n\u003e\u003e\u003e all_vars\n[8, 1, 6, X04, X05, X06, X07, X08, X09]\n```\nNotice that we were able to deduce `X03` must be 6 and that several of the constraints have been simplified.\n\nNow that we have the basic machinery we are ready to define predicates to carry out the search.\n\nWhen we execute (call) a predicate there are two possible outcomes. One is that the call fails and the other is that it succeeds. On failure, all bindings created during execute will be undone. If execute succeeds then, depending on how execute is called, all the bindings will be removed (the default) or\nall bindings will be kept. For a top-level call on execute it might be best to use the default so\nas bindings don't interfere with further computations. For calling execute within execute the default\nis like using the `NotNot` predicate while the alternative behaviour is like using a `Once` predicate.\n\nIf the default behaviour is used, in order to get solutions, we need to either print them\nor deal with solutions some other way inside a predicate - for example store them away, send them as a message or put them on a queue.\n\nBelow is a predicate definition that prints the array when called.\n\n```python  \nclass Print(pls.DetPred):\n    \"\"\"Pretty print the supplied array.\"\"\"\n\n    def __init__(self, array):\n        self.array = array\n\n    def initialize_call(self):\n        for j in range(N):\n            print(''.join(f'{str(pls.dereference(self.array[j][i])):\u003e5}'\n                           for i in range(N)))\n        print()\n```\n`Print` is declared as a `DetPred` which means it is deterministic - it has exactly one solution. We are required to define `initialize_call` that gets executed as soon as a `Print` predicate is called.\n\nWe can test this in the interpreter as follows (continuing on from the earlier interpreter interaction)\n\n```\n\u003e\u003e\u003e pls.engine.execute(Print(square))\n    8    1    6\n  X04  X05  X06\n  X07  X08  X09\nTrue\n```\n\nNow we are ready to write the code that will carry out the search.\n\nPseudo-code for this kind of problem is something like\n```\nwhile there are variables left\n    pick a variable\n    calculate the possible choices for the variable\n    make a choice for the variable\n    if the constraints are satisfiable then continue\n    else backtrack and make another choice\n```\nThis strategy can be implemented using the `Loop` meta-predicate. This takes a single argument that is a subclass of `LoopBodyFactory` with definitions for `loop_continues` (which returns True if the loop should continue) and `make_body_pred` that generates a predicate to be called in the body of the loop.\n\nSuitable definitions are given below (including `get_best_var`)\n\n```python\ndef get_best_var(all_vars):\n    \"\"\"Return the first remaining variable in all_vars else return None if\n    there are no more variables.\"\"\"\n    for v in all_vars:\n        if pls.var(v):\n            return v\n    return None\n\nclass BodyPred(pls.Pred):\n    def __init__(self, constrains, all_vars, best_var):\n        self.constraints = constraints\n        self.all_vars = all_vars\n        self.best_var = best_var\n\n    def initialize_call(self):\n        #required method and self.choice_iterator must be given a value\n        self.choice_iterator = \\\n            pls.VarChoiceIterator(self.best_var, self.best_var.get_choices())\n        return True\n\n    def test_choice(self):\n        # We need to check the constraints and carry out deductions so this\n        # method is required\n        return check_constraints(self.constraints)\n\nclass MSFactory(pls.LoopBodyFactory):\n\n    def __init__(self, constraints, all_vars):\n        self.constraints =  constraints\n        self.all_vars = all_vars\n\n    def loop_continues(self):\n        self.best_var = get_best_var(self.all_vars)\n        return self.best_var is not None\n\n    def make_body_pred(self) -\u003e pls.Pred:\n        return BodyPred(self.constraints, self.all_vars, self.best_var)\n\n```\nHere we take the simplest approach and choose the first remaining variable in `all_vars` for `get_best_var` but we probably should have chosen a variable from a constraint with the smallest left hand side.\n\nNow we can carry out the search. If we just want the first solution we can try:\n```\npls.engine.execute(pls.conjunct([pls.Loop(MSFactory(constraints, all_vars)), Print(square)])\n```\nand we will get the output\n```\n    2    7    6\n    9    5    1\n    4    3    8\n```\nOn the other hand, if we want all solutions we can try:\n```\npls.engine.execute(pls.conjunct([pls.Loop(MSFactory(constraints, all_vars)), Print(square), pls.fail])\n```\nAlso note that, for a single solution, we could have used\n\n```\npls.engine.execute(pls.Loop(MSFactory(constraints, all_vars)), False)\n```\nand printed out the solution after this as solution bindings will be preserved.\n\nThe meta-predicate `pls.conjunct` conjoins a list of predicates into one predicate by chaining the predicates continuations and is like conjunction in Prolog. The builtin predicate `pls.fail` simply fails, triggering backtracking (like fail in Prolog).\n\nFor this problem we use the builtin ```VarChoiceIterator``` because we simply need to try each choice for a given variable. In more complicated situations the programmer might need to define their own choice iterator.\n\nAs an example consider the case where we have a list of variables and we want the choices to be all possible sublists (without order) of some list of values. In ```test/test1.py``` we define the following.\n\n```python\nclass SetChoiceIterator:\n\n    def __init__(self, vars_, choices):\n        self.vars_ = vars_\n        n = len(vars_)\n        self.choices = itertools.combinations(choices, n)\n\n    def __iter__(self):\n        return self\n\n    def __next__(self):\n        return pls.VarChoice(self.vars_, list(next(self.choices)))\n\nclass SetChoicePred(pls.Pred):\n    def __init__(self, vars_, choices):\n        self.vars_ = vars_\n        self.choices = choices\n\n    def initialize_call(self):\n        self.choice_iterator = SetChoiceIterator(self.vars_, self.choices)\n\n```\nIn the above example we still use ```VarChoice``` as we are simply unifying two terms. In an even more sophisticated example, each choice might mean the addition of some sort of constraint. In that case we would need to define a choice iterator class  as well as a Choice class for adding the constraint.\n\n## TODO\nThere is now a C++ version of pl_search in the pl_search_cpp repository. Improvements made in that version need to be applied to this version.\n\n## Version History\n* 1.15\n  - Add a reset method to Engine that does a full backtrack and removes all entries on the environment stack.\n* 1.14\n  - Update execute so that execute can be called within an outer execute.\n  - Add a flag for execute so that bindings can be kept when execute terminates.\n* 1.13\n  - Clean up continuation setter code.\n* 1.12\n  - Removing the need for the EXIT status means that the call status can be replaced by a boolean.\n* 1.11\n  - Re-factor code - split code into multiple files, change dereference from a method to a function\n* 1.10\n  - Fix problem when calculating continuations for more complex examples typically containing a conjunction within another predicate.\n  - Add the NotNot meta-predicate\n  - Update the test program\n* 1.9\n  - Simplify SemiDetPred and DetPred\n* 1.8\n  - Carry out some minor optimisations - produced a 6% speed increase for the simple send_more_money example.\n* 1.7\n  - Make choice iteration more generic by having choice iterators generate Choice objects that are responsible for making the choice.\n  - Update the examples and README to use these choice iterators.\n* 1.6\n  - Add sections on installation and example use to README\n* 1.5\n  - Improve efficiency of send_more_money example by computing best_var only once per loop iteration\n  - add a check to test1.py to determine if output is OK\n* 1.4\n  - Add SemiDetPred (semi-deterministic predicate)\n* 1.3\n  - Add DetPred (deterministic predicate)\n  - Update top-level docstring\n* 1.2\n  - Improve efficiency of Loop\n* 1.1\n  - Add Disjunction\n  - Fix bug - not untrailing on execute success\n* 1.0\n  - Major rewrite of Pred to simplify the programmers task.\n  - The addition of predicate conjunction, looping and once\n  - Re-factoring of Engine.\n* 0.1\n  - Initial release.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpjritee%2Fpl_search","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpjritee%2Fpl_search","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpjritee%2Fpl_search/lists"}