Skip to content

Commit

Permalink
fix: Python custom justifications with Decimal scores
Browse files Browse the repository at this point in the history
The translator automatically converts the result of Java method calls
on Java objects into the corresponding Python types. This is normally
wanted, since it allow users to use the result of ConstraintCollectors
(which are Java objects).

However, `to_python_score` does not want this; it expects the normal
Java result. It is typically called in an untranslated context and
thus usually does not run into this issue. However, when a custom
justification function is used, it is called in a translation
context. For normal scores, it works, since we don't access any
properties of the long when creating the corresponding Python
score. For decimal scores, we call toPlainString() so we can
create a Python Decimal instance from a Java BigDecimal.
Python Decimal does not have a toPlainString method normally,
so it causes an attribute lookup failure at runtime.

The fix is to add a toPlainString method to PythonDecimal,
that just delegate to the toPlainString method of its value.
  • Loading branch information
Christopher-Chianelli authored and triceo committed Jan 22, 2025
1 parent 292ea28 commit 8ca431c
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,19 @@ public String toString() {
return value.toPlainString();
}

// Required since to_python_score expects a BigDecimal instance
// (which comes from the Java *BigDecimalScore)
// but the translator will automatically convert all BigDecimals
// into PythonDecimal instances.
// In particular, it expects a toPlainString method so it can
// create a Python decimal from a BigDecimal.
// The function can be called either untranslated (normally)
// or translated (when used in a justification function),
// hence why we define this method.
public PythonString $method$toPlainString() {
return PythonString.valueOf(value.toPlainString());
}

public boolean equals(Object o) {
if (o instanceof PythonNumber number) {
return compareTo(number) == 0;
Expand Down
43 changes: 43 additions & 0 deletions python/python-core/tests/test_constraint_streams.py
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,49 @@ def define_constraints(constraint_factory: ConstraintFactory):
assert len(justifications) == 0


def test_decimal_justifications():
@dataclass(unsafe_hash=True)
class MyJustification(ConstraintJustification):
code: str
score: SimpleDecimalScore

@constraint_provider
def define_constraints(constraint_factory: ConstraintFactory):
return [
constraint_factory.for_each(Entity)
.reward(SimpleDecimalScore.ONE, lambda e: e.value.number)
.justify_with(lambda e, score: MyJustification(e.code, score))
.as_constraint('Maximize value')
]

score_manager = create_score_manager(define_constraints,
solution_class=DecimalSolution)
entity_a: Entity = Entity('A')
entity_b: Entity = Entity('B')

value_1 = Value(1)
value_2 = Value(2)
value_3 = Value(3)

entity_a.value = value_1
entity_b.value = value_3

problem = DecimalSolution([entity_a, entity_b], [value_1, value_2, value_3])

justifications = score_manager.explain(problem).get_justification_list()
assert len(justifications) == 2
assert MyJustification('A', SimpleDecimalScore.of(Decimal(1))) in justifications
assert MyJustification('B', SimpleDecimalScore.of(Decimal(3))) in justifications

justifications = score_manager.explain(problem).get_justification_list(MyJustification)
assert len(justifications) == 2
assert MyJustification('A', SimpleDecimalScore.of(Decimal(1))) in justifications
assert MyJustification('B', SimpleDecimalScore.of(Decimal(3))) in justifications

justifications = score_manager.explain(problem).get_justification_list(DefaultConstraintJustification)
assert len(justifications) == 0


def test_long_scores():
@constraint_provider
def define_constraints(constraint_factory: ConstraintFactory):
Expand Down

0 comments on commit 8ca431c

Please sign in to comment.