Skip to content

Commit ef52c67

Browse files
authored
Merge pull request #58 from ourstudio-se/57-add-a-test-for-default-prio-as-given-below
Added test for multiple defaults and a new evaluate function
2 parents 1010972 + 766d4f7 commit ef52c67

4 files changed

Lines changed: 198 additions & 77 deletions

File tree

puan/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,12 @@ class Bounds:
4545
lower: int = default_min_int
4646
upper: int = default_max_int
4747

48+
def __init__(self, lower: int, upper: int):
49+
if lower > upper:
50+
raise ValueError(f"upper bound must be higher than lower bound, got: ({lower}, {upper})")
51+
self.lower = lower
52+
self.upper = upper
53+
4854
def __iter__(self):
4955
return iter([self.lower, self.upper])
5056

puan/logic/plog/__init__.py

Lines changed: 94 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ def flatten(self) -> list:
196196
197197
Examples
198198
--------
199-
>>> proposition = AtLeast(1, [AtLeast(1, ["a", "b"], "B"), AtLeast(1, ["c", "d"], "C"), "e"], "A")
199+
>>> proposition = AtLeast(1, [AtLeast(1, ["a", "b"], "B"),
200+
... AtLeast(1, ["c", "d"], "C"), "e"], "A")
200201
>>> proposition.flatten()
201202
[A: +(B,C,e)>=1, B: +(a,b)>=1, C: +(c,d)>=1, variable(id='a', bounds=Bounds(lower=0, upper=1)), variable(id='b', bounds=Bounds(lower=0, upper=1)), variable(id='c', bounds=Bounds(lower=0, upper=1)), variable(id='d', bounds=Bounds(lower=0, upper=1)), variable(id='e', bounds=Bounds(lower=0, upper=1))]
202203
"""
@@ -515,6 +516,7 @@ def is_contradiction(self) -> bool:
515516
>>> model = AtMost(-1,["x","y"])
516517
>>> model.is_contradiction
517518
True
519+
518520
519521
Returns
520522
-------
@@ -523,60 +525,118 @@ def is_contradiction(self) -> bool:
523525
# When the highest sum from equation still not satisfied inequality, this is a contradition
524526
return self.equation_bounds[1] < 0
525527

526-
def evaluate(self, interpretation: typing.List[puan.SolutionVariable]) -> bool:
528+
def evaluate(self, interpretation: typing.Dict[typing.Union[str, puan.variable], int]) -> bool:
527529

528530
"""
529531
Evaluates interpretation on this model. It will evaluate sub propositions
530532
bottoms-up and propagate results upwards. This means that even though
531533
intermediate variables are not set in interpretation, they receive a value
532534
based on the evaluation of its propositions.
533535
536+
Parameters
537+
----------
538+
interpretation: typing.Dict[typing.Union[str, puan.variable], int]
539+
the values of the variables in the model to evaluate it for
540+
534541
Examples
535542
--------
536-
>>> All(*"xy", variable="A").evaluate([puan.SolutionVariable("x", value=1)])
543+
>>> All(*"xy", variable="A").evaluate({"x": 1})
537544
False
538545
539-
>>> All(*"xy", variable="A").evaluate([puan.SolutionVariable("x", value=1), puan.SolutionVariable("y", value=1)])
546+
>>> All(*"xy", variable="A").evaluate(
547+
... {puan.variable("x"): 1, puan.variable("y"): 1})
540548
True
541549
542-
>>> AtLeast(propositions=[puan.variable("x", dtype="int")], value=10).evaluate([puan.SolutionVariable("x", value=9)])
550+
>>> AtLeast(propositions=[puan.variable("x", dtype="int")],
551+
... value=10).evaluate({puan.variable("x"): 9})
543552
False
544553
545-
>>> AtLeast(propositions=[puan.variable("x", dtype="int")], value=10).evaluate([puan.SolutionVariable("x", value=10)])
554+
>>> AtLeast(propositions=[puan.variable("x", dtype="int")],
555+
... value=10).evaluate({puan.variable("x"): 10})
546556
True
557+
558+
See also
559+
--------
560+
evaluate_propositions : Evaluates propositions on this model given a dict with variables and their values.
561+
562+
Returns
563+
-------
564+
bool
547565
"""
548566

549-
interpretation_map = dict(
550-
zip(
551-
map(
552-
operator.attrgetter("id"),
553-
interpretation
554-
),
555-
map(
556-
operator.attrgetter("value"),
557-
interpretation
558-
),
559-
)
560-
)
567+
return self.evaluate_propositions(interpretation)[self.variable.id] > 0
561568

562-
return (
563-
sum(
564-
map(
565-
lambda x: interpretation_map.get(x.id, 0)*self.sign if not type(x) == bool else x*1,
566-
itertools.chain(
567-
self.atomic_propositions,
568-
map(
569-
operator.methodcaller(
570-
"evaluate",
571-
interpretation=interpretation,
572-
),
573-
self.compound_propositions
569+
def evaluate_propositions(self, interpretation: typing.Dict[typing.Union[str, puan.variable], int]) -> typing.Dict[typing.Union[str, puan.variable], int]:
570+
"""
571+
Evaluates propositions on this model given a dict with variables and their values.
572+
573+
Parameters
574+
----------
575+
interpretation: typing.Dict[typing.Union[str, puan.variable]
576+
the values of the variables in the model to evaluate it for
577+
578+
Notes
579+
-----
580+
Bounds and dtypes of puan.variables in the interpretation are neglected, those values are only considered for the variables of the model.
581+
582+
A variable which is not included in the initial dict is calculated from its sub propositions or
583+
defaulted to its lower bound (if variable doesn't have any subpropositions).
584+
585+
Examples
586+
--------
587+
>>> All(*"xy", variable="A").evaluate_propositions({"x": 1})
588+
{'x': 1, 'y': 0, 'A': 0}
589+
590+
>>> All(*"xy", variable="A").evaluate_propositions({"x": 1, "y": 1})
591+
{'x': 1, 'y': 1, 'A': 1}
592+
593+
>>> AtLeast(propositions=[puan.variable("x", dtype="int")],
594+
... value=10).evaluate_propositions({"x": 9})
595+
{'x': 9, 'VARa6aef82726db5033e4b25e6fec8b5770cf89fe44ff8336731eef2bfa9a8ab35f': 0}
596+
597+
>>> AtLeast(propositions=[puan.variable("x", dtype="int")],
598+
... value=10).evaluate_propositions({"x": 10})
599+
{'x': 10, 'VARa6aef82726db5033e4b25e6fec8b5770cf89fe44ff8336731eef2bfa9a8ab35f': 1}
600+
601+
See also
602+
--------
603+
evaluate : Evaluates interpretation on this model.
604+
605+
Returns
606+
-------
607+
out : typing.Dict[typing.Union[str, puan.variable], int]
608+
"""
609+
def _check_variable_in_bounds(id, val, bounds):
610+
if val not in range(bounds.lower, bounds.upper+1):
611+
raise ValueError("Variable {} is out of bounds, value: {}, bounds: {}".format(id, val, bounds))
612+
def _get_variable_val(variable, interpretation):
613+
if not variable in interpretation.keys():
614+
interpretation[variable.id] = variable.bounds.lower
615+
return interpretation
616+
def _evaluate_propositions(prop, interpretation):
617+
if not prop.variable.id in interpretation.keys():
618+
# Calculate variable value and add to dict
619+
val = sum(
620+
itertools.chain(
621+
map(
622+
lambda x: _evaluate_propositions(x, interpretation)[x.id]*prop.sign,
623+
prop.compound_propositions
624+
),
625+
map(
626+
lambda x: _get_variable_val(x, interpretation)[x.id]*prop.sign,
627+
prop.atomic_propositions
628+
)
629+
)
574630
)
631+
_check_variable_in_bounds(prop.variable.id, 1*(val-prop.value >= 0), prop.variable.bounds)
632+
interpretation[prop.variable.id] = 1*(
633+
val-prop.value >= 0
575634
)
576-
)
577-
)-self.value
578-
) >= 0
579-
635+
return interpretation
636+
interpretation_map = dict(map(lambda x: (x.id, x), filter(lambda x: type(x) == puan.variable, self.flatten())))
637+
for x, y in filter(lambda x: x[0] in interpretation_map, interpretation.items()):
638+
_check_variable_in_bounds(x, y, interpretation_map[x].bounds)
639+
return _evaluate_propositions(self, interpretation)
580640

581641
def to_short(self) -> tuple:
582642

puan/ndarray/__init__.py

Lines changed: 32 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1170,33 +1170,38 @@ def row_distribution(self, row_index: int) -> numpy.ndarray:
11701170
Column index 0 is a range from lowest to highest value of row equation.
11711171
Column index 1 is a counter of how many combinations, generated from variable bounds, evaluated into that value.
11721172
1173-
Example 1:
1174-
Equation = x+y+0
1175-
Result = array([
1176-
[0, 1],
1177-
[1, 2],
1178-
[2, 1]
1179-
])
1180-
1181-
2 | <- 2 combinations ([1,0], [0,1]) results in value 1
1182-
1 | | | <- 1 combination ([0,0]) results in value 0 and 1 combination ([1,1]) results in value 2
1183-
0 ----------
1184-
0 1 2
1185-
1186-
Example 2:
1187-
Equation = 2x+2y+0
1188-
Result = array([
1189-
[0, 1],
1190-
[1, 0],
1191-
[2, 2],
1192-
[3, 0],
1193-
[4, 1]
1194-
])
1195-
1196-
2 |
1197-
1 | | |
1198-
0 ---------------
1199-
0 1 2 3 4
1173+
1174+
.. code-block::
1175+
:caption: Example 1
1176+
1177+
Equation = x+y+0
1178+
Result = array([
1179+
[0, 1],
1180+
[1, 2],
1181+
[2, 1]
1182+
])
1183+
1184+
2 | <- 2 combinations ([1,0], [0,1]) results in value 1
1185+
1 | | | <- 1 combination ([0,0]) results in value 0
1186+
---------- and 1 combination ([1,1]) results in value 2
1187+
0 1 2
1188+
1189+
.. code-block::
1190+
:caption: Example 2
1191+
1192+
Equation = 2x+2y+0
1193+
Result = array([
1194+
[0, 1],
1195+
[1, 0],
1196+
[2, 2],
1197+
[3, 0],
1198+
[4, 1]
1199+
])
1200+
1201+
2 |
1202+
1 | | |
1203+
---------------
1204+
0 1 2 3 4
12001205
12011206
Notes
12021207
-----

tests/test_puan.py

Lines changed: 66 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ def atom_proposition_strategy():
3636
puan.variable,
3737
id=st.text(),
3838
bounds=st.tuples(
39-
st.integers(min_value=-10, max_value=10),
40-
st.integers(min_value=-10, max_value=10),
39+
st.integers(min_value=-5, max_value=0),
40+
st.integers(min_value=0, max_value=5),
4141
),
4242
)
4343

@@ -1670,6 +1670,56 @@ def test_not_when_single_str_or_variable():
16701670
for entity in ["a", puan.variable("a")]:
16711671
assert type(pg.Not(entity)) in [pg.All, pg.AtLeast]
16721672

1673+
def test_multiple_defaults():
1674+
#a -> p ^ q (q)
1675+
#a -> p ^ r (r)
1676+
model = cc.StingyConfigurator(pg.All(pg.Imply("a", cc.Xor(*"pq", default="q")), pg.Imply("a", cc.Xor(*"pr", default="r"))))
1677+
config1 = {"a": 1, "p": 0, "q": 1, "r": 1}
1678+
config2 = {"a": 1, "p": 1}
1679+
full_config1 = model.evaluate_propositions(config1)
1680+
full_config2 = model.evaluate_propositions(config2)
1681+
keys1 = list(full_config1.keys())[:-1]
1682+
keys1.sort()
1683+
keys2 = list(full_config2.keys())[:-1]
1684+
keys2.sort()
1685+
int_ndarray1 = numpy.array(list(map(lambda x: full_config1[x], keys1)))
1686+
int_ndarray2 = numpy.array(list(map(lambda x: full_config2[x], keys2)))
1687+
objective_function = puan.ndarray.ndint_compress(model.polyhedron.default_prio_vector, method="shadow")
1688+
assert objective_function.dot(int_ndarray1) > objective_function.dot(int_ndarray2)
1689+
1690+
def test_evaluate_propositions():
1691+
# Raises ValueError when configuration variable is out of bounds
1692+
with pytest.raises(ValueError):
1693+
pg.All(*"xy", variable="A").evaluate_propositions({"x": 3})
1694+
1695+
# Unsatisfiable model
1696+
assert pg.AtLeast(2, "x").evaluate_propositions({"x": 0}) == {'VAR13c463c68a23bc633637b613e63e0025e2d498cf52ef76877ae6ed3475f4aba6': 0, "x": 0}
1697+
1698+
# Faulty configuration input
1699+
with pytest.raises(ValueError):
1700+
pg.All(*"xy", variable="A").evaluate_propositions({"x": "str"})
1701+
1702+
# Variable defaults to lower bounds when not given as configuration
1703+
assert pg.AtLeast(1, [puan.variable("x", bounds=(0,1))], variable="A").evaluate_propositions({}) == {"A": 0, "x": 0}
1704+
assert pg.AtLeast(1, [puan.variable("x", bounds=(1,10))], variable="A").evaluate_propositions({}) == {"A": 1, "x": 1}
1705+
1706+
# Puan.variable as input in configuration
1707+
assert pg.All(*"xy", variable="A").evaluate_propositions({puan.variable("x"): 1}) == {puan.variable(id='x', bounds=(0, 1)): 1, 'A': 0, 'y': 0}
1708+
1709+
def configuration_dict_strategy():
1710+
return st.dictionaries(st.text(), st.integers(-2,2))
1711+
1712+
@given(proposition_strategy(), configuration_dict_strategy())
1713+
def test_propositions_evaluations(proposition, configuration):
1714+
value_error_raised = False
1715+
try:
1716+
evaluate_propositions_result = proposition.evaluate_propositions(configuration)[proposition.variable.id] > 0
1717+
evaluate_result = proposition.evaluate(configuration)
1718+
except ValueError:
1719+
value_error_raised = True
1720+
if not value_error_raised:
1721+
assert evaluate_propositions_result == evaluate_result
1722+
16731723
# def test_assuming_integer_variables():
16741724

16751725
# """
@@ -1764,7 +1814,7 @@ def test_bound_approx():
17641814
])
17651815
assert (actual == expected).all()
17661816

1767-
# When a taighter bound exists
1817+
# When a tighter bound exists
17681818
actual = puan.ndarray.ge_polyhedron([
17691819
[ 3, 1, 0, 0, 0], # a_lb = 3
17701820
[ 3, 0, 1, 0, 0], # a_lb = 3
@@ -2416,22 +2466,22 @@ def test_plog_evaluate_method():
24162466
variable="fridge"
24172467
)
24182468

2419-
cart = [
2420-
puan.SolutionVariable.from_variable(milk_home, 1),
2421-
puan.SolutionVariable.from_variable(milk_bought, 0),
2422-
puan.SolutionVariable.from_variable(tomatoes, 2+2),
2423-
puan.SolutionVariable.from_variable(cucumbers, 0),
2424-
]
2469+
cart = {
2470+
milk_home: 1,
2471+
milk_bought: 0,
2472+
tomatoes: 2+2,
2473+
cucumbers: 0
2474+
}
24252475

24262476
assert not fridge_model.evaluate(cart)
24272477

2428-
new_cart = [
2429-
puan.SolutionVariable.from_variable(chips, 1),
2430-
puan.SolutionVariable.from_variable(milk_home, 1),
2431-
puan.SolutionVariable.from_variable(milk_bought, 0),
2432-
puan.SolutionVariable.from_variable(tomatoes, 2+2),
2433-
puan.SolutionVariable.from_variable(cucumbers, 1),
2434-
]
2478+
new_cart = {
2479+
chips: 1,
2480+
milk_home: 1,
2481+
milk_bought: 0,
2482+
tomatoes: 2+2,
2483+
cucumbers: 1
2484+
}
24352485

24362486
assert fridge_model.evaluate(new_cart)
24372487

0 commit comments

Comments
 (0)