Skip to content

Improve optimization API and add fallback implementation #477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/Optimization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

38 changes: 38 additions & 0 deletions src/org/sosy_lab/java_smt/SolverContextFactory.java
Original file line number Diff line number Diff line change
Expand Up @@ -366,4 +366,42 @@ public static SolverContext createSolverContext(Solvers solver)
NativeLibraries::loadLibrary)
.generateContext(solver);
}

/**
* Create a new optimization prover environment.
* @param context The solver context
* @param options The prover options
* @return An optimization prover environment
* @throws SolverOptimizationException if the solver doesn't support optimization
*/
public static OptimizationProverEnvironment createOptimizationProver(
Copy link
Member

Choose a reason for hiding this comment

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

The SolverContextFactory is intended to construct a SolverContext. This method violates this constraint, as it constructs a ProverEnvironment.

SolverContext context, Set<ProverOptions> options) {
Solvers solver = context.getSolver();

// Check if optimization is supported
if (!SolverVersionChecker.isOptimizationSupported(solver)) {
logger.log(Level.WARNING, SolverVersionChecker.getOptimizationSupportMessage(solver));
return new FallbackOptimizationProver(context, logger, context.getFormulaManager(), options);
}

// Check version compatibility for MathSAT5
if (solver == Solvers.MATHSAT5) {
try {
SolverVersionChecker.checkMathSATVersion(context.getVersion());
} catch (SolverOptimizationException e) {
logger.log(Level.WARNING, e.getMessage());
return new FallbackOptimizationProver(context, logger, context.getFormulaManager(), options);
}
}

// Create solver-specific optimization prover
switch (solver) {
case Z3:
return new Z3OptimizationProver(/*...*/);
case MATHSAT5:
return new MathSATOptimizationProver(/*...*/);
default:
return new FallbackOptimizationProver(context, logger, context.getFormulaManager(), options);
}
}
}
31 changes: 31 additions & 0 deletions src/org/sosy_lab/java_smt/api/SolverOptimizationException.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.sosy_lab.java_smt.api;

/**
* Exception thrown when a solver does not support optimization features
* or when there are version-specific optimization issues.
*/
public class SolverOptimizationException extends SolverException {
Copy link
Member

Choose a reason for hiding this comment

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

This looks like a complex way of throwing an UnsupportedOperationException, as it is done for other unsupported features, too, such as Interpolation in several solvers (see https://github.com/sosy-lab/java-smt/blob/master/src/org/sosy_lab/java_smt/solvers/bitwuzla/BitwuzlaSolverContext.java#L308). A possible benefit is that this exception class extends SolverException, but this makes it inconsistent to other unsupported features.

private static final long serialVersionUID = 1L;
private final String solverName;
private final String feature;

public SolverOptimizationException(String solverName, String feature) {
super(String.format("%s does not support %s", solverName, feature));
this.solverName = solverName;
this.feature = feature;
}

public SolverOptimizationException(String solverName, String feature, String reason) {
super(String.format("%s does not support %s: %s", solverName, feature, reason));
this.solverName = solverName;
this.feature = feature;
}

public String getSolverName() {
return solverName;
}

public String getFeature() {
return feature;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package org.sosy_lab.java_smt.basicimpl;

import org.sosy_lab.common.log.LogManager;
import org.sosy_lab.java_smt.api.BasicProverEnvironment;
import org.sosy_lab.java_smt.api.Formula;
import org.sosy_lab.java_smt.api.FormulaManager;
import org.sosy_lab.java_smt.api.OptimizationProverEnvironment;
import org.sosy_lab.java_smt.api.SolverException;
import org.sosy_lab.common.rationals.Rational;

import java.util.Collection;
import java.util.Optional;

public abstract class AbstractOptimizationProver extends AbstractProver implements OptimizationProverEnvironment {

protected final LogManager logger;
protected final FormulaManager formulaManager;

protected AbstractOptimizationProver(LogManager pLogger, FormulaManager pMgr) {
this.logger = pLogger;
this.formulaManager = pMgr;
}

@Override
public boolean isOptimizationSupported() {
return false; // Default implementation
}

@Override
public int maximize(Formula objective) {
if (!isOptimizationSupported()) {
throw new UnsupportedOperationException(
"Optimization not supported by " + getSolverName());
}
return maximizeInternal(objective);
}

@Override
public int minimize(Formula objective) {
if (!isOptimizationSupported()) {
throw new UnsupportedOperationException(
"Optimization not supported by " + getSolverName());
}
return minimizeInternal(objective);
}

protected abstract int maximizeInternal(Formula objective);
protected abstract int minimizeInternal(Formula objective);
protected abstract String getSolverName();

@Override
public Optional<Rational> upper(int handle, Rational epsilon) {
if (!isOptimizationSupported()) {
throw new UnsupportedOperationException(
"Optimization not supported by " + getSolverName());
}
return upperInternal(handle, epsilon);
}

@Override
public Optional<Rational> lower(int handle, Rational epsilon) {
if (!isOptimizationSupported()) {
throw new UnsupportedOperationException(
"Optimization not supported by " + getSolverName());
}
return lowerInternal(handle, epsilon);
}

protected abstract Optional<Rational> upperInternal(int handle, Rational epsilon);
protected abstract Optional<Rational> lowerInternal(int handle, Rational epsilon);
}
203 changes: 203 additions & 0 deletions src/org/sosy_lab/java_smt/basicimpl/FallbackOptimizationProver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
package org.sosy_lab.java_smt.basicimpl;

import org.sosy_lab.common.log.LogManager;
import org.sosy_lab.java_smt.api.BasicProverEnvironment;
import org.sosy_lab.java_smt.api.BooleanFormula;
import org.sosy_lab.java_smt.api.Formula;
import org.sosy_lab.java_smt.api.FormulaManager;
import org.sosy_lab.java_smt.api.IntegerFormulaManager;
import org.sosy_lab.java_smt.api.NumeralFormula;
import org.sosy_lab.java_smt.api.NumeralFormula.IntegerFormula;
import org.sosy_lab.java_smt.api.NumeralFormula.RationalFormula;
import org.sosy_lab.java_smt.api.OptimizationProverEnvironment;
import org.sosy_lab.java_smt.api.SolverContext;
import org.sosy_lab.java_smt.api.SolverException;
import org.sosy_lab.common.rationals.Rational;

import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.logging.Level;

/**
* A fallback implementation of OptimizationProverEnvironment for solvers
* that don't support optimization natively. This implementation uses
* iterative solving to approximate optimization results.
*/
public class FallbackOptimizationProver extends AbstractOptimizationProver {
private final BasicProverEnvironment baseProver;
private final SolverContext context;
private final Map<Integer, Formula> objectives;
private int nextHandle;

public FallbackOptimizationProver(
SolverContext pContext,
LogManager pLogger,
FormulaManager pMgr,
Set<SolverContext.ProverOptions> pOptions) {
super(pLogger, pMgr);
this.context = pContext;
this.baseProver = pContext.newProverEnvironment(pOptions);
this.objectives = new HashMap<>();
this.nextHandle = 0;
}

@Override
public boolean isOptimizationSupported() {
return true; // We always support optimization through fallback
}

@Override
protected String getSolverName() {
return context.getSolverName();
}

@Override
protected int maximizeInternal(Formula objective) {
int handle = nextHandle++;
objectives.put(handle, objective);
return handle;
}

@Override
protected int minimizeInternal(Formula objective) {
// For minimization, we negate the objective and maximize
FormulaManager fmgr = context.getFormulaManager();
if (objective instanceof IntegerFormula) {
IntegerFormulaManager imgr = fmgr.getIntegerFormulaManager();
return maximizeInternal(imgr.negate((IntegerFormula) objective));
} else if (objective instanceof RationalFormula) {
// Handle rational formulas similarly
// Implementation depends on the specific formula manager
throw new UnsupportedOperationException(
"Rational optimization not yet implemented in fallback");
} else {
throw new UnsupportedOperationException(
"Unsupported formula type for optimization: " + objective.getClass());
}
}

@Override
protected Optional<Rational> upperInternal(int handle, Rational epsilon) {
Formula objective = objectives.get(handle);
if (objective == null) {
throw new IllegalArgumentException("Invalid objective handle: " + handle);
}

try {
// Start with a large upper bound
Rational upperBound = Rational.ofLong(1000000);
Rational lowerBound = Rational.ofLong(-1000000);

// Binary search to find the maximum value
while (upperBound.subtract(lowerBound).compareTo(epsilon) > 0) {
Rational mid = upperBound.add(lowerBound).divide(Rational.ofLong(2));

// Create constraint: objective <= mid
BooleanFormula constraint = createUpperBoundConstraint(objective, mid);
baseProver.push();
baseProver.addConstraint(constraint);

try {
if (baseProver.isUnsat()) {
// No solution exists with this upper bound
upperBound = mid;
} else {
// Solution exists, try a higher value
lowerBound = mid;
}
} finally {
baseProver.pop();
}
}

return Optional.of(upperBound);
Copy link
Member

Choose a reason for hiding this comment

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

Binary search is a valid idea and it should work in most applications. As a first prototype, this might work for several use cases. The performance overhead is not that large, e.g., several SAT checks are required (which is ok) and the number of them is logarithmic in the bounding window size (which is good).

There are some open points:

  • We need to think about upper/lower bounds on unbounded theories like Integer or Rational.
  • Maybe keep the prover content if the next contraint it stricter than the previous. This might improve reuse and perfomance on larger queries.

} catch (SolverException | InterruptedException e) {
logger.log(Level.WARNING, "Error during optimization", e);
return Optional.empty();
}
}

@Override
protected Optional<Rational> lowerInternal(int handle, Rational epsilon) {
Formula objective = objectives.get(handle);
if (objective == null) {
throw new IllegalArgumentException("Invalid objective handle: " + handle);
}

try {
// Start with a small lower bound
Rational lowerBound = Rational.ofLong(-1000000);
Rational upperBound = Rational.ofLong(1000000);

// Binary search to find the minimum value
while (upperBound.subtract(lowerBound).compareTo(epsilon) > 0) {
Rational mid = upperBound.add(lowerBound).divide(Rational.ofLong(2));

// Create constraint: objective >= mid
BooleanFormula constraint = createLowerBoundConstraint(objective, mid);
baseProver.push();
baseProver.addConstraint(constraint);

try {
if (baseProver.isUnsat()) {
// No solution exists with this lower bound
lowerBound = mid;
} else {
// Solution exists, try a lower value
upperBound = mid;
}
} finally {
baseProver.pop();
}
}

return Optional.of(lowerBound);
} catch (SolverException | InterruptedException e) {
logger.log(Level.WARNING, "Error during optimization", e);
return Optional.empty();
}
}

private BooleanFormula createUpperBoundConstraint(Formula objective, Rational bound) {
FormulaManager fmgr = context.getFormulaManager();
if (objective instanceof IntegerFormula) {
IntegerFormulaManager imgr = fmgr.getIntegerFormulaManager();
return imgr.lessOrEquals(
(IntegerFormula) objective,
imgr.makeNumber(bound.longValue()));
} else if (objective instanceof RationalFormula) {
// Handle rational formulas similarly
// Implementation depends on the specific formula manager
throw new UnsupportedOperationException(
"Rational optimization not yet implemented in fallback");
} else {
throw new UnsupportedOperationException(
"Unsupported formula type for optimization: " + objective.getClass());
}
}

private BooleanFormula createLowerBoundConstraint(Formula objective, Rational bound) {
FormulaManager fmgr = context.getFormulaManager();
if (objective instanceof IntegerFormula) {
IntegerFormulaManager imgr = fmgr.getIntegerFormulaManager();
return imgr.greaterOrEquals(
(IntegerFormula) objective,
imgr.makeNumber(bound.longValue()));
} else if (objective instanceof RationalFormula) {
// Handle rational formulas similarly
// Implementation depends on the specific formula manager
throw new UnsupportedOperationException(
"Rational optimization not yet implemented in fallback");
} else {
throw new UnsupportedOperationException(
"Unsupported formula type for optimization: " + objective.getClass());
}
}

@Override
public void close() {
baseProver.close();
}
}
Loading