diff --git a/docs/source/reference/puan.logic.plog.rst b/docs/source/reference/puan.logic.plog.rst index cefa4dc..6af34eb 100644 --- a/docs/source/reference/puan.logic.plog.rst +++ b/docs/source/reference/puan.logic.plog.rst @@ -13,7 +13,9 @@ Data types :class:`puan.logic.plog.AtLeast` : ``AtLeast`` is a compound proposition which takes propositions and represents a lower bound on the result of those propositions. For example, select at least one of x, y and z would be defined as ``AtLeast(propositions=["x","y","z"], value=1)`` and represented by the linear inequality :math:`x+y+z \ge 1`. - :class:`puan.logic.plog.AtMost` : ``AtMost`` is a compound proposition which takes propositions and represents a lower bound on the result of those propositions. For example, select at most two of x, y and z would be defined as ``AtMost(propositions=["x","y","z"], value=2)`` and represented by the linear inequality :math:`-x-y-z \ge -2`. + :class:`puan.logic.plog.AtMost` : ``AtMost`` is a compound proposition which takes propositions and represents an upper bound on the result of those propositions. For example, select at most two of x, y and z would be defined as ``AtMost(propositions=["x","y","z"], value=2)`` and represented by the linear inequality :math:`-x-y-z \ge -2`. + + :class:`puan.logic.plog.Equal` : ``Equal`` is a compound proposition which takes propositions and represents an exact bound on the result of those propositions. For example, select propositions equal to two of x, y and z would be defined as ``Equal(propositions=["x","y","z"], value=2)`` and represented by the linear inequality :math:`x+y+z = -2`. :class:`puan.logic.plog.All` : ``All`` is a compound proposition representing a conjunction of all given propositions. ``All`` is represented by an ``AtLeast`` proposition with value set to the number of given propositions. For example, ``All("x","y","z")`` is equivalent to ``AtLeast(propositions=["x","y","z"], value=3)``. @@ -44,6 +46,13 @@ AtMost :undoc-members: :show-inheritance: +Equal +++++++ +.. autoclass:: Equal + :members: + :undoc-members: + :show-inheritance: + All +++ .. autoclass:: All diff --git a/puan/logic/plog/__init__.py b/puan/logic/plog/__init__.py index 29635fd..b201f47 100644 --- a/puan/logic/plog/__init__.py +++ b/puan/logic/plog/__init__.py @@ -1476,6 +1476,7 @@ def to_json(self) -> typing.Dict[str, typing.Any]: d['value'] = -1*self.value return d + class All(AtLeast): """ @@ -1561,6 +1562,79 @@ def to_json(self) -> typing.Dict[str, typing.Any]: ) return d +class Equal(All): + + """ + ``Equal`` proposition is a combination of an :class:`AtLeast` proposition and an :class:`AtMost` proposition + (e.g. :math:`x+y+z-1 = 0`). Sub propositions may take on any value given by their equation bounds + + Parameters + ---------- + value : integer value constraint constant - right hand side of the equality + propositions : a list of :class:`puan.Proposition` instances or ``str`` + variable : variable connected to this proposition + + Notes + ----- + - Propositions may be of type ``str``, :class:`puan.variable` or :class:`AtLeast` (or other inheriting :class:`AtLeast`) + - Propositions list cannot be empty. + + Examples + -------- + Meaning exactly two of x, y and z. + >>> Equal(2, list("xyz"), variable='A') + A: +(VAR658adc74c6913fb83b42c3968866f4bdb967fcad34692fc71d3de5f9a94b6970,VARe4568f4a0e4e55b7e3afa57b3527153272b53fde10f6e292b510a5c3bf797d3d)>=2 + >>> Equal(2, list("xyz"), variable='A').propositions + [VAR658adc74c6913fb83b42c3968866f4bdb967fcad34692fc71d3de5f9a94b6970: -(x,y,z)>=-2, VARe4568f4a0e4e55b7e3afa57b3527153272b53fde10f6e292b510a5c3bf797d3d: +(x,y,z)>=2] + """ + + def __init__(self, value: int, propositions: typing.List[typing.Union[str, puan.variable]], variable: typing.Union[str, puan.variable] = None): + super().__init__( + AtLeast(value=value, propositions=propositions), + AtMost(value=value, propositions=propositions), + variable=variable, + ) + + @staticmethod + def from_json(data: dict, class_map) -> "Equal": + """ + Convert from JSON data to a proposition. + + Returns + ------- + out : :class:`Equal` + """ + propositions = data.get('propositions', []) + return Equal( + value=data.get('value', 1), + propositions=list(map(functools.partial(from_json, class_map=class_map), propositions)), + variable=data.get('id', None) + ) + + def to_json(self) -> typing.Dict[str, typing.Any]: + + """ + Returns proposition as a readable JSON. + + Returns + ------- + out : Dict[str, Any] + """ + d = { + 'type': self.__class__.__name__, + 'value': self.propositions[0].sign*self.propositions[0].value, + 'propositions': list( + map( + operator.methodcaller("to_json"), + self.propositions[0].propositions + ) + ) if len(self.propositions) > 0 else [], + } + if not self.generated_id: + d['id'] = self.id + return d + + class Any(AtLeast): """ @@ -2023,7 +2097,7 @@ def to_json(self) -> typing.Dict[str, typing.Any]: d['id'] = self.id return d -def from_json(data: dict, class_map: list = [puan.variable,AtLeast,AtMost,All,Any,Xor,ExactlyOne,Not,XNor,Imply]) -> typing.Any: +def from_json(data: dict, class_map: list = [puan.variable,AtLeast,AtMost,Equal,All,Any,Xor,ExactlyOne,Not,XNor,Imply]) -> typing.Any: """ Convert from json data to a proposition. diff --git a/puan/modules/configurator/__init__.py b/puan/modules/configurator/__init__.py index 60010a7..690b104 100644 --- a/puan/modules/configurator/__init__.py +++ b/puan/modules/configurator/__init__.py @@ -380,6 +380,7 @@ def from_json(data: dict) -> "StingyConfigurator": pg.Not, pg.XNor, pg.Imply, + pg.Equal, ] return StingyConfigurator( *map( diff --git a/tests/test_puan.py b/tests/test_puan.py index 2f0a17a..9c4388b 100644 --- a/tests/test_puan.py +++ b/tests/test_puan.py @@ -134,6 +134,14 @@ def not_proposition_strategy(): atom_proposition_strategy(), ) +def equal_proposition_strategy(): + return strategies.builds( + pg.Equal, + propositions=atoms_propositions_strategy(), + variable=variable_boolean_proposition_strategy(), + value=strategies.integers(min_value=-5, max_value=5), + ) + def proposition_strategy(): return strategies.one_of( atleast_proposition_strategy(), @@ -144,6 +152,7 @@ def proposition_strategy(): xor_proposition_strategy(), xnor_proposition_strategy(), not_proposition_strategy(), + equal_proposition_strategy() ) def cc_proposition_strategy(): @@ -158,6 +167,7 @@ def cc_proposition_strategy(): xor_cc_proposition_strategy(), xnor_proposition_strategy(), not_proposition_strategy(), + equal_proposition_strategy() ) def propositions_strategy(): @@ -1320,6 +1330,9 @@ def test_json_conversion(): json_model = {"type": "AtMost", "value": 1, "propositions": [{"id": "x", "bounds": {"lower": -10, "upper": 10}}]} assert json_model == pg.AtMost.from_json(json_model, [puan.variable]).to_json() + + json_model = {"type": "Equal", "value": 1, "id": "A", "propositions": [{"id": "x", "bounds": {"lower": -10, "upper": 10}}]} + assert pg.from_json(json_model).to_json() == pg.Equal.from_json(json_model, [puan.variable]).to_json()