Skip to content

Commit

Permalink
Merge pull request #11 from ImogenBits/instance_class
Browse files Browse the repository at this point in the history
Change Problem class definition
  • Loading branch information
Benezivas authored Aug 21, 2023
2 parents 0965dab + 314ae5e commit 1964b3b
Show file tree
Hide file tree
Showing 21 changed files with 482 additions and 469 deletions.
71 changes: 35 additions & 36 deletions algobattle_problems/biclique/problem.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
"""The Biclique problem class."""
from typing import ClassVar

from algobattle.problem import UndirectedGraph, SolutionModel, ValidationError
from algobattle.util import u64


class Biclique(UndirectedGraph):
"""The Biclique problem class."""

name: ClassVar[str] = "Bipartite Clique"
min_size: ClassVar[int] = 5

class Solution(SolutionModel):
"""A solution to a bipartite clique problem."""

direction: ClassVar = "maximize"

s_1: set[u64]
s_2: set[u64]

def validate_solution(self, instance: "Biclique") -> None:
edge_set = set(instance.edges) | set(edge[::-1] for edge in instance.edges)
super().validate_solution(instance)
if any(i >= instance.num_vertices for i in self.s_1 | self.s_2):
raise ValidationError("Solution contains vertices that aren't in the instance.")
if len(self.s_1.intersection(self.s_2)) != 0:
raise ValidationError("Solution contains vertex sets that aren't disjoint.")
if any((u, v) not in edge_set for u in self.s_1 for v in self.s_2):
raise ValidationError("The instance graph is missing an edge between the solution vertex sets.")
if any((u, v) in edge_set for u in self.s_1 for v in self.s_1) or any(
(u, v) in edge_set for u in self.s_2 for v in self.s_2
):
raise ValidationError("The solution is not a bipartite graph.")

def score(self, instance: "Biclique") -> float:
return len(self.s_1) + len(self.s_2)
from algobattle.problem import Problem, UndirectedGraph, SolutionModel, ValidationError, maximize
from algobattle.util import u64, Role


class Solution(SolutionModel[UndirectedGraph]):
"""A solution to a bipartite clique problem."""

s_1: set[u64]
s_2: set[u64]

def validate_solution(self, instance: UndirectedGraph, role: Role) -> None:
edge_set = set(instance.edges) | set(edge[::-1] for edge in instance.edges)
super().validate_solution(instance, role)
if any(i >= instance.num_vertices for i in self.s_1 | self.s_2):
raise ValidationError("Solution contains vertices that aren't in the instance.")
if len(self.s_1.intersection(self.s_2)) != 0:
raise ValidationError("Solution contains vertex sets that aren't disjoint.")
if any((u, v) not in edge_set for u in self.s_1 for v in self.s_2):
raise ValidationError("The instance graph is missing an edge between the solution vertex sets.")
if any((u, v) in edge_set for u in self.s_1 for v in self.s_1) or any(
(u, v) in edge_set for u in self.s_2 for v in self.s_2
):
raise ValidationError("The solution is not a bipartite graph.")

@maximize
def score(self, instance: UndirectedGraph, role: Role) -> float:
return len(self.s_1) + len(self.s_2)


Biclique = Problem(
name="Bipartite Clique",
instance_cls=UndirectedGraph,
solution_cls=Solution,
min_size=5,
)
20 changes: 10 additions & 10 deletions algobattle_problems/biclique/tests.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
"""Tests for the biclique problem."""
import unittest

from algobattle_problems.biclique.problem import Biclique, ValidationError
from algobattle_problems.biclique.problem import UndirectedGraph, Solution, ValidationError, Role


class Tests(unittest.TestCase):
"""Tests for the Biclique problem solution class."""

def test_vertices_exist(self):
"""Tests that only valid vertex indices are allowed."""
graph = Biclique(num_vertices=10, edges=[(i, j) for i in range(10) for j in range(i)])
sol = Biclique.Solution(s_1=set(), s_2={20})
graph = UndirectedGraph(num_vertices=10, edges=[(i, j) for i in range(10) for j in range(i)])
sol = Solution(s_1=set(), s_2={20})
with self.assertRaises(ValidationError):
sol.validate_solution(graph)
sol.validate_solution(graph, Role.generator)

def test_edges_exist(self):
"""Tests that solutions that arent complete bicliques are not allowed."""
graph = Biclique(num_vertices=10, edges=[])
sol = Biclique.Solution(s_1={1}, s_2={2})
graph = UndirectedGraph(num_vertices=10, edges=[])
sol = Solution(s_1={1}, s_2={2})
with self.assertRaises(ValidationError):
sol.validate_solution(graph)
sol.validate_solution(graph, Role.generator)

def test_edges_missing(self):
"""Asserts that solutions that aren't bipartite are not allowed."""
graph = Biclique(num_vertices=10, edges=[(i, j) for i in range(10) for j in range(i)])
sol = Biclique.Solution(s_1={1, 2}, s_2={3, 4})
graph = UndirectedGraph(num_vertices=10, edges=[(i, j) for i in range(10) for j in range(i)])
sol = Solution(s_1={1, 2}, s_2={3, 4})
with self.assertRaises(ValidationError):
sol.validate_solution(graph)
sol.validate_solution(graph, Role.generator)


if __name__ == "__main__":
Expand Down
108 changes: 53 additions & 55 deletions algobattle_problems/c4subgraphiso/problem.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,66 @@
"""The C4subgraphiso problem class."""
from typing import ClassVar

from algobattle.problem import UndirectedGraph, SolutionModel, ValidationError
from algobattle.util import u64
from algobattle.problem import Problem, UndirectedGraph, SolutionModel, ValidationError, maximize
from algobattle.util import u64, Role


class C4subgraphiso(UndirectedGraph):
"""The C4subgraphiso problem class."""
class Solution(SolutionModel[UndirectedGraph]):
"""A solution to a Square Subgraph Isomorphism problem."""

name: ClassVar[str] = "Square Subgraph Isomorphism"
min_size: ClassVar[int] = 4
squares: set[tuple[u64, u64, u64, u64]]

class Solution(SolutionModel):
"""A solution to a Square Subgraph Isomorphism problem."""
def validate_solution(self, instance: UndirectedGraph, role: Role) -> None:
super().validate_solution(instance, role)
self._all_entries_bounded_in_size(instance)
self._all_squares_in_instance(instance)
self._all_squares_node_disjoint()
self._all_squares_induced(instance)

direction: ClassVar = "maximize"
def _all_entries_bounded_in_size(self, instance: UndirectedGraph) -> None:
for square in self.squares:
if any(node >= instance.num_vertices for node in square):
raise ValidationError("An element of the solution doesn't index an instance vertex.")

squares: set[tuple[u64, u64, u64, u64]]
def _all_squares_node_disjoint(self) -> None:
used_nodes = set()
for square in self.squares:
for node in square:
if node in used_nodes:
raise ValidationError("A square in the solution is not node-disjoint to at least one other square.")
used_nodes.add(node)

def validate_solution(self, instance: "C4subgraphiso") -> None:
super().validate_solution(instance)
self._all_entries_bounded_in_size(instance)
self._all_squares_in_instance(instance)
self._all_squares_node_disjoint()
self._all_squares_induced(instance)
def _all_squares_induced(self, instance: UndirectedGraph) -> None:
edge_set = set(instance.edges)
for square in self.squares:
# Edges between opposing nodes of a square would mean the square is not induced by its nodes
unwanted_edges = [
(square[0], square[2]),
(square[2], square[0]),
(square[1], square[3]),
(square[3], square[1]),
]
if any(edge in edge_set for edge in unwanted_edges):
raise ValidationError("A square in the solution is not induced in the instance.")

def _all_entries_bounded_in_size(self, instance: "C4subgraphiso") -> None:
for square in self.squares:
if any(node >= instance.num_vertices for node in square):
raise ValidationError("An element of the solution doesn't index an instance vertex.")
def _all_squares_in_instance(self, instance: UndirectedGraph) -> None:
edge_set = set(instance.edges)
for square in self.squares:
if (
not ((square[0], square[1]) in edge_set or (square[1], square[0]) in edge_set)
or not ((square[1], square[2]) in edge_set or (square[2], square[1]) in edge_set)
or not ((square[2], square[3]) in edge_set or (square[3], square[2]) in edge_set)
or not ((square[3], square[0]) in edge_set or (square[0], square[3]) in edge_set)
):
raise ValidationError("A square is not part of the instance.")

def _all_squares_node_disjoint(self) -> None:
used_nodes = set()
for square in self.squares:
for node in square:
if node in used_nodes:
raise ValidationError(
"A square in the solution is not node-disjoint to at least one other square."
)
used_nodes.add(node)
@maximize
def score(self, instance: UndirectedGraph, role: Role) -> float:
return len(self.squares)

def _all_squares_induced(self, instance: "C4subgraphiso") -> None:
edge_set = set(instance.edges)
for square in self.squares:
# Edges between opposing nodes of a square would mean the square is not induced by its nodes
unwanted_edges = [
(square[0], square[2]),
(square[2], square[0]),
(square[1], square[3]),
(square[3], square[1]),
]
if any(edge in edge_set for edge in unwanted_edges):
raise ValidationError("A square in the solution is not induced in the instance.")

def _all_squares_in_instance(self, instance: "C4subgraphiso") -> None:
edge_set = set(instance.edges)
for square in self.squares:
if (
not ((square[0], square[1]) in edge_set or (square[1], square[0]) in edge_set)
or not ((square[1], square[2]) in edge_set or (square[2], square[1]) in edge_set)
or not ((square[2], square[3]) in edge_set or (square[3], square[2]) in edge_set)
or not ((square[3], square[0]) in edge_set or (square[0], square[3]) in edge_set)
):
raise ValidationError("A square is not part of the instance.")

def score(self, instance: "C4subgraphiso") -> float:
return len(self.squares)
C4subgraphiso = Problem(
name="Square Subgraph Isomorphism",
instance_cls=UndirectedGraph,
solution_cls=Solution,
min_size=4,
)
28 changes: 14 additions & 14 deletions algobattle_problems/c4subgraphiso/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from pydantic import ValidationError as PydanticValidationError

from algobattle_problems.c4subgraphiso.problem import C4subgraphiso, ValidationError
from algobattle_problems.c4subgraphiso.problem import UndirectedGraph, Solution, ValidationError, Role


class Tests(unittest.TestCase):
Expand All @@ -12,7 +12,7 @@ class Tests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
super().setUpClass()
cls.instance = C4subgraphiso(
cls.instance = UndirectedGraph(
num_vertices=10,
edges=[
(0, 1),
Expand All @@ -35,7 +35,7 @@ def setUpClass(cls) -> None:

def test_no_duplicate_squares(self):
with self.assertRaises(PydanticValidationError):
C4subgraphiso.parse_obj(
UndirectedGraph.parse_obj(
{
"squares": {
(0, 1, 2, 3),
Expand All @@ -45,29 +45,29 @@ def test_no_duplicate_squares(self):
)

def test_vertex_too_big(self):
solution = C4subgraphiso.Solution(squares={(0, 1, 2, 10)})
solution = Solution(squares={(0, 1, 2, 10)})
with self.assertRaises(ValidationError):
solution.validate_solution(self.instance)
solution.validate_solution(self.instance, Role.generator)

def test_edge_missing(self):
solution = C4subgraphiso.Solution(squares={(2, 3, 4, 5)})
solution = Solution(squares={(2, 3, 4, 5)})
with self.assertRaises(ValidationError):
solution.validate_solution(self.instance)
solution.validate_solution(self.instance, Role.generator)

def test_additional_edge(self):
solution = C4subgraphiso.Solution(squares={(1, 2, 4, 8)})
solution = Solution(squares={(1, 2, 4, 8)})
with self.assertRaises(ValidationError):
solution.validate_solution(self.instance)
solution.validate_solution(self.instance, Role.generator)

def test_score(self):
solution = C4subgraphiso.Solution(squares={(0, 1, 8, 9), (4, 5, 6, 7)})
solution.validate_solution(self.instance)
self.assertEqual(solution.score(self.instance), 2)
solution = Solution(squares={(0, 1, 8, 9), (4, 5, 6, 7)})
solution.validate_solution(self.instance, Role.generator)
self.assertEqual(solution.score(self.instance, Role.solver), 2)

def test_squares_disjoin(self):
solution = C4subgraphiso.Solution(squares={(0, 1, 2, 3), (0, 1, 8, 9)})
solution = Solution(squares={(0, 1, 2, 3), (0, 1, 8, 9)})
with self.assertRaises(ValidationError):
solution.validate_solution(self.instance)
solution.validate_solution(self.instance, Role.generator)


if __name__ == "__main__":
Expand Down
72 changes: 36 additions & 36 deletions algobattle_problems/clusterediting/problem.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,50 @@
"""The Clusterediting problem class."""
from collections import defaultdict
from itertools import combinations
from typing import ClassVar

from algobattle.problem import UndirectedGraph, SolutionModel, ValidationError
from algobattle.util import u64
from algobattle.problem import Problem, UndirectedGraph, SolutionModel, ValidationError, minimize
from algobattle.util import u64, Role


class Clusterediting(UndirectedGraph):
"""The Clusterediting problem class."""
class Solution(SolutionModel[UndirectedGraph]):
"""A solution to a Cluster Editing problem."""

name: ClassVar[str] = "Cluster Editing"
min_size: ClassVar[int] = 4
add: set[tuple[u64, u64]]
delete: set[tuple[u64, u64]]

class Solution(SolutionModel):
"""A solution to a Cluster Editing problem."""
def validate_solution(self, instance: UndirectedGraph, role: Role) -> None:
edge_set = set(instance.edges)

direction: ClassVar = "minimize"
# Apply modifications to graph
for edge in self.add:
if edge[::-1] not in edge_set:
edge_set.add(edge)
for edge in self.delete:
if edge in edge_set:
edge_set.remove(edge)
elif (edge[1], edge[0]) in edge_set:
edge_set.remove((edge[1], edge[0]))
else:
raise ValidationError("Solution contains edge not found in instance.")

add: set[tuple[u64, u64]]
delete: set[tuple[u64, u64]]
neighbors: defaultdict[int, set[int]] = defaultdict(set)
for u, v in edge_set:
neighbors[u].add(v)
neighbors[v].add(u)

def validate_solution(self, instance: "Clusterediting") -> None:
edge_set = set(instance.edges)
for u in range(instance.num_vertices):
for v, w in combinations(neighbors[u], 2):
if not (v, w) in edge_set and not (w, v) in edge_set:
raise ValidationError("The solution does not transform the graph into a cluster.")

# Apply modifications to graph
for edge in self.add:
if edge[::-1] not in edge_set:
edge_set.add(edge)
for edge in self.delete:
if edge in edge_set:
edge_set.remove(edge)
elif (edge[1], edge[0]) in edge_set:
edge_set.remove((edge[1], edge[0]))
else:
raise ValidationError("Solution contains edge not found in instance.")
@minimize
def score(self, instance: UndirectedGraph, role: Role) -> float:
return len(self.add) + len(self.delete)

neighbors: defaultdict[int, set[int]] = defaultdict(set)
for u, v in edge_set:
neighbors[u].add(v)
neighbors[v].add(u)

for u in range(instance.num_vertices):
for v, w in combinations(neighbors[u], 2):
if not (v, w) in edge_set and not (w, v) in edge_set:
raise ValidationError("The solution does not transform the graph into a cluster.")

def score(self, instance: "Clusterediting") -> float:
return len(self.add) + len(self.delete)
Clusterediting = Problem(
name="Cluster Editing",
min_size=4,
instance_cls=UndirectedGraph,
solution_cls=Solution,
)
Loading

0 comments on commit 1964b3b

Please sign in to comment.