Adding MPS Export #49

Merged
SinusFox merged 61 commits from adding-unit-tests-and-language-switching into main 2024-10-11 14:08:24 +00:00
10 changed files with 479 additions and 14 deletions
Showing only changes of commit 231f9c90c1 - Show all commits
+8
View File
@@ -24,6 +24,8 @@ export default function text(lang: string, input: string): string {
return "Listen Sie alle Ihre Variablen auf. Eine pro Zeile (mit der 'Enter'-Taste trennen). Erlaubte Symbole sind a-z, A-Z.\nBeispiel:\nx\ny"; return "Listen Sie alle Ihre Variablen auf. Eine pro Zeile (mit der 'Enter'-Taste trennen). Erlaubte Symbole sind a-z, A-Z.\nBeispiel:\nx\ny";
case "boxExportLP": case "boxExportLP":
return "Als LP exportieren"; return "Als LP exportieren";
case "boxExportMPS":
return "Als MPS exportieren";
case "boxOut": case "boxOut":
return "Geben Sie ein Problem ein und drücken Sie eine Aktionstaste, um die Ausgabe anzuzeigen..."; return "Geben Sie ein Problem ein und drücken Sie eine Aktionstaste, um die Ausgabe anzuzeigen...";
case "buttonCalc": case "buttonCalc":
@@ -92,6 +94,8 @@ export default function text(lang: string, input: string): string {
return "Download wird vorbereitet..."; return "Download wird vorbereitet...";
case "downloadFetchInput": case "downloadFetchInput":
return "Eingaben werden geladen..."; return "Eingaben werden geladen...";
case "downloadCheckInput":
return "Überprüfe auf leere Eingabefelder...";
case "importing": case "importing":
return "Importiere..."; return "Importiere...";
default: default:
@@ -125,6 +129,8 @@ export default function text(lang: string, input: string): string {
return "List all your variables. One per line (divide by 'return' button). Allowed symbols are a-z, A-Z.\nExample:\nx\ny"; return "List all your variables. One per line (divide by 'return' button). Allowed symbols are a-z, A-Z.\nExample:\nx\ny";
case "boxExportLP": case "boxExportLP":
return "Export as LP"; return "Export as LP";
case "boxExportMPS":
return "Export as MPS";
case "boxOut": case "boxOut":
return "Input a problem and an action button to display output..."; return "Input a problem and an action button to display output...";
case "buttonCalc": case "buttonCalc":
@@ -193,6 +199,8 @@ export default function text(lang: string, input: string): string {
return "Preparing download..."; return "Preparing download...";
case "downloadFetchInput": case "downloadFetchInput":
return "Fetching input..."; return "Fetching input...";
case "downloadCheckInput":
return "Checking for empty input boxes...";
case "importing": case "importing":
return "Importing..."; return "Importing...";
default: default:
+6 -1
View File
@@ -2,7 +2,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Box, Button, Output } from "./modules"; import { Box, Button, Output } from "./modules";
import { calculate_click, downloadLP } from "./scripts"; import { calculate_click, downloadLP, downloadMPS } from "./scripts";
import text from "./lang"; import text from "./lang";
export default function Home() { export default function Home() {
@@ -24,6 +24,7 @@ export default function Home() {
const tr_calc_max = text(language, "maximize"); const tr_calc_max = text(language, "maximize");
const tr_calc_min = text(language, "minimize"); const tr_calc_min = text(language, "minimize");
const tr_calcButton = text(language, "buttonCalc"); const tr_calcButton = text(language, "buttonCalc");
const tr_boxExportMPS = text(language, "boxExportMPS");
const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => { const handleLanguageChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
setLanguage(event.target.value); setLanguage(event.target.value);
@@ -85,6 +86,10 @@ export default function Home() {
className={"button"} className={"button"}
onClickFunc={downloadLP} onClickFunc={downloadLP}
/> />
<Button
title={tr_boxExportMPS}
className={"button"}
onClickFunc={downloadMPS} />
<br /> <br />
<Output <Output
id="out" id="out"
+398 -13
View File
@@ -1,7 +1,7 @@
import * as MIP from "../parser/parseMIP" import { LP } from "../solver/jvail/LP";
import * as LP from "../parser/parseLP" import { Bound } from "../solver/jvail/Bound";
import * as LPAPI from "../api/optimizeLP.js" import { Variable } from "../solver/jvail/Variable";
import { Bounds, GLP_MAX, GLP_MIN, GLP_UP, GLP_LO, GLP_FX, GLP_FR, GLP_DB } from "../solver/jvail/Bounds";
import * as GLPKAPI from "../solver/glpk.min.js" import * as GLPKAPI from "../solver/glpk.min.js"
import { start } from "repl"; import { start } from "repl";
@@ -108,7 +108,31 @@ export function isInputValidRegex(obj: string | undefined, subj: string | undefi
return true; return true;
} }
export function isInputFilled(obj: string | undefined, subj: string | undefined, bounds: string | undefined, vars: string | undefined): boolean { export function isInputFilled(obj: string | undefined, subj: string | undefined, bounds: string | undefined, vars: string | undefined) {
// if empty input: fetching inputs
if (obj == "" || subj == "" || bounds == "" || vars == "") {
const objectiveElement = document.getElementById('objective');
if (objectiveElement !== null) {
obj = (objectiveElement as HTMLInputElement).value;
}
const subjectElement = document.getElementById('subject');
if (subjectElement !== null) {
subj = (subjectElement as HTMLInputElement).value;
}
const boundsElement = document.getElementById('bounds');
if (boundsElement !== null) {
bounds = (boundsElement as HTMLInputElement).value;
}
const varsElement = document.getElementById('vars');
if (varsElement !== null) {
vars = (varsElement as HTMLInputElement).value;
}
}
if (obj == "" || obj == null || obj == undefined) { if (obj == "" || obj == null || obj == undefined) {
customLog("err_emptyBox"); customLog("err_emptyBox");
return false; return false;
@@ -238,7 +262,7 @@ export function downloadLPFormatting(objective: any, subject: any, bounds: any)
let operator = "Minimize"; let operator = "Minimize";
if (maxmin == "maximize") operator = "Maximize"; if (maxmin == "maximize") operator = "Maximize";
// Header mit Problemname // Header with problem name
const header = "\\ Your problem\n"; const header = "\\ Your problem\n";
// format objective // format objective
@@ -256,6 +280,7 @@ export function downloadLPFormatting(objective: any, subject: any, bounds: any)
return lpFormat; return lpFormat;
} }
function downloadProblemDownload(content: string) { function downloadProblemDownload(content: string) {
customLog("downloadPrepFile"); customLog("downloadPrepFile");
customLog(""); customLog("");
@@ -271,9 +296,19 @@ export function downloadLP() {
customLogClear(); customLogClear();
customLog("downloadPrep"); customLog("downloadPrep");
customLog(""); customLog("");
customLog("downloadFetchInput"); customLog("downloadCheckInput");
customLog(""); customLog("");
if (!isInputFilled("","","","")) return;
let exportString: string | undefined = getInputsForLPAsString();
if (exportString === undefined) return;
downloadProblemDownload(exportString);
}
function getInputsForLP() {
let objective: string | undefined; let objective: string | undefined;
const objectiveElement = document.getElementById('objective'); const objectiveElement = document.getElementById('objective');
if (objectiveElement !== null) { if (objectiveElement !== null) {
@@ -298,10 +333,360 @@ export function downloadLP() {
variables = (varsElement as HTMLInputElement).value; variables = (varsElement as HTMLInputElement).value;
} }
// catch error: empty input field(s) return { objective, subject, bounds };
if (!isInputFilled(objective, subject, bounds, variables)) return;
const exportString: string = downloadLPFormatting(objective, subject, bounds);
downloadProblemDownload(exportString);
} }
function getInputsForLPAsString(): string {
let inputs = getInputsForLP();
let obj = inputs?.objective;
let sub = inputs?.subject;
let bnds = inputs?.bounds;
const exportString: string = downloadLPFormatting(obj, sub, bnds);
return exportString;
}
export function convertLPToMPS(lp: LP): string {
let mpsString = '';
// NAME section
mpsString += `NAME ${lp.name}\n`;
// ROWS section
mpsString += 'ROWS\n';
mpsString += ` N ${lp.objective.name}\n`; // Objective row
lp.subjectTo.forEach(constraint => {
if (constraint.bounds.type === GLP_UP) { // <=
mpsString += ` L ${constraint.name}\n`;
} else if (constraint.bounds.type === GLP_LO) { // >=
mpsString += ` G ${constraint.name}\n`;
} else if (constraint.bounds.type === GLP_FX) { // =
mpsString += ` E ${constraint.name}\n`;
}
});
// COLUMNS section
mpsString += 'COLUMNS\n';
const variableMap: { [key: string]: { row: string; coef: number }[] } = {};
lp.objective.vars.forEach(varObj => {
if (!variableMap[varObj.name]) {
variableMap[varObj.name] = [];
}
variableMap[varObj.name].push({ row: lp.objective.name, coef: varObj.coef });
});
lp.subjectTo.forEach(constraint => {
constraint.vars.forEach(varObj => {
if (!variableMap[varObj.name]) {
variableMap[varObj.name] = [];
}
variableMap[varObj.name].push({ row: constraint.name, coef: varObj.coef });
});
});
for (const [variable, rows] of Object.entries(variableMap)) {
rows.forEach(entry => {
mpsString += ` ${variable} ${entry.row} ${entry.coef}\n`;
});
}
// RHS section
mpsString += 'RHS\n';
lp.subjectTo.forEach(constraint => {
if (constraint.bounds.type === GLP_UP) { // <= or =
mpsString += ` RHS1 ${constraint.name} ${constraint.bounds.ub}\n`;
} else if (constraint.bounds.type === GLP_LO || constraint.bounds.type === GLP_FX) { // >=
mpsString += ` RHS1 ${constraint.name} ${constraint.bounds.lb}\n`;
}
});
// BOUNDS section
if (lp.bounds && lp.bounds.length > 0) {
mpsString += 'BOUNDS\n';
lp.bounds.forEach(bound => {
if (bound.lb !== -Infinity) {
mpsString += ` LO BND1 ${bound.name} ${bound.lb}\n`;
}
if (bound.ub !== Infinity) {
mpsString += ` UP BND1 ${bound.name} ${bound.ub}\n`;
}
});
}
// BINARY section
if (lp.binaries && lp.binaries.length > 0) {
mpsString += 'BINARY\n';
lp.binaries.forEach(bin => {
mpsString += ` ${bin}\n`;
});
}
// GENERAL section
if (lp.generals && lp.generals.length > 0) {
mpsString += 'GENERAL\n';
lp.generals.forEach(gen => {
mpsString += ` ${gen}\n`;
});
}
// ENDATA section
mpsString += 'ENDATA\n';
return mpsString;
}
// read LP format from string
function parseLP(lpString: string): LP {
const lines = lpString.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
let mode: string = '';
let lp: LP = {
name: '',
objective: {
direction: GLP_MAX, // 1 for maximize, -1 for minimize
name: '',
vars: []
},
subjectTo: [],
bounds: [],
binaries: [],
generals: []
};
let objectiveExpression = '';
let constraintExpression = '';
let currentConstraintName = '';
for (let line of lines) {
// handle each block differently
// set mode for each to determine parsing path
if (line.startsWith("Maximize")) {
lp.objective.direction = GLP_MAX;
mode = 'objective';
continue;
} else if (line.startsWith("Minimize")) {
lp.objective.direction = GLP_MIN;
mode = 'objective';
continue;
} else if (line.startsWith("Subject To")) {
// Constraint section
if (objectiveExpression.length > 0) {
lp.objective.vars = parseLPExpression(objectiveExpression.trim());
objectiveExpression = '';
}
mode = 'subjectTo';
continue;
} else if (line.startsWith("Bounds")) {
// Bound section
if (constraintExpression.length > 0 && currentConstraintName.length > 0) {
const { vars, bound } = parseLPConstraint(constraintExpression.trim());
lp.subjectTo.push({
name: currentConstraintName,
vars: vars,
bounds: bound
});
constraintExpression = '';
currentConstraintName = '';
}
mode = 'bounds';
continue;
} else if (line.startsWith("Binary")) {
mode = 'binaries';
continue;
} else if (line.startsWith("General")) {
mode = 'generals';
continue;
} else if (line.startsWith("End")) {
mode = 'end';
continue;
}
// Parse based on current mode
if (mode === 'objective') {
const splitLine = line.split(":");
if (splitLine.length === 2) {
lp.objective.name = splitLine[0].trim(); // if name in first line
objectiveExpression += splitLine[1].trim() + ' ';
} else {
objectiveExpression += line.trim() + ' '; // multiline expansion of objective
}
} else if (mode === 'subjectTo') {
const splitLine = line.split(":");
if (splitLine.length === 2) {
if (constraintExpression.length > 0 && currentConstraintName.length > 0) {
// new Constraint -> add previous to the subjects
const { vars, bound } = parseLPConstraint(constraintExpression.trim());
lp.subjectTo.push({
name: currentConstraintName,
vars: vars,
bounds: bound
});
}
// start collecting the new constraint
currentConstraintName = splitLine[0].trim();
constraintExpression = splitLine[1].trim() + ' ';
} else {
// continue collecting the expression if it's multi-line
constraintExpression += line.trim() + ' ';
}
} else if (mode === 'bounds') {
const bound = parseLPBound(line.trim());
if (bound && lp.bounds) {
lp.bounds.push(bound);
}
} else if (mode === 'binaries') {
lp.binaries?.push(line.trim());
} else if (mode === 'generals') {
lp.generals?.push(line.trim());
}
}
// if no "subject to" previously, do it here
if (objectiveExpression.length > 0) {
lp.objective.vars = parseLPExpression(objectiveExpression.trim());
}
// same for "bounds" and "end"
if (constraintExpression.length > 0 && currentConstraintName.length > 0) {
const { vars, bound } = parseLPConstraint(constraintExpression.trim());
lp.subjectTo.push({
name: currentConstraintName,
vars: vars,
bounds: bound
});
}
return lp;
}
// Helper for expressions
function parseLPExpression(expr: string): Variable[] {
const regex = /([+-]?\s*\d*\.?\d*)\s*([a-zA-Z_][a-zA-Z_0-9]*)/g;
let match;
let vars: Variable[] = [];
// loop over all variables in expresion
while ((match = regex.exec(expr)) !== null) {
let temp_coef = match[1].replace(/\s+/g, '').trim() || '1';
temp_coef = temp_coef === "-" || temp_coef === '+' ? `${temp_coef}1` : temp_coef;
const coef = parseFloat(temp_coef);
const name = match[2];
vars.push({ name: name, coef: coef });
}
return vars;
}
// Helper for Constraint section
function parseLPConstraint(constraint: string): { vars: Variable[], bound: Bounds } {
// Valid operators in Constraints
const operators = ["<=", ">=", "="];
let operator = operators.find(op => constraint.includes(op));
if (!operator) {
throw new Error("Invalid constraint format");
}
const [expr, boundStr] = constraint.split(operator);
const vars: Variable[] = parseLPExpression(expr.trim());
const boundValue = parseFloat(boundStr.trim());
// determine bound type
let boundType = GLP_UP;
if (operator === "<=") {
boundType = GLP_UP;
} else if (operator === ">=") {
boundType = GLP_LO;
} else if (operator === "=") {
boundType = GLP_FX;
}
let lb = boundType === GLP_FX ? boundValue : boundType === GLP_LO ? boundValue : -Infinity;
let ub = boundType === GLP_UP ? boundValue : Infinity;
const bound: Bounds = {
type: boundType,
lb: lb,
ub: ub
} as Bounds;
return { vars, bound };
}
// Helper for Bound section
function parseLPBound(boundStr: string): Bound | null {
// Regex to handle various bound formats
const regex = /^([-]?\d*\.?\d*)?\s*(<=|>=|=)?\s*([a-zA-Z_][a-zA-Z_0-9]*)\s*(<=|>=|=)?\s*([-]?\d*\.?\d*)?$/;
const match = regex.exec(boundStr.trim());
if (match) {
const [, lbStr, leftOperator, varName, rightOperator, ubStr] = match;
let lb = lbStr ? parseFloat(lbStr) : undefined;
let ub = ubStr ? parseFloat(ubStr) : undefined;
let type: number;
// Handle free "edgecase"
if (boundStr.toLowerCase().includes('free')) {
return {
type: GLP_FR,
name: varName,
lb: -Infinity,
ub: Infinity
} as Bound;
}
// Determine bound type
if (leftOperator && rightOperator) {
if (leftOperator === '<=' && rightOperator === '<=') {
type = GLP_DB; // Double bound
} else if (leftOperator === '>=' && rightOperator === '>=') {
type = GLP_DB; // Double bound (reverse order)
[lb, ub] = [ub, lb]; // Swap lb and ub
} else {
return null; // Invalid combination
}
// detect one-sided bounds
} else if (leftOperator === '<=') {
type = GLP_UP;
ub = lb;
lb = undefined;
} else if (rightOperator === '<=') {
type = GLP_UP;
} else if (leftOperator === '>=') {
type = GLP_LO;
} else if (rightOperator === '>=') {
type = GLP_LO;
lb = ub;
ub = undefined;
} else if (leftOperator === '=' || rightOperator === '=') {
type = GLP_FX;
if (leftOperator === '=') ub = lb;
else lb = ub;
} else {
type = GLP_FR; // No bounds specified, assume free
}
return {
type,
name: varName,
lb: lb !== undefined ? lb : -Infinity,
ub: ub !== undefined ? ub : Infinity
} as Bound;
}
return null;
}
export function downloadMPS() {
customLogClear();
customLog("downloadPrep");
customLog("");
customLog("downloadCheckInput");
customLog("");
if (!isInputFilled("","","","")) return;
let inputs = getInputsForLPAsString();
let lp = parseLP(inputs);
let mps = convertLPToMPS(lp);
downloadProblemDownload(mps);
}
+6
View File
@@ -0,0 +1,6 @@
export interface Bound{
name: string;
type: number; // 1 for lower bound, 2 for upper bound, 3 for equal
ub: number;
lb: number;
}
+9
View File
@@ -0,0 +1,9 @@
export interface Bounds{ type: number, ub: number, lb: number }
export const GLP_FR = 1; /* free (unbounded) variable */
export const GLP_LO = 2; /* variable with lower bound */
export const GLP_UP = 3; /* variable with upper bound */
export const GLP_DB = 4; /* double-bounded variable */
export const GLP_FX = 5; /* fixed variable */
export const GLP_MAX = 2;
export const GLP_MIN = 1
+8
View File
@@ -0,0 +1,8 @@
import {Variable} from "./Variable";
import {Bounds} from "./Bounds";
export interface Constraint {
name: string;
vars: Variable[];
bounds: Bounds;
}
+18
View File
@@ -0,0 +1,18 @@
import {Variable} from "./Variable";
import {Bound} from "./Bound";
import {Options} from "./Options";
import {Constraint} from "./Constraint";
export interface LP {
name: string,
objective: {
direction: number,
name: string,
vars: Variable[]
},
subjectTo: Constraint[],
bounds?: Bound[],
binaries?: string[],
generals?: string[],
options?: Options
}
+12
View File
@@ -0,0 +1,12 @@
import {Result} from "./Result";
export interface Options {
mipgap?: number, /* set relative mip gap tolerance to mipgap, default 0.0 */
tmlim?: number, /* limit solution time to tmlim seconds, default INT_MAX */
msglev?: number, /* message level for terminal output, default GLP_MSG_ERR */
presol?: boolean, /* use presolver, default true */
cb?: { /* a callback called at each 'each' iteration (only simplex) */
call(result: Result):Result,
each: number
}
}
+10
View File
@@ -0,0 +1,10 @@
export interface Result {
name: string;
time: number;
result: {
status: number;
z: number;
vars: {[key:string]: number};
dual?: { [key: string]: number }; /* simplex only */
};
}
+4
View File
@@ -0,0 +1,4 @@
export interface Variable {
name: string;
coef: number
}