Skip to content

Conversation

@mikand
Copy link
Contributor

@mikand mikand commented Sep 14, 2017

Added initial support for Optimization Modulo Theory.

Currently, the only "real" optimizing solver we have is Z3, but the extension is designed to be general and extensible, on the same line as the solver factory.

Moreover, this PR introduces two mixins that can use any solver with an incremental or "solving under assumption" interface to perform a simple optimization loop on the SAT side. The technique is not complete and can diverge in general, but it is simple and also effective in some cases.

@marcogario marcogario added this to the 0.7.5 milestone Sep 16, 2017
@marcogario
Copy link
Contributor

Is this ready for review?

@mikand
Copy link
Contributor Author

mikand commented Sep 27, 2017

Not really, I would like to add some more tests and features, but if you have time for an early review it is most welcome! 😄

@marcogario marcogario modified the milestones: 0.7.5, 0.8.0 May 20, 2018
@mikand mikand requested a review from marcogario January 21, 2019 16:02
@mikand
Copy link
Contributor Author

mikand commented Jan 22, 2019

Interestingly, the tests trigger a deterministic segmentation fault on z3 with python3.6 for windows. I will try to reproduce locally.

@marcogario
Copy link
Contributor

👋 Thanks for pushing this through! I will re-review this in the next few days. Since the PR description is almost 2yr old 👴 , could you make sure that the PR description captures the changes that were made?

@mikand
Copy link
Contributor Author

mikand commented Mar 31, 2020

@marcogario wait before merging this. With @arrighi79 and @PatrickTrentin88 we are working on a more comprehensive and adherent refactoring of the optimization. I will use this branch as "master" for a series of PRs and then we'll merge this afterwards.

@marcogario marcogario modified the milestones: 0.9.0, Backlog Apr 18, 2020
Copy link
Contributor

@marcogario marcogario left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very promising! 🎉

The PR is doing quite a bunch of things and I think it would better to split it in more manageable chunks.

  1. The optimization interface using "dumb" SMT solvers
  2. Integration with OptiMathsat
  3. Other changes (e.g., bug fixes in configuration, Z3 interpolants)

I am not particularly convinced that the dynamic loading of OptiMathsat is the best way to proceed. This adds a layer of indirection that needs to be resolved for basically every solver call. I wonder if this introduces a performance regression. Could you update the description of the PR explaining the rationale for this approach and alternatives that were considered?

Comment on lines +1123 to +1130
# Check if we are working on a version MathSAT supporting quantifier elimination
self._msat_lib = MSATLibLoader(self.__class__.__lib_name__)
if not hasattr(self._msat_lib, "MSAT_EXIST_ELIM_ALLSMT_FM"):
raise NotImplementedError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we still need this?

Interface for the optimization
"""

def optimize(self, cost_function, **kwargs):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this called minimize? optimize does not make it clear in which direction we are working (min/max).

Comment on lines 94 to 115
def _setup(self):
raise NotImplementedError

def _cleanup(self, client_data):
raise NotImplementedError

def _pareto_setup(self, client_data):
raise NotImplementedError

def _pareto_cleanup(self, client_data):
raise NotImplementedError

def _pareto_check_progress(self, client_data, cost_functions,
costs_so_far, lts, les):
raise NotImplementedError

def _pareto_block_model(self, client_data, cost_functions, last_model, lts, les):
raise NotImplementedError

def _optimization_check_progress(self, client_data, cost_function,
cost_so_far, lt, le, strategy):
raise NotImplementedError
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to document what an implementation of these methods would do?

feasible_solution_callback=None,
step_size=1, **kwargs):
"""This function performs the optimization as described in
`Optimizer.optimize()`. However. two additional parameters are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`Optimizer.optimize()`. However. two additional parameters are
`Optimizer.optimize()`. However. three additional parameters are

the satisfiable models.
`feasible_solution_callback` is a function with a single
argument or None. If specified, the function will be called
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The argument can be None? If so

Suggested change
argument or None. If specified, the function will be called
argument (possibly None). If specified, the function will be called

@marcogario marcogario modified the milestones: Backlog, 1.0.0 May 23, 2020
@yxliang01 yxliang01 mentioned this pull request Oct 26, 2020
@mikand mikand assigned mikand and unassigned marcogario Dec 22, 2020
@mikand
Copy link
Contributor Author

mikand commented May 12, 2025

Ciao @marcogario,

while @Framba-Luca fixes the tests and re-aligns the code with master, can you have a quick look and tell us if something is missing? I would like to merge this soon!

@mikand mikand requested a review from marcogario May 12, 2025 11:56
Copy link
Contributor

@marcogario marcogario left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've taken a very superficial look as I do not have much context on the change and this is quite a large PR.

It seems the test coverage is quite good, so I don't see a reason to delay merging these changes.

Maybe 2 things that we might add (as a follow-up is fine) are some words about this feature in the README and a dedicated example in the examples/ folder.

You have tests and you have quite a fair amount of explanation in optimizer.py about the various types of optimization problems. The goal of examples/ is to have that information in a format that is more "didactic", so people can quickly grasp the capabilities of pysmt and understand how to do something.

Comment on lines 202 to 211
# check if gmpy2 is installed or not and decide which numeric classes are supported
accepted_integer_numbers_class = (int, )
accepted_real_numbers_class = (float, Fraction)
try:
import gmpy2
accepted_integer_numbers_class += (gmpy2.mpz, )
accepted_real_numbers_class += (gmpy2.mpq, gmpy2.mpfr)
except ImportError:
pass
accepted_numbers_class = accepted_integer_numbers_class + accepted_real_numbers_class
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit odd that you are running this half-way through the file. Pushing this to the top or having dedicated files per goal type would make this more obvious.

from warnings import warn

from pysmt import git_version
from pysmt import __version__
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. This change could probably be its own PR.

Comment on lines -41 to -45
STR_LENGTH, STR_CONCAT, STR_CONTAINS,
STR_INDEXOF, STR_REPLACE, STR_SUBSTR,
STR_PREFIXOF, STR_SUFFIXOF,
STR_TO_INT, INT_TO_STR,
STR_CHARAT,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume these were unused, otherwise CI would go 💥 , right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We checked, this is just a cleanup of the imports, the FNodel class only uses the STR_OPERATORS set

Comment on lines 223 to 224
self.id = MaxSMTGoal._instance_id
MaxSMTGoal._instance_id = MaxSMTGoal._instance_id + 1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👀 Why do you need this?

Comment on lines 32 to 35
"""Returns a pair `(model, cost)` where `model` is an object
that obtained according to `goal` while satisfying all
the formulae asserted in the optimizer, while `cost` is the
objective function value for the model.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"""Returns a pair `(model, cost)` where `model` is an object
that obtained according to `goal` while satisfying all
the formulae asserted in the optimizer, while `cost` is the
objective function value for the model.
"""Returns a pair `(model, cost)` where `model` is an object
obtained according to `goal` while satisfying all
the formulae asserted in the optimizer, while `cost` is the
objective function value for the model.

self.client_data = client_data

def linear_search_cut(self):
"""must be called always"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is not really clear. Maybe the assumptions on how to use this object should be part of the class doctstring.

Comment on lines 34 to 38
# TODO:
# - check msat does not instantiate any MSAT class directly (use virtual override)
# - is it possible to reintroduce file-level try-except for library import?
# - the "Not in Python's Path" message is wrong for MathSAT when only OptiMAthSAT
# is installed.. the current implementation must be revised.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean-up?

Comment on lines 80 to 81
# TODO: support FP
# TODO: support signed/unsigned BV optimization
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean-up?

warnings.warn("Test (%s, %s) skipped because Boolector can't handle QF_UF." % (logic, smtfile))
return
log = script.evaluate(solver)
print(log)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean-up?

Comment on lines +279 to +284
(QF_LIA, "smtlib2_load_objective_model.smt2", SAT, {
OptimizationTypes.BOXED: [Int(3), Int(30)],
OptimizationTypes.PARETO: [ # TODO weird. if pareto is tested before boxed in optimsat it works, otherwise it fails
(Int(x), Int(2*x + 20)) for x in range(3, 6)
],
OptimizationTypes.LEXICOGRAPHIC: [Int(3), Int(26)],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you need to create a follow-up issue for this?

Comment on lines 468 to 478
if not goal.is_maxsmt_goal():
term_type = goal.term().get_type()
# For bit-vectors, start lower and upper bounds to their width limit
if term_type.is_bv_type():
# add 1 top upper bound because it is excluded
if goal.signed:
lower = -(2**(term_type.width-1))
upper = (2**(term_type.width-1))
else:
lower = 0
upper = (2**term_type.width) + 1
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why put this here and not in the constructor of OptSearchInterval?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note after sync: being that minimization has [lower, upper) and maximization has (lower, upper], here we have to check if we are minimizing or maximizing to understand if we have to add 1 to the upper (when maximizing) or remove 1 to the lower (when minimizing)

if formula is not None:
self.add_assertion(formula)
rt = self.solve()
# TODO for example in z3 the get_model must be called before the pop.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a design bug: after the pop() we are NOT guaranteed to have access to the model, so it would be better to get it before the pop() and return it to the caller (also in the pareto case)

Comment on lines 284 to 285
add = 1 if self._obj.is_minimization_goal() else 0
return (l + u + add) // 2
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. using a +1 is OK-ish for QF_LIA and QF_BV objectives, but not for QF_LRA (where we can simply use / 2 instead of '// 2')
  2. I think the general rule is: if I am minimizing, then at each step an optimal solution s is s.t. lower < s <= upper. Instead, for maximization, lower <= s < upper. We have to document this. However, this means that if I have lower=-10 and upper=-9 in the case of maximization I would get a pivot=-9 which would make the algorithm loop. I think a simpler approach would be to compute the pivot as (l+u) // 2 and then adjust it with a +1 or -1 if it is outside of the ranges above.

…ome edge cases. maxsmt on real with binary strategy could diverge
…on diverging for unbounded cases and use that method in tests that use the Real logic, avoiding non-endin tests in CI
Comment on lines 27 to 36
# check if gmpy2 is installed or not and decide which numeric classes are supported
accepted_integer_numbers_class = (int, )
accepted_real_numbers_class = (float, Fraction)
try:
import gmpy2
accepted_integer_numbers_class += (gmpy2.mpz, )
accepted_real_numbers_class += (gmpy2.mpq, gmpy2.mpfr)
except ImportError:
pass
accepted_numbers_class = accepted_integer_numbers_class + accepted_real_numbers_class
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code could be removed, as we now ude the Real or Int constructors

Comment on lines -41 to -45
STR_LENGTH, STR_CONCAT, STR_CONTAINS,
STR_INDEXOF, STR_REPLACE, STR_SUBSTR,
STR_PREFIXOF, STR_SUFFIXOF,
STR_TO_INT, INT_TO_STR,
STR_CHARAT,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We checked, this is just a cleanup of the imports, the FNodel class only uses the STR_OPERATORS set


@clear_pending_pop
def optimize(self, goal, **kwargs):
msat_obj = self._assert_msat_goal(goal, 0)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that this works if called multiple times. (Add tests)

@mikand mikand merged commit d1a1a25 into master May 27, 2025
42 of 49 checks passed
@mikand mikand deleted the optimization branch May 27, 2025 12:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants