Skip to content

Problem

This module contains the Problem, Instance, Solution, and other related classes and thus the interface you can use to provide your own problems.

algobattle.problem.Problem

The definition of a problem.

Source code in algobattle/problem.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
class Problem:
    """The definition of a problem."""

    @overload
    def __init__(  # noqa: D107
        self,
        *,
        name: str,
        instance_cls: type[InstanceT],
        solution_cls: type[SolutionT],
        min_size: int = 0,
        with_solution: Literal[True] = True,
        score_function: ScoreFunctionWithSol[InstanceT, SolutionT] = default_score,
        test_instance: InstanceT | None = None,
    ) -> None:
        ...

    @overload
    def __init__(  # noqa: D107
        self,
        *,
        name: str,
        instance_cls: type[InstanceT],
        solution_cls: type[SolutionT],
        min_size: int = 0,
        with_solution: Literal[False],
        score_function: ScoreFunctionNoSol[InstanceT, SolutionT] = default_score,
        test_instance: InstanceT | None = None,
    ) -> None:
        ...

    def __init__(
        self,
        *,
        name: str,
        instance_cls: type[InstanceT],
        solution_cls: type[SolutionT],
        min_size: int = 0,
        with_solution: bool = True,
        score_function: ScoreFunction[InstanceT, SolutionT] = default_score,
        test_instance: InstanceT | None = None,
    ) -> None:
        """The definition of a problem.

        Args:
            name: The name of the problem.
            instance_cls: Class defining what instances of this problem look like.
            solution_cls: Class definitng what solutions of this problem look like.
            min_size: Minimum size of valid instances of this problem.
            with_solution: Whether the generator should also create a solution.
            score_function: Function used to score how well a solution solves a problem instance.

                The default scoring function returns the quotient of the solver's to the generator's solution score.

                The score function always takes the instance as the first argument. If `with_solution` is set it then
                gets the generated solutions at `generator_solution` and `solver_solution`. If it is not set it receives
                the solver's solution at `solution`. It should return the calculated score, a number in [0, 1] with a
                value of 0 indicating that the solver failed completely and 1 that it solved the instance perfectly.
            test_instance: A dummy instance that can be used to test whether a solver produces correct output.
        """
        self.name = name
        self.instance_cls = instance_cls
        self.solution_cls = solution_cls
        self.min_size = min_size
        self.with_solution = with_solution
        self.score_function = score_function
        self.test_instance = test_instance
        self._problems[name] = self

    __slots__ = ("name", "instance_cls", "solution_cls", "min_size", "with_solution", "score_function", "test_instance")
    _problems: ClassVar[dict[str, Self]] = {}

    @overload
    def score(self, instance: InstanceT, *, solution: Solution[InstanceT]) -> float:
        ...

    @overload
    def score(
        self, instance: InstanceT, *, generator_solution: Solution[InstanceT], solver_solution: Solution[InstanceT]
    ) -> float:
        ...

    def score(
        self,
        instance: Instance,
        *,
        solution: SolutionT | None = None,
        generator_solution: SolutionT | None = None,
        solver_solution: SolutionT | None = None,
    ) -> float:
        """Helper function to call self.score_function with easier to use overloads."""
        if self.with_solution:
            if not (
                isinstance(instance, self.instance_cls)
                and isinstance(generator_solution, self.solution_cls)
                and isinstance(solver_solution, self.solution_cls)
                and solution is None
            ):
                raise TypeError
            if TYPE_CHECKING:
                assert isinstance(self.score_function, ScoreFunctionWithSol)
            return self.score_function(instance, generator_solution=generator_solution, solver_solution=solver_solution)
        else:
            if not (
                isinstance(instance, self.instance_cls)
                and isinstance(solution, self.solution_cls)
                and generator_solution is None
                and solver_solution is None
            ):
                raise TypeError
            if TYPE_CHECKING:
                assert isinstance(self.score_function, ScoreFunctionNoSol)
            return self.score_function(instance, solution=solution)

    @classmethod
    def load_file(cls, name: str, file: Path) -> Self:
        """Loads the problem from the specified file."""
        existing_problems = cls._problems.copy()
        cls._problems = {}
        try:
            import_file_as_module(file, "__algobattle_problem__")
            if name not in cls._problems:
                raise ValueError(f"The {name} problem is not defined in {file}")
            else:
                return cls._problems[name]
        finally:
            cls._problems = existing_problems

    @classmethod
    def load(cls, name: str, file: Path | None = None) -> Self:
        """Loads the problem with the given name.

        Args:
            name: The name of the Problem to use.
            file: Path to a file containing this problem.

        Raises:
            ValueError: If the problem is not specified properly
            RuntimeError: If the problem's dynamic import fails
        """
        if file:
            return cls.load_file(name, file)
        if name in cls._problems:
            return cls._problems[name]
        match list(entry_points(group="algobattle.problem", name=name)):
            case []:
                raise ValueError("Problem name is not valid.")
            case [e]:
                loaded: object = e.load()
                if not isinstance(loaded, cls):
                    raise ValueError(
                        f"The entrypoint '{name}' doesn't point to a problem but a {loaded.__class__.__qualname__}."
                    )
                return loaded
            case entypoints:
                raise ValueError(
                    f"Multiple problem entrypoints with the name {name} exist!"
                    f" The modules providing them are: {', '.join(e.module for e in entypoints)}."
                )

    @classmethod
    def available(cls) -> set[str]:
        """Returns the names of all available Problems."""
        return set(chain(cls._problems.keys(), (e.name for e in entry_points(group="algobattle.problem"))))

__init__(*, name, instance_cls, solution_cls, min_size=0, with_solution=True, score_function=default_score, test_instance=None)

The definition of a problem.

Parameters:

Name Type Description Default
name str

The name of the problem.

required
instance_cls type[InstanceT]

Class defining what instances of this problem look like.

required
solution_cls type[SolutionT]

Class definitng what solutions of this problem look like.

required
min_size int

Minimum size of valid instances of this problem.

0
with_solution bool

Whether the generator should also create a solution.

True
score_function ScoreFunction[InstanceT, SolutionT]

Function used to score how well a solution solves a problem instance.

The default scoring function returns the quotient of the solver's to the generator's solution score.

The score function always takes the instance as the first argument. If with_solution is set it then gets the generated solutions at generator_solution and solver_solution. If it is not set it receives the solver's solution at solution. It should return the calculated score, a number in [0, 1] with a value of 0 indicating that the solver failed completely and 1 that it solved the instance perfectly.

default_score
test_instance InstanceT | None

A dummy instance that can be used to test whether a solver produces correct output.

None
Source code in algobattle/problem.py
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
def __init__(
    self,
    *,
    name: str,
    instance_cls: type[InstanceT],
    solution_cls: type[SolutionT],
    min_size: int = 0,
    with_solution: bool = True,
    score_function: ScoreFunction[InstanceT, SolutionT] = default_score,
    test_instance: InstanceT | None = None,
) -> None:
    """The definition of a problem.

    Args:
        name: The name of the problem.
        instance_cls: Class defining what instances of this problem look like.
        solution_cls: Class definitng what solutions of this problem look like.
        min_size: Minimum size of valid instances of this problem.
        with_solution: Whether the generator should also create a solution.
        score_function: Function used to score how well a solution solves a problem instance.

            The default scoring function returns the quotient of the solver's to the generator's solution score.

            The score function always takes the instance as the first argument. If `with_solution` is set it then
            gets the generated solutions at `generator_solution` and `solver_solution`. If it is not set it receives
            the solver's solution at `solution`. It should return the calculated score, a number in [0, 1] with a
            value of 0 indicating that the solver failed completely and 1 that it solved the instance perfectly.
        test_instance: A dummy instance that can be used to test whether a solver produces correct output.
    """
    self.name = name
    self.instance_cls = instance_cls
    self.solution_cls = solution_cls
    self.min_size = min_size
    self.with_solution = with_solution
    self.score_function = score_function
    self.test_instance = test_instance
    self._problems[name] = self

score(instance, *, solution=None, generator_solution=None, solver_solution=None)

Helper function to call self.score_function with easier to use overloads.

Source code in algobattle/problem.py
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
def score(
    self,
    instance: Instance,
    *,
    solution: SolutionT | None = None,
    generator_solution: SolutionT | None = None,
    solver_solution: SolutionT | None = None,
) -> float:
    """Helper function to call self.score_function with easier to use overloads."""
    if self.with_solution:
        if not (
            isinstance(instance, self.instance_cls)
            and isinstance(generator_solution, self.solution_cls)
            and isinstance(solver_solution, self.solution_cls)
            and solution is None
        ):
            raise TypeError
        if TYPE_CHECKING:
            assert isinstance(self.score_function, ScoreFunctionWithSol)
        return self.score_function(instance, generator_solution=generator_solution, solver_solution=solver_solution)
    else:
        if not (
            isinstance(instance, self.instance_cls)
            and isinstance(solution, self.solution_cls)
            and generator_solution is None
            and solver_solution is None
        ):
            raise TypeError
        if TYPE_CHECKING:
            assert isinstance(self.score_function, ScoreFunctionNoSol)
        return self.score_function(instance, solution=solution)

load_file(name, file) classmethod

Loads the problem from the specified file.

Source code in algobattle/problem.py
343
344
345
346
347
348
349
350
351
352
353
354
355
@classmethod
def load_file(cls, name: str, file: Path) -> Self:
    """Loads the problem from the specified file."""
    existing_problems = cls._problems.copy()
    cls._problems = {}
    try:
        import_file_as_module(file, "__algobattle_problem__")
        if name not in cls._problems:
            raise ValueError(f"The {name} problem is not defined in {file}")
        else:
            return cls._problems[name]
    finally:
        cls._problems = existing_problems

load(name, file=None) classmethod

Loads the problem with the given name.

Parameters:

Name Type Description Default
name str

The name of the Problem to use.

required
file Path | None

Path to a file containing this problem.

None

Raises:

Type Description
ValueError

If the problem is not specified properly

RuntimeError

If the problem's dynamic import fails

Source code in algobattle/problem.py
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
@classmethod
def load(cls, name: str, file: Path | None = None) -> Self:
    """Loads the problem with the given name.

    Args:
        name: The name of the Problem to use.
        file: Path to a file containing this problem.

    Raises:
        ValueError: If the problem is not specified properly
        RuntimeError: If the problem's dynamic import fails
    """
    if file:
        return cls.load_file(name, file)
    if name in cls._problems:
        return cls._problems[name]
    match list(entry_points(group="algobattle.problem", name=name)):
        case []:
            raise ValueError("Problem name is not valid.")
        case [e]:
            loaded: object = e.load()
            if not isinstance(loaded, cls):
                raise ValueError(
                    f"The entrypoint '{name}' doesn't point to a problem but a {loaded.__class__.__qualname__}."
                )
            return loaded
        case entypoints:
            raise ValueError(
                f"Multiple problem entrypoints with the name {name} exist!"
                f" The modules providing them are: {', '.join(e.module for e in entypoints)}."
            )

available() classmethod

Returns the names of all available Problems.

Source code in algobattle/problem.py
389
390
391
392
@classmethod
def available(cls) -> set[str]:
    """Returns the names of all available Problems."""
    return set(chain(cls._problems.keys(), (e.name for e in entry_points(group="algobattle.problem"))))

algobattle.problem.Instance

Bases: Encodable, ABC

Instance base class.

Source code in algobattle/problem.py
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
class Instance(Encodable, ABC):
    """Instance base class."""

    @property
    @abstractmethod
    def size(self) -> int:
        """The instance's size."""
        raise NotImplementedError

    def validate_instance(self) -> None:
        """Confirms that the parsed instance is valid.

        Should be idempotent, but may also perform additional postprocessing such as bringing the instance
        into a normal form.

        Raises:
            ValidationError: if the created instance is invalid.
        """
        return

size: int abstractmethod property

The instance's size.

validate_instance()

Confirms that the parsed instance is valid.

Should be idempotent, but may also perform additional postprocessing such as bringing the instance into a normal form.

Raises:

Type Description
ValidationError

if the created instance is invalid.

Source code in algobattle/problem.py
59
60
61
62
63
64
65
66
67
68
def validate_instance(self) -> None:
    """Confirms that the parsed instance is valid.

    Should be idempotent, but may also perform additional postprocessing such as bringing the instance
    into a normal form.

    Raises:
        ValidationError: if the created instance is invalid.
    """
    return

algobattle.problem.Solution

Bases: EncodableBase, Generic[InstanceT], ABC

A proposed solution for an instance of this problem.

Source code in algobattle/problem.py
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
class Solution(EncodableBase, Generic[InstanceT], ABC):
    """A proposed solution for an instance of this problem."""

    @classmethod
    @abstractmethod
    def decode(cls, source: Path, max_size: int, role: Role, instance: InstanceT) -> Self:  # noqa: D102
        raise NotImplementedError

    def validate_solution(self, instance: InstanceT, role: Role) -> None:
        """Confirms that the parsed solution is valid.

        Should be idempotent, but may also perform additional postprocessing such as bringing the solution
        into a normal form.

        Args:
            instance: The problem instance this solution is purported to solve.
            role: The role of the team that generated this solution.

        Raises:
            ValidationError: if the created instance is invalid.
        """
        return

    def score(self, instance: InstanceT, role: Role) -> float:
        """Calculate the score of this solution for the given problem instance.

        The default implementation always returns 1, indicating that all solutions of this problem are equally good.

        Args:
            instance: The instance this solution solves
            role: The role of the team that generated this solution
        Returns:
            The calculates score of this solution. Must be a nonnegative number. Bigger scores are considered better,
            if your score rates better scores lower you can use the @minimize decorator.
        """
        return 1

validate_solution(instance, role)

Confirms that the parsed solution is valid.

Should be idempotent, but may also perform additional postprocessing such as bringing the solution into a normal form.

Parameters:

Name Type Description Default
instance InstanceT

The problem instance this solution is purported to solve.

required
role Role

The role of the team that generated this solution.

required

Raises:

Type Description
ValidationError

if the created instance is invalid.

Source code in algobattle/problem.py
83
84
85
86
87
88
89
90
91
92
93
94
95
96
def validate_solution(self, instance: InstanceT, role: Role) -> None:
    """Confirms that the parsed solution is valid.

    Should be idempotent, but may also perform additional postprocessing such as bringing the solution
    into a normal form.

    Args:
        instance: The problem instance this solution is purported to solve.
        role: The role of the team that generated this solution.

    Raises:
        ValidationError: if the created instance is invalid.
    """
    return

score(instance, role)

Calculate the score of this solution for the given problem instance.

The default implementation always returns 1, indicating that all solutions of this problem are equally good.

Parameters:

Name Type Description Default
instance InstanceT

The instance this solution solves

required
role Role

The role of the team that generated this solution

required

Returns: The calculates score of this solution. Must be a nonnegative number. Bigger scores are considered better, if your score rates better scores lower you can use the @minimize decorator.

Source code in algobattle/problem.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def score(self, instance: InstanceT, role: Role) -> float:
    """Calculate the score of this solution for the given problem instance.

    The default implementation always returns 1, indicating that all solutions of this problem are equally good.

    Args:
        instance: The instance this solution solves
        role: The role of the team that generated this solution
    Returns:
        The calculates score of this solution. Must be a nonnegative number. Bigger scores are considered better,
        if your score rates better scores lower you can use the @minimize decorator.
    """
    return 1

algobattle.problem.InstanceModel

Bases: InstanceSolutionModel, EncodableModel, Instance, ABC

An instance that can easily be parsed to/from a json file.

Source code in algobattle/problem.py
555
556
557
558
class InstanceModel(InstanceSolutionModel, EncodableModel, Instance, ABC):
    """An instance that can easily be parsed to/from a json file."""

    pass

algobattle.problem.SolutionModel

Bases: InstanceSolutionModel, Solution[InstanceT], ABC

A solution that can easily be parsed to/from a json file.

Source code in algobattle/problem.py
561
562
563
564
565
566
567
568
class SolutionModel(InstanceSolutionModel, Solution[InstanceT], ABC):
    """A solution that can easily be parsed to/from a json file."""

    @classmethod
    def decode(cls, source: Path, max_size: int, role: Role, instance: InstanceT) -> Self:
        """Uses pydantic to create a python object from a `.json` file."""
        context: dict[str, Any] = {"max_size": max_size, "role": role, "instance": instance}
        return cls._decode(cls, source, **context)

decode(source, max_size, role, instance) classmethod

Uses pydantic to create a python object from a .json file.

Source code in algobattle/problem.py
564
565
566
567
568
@classmethod
def decode(cls, source: Path, max_size: int, role: Role, instance: InstanceT) -> Self:
    """Uses pydantic to create a python object from a `.json` file."""
    context: dict[str, Any] = {"max_size": max_size, "role": role, "instance": instance}
    return cls._decode(cls, source, **context)