diff --git a/CHANGELOG.md b/CHANGELOG.md
index d948ea02d..48cef1263 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/src/main/java/org/ojalgo/optimisation/service/OptimisationService.java b/src/main/java/org/ojalgo/optimisation/service/OptimisationService.java
index 19d7f3e0c..8f3998b17 100644
--- a/src/main/java/org/ojalgo/optimisation/service/OptimisationService.java
+++ b/src/main/java/org/ojalgo/optimisation/service/OptimisationService.java
@@ -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).
- *
- * There is a test/demo version of that service available at: http://test-service.optimatika.se
- *
- * That particular instance is NOT for production use, and may be restricted or removed without warning.
- *
- * 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:
+ *
+ * - Put optimisation problems on the solve queue bu calling {@link #putOnQueue(Sense, byte[], FileFormat)}
+ *
- Check the status of the optimisation by calling {@link #getStatus(String)} – is it {@link Status#DONE}
+ * or still {@link Status#PENDING}?
+ *
- 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 {
+ 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 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 myQueue = new LinkedBlockingQueue<>(128);
+ private final ForgetfulMap myResultCache = ForgetfulMap.newBuilder().expireAfterAccess(Duration.ofHours(1)).build();
+
+ private final ForgetfulMap 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 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);
+ }
+
}
+
}
diff --git a/src/main/java/org/ojalgo/optimisation/service/ServiceIntegration.java b/src/main/java/org/ojalgo/optimisation/service/ServiceIntegration.java
new file mode 100644
index 000000000..801094fd2
--- /dev/null
+++ b/src/main/java/org/ojalgo/optimisation/service/ServiceIntegration.java
@@ -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 {
+
+ 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 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;
+ }
+ }
+
+}
diff --git a/src/main/java/org/ojalgo/optimisation/service/ServiceSolver.java b/src/main/java/org/ojalgo/optimisation/service/ServiceSolver.java
new file mode 100644
index 000000000..0cf73a06f
--- /dev/null
+++ b/src/main/java/org/ojalgo/optimisation/service/ServiceSolver.java
@@ -0,0 +1,62 @@
+/*
+ * 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.InMemoryFile;
+import org.ojalgo.netio.ServiceClient;
+import org.ojalgo.netio.ServiceClient.Response;
+import org.ojalgo.optimisation.ExpressionsBasedModel;
+import org.ojalgo.optimisation.Optimisation;
+
+final class ServiceSolver implements Optimisation.Solver {
+
+ private static final String PATH_MAXIMISE = "/optimisation/v01/maximise";
+ private static final String PATH_MINIMISE = "/optimisation/v01/minimise";
+
+ private final String myHost;
+ private final ExpressionsBasedModel myModel;
+ private final Optimisation.Sense myOptimisationSense;
+
+ ServiceSolver(final ExpressionsBasedModel model, final String host) {
+ super();
+ myModel = model;
+ myOptimisationSense = model.getOptimisationSense();
+ myHost = host;
+ }
+
+ @Override
+ public Result solve(final Result kickStarter) {
+
+ InMemoryFile file = new InMemoryFile();
+
+ myModel.simplify().writeTo(file);
+
+ // String modelAsString = file.getContentsAsString();
+
+ Response response = myOptimisationSense == Optimisation.Sense.MAX ? ServiceClient.post(myHost + PATH_MAXIMISE, file.getContentsAsByteArray())
+ : ServiceClient.post(myHost + PATH_MINIMISE, file.getContentsAsByteArray());
+
+ String responseBody = response.getBody();
+ return Optimisation.Result.parse(responseBody);
+ }
+
+}
diff --git a/src/test/java/org/ojalgo/optimisation/service/OptimisationServiceTest.java b/src/test/java/org/ojalgo/optimisation/service/ServiceIntegrationTest.java
similarity index 84%
rename from src/test/java/org/ojalgo/optimisation/service/OptimisationServiceTest.java
rename to src/test/java/org/ojalgo/optimisation/service/ServiceIntegrationTest.java
index 1c38712e9..e18720ec0 100644
--- a/src/test/java/org/ojalgo/optimisation/service/OptimisationServiceTest.java
+++ b/src/test/java/org/ojalgo/optimisation/service/ServiceIntegrationTest.java
@@ -9,7 +9,7 @@
import org.ojalgo.optimisation.Optimisation.Result;
@Tag("network")
-public class OptimisationServiceTest {
+public class ServiceIntegrationTest {
private static final String HOST = "http://16.16.99.66:8080";
// private static final String HOST = "http://localhost:8080";
@@ -25,7 +25,7 @@ public void clearIntegrations() {
@Test
public void testEnvironment() {
- String environment = OptimisationService.newIntegration(HOST).getEnvironment();
+ String environment = ServiceIntegration.newInstance(HOST).getEnvironment();
if (DEBUG) {
BasicLogger.debug(environment);
@@ -37,7 +37,7 @@ public void testEnvironment() {
@Test
public void testTest() {
- TestUtils.assertTrue(OptimisationService.newIntegration(HOST).test().booleanValue());
+ TestUtils.assertTrue(ServiceIntegration.newInstance(HOST).test().booleanValue());
}
@Test
@@ -53,7 +53,7 @@ public void testVeryBasicModel() {
Result expMax = model.maximise();
Result expMin = model.minimise();
- ExpressionsBasedModel.addIntegration(OptimisationService.newIntegration(HOST));
+ ExpressionsBasedModel.addIntegration(ServiceIntegration.newInstance(HOST));
Result actMax = model.maximise();
Result actMin = model.minimise();