Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ install:
- pip install -r tests/requirements.txt

script:
- pytest --cov=. --cov-report=term-missing
- pytest --cov=combcov --cov-report=term-missing

after_success:
- coveralls
Expand Down
28 changes: 10 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,24 @@ A generalization of the permutation-specific algorithm [Struct](https://github.c
extended for other types of combinatorial objects.


## Example usage
## Demo

Below is an example where CombCov finds a _String Set_ cover for the set of string over the alphabet `{a,b}` that
avoids the substring `aa` (meaning no string in the set contains `aa` as a substring).
Take a look at `demo/string_set.py` as an example on how to use `CombCov` with your own combinatorial object. It finds
a _String Set_ cover for the set of string over the alphabet `{a,b}` that avoids the substring `aa` (meaning no string
in the set contains `aa` as a substring).

```python
from string_set import StringSet
from comb_cov import CombCov

alphabet = ['a', 'b']
avoid = ['aa']
string_set = StringSet(alphabet, avoid)

max_elmnt_size = 7
comb_cov = CombCov(string_set, max_elmnt_size)
```bash
python -m demo.string_set
```

It prints out the following:

```text
Trying to find a cover for 'Av(aa) over ∑={a,b}' up to size 7 using 27 self-generated rules.
Trying to find a cover for ''*Av(aa) over ∑={a,b} using elements up to size 7.
(Enumeration: [1, 2, 3, 5, 8, 13, 21, 34])
SUCCESS! Found 1 solution(s).
Solution nr. 1:
- ''*Av(b,a) over ∑={a,b}
- 'a'*Av(b,a) over ∑={a,b}
- ''*Av(a,b) over ∑={a,b}
- 'a'*Av(a,b) over ∑={a,b}
- 'b'*Av(aa) over ∑={a,b}
- 'ab'*Av(aa) over ∑={a,b}
```
Expand All @@ -44,5 +36,5 @@ Run unittests:

```bash
pip install -r tests/requirements.txt
pytest
pytest --cov=combcov --cov=demo --cov-report=term-missing
```
62 changes: 0 additions & 62 deletions comb_cov.py

This file was deleted.

2 changes: 2 additions & 0 deletions combcov/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .combcov import CombCov, Rule
from .exact_cover import ExactCover
77 changes: 77 additions & 0 deletions combcov/combcov.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import abc

from combcov.exact_cover import ExactCover


class CombCov():

def __init__(self, root_object, max_elmnt_size):
self.root_object = root_object
self.max_elmnt_size = max_elmnt_size
self._enumerate_all_elmnts_up_to_max_size()
self._create_binary_strings_from_rules()

def _enumerate_all_elmnts_up_to_max_size(self):
elmnts = []
self.enumeration = [None] * (self.max_elmnt_size + 1)
for n in range(self.max_elmnt_size + 1):
elmnts_of_length_n = self.root_object.get_elmnts(of_size=n)
self.enumeration[n] = len(elmnts_of_length_n)
elmnts.extend(elmnts_of_length_n)

self.elmnts_dict = {
string: nr for nr, string in enumerate(elmnts, start=0)
}

def _create_binary_strings_from_rules(self):
self.rules_dict = {}
self.rules = []
for rule in self.root_object.get_subrules():
rule_is_good = True
binary_string = 0
for elmnt_size in range(self.max_elmnt_size + 1):
seen_elmnts = set()
for elmnt in rule.get_elmnts(of_size=elmnt_size):
if elmnt not in self.elmnts_dict or elmnt in seen_elmnts:
rule_is_good = False
break
else:
seen_elmnts.add(elmnt)
binary_string += 2 ** (self.elmnts_dict[elmnt])

if not rule_is_good:
break

if rule_is_good:
self.rules.append(rule)
self.rules_dict[rule] = binary_string

def solve(self):
self.ec = ExactCover(list(self.rules_dict.values()), len(self.elmnts_dict))
self.solutions_indices = self.ec.exact_cover()

def get_solutions(self):
solutions = []
for solution_indices in self.solutions_indices:
solution = [self.rules[binary_string] for binary_string in solution_indices]
solutions.append(solution)

return solutions


class Rule(abc.ABC):
@abc.abstractmethod
def get_elmnts(self, of_size):
pass

@abc.abstractmethod
def get_subrules(self):
pass

@abc.abstractmethod
def __hash__(self):
pass

@abc.abstractmethod
def __str__(self):
pass
68 changes: 68 additions & 0 deletions combcov/exact_cover.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import os
import shutil
import tempfile
from subprocess import Popen, DEVNULL


class ExactCover:

def __init__(self, bitstrings, cover_string_length):
self.bitstrings = bitstrings
self.cover_string_length = cover_string_length

def exact_cover(self):
try:
for res in self.exact_cover_gurobi():
yield res
except Exception as exc:
raise RuntimeError(
"Gurobi may not be installed and there are no alternative solution method at the moment.") from exc

def _call_Popen(self, inp, outp):
return Popen('gurobi_cl ResultFile=%s %s' % (outp, inp), shell=True, stdout=DEVNULL)

def exact_cover_gurobi(self):
tdir = None
used = set()
anything = False
try:
tdir = str(tempfile.mkdtemp(prefix='combcov_tmp'))
inp = os.path.join(tdir, 'inp.lp')
outp = os.path.join(tdir, 'out.sol')

with open(inp, 'w') as lp:
lp.write('Minimize %s\n' % ' + '.join('x%d' % i for i in range(len(self.bitstrings))))
lp.write('Subject To\n')

for i in range(self.cover_string_length):
here = []
for j in range(len(self.bitstrings)):
if (self.bitstrings[j] & (1 << i)) != 0:
here.append(j)
lp.write(' %s = 1\n' % ' + '.join('x%d' % x for x in here))

lp.write('Binary\n')
lp.write(' %s\n' % ' '.join('x%d' % i for i in range(len(self.bitstrings))))
lp.write('End\n')

p = self._call_Popen(inp, outp)
assert p.wait() == 0

with open(outp, 'r') as sol:
while True:
line = sol.readline()
if not line:
break
if line.startswith('#') or not line.strip():
continue
anything = True

k, v = line.strip().split()
if int(v) == 1:
used.add(int(k[1:]))
finally:
if tdir is not None:
shutil.rmtree(tdir)

if anything:
yield sorted(used)
1 change: 1 addition & 0 deletions demo/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .string_set import StringSet
112 changes: 112 additions & 0 deletions demo/string_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import itertools

from combcov import CombCov, Rule


class StringSet(Rule):

def __init__(self, alphabet=tuple(), avoid=frozenset(), prefix=""):
self.alphabet = tuple(alphabet)
self.avoid = frozenset(avoid)
self.prefix = prefix
self.max_prefix_size = max(0, max(len(av) for av in self.avoid))

def contains(self, string):
return all(av not in string for av in self.avoid)

def next_lexicographical_string(self, from_string):
if from_string is None:
return ""

else:
string = list(from_string)

# Increasing last character by one and carry over if needed
for i in range(len(string)):
pos = -(i + 1)
char = string[pos]
index = self.alphabet.index(char)
next_index = index + 1
if next_index == len(self.alphabet):
string[pos] = self.alphabet[0]
# ...and carry one over
else:
string[pos] = self.alphabet[next_index]
return "".join(string)

# If we get this far we need to increase the length of the string
return self.alphabet[0] + "".join(string)

@staticmethod
def _get_all_substrings_of(s):
# list of set because we don't want duplicates
return sorted(list(set(s[i:j + 1] for i in range(len(s)) for j in range(i, len(s)))))

def get_all_avoiding_subsets(self):
avoiding_substrings = [self._get_all_substrings_of(avoid) for avoid in self.avoid]
return {frozenset(product) for product in itertools.product(*avoiding_substrings)}

def get_elmnts(self, of_size):
strings_of_length = []

padding = of_size - len(self.prefix)
rest = self.alphabet[0] * padding

while len(rest) == padding:
if self.contains(rest):
strings_of_length.append(self.prefix + rest)
rest = self.next_lexicographical_string(rest)

return strings_of_length

def get_subrules(self):
rules = []
prefixes = []
for n in range(self.max_prefix_size):
prefixes.extend(self.get_elmnts(n + 1))

# Singleton rules, on the form prefix + empty StringSet
for prefix in [''] + prefixes:
empty_string_set = StringSet(alphabet=self.alphabet, avoid=frozenset(self.alphabet), prefix=prefix)
rules.append(empty_string_set)

# Regular rules of the from prefix + non-empty StringSet
for prefix in prefixes:
for avoiding_subset in self.get_all_avoiding_subsets():
substring_set = StringSet(self.alphabet, avoiding_subset, prefix)
rules.append(substring_set)

return rules

def __eq__(self, other):
if isinstance(other, StringSet):
return (self.alphabet == other.alphabet and self.avoid == other.avoid and self.prefix == other.prefix)
return False

def __hash__(self):
return hash(self.alphabet) ^ hash(self.avoid) ^ hash(self.prefix)

def __str__(self):
return "'{}'*Av({}) over ∑={{{}}}".format(self.prefix, ",".join(self.avoid), ",".join(self.alphabet))


def main():
alphabet = ('a', 'b')
avoid = frozenset(['aa'])
string_set = StringSet(alphabet, avoid)
max_elmnt_size = 7

print("Trying to find a cover for {} using elements up to size {}.".format(string_set, max_elmnt_size))
comb_cov = CombCov(string_set, max_elmnt_size)
comb_cov.solve()

print("(Enumeration: {})".format(comb_cov.enumeration))

for nr, solution in enumerate(comb_cov.get_solutions(), start=1):
print("Solution nr. {}:".format(nr))
for rule in solution:
print(" - {}".format(rule))


if __name__ == "__main__":
main()
Loading