Reformat source code; implement import/export

feature/gui
Alinson S. Xavier 4 years ago
parent 56b673fb9e
commit 01452441dc
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

@ -11,6 +11,7 @@
"@testing-library/react": "^12.1.4", "@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"d3": "^7.3.0", "d3": "^7.3.0",
"dagre": "^0.8.5",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-flow-renderer": "^9.7.4", "react-flow-renderer": "^9.7.4",
@ -6266,6 +6267,15 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/dagre": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"dependencies": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
}
},
"node_modules/damerau-levenshtein": { "node_modules/damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -8310,6 +8320,14 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
}, },
"node_modules/graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"dependencies": {
"lodash": "^4.17.15"
}
},
"node_modules/gzip-size": { "node_modules/gzip-size": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@ -21012,6 +21030,15 @@
"d3-transition": "2 - 3" "d3-transition": "2 - 3"
} }
}, },
"dagre": {
"version": "0.8.5",
"resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"requires": {
"graphlib": "^2.1.8",
"lodash": "^4.17.15"
}
},
"damerau-levenshtein": { "damerau-levenshtein": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
@ -22504,6 +22531,14 @@
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
}, },
"graphlib": {
"version": "2.1.8",
"resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
"integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
"requires": {
"lodash": "^4.17.15"
}
},
"gzip-size": { "gzip-size": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",

@ -2,11 +2,18 @@
"name": "relog-web", "name": "relog-web",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"homepage": "/RELOG/0.6/casebuilder",
"jest": {
"moduleNameMapper": {
"d3": "<rootDir>/node_modules/d3/dist/d3.min.js"
}
},
"dependencies": { "dependencies": {
"@testing-library/jest-dom": "^5.16.2", "@testing-library/jest-dom": "^5.16.2",
"@testing-library/react": "^12.1.4", "@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"d3": "^7.3.0", "d3": "^7.3.0",
"dagre": "^0.8.5",
"react": "^17.0.2", "react": "^17.0.2",
"react-dom": "^17.0.2", "react-dom": "^17.0.2",
"react-flow-renderer": "^9.7.4", "react-flow-renderer": "^9.7.4",

@ -1,4 +1,4 @@
import styles from './Button.module.css'; import styles from "./Button.module.css";
const Button = (props) => { const Button = (props) => {
let className = styles.Button; let className = styles.Button;
@ -7,7 +7,7 @@ const Button = (props) => {
} }
let tooltip = ""; let tooltip = "";
if (props.tooltip != undefined) { if (props.tooltip !== undefined) {
tooltip = <span className={styles.tooltip}>{props.tooltip}</span>; tooltip = <span className={styles.tooltip}>{props.tooltip}</span>;
} }

@ -11,10 +11,7 @@
text-transform: uppercase; text-transform: uppercase;
font-weight: bold; font-weight: bold;
font-size: 12px; font-size: 12px;
background: linear-gradient( background: linear-gradient(rgb(255, 255, 255) 25%, rgb(245, 245, 245) 100%);
rgb(255, 255, 255) 25%,
rgb(245, 245, 245) 100%
)
} }
.Button:hover { .Button:hover {
@ -52,7 +49,7 @@
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.25); box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.25);
line-height: 18px; line-height: 18px;
padding: 6px; padding: 6px;
transition: opacity .5s; transition: opacity 0.5s;
font-weight: normal; font-weight: normal;
text-align: left; text-align: left;
padding: 6px 12px; padding: 6px 12px;
@ -61,5 +58,5 @@
.Button:hover .tooltip { .Button:hover .tooltip {
visibility: visible; visibility: visible;
opacity: 100%; opacity: 100%;
transition: opacity .5s; transition: opacity 0.5s;
} }

@ -1,7 +0,0 @@
import styles from './ButtonRow.module.css';
const ButtonRow = (props) => {
return <div className={styles.ButtonRow}>{props.children}</div>;
};
export default ButtonRow;

@ -1,3 +0,0 @@
.ButtonRow {
text-align: center;
}

@ -1,7 +1,7 @@
import styles from './Card.module.css'; import styles from "./Card.module.css";
const Card = (props) => { const Card = (props) => {
return (<div className={styles.Card}>{props.children}</div>); return <div className={styles.Card}>{props.children}</div>;
}; };
export default Card; export default Card;

@ -1,6 +1,6 @@
import form_styles from './Form.module.css'; import form_styles from "./Form.module.css";
import Button from './Button'; import Button from "./Button";
import { validate } from './Form'; import { validate } from "./Form";
const DictInputRow = (props) => { const DictInputRow = (props) => {
const dict = { ...props.value }; const dict = { ...props.value };
@ -14,7 +14,7 @@ const DictInputRow = (props) => {
} }
let tooltip = ""; let tooltip = "";
if (props.tooltip != undefined) { if (props.tooltip !== undefined) {
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />; tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
} }
@ -32,7 +32,11 @@ const DictInputRow = (props) => {
const form = []; const form = [];
Object.keys(dict).forEach((key, index) => { Object.keys(dict).forEach((key, index) => {
let label = <span>{props.label} {unit}</span>; let label = (
<span>
{props.label} {unit}
</span>
);
if (index > 0) { if (index > 0) {
label = ""; label = "";
} }
@ -54,7 +58,7 @@ const DictInputRow = (props) => {
value={key} value={key}
placeholder={props.keyPlaceholder} placeholder={props.keyPlaceholder}
disabled={props.disableKeys} disabled={props.disableKeys}
onChange={e => onChangeKey(key, e.target.value)} onChange={(e) => onChangeKey(key, e.target.value)}
/> />
<input <input
type="text" type="text"
@ -62,7 +66,7 @@ const DictInputRow = (props) => {
value={dict[key]} value={dict[key]}
placeholder={props.valuePlaceholder} placeholder={props.valuePlaceholder}
className={className} className={className}
onChange={e => onChangeValue(key, e.target.value)} onChange={(e) => onChangeValue(key, e.target.value)}
/> />
{tooltip} {tooltip}
</div> </div>
@ -72,7 +76,7 @@ const DictInputRow = (props) => {
return <>{form}</>; return <>{form}</>;
}; };
function renameKey(obj, prevKey, newKey) { export function renameKey(obj, prevKey, newKey) {
const keys = Object.keys(obj); const keys = Object.keys(obj);
return keys.reduce((acc, val) => { return keys.reduce((acc, val) => {
if (val === prevKey) { if (val === prevKey) {

@ -1,11 +1,10 @@
import form_styles from './Form.module.css'; import form_styles from "./Form.module.css";
import Button from './Button'; import Button from "./Button";
import { useRef } from 'react'; import { useRef } from "react";
const FileInputRow = (props) => { const FileInputRow = (props) => {
let tooltip = ""; let tooltip = "";
if (props.tooltip != undefined) { if (props.tooltip !== undefined) {
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />; tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
} }
@ -27,29 +26,14 @@ const FileInputRow = (props) => {
fileElem.current.value = ""; fileElem.current.value = "";
}; };
return <div className={form_styles.FormRow}> return (
<div className={form_styles.FormRow}>
<label>{props.label}</label> <label>{props.label}</label>
<input type="text" value={props.value} disabled="disabled" /> <input type="text" value={props.value} disabled="disabled" />
<Button <Button label="Upload" kind="inline" onClick={onClickUpload} />
label="Upload" <Button label="Download" kind="inline" onClick={props.onDownload} />
kind="inline" <Button label="Clear" kind="inline" onClick={props.onClear} />
onClick={onClickUpload} <Button label="Template" kind="inline" onClick={props.onTemplate} />
/>
<Button
label="Download"
kind="inline"
onClick={props.onDownload}
/>
<Button
label="Clear"
kind="inline"
onClick={props.onClear}
/>
<Button
label="Template"
kind="inline"
onClick={props.onTemplate}
/>
{tooltip} {tooltip}
<input <input
type="file" type="file"
@ -58,7 +42,8 @@ const FileInputRow = (props) => {
style={{ display: "none" }} style={{ display: "none" }}
onChange={onFileSelected} onChange={onFileSelected}
/> />
</div>; </div>
);
}; };
export default FileInputRow; export default FileInputRow;

@ -1,10 +1,15 @@
import styles from './Footer.module.css'; import styles from "./Footer.module.css";
const Footer = () => { const Footer = () => {
return <div className={styles.Footer}> return (
<div className={styles.Footer}>
<p>RELOG: Reverse Logistics Optimization</p> <p>RELOG: Reverse Logistics Optimization</p>
<p>Copyright &copy; 2020&mdash;2022, UChicago Argonne, LLC. All Rights Reserved.</p> <p>
</div>; Copyright &copy; 2020&mdash;2022, UChicago Argonne, LLC. All Rights
Reserved.
</p>
</div>
);
}; };
export default Footer; export default Footer;

@ -7,5 +7,4 @@
font-size: 14px; font-size: 14px;
line-height: 8px; line-height: 8px;
min-width: 900px; min-width: 900px;
} }

@ -1,11 +1,11 @@
const VALIDATION_REGEX = { const VALIDATION_REGEX = {
int: new RegExp('^[0-9]+$'), int: new RegExp("^[0-9]+$"),
intList: new RegExp('^\[[0-9]+(,[0-9]+)*\]$'), intList: new RegExp("[[0-9]*]$"),
float: new RegExp('^[0-9]*\\.?[0-9]*$'), float: new RegExp("^[0-9]*\\.?[0-9]*$"),
floatList: new RegExp("^[?[0-9,.]*]?$"),
}; };
export const validate = (kind, value) => { export const validate = (kind, value) => {
if (value.length == 0) return false;
if (!VALIDATION_REGEX[kind].test(value)) { if (!VALIDATION_REGEX[kind].test(value)) {
return false; return false;
} }

@ -1,10 +1,14 @@
import styles from './Header.module.css'; import styles from "./Header.module.css";
const Header = () => { const Header = (props) => {
return ( return (
<div className={styles.HeaderBox}> <div className={styles.HeaderBox}>
<div className={styles.HeaderContent}> <div className={styles.HeaderContent}>
<h1>RELOG</h1> <h1>RELOG</h1>
<h2>{props.title}</h2>
<div style={{ float: "right", paddingTop: "5px" }}>
{props.children}
</div>
</div> </div>
</div> </div>
); );

@ -11,9 +11,18 @@
max-width: var(--site-width); max-width: var(--site-width);
} }
.HeaderContent h1 { .HeaderContent h1,
.HeaderContent h2 {
line-height: 48px; line-height: 48px;
font-size: 28px; font-size: 28px;
padding: 12px; padding: 12px;
margin: 0; margin: 0;
display: inline-block;
vertical-align: middle;
}
.HeaderContent h2 {
font-size: 22px;
font-weight: normal;
color: rgba(0, 0, 0, 0.6);
} }

@ -1,47 +1,76 @@
import React, { useState } from 'react'; import React, { useState, useRef } from "react";
import './index.css'; import "./index.css";
import PipelineBlock from './PipelineBlock'; import PipelineBlock from "./PipelineBlock";
import ParametersBlock from './ParametersBlock'; import ParametersBlock from "./ParametersBlock";
import ProductBlock from './ProductBlock'; import ProductBlock from "./ProductBlock";
import PlantBlock from './PlantBlock'; import PlantBlock from "./PlantBlock";
import ButtonRow from './ButtonRow'; import Button from "./Button";
import Button from './Button'; import Header from "./Header";
import Footer from "./Footer";
const defaultData = { import { defaultData, defaultPlant, defaultProduct } from "./defaults";
parameters: { import { randomPosition } from "./PipelineBlock";
"time horizon (years)": "1", import { exportData, importData } from "./export";
"building period (years)": "[1]", import { generateFile } from "./csv";
"annual inflation rate (%)": "0",
}, const setDefaults = (actualDict, defaultDict) => {
products: { for (const [key, defaultValue] of Object.entries(defaultDict)) {
}, if (!(key in actualDict)) {
plants: { if (typeof defaultValue === "object") {
actualDict[key] = { ...defaultValue };
} else {
actualDict[key] = defaultValue;
}
}
} }
}; };
const defaultProduct = { const cleanDict = (dict, defaultDict) => {
"initial amounts": {}, for (const key of Object.keys(dict)) {
"acquisition cost ($/tonne)": "0", if (!(key in defaultDict)) {
"disposal cost ($/tonne)": "0", delete dict[key];
"disposal limit (tonne)": "0", }
"transportation cost ($/km/tonne)": "0", }
"transportation energy (J/km/tonne)": "0",
"transportation emissions (J/km/tonne)": {}
}; };
const randomPosition = () => { const fixLists = (dict, blacklist, stringify) => {
return Math.round(Math.random() * 30) * 15; for (const [key, val] of Object.entries(dict)) {
if (blacklist.includes(key)) continue;
if (Array.isArray(val)) {
// Replace constant lists by a single number
let isConstant = true;
for (let i = 1; i < val.length; i++) {
if (val[i - 1] !== val[i]) {
isConstant = false;
break;
}
}
if (isConstant) dict[key] = val[0];
// Convert lists to JSON strings
if (stringify) dict[key] = JSON.stringify(dict[key]);
}
if (typeof val === "object") {
fixLists(val, blacklist, stringify);
}
}
}; };
const InputPage = () => { const InputPage = () => {
let [data, setData] = useState(defaultData); const fileElem = useRef();
let savedData = JSON.parse(localStorage.getItem("data"));
if (!savedData) savedData = defaultData;
let [data, setData] = useState(savedData);
const save = (data) => {
localStorage.setItem("data", JSON.stringify(data));
};
// onAdd
// ------------------------------------------------------------------------
const promptName = (prevData) => { const promptName = (prevData) => {
const name = prompt("Name"); const name = prompt("Name");
if (!name || name.length == 0) return; if (!name || name.length === 0) return;
if (name in prevData.products || name in prevData.plants) return; if (name in prevData.products || name in prevData.plants) return;
return name; return name;
}; };
@ -51,11 +80,13 @@ const InputPage = () => {
const name = promptName(prevData); const name = promptName(prevData);
if (name === undefined) return prevData; if (name === undefined) return prevData;
const newData = { ...prevData }; const newData = { ...prevData };
const [x, y] = randomPosition();
newData.plants[name] = { newData.plants[name] = {
x: randomPosition(), ...defaultPlant,
y: randomPosition(), x: x,
outputs: {}, y: y,
}; };
save(newData);
return newData; return newData;
}); });
}; };
@ -65,22 +96,24 @@ const InputPage = () => {
const name = promptName(prevData); const name = promptName(prevData);
if (name === undefined) return prevData; if (name === undefined) return prevData;
const newData = { ...prevData }; const newData = { ...prevData };
const [x, y] = randomPosition();
console.log(x, y);
newData.products[name] = { newData.products[name] = {
...defaultProduct, ...defaultProduct,
x: randomPosition(), x: x,
y: randomPosition(), y: y,
}; };
save(newData);
return newData; return newData;
}); });
}; };
// onRename
// ------------------------------------------------------------------------
const onRenamePlant = (prevName, newName) => { const onRenamePlant = (prevName, newName) => {
setData((prevData) => { setData((prevData) => {
const newData = { ...prevData }; const newData = { ...prevData };
newData.plants[newName] = newData.plants[prevName]; newData.plants[newName] = newData.plants[prevName];
delete newData.plants[prevName]; delete newData.plants[prevName];
save(newData);
return newData; return newData;
}); });
}; };
@ -90,30 +123,33 @@ const InputPage = () => {
const newData = { ...prevData }; const newData = { ...prevData };
newData.products[newName] = newData.products[prevName]; newData.products[newName] = newData.products[prevName];
delete newData.products[prevName]; delete newData.products[prevName];
for (const [plantName, plant] of Object.entries(newData.plants)) { for (const [, plant] of Object.entries(newData.plants)) {
if (plant.input == prevName) { if (plant.input === prevName) {
plant.input = newName; plant.input = newName;
} }
let outputFound = false; let outputFound = false;
for (const [outputName, outputValue] of Object.entries(plant.outputs)) { for (const [outputName] of Object.entries(
if (outputName == prevName) outputFound = true; plant["outputs (tonne/tonne)"]
)) {
if (outputName === prevName) outputFound = true;
} }
if (outputFound) { if (outputFound) {
plant.outputs[newName] = plant.outputs[prevName]; plant["outputs (tonne/tonne)"][newName] =
delete plant.outputs[prevName]; plant["outputs (tonne/tonne)"][prevName];
delete plant["outputs (tonne/tonne)"][prevName];
} }
} }
save(newData);
return newData; return newData;
}); });
}; };
// onMove
// ------------------------------------------------------------------------
const onMovePlant = (plantName, x, y) => { const onMovePlant = (plantName, x, y) => {
setData((prevData) => { setData((prevData) => {
const newData = { ...prevData }; const newData = { ...prevData };
newData.plants[plantName].x = x; newData.plants[plantName].x = x;
newData.plants[plantName].y = y; newData.plants[plantName].y = y;
save(newData);
return newData; return newData;
}); });
}; };
@ -123,16 +159,16 @@ const InputPage = () => {
const newData = { ...prevData }; const newData = { ...prevData };
newData.products[productName].x = x; newData.products[productName].x = x;
newData.products[productName].y = y; newData.products[productName].y = y;
save(newData);
return newData; return newData;
}); });
}; };
// onRemove
// ------------------------------------------------------------------------
const onRemovePlant = (plantName) => { const onRemovePlant = (plantName) => {
setData((prevData) => { setData((prevData) => {
const newData = { ...prevData }; const newData = { ...prevData };
delete newData.plants[plantName]; delete newData.plants[plantName];
save(newData);
return newData; return newData;
}); });
}; };
@ -141,69 +177,81 @@ const InputPage = () => {
setData((prevData) => { setData((prevData) => {
const newData = { ...prevData }; const newData = { ...prevData };
delete newData.products[productName]; delete newData.products[productName];
for (const [plantName, plant] of Object.entries(newData.plants)) { for (const [, plant] of Object.entries(newData.plants)) {
if (plant.input == productName) { if (plant.input === productName) {
delete plant.input; delete plant.input;
} }
let outputFound = false; let outputFound = false;
for (const [outputName, outputValue] of Object.entries(plant.outputs)) { for (const [outputName] of Object.entries(
if (outputName == productName) outputFound = true; plant["outputs (tonne/tonne)"]
)) {
if (outputName === productName) outputFound = true;
} }
if (outputFound) { if (outputFound) {
delete plant.outputs[productName]; delete plant["outputs (tonne/tonne)"][productName];
} }
} }
save(newData);
return newData; return newData;
}); });
}; };
// Inputs & Outputs
// ------------------------------------------------------------------------
const onSetPlantInput = (plantName, productName) => { const onSetPlantInput = (plantName, productName) => {
setData((prevData) => { setData((prevData) => {
const newData = { ...prevData }; const newData = { ...prevData };
newData.plants[plantName].input = productName; newData.plants[plantName].input = productName;
save(newData);
return newData; return newData;
}); });
}; };
const onAddPlantOutput = (plantName, productName) => { const onAddPlantOutput = (plantName, productName) => {
setData((prevData) => { setData((prevData) => {
if (productName in prevData.plants[plantName].outputs) { if (productName in prevData.plants[plantName]["outputs (tonne/tonne)"]) {
return prevData; return prevData;
} }
const newData = { ...prevData }; const newData = { ...prevData };
newData.plants[plantName].outputs[productName] = 0; [
"outputs (tonne/tonne)",
"disposal cost ($/tonne)",
"disposal limit (tonne)",
].forEach((key) => {
newData.plants[plantName][key] = { ...newData.plants[plantName][key] };
newData.plants[plantName][key][productName] = 0;
});
save(newData);
return newData; return newData;
}); });
}; };
// onSave
// ------------------------------------------------------------------------
const onSave = () => { const onSave = () => {
console.log(data); generateFile("case.json", JSON.stringify(exportData(data), null, 2));
}; };
// onChange const onClear = () => {
// ------------------------------------------------------------------------ const newData = JSON.parse(JSON.stringify(defaultData));
const onChangeParameters = (val) => { setData(newData);
setData(prevData => { save(newData);
const newData = { ...prevData }; };
newData.parameters = val;
return newData; const onLoad = (contents) => {
}); const newData = importData(JSON.parse(contents));
setData(newData);
save(newData);
}; };
const onChangeProduct = (prodName, val) => { const onChange = (val, field1, field2) => {
setData(prevData => { setData((prevData) => {
const newData = { ...prevData }; const newData = { ...prevData };
newData.products[prodName] = val; if (field2 !== undefined) {
newData[field1][field2] = val;
} else {
newData[field1] = val;
}
save(newData);
return newData; return newData;
}); });
}; };
// ------------------------------------------------------------------------
let productComps = []; let productComps = [];
for (const [prodName, prod] of Object.entries(data.products)) { for (const [prodName, prod] of Object.entries(data.products)) {
productComps.push( productComps.push(
@ -211,7 +259,7 @@ const InputPage = () => {
key={prodName} key={prodName}
name={prodName} name={prodName}
value={prod} value={prod}
onChange={v => onChangeProduct(prodName, v)} onChange={(v) => onChange(v, "products", prodName, v)}
/> />
); );
} }
@ -219,11 +267,42 @@ const InputPage = () => {
let plantComps = []; let plantComps = [];
for (const [plantName, plant] of Object.entries(data.plants)) { for (const [plantName, plant] of Object.entries(data.plants)) {
plantComps.push( plantComps.push(
<PlantBlock key={plantName} name={plantName} /> <PlantBlock
key={plantName}
name={plantName}
value={plant}
onChange={(v) => onChange(v, "plants", plantName)}
/>
); );
} }
return <> const onFileSelected = () => {
const file = fileElem.current.files[0];
if (file) {
const reader = new FileReader();
reader.addEventListener("load", () => {
onLoad(reader.result);
});
reader.readAsText(file);
}
fileElem.current.value = "";
};
return (
<>
<Header title="Case Builder">
<Button label="Clear" onClick={onClear} />
<Button label="Load" onClick={(e) => fileElem.current.click()} />
<Button label="Save" onClick={onSave} />
<input
type="file"
ref={fileElem}
accept=".json"
style={{ display: "none" }}
onChange={onFileSelected}
/>
</Header>
<div id="content">
<PipelineBlock <PipelineBlock
onAddPlant={onAddPlant} onAddPlant={onAddPlant}
onAddPlantOutput={onAddPlantOutput} onAddPlantOutput={onAddPlantOutput}
@ -240,15 +319,14 @@ const InputPage = () => {
/> />
<ParametersBlock <ParametersBlock
value={data.parameters} value={data.parameters}
onChange={onChangeParameters} onChange={(v) => onChange(v, "parameters")}
/> />
{productComps} {productComps}
{plantComps} {plantComps}
<ButtonRow> </div>
<Button label="Load" /> <Footer />
<Button label="Save" onClick={onSave} /> </>
</ButtonRow> );
</>;
}; };
export default InputPage; export default InputPage;

@ -1,7 +1,7 @@
import Section from './Section'; import Section from "./Section";
import Card from './Card'; import Card from "./Card";
import Form from './Form'; import Form from "./Form";
import TextInputRow from './TextInputRow'; import TextInputRow from "./TextInputRow";
const ParametersBlock = (props) => { const ParametersBlock = (props) => {
const onChangeField = (field, val) => { const onChangeField = (field, val) => {
@ -18,7 +18,7 @@ const ParametersBlock = (props) => {
unit="years" unit="years"
tooltip="Number of years in the simulation." tooltip="Number of years in the simulation."
value={props.value["time horizon (years)"]} value={props.value["time horizon (years)"]}
onChange={v => onChangeField("time horizon (years)", v)} onChange={(v) => onChangeField("time horizon (years)", v)}
validate="int" validate="int"
/> />
<TextInputRow <TextInputRow
@ -26,17 +26,17 @@ const ParametersBlock = (props) => {
unit="years" unit="years"
tooltip="List of years in which we are allowed to open new plants. For example, if this parameter is set to [1,2,3], we can only open plants during the first three years. By default, this equals [1]; that is, plants can only be opened during the first year." tooltip="List of years in which we are allowed to open new plants. For example, if this parameter is set to [1,2,3], we can only open plants during the first three years. By default, this equals [1]; that is, plants can only be opened during the first year."
value={props.value["building period (years)"]} value={props.value["building period (years)"]}
onChange={v => onChangeField("building period (years)", v)} onChange={(v) => onChangeField("building period (years)", v)}
validate="intList" validate="intList"
/> />
<TextInputRow {/* <TextInputRow
label="Annual inflation rate" label="Annual inflation rate"
unit="%" unit="%"
tooltip="Rate of inflation applied to all costs." tooltip="Rate of inflation applied to all costs."
value={props.value["annual inflation rate (%)"]} value={props.value["annual inflation rate (%)"]}
onChange={v => onChangeField("annual inflation rate (%)", v)} onChange={(v) => onChangeField("annual inflation rate (%)", v)}
validate="float" validate="float"
/> /> */}
</Form> </Form>
</Card> </Card>
</> </>

@ -1,10 +1,48 @@
import React from 'react'; import React from "react";
import ReactFlow, { Background } from 'react-flow-renderer'; import ReactFlow, { Background, isNode } from "react-flow-renderer";
import Section from './Section'; import Section from "./Section";
import Card from './Card'; import Card from "./Card";
import Button from './Button'; import Button from "./Button";
import styles from './PipelineBlock.module.css'; import styles from "./PipelineBlock.module.css";
import dagre from "dagre";
window.nextX = 15;
window.nextY = 15;
export const randomPosition = () => {
window.nextY += 60;
if (window.nextY >= 500) {
window.nextY = 15;
window.nextX += 150;
}
return [window.nextX, window.nextY];
};
const getLayoutedElements = (elements) => {
const nodeWidth = 125;
const nodeHeight = 45;
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({ rankdir: "LR" });
elements.forEach((el) => {
if (isNode(el)) {
dagreGraph.setNode(el.id, { width: nodeWidth, height: nodeHeight });
} else {
dagreGraph.setEdge(el.source, el.target);
}
});
dagre.layout(dagreGraph);
return elements.map((el) => {
if (isNode(el)) {
const n = dagreGraph.node(el.id);
el.position = {
x: 15 + n.x - nodeWidth / 2,
y: 15 + n.y - nodeHeight / 2,
};
}
return el;
});
};
const PipelineBlock = (props) => { const PipelineBlock = (props) => {
let elements = []; let elements = [];
@ -13,10 +51,10 @@ const PipelineBlock = (props) => {
mapNameToType[productName] = "product"; mapNameToType[productName] = "product";
elements.push({ elements.push({
id: productName, id: productName,
data: { label: productName, type: 'product' }, data: { label: productName, type: "product" },
position: { x: product.x, y: product.y }, position: { x: product.x, y: product.y },
sourcePosition: 'right', sourcePosition: "right",
targetPosition: 'left', targetPosition: "left",
className: styles.ProductNode, className: styles.ProductNode,
}); });
} }
@ -25,14 +63,14 @@ const PipelineBlock = (props) => {
mapNameToType[plantName] = "plant"; mapNameToType[plantName] = "plant";
elements.push({ elements.push({
id: plantName, id: plantName,
data: { label: plantName, type: 'plant' }, data: { label: plantName, type: "plant" },
position: { x: plant.x, y: plant.y }, position: { x: plant.x, y: plant.y },
sourcePosition: 'right', sourcePosition: "right",
targetPosition: 'left', targetPosition: "left",
className: styles.PlantNode, className: styles.PlantNode,
}); });
if (plant.input != undefined) { if (plant.input !== undefined) {
elements.push({ elements.push({
id: `${plant.input}-${plantName}`, id: `${plant.input}-${plantName}`,
source: plant.input, source: plant.input,
@ -43,7 +81,9 @@ const PipelineBlock = (props) => {
}); });
} }
for (const [productName, amount] of Object.entries(plant.outputs)) { for (const [productName] of Object.entries(
plant["outputs (tonne/tonne)"]
)) {
elements.push({ elements.push({
id: `${plantName}-${productName}`, id: `${plantName}-${productName}`,
source: plantName, source: plantName,
@ -53,15 +93,14 @@ const PipelineBlock = (props) => {
selectable: false, selectable: false,
}); });
} }
} }
const onNodeDoubleClick = (ev, node) => { const onNodeDoubleClick = (ev, node) => {
const oldName = node.data.label; const oldName = node.data.label;
const newName = window.prompt("Enter new name", oldName); const newName = window.prompt("Enter new name", oldName);
if (newName == undefined || newName.length == 0) return; if (newName === undefined || newName.length === 0) return;
if (newName in mapNameToType) return; if (newName in mapNameToType) return;
if (node.data.type == "plant") { if (node.data.type === "plant") {
props.onRenamePlant(oldName, newName); props.onRenamePlant(oldName, newName);
} else { } else {
props.onRenameProduct(oldName, newName); props.onRenameProduct(oldName, newName);
@ -69,9 +108,9 @@ const PipelineBlock = (props) => {
}; };
const onElementsRemove = (elements) => { const onElementsRemove = (elements) => {
elements.forEach(el => { elements.forEach((el) => {
if (!(el.id in mapNameToType)) return; if (!(el.id in mapNameToType)) return;
if (el.data.type == "plant") { if (el.data.type === "plant") {
props.onRemovePlant(el.data.label); props.onRemovePlant(el.data.label);
} else { } else {
props.onRemoveProduct(el.data.label); props.onRemoveProduct(el.data.label);
@ -80,7 +119,7 @@ const PipelineBlock = (props) => {
}; };
const onNodeDragStop = (ev, node) => { const onNodeDragStop = (ev, node) => {
if (node.data.type == "plant") { if (node.data.type === "plant") {
props.onMovePlant(node.data.label, node.position.x, node.position.y); props.onMovePlant(node.data.label, node.position.x, node.position.y);
} else { } else {
props.onMoveProduct(node.data.label, node.position.x, node.position.y); props.onMoveProduct(node.data.label, node.position.x, node.position.y);
@ -97,6 +136,19 @@ const PipelineBlock = (props) => {
} }
}; };
const onLayout = () => {
const layoutedElements = getLayoutedElements(elements);
layoutedElements.forEach((el) => {
if (isNode(el)) {
if (el.data.type === "plant") {
props.onMovePlant(el.data.label, el.position.x, el.position.y);
} else {
props.onMoveProduct(el.data.label, el.position.x, el.position.y);
}
}
});
};
return ( return (
<> <>
<Section title="Pipeline" /> <Section title="Pipeline" />
@ -117,17 +169,14 @@ const PipelineBlock = (props) => {
<Background /> <Background />
</ReactFlow> </ReactFlow>
</div> </div>
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: "center" }}>
<Button <Button
label="Add product" label="Add product"
kind="inline" kind="inline"
onClick={props.onAddProduct} onClick={props.onAddProduct}
/> />
<Button <Button label="Add plant" kind="inline" onClick={props.onAddPlant} />
label="Add plant" <Button label="Auto-Layout" kind="inline" onClick={onLayout} />
kind="inline"
onClick={props.onAddPlant}
/>
<Button <Button
label="?" label="?"
kind="inline" kind="inline"

@ -1,24 +1,25 @@
.PipelineBlock { .PipelineBlock {
height: 600px; height: 600px !important;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1) !important;
border-radius: var(--border-radius); border-radius: var(--border-radius) !important;
margin-bottom: 12px; margin-bottom: 12px !important;
} }
.PlantNode, .ProductNode { .PlantNode,
border-color: rgba(0, 0, 0, 0.8); .ProductNode {
color: black; border-color: rgba(0, 0, 0, 0.8) !important;
font-size: 13px; color: black !important;
border-width: 1px; font-size: 13px !important;
border-radius: 6px; border-width: 1px !important;
box-shadow: 0px 2px 4px -3px black; border-radius: 6px !important;
width: 100px; box-shadow: 0px 2px 4px -3px black !important;
width: 100px !important;
} }
.PlantNode { .PlantNode {
background-color: #0df; background-color: #8d8 !important;
} }
.ProductNode { .ProductNode {
background-color: #f6f6f6; background-color: #e6e6e6 !important;
} }

@ -1,20 +1,104 @@
import Section from './Section'; import Section from "./Section";
import Card from './Card'; import Card from "./Card";
import Form from './Form'; import Form from "./Form";
import TextInputRow from './TextInputRow'; import TextInputRow from "./TextInputRow";
import FileInputRow from './FileInputRow'; import FileInputRow from "./FileInputRow";
import DictInputRow from './DictInputRow'; import DictInputRow from "./DictInputRow";
import { csvFormat, csvParse, generateFile } from "./csv";
const PlantBlock = (props) => { const PlantBlock = (props) => {
const emissions = { const onChange = (val, field1, field2, field3) => {
"CO2": "0.05", const newPlant = { ...props.value };
"CH4": "0.01", if (field3 !== undefined) {
"N2O": "0.04", newPlant[field1][field2][field3] = val;
} else if (field2 !== undefined) {
newPlant[field1][field2] = val;
} else {
newPlant[field1] = val;
}
props.onChange(newPlant);
}; };
const output = {
"Nickel": "0.5", const onCandidateLocationsTemplate = () => {
"Metal casing": "0.35", generateFile(
"Candidate locations - Template.csv",
csvFormat([
{
name: "Washakie County",
"latitude (deg)": "43.8356",
"longitude (deg)": "-107.6602",
"area cost factor": "0.88",
},
{
name: "Platte County",
"latitude (deg)": "42.1314",
"longitude (deg)": "-104.9676",
"area cost factor": "1.29",
},
{
name: "Park County",
"latitude (deg)": "44.4063",
"longitude (deg)": "-109.4153",
"area cost factor": "0.99",
},
{
name: "Goshen County",
"latitude (deg)": "42.0853",
"longitude (deg)": "-104.3534",
"area cost factor": "1",
},
])
);
};
const onCandidateLocationsFile = (contents) => {
const data = csvParse({
contents: contents,
requiredCols: [
"name",
"latitude (deg)",
"longitude (deg)",
"area cost factor",
],
});
const result = {};
data.forEach((el) => {
result[el["name"]] = {
"latitude (deg)": el["latitude (deg)"],
"longitude (deg)": el["longitude (deg)"],
"area cost factor": el["area cost factor"],
};
});
onChange(result, "locations");
};
const onCandidateLocationsDownload = () => {
const result = [];
for (const [locationName, locationDict] of Object.entries(
props.value["locations"]
)) {
result.push({
name: locationName,
"latitude (deg)": locationDict["latitude (deg)"],
"longitude (deg)": locationDict["longitude (deg)"],
"area cost factor": locationDict["area cost factor"],
});
}
generateFile(`Candidate locations - ${props.name}.csv`, csvFormat(result));
}; };
const onCandidateLocationsClear = () => {
onChange({}, "locations");
};
let description = "No locations set";
const nCenters = Object.keys(props.value["locations"]).length;
if (nCenters > 0) description = `${nCenters} locations`;
const shouldDisableMaxCap =
props.value["minimum capacity (tonne)"] ===
props.value["maximum capacity (tonne)"];
return ( return (
<> <>
<Section title={props.name} /> <Section title={props.name} />
@ -23,74 +107,109 @@ const PlantBlock = (props) => {
<h1>General information</h1> <h1>General information</h1>
<FileInputRow <FileInputRow
label="Candidate locations" label="Candidate locations"
tooltip="A dictionary mapping the name of the location to a dictionary which describes the site characteristics." tooltip="A table describing potential locations where plants can be built and their characteristics."
onTemplate={onCandidateLocationsTemplate}
onFile={onCandidateLocationsFile}
onDownload={onCandidateLocationsDownload}
onClear={onCandidateLocationsClear}
value={description}
/> />
<h1>Inputs & Outputs</h1> <h1>Inputs & Outputs</h1>
<TextInputRow <TextInputRow
label="Input" label="Input"
tooltip="The name of the product that this plant takes as input. Only one input is accepted per plant." tooltip="The name of the product that this plant takes as input."
disabled="disabled" disabled="disabled"
value="Battery" value="Battery"
/> />
<DictInputRow <DictInputRow
label="Outputs" label="Outputs"
unit="tonne/tonne" unit="tonne/tonne"
tooltip="A dictionary specifying how many tonnes of each product is produced for each tonnes of input. If the plant does not output anything, this key may be omitted." tooltip="A dictionary specifying how many tonnes of each product is produced for each tonne of input."
value={output} value={props.value["outputs (tonne/tonne)"]}
onChange={(v) => onChange(v, "outputs (tonne/tonne)")}
disableKeys={true} disableKeys={true}
default="0" validate="float"
/> />
<h1>Capacity & costs</h1> <h1>Capacity & Costs</h1>
<TextInputRow <TextInputRow
label="Minimum capacity" label="Minimum capacity"
unit="tonne" unit="tonne"
tooltip="The minimum size of the plant." tooltip="The minimum size of the plant."
default="0" value={props.value["minimum capacity (tonne)"]}
onChange={(v) => onChange(v, "minimum capacity (tonne)")}
validate="float"
/> />
<TextInputRow <TextInputRow
label="Opening cost (min capacity)" label="Opening cost (min capacity)"
unit="$" unit="$"
tooltip="The cost to open the plant at minimum capacity." tooltip="The cost to open the plant at minimum capacity."
default="0.00" value={props.value["opening cost (min capacity) ($)"]}
onChange={(v) => onChange(v, "opening cost (min capacity) ($)")}
validate="float"
/> />
<TextInputRow <TextInputRow
label="Fixed operating cost (min capacity)" label="Fixed operating cost (min capacity)"
unit="$" unit="$"
tooltip="The cost to keep the plant open, even if the plant doesn't process anything." tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
default="0.00" value={props.value["fixed operating cost (min capacity) ($)"]}
onChange={(v) =>
onChange(v, "fixed operating cost (min capacity) ($)")
}
validate="float"
/> />
<TextInputRow <TextInputRow
label="Maximum capacity" label="Maximum capacity"
unit="tonne" unit="tonne"
tooltip="The maximum size of the plant." tooltip="The maximum size of the plant."
default="0" value={props.value["maximum capacity (tonne)"]}
onChange={(v) => onChange(v, "maximum capacity (tonne)")}
validate="float"
/> />
<TextInputRow <TextInputRow
label="Opening cost (max capacity)" label="Opening cost (max capacity)"
unit="$" unit="$"
tooltip="The cost to open a plant of this size." tooltip="The cost to open a plant of this size."
default="0.00" value={
shouldDisableMaxCap
? ""
: props.value["opening cost (max capacity) ($)"]
}
onChange={(v) => onChange(v, "opening cost (max capacity) ($)")}
validate="float"
disabled={shouldDisableMaxCap}
/> />
<TextInputRow <TextInputRow
label="Fixed operating cost (max capacity)" label="Fixed operating cost (max capacity)"
unit="$" unit="$"
tooltip="The cost to keep the plant open, even if the plant doesn't process anything." tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
default="0.00" value={
shouldDisableMaxCap
? ""
: props.value["fixed operating cost (max capacity) ($)"]
}
onChange={(v) =>
onChange(v, "fixed operating cost (max capacity) ($)")
}
validate="float"
disabled={shouldDisableMaxCap}
/> />
<TextInputRow <TextInputRow
label="Variable operating cost" label="Variable operating cost"
unit="$" unit="$"
tooltip="The cost that the plant incurs to process each tonne of input." tooltip="The cost that the plant incurs to process each tonne of input."
default="0.00" value={props.value["variable operating cost ($/tonne)"]}
onChange={(v) => onChange(v, "variable operating cost ($/tonne)")}
validate="float"
/> />
<TextInputRow <TextInputRow
label="Energy expenditure" label="Energy expenditure"
unit="GJ/tonne" unit="GJ/tonne"
tooltip="The energy required to process 1 tonne of the input." tooltip="The energy required to process one tonne of the input."
default="0" value={props.value["energy (GJ/tonne)"]}
onChange={(v) => onChange(v, "energy (GJ/tonne)")}
validate="float"
/> />
<h1>Storage</h1> <h1>Storage</h1>
@ -98,13 +217,15 @@ const PlantBlock = (props) => {
label="Storage cost" label="Storage cost"
unit="$/tonne" unit="$/tonne"
tooltip="The cost to store a tonne of input product for one time period." tooltip="The cost to store a tonne of input product for one time period."
default="0.00" value={props.value["storage"]["cost ($/tonne)"]}
onChange={(v) => onChange(v, "storage", "cost ($/tonne)")}
/> />
<TextInputRow <TextInputRow
label="Storage limit" label="Storage limit"
unit="tonne" unit="tonne"
tooltip="The maximum amount of input product this plant can have in storage at any given time." tooltip="The maximum amount of input product this plant can have in storage at any given time."
default="0" value={props.value["storage"]["limit (tonne)"]}
onChange={(v) => onChange(v, "storage", "limit (tonne)")}
/> />
<h1>Disposal</h1> <h1>Disposal</h1>
@ -112,15 +233,18 @@ const PlantBlock = (props) => {
label="Disposal cost" label="Disposal cost"
unit="$/tonne" unit="$/tonne"
tooltip="The cost to dispose of the product." tooltip="The cost to dispose of the product."
value={output} value={props.value["disposal cost ($/tonne)"]}
onChange={(v) => onChange(v, "disposal cost ($/tonne)")}
disableKeys={true} disableKeys={true}
/> />
<DictInputRow <DictInputRow
label="Disposal limit" label="Disposal limit"
unit="tonne" unit="tonne"
tooltip="The maximum amount that can be disposed of. If an unlimited amount can be disposed, this key may be omitted." tooltip="The maximum amount that can be disposed of. If an unlimited amount can be disposed, leave blank."
value={output} value={props.value["disposal limit (tonne)"]}
onChange={(v) => onChange(v, "disposal limit (tonne)")}
disableKeys={true} disableKeys={true}
valuePlaceholder="Unlimited"
/> />
<h1>Emissions</h1> <h1>Emissions</h1>
@ -128,11 +252,11 @@ const PlantBlock = (props) => {
label="Emissions" label="Emissions"
unit="tonne/tonne" unit="tonne/tonne"
tooltip="A dictionary mapping the name of each greenhouse gas, produced to process each tonne of input, to the amount of gas produced (in tonne)." tooltip="A dictionary mapping the name of each greenhouse gas, produced to process each tonne of input, to the amount of gas produced (in tonne)."
value={emissions} value={props.value["emissions (tonne/tonne)"]}
onChange={(v) => onChange(v, "emissions (tonne/tonne)")}
keyPlaceholder="Emission name" keyPlaceholder="Emission name"
valuePlaceholder="0" valuePlaceholder="0"
/> />
</Form> </Form>
</Card> </Card>
</> </>

@ -1,11 +1,11 @@
import { useState } from 'react'; import Section from "./Section";
import Section from './Section'; import Card from "./Card";
import Card from './Card'; import Form from "./Form";
import Form from './Form'; import TextInputRow from "./TextInputRow";
import TextInputRow from './TextInputRow'; import FileInputRow from "./FileInputRow";
import FileInputRow from './FileInputRow'; import DictInputRow from "./DictInputRow";
import DictInputRow from './DictInputRow'; import { csvParse, extractNumericColumns, generateFile } from "./csv";
import * as d3 from 'd3'; import { csvFormat } from "d3";
const ProductBlock = (props) => { const ProductBlock = (props) => {
const onChange = (field, val) => { const onChange = (field, val) => {
@ -15,39 +15,18 @@ const ProductBlock = (props) => {
}; };
const onInitialAmountsFile = (contents) => { const onInitialAmountsFile = (contents) => {
const data = d3.csvParse(contents); const data = csvParse({
const T = data.columns.length - 3; contents: contents,
requiredCols: ["latitude (deg)", "longitude (deg)", "name"],
// Construct list of required columns
let isValid = true;
const requiredCols = ["latitude (deg)", "longitude (deg)", "name"];
for (let t = 0; t < T; t++) {
requiredCols.push(t + 1);
}
// Check required columns
requiredCols.forEach(col => {
if (!(col in data[0])) {
console.log(`Column "${col}" not found in CSV file.`);
isValid = false;
}
}); });
if (!isValid) return;
// Construct initial amounts dict
const result = {}; const result = {};
data.forEach(el => { data.forEach((el) => {
let amounts = [];
for (let t = 0; t < T; t++) {
amounts.push(el[t + 1]);
}
result[el["name"]] = { result[el["name"]] = {
"latitude (deg)": el["latitude (deg)"], "latitude (deg)": el["latitude (deg)"],
"longitude (deg)": el["longitude (deg)"], "longitude (deg)": el["longitude (deg)"],
"amount (tonne)": amounts, "amount (tonne)": extractNumericColumns(el, "amount"),
}; };
}); });
onChange("initial amounts", result); onChange("initial amounts", result);
}; };
@ -56,86 +35,109 @@ const ProductBlock = (props) => {
}; };
const onInitialAmountsTemplate = () => { const onInitialAmountsTemplate = () => {
exportToCsv( generateFile(
"Initial amounts - Template.csv", [ "Initial amounts - Template.csv",
["name", "latitude (deg)", "longitude (deg)", "1", "2", "3", "4", "5"], csvFormat([
["Washakie County", "43.8356", "-107.6602", "21902", "6160", "2721", "12917", "18048"], {
["Platte County", "42.1314", "-104.9676", "16723", "8709", "22584", "12278", "7196"], name: "Washakie County",
["Park County", "44.4063", "-109.4153", "14731", "11729", "15562", "7703", "23349"], "latitude (deg)": "43.8356",
["Goshen County", "42.0853", "-104.3534", "23266", "16299", "11470", "20107", "21592"], "longitude (deg)": "-107.6602",
]); "amount 1": "21902",
"amount 2": "6160",
"amount 3": "2721",
"amount 4": "12917",
"amount 5": "18048",
},
{
name: "Platte County",
"latitude (deg)": "42.1314",
"longitude (deg)": "-104.9676",
"amount 1": "16723",
"amount 2": "8709",
"amount 3": "22584",
"amount 4": "12278",
"amount 5": "7196",
},
{
name: "Park County",
"latitude (deg)": "44.4063",
"longitude (deg)": "-109.4153",
"amount 1": "14731",
"amount 2": "11729",
"amount 3": "15562",
"amount 4": "7703",
"amount 5": "23349",
},
])
);
}; };
const onInitialAmountsDownload = () => { const onInitialAmountsDownload = () => {
const result = []; const results = [];
for (const [locationName, locationDict] of Object.entries(props.value["initial amounts"])) { for (const [locationName, locationDict] of Object.entries(
// Add header props.value["initial amounts"]
if (result.length == 0) { )) {
const T = locationDict["amount (tonne)"].length; const row = {
const row = ["name", "latitude (deg)", "longitude (deg)"]; name: locationName,
for (let t = 0; t < T; t++) { "latitude (deg)": locationDict["latitude (deg)"],
row.push(t + 1); "longitude (deg)": locationDict["longitude (deg)"],
} };
result.push(row); locationDict["amount (tonne)"].forEach((el, idx) => {
} row[`amount ${idx + 1}`] = el;
// Add content row
const row = [locationName, locationDict["latitude (deg)"], locationDict["longitude (deg)"]];
locationDict["amount (tonne)"].forEach(el => {
row.push(el);
}); });
result.push(row); results.push(row);
} }
exportToCsv(`Initial amounts - ${props.name}`, result); generateFile(`Initial amounts - ${props.name}.csv`, csvFormat(results));
}; };
let description = "Not initially available"; let description = "Not initially available";
const nCenters = Object.keys(props.value["initial amounts"]).length; const nCenters = Object.keys(props.value["initial amounts"]).length;
if (nCenters > 0) { if (nCenters > 0) description = `${nCenters} collection centers`;
description = `${nCenters} collection centers`;
}
return ( return (
<> <>
<Section title={props.name} /> <Section title={props.name} />
<Card> <Card>
<Form> <Form>
<h1>General information</h1> <h1>General Information</h1>
<FileInputRow <FileInputRow
value={description} value={description}
label="Initial amounts" label="Initial amounts"
tooltip="A dictionary mapping the name of each location to its description (see below). If this product is not initially available, this key may be omitted." tooltip="A table indicating the amount of this product initially available at each collection center."
accept=".csv" accept=".csv"
onFile={onInitialAmountsFile} onFile={onInitialAmountsFile}
onDownload={onInitialAmountsDownload} onDownload={onInitialAmountsDownload}
onClear={onInitialAmountsClear} onClear={onInitialAmountsClear}
onTemplate={onInitialAmountsTemplate} onTemplate={onInitialAmountsTemplate}
/> />
<TextInputRow
label="Acquisition cost"
unit="$/tonne"
tooltip="The cost to acquire one tonne of this product from collection centers. Does not apply to plant outputs."
value={props.value["acquisition cost ($/tonne)"]}
onChange={v => onChange("acquisition cost ($/tonne)", v)}
validate="float"
/>
<h1>Disposal</h1> <h1>Disposal</h1>
<TextInputRow <TextInputRow
label="Disposal cost" label="Disposal cost"
unit="$/tonne" unit="$/tonne"
tooltip="The cost to dispose of one tonne of this product at a collection center, without further processing. Does not apply to plant outputs." tooltip="The cost to dispose of one tonne of this product at a collection center, without further processing."
value={props.value["disposal cost ($/tonne)"]} value={props.value["disposal cost ($/tonne)"]}
onChange={v => onChange("disposal cost ($/tonne)", v)} onChange={(v) => onChange("disposal cost ($/tonne)", v)}
validate="float" validate="floatList"
/> />
<TextInputRow <TextInputRow
label="Disposal limit" label="Disposal limit"
unit="tonne" unit="tonne"
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing." tooltip="The maximum amount (in tonnes) of this product that can be disposed of across all collection centers, without further processing."
value={props.value["disposal limit (tonne)"]} value={props.value["disposal limit (tonne)"]}
onChange={v => onChange("disposal limit (tonne)", v)} onChange={(v) => onChange("disposal limit (tonne)", v)}
validate="float" validate="floatList"
disabled={String(props.value["disposal limit (%)"]).length > 0}
/>
<TextInputRow
label="Disposal limit"
unit="%"
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing, as a percentage of the total amount available."
value={props.value["disposal limit (%)"]}
onChange={(v) => onChange("disposal limit (%)", v)}
validate="floatList"
disabled={props.value["disposal limit (tonne)"].length > 0}
/> />
<h1>Transportation</h1> <h1>Transportation</h1>
@ -144,25 +146,27 @@ const ProductBlock = (props) => {
unit="$/km/tonne" unit="$/km/tonne"
tooltip="The cost to transport this product." tooltip="The cost to transport this product."
value={props.value["transportation cost ($/km/tonne)"]} value={props.value["transportation cost ($/km/tonne)"]}
onChange={v => onChange("transportation cost ($/km/tonne)", v)} onChange={(v) => onChange("transportation cost ($/km/tonne)", v)}
validate="float" validate="floatList"
/> />
<TextInputRow <TextInputRow
label="Transportation energy" label="Transportation energy"
unit="J/km/tonne" unit="J/km/tonne"
tooltip="The energy required to transport this product." tooltip="The energy required to transport this product."
value={props.value["transportation energy (J/km/tonne)"]} value={props.value["transportation energy (J/km/tonne)"]}
onChange={v => onChange("transportation energy (J/km/tonne)", v)} onChange={(v) => onChange("transportation energy (J/km/tonne)", v)}
validate="float" validate="floatList"
/> />
<DictInputRow <DictInputRow
label="Transportation emissions" label="Transportation emissions"
unit="J/km/tonne" unit="J/km/tonne"
tooltip="A dictionary mapping the name of each greenhouse gas, produced to transport one tonne of this product along one kilometer, to the amount of gas produced (in tonnes)." tooltip="A dictionary mapping the name of each greenhouse gas, produced to transport one tonne of this product along one kilometer, to the amount of gas produced."
keyPlaceholder="Emission name" keyPlaceholder="Emission name"
value={props.value["transportation emissions (J/km/tonne)"]} value={props.value["transportation emissions (tonne/km/tonne)"]}
onChange={v => onChange("transportation emissions (J/km/tonne)", v)} onChange={(v) =>
validate="float" onChange("transportation emissions (tonne/km/tonne)", v)
}
validate="floatList"
/> />
</Form> </Form>
</Card> </Card>
@ -170,43 +174,4 @@ const ProductBlock = (props) => {
); );
}; };
function exportToCsv(filename, rows) {
var processRow = function (row) {
var finalVal = "";
for (var j = 0; j < row.length; j++) {
var innerValue = row[j] === null ? "" : row[j].toString();
if (row[j] instanceof Date) {
innerValue = row[j].toLocaleString();
}
var result = innerValue.replace(/"/g, '""');
if (result.search(/("|,|\n)/g) >= 0) result = '"' + result + '"';
if (j > 0) finalVal += ",";
finalVal += result;
}
return finalVal + "\n";
};
var csvFile = "";
for (var i = 0; i < rows.length; i++) {
csvFile += processRow(rows[i]);
}
var blob = new Blob([csvFile], { type: "text/csv;charset=utf-8;" });
if (navigator.msSaveBlob) {
// IE 10+
navigator.msSaveBlob(blob, filename);
} else {
var link = document.createElement("a");
if (link.download !== undefined) {
var url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", filename);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}
}
export default ProductBlock; export default ProductBlock;

@ -1,4 +1,4 @@
import styles from './Section.module.css'; import styles from "./Section.module.css";
const Section = (props) => { const Section = (props) => {
return <h2 className={styles.Section}>{props.title}</h2>; return <h2 className={styles.Section}>{props.title}</h2>;

@ -1,27 +1,29 @@
import form_styles from './Form.module.css'; import form_styles from "./Form.module.css";
import Button from './Button'; import Button from "./Button";
import { validate } from './Form'; import { validate } from "./Form";
import React from "react";
const TextInputRow = (props) => { const TextInputRow = React.forwardRef((props, ref) => {
let unit = ""; let unit = "";
if (props.unit) { if (props.unit) {
unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>; unit = <span className={form_styles.FormRow_unit}>({props.unit})</span>;
} }
let tooltip = ""; let tooltip = "";
if (props.tooltip != undefined) { if (props.tooltip !== undefined) {
tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />; tooltip = <Button label="?" kind="inline" tooltip={props.tooltip} />;
} }
let isValid = true; let isValid = true;
if (props.validate !== undefined) { if (!props.disabled && props.validate !== undefined) {
isValid = validate(props.validate, props.value); isValid = validate(props.validate, props.value);
} }
let className = ""; let className = "";
if (!isValid) className = form_styles.invalid; if (!isValid) className = form_styles.invalid;
return <div className={form_styles.FormRow}> return (
<div className={form_styles.FormRow}>
<label> <label>
{props.label} {unit} {props.label} {unit}
</label> </label>
@ -31,10 +33,12 @@ const TextInputRow = (props) => {
disabled={props.disabled} disabled={props.disabled}
value={props.value} value={props.value}
className={className} className={className}
onChange={e => props.onChange(e.target.value)} onChange={(e) => props.onChange(e.target.value)}
ref={ref}
/> />
{tooltip} {tooltip}
</div>; </div>
}; );
});
export default TextInputRow; export default TextInputRow;

@ -0,0 +1,50 @@
import * as d3 from "d3";
export const csvParse = ({ contents, requiredCols }) => {
const data = d3.csvParse(contents, d3.autoType);
requiredCols.forEach((col) => {
if (!(col in data[0])) {
throw Error(`Column "${col}" not found in CSV file.`);
}
});
return data;
};
export const parseCsv = (contents, requiredCols = []) => {
const data = d3.csvParse(contents);
const T = data.columns.length - requiredCols.length;
let isValid = true;
for (let t = 0; t < T; t++) {
requiredCols.push(t + 1);
}
requiredCols.forEach((col) => {
if (!(col in data[0])) {
console.log(`Column "${col}" not found in CSV file.`);
isValid = false;
}
});
if (!isValid) return [undefined, undefined];
return [data, T];
};
export const extractNumericColumns = (obj, prefix) => {
const result = [];
for (let i = 1; `${prefix} ${i}` in obj; i++) {
result.push(obj[`${prefix} ${i}`]);
}
return result;
};
export const csvFormat = (data) => {
return d3.csvFormat(data);
};
export const generateFile = (filename, contents) => {
var link = document.createElement("a");
link.setAttribute("href", URL.createObjectURL(new Blob([contents])));
link.setAttribute("download", filename);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};

@ -0,0 +1,53 @@
import { csvParse, extractNumericColumns, csvFormat } from "./csv";
import { exportValue } from "./export";
test("parse CSV", () => {
const contents = "name,location,1,2,3\ntest,illinois,100,200,300";
const actual = csvParse({
contents: contents,
requiredCols: ["name", "location"],
});
expect(actual.length).toEqual(1);
expect(actual[0]).toEqual({
name: "test",
location: "illinois",
1: 100,
2: 200,
3: 300,
});
});
test("parse CSV with missing columns", () => {
const contents = "name,location,1,2,3\ntest,illinois,100,200,300";
expect(() =>
csvParse({
contents: contents,
requiredCols: ["name", "location", "latitude"],
})
).toThrow('Column "latitude" not found in CSV file.');
});
test("extract numeric columns from object", () => {
const obj1 = {
"amount 1": "hello",
"amount 2": "world",
"amount 4": "ignored",
};
const obj2 = { hello: "world" };
expect(extractNumericColumns(obj1, "amount")).toEqual(["hello", "world"]);
expect(extractNumericColumns(obj2, "amount")).toEqual([]);
});
test("generate CSV", () => {
const data = [
{ name: "alice", age: 20 },
{ name: "bob", age: null },
];
expect(csvFormat(data)).toEqual("name,age\nalice,20\nbob,");
});
test("export value", () => {
expect(exportValue("1")).toEqual(1);
expect(exportValue("[1,2,3]")).toEqual([1, 2, 3]);
expect(exportValue("qwe")).toEqual("qwe");
});

@ -0,0 +1,49 @@
export const defaultProduct = {
"initial amounts": {},
"disposal cost ($/tonne)": "0",
"disposal limit (tonne)": "0",
"disposal limit (%)": "",
"transportation cost ($/km/tonne)": "0",
"transportation energy (J/km/tonne)": "0",
"transportation emissions (tonne/km/tonne)": {},
x: 0,
y: 0,
};
export const defaultPlantLocation = {
"area cost factor": 1.0,
"latitude (deg)": 0,
"longitude (deg)": 0,
};
export const defaultPlant = {
locations: {},
"outputs (tonne/tonne)": {},
"disposal cost ($/tonne)": {},
"disposal limit (tonne)": {},
"emissions (tonne/tonne)": {},
storage: {
"cost ($/tonne)": 0,
"limit (tonne)": 0,
},
"maximum capacity (tonne)": 0,
"minimum capacity (tonne)": 0,
"opening cost (max capacity) ($)": 0,
"opening cost (min capacity) ($)": 0,
"fixed operating cost (max capacity) ($)": 0,
"fixed operating cost (min capacity) ($)": 0,
"variable operating cost ($/tonne)": 0,
"energy (GJ/tonne)": 0,
x: 0,
y: 0,
};
export const defaultData = {
parameters: {
"time horizon (years)": "1",
"building period (years)": "[1]",
// "annual inflation rate (%)": "0",
},
products: {},
plants: {},
};

@ -0,0 +1,464 @@
const isNumeric = (val) => {
return String(val).length > 0 && !isNaN(val);
};
const keysToList = (obj) => {
const result = [];
for (const key of Object.keys(obj)) {
result.push(key);
}
return result;
};
export const exportValue = (original, T) => {
if (isNumeric(original)) {
if (T) {
const v = parseFloat(original);
const result = [];
for (let i = 0; i < T; i++) result.push(v);
return result;
} else {
return parseFloat(original);
}
}
try {
const parsed = JSON.parse(original);
return parsed;
} catch {
// ignore
}
return original;
};
const exportValueDict = (original, T) => {
const result = {};
for (const [key, val] of Object.entries(original)) {
if (key.length === 0) continue;
result[key] = exportValue(val, T);
}
if (Object.keys(result).length > 0) {
return result;
} else {
return null;
}
};
const computeTotalInitialAmount = (prod) => {
let total = null;
for (const locDict of Object.values(prod["initial amounts"])) {
const locAmount = locDict["amount (tonne)"];
if (!total) total = [...locAmount];
else {
for (let i = 0; i < locAmount.length; i++) {
total[i] += locAmount[i];
}
}
}
return total;
};
export const importList = (args) => {
if (!args) return "";
if (Array.isArray(args) && args.length > 0) {
let isConstant = true;
for (let i = 1; i < args.length; i++) {
if (args[i - 1] !== args[i]) {
isConstant = false;
break;
}
}
if (isConstant) {
return String(args[0]);
} else {
return JSON.stringify(args);
}
} else {
return args;
}
};
export const importDict = (args) => {
if (!args) return {};
const result = {};
for (const [key, val] of Object.entries(args)) {
result[key] = importList(val);
}
return result;
};
const computeAbsDisposal = (prod) => {
const disposalPerc = prod["disposal limit (%)"];
const total = computeTotalInitialAmount(prod);
const disposalAbs = [];
for (let i = 0; i < total.length; i++) {
disposalAbs[i] = (total[i] * disposalPerc) / 100;
}
return disposalAbs;
};
export const exportProduct = (original, T) => {
const result = {};
// Copy time series values
result["initial amounts"] = original["initial amounts"];
[
"disposal cost ($/tonne)",
"disposal limit (tonne)",
"transportation cost ($/km/tonne)",
"transportation energy (J/km/tonne)",
].forEach((key) => {
const v = exportValue(original[key], T);
if (v.length > 0) result[key] = v;
});
// Copy dictionaries
["transportation emissions (tonne/km/tonne)"].forEach((key) => {
const v = exportValueDict(original[key], T);
if (v) result[key] = v;
});
// Transform percentage disposal limits into absolute
if (isNumeric(original["disposal limit (%)"])) {
result["disposal limit (tonne)"] = computeAbsDisposal(original);
}
return result;
};
export const exportPlant = (original, T) => {
const result = {};
// Copy scalar values
["input"].forEach((key) => {
result[key] = original[key];
});
// Copy time series values
["energy (GJ/tonne)"].forEach((key) => {
result[key] = exportValue(original[key], T);
});
// Copy scalar dicts
["outputs (tonne/tonne)"].forEach((key) => {
const v = exportValueDict(original[key]);
if (v) result[key] = v;
});
// Copy time series dicts
["emissions (tonne/tonne)"].forEach((key) => {
const v = exportValueDict(original[key], T);
if (v) result[key] = v;
});
const minCap = original["minimum capacity (tonne)"];
const maxCap = original["maximum capacity (tonne)"];
result.locations = {};
for (const [locName, origDict] of Object.entries(original["locations"])) {
const resDict = (result.locations[locName] = {});
const capDict = (resDict["capacities (tonne)"] = {});
const acf = origDict["area cost factor"];
const exportValueAcf = (obj, T) => {
const v = exportValue(obj, T);
if (Array.isArray(v)) {
return v.map((v) => v * acf);
}
return "";
};
// Copy scalar values
["latitude (deg)", "longitude (deg)"].forEach((key) => {
resDict[key] = origDict[key];
});
// Copy minimum capacity dict
capDict[minCap] = {};
for (const [resKeyName, origKeyName] of Object.entries({
"opening cost ($)": "opening cost (min capacity) ($)",
"fixed operating cost ($)": "fixed operating cost (min capacity) ($)",
"variable operating cost ($/tonne)": "variable operating cost ($/tonne)",
})) {
capDict[minCap][resKeyName] = exportValueAcf(original[origKeyName], T);
}
if (maxCap !== minCap) {
// Copy maximum capacity dict
capDict[maxCap] = {};
for (const [resKeyName, origKeyName] of Object.entries({
"opening cost ($)": "opening cost (max capacity) ($)",
"fixed operating cost ($)": "fixed operating cost (max capacity) ($)",
"variable operating cost ($/tonne)":
"variable operating cost ($/tonne)",
})) {
capDict[maxCap][resKeyName] = exportValueAcf(original[origKeyName], T);
}
}
// Copy disposal
resDict.disposal = {};
for (const [dispName, dispCost] of Object.entries(
original["disposal cost ($/tonne)"]
)) {
if (dispName.length === 0) continue;
const v = exportValueAcf(dispCost, T);
if (v) {
resDict.disposal[dispName] = { "cost ($/tonne)": v };
const limit = original["disposal limit (tonne)"][dispName];
if (isNumeric(limit)) {
resDict.disposal[dispName]["limit (tonne)"] = exportValue(limit, T);
}
}
}
// Copy storage
resDict.storage = {
"cost ($/tonne)": exportValueAcf(
original["storage"]["cost ($/tonne)"],
T
),
};
const storLimit = original["storage"]["limit (tonne)"];
if (isNumeric(storLimit)) {
resDict.storage["limit (tonne)"] = exportValue(storLimit);
}
}
return result;
};
export const exportData = (original) => {
const result = {
parameters: {},
products: {},
plants: {},
};
// Export parameters
["time horizon (years)", "building period (years)"].forEach((key) => {
result.parameters[key] = exportValue(original.parameters[key]);
});
// Read time horizon
let T = result.parameters["time horizon (years)"];
if (!isNumeric(T)) T = 1;
// Export products
for (const [prodName, prodDict] of Object.entries(original.products)) {
result.products[prodName] = exportProduct(prodDict, T);
}
// Export plants
for (const [plantName, plantDict] of Object.entries(original.plants)) {
result.plants[plantName] = exportPlant(plantDict, T);
}
return result;
};
const compressDisposalLimits = (original, result) => {
if (!("disposal limit (tonne)" in original)) {
return;
}
const total = computeTotalInitialAmount(original);
const limit = original["disposal limit (tonne)"];
let perc = Math.round((limit[0] / total[0]) * 1e6) / 1e6;
for (let i = 1; i < limit.length; i++) {
if (Math.abs(limit[i] / total[i] - perc) > 1e-5) {
return;
}
}
result["disposal limit (tonne)"] = "";
result["disposal limit (%)"] = String(perc * 100);
};
export const importProduct = (original) => {
const result = {};
result["initial amounts"] = { ...original["initial amounts"] };
// Initialize null values
["x", "y"].forEach((key) => {
result[key] = null;
});
// Initialize empty values
["disposal limit (%)"].forEach((key) => {
result[key] = "";
});
// Import lists
[
"transportation energy (J/km/tonne)",
"transportation cost ($/km/tonne)",
"disposal cost ($/tonne)",
"disposal limit (tonne)",
].forEach((key) => {
result[key] = importList(original[key]);
});
// Import dicts
["transportation emissions (tonne/km/tonne)"].forEach((key) => {
result[key] = importDict(original[key]);
});
// Attempt to convert absolute disposal limits to relative
compressDisposalLimits(original, result);
return result;
};
export const importPlant = (original) => {
const result = {};
// Initialize null values
["x", "y"].forEach((key) => {
result[key] = null;
});
// Import scalar values
["input"].forEach((key) => {
result[key] = original[key];
});
// Import timeseries values
["energy (GJ/tonne)"].forEach((key) => {
result[key] = importList(original[key]);
});
// Import dicts
["outputs (tonne/tonne)", "emissions (tonne/tonne)"].forEach((key) => {
result[key] = importDict(original[key]);
});
// Read locations
let costsInitialized = false;
const resLocDict = (result.locations = {});
for (const [locName, origLocDict] of Object.entries(original["locations"])) {
resLocDict[locName] = {};
// Import latitude and longitude
["latitude (deg)", "longitude (deg)"].forEach((key) => {
resLocDict[locName][key] = origLocDict[key];
});
const capacities = keysToList(origLocDict["capacities (tonne)"]);
const last = capacities.length - 1;
const minCap = capacities[0];
const maxCap = capacities[last];
const minCapDict = origLocDict["capacities (tonne)"][minCap];
const maxCapDict = origLocDict["capacities (tonne)"][maxCap];
// Import min/max capacity
if ("minimum capacity (tonne)" in result) {
if (
result["minimum capacity (tonne)"] !== minCap ||
result["maximum capacity (tonne)"] !== maxCap
) {
throw "Data loss";
}
} else {
result["minimum capacity (tonne)"] = minCap;
result["maximum capacity (tonne)"] = maxCap;
}
// Compute area cost factor
let acf = 1;
if (costsInitialized) {
acf = result["opening cost (min capacity) ($)"];
if (Array.isArray(acf)) acf = acf[0];
acf = minCapDict["opening cost ($)"][0] / acf;
}
resLocDict[locName]["area cost factor"] = acf;
// Read adjusted costs
const importListAcf = (obj) => importList(obj.map((v) => v / acf));
const openCostMax = importListAcf(maxCapDict["opening cost ($)"]);
const openCostMin = importListAcf(minCapDict["opening cost ($)"]);
const fixCostMax = importListAcf(maxCapDict["fixed operating cost ($)"]);
const fixCostMin = importListAcf(minCapDict["fixed operating cost ($)"]);
const storCost = importListAcf(origLocDict.storage["cost ($/tonne)"]);
const storLimit = String(origLocDict.storage["limit (tonne)"]);
const varCost = importListAcf(
minCapDict["variable operating cost ($/tonne)"]
);
const dispCost = {};
const dispLimit = {};
for (const prodName of Object.keys(original["outputs (tonne/tonne)"])) {
dispCost[prodName] = "";
dispLimit[prodName] = "";
if (prodName in origLocDict["disposal"]) {
const prodDict = origLocDict["disposal"][prodName];
dispCost[prodName] = importListAcf(prodDict["cost ($/tonne)"]);
if ("limit (tonne)" in prodDict)
dispLimit[prodName] = importList(prodDict["limit (tonne)"]);
}
}
const check = (left, right) => {
let valid = true;
if (isNumeric(left) && isNumeric(right)) {
valid = Math.abs(left - right) < 1.0;
} else {
valid = left === right;
}
if (!valid)
console.warn(`Data loss detected: ${locName}, ${left} != ${right}`);
};
if (costsInitialized) {
// Verify that location costs match the previously initialized ones
check(result["opening cost (max capacity) ($)"], openCostMax);
check(result["opening cost (min capacity) ($)"], openCostMin);
check(result["fixed operating cost (max capacity) ($)"], fixCostMax);
check(result["fixed operating cost (min capacity) ($)"], fixCostMin);
check(result["variable operating cost ($/tonne)"], varCost);
check(result["storage"]["cost ($/tonne)"], storCost);
check(result["storage"]["limit (tonne)"], storLimit);
check(String(result["disposal cost ($/tonne)"]), String(dispCost));
check(String(result["disposal limit (tonne)"]), String(dispLimit));
} else {
// Initialize plant costs
costsInitialized = true;
result["opening cost (max capacity) ($)"] = openCostMax;
result["opening cost (min capacity) ($)"] = openCostMin;
result["fixed operating cost (max capacity) ($)"] = fixCostMax;
result["fixed operating cost (min capacity) ($)"] = fixCostMin;
result["variable operating cost ($/tonne)"] = varCost;
result["storage"] = {};
result["storage"]["cost ($/tonne)"] = storCost;
result["storage"]["limit (tonne)"] = storLimit;
result["disposal cost ($/tonne)"] = dispCost;
result["disposal limit (tonne)"] = dispLimit;
}
}
return result;
};
export const importData = (original) => {
["parameters", "plants", "products"].forEach((key) => {
if (!(key in original)) {
throw "File not recognized.";
}
});
const result = {};
result.parameters = importDict(original.parameters);
// Import products
result.products = {};
for (const [prodName, origProdDict] of Object.entries(original.products)) {
result.products[prodName] = importProduct(origProdDict);
}
// Import plants
result.plants = {};
for (const [plantName, origPlantDict] of Object.entries(original.plants)) {
result.plants[plantName] = importPlant(origPlantDict);
}
return result;
};

@ -0,0 +1,564 @@
import {
exportProduct,
exportPlant,
importProduct,
importList,
importDict,
importPlant,
} from "./export";
const sampleProductsOriginal = [
// basic product
{
"initial amounts": {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"amount (tonne)": [100, 200, 300],
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"amount (tonne)": [100, 200, 300],
},
"Park County": {
"latitude (deg)": 44.4063,
"longitude (deg)": -109.4153,
"amount (tonne)": [100, 200, 300],
},
},
"disposal cost ($/tonne)": "50",
"disposal limit (tonne)": "30",
"disposal limit (%)": "",
"transportation cost ($/km/tonne)": "5",
"transportation energy (J/km/tonne)": "10",
"transportation emissions (tonne/km/tonne)": {
CO2: "0.5",
},
x: null,
y: null,
},
// product with percentage disposal limit
{
"initial amounts": {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"amount (tonne)": [100, 200, 300],
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"amount (tonne)": [100, 200, 300],
},
"Park County": {
"latitude (deg)": 44.4063,
"longitude (deg)": -109.4153,
"amount (tonne)": [100, 200, 300],
},
},
"disposal cost ($/tonne)": "50",
"disposal limit (tonne)": "",
"disposal limit (%)": "10",
"transportation cost ($/km/tonne)": "5",
"transportation energy (J/km/tonne)": "10",
"transportation emissions (tonne/km/tonne)": {
CO2: "0.5",
},
x: null,
y: null,
},
// product using defaults
{
"initial amounts": {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"amount (tonne)": [100, 200, 300],
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"amount (tonne)": [100, 200, 300],
},
"Park County": {
"latitude (deg)": 44.4063,
"longitude (deg)": -109.4153,
"amount (tonne)": [100, 200, 300],
},
},
"disposal cost ($/tonne)": "50",
"disposal limit (tonne)": "",
"disposal limit (%)": "",
"transportation cost ($/km/tonne)": "5",
"transportation energy (J/km/tonne)": "",
"transportation emissions (tonne/km/tonne)": {},
x: null,
y: null,
},
];
const sampleProductsExported = [
// basic product
{
"initial amounts": {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"amount (tonne)": [100, 200, 300],
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"amount (tonne)": [100, 200, 300],
},
"Park County": {
"latitude (deg)": 44.4063,
"longitude (deg)": -109.4153,
"amount (tonne)": [100, 200, 300],
},
},
"disposal cost ($/tonne)": [50, 50, 50],
"disposal limit (tonne)": [30, 30, 30],
"transportation cost ($/km/tonne)": [5, 5, 5],
"transportation energy (J/km/tonne)": [10, 10, 10],
"transportation emissions (tonne/km/tonne)": {
CO2: [0.5, 0.5, 0.5],
},
},
// product with percentage disposal limit
{
"initial amounts": {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"amount (tonne)": [100, 200, 300],
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"amount (tonne)": [100, 200, 300],
},
"Park County": {
"latitude (deg)": 44.4063,
"longitude (deg)": -109.4153,
"amount (tonne)": [100, 200, 300],
},
},
"disposal cost ($/tonne)": [50, 50, 50],
"disposal limit (tonne)": [30, 60, 90],
"transportation cost ($/km/tonne)": [5, 5, 5],
"transportation energy (J/km/tonne)": [10, 10, 10],
"transportation emissions (tonne/km/tonne)": {
CO2: [0.5, 0.5, 0.5],
},
},
// product using defaults
{
"initial amounts": {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"amount (tonne)": [100, 200, 300],
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"amount (tonne)": [100, 200, 300],
},
"Park County": {
"latitude (deg)": 44.4063,
"longitude (deg)": -109.4153,
"amount (tonne)": [100, 200, 300],
},
},
"disposal cost ($/tonne)": [50, 50, 50],
"transportation cost ($/km/tonne)": [5, 5, 5],
},
];
const samplePlantsOriginal = [
// basic plant
{
input: "Baled agricultural biomass",
"outputs (tonne/tonne)": {
"Hydrogen gas": 0.095,
"Carbon dioxide": 1.164,
Tar: 0.06,
},
"energy (GJ/tonne)": "50",
locations: {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"area cost factor": 1.0,
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"area cost factor": 0.5,
},
},
"disposal cost ($/tonne)": {
"Hydrogen gas": "0",
"Carbon dioxide": "0",
Tar: "200",
},
"disposal limit (tonne)": {
"Hydrogen gas": "10",
"Carbon dioxide": "",
Tar: "",
},
"emissions (tonne/tonne)": {
CO2: "100",
},
storage: {
"cost ($/tonne)": "5",
"limit (tonne)": "10000",
},
"maximum capacity (tonne)": "730000",
"minimum capacity (tonne)": "182500",
"opening cost (max capacity) ($)": "300000",
"opening cost (min capacity) ($)": "200000",
"fixed operating cost (max capacity) ($)": "7000",
"fixed operating cost (min capacity) ($)": "5000",
"variable operating cost ($/tonne)": "10",
x: null,
y: null,
},
// plant with fixed capacity
{
input: "Baled agricultural biomass",
"outputs (tonne/tonne)": {
"Hydrogen gas": 0.095,
"Carbon dioxide": 1.164,
Tar: 0.06,
},
"energy (GJ/tonne)": "50",
locations: {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"area cost factor": 1.0,
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"area cost factor": 0.5,
},
},
"disposal cost ($/tonne)": {
"Hydrogen gas": "0",
"Carbon dioxide": "0",
Tar: "200",
},
"disposal limit (tonne)": {
"Hydrogen gas": "10",
"Carbon dioxide": "",
Tar: "",
},
"emissions (tonne/tonne)": {
CO2: "100",
},
storage: {
"cost ($/tonne)": "5",
"limit (tonne)": "10000",
},
"maximum capacity (tonne)": "182500",
"minimum capacity (tonne)": "182500",
"opening cost (max capacity) ($)": "200000",
"opening cost (min capacity) ($)": "200000",
"fixed operating cost (max capacity) ($)": "5000",
"fixed operating cost (min capacity) ($)": "5000",
"variable operating cost ($/tonne)": "10",
x: null,
y: null,
},
// plant with defaults
{
input: "Baled agricultural biomass",
"outputs (tonne/tonne)": {
"Hydrogen gas": 0.095,
"Carbon dioxide": 1.164,
Tar: 0.06,
},
"energy (GJ/tonne)": "50",
locations: {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
"area cost factor": 1.0,
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
"area cost factor": 0.5,
},
},
"disposal cost ($/tonne)": {
"Hydrogen gas": "",
"Carbon dioxide": "",
Tar: "",
},
"disposal limit (tonne)": {
"Hydrogen gas": "",
"Carbon dioxide": "",
Tar: "",
},
"emissions (tonne/tonne)": {
CO2: "100",
},
storage: {
"cost ($/tonne)": "5",
"limit (tonne)": "10000",
},
"maximum capacity (tonne)": "730000",
"minimum capacity (tonne)": "182500",
"opening cost (max capacity) ($)": "300000",
"opening cost (min capacity) ($)": "200000",
"fixed operating cost (max capacity) ($)": "7000",
"fixed operating cost (min capacity) ($)": "5000",
"variable operating cost ($/tonne)": "10",
x: null,
y: null,
},
];
const samplePlantsExported = [
//basic plant
{
input: "Baled agricultural biomass",
"outputs (tonne/tonne)": {
"Hydrogen gas": 0.095,
"Carbon dioxide": 1.164,
Tar: 0.06,
},
"energy (GJ/tonne)": [50, 50, 50],
locations: {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
disposal: {
"Hydrogen gas": {
"cost ($/tonne)": [0, 0, 0],
"limit (tonne)": [10, 10, 10],
},
"Carbon dioxide": {
"cost ($/tonne)": [0, 0, 0],
},
Tar: {
"cost ($/tonne)": [200.0, 200.0, 200.0],
},
},
storage: {
"cost ($/tonne)": [5, 5, 5],
"limit (tonne)": 10000,
},
"capacities (tonne)": {
182500: {
"opening cost ($)": [200000, 200000, 200000],
"fixed operating cost ($)": [5000, 5000, 5000],
"variable operating cost ($/tonne)": [10, 10, 10],
},
730000: {
"opening cost ($)": [300000, 300000, 300000],
"fixed operating cost ($)": [7000, 7000, 7000],
"variable operating cost ($/tonne)": [10, 10, 10],
},
},
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
disposal: {
"Hydrogen gas": {
"cost ($/tonne)": [0, 0, 0],
"limit (tonne)": [10, 10, 10],
},
"Carbon dioxide": {
"cost ($/tonne)": [0, 0, 0],
},
Tar: {
"cost ($/tonne)": [100.0, 100.0, 100.0],
},
},
storage: {
"cost ($/tonne)": [2.5, 2.5, 2.5],
"limit (tonne)": 10000,
},
"capacities (tonne)": {
182500: {
"opening cost ($)": [100000, 100000, 100000],
"fixed operating cost ($)": [2500, 2500, 2500],
"variable operating cost ($/tonne)": [5, 5, 5],
},
730000: {
"opening cost ($)": [150000, 150000, 150000],
"fixed operating cost ($)": [3500, 3500, 3500],
"variable operating cost ($/tonne)": [5, 5, 5],
},
},
},
},
"emissions (tonne/tonne)": {
CO2: [100, 100, 100],
},
},
// plant with fixed capacity
{
input: "Baled agricultural biomass",
"outputs (tonne/tonne)": {
"Hydrogen gas": 0.095,
"Carbon dioxide": 1.164,
Tar: 0.06,
},
"energy (GJ/tonne)": [50, 50, 50],
locations: {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
disposal: {
"Hydrogen gas": {
"cost ($/tonne)": [0, 0, 0],
"limit (tonne)": [10, 10, 10],
},
"Carbon dioxide": {
"cost ($/tonne)": [0, 0, 0],
},
Tar: {
"cost ($/tonne)": [200.0, 200.0, 200.0],
},
},
storage: {
"cost ($/tonne)": [5, 5, 5],
"limit (tonne)": 10000,
},
"capacities (tonne)": {
182500: {
"opening cost ($)": [200000, 200000, 200000],
"fixed operating cost ($)": [5000, 5000, 5000],
"variable operating cost ($/tonne)": [10, 10, 10],
},
},
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
disposal: {
"Hydrogen gas": {
"cost ($/tonne)": [0, 0, 0],
"limit (tonne)": [10, 10, 10],
},
"Carbon dioxide": {
"cost ($/tonne)": [0, 0, 0],
},
Tar: {
"cost ($/tonne)": [100.0, 100.0, 100.0],
},
},
storage: {
"cost ($/tonne)": [2.5, 2.5, 2.5],
"limit (tonne)": 10000,
},
"capacities (tonne)": {
182500: {
"opening cost ($)": [100000, 100000, 100000],
"fixed operating cost ($)": [2500, 2500, 2500],
"variable operating cost ($/tonne)": [5, 5, 5],
},
},
},
},
"emissions (tonne/tonne)": {
CO2: [100, 100, 100],
},
},
// plant with defaults
{
input: "Baled agricultural biomass",
"outputs (tonne/tonne)": {
"Hydrogen gas": 0.095,
"Carbon dioxide": 1.164,
Tar: 0.06,
},
"energy (GJ/tonne)": [50, 50, 50],
locations: {
"Washakie County": {
"latitude (deg)": 43.8356,
"longitude (deg)": -107.6602,
disposal: {},
storage: {
"cost ($/tonne)": [5, 5, 5],
"limit (tonne)": 10000,
},
"capacities (tonne)": {
182500: {
"opening cost ($)": [200000, 200000, 200000],
"fixed operating cost ($)": [5000, 5000, 5000],
"variable operating cost ($/tonne)": [10, 10, 10],
},
730000: {
"opening cost ($)": [300000, 300000, 300000],
"fixed operating cost ($)": [7000, 7000, 7000],
"variable operating cost ($/tonne)": [10, 10, 10],
},
},
},
"Platte County": {
"latitude (deg)": 42.1314,
"longitude (deg)": -104.9676,
disposal: {},
storage: {
"cost ($/tonne)": [2.5, 2.5, 2.5],
"limit (tonne)": 10000,
},
"capacities (tonne)": {
182500: {
"opening cost ($)": [100000, 100000, 100000],
"fixed operating cost ($)": [2500, 2500, 2500],
"variable operating cost ($/tonne)": [5, 5, 5],
},
730000: {
"opening cost ($)": [150000, 150000, 150000],
"fixed operating cost ($)": [3500, 3500, 3500],
"variable operating cost ($/tonne)": [5, 5, 5],
},
},
},
},
"emissions (tonne/tonne)": {
CO2: [100, 100, 100],
},
},
];
test("export products", () => {
for (let i = 0; i < sampleProductsOriginal.length; i++) {
const original = sampleProductsOriginal[i];
const exported = sampleProductsExported[i];
expect(exportProduct(original, 3)).toEqual(exported);
expect(importProduct(exported)).toEqual(original);
}
});
test("export plants", () => {
for (let i = 0; i < samplePlantsOriginal.length; i++) {
const original = samplePlantsOriginal[i];
const exported = samplePlantsExported[i];
expect(exportPlant(original, 3)).toEqual(exported);
expect(importPlant(exported)).toEqual(original);
}
});
test("importList", () => {
expect(importList("invalid")).toEqual("invalid");
expect(importList([1, 1, 1])).toEqual("1");
expect(importList([1, 2, 3])).toEqual("[1,2,3]");
expect(importList(["A", "A", "A"])).toEqual("A");
});
test("importDict", () => {
expect(importDict({ a: [5, 5, 5] })).toEqual({ a: "5" });
expect(importDict({ a: [1, 2, 3] })).toEqual({ a: "[1,2,3]" });
expect(importDict({ a: "invalid" })).toEqual({ a: "invalid" });
});

@ -5,7 +5,8 @@
--border-radius: 4px; --border-radius: 4px;
} }
html, body { html,
body {
margin: 0; margin: 0;
padding: 0; padding: 0;
border: 0; border: 0;
@ -29,12 +30,11 @@ body {
margin-top: -1px !important; margin-top: -1px !important;
margin-left: -1px !important; margin-left: -1px !important;
border-radius: 8px !important; border-radius: 8px !important;
font-weight: bold;
} }
.react-flow__handle { .react-flow__handle {
width: 8px !important; width: 6px !important;
height: 8px !important; height: 6px !important;
background-color: white !important; background-color: white !important;
border: 1px solid black !important; border: 1px solid black !important;
} }
@ -44,9 +44,9 @@ body {
} }
.react-flow__handle-right { .react-flow__handle-right {
right: -5px !important; right: -4px !important;
} }
.react-flow__handle-left { .react-flow__handle-left {
left: -5px !important; left: -4px !important;
} }

@ -1,17 +1,11 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import './index.css'; import "./index.css";
import Header from './Header'; import InputPage from "./InputPage";
import InputPage from './InputPage';
import Footer from './Footer';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<Header />
<div id="content">
<InputPage /> <InputPage />
</div>
<Footer />
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById("root")
); );

Loading…
Cancel
Save