Create solver page; add Dockerfile

feature/gui
Alinson S. Xavier 3 years ago
parent 9112d9fde5
commit ee767b9ebd

@ -0,0 +1,4 @@
build
jobs
relog-web/node_modules
relog-web/build

@ -0,0 +1,27 @@
FROM julia:1.7-buster
# Install Node.js & zip
RUN apt-get update -yq && \
apt-get -yq install curl gnupg ca-certificates && \
curl -L https://deb.nodesource.com/setup_18.x | bash && \
apt-get update -yq && \
apt-get install -yq nodejs zip
# Install Julia dependencies
ADD Project.toml /app/
ADD src/RELOG.jl /app/src/
RUN julia --project=/app -e 'using Pkg; Pkg.update()'
# Install JS dependencies
ADD relog-web/package*.json /app/relog-web/
RUN cd /app/relog-web && npm install
# Copy source code
ADD . /app
RUN julia --project=/app -e 'using Pkg; Pkg.precompile()'
# Build JS app
RUN cd /app/relog-web && npm run build
WORKDIR /app
CMD julia --project=/app -e 'import RELOG; RELOG.web("0.0.0.0")'

@ -17,6 +17,9 @@ clean:
docs:
mkdocs build -d ../docs/$(VERSION)/
docker-build:
docker build --tag relog:0.6 .
format:
julia -e 'using JuliaFormatter; format(["src", "test"], verbose=true);'
@ -26,3 +29,4 @@ test-watch:
bash -c "while true; do make test --quiet; sleep 1; done"
.PHONY: docs test

@ -10,9 +10,11 @@ Cbc = "9961bab8-2fa3-5c5a-9d89-47fab24efd76"
Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b"
Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6"
GZip = "92fee26a-97fe-5a0c-ad85-20a5f3185b63"
Geodesy = "0ef565a4-170c-5f04-8de2-149903a85f3d"
HTTP = "cd3eb016-35fb-5094-929b-558a96fad6f3"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
JSONSchema = "7d188eb4-7ad8-530c-ae41-71a32a6d4692"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
@ -20,8 +22,10 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
PackageCompiler = "9b87118b-4619-50d2-8e1e-99f35a4d4d9d"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
ProgressBars = "49802e3a-d2f1-5c88-81d8-b72133a6f568"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Shapefile = "8e980c4a-a4fe-5da2-b3a7-4b4b0353a2f4"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
@ -36,6 +40,7 @@ DataFrames = "0.21"
DataStructures = "0.17"
GZip = "0.5"
Geodesy = "0.5"
HTTP = "0.9"
JSON = "0.21"
JSONSchema = "0.3"
JuMP = "0.21"

@ -13,23 +13,22 @@
**RELOG** is a supply chain optimization package focusing on reverse logistics and reverse manufacturing. For example, the package can be used to determine where to build recycling plants, what sizes should they have and which customers should be served by which plants. The package supports customized reverse logistics pipelines, with multiple types of plants, multiple types of product and multiple time periods.
<img src="https://anl-ceeesa.github.io/RELOG/0.5/images/ex_transportation.png" width="1000px"/>
### Documentation
* [Usage](https://anl-ceeesa.github.io/RELOG/0.5/usage)
* [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.5/format)
* [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.5/reports)
* [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.5/model)
- [Usage](https://anl-ceeesa.github.io/RELOG/0.5/usage)
- [Input and Output Data Formats](https://anl-ceeesa.github.io/RELOG/0.5/format)
- [Simplified Solution Reports](https://anl-ceeesa.github.io/RELOG/0.5/reports)
- [Optimization Model](https://anl-ceeesa.github.io/RELOG/0.5/model)
### Authors
* **Alinson S. Xavier** <<axavier@anl.gov>>
* **Nwike Iloeje** <<ciloeje@anl.gov>>
* **John Atkins**
* **Kyle Sun**
- **Alinson S. Xavier** <<axavier@anl.gov>>
- **Nwike Iloeje** <<ciloeje@anl.gov>>
- **John Atkins**
- **Kyle Sun**
- **Audrey Gallier**
### License

@ -18,6 +18,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-flow-renderer": "^9.7.4",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
}
@ -8495,6 +8496,19 @@
"he": "bin/he"
}
},
"node_modules/history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"dependencies": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -11486,6 +11500,19 @@
"node": ">=4"
}
},
"node_modules/mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"dependencies": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
},
"peerDependencies": {
"prop-types": "^15.0.0",
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
}
},
"node_modules/mini-css-extract-plugin": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz",
@ -13769,6 +13796,61 @@
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng==",
"dependencies": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.3",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"peerDependencies": {
"react": ">=15"
}
},
"node_modules/react-router/node_modules/isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"node_modules/react-router/node_modules/path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"dependencies": {
"isarray": "0.0.1"
}
},
"node_modules/react-router/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"node_modules/react-scripts": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz",
@ -14083,6 +14165,11 @@
"node": ">=8"
}
},
"node_modules/resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"node_modules/resolve-url-loader": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz",
@ -15365,6 +15452,16 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"node_modules/tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
},
"node_modules/tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"node_modules/tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -15538,6 +15635,19 @@
"is-typedarray": "^1.0.0"
}
},
"node_modules/typescript": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
"integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@ -15703,6 +15813,11 @@
"node": ">= 8"
}
},
"node_modules/value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"node_modules/vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
@ -22751,6 +22866,19 @@
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
},
"history": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
"integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
"requires": {
"@babel/runtime": "^7.1.2",
"loose-envify": "^1.2.0",
"resolve-pathname": "^3.0.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0",
"value-equal": "^1.0.1"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
@ -24911,6 +25039,15 @@
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="
},
"mini-create-react-context": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.4.1.tgz",
"integrity": "sha512-YWCYEmd5CQeHGSAKrYvXgmzzkrvssZcuuQDDeqkT+PziKGMgE+0MCCtcKbROzocGBG1meBLl2FotlRwf4gAzbQ==",
"requires": {
"@babel/runtime": "^7.12.1",
"tiny-warning": "^1.0.3"
}
},
"mini-css-extract-plugin": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.6.0.tgz",
@ -26433,6 +26570,57 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz",
"integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A=="
},
"react-router": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.3.tgz",
"integrity": "sha512-mzQGUvS3bM84TnbtMYR8ZjKnuPJ71IjSzR+DE6UkUqvN4czWIqEs17yLL8xkAycv4ev0AiN+IGrWu88vJs/p2w==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.4.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"isarray": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
"integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ=="
},
"path-to-regexp": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz",
"integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==",
"requires": {
"isarray": "0.0.1"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}
}
},
"react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-Ov0tGPMBgqmbu5CDmN++tv2HQ9HlWDuWIIqn4b88gjlAN5IHI+4ZUZRcpz9Hl0azFIwihbLDYw1OiHGRo7ZIng==",
"requires": {
"@babel/runtime": "^7.12.13",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.3.3",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
},
"react-scripts": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-scripts/-/react-scripts-5.0.0.tgz",
@ -26674,6 +26862,11 @@
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
"integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="
},
"resolve-pathname": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
"integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
},
"resolve-url-loader": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-4.0.0.tgz",
@ -27619,6 +27812,16 @@
"resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz",
"integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q="
},
"tiny-invariant": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.2.0.tgz",
"integrity": "sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg=="
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tmpl": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz",
@ -27753,6 +27956,12 @@
"is-typedarray": "^1.0.0"
}
},
"typescript": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.2.tgz",
"integrity": "sha512-Mamb1iX2FDUpcTRzltPxgWMKy3fhg0TN378ylbktPGPK/99KbDtMQ4W1hwgsbPAsG3a0xKa1vmw4VKZQbkvz5A==",
"peer": true
},
"unbox-primitive": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz",
@ -27877,6 +28086,11 @@
}
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
"integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
},
"vary": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",

@ -2,7 +2,7 @@
"name": "relog-web",
"version": "0.1.0",
"private": true,
"homepage": "/RELOG/0.6/casebuilder",
"homepage": "/",
"jest": {
"moduleNameMapper": {
"d3": "<rootDir>/node_modules/d3/dist/d3.min.js"
@ -19,6 +19,7 @@
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-flow-renderer": "^9.7.4",
"react-router-dom": "^5.3.3",
"react-scripts": "5.0.0",
"web-vitals": "^2.1.4"
},

@ -1,19 +1,18 @@
import React, { useState, useRef, useEffect } from "react";
import { openDB, deleteDB, wrap, unwrap } from "idb";
import "./index.css";
import PipelineBlock from "./PipelineBlock";
import ParametersBlock from "./ParametersBlock";
import ProductBlock from "./ProductBlock";
import PlantBlock from "./PlantBlock";
import Button from "./Button";
import Header from "./Header";
import Footer from "./Footer";
import { openDB } from "idb";
import React, { useEffect, useRef, useState } from "react";
import Button from "../common/Button";
import Footer from "../common/Footer";
import Header from "../common/Header";
import "../index.css";
import { generateFile } from "./csv";
import { defaultData, defaultPlant, defaultProduct } from "./defaults";
import { randomPosition } from "./PipelineBlock";
import { exportData, importData } from "./export";
import { generateFile } from "./csv";
import ParametersBlock from "./ParametersBlock";
import PipelineBlock, { randomPosition } from "./PipelineBlock";
import PlantBlock from "./PlantBlock";
import ProductBlock from "./ProductBlock";
import { validate } from "./validate";
import { useHistory } from "react-router-dom";
const setDefaults = (actualDict, defaultDict) => {
for (const [key, defaultValue] of Object.entries(defaultDict)) {
@ -83,6 +82,8 @@ const InputPage = () => {
if (data) setData(data);
}, []);
const history = useHistory();
const promptName = (prevData) => {
const name = prompt("Name");
if (!name || name.length === 0) return;
@ -301,6 +302,24 @@ const InputPage = () => {
);
}
const onSubmit = () => {
const exported = exportData(data);
const valid = validate(exported);
if (valid) {
fetch("/submit", {
method: "POST",
body: JSON.stringify(exported),
})
.then((response) => {
return response.json();
})
.then((data) => {
console.log(data);
history.push(`/solver/${data.job_id}`);
});
}
};
let plantComps = [];
for (const [plantName, plant] of Object.entries(data.plants)) {
plantComps.push(
@ -341,6 +360,7 @@ const InputPage = () => {
<Button label="Clear" onClick={onClear} />
<Button label="Load" onClick={(e) => fileElem.current.click()} />
<Button label="Save" onClick={onSave} />
<Button label="Submit" onClick={onSubmit} />
<input
type="file"
ref={fileElem}
@ -349,6 +369,7 @@ const InputPage = () => {
onChange={onFileSelected}
/>
</Header>
<div id="contentBackground">
<div id="content">
<PipelineBlock
onAddPlant={onAddPlant}
@ -371,6 +392,7 @@ const InputPage = () => {
{productComps}
{plantComps}
</div>
</div>
<div id="messageTray">{messageComps}</div>
<Footer />
</>

@ -1,7 +1,7 @@
import Section from "./Section";
import Card from "./Card";
import Form from "./Form";
import TextInputRow from "./TextInputRow";
import Section from "../common/Section";
import Card from "../common/Card";
import Form from "../common/Form";
import TextInputRow from "../common/TextInputRow";
const ParametersBlock = (props) => {
const onChangeField = (field, val) => {

@ -1,8 +1,8 @@
import React, { useEffect } from "react";
import ReactFlow, { Background, isNode, Controls } from "react-flow-renderer";
import Section from "./Section";
import Card from "./Card";
import Button from "./Button";
import Section from "../common/Section";
import Card from "../common/Card";
import Button from "../common/Button";
import styles from "./PipelineBlock.module.css";
import dagre from "dagre";

@ -1,9 +1,9 @@
import Section from "./Section";
import Card from "./Card";
import Form from "./Form";
import TextInputRow from "./TextInputRow";
import FileInputRow from "./FileInputRow";
import DictInputRow from "./DictInputRow";
import Section from "../common/Section";
import Card from "../common/Card";
import Form from "../common/Form";
import TextInputRow from "../common/TextInputRow";
import FileInputRow from "../common/FileInputRow";
import DictInputRow from "../common/DictInputRow";
import { csvFormat, csvParse, generateFile } from "./csv";
const PlantBlock = (props) => {

@ -1,9 +1,9 @@
import Section from "./Section";
import Card from "./Card";
import Form from "./Form";
import TextInputRow from "./TextInputRow";
import FileInputRow from "./FileInputRow";
import DictInputRow from "./DictInputRow";
import Section from "../common/Section";
import Card from "../common/Card";
import Form from "../common/Form";
import TextInputRow from "../common/TextInputRow";
import FileInputRow from "../common/FileInputRow";
import DictInputRow from "../common/DictInputRow";
import { csvParse, extractNumericColumns, generateFile } from "./csv";
import { csvFormat } from "d3";

@ -1,7 +1,5 @@
.Footer {
background-color: rgba(0, 0, 0, 0.8);
padding: 24px;
margin-top: 24px;
padding: 12px;
color: rgba(255, 255, 255, 0.5);
text-align: center;
font-size: 14px;

@ -3,6 +3,7 @@
--box-border: 1px solid rgba(0, 0, 0, 0.2);
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
--border-radius: 4px;
--primary: #0d6efd;
}
html,
@ -14,15 +15,19 @@ body {
}
body {
background-color: #f6f6f6;
background-color: #333;
color: rgba(0, 0, 0, 0.95);
}
#contentBackground {
background-color: #f6f6f6;
}
#content {
max-width: var(--site-width);
min-width: 900px;
margin: 0 auto;
padding: 0 6px;
padding: 1px 6px 32px 6px;
}
.react-flow__node.selected {

@ -1,11 +1,25 @@
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import InputPage from "./InputPage";
import InputPage from "./casebuilder/InputPage";
import SolverPage from "./solver/SolverPage";
import { Route, BrowserRouter, Switch, Redirect } from "react-router-dom";
ReactDOM.render(
<BrowserRouter>
<React.StrictMode>
<Switch>
<Route path="/casebuilder">
<InputPage />
</React.StrictMode>,
</Route>
<Route path="/solver/:job_id">
<SolverPage />
</Route>
<Route path="/">
<Redirect to="/casebuilder" />
</Route>
</Switch>
</React.StrictMode>
</BrowserRouter>,
document.getElementById("root")
);

@ -0,0 +1,47 @@
import { useState } from "react";
import { useEffect } from "react";
import Section from "../common/Section";
import Card from "../common/Card";
import styles from "./FilesBlock.module.css";
import { useRef } from "react";
const FilesBlock = (props) => {
const [filesFound, setFilesFound] = useState(false);
const fetchFiles = async () => {
const response = await fetch(`/jobs/${props.job}/output.json`);
if (response.ok) {
setFilesFound(true);
}
};
// Fetch files periodically from the server
useEffect(() => {
fetchFiles();
console.log(filesFound);
if (!filesFound) {
const interval = setInterval(() => {
fetchFiles();
}, 1000);
return () => clearInterval(interval);
}
}, [filesFound]);
let content = <div className={styles.nodata}>No files available</div>;
if (filesFound) {
content = (
<div className={styles.files}>
<a href={`/jobs/${props.job}/output.zip`}>output.zip</a>
</div>
);
}
return (
<>
<Section title="Output Files" />
<Card>{content}</Card>
</>
);
};
export default FilesBlock;

@ -0,0 +1,19 @@
.files a {
display: block;
padding: 16px;
text-decoration: none;
color: var(--primary);
}
.files a:hover {
background-color: var(--primary);
color: white;
border-radius: var(--border-radius);
}
.nodata {
text-align: center;
padding: 24px 0;
color: #888;
margin: 0;
}

@ -0,0 +1,46 @@
import { useState } from "react";
import { useEffect } from "react";
import Section from "../common/Section";
import Card from "../common/Card";
import styles from "./LogBlock.module.css";
import { useRef } from "react";
const LogBlock = (props) => {
const [log, setLog] = useState();
const preRef = useRef(null);
const fetchLog = async () => {
const response = await fetch(`/jobs/${props.job}/solve.log`);
const data = await response.text();
if (log !== data) {
setLog(data);
}
};
// Fetch log periodically from the server
useEffect(() => {
fetchLog();
const interval = setInterval(() => {
fetchLog();
}, 1000);
return () => clearInterval(interval);
}, []);
// Scroll to bottom whenever the log is updated
useEffect(() => {
preRef.current.scrollTop = preRef.current.scrollHeight;
}, [log]);
return (
<>
<Section title="Optimization Log" />
<Card>
<pre ref={preRef} className={styles.log}>
{log}
</pre>
</Card>
</>
);
};
export default LogBlock;

@ -0,0 +1,8 @@
.log {
max-height: 500px;
min-height: 500px;
border: 0;
margin: 0;
overflow: auto;
line-height: 1.4em;
}

@ -0,0 +1,26 @@
import React from "react";
import { useParams } from "react-router-dom";
import Footer from "../common/Footer";
import Header from "../common/Header";
import LogBlock from "./LogBlock";
import FilesBlock from "./FilesBlock";
const SolverPage = () => {
const params = useParams();
return (
<>
<Header title="Solver"></Header>
<div id="contentBackground">
{" "}
<div id="content">
<LogBlock job={params.job_id} />
<FilesBlock job={params.job_id} />
</div>
</div>
<Footer />
</>
);
};
export default SolverPage;

@ -25,4 +25,5 @@ include("reports/products.jl")
include("reports/tr_emissions.jl")
include("reports/tr.jl")
include("reports/write.jl")
include("web/web.jl")
end

@ -0,0 +1,126 @@
println("Initializing...")
using Logging
using Cbc
using JSON
using JuMP
using RELOG
function solve(root, filename)
ref_file = "$root/$filename"
optimizer = optimizer_with_attributes(
Cbc.Optimizer,
"seconds" => 900,
)
ref_solution, ref_model = RELOG.solve(
ref_file,
optimizer=optimizer,
return_model=true,
marginal_costs=false,
)
Libc.flush_cstdio()
flush(stdout)
sleep(1)
if length(ref_solution) == 0
return
end
RELOG.write_products_report(
ref_solution,
replace(ref_file, ".json" => "_products.csv"),
)
RELOG.write_plants_report(
ref_solution,
replace(ref_file, ".json" => "_plants.csv"),
)
RELOG.write_plant_outputs_report(
ref_solution,
replace(ref_file, ".json" => "_plant_outputs.csv"),
)
RELOG.write_plant_emissions_report(
ref_solution,
replace(ref_file, ".json" => "_plant_emissions.csv"),
)
RELOG.write_transportation_report(
ref_solution,
replace(ref_file, ".json" => "_tr.csv"),
)
RELOG.write_transportation_emissions_report(
ref_solution,
replace(ref_file, ".json" => "_tr_emissions.csv"),
)
isdir("$root/scenarios") || return
for filename in readdir("$root/scenarios")
scenario = "$root/scenarios/$filename"
endswith(filename, ".json") || continue
sc_solution = RELOG.resolve(
ref_model,
scenario,
optimizer=optimizer,
)
if length(sc_solution) == 0
return
end
RELOG.write_plants_report(
sc_solution,
replace(scenario, ".json" => "_plants.csv"),
)
RELOG.write_products_report(
sc_solution,
replace(scenario, ".json" => "_products.csv"),
)
RELOG.write_plant_outputs_report(
sc_solution,
replace(scenario, ".json" => "_plant_outputs.csv"),
)
RELOG.write_plant_emissions_report(
sc_solution,
replace(scenario, ".json" => "_plant_emissions.csv"),
)
RELOG.write_transportation_report(
sc_solution,
replace(scenario, ".json" => "_tr.csv"),
)
RELOG.write_transportation_emissions_report(
sc_solution,
replace(scenario, ".json" => "_tr_emissions.csv"),
)
end
end
function solve_recursive(path)
# Solve instances
for (root, dirs, files) in walkdir(path)
if occursin(r"scenarios"i, root)
continue
end
for filename in files
endswith(filename, ".json") || continue
solve(root, filename)
end
end
# Collect results
results = []
for (root, dirs, files) in walkdir(path)
for filename in files
endswith(filename, "_plants.csv") || continue
push!(
results,
joinpath(
replace(root, path => ""),
replace(filename, "_plants.csv" => ""),
),
)
end
end
open("$path/output.json", "w") do file
JSON.print(file, results)
end
run(`zip -r $path/output.zip $path`)
end
solve_recursive(ARGS[1])

@ -0,0 +1,66 @@
import HTTP
import JSON
using Random
const ROUTER = HTTP.Router()
const PROJECT_DIR = joinpath(dirname(@__FILE__), "..", "..")
const STATIC_DIR = joinpath(PROJECT_DIR, "relog-web", "build", "static")
const JOBS_DIR = joinpath(PROJECT_DIR, "jobs")
function serve_file(req::HTTP.Request, filename)
if isfile(filename)
open(filename) do file
return HTTP.Response(200, read(file))
end
else
return HTTP.Response(404)
end
end
function submit(req::HTTP.Request)
# Generate random job id
job_id = lowercase(randstring(12))
# Create job folder
job_path = joinpath(JOBS_DIR, job_id)
mkpath(job_path)
# Write JSON file
case = JSON.parse(String(req.body))
open(joinpath(job_path, "case.json"), "w") do file
JSON.print(file, case)
end
# Run job
run(`bash -c "(julia --project=$PROJECT_DIR $PROJECT_DIR/src/web/run.jl $job_path 2>&1 | tee $job_path/solve.log) >/dev/null 2>&1 &"`)
response = Dict(
"job_id" => job_id,
)
return HTTP.Response(200, body = JSON.json(response))
end
function get_index(req::HTTP.Request)
return serve_file(req, joinpath(STATIC_DIR, "..", "index.html"))
end
function get_static(req::HTTP.Request)
return serve_file(req, joinpath(STATIC_DIR, req.target[9:end]))
end
function get_jobs(req::HTTP.Request)
return serve_file(req, joinpath(JOBS_DIR, req.target[7:end]))
end
HTTP.@register(ROUTER, "GET", "/static", get_static)
HTTP.@register(ROUTER, "GET", "/jobs", get_jobs)
HTTP.@register(ROUTER, "POST", "/submit", submit)
HTTP.@register(ROUTER, "GET", "/", get_index)
function web(host = "127.0.0.1", port = 8080)
@info "Launching web interface: http://$(host):$(port)/"
Base.exit_on_sigint(false)
HTTP.serve(ROUTER, host, port)
Base.exit_on_sigint(true)
end

@ -3,9 +3,11 @@
using RELOG
BASEDIR = dirname(@__FILE__)
@testset "Resolve" begin
# Shoud not crash
filename = "$(pwd())/../instances/s1.json"
filename = joinpath(BASEDIR, "..", "..", "instances", "s1.json")
solution_old, model_old = RELOG.solve(filename, return_model = true)
solution_new = RELOG.resolve(model_old, filename)
end

@ -4,9 +4,11 @@
using RELOG, JSON, GZip
BASEDIR = dirname(@__FILE__)
@testset "Reports" begin
@testset "from solve" begin
solution = RELOG.solve("$(pwd())/../instances/s1.json")
solution = RELOG.solve(joinpath(BASEDIR, "..", "instances", "s1.json"))
tmp_filename = tempname()
# The following should not crash
RELOG.write_plant_emissions_report(solution, tmp_filename)

Loading…
Cancel
Save