linny-r 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/console.js +973 -0
  4. package/package.json +32 -0
  5. package/server.js +1547 -0
  6. package/static/fonts/FantasqueSansMono-Bold.ttf +0 -0
  7. package/static/fonts/FantasqueSansMono-BoldItalic.ttf +0 -0
  8. package/static/fonts/FantasqueSansMono-Italic.ttf +0 -0
  9. package/static/fonts/FantasqueSansMono-Regular.ttf +0 -0
  10. package/static/fonts/Hack-Bold.ttf +0 -0
  11. package/static/fonts/Hack-BoldItalic.ttf +0 -0
  12. package/static/fonts/Hack-Italic.ttf +0 -0
  13. package/static/fonts/Hack-Regular.ttf +0 -0
  14. package/static/fonts/Lato-Bold.ttf +0 -0
  15. package/static/fonts/Lato-BoldItalic.ttf +0 -0
  16. package/static/fonts/Lato-Italic.ttf +0 -0
  17. package/static/fonts/Lato-Regular.ttf +0 -0
  18. package/static/fonts/mplus-1m-bold.ttf +0 -0
  19. package/static/fonts/mplus-1m-light.ttf +0 -0
  20. package/static/fonts/mplus-1m-medium.ttf +0 -0
  21. package/static/fonts/mplus-1m-regular.ttf +0 -0
  22. package/static/fonts/mplus-1m-thin.ttf +0 -0
  23. package/static/images/access.png +0 -0
  24. package/static/images/actor.png +0 -0
  25. package/static/images/actors.png +0 -0
  26. package/static/images/add-selector.png +0 -0
  27. package/static/images/add.png +0 -0
  28. package/static/images/back.png +0 -0
  29. package/static/images/black-box.png +0 -0
  30. package/static/images/by-sa.svg +74 -0
  31. package/static/images/cancel.png +0 -0
  32. package/static/images/chart.png +0 -0
  33. package/static/images/check-disab.png +0 -0
  34. package/static/images/check-off.png +0 -0
  35. package/static/images/check-on.png +0 -0
  36. package/static/images/check-x.png +0 -0
  37. package/static/images/clone.png +0 -0
  38. package/static/images/close.png +0 -0
  39. package/static/images/cluster.png +0 -0
  40. package/static/images/compare.png +0 -0
  41. package/static/images/compress.png +0 -0
  42. package/static/images/constraint.png +0 -0
  43. package/static/images/copy.png +0 -0
  44. package/static/images/data-to-clpbrd.png +0 -0
  45. package/static/images/dataset.png +0 -0
  46. package/static/images/delete.png +0 -0
  47. package/static/images/diagram.png +0 -0
  48. package/static/images/down.png +0 -0
  49. package/static/images/edit-chart.png +0 -0
  50. package/static/images/edit.png +0 -0
  51. package/static/images/eq.png +0 -0
  52. package/static/images/equation.png +0 -0
  53. package/static/images/experiment.png +0 -0
  54. package/static/images/favicon.ico +0 -0
  55. package/static/images/fewer-dec.png +0 -0
  56. package/static/images/filter.png +0 -0
  57. package/static/images/find.png +0 -0
  58. package/static/images/forward.png +0 -0
  59. package/static/images/host-logo.png +0 -0
  60. package/static/images/icon.png +0 -0
  61. package/static/images/icon.svg +23 -0
  62. package/static/images/ignore.png +0 -0
  63. package/static/images/include.png +0 -0
  64. package/static/images/info-to-clpbrd.png +0 -0
  65. package/static/images/info.png +0 -0
  66. package/static/images/is-black-box.png +0 -0
  67. package/static/images/lbl.png +0 -0
  68. package/static/images/lift.png +0 -0
  69. package/static/images/link.png +0 -0
  70. package/static/images/linny-r.icns +0 -0
  71. package/static/images/linny-r.ico +0 -0
  72. package/static/images/linny-r.png +0 -0
  73. package/static/images/linny-r.svg +21 -0
  74. package/static/images/logo.png +0 -0
  75. package/static/images/model-info.png +0 -0
  76. package/static/images/module.png +0 -0
  77. package/static/images/monitor.png +0 -0
  78. package/static/images/more-dec.png +0 -0
  79. package/static/images/ne.png +0 -0
  80. package/static/images/new.png +0 -0
  81. package/static/images/note.png +0 -0
  82. package/static/images/ok.png +0 -0
  83. package/static/images/open.png +0 -0
  84. package/static/images/outcome.png +0 -0
  85. package/static/images/parent.png +0 -0
  86. package/static/images/paste.png +0 -0
  87. package/static/images/pause.png +0 -0
  88. package/static/images/print-chart.png +0 -0
  89. package/static/images/print.png +0 -0
  90. package/static/images/process.png +0 -0
  91. package/static/images/product.png +0 -0
  92. package/static/images/pwlf.png +0 -0
  93. package/static/images/receiver.png +0 -0
  94. package/static/images/redo.png +0 -0
  95. package/static/images/remove.png +0 -0
  96. package/static/images/rename.png +0 -0
  97. package/static/images/repo-logo.png +0 -0
  98. package/static/images/repository.png +0 -0
  99. package/static/images/reset.png +0 -0
  100. package/static/images/resize.png +0 -0
  101. package/static/images/restore.png +0 -0
  102. package/static/images/save-chart.png +0 -0
  103. package/static/images/save-data.png +0 -0
  104. package/static/images/save-diagram.png +0 -0
  105. package/static/images/save.png +0 -0
  106. package/static/images/sensitivity.png +0 -0
  107. package/static/images/settings.png +0 -0
  108. package/static/images/solve.png +0 -0
  109. package/static/images/solver-logo.png +0 -0
  110. package/static/images/stats-to-clpbrd.png +0 -0
  111. package/static/images/stats.png +0 -0
  112. package/static/images/stop.png +0 -0
  113. package/static/images/store.png +0 -0
  114. package/static/images/stretch.png +0 -0
  115. package/static/images/table-to-clpbrd.png +0 -0
  116. package/static/images/table.png +0 -0
  117. package/static/images/tree.png +0 -0
  118. package/static/images/tudelft.png +0 -0
  119. package/static/images/ubl.png +0 -0
  120. package/static/images/undo.png +0 -0
  121. package/static/images/up.png +0 -0
  122. package/static/images/zoom-in.png +0 -0
  123. package/static/images/zoom-out.png +0 -0
  124. package/static/index.html +3088 -0
  125. package/static/linny-r.css +4722 -0
  126. package/static/scripts/iro.min.js +7 -0
  127. package/static/scripts/linny-r-config.js +105 -0
  128. package/static/scripts/linny-r-ctrl.js +1199 -0
  129. package/static/scripts/linny-r-gui.js +14814 -0
  130. package/static/scripts/linny-r-milp.js +286 -0
  131. package/static/scripts/linny-r-model.js +10405 -0
  132. package/static/scripts/linny-r-utils.js +687 -0
  133. package/static/scripts/linny-r-vm.js +7079 -0
  134. package/static/show-diff.html +84 -0
  135. package/static/show-png.html +113 -0
  136. package/static/sounds/error.wav +0 -0
  137. package/static/sounds/notification.wav +0 -0
  138. package/static/sounds/warning.wav +0 -0
@@ -0,0 +1,286 @@
1
+ /*
2
+ Linny-R is an executable graphical specification language for (mixed integer)
3
+ linear programming (MILP) problems, especially unit commitment problems (UCP).
4
+ The Linny-R language and tool have been developed by Pieter Bots at Delft
5
+ University of Technology, starting in 2009. The project to develop a browser-
6
+ based version started in 2017. See https://linny-r.org for more information.
7
+
8
+ This JavaScript file (linny-r-milp.js) implements the Node.js interface between
9
+ Linny-R and a MILP solver that has been installed on the computer where this
10
+ software is running.
11
+
12
+ NOTE: For browser-based Linny-R, this file should NOT be loaded, as it is
13
+ already included in the server.
14
+ */
15
+
16
+ /*
17
+ Copyright (c) 2017-2022 Delft University of Technology
18
+
19
+ Permission is hereby granted, free of charge, to any person obtaining a copy
20
+ of this software and associated documentation files (the "Software"), to deal
21
+ in the Software without restriction, including without limitation the rights to
22
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
23
+ of the Software, and to permit persons to whom the Software is furnished to do
24
+ so, subject to the following conditions:
25
+
26
+ The above copyright notice and this permission notice shall be included in
27
+ all copies or substantial portions of the Software.
28
+
29
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
31
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
32
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
33
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
34
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
35
+ SOFTWARE.
36
+ */
37
+
38
+ const
39
+ child_process = require('child_process'),
40
+ fs = require('fs'),
41
+ os = require('os'),
42
+ path = require('path');
43
+
44
+ // Class MILPSolver implements the connection with the solver
45
+ module.exports = class MILPSolver {
46
+ constructor(settings, workspace) {
47
+ this.name = settings.solver;
48
+ this.solver_path = settings.solver_path;
49
+ console.log(`Selected solver: "${this.name}"`);
50
+ this.id = this.name.toLowerCase();
51
+ // Each external MILP solver application has its own interface
52
+ // NOTE: the list may be extended to accommodate more MILP solvers
53
+ if(this.id === 'gurobi') {
54
+ this.ext = '.mps';
55
+ this.user_model = path.join(workspace.solver_output, 'usr_model.mps');
56
+ this.solver_model = path.join(workspace.solver_output, 'solver_model.lp');
57
+ this.solution = path.join(workspace.solver_output, 'model.json');
58
+ this.log = path.join(workspace.solver_output, 'model.log');
59
+ this.args = [
60
+ 'timeLimit=30',
61
+ 'intFeasTol=0.5e-6',
62
+ 'JSONSolDetail=1',
63
+ `LogFile=${this.log}`,
64
+ `ResultFile=${this.solution}`,
65
+ `ResultFile=${this.solver_model}`,
66
+ `${this.user_model}`
67
+ ];
68
+ this.errors = {
69
+ 1: 'Model loaded -- no further information',
70
+ 2: 'Optimal solution found',
71
+ 3: 'The model is infeasible',
72
+ 4: 'The model is either unbounded or infeasible',
73
+ 5: 'The model is unbounded',
74
+ 6: 'Aborted -- Optimal objective is worse than specified cut-off',
75
+ 7: 'Halted -- Iteration limit exceeded',
76
+ 8: 'Halted -- Node limit exceeded',
77
+ 9: 'Halted -- Solver time limit exceeded',
78
+ 10: 'Halted -- Solution count limit exceeded',
79
+ 11: 'Halted -- Optimization terminated by user',
80
+ 12: 'Halted -- Unrecoverable numerical difficulties',
81
+ 13: 'The model is sub-obtimal',
82
+ 14: 'Optimization still in progress',
83
+ 15: 'User-specified objective limit has been reached'
84
+ };
85
+ } else if(this.id === 'lp_solve') {
86
+ // Execute file commands differ across platforms
87
+ if(os.platform().startsWith('win')) {
88
+ this.solve_cmd = 'lp_solve.exe ';
89
+ } else {
90
+ this.solve_cmd = './lp_solve ';
91
+ }
92
+ this.ext = '.lp';
93
+ this.user_model = path.join('user', 'solver', 'usr_model.lp');
94
+ this.solver_model = path.join('user', 'solver', 'solver_model.lp');
95
+ this.solution = path.join('.', 'user', 'solver', 'output.txt');
96
+ this.args = [
97
+ '-timeout 300',
98
+ '-v4',
99
+ '-g 1.0e-11',
100
+ '-epsel 1.0e-7',
101
+ `-wlp ${this.solver_model}`,
102
+ `>${this.solution}`,
103
+ this.user_model
104
+ ];
105
+ this.errors = {
106
+ '-2': 'Out of memory',
107
+ 1: 'The model is sub-optimal',
108
+ 2: 'The model is infeasible',
109
+ 3: 'The model is unbounded',
110
+ 4: 'The model is degenerative',
111
+ 5: 'Numerical failure encountered',
112
+ 6: 'Solver was stopped by user',
113
+ 7: 'Solver time limit exceeded',
114
+ 9: 'The model could be solved by presolve',
115
+ 25: 'Accuracy error encountered'
116
+ };
117
+ } else {
118
+ console.log(`WARNING: Unknown solver "${this.name}"`);
119
+ this.id = '';
120
+ }
121
+ }
122
+
123
+ solveBlock(sp) {
124
+ // Saves model file, executes solver, and returns results
125
+ const result = {
126
+ block: sp.get('block'),
127
+ round: sp.get('round'),
128
+ status: 0,
129
+ error: '',
130
+ messages: []
131
+ };
132
+ let timeout = parseInt(sp.get('timeout'));
133
+ // Default timeout per block is 30 seconds
134
+ if(isNaN(timeout)) timeout = 30;
135
+ if(!this.id) {
136
+ result.status = -999;
137
+ result.error = 'No MILP solver';
138
+ return result;
139
+ } else {
140
+ console.log('Solve block', result.block, result.round);
141
+ // Write the POSTed MILP model to a file
142
+ fs.writeFileSync(this.user_model, sp.get('data').trim());
143
+ // Delete previous log file (if any)
144
+ try {
145
+ if(this.log) fs.unlinkSync(this.log);
146
+ } catch(err) {
147
+ // Ignore error
148
+ }
149
+ // Delete previous solution file (if any)
150
+ try {
151
+ if(this.solution) fs.unlinkSync(this.solution);
152
+ } catch(err) {
153
+ // Ignore error
154
+ }
155
+ let spawn = null,
156
+ error = null,
157
+ status = 0;
158
+ try {
159
+ if(this.id === 'lp_solve') {
160
+ this.args[0] = '-timeout ' + timeout;
161
+ // NOTES:
162
+ // (1) LP_solve is picky about its command line, and will not work
163
+ // when the arguments are passed as an array; therefore execute
164
+ // it as a single command string that includes all arguments
165
+ // (2) the shell option must be set to TRUE (so the command is
166
+ // executed within an OS shell script) or LP_solve will interpret
167
+ // the first argument as the model file, and complain
168
+ // (3) output must be ignored, as LP_solve will output many warnings
169
+ // about 0-value coefficients, and these would otherwise also
170
+ // appear on the console
171
+ // (4) prevent Windows opening a visible sub-process shell window
172
+ const
173
+ cmd = this.solve_cmd + ' ' + this.args.join(' '),
174
+ options = {shell: true, stdio: 'ignore', windowsHide: true};
175
+ spawn = child_process.spawnSync(cmd, options);
176
+ } else {
177
+ this.args[0] = 'TimeLimit=' + timeout;
178
+ // When using Gurobi, the standard way works well
179
+ const options = {windowsHide: true};
180
+ spawn = child_process.spawnSync(this.solver_path, this.args, options);
181
+ }
182
+ status = spawn.status;
183
+ } catch(err) {
184
+ status = -13;
185
+ error = err;
186
+ }
187
+ if(status) console.log(`Process status: ${status}`);
188
+ if(status in this.errors) {
189
+ // If solver exited with known status code, report message
190
+ result.status = status;
191
+ result.error = this.errors[status];
192
+ } else if(status !== 0) {
193
+ result.status = -13;
194
+ const msg = (error ? error.message : 'Unknown error');
195
+ result.error += 'ERROR: ' + msg;
196
+ }
197
+ return this.processSolverOutput(result);
198
+ }
199
+ }
200
+
201
+ processSolverOutput(result) {
202
+ // Read solver output files and return solution (or error)
203
+ const x_values = [];
204
+ if(this.id === 'gurobi') {
205
+ // `messages` must be an array of strings
206
+ result.messages = fs.readFileSync(this.log, 'utf8').split(os.EOL);
207
+ if(result.status !== 0) {
208
+ // Non-zero solver exit code may indicate expired license
209
+ result.error = 'Your Gurobi license may have expired';
210
+ } else {
211
+ try {
212
+ // Read JSON string from solution file
213
+ const
214
+ json = fs.readFileSync(this.solution, 'utf8').trim(),
215
+ sol = JSON.parse(json);
216
+ result.seconds = sol.SolutionInfo.Runtime;
217
+ // NOTE: Status = 2 indicates success!
218
+ if(sol.SolutionInfo.Status !== 2) {
219
+ result.status = sol.SolutionInfo.Status;
220
+ result.error = this.errors[result.status];
221
+ if(!result.error) result.error = 'Unknown solver error';
222
+ console.log(`Solver status: ${result.status} - ${result.error}`);
223
+ }
224
+ // Objective value
225
+ result.obj = sol.SolutionInfo.ObjVal;
226
+ // Values of solution vector
227
+ if(sol.Vars) {
228
+ for(let i = 0; i < sol.Vars.length; i++) {
229
+ x_values.push(sol.Vars[i].X);
230
+ }
231
+ }
232
+ } catch(err) {
233
+ console.log('WARNING: Could not read solution file');
234
+ console.log(err.message);
235
+ result.status = -13;
236
+ result.error = 'No solution found';
237
+ }
238
+ }
239
+ } else if(this.id === 'lp_solve') {
240
+ // Read solver messages from file
241
+ // NOTE: Linny-R client expects a list of strings
242
+ const
243
+ output = fs.readFileSync(
244
+ this.solution, 'utf8').trim().split(os.EOL),
245
+ msgs = [];
246
+ result.seconds = 0;
247
+ let i = 0,
248
+ solved = false;
249
+ while(i< output.length && !solved) {
250
+ msgs.push(output[i]);
251
+ const m = output[i].match(/in total (\d+\.\d+) seconds/);
252
+ if(m && m.length > 1) result.seconds = parseFloat(m[1]);
253
+ solved = output[i].startsWith('Value of objective function:');
254
+ i++;
255
+ }
256
+ result.messages = msgs;
257
+ if(solved) {
258
+ while(i < output.length && !output[i].startsWith('C1')) i++;
259
+ while(i < output.length) {
260
+ let v = output[i].replace(/C\d+\s*/, '');
261
+ // Remove variable names from result output
262
+ v = parseFloat(v);
263
+ x_values.push(v);
264
+ i++;
265
+ }
266
+ } else {
267
+ console.log('No solution found');
268
+ }
269
+ }
270
+ // Add data and model to the results dict
271
+ result.data = {
272
+ block: result.block,
273
+ round: result.round,
274
+ seconds: result.seconds,
275
+ x: x_values
276
+ };
277
+ try {
278
+ result.model = fs.readFileSync(this.solver_model, 'utf8');
279
+ } catch(err) {
280
+ console.log(err);
281
+ result.model = 'ERROR reading solver model file: ' + err;
282
+ }
283
+ return result;
284
+ }
285
+
286
+ }; // END of class MILPSolver (semicolon needed because of export statement)