Skip to content

Commit

Permalink
OptimisationService
Browse files Browse the repository at this point in the history
  • Loading branch information
apete committed Nov 24, 2024
1 parent bde45d1 commit c70aea8
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 69 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ Added / Changed / Deprecated / Fixed / Removed / Security

> Corresponds to changes in the `develop` branch since the last release
### Added

#### org.ojalgo.type

- A new cache implementation named `ForgetfulMap`. To save you adding a dependency on Caffeine or similar.

#### org.ojalgo.optimisation

- There is a new `OptimisationService` implementation with which you can queue up optimisation problems to have them solved. This service is (will be) configurable regarding how many problems to work on concurrently, and how many threads the solvers can use, among other things.

### Changed

#### org.ojalgo.optimisation

- Refactoring and additions to what's in the `org.ojalgo.optimisation.service` package. They're breaking changes, but most likely no one outside Optimatika used this.

### Deprecated

#### org.ojalgo.type

- `TypeCache` is replaced by `ForgetfulMap.ValueCache`.

## [55.0.1] – 2024-11-17

### Added
Expand Down
220 changes: 155 additions & 65 deletions src/main/java/org/ojalgo/optimisation/service/OptimisationService.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,109 +21,199 @@
*/
package org.ojalgo.optimisation.service;

import java.io.ByteArrayInputStream;
import java.time.Duration;
import java.util.Arrays;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import org.ojalgo.RecoverableCondition;
import org.ojalgo.concurrent.Parallelism;
import org.ojalgo.concurrent.ProcessingService;
import org.ojalgo.function.constant.PrimitiveMath;
import org.ojalgo.netio.ASCII;
import org.ojalgo.netio.BasicLogger;
import org.ojalgo.netio.InMemoryFile;
import org.ojalgo.netio.ServiceClient;
import org.ojalgo.netio.ServiceClient.Response;
import org.ojalgo.optimisation.ExpressionsBasedModel;
import org.ojalgo.optimisation.ExpressionsBasedModel.FileFormat;
import org.ojalgo.optimisation.Optimisation;
import org.ojalgo.optimisation.Optimisation.Result;
import org.ojalgo.optimisation.Optimisation.Sense;
import org.ojalgo.optimisation.integer.IntegerSolver;
import org.ojalgo.optimisation.integer.IntegerStrategy;
import org.ojalgo.type.ForgetfulMap;

/**
* {@link Solver} and {@link Integration} implementations that make use of Optimatika's
* Optimisation-as-a-Service (OaaS).
* <p>
* There is a test/demo version of that service available at: http://test-service.optimatika.se
* <p>
* That particular instance is NOT for production use, and may be restricted or removed without warning.
* <p>
* If you'd like access to a service instance for (private) production use, you should contact Optimatika
* using: https://www.optimatika.se/products-services-inquiry/
*
* @author apete
* @see https://www.optimatika.se/products-services-inquiry/
* Basic usage:
* <ol>
* <li>Put optimisation problems on the solve queue bu calling {@link #putOnQueue(Sense, byte[], FileFormat)}
* <li>Check the status of the optimisation by calling {@link #getStatus(String)} – is it {@link Status#DONE}
* or still {@link Status#PENDING}?
* <li>Get the result of the optimisation by calling {@link #getResult(String)} – when {@link Status#DONE}
*/
public abstract class OptimisationService {
public final class OptimisationService {

public static final class Integration extends ExpressionsBasedModel.Integration<OptimisationService.Solver> {
public enum Status {
DONE, PENDING;
}

private static final String PATH_ENVIRONMENT = "/optimisation/v01/environment";
private static final String PATH_TEST = "/optimisation/v01/test";
static final class Problem {

private Boolean myCapable = null;
private final String myHost;
private final byte[] myContents;
private final FileFormat myFormat;
private final String myKey;
private final Optimisation.Sense mySense;

Integration(final String host) {
Problem(final String key, final Sense sense, final byte[] contents, final FileFormat format) {
super();
myHost = host;
myKey = key;
mySense = sense;
myContents = contents;
myFormat = format;
}

@Override
public OptimisationService.Solver build(final ExpressionsBasedModel model) {
return new OptimisationService.Solver(model, myHost);
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Problem)) {
return false;
}
Problem other = (Problem) obj;
return Arrays.equals(myContents, other.myContents) && myFormat == other.myFormat && Objects.equals(myKey, other.myKey) && mySense == other.mySense;
}

public String getEnvironment() {
return ServiceClient.get(myHost + PATH_ENVIRONMENT).getBody();
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.hashCode(myContents);
result = prime * result + Objects.hash(myFormat, myKey, mySense);
return result;
}

@Override
public boolean isCapable(final ExpressionsBasedModel model) {
byte[] getContents() {
return myContents;
}

if (myCapable == null) {
myCapable = this.test();
}
FileFormat getFormat() {
return myFormat;
}

return Boolean.TRUE.equals(myCapable);
String getKey() {
return myKey;
}

public Boolean test() {
Response<String> response = ServiceClient.get(myHost + PATH_TEST);
if (response.isResponseOK() && response.getBody().contains("VALID")) {
return Boolean.TRUE;
} else {
BasicLogger.error("Calling {} failed!", myHost + PATH_TEST);
return Boolean.FALSE;
}
Optimisation.Sense getSense() {
return mySense;
}

}

static final class Solver implements Optimisation.Solver {

private static final String PATH_MAXIMISE = "/optimisation/v01/maximise";
private static final String PATH_MINIMISE = "/optimisation/v01/minimise";
private static final Result FAILED = Optimisation.Result.of(PrimitiveMath.NaN, Optimisation.State.FAILED);

private final String myHost;
private final ExpressionsBasedModel myModel;
private final Optimisation.Sense myOptimisationSense;
public static ServiceIntegration newIntegration(final String host) {
return ServiceIntegration.newInstance(host);
}

Solver(final ExpressionsBasedModel model, final String host) {
super();
myModel = model;
myOptimisationSense = model.getOptimisationSense();
myHost = host;
private static Optimisation.Result doOptimise(final Sense sense, final byte[] contents, final FileFormat format) throws RecoverableCondition {
try (ByteArrayInputStream input = new ByteArrayInputStream(contents)) {
ExpressionsBasedModel model = ExpressionsBasedModel.parse(input, format);
model.options.progress(IntegerSolver.class);
return sense == Sense.MAX ? model.maximise() : model.minimise();
} catch (Exception cause) {
throw new RecoverableCondition(cause);
}
}

@Override
public Result solve(final Result kickStarter) {
private static String generateKey() {
return ASCII.generateRandom(16, ASCII::isAlphanumeric);
}

private final int myNumberOfWorkers;
private final Optimisation.Options myOptimisationOptions;
private final ProcessingService myProcessingService = ProcessingService.newInstance("optimisation-worker");
private final BlockingQueue<Problem> myQueue = new LinkedBlockingQueue<>(128);
private final ForgetfulMap<String, Optimisation.Result> myResultCache = ForgetfulMap.newBuilder().expireAfterAccess(Duration.ofHours(1)).build();

private final ForgetfulMap<String, Status> myStatusCache = ForgetfulMap.newBuilder().expireAfterAccess(Duration.ofHours(1)).build();

InMemoryFile file = new InMemoryFile();
public OptimisationService() {

myModel.simplify().writeTo(file);
super();

// String modelAsString = file.getContentsAsString();
Parallelism baseParallelism = Parallelism.THREADS;
int nbStrategies = IntegerStrategy.DEFAULT.countUniqueStrategies();

Response<String> response = myOptimisationSense == Optimisation.Sense.MAX
? ServiceClient.post(myHost + PATH_MAXIMISE, file.getContentsAsByteArray())
: ServiceClient.post(myHost + PATH_MINIMISE, file.getContentsAsByteArray());
myNumberOfWorkers = baseParallelism.divideBy(2 * nbStrategies).getAsInt();

IntegerStrategy integerStrategy = IntegerStrategy.DEFAULT.withParallelism(baseParallelism.divideBy(myNumberOfWorkers));

myOptimisationOptions = new Optimisation.Options();
myOptimisationOptions.integer(integerStrategy);

myProcessingService.take(myQueue, myNumberOfWorkers, this::doOptimise);
}

public Optimisation.Result getResult(final String key) {
return myResultCache.getIfPresent(key);
}

String responseBody = response.getBody();
return Optimisation.Result.parse(responseBody);
public Status getStatus(final String key) {
return myStatusCache.getIfPresent(key);
}

public Optimisation.Result optimise(final Sense sense, final byte[] contents, final FileFormat format) {
try {
return OptimisationService.doOptimise(sense, contents, format);
} catch (RecoverableCondition cause) {
BasicLogger.error("Optimisation failed!", cause);
return FAILED;
}
}

public String putOnQueue(final Optimisation.Sense sense, final byte[] contents, final FileFormat format) throws RecoverableCondition {

String key = OptimisationService.generateKey();

Problem problem = new Problem(key, sense, contents, format);

if (myQueue.offer(problem)) {

myStatusCache.put(key, Status.PENDING);

return key;

} else {

throw new RecoverableCondition("Queue is full!");
}
}

public static OptimisationService.Integration newIntegration(final String host) {
return new Integration(host);
private void doOptimise(final Problem problem) {

try {

Sense sense = problem.getSense();
byte[] contents = problem.getContents();
FileFormat format = problem.getFormat();

Optimisation.Result result = OptimisationService.doOptimise(sense, contents, format);

myResultCache.put(problem.getKey(), result);

} catch (RecoverableCondition cause) {

BasicLogger.error("Optimisation failed!", cause);

myResultCache.put(problem.getKey(), FAILED);

} finally {

myStatusCache.put(problem.getKey(), Status.DONE);
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright 1997-2024 Optimatika
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.ojalgo.optimisation.service;

import org.ojalgo.netio.BasicLogger;
import org.ojalgo.netio.ServiceClient;
import org.ojalgo.netio.ServiceClient.Response;
import org.ojalgo.optimisation.ExpressionsBasedModel;

public final class ServiceIntegration extends ExpressionsBasedModel.Integration<ServiceSolver> {

private static final String PATH_ENVIRONMENT = "/optimisation/v01/environment";
private static final String PATH_TEST = "/optimisation/v01/test";

public static ServiceIntegration newInstance(final String host) {
return new ServiceIntegration(host);
}

private Boolean myCapable = null;
private final String myHost;

ServiceIntegration(final String host) {
super();
myHost = host;
}

@Override
public ServiceSolver build(final ExpressionsBasedModel model) {
return new ServiceSolver(model, myHost);
}

public String getEnvironment() {
return ServiceClient.get(myHost + PATH_ENVIRONMENT).getBody();
}

@Override
public boolean isCapable(final ExpressionsBasedModel model) {

if (myCapable == null) {
myCapable = this.test();
}

return Boolean.TRUE.equals(myCapable);
}

public Boolean test() {
Response<String> response = ServiceClient.get(myHost + PATH_TEST);
if (response.isResponseOK() && response.getBody().contains("VALID")) {
return Boolean.TRUE;
} else {
BasicLogger.error("Calling {} failed!", myHost + PATH_TEST);
return Boolean.FALSE;
}
}

}
Loading

0 comments on commit c70aea8

Please sign in to comment.