Skip to content

Commit c70aea8

Browse files
committed
OptimisationService
1 parent bde45d1 commit c70aea8

File tree

5 files changed

+318
-69
lines changed

5 files changed

+318
-69
lines changed

Diff for: CHANGELOG.md

+22
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@ Added / Changed / Deprecated / Fixed / Removed / Security
1111

1212
> Corresponds to changes in the `develop` branch since the last release
1313
14+
### Added
15+
16+
#### org.ojalgo.type
17+
18+
- A new cache implementation named `ForgetfulMap`. To save you adding a dependency on Caffeine or similar.
19+
20+
#### org.ojalgo.optimisation
21+
22+
- 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.
23+
24+
### Changed
25+
26+
#### org.ojalgo.optimisation
27+
28+
- 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.
29+
30+
### Deprecated
31+
32+
#### org.ojalgo.type
33+
34+
- `TypeCache` is replaced by `ForgetfulMap.ValueCache`.
35+
1436
## [55.0.1] – 2024-11-17
1537

1638
### Added

Diff for: src/main/java/org/ojalgo/optimisation/service/OptimisationService.java

+155-65
Original file line numberDiff line numberDiff line change
@@ -21,109 +21,199 @@
2121
*/
2222
package org.ojalgo.optimisation.service;
2323

24+
import java.io.ByteArrayInputStream;
25+
import java.time.Duration;
26+
import java.util.Arrays;
27+
import java.util.Objects;
28+
import java.util.concurrent.BlockingQueue;
29+
import java.util.concurrent.LinkedBlockingQueue;
30+
31+
import org.ojalgo.RecoverableCondition;
32+
import org.ojalgo.concurrent.Parallelism;
33+
import org.ojalgo.concurrent.ProcessingService;
34+
import org.ojalgo.function.constant.PrimitiveMath;
35+
import org.ojalgo.netio.ASCII;
2436
import org.ojalgo.netio.BasicLogger;
25-
import org.ojalgo.netio.InMemoryFile;
26-
import org.ojalgo.netio.ServiceClient;
27-
import org.ojalgo.netio.ServiceClient.Response;
2837
import org.ojalgo.optimisation.ExpressionsBasedModel;
38+
import org.ojalgo.optimisation.ExpressionsBasedModel.FileFormat;
2939
import org.ojalgo.optimisation.Optimisation;
40+
import org.ojalgo.optimisation.Optimisation.Result;
41+
import org.ojalgo.optimisation.Optimisation.Sense;
42+
import org.ojalgo.optimisation.integer.IntegerSolver;
43+
import org.ojalgo.optimisation.integer.IntegerStrategy;
44+
import org.ojalgo.type.ForgetfulMap;
3045

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

47-
public static final class Integration extends ExpressionsBasedModel.Integration<OptimisationService.Solver> {
56+
public enum Status {
57+
DONE, PENDING;
58+
}
4859

49-
private static final String PATH_ENVIRONMENT = "/optimisation/v01/environment";
50-
private static final String PATH_TEST = "/optimisation/v01/test";
60+
static final class Problem {
5161

52-
private Boolean myCapable = null;
53-
private final String myHost;
62+
private final byte[] myContents;
63+
private final FileFormat myFormat;
64+
private final String myKey;
65+
private final Optimisation.Sense mySense;
5466

55-
Integration(final String host) {
67+
Problem(final String key, final Sense sense, final byte[] contents, final FileFormat format) {
5668
super();
57-
myHost = host;
69+
myKey = key;
70+
mySense = sense;
71+
myContents = contents;
72+
myFormat = format;
5873
}
5974

6075
@Override
61-
public OptimisationService.Solver build(final ExpressionsBasedModel model) {
62-
return new OptimisationService.Solver(model, myHost);
76+
public boolean equals(final Object obj) {
77+
if (this == obj) {
78+
return true;
79+
}
80+
if (!(obj instanceof Problem)) {
81+
return false;
82+
}
83+
Problem other = (Problem) obj;
84+
return Arrays.equals(myContents, other.myContents) && myFormat == other.myFormat && Objects.equals(myKey, other.myKey) && mySense == other.mySense;
6385
}
6486

65-
public String getEnvironment() {
66-
return ServiceClient.get(myHost + PATH_ENVIRONMENT).getBody();
87+
@Override
88+
public int hashCode() {
89+
final int prime = 31;
90+
int result = 1;
91+
result = prime * result + Arrays.hashCode(myContents);
92+
result = prime * result + Objects.hash(myFormat, myKey, mySense);
93+
return result;
6794
}
6895

69-
@Override
70-
public boolean isCapable(final ExpressionsBasedModel model) {
96+
byte[] getContents() {
97+
return myContents;
98+
}
7199

72-
if (myCapable == null) {
73-
myCapable = this.test();
74-
}
100+
FileFormat getFormat() {
101+
return myFormat;
102+
}
75103

76-
return Boolean.TRUE.equals(myCapable);
104+
String getKey() {
105+
return myKey;
77106
}
78107

79-
public Boolean test() {
80-
Response<String> response = ServiceClient.get(myHost + PATH_TEST);
81-
if (response.isResponseOK() && response.getBody().contains("VALID")) {
82-
return Boolean.TRUE;
83-
} else {
84-
BasicLogger.error("Calling {} failed!", myHost + PATH_TEST);
85-
return Boolean.FALSE;
86-
}
108+
Optimisation.Sense getSense() {
109+
return mySense;
87110
}
88111

89112
}
90113

91-
static final class Solver implements Optimisation.Solver {
92-
93-
private static final String PATH_MAXIMISE = "/optimisation/v01/maximise";
94-
private static final String PATH_MINIMISE = "/optimisation/v01/minimise";
114+
private static final Result FAILED = Optimisation.Result.of(PrimitiveMath.NaN, Optimisation.State.FAILED);
95115

96-
private final String myHost;
97-
private final ExpressionsBasedModel myModel;
98-
private final Optimisation.Sense myOptimisationSense;
116+
public static ServiceIntegration newIntegration(final String host) {
117+
return ServiceIntegration.newInstance(host);
118+
}
99119

100-
Solver(final ExpressionsBasedModel model, final String host) {
101-
super();
102-
myModel = model;
103-
myOptimisationSense = model.getOptimisationSense();
104-
myHost = host;
120+
private static Optimisation.Result doOptimise(final Sense sense, final byte[] contents, final FileFormat format) throws RecoverableCondition {
121+
try (ByteArrayInputStream input = new ByteArrayInputStream(contents)) {
122+
ExpressionsBasedModel model = ExpressionsBasedModel.parse(input, format);
123+
model.options.progress(IntegerSolver.class);
124+
return sense == Sense.MAX ? model.maximise() : model.minimise();
125+
} catch (Exception cause) {
126+
throw new RecoverableCondition(cause);
105127
}
128+
}
106129

107-
@Override
108-
public Result solve(final Result kickStarter) {
130+
private static String generateKey() {
131+
return ASCII.generateRandom(16, ASCII::isAlphanumeric);
132+
}
133+
134+
private final int myNumberOfWorkers;
135+
private final Optimisation.Options myOptimisationOptions;
136+
private final ProcessingService myProcessingService = ProcessingService.newInstance("optimisation-worker");
137+
private final BlockingQueue<Problem> myQueue = new LinkedBlockingQueue<>(128);
138+
private final ForgetfulMap<String, Optimisation.Result> myResultCache = ForgetfulMap.newBuilder().expireAfterAccess(Duration.ofHours(1)).build();
139+
140+
private final ForgetfulMap<String, Status> myStatusCache = ForgetfulMap.newBuilder().expireAfterAccess(Duration.ofHours(1)).build();
109141

110-
InMemoryFile file = new InMemoryFile();
142+
public OptimisationService() {
111143

112-
myModel.simplify().writeTo(file);
144+
super();
113145

114-
// String modelAsString = file.getContentsAsString();
146+
Parallelism baseParallelism = Parallelism.THREADS;
147+
int nbStrategies = IntegerStrategy.DEFAULT.countUniqueStrategies();
115148

116-
Response<String> response = myOptimisationSense == Optimisation.Sense.MAX
117-
? ServiceClient.post(myHost + PATH_MAXIMISE, file.getContentsAsByteArray())
118-
: ServiceClient.post(myHost + PATH_MINIMISE, file.getContentsAsByteArray());
149+
myNumberOfWorkers = baseParallelism.divideBy(2 * nbStrategies).getAsInt();
150+
151+
IntegerStrategy integerStrategy = IntegerStrategy.DEFAULT.withParallelism(baseParallelism.divideBy(myNumberOfWorkers));
152+
153+
myOptimisationOptions = new Optimisation.Options();
154+
myOptimisationOptions.integer(integerStrategy);
155+
156+
myProcessingService.take(myQueue, myNumberOfWorkers, this::doOptimise);
157+
}
158+
159+
public Optimisation.Result getResult(final String key) {
160+
return myResultCache.getIfPresent(key);
161+
}
119162

120-
String responseBody = response.getBody();
121-
return Optimisation.Result.parse(responseBody);
163+
public Status getStatus(final String key) {
164+
return myStatusCache.getIfPresent(key);
165+
}
166+
167+
public Optimisation.Result optimise(final Sense sense, final byte[] contents, final FileFormat format) {
168+
try {
169+
return OptimisationService.doOptimise(sense, contents, format);
170+
} catch (RecoverableCondition cause) {
171+
BasicLogger.error("Optimisation failed!", cause);
172+
return FAILED;
122173
}
174+
}
175+
176+
public String putOnQueue(final Optimisation.Sense sense, final byte[] contents, final FileFormat format) throws RecoverableCondition {
177+
178+
String key = OptimisationService.generateKey();
179+
180+
Problem problem = new Problem(key, sense, contents, format);
123181

182+
if (myQueue.offer(problem)) {
183+
184+
myStatusCache.put(key, Status.PENDING);
185+
186+
return key;
187+
188+
} else {
189+
190+
throw new RecoverableCondition("Queue is full!");
191+
}
124192
}
125193

126-
public static OptimisationService.Integration newIntegration(final String host) {
127-
return new Integration(host);
194+
private void doOptimise(final Problem problem) {
195+
196+
try {
197+
198+
Sense sense = problem.getSense();
199+
byte[] contents = problem.getContents();
200+
FileFormat format = problem.getFormat();
201+
202+
Optimisation.Result result = OptimisationService.doOptimise(sense, contents, format);
203+
204+
myResultCache.put(problem.getKey(), result);
205+
206+
} catch (RecoverableCondition cause) {
207+
208+
BasicLogger.error("Optimisation failed!", cause);
209+
210+
myResultCache.put(problem.getKey(), FAILED);
211+
212+
} finally {
213+
214+
myStatusCache.put(problem.getKey(), Status.DONE);
215+
}
216+
128217
}
218+
129219
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 1997-2024 Optimatika
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in
12+
* all copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
package org.ojalgo.optimisation.service;
23+
24+
import org.ojalgo.netio.BasicLogger;
25+
import org.ojalgo.netio.ServiceClient;
26+
import org.ojalgo.netio.ServiceClient.Response;
27+
import org.ojalgo.optimisation.ExpressionsBasedModel;
28+
29+
public final class ServiceIntegration extends ExpressionsBasedModel.Integration<ServiceSolver> {
30+
31+
private static final String PATH_ENVIRONMENT = "/optimisation/v01/environment";
32+
private static final String PATH_TEST = "/optimisation/v01/test";
33+
34+
public static ServiceIntegration newInstance(final String host) {
35+
return new ServiceIntegration(host);
36+
}
37+
38+
private Boolean myCapable = null;
39+
private final String myHost;
40+
41+
ServiceIntegration(final String host) {
42+
super();
43+
myHost = host;
44+
}
45+
46+
@Override
47+
public ServiceSolver build(final ExpressionsBasedModel model) {
48+
return new ServiceSolver(model, myHost);
49+
}
50+
51+
public String getEnvironment() {
52+
return ServiceClient.get(myHost + PATH_ENVIRONMENT).getBody();
53+
}
54+
55+
@Override
56+
public boolean isCapable(final ExpressionsBasedModel model) {
57+
58+
if (myCapable == null) {
59+
myCapable = this.test();
60+
}
61+
62+
return Boolean.TRUE.equals(myCapable);
63+
}
64+
65+
public Boolean test() {
66+
Response<String> response = ServiceClient.get(myHost + PATH_TEST);
67+
if (response.isResponseOK() && response.getBody().contains("VALID")) {
68+
return Boolean.TRUE;
69+
} else {
70+
BasicLogger.error("Calling {} failed!", myHost + PATH_TEST);
71+
return Boolean.FALSE;
72+
}
73+
}
74+
75+
}

0 commit comments

Comments
 (0)