|
21 | 21 | */
|
22 | 22 | package org.ojalgo.optimisation.service;
|
23 | 23 |
|
| 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; |
24 | 36 | import org.ojalgo.netio.BasicLogger;
|
25 |
| -import org.ojalgo.netio.InMemoryFile; |
26 |
| -import org.ojalgo.netio.ServiceClient; |
27 |
| -import org.ojalgo.netio.ServiceClient.Response; |
28 | 37 | import org.ojalgo.optimisation.ExpressionsBasedModel;
|
| 38 | +import org.ojalgo.optimisation.ExpressionsBasedModel.FileFormat; |
29 | 39 | 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; |
30 | 45 |
|
31 | 46 | /**
|
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} |
44 | 53 | */
|
45 |
| -public abstract class OptimisationService { |
| 54 | +public final class OptimisationService { |
46 | 55 |
|
47 |
| - public static final class Integration extends ExpressionsBasedModel.Integration<OptimisationService.Solver> { |
| 56 | + public enum Status { |
| 57 | + DONE, PENDING; |
| 58 | + } |
48 | 59 |
|
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 { |
51 | 61 |
|
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; |
54 | 66 |
|
55 |
| - Integration(final String host) { |
| 67 | + Problem(final String key, final Sense sense, final byte[] contents, final FileFormat format) { |
56 | 68 | super();
|
57 |
| - myHost = host; |
| 69 | + myKey = key; |
| 70 | + mySense = sense; |
| 71 | + myContents = contents; |
| 72 | + myFormat = format; |
58 | 73 | }
|
59 | 74 |
|
60 | 75 | @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; |
63 | 85 | }
|
64 | 86 |
|
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; |
67 | 94 | }
|
68 | 95 |
|
69 |
| - @Override |
70 |
| - public boolean isCapable(final ExpressionsBasedModel model) { |
| 96 | + byte[] getContents() { |
| 97 | + return myContents; |
| 98 | + } |
71 | 99 |
|
72 |
| - if (myCapable == null) { |
73 |
| - myCapable = this.test(); |
74 |
| - } |
| 100 | + FileFormat getFormat() { |
| 101 | + return myFormat; |
| 102 | + } |
75 | 103 |
|
76 |
| - return Boolean.TRUE.equals(myCapable); |
| 104 | + String getKey() { |
| 105 | + return myKey; |
77 | 106 | }
|
78 | 107 |
|
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; |
87 | 110 | }
|
88 | 111 |
|
89 | 112 | }
|
90 | 113 |
|
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); |
95 | 115 |
|
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 | + } |
99 | 119 |
|
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); |
105 | 127 | }
|
| 128 | + } |
106 | 129 |
|
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(); |
109 | 141 |
|
110 |
| - InMemoryFile file = new InMemoryFile(); |
| 142 | + public OptimisationService() { |
111 | 143 |
|
112 |
| - myModel.simplify().writeTo(file); |
| 144 | + super(); |
113 | 145 |
|
114 |
| - // String modelAsString = file.getContentsAsString(); |
| 146 | + Parallelism baseParallelism = Parallelism.THREADS; |
| 147 | + int nbStrategies = IntegerStrategy.DEFAULT.countUniqueStrategies(); |
115 | 148 |
|
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 | + } |
119 | 162 |
|
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; |
122 | 173 | }
|
| 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); |
123 | 181 |
|
| 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 | + } |
124 | 192 | }
|
125 | 193 |
|
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 | + |
128 | 217 | }
|
| 218 | + |
129 | 219 | }
|
0 commit comments