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",
@ -37,4 +44,4 @@
"last 1 safari version" "last 1 safari version"
] ]
} }
} }

@ -1,22 +1,22 @@
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;
if (props.kind === "inline") { if (props.kind === "inline") {
className += " " + styles.inline; className += " " + styles.inline;
} }
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>;
} }
return ( return (
<button className={className} onClick={props.onClick}> <button className={className} onClick={props.onClick}>
{tooltip} {tooltip}
{props.label} {props.label}
</button> </button>
); );
}; };
export default Button; export default Button;

@ -1,35 +1,32 @@
.Button { .Button {
padding: 6px 36px; padding: 6px 36px;
margin: 12px 6px; margin: 12px 6px;
line-height: 24px; line-height: 24px;
border: var(--box-border); border: var(--box-border);
/* background-color: white; */ /* background-color: white; */
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
border-radius: var(--border-radius); border-radius: var(--border-radius);
cursor: pointer; cursor: pointer;
color: rgba(0, 0, 0, 0.8); color: rgba(0, 0, 0, 0.8);
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 {
background: rgb(245, 245, 245); background: rgb(245, 245, 245);
} }
.Button:active { .Button:active {
background: rgba(220, 220, 220); background: rgba(220, 220, 220);
} }
.inline { .inline {
padding: 0 12px; padding: 0 12px;
margin: 2px 4px 2px 0; margin: 2px 4px 2px 0;
height: 32px; height: 32px;
font-size: 11px; font-size: 11px;
} }
/* .inline:last-child { /* .inline:last-child {
@ -37,29 +34,29 @@
} */ } */
.tooltip { .tooltip {
visibility: hidden; visibility: hidden;
background-color: #333; background-color: #333;
color: white; color: white;
opacity: 0%; opacity: 0%;
width: 180px; width: 180px;
margin-top: 36px; margin-top: 36px;
margin-left: -180px; margin-left: -180px;
position: absolute; position: absolute;
z-index: 100; z-index: 100;
text-transform: none; text-transform: none;
font-size: 13px; font-size: 13px;
border-radius: 4px; border-radius: 4px;
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;
} }
.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,22 +1,22 @@
.Card { .Card {
border: var(--box-border); border: var(--box-border);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: white; background-color: white;
padding: 12px; padding: 12px;
min-height: 24px; min-height: 24px;
} }
.Card h1 { .Card h1 {
margin: 12px -12px 0px -12px; margin: 12px -12px 0px -12px;
padding: 6px 12px 0px 12px; padding: 6px 12px 0px 12px;
font-size: 14px; font-size: 14px;
line-height: 35px; line-height: 35px;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }
.Card h1:first-child { .Card h1:first-child {
margin: -12px -12px 0px -12px; margin: -12px -12px 0px -12px;
border-top: none; border-top: none;
background: none; background: none;
} }

@ -1,87 +1,91 @@
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 };
if (!props.disableKeys) { if (!props.disableKeys) {
dict[""] = "0"; dict[""] = "0";
} }
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} />;
} }
const onChangeValue = (key, v) => { const onChangeValue = (key, v) => {
const newDict = { ...dict }; const newDict = { ...dict };
newDict[key] = v; newDict[key] = v;
props.onChange(newDict); props.onChange(newDict);
}; };
const onChangeKey = (prevKey, newKey) => { const onChangeKey = (prevKey, newKey) => {
const newDict = renameKey(dict, prevKey, newKey); const newDict = renameKey(dict, prevKey, newKey);
if (!("" in newDict)) newDict[""] = ""; if (!("" in newDict)) newDict[""] = "";
props.onChange(newDict); props.onChange(newDict);
}; };
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 = (
if (index > 0) { <span>
label = ""; {props.label} {unit}
} </span>
);
if (index > 0) {
label = "";
}
let isValid = true; let isValid = true;
if (props.validate !== undefined) { if (props.validate !== undefined) {
isValid = validate(props.validate, dict[key]); isValid = validate(props.validate, dict[key]);
} }
let className = ""; let className = "";
if (!isValid) className = form_styles.invalid; if (!isValid) className = form_styles.invalid;
form.push( form.push(
<div className={form_styles.FormRow} key={index}> <div className={form_styles.FormRow} key={index}>
<label>{label}</label> <label>{label}</label>
<input <input
type="text" type="text"
data-index={index} data-index={index}
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"
data-index={index} data-index={index}
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>
); );
}); });
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) {
acc[newKey] = obj[prevKey]; acc[newKey] = obj[prevKey];
} else { } else {
acc[val] = obj[val]; acc[val] = obj[val];
} }
return acc; return acc;
}, {}); }, {});
} }
export default DictInputRow; export default DictInputRow;

@ -1,64 +1,49 @@
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} />;
} }
const fileElem = useRef(); const fileElem = useRef();
const onClickUpload = () => { const onClickUpload = () => {
fileElem.current.click(); fileElem.current.click();
}; };
const onFileSelected = () => { const onFileSelected = () => {
const file = fileElem.current.files[0]; const file = fileElem.current.files[0];
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader();
reader.addEventListener("load", () => { reader.addEventListener("load", () => {
props.onFile(reader.result); props.onFile(reader.result);
}); });
reader.readAsText(file); reader.readAsText(file);
} }
fileElem.current.value = ""; fileElem.current.value = "";
}; };
return <div className={form_styles.FormRow}> return (
<label>{props.label}</label> <div className={form_styles.FormRow}>
<input type="text" value={props.value} disabled="disabled" /> <label>{props.label}</label>
<Button <input type="text" value={props.value} disabled="disabled" />
label="Upload" <Button label="Upload" kind="inline" onClick={onClickUpload} />
kind="inline" <Button label="Download" kind="inline" onClick={props.onDownload} />
onClick={onClickUpload} <Button label="Clear" kind="inline" onClick={props.onClear} />
/> <Button label="Template" kind="inline" onClick={props.onTemplate} />
<Button {tooltip}
label="Download" <input
kind="inline" type="file"
onClick={props.onDownload} ref={fileElem}
/> accept={props.accept}
<Button style={{ display: "none" }}
label="Clear" onChange={onFileSelected}
kind="inline" />
onClick={props.onClear} </div>
/> );
<Button
label="Template"
kind="inline"
onClick={props.onTemplate}
/>
{tooltip}
<input
type="file"
ref={fileElem}
accept={props.accept}
style={{ display: "none" }}
onChange={onFileSelected}
/>
</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 (
<p>RELOG: Reverse Logistics Optimization</p> <div className={styles.Footer}>
<p>Copyright &copy; 2020&mdash;2022, UChicago Argonne, LLC. All Rights Reserved.</p> <p>RELOG: Reverse Logistics Optimization</p>
</div>; <p>
Copyright &copy; 2020&mdash;2022, UChicago Argonne, LLC. All Rights
Reserved.
</p>
</div>
);
}; };
export default Footer; export default Footer;

@ -1,11 +1,10 @@
.Footer { .Footer {
background-color: rgba(0, 0, 0, 0.8); background-color: rgba(0, 0, 0, 0.8);
padding: 24px; padding: 24px;
margin-top: 24px; margin-top: 24px;
color: rgba(255, 255, 255, 0.5); color: rgba(255, 255, 255, 0.5);
text-align: center; text-align: center;
font-size: 14px; font-size: 14px;
line-height: 8px; line-height: 8px;
min-width: 900px; min-width: 900px;
}
}

@ -1,19 +1,19 @@
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; }
} return true;
return true;
}; };
const Form = (props) => { const Form = (props) => {
return <>{props.children}</>; return <>{props.children}</>;
}; };
export default Form; export default Form;

@ -1,28 +1,28 @@
.FormRow { .FormRow {
display: flex; display: flex;
line-height: 24px; line-height: 24px;
} }
.FormRow label { .FormRow label {
width: 350px; width: 350px;
padding: 6px 12px; padding: 6px 12px;
text-align: right; text-align: right;
} }
.FormRow input { .FormRow input {
flex: 1; flex: 1;
font-family: monospace; font-family: monospace;
border: var(--box-border); border: var(--box-border);
border-radius: var(--border-radius); border-radius: var(--border-radius);
padding: 4px; padding: 4px;
margin: 2px 3px; margin: 2px 3px;
} }
.FormRow_unit { .FormRow_unit {
color: rgba(0, 0, 0, 0.4); color: rgba(0, 0, 0, 0.4);
} }
.invalid { .invalid {
border: 2px solid #faa !important; border: 2px solid #faa !important;
background-color: rgba(255, 0, 0, 0.05); background-color: rgba(255, 0, 0, 0.05);
} }

@ -1,13 +1,17 @@
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>
</div> <h2>{props.title}</h2>
<div style={{ float: "right", paddingTop: "5px" }}>
{props.children}
</div> </div>
); </div>
</div>
);
}; };
export default Header; export default Header;

@ -1,19 +1,28 @@
.HeaderBox { .HeaderBox {
background-color: white; background-color: white;
border-bottom: var(--box-border); border-bottom: var(--box-border);
box-shadow: var(--box-shadow); box-shadow: var(--box-shadow);
padding: 0; padding: 0;
margin: 0; margin: 0;
} }
.HeaderContent { .HeaderContent {
margin: 0 auto; margin: 0 auto;
max-width: var(--site-width); max-width: var(--site-width);
} }
.HeaderContent h1 { .HeaderContent h1,
line-height: 48px; .HeaderContent h2 {
font-size: 28px; line-height: 48px;
padding: 12px; font-size: 28px;
margin: 0; padding: 12px;
} margin: 0;
display: inline-block;
vertical-align: middle;
}
.HeaderContent h2 {
font-size: 22px;
font-weight: normal;
color: rgba(0, 0, 0, 0.6);
}

@ -1,254 +1,332 @@
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();
// onAdd let savedData = JSON.parse(localStorage.getItem("data"));
// ------------------------------------------------------------------------ if (!savedData) savedData = defaultData;
const promptName = (prevData) => {
const name = prompt("Name"); let [data, setData] = useState(savedData);
if (!name || name.length == 0) return;
if (name in prevData.products || name in prevData.plants) return; const save = (data) => {
return name; localStorage.setItem("data", JSON.stringify(data));
}; };
const onAddPlant = () => { const promptName = (prevData) => {
setData((prevData) => { const name = prompt("Name");
const name = promptName(prevData); if (!name || name.length === 0) return;
if (name === undefined) return prevData; if (name in prevData.products || name in prevData.plants) return;
const newData = { ...prevData }; return name;
newData.plants[name] = { };
x: randomPosition(),
y: randomPosition(), const onAddPlant = () => {
outputs: {}, setData((prevData) => {
}; const name = promptName(prevData);
return newData; if (name === undefined) return prevData;
}); const newData = { ...prevData };
}; const [x, y] = randomPosition();
newData.plants[name] = {
const onAddProduct = () => { ...defaultPlant,
setData((prevData) => { x: x,
const name = promptName(prevData); y: y,
if (name === undefined) return prevData; };
const newData = { ...prevData }; save(newData);
newData.products[name] = { return newData;
...defaultProduct, });
x: randomPosition(), };
y: randomPosition(),
}; const onAddProduct = () => {
return newData; setData((prevData) => {
}); const name = promptName(prevData);
}; if (name === undefined) return prevData;
const newData = { ...prevData };
// onRename const [x, y] = randomPosition();
// ------------------------------------------------------------------------ console.log(x, y);
const onRenamePlant = (prevName, newName) => { newData.products[name] = {
setData((prevData) => { ...defaultProduct,
const newData = { ...prevData }; x: x,
newData.plants[newName] = newData.plants[prevName]; y: y,
delete newData.plants[prevName]; };
return newData; save(newData);
}); return newData;
}; });
};
const onRenameProduct = (prevName, newName) => {
setData((prevData) => { const onRenamePlant = (prevName, newName) => {
const newData = { ...prevData }; setData((prevData) => {
newData.products[newName] = newData.products[prevName]; const newData = { ...prevData };
delete newData.products[prevName]; newData.plants[newName] = newData.plants[prevName];
for (const [plantName, plant] of Object.entries(newData.plants)) { delete newData.plants[prevName];
if (plant.input == prevName) { save(newData);
plant.input = newName; return newData;
} });
let outputFound = false; };
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
if (outputName == prevName) outputFound = true;
}
if (outputFound) {
plant.outputs[newName] = plant.outputs[prevName];
delete plant.outputs[prevName];
}
}
return newData;
});
};
// onMove
// ------------------------------------------------------------------------
const onMovePlant = (plantName, x, y) => {
setData((prevData) => {
const newData = { ...prevData };
newData.plants[plantName].x = x;
newData.plants[plantName].y = y;
return newData;
});
};
const onMoveProduct = (productName, x, y) => {
setData((prevData) => {
const newData = { ...prevData };
newData.products[productName].x = x;
newData.products[productName].y = y;
return newData;
});
};
// onRemove
// ------------------------------------------------------------------------
const onRemovePlant = (plantName) => {
setData((prevData) => {
const newData = { ...prevData };
delete newData.plants[plantName];
return newData;
});
};
const onRemoveProduct = (productName) => {
setData((prevData) => {
const newData = { ...prevData };
delete newData.products[productName];
for (const [plantName, plant] of Object.entries(newData.plants)) {
if (plant.input == productName) {
delete plant.input;
}
let outputFound = false;
for (const [outputName, outputValue] of Object.entries(plant.outputs)) {
if (outputName == productName) outputFound = true;
}
if (outputFound) {
delete plant.outputs[productName];
}
}
return newData;
});
};
// Inputs & Outputs
// ------------------------------------------------------------------------
const onSetPlantInput = (plantName, productName) => {
setData((prevData) => {
const newData = { ...prevData };
newData.plants[plantName].input = productName;
return newData;
});
};
const onAddPlantOutput = (plantName, productName) => {
setData((prevData) => {
if (productName in prevData.plants[plantName].outputs) {
return prevData;
}
const newData = { ...prevData };
newData.plants[plantName].outputs[productName] = 0;
return newData;
});
};
// onSave
// ------------------------------------------------------------------------
const onSave = () => {
console.log(data);
};
// onChange
// ------------------------------------------------------------------------
const onChangeParameters = (val) => {
setData(prevData => {
const newData = { ...prevData };
newData.parameters = val;
return newData;
});
};
const onChangeProduct = (prodName, val) => {
setData(prevData => {
const newData = { ...prevData };
newData.products[prodName] = val;
return newData;
});
};
// ------------------------------------------------------------------------
let productComps = [];
for (const [prodName, prod] of Object.entries(data.products)) {
productComps.push(
<ProductBlock
key={prodName}
name={prodName}
value={prod}
onChange={v => onChangeProduct(prodName, v)}
/>
);
}
let plantComps = []; const onRenameProduct = (prevName, newName) => {
for (const [plantName, plant] of Object.entries(data.plants)) { setData((prevData) => {
plantComps.push( const newData = { ...prevData };
<PlantBlock key={plantName} name={plantName} /> newData.products[newName] = newData.products[prevName];
); delete newData.products[prevName];
for (const [, plant] of Object.entries(newData.plants)) {
if (plant.input === prevName) {
plant.input = newName;
}
let outputFound = false;
for (const [outputName] of Object.entries(
plant["outputs (tonne/tonne)"]
)) {
if (outputName === prevName) outputFound = true;
}
if (outputFound) {
plant["outputs (tonne/tonne)"][newName] =
plant["outputs (tonne/tonne)"][prevName];
delete plant["outputs (tonne/tonne)"][prevName];
}
}
save(newData);
return newData;
});
};
const onMovePlant = (plantName, x, y) => {
setData((prevData) => {
const newData = { ...prevData };
newData.plants[plantName].x = x;
newData.plants[plantName].y = y;
save(newData);
return newData;
});
};
const onMoveProduct = (productName, x, y) => {
setData((prevData) => {
const newData = { ...prevData };
newData.products[productName].x = x;
newData.products[productName].y = y;
save(newData);
return newData;
});
};
const onRemovePlant = (plantName) => {
setData((prevData) => {
const newData = { ...prevData };
delete newData.plants[plantName];
save(newData);
return newData;
});
};
const onRemoveProduct = (productName) => {
setData((prevData) => {
const newData = { ...prevData };
delete newData.products[productName];
for (const [, plant] of Object.entries(newData.plants)) {
if (plant.input === productName) {
delete plant.input;
}
let outputFound = false;
for (const [outputName] of Object.entries(
plant["outputs (tonne/tonne)"]
)) {
if (outputName === productName) outputFound = true;
}
if (outputFound) {
delete plant["outputs (tonne/tonne)"][productName];
}
}
save(newData);
return newData;
});
};
const onSetPlantInput = (plantName, productName) => {
setData((prevData) => {
const newData = { ...prevData };
newData.plants[plantName].input = productName;
save(newData);
return newData;
});
};
const onAddPlantOutput = (plantName, productName) => {
setData((prevData) => {
if (productName in prevData.plants[plantName]["outputs (tonne/tonne)"]) {
return prevData;
}
const newData = { ...prevData };
[
"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;
});
};
const onSave = () => {
generateFile("case.json", JSON.stringify(exportData(data), null, 2));
};
const onClear = () => {
const newData = JSON.parse(JSON.stringify(defaultData));
setData(newData);
save(newData);
};
const onLoad = (contents) => {
const newData = importData(JSON.parse(contents));
setData(newData);
save(newData);
};
const onChange = (val, field1, field2) => {
setData((prevData) => {
const newData = { ...prevData };
if (field2 !== undefined) {
newData[field1][field2] = val;
} else {
newData[field1] = val;
}
save(newData);
return newData;
});
};
let productComps = [];
for (const [prodName, prod] of Object.entries(data.products)) {
productComps.push(
<ProductBlock
key={prodName}
name={prodName}
value={prod}
onChange={(v) => onChange(v, "products", prodName, v)}
/>
);
}
let plantComps = [];
for (const [plantName, plant] of Object.entries(data.plants)) {
plantComps.push(
<PlantBlock
key={plantName}
name={plantName}
value={plant}
onChange={(v) => onChange(v, "plants", plantName)}
/>
);
}
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 <> 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}
onAddProduct={onAddProduct} onAddProduct={onAddProduct}
onMovePlant={onMovePlant} onMovePlant={onMovePlant}
onMoveProduct={onMoveProduct} onMoveProduct={onMoveProduct}
onRenamePlant={onRenamePlant} onRenamePlant={onRenamePlant}
onRenameProduct={onRenameProduct} onRenameProduct={onRenameProduct}
onSetPlantInput={onSetPlantInput} onSetPlantInput={onSetPlantInput}
onRemovePlant={onRemovePlant} onRemovePlant={onRemovePlant}
onRemoveProduct={onRemoveProduct} onRemoveProduct={onRemoveProduct}
plants={data.plants} plants={data.plants}
products={data.products} products={data.products}
/> />
<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,46 +1,46 @@
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) => {
props.value[field] = val; props.value[field] = val;
props.onChange(props.value); props.onChange(props.value);
}; };
return ( return (
<> <>
<Section title="Parameters" /> <Section title="Parameters" />
<Card> <Card>
<Form> <Form>
<TextInputRow <TextInputRow
label="Time horizon" label="Time horizon"
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
label="Building period" label="Building period"
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>
</> </>
); );
}; };
export default ParametersBlock; export default ParametersBlock;

@ -1,142 +1,191 @@
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;
const PipelineBlock = (props) => { export const randomPosition = () => {
let elements = []; window.nextY += 60;
let mapNameToType = {}; if (window.nextY >= 500) {
for (const [productName, product] of Object.entries(props.products)) { window.nextY = 15;
mapNameToType[productName] = "product"; window.nextX += 150;
elements.push({ }
id: productName, return [window.nextX, window.nextY];
data: { label: productName, type: 'product' }, };
position: { x: product.x, y: product.y },
sourcePosition: 'right', const getLayoutedElements = (elements) => {
targetPosition: 'left', const nodeWidth = 125;
className: styles.ProductNode, 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;
});
};
for (const [plantName, plant] of Object.entries(props.plants)) { const PipelineBlock = (props) => {
mapNameToType[plantName] = "plant"; let elements = [];
elements.push({ let mapNameToType = {};
id: plantName, for (const [productName, product] of Object.entries(props.products)) {
data: { label: plantName, type: 'plant' }, mapNameToType[productName] = "product";
position: { x: plant.x, y: plant.y }, elements.push({
sourcePosition: 'right', id: productName,
targetPosition: 'left', data: { label: productName, type: "product" },
className: styles.PlantNode, position: { x: product.x, y: product.y },
}); sourcePosition: "right",
targetPosition: "left",
className: styles.ProductNode,
});
}
if (plant.input != undefined) { for (const [plantName, plant] of Object.entries(props.plants)) {
elements.push({ mapNameToType[plantName] = "plant";
id: `${plant.input}-${plantName}`, elements.push({
source: plant.input, id: plantName,
target: plantName, data: { label: plantName, type: "plant" },
animated: true, position: { x: plant.x, y: plant.y },
style: { stroke: "black" }, sourcePosition: "right",
selectable: false, targetPosition: "left",
}); className: styles.PlantNode,
} });
for (const [productName, amount] of Object.entries(plant.outputs)) { if (plant.input !== undefined) {
elements.push({ elements.push({
id: `${plantName}-${productName}`, id: `${plant.input}-${plantName}`,
source: plantName, source: plant.input,
target: productName, target: plantName,
animated: true, animated: true,
style: { stroke: "black" }, style: { stroke: "black" },
selectable: false, selectable: false,
}); });
} }
for (const [productName] of Object.entries(
plant["outputs (tonne/tonne)"]
)) {
elements.push({
id: `${plantName}-${productName}`,
source: plantName,
target: productName,
animated: true,
style: { stroke: "black" },
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);
} }
}; };
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);
} }
}); });
}; };
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);
} }
}; };
const onConnect = (args) => {
const sourceType = mapNameToType[args.source];
const targetType = mapNameToType[args.target];
if (sourceType === "product" && targetType === "plant") {
props.onSetPlantInput(args.target, args.source);
} else if (sourceType === "plant" && targetType === "product") {
props.onAddPlantOutput(args.source, args.target);
}
};
const onConnect = (args) => { const onLayout = () => {
const sourceType = mapNameToType[args.source]; const layoutedElements = getLayoutedElements(elements);
const targetType = mapNameToType[args.target]; layoutedElements.forEach((el) => {
if (sourceType === "product" && targetType === "plant") { if (isNode(el)) {
props.onSetPlantInput(args.target, args.source); if (el.data.type === "plant") {
} else if (sourceType === "plant" && targetType === "product") { props.onMovePlant(el.data.label, el.position.x, el.position.y);
props.onAddPlantOutput(args.source, args.target); } else {
props.onMoveProduct(el.data.label, el.position.x, el.position.y);
} }
}; }
});
};
return ( return (
<> <>
<Section title="Pipeline" /> <Section title="Pipeline" />
<Card> <Card>
<div className={styles.PipelineBlock}> <div className={styles.PipelineBlock}>
<ReactFlow <ReactFlow
elements={elements} elements={elements}
onNodeDoubleClick={onNodeDoubleClick} onNodeDoubleClick={onNodeDoubleClick}
onNodeDragStop={onNodeDragStop} onNodeDragStop={onNodeDragStop}
onConnect={onConnect} onConnect={onConnect}
onElementsRemove={onElementsRemove} onElementsRemove={onElementsRemove}
deleteKeyCode={46} deleteKeyCode={46}
maxZoom={1} maxZoom={1}
minZoom={1} minZoom={1}
snapToGrid={true} snapToGrid={true}
preventScrolling={false} preventScrolling={false}
> >
<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" <Button
onClick={props.onAddPlant} label="?"
/> kind="inline"
<Button tooltip="Drag from one connector to another to create links between products and plants. Double click to rename an element. Press [Delete] to remove an element."
label="?" />
kind="inline" </div>
tooltip="Drag from one connector to another to create links between products and plants. Double click to rename an element. Press [Delete] to remove an element." </Card>
/> </>
</div> );
</Card>
</>
);
}; };
export default PipelineBlock; export default PipelineBlock;

@ -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,142 +1,266 @@
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) {
const output = { newPlant[field1][field2] = val;
"Nickel": "0.5", } else {
"Metal casing": "0.35", newPlant[field1] = val;
}; }
return ( props.onChange(newPlant);
<> };
<Section title={props.name} />
<Card>
<Form>
<h1>General information</h1>
<FileInputRow
label="Candidate locations"
tooltip="A dictionary mapping the name of the location to a dictionary which describes the site characteristics."
/>
const onCandidateLocationsTemplate = () => {
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",
},
])
);
};
<h1>Inputs & Outputs</h1> const onCandidateLocationsFile = (contents) => {
<TextInputRow const data = csvParse({
label="Input" contents: contents,
tooltip="The name of the product that this plant takes as input. Only one input is accepted per plant." requiredCols: [
disabled="disabled" "name",
value="Battery" "latitude (deg)",
/> "longitude (deg)",
<DictInputRow "area cost factor",
label="Outputs" ],
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." const result = {};
value={output} data.forEach((el) => {
disableKeys={true} result[el["name"]] = {
default="0" "latitude (deg)": el["latitude (deg)"],
/> "longitude (deg)": el["longitude (deg)"],
"area cost factor": el["area cost factor"],
};
});
onChange(result, "locations");
};
<h1>Capacity & costs</h1> const onCandidateLocationsDownload = () => {
<TextInputRow const result = [];
label="Minimum capacity" for (const [locationName, locationDict] of Object.entries(
unit="tonne" props.value["locations"]
tooltip="The minimum size of the plant." )) {
default="0" result.push({
/> name: locationName,
<TextInputRow "latitude (deg)": locationDict["latitude (deg)"],
label="Opening cost (min capacity)" "longitude (deg)": locationDict["longitude (deg)"],
unit="$" "area cost factor": locationDict["area cost factor"],
tooltip="The cost to open the plant at minimum capacity." });
default="0.00" }
/> generateFile(`Candidate locations - ${props.name}.csv`, csvFormat(result));
<TextInputRow };
label="Fixed operating cost (min capacity)"
unit="$"
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
default="0.00"
/>
<TextInputRow
label="Maximum capacity"
unit="tonne"
tooltip="The maximum size of the plant."
default="0"
/>
<TextInputRow
label="Opening cost (max capacity)"
unit="$"
tooltip="The cost to open a plant of this size."
default="0.00"
/>
<TextInputRow
label="Fixed operating cost (max capacity)"
unit="$"
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
default="0.00"
/>
<TextInputRow
label="Variable operating cost"
unit="$"
tooltip="The cost that the plant incurs to process each tonne of input."
default="0.00"
/>
<TextInputRow
label="Energy expenditure"
unit="GJ/tonne"
tooltip="The energy required to process 1 tonne of the input."
default="0"
/>
<h1>Storage</h1> const onCandidateLocationsClear = () => {
<TextInputRow onChange({}, "locations");
label="Storage cost" };
unit="$/tonne"
tooltip="The cost to store a tonne of input product for one time period."
default="0.00"
/>
<TextInputRow
label="Storage limit"
unit="tonne"
tooltip="The maximum amount of input product this plant can have in storage at any given time."
default="0"
/>
<h1>Disposal</h1> let description = "No locations set";
<DictInputRow const nCenters = Object.keys(props.value["locations"]).length;
label="Disposal cost" if (nCenters > 0) description = `${nCenters} locations`;
unit="$/tonne"
tooltip="The cost to dispose of the product."
value={output}
disableKeys={true}
/>
<DictInputRow
label="Disposal limit"
unit="tonne"
tooltip="The maximum amount that can be disposed of. If an unlimited amount can be disposed, this key may be omitted."
value={output}
disableKeys={true}
/>
<h1>Emissions</h1> const shouldDisableMaxCap =
<DictInputRow props.value["minimum capacity (tonne)"] ===
label="Emissions" props.value["maximum capacity (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)."
value={emissions}
keyPlaceholder="Emission name"
valuePlaceholder="0"
/>
</Form> return (
</Card> <>
</> <Section title={props.name} />
); <Card>
<Form>
<h1>General information</h1>
<FileInputRow
label="Candidate locations"
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>
<TextInputRow
label="Input"
tooltip="The name of the product that this plant takes as input."
disabled="disabled"
value="Battery"
/>
<DictInputRow
label="Outputs"
unit="tonne/tonne"
tooltip="A dictionary specifying how many tonnes of each product is produced for each tonne of input."
value={props.value["outputs (tonne/tonne)"]}
onChange={(v) => onChange(v, "outputs (tonne/tonne)")}
disableKeys={true}
validate="float"
/>
<h1>Capacity & Costs</h1>
<TextInputRow
label="Minimum capacity"
unit="tonne"
tooltip="The minimum size of the plant."
value={props.value["minimum capacity (tonne)"]}
onChange={(v) => onChange(v, "minimum capacity (tonne)")}
validate="float"
/>
<TextInputRow
label="Opening cost (min capacity)"
unit="$"
tooltip="The cost to open the plant at minimum capacity."
value={props.value["opening cost (min capacity) ($)"]}
onChange={(v) => onChange(v, "opening cost (min capacity) ($)")}
validate="float"
/>
<TextInputRow
label="Fixed operating cost (min capacity)"
unit="$"
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
value={props.value["fixed operating cost (min capacity) ($)"]}
onChange={(v) =>
onChange(v, "fixed operating cost (min capacity) ($)")
}
validate="float"
/>
<TextInputRow
label="Maximum capacity"
unit="tonne"
tooltip="The maximum size of the plant."
value={props.value["maximum capacity (tonne)"]}
onChange={(v) => onChange(v, "maximum capacity (tonne)")}
validate="float"
/>
<TextInputRow
label="Opening cost (max capacity)"
unit="$"
tooltip="The cost to open a plant of this size."
value={
shouldDisableMaxCap
? ""
: props.value["opening cost (max capacity) ($)"]
}
onChange={(v) => onChange(v, "opening cost (max capacity) ($)")}
validate="float"
disabled={shouldDisableMaxCap}
/>
<TextInputRow
label="Fixed operating cost (max capacity)"
unit="$"
tooltip="The cost to keep the plant open, even if the plant doesn't process anything."
value={
shouldDisableMaxCap
? ""
: props.value["fixed operating cost (max capacity) ($)"]
}
onChange={(v) =>
onChange(v, "fixed operating cost (max capacity) ($)")
}
validate="float"
disabled={shouldDisableMaxCap}
/>
<TextInputRow
label="Variable operating cost"
unit="$"
tooltip="The cost that the plant incurs to process each tonne of input."
value={props.value["variable operating cost ($/tonne)"]}
onChange={(v) => onChange(v, "variable operating cost ($/tonne)")}
validate="float"
/>
<TextInputRow
label="Energy expenditure"
unit="GJ/tonne"
tooltip="The energy required to process one tonne of the input."
value={props.value["energy (GJ/tonne)"]}
onChange={(v) => onChange(v, "energy (GJ/tonne)")}
validate="float"
/>
<h1>Storage</h1>
<TextInputRow
label="Storage cost"
unit="$/tonne"
tooltip="The cost to store a tonne of input product for one time period."
value={props.value["storage"]["cost ($/tonne)"]}
onChange={(v) => onChange(v, "storage", "cost ($/tonne)")}
/>
<TextInputRow
label="Storage limit"
unit="tonne"
tooltip="The maximum amount of input product this plant can have in storage at any given time."
value={props.value["storage"]["limit (tonne)"]}
onChange={(v) => onChange(v, "storage", "limit (tonne)")}
/>
<h1>Disposal</h1>
<DictInputRow
label="Disposal cost"
unit="$/tonne"
tooltip="The cost to dispose of the product."
value={props.value["disposal cost ($/tonne)"]}
onChange={(v) => onChange(v, "disposal cost ($/tonne)")}
disableKeys={true}
/>
<DictInputRow
label="Disposal limit"
unit="tonne"
tooltip="The maximum amount that can be disposed of. If an unlimited amount can be disposed, leave blank."
value={props.value["disposal limit (tonne)"]}
onChange={(v) => onChange(v, "disposal limit (tonne)")}
disableKeys={true}
valuePlaceholder="Unlimited"
/>
<h1>Emissions</h1>
<DictInputRow
label="Emissions"
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)."
value={props.value["emissions (tonne/tonne)"]}
onChange={(v) => onChange(v, "emissions (tonne/tonne)")}
keyPlaceholder="Emission name"
valuePlaceholder="0"
/>
</Form>
</Card>
</>
);
}; };
export default PlantBlock; export default PlantBlock;

@ -1,212 +1,177 @@
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) => {
const newProduct = { ...props.value }; const newProduct = { ...props.value };
newProduct[field] = val; newProduct[field] = val;
props.onChange(newProduct); props.onChange(newProduct);
}; };
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 result = {};
const requiredCols = ["latitude (deg)", "longitude (deg)", "name"]; data.forEach((el) => {
for (let t = 0; t < T; t++) { result[el["name"]] = {
requiredCols.push(t + 1); "latitude (deg)": el["latitude (deg)"],
} "longitude (deg)": el["longitude (deg)"],
"amount (tonne)": extractNumericColumns(el, "amount"),
// Check required columns };
requiredCols.forEach(col => { });
if (!(col in data[0])) { onChange("initial amounts", result);
console.log(`Column "${col}" not found in CSV file.`); };
isValid = false;
} const onInitialAmountsClear = () => {
}); onChange("initial amounts", {});
if (!isValid) return; };
// Construct initial amounts dict const onInitialAmountsTemplate = () => {
const result = {}; generateFile(
data.forEach(el => { "Initial amounts - Template.csv",
let amounts = []; csvFormat([
for (let t = 0; t < T; t++) { {
amounts.push(el[t + 1]); name: "Washakie County",
} "latitude (deg)": "43.8356",
result[el["name"]] = { "longitude (deg)": "-107.6602",
"latitude (deg)": el["latitude (deg)"], "amount 1": "21902",
"longitude (deg)": el["longitude (deg)"], "amount 2": "6160",
"amount (tonne)": amounts, "amount 3": "2721",
}; "amount 4": "12917",
}); "amount 5": "18048",
},
onChange("initial amounts", result); {
}; name: "Platte County",
"latitude (deg)": "42.1314",
const onInitialAmountsClear = () => { "longitude (deg)": "-104.9676",
onChange("initial amounts", {}); "amount 1": "16723",
}; "amount 2": "8709",
"amount 3": "22584",
const onInitialAmountsTemplate = () => { "amount 4": "12278",
exportToCsv( "amount 5": "7196",
"Initial amounts - Template.csv", [ },
["name", "latitude (deg)", "longitude (deg)", "1", "2", "3", "4", "5"], {
["Washakie County", "43.8356", "-107.6602", "21902", "6160", "2721", "12917", "18048"], name: "Park County",
["Platte County", "42.1314", "-104.9676", "16723", "8709", "22584", "12278", "7196"], "latitude (deg)": "44.4063",
["Park County", "44.4063", "-109.4153", "14731", "11729", "15562", "7703", "23349"], "longitude (deg)": "-109.4153",
["Goshen County", "42.0853", "-104.3534", "23266", "16299", "11470", "20107", "21592"], "amount 1": "14731",
]); "amount 2": "11729",
}; "amount 3": "15562",
"amount 4": "7703",
const onInitialAmountsDownload = () => { "amount 5": "23349",
const result = []; },
for (const [locationName, locationDict] of Object.entries(props.value["initial amounts"])) { ])
// Add header
if (result.length == 0) {
const T = locationDict["amount (tonne)"].length;
const row = ["name", "latitude (deg)", "longitude (deg)"];
for (let t = 0; t < T; t++) {
row.push(t + 1);
}
result.push(row);
}
// Add content row
const row = [locationName, locationDict["latitude (deg)"], locationDict["longitude (deg)"]];
locationDict["amount (tonne)"].forEach(el => {
row.push(el);
});
result.push(row);
}
exportToCsv(`Initial amounts - ${props.name}`, result);
};
let description = "Not initially available";
const nCenters = Object.keys(props.value["initial amounts"]).length;
if (nCenters > 0) {
description = `${nCenters} collection centers`;
}
return (
<>
<Section title={props.name} />
<Card>
<Form>
<h1>General information</h1>
<FileInputRow
value={description}
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."
accept=".csv"
onFile={onInitialAmountsFile}
onDownload={onInitialAmountsDownload}
onClear={onInitialAmountsClear}
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>
<TextInputRow
label="Disposal cost"
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."
value={props.value["disposal cost ($/tonne)"]}
onChange={v => onChange("disposal cost ($/tonne)", v)}
validate="float"
/>
<TextInputRow
label="Disposal limit"
unit="tonne"
tooltip="The maximum amount of this product that can be disposed of across all collection centers, without further processing."
value={props.value["disposal limit (tonne)"]}
onChange={v => onChange("disposal limit (tonne)", v)}
validate="float"
/>
<h1>Transportation</h1>
<TextInputRow
label="Transportation cost"
unit="$/km/tonne"
tooltip="The cost to transport this product."
value={props.value["transportation cost ($/km/tonne)"]}
onChange={v => onChange("transportation cost ($/km/tonne)", v)}
validate="float"
/>
<TextInputRow
label="Transportation energy"
unit="J/km/tonne"
tooltip="The energy required to transport this product."
value={props.value["transportation energy (J/km/tonne)"]}
onChange={v => onChange("transportation energy (J/km/tonne)", v)}
validate="float"
/>
<DictInputRow
label="Transportation emissions"
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)."
keyPlaceholder="Emission name"
value={props.value["transportation emissions (J/km/tonne)"]}
onChange={v => onChange("transportation emissions (J/km/tonne)", v)}
validate="float"
/>
</Form>
</Card>
</>
); );
}; };
function exportToCsv(filename, rows) { const onInitialAmountsDownload = () => {
var processRow = function (row) { const results = [];
var finalVal = ""; for (const [locationName, locationDict] of Object.entries(
for (var j = 0; j < row.length; j++) { props.value["initial amounts"]
var innerValue = row[j] === null ? "" : row[j].toString(); )) {
if (row[j] instanceof Date) { const row = {
innerValue = row[j].toLocaleString(); name: locationName,
} "latitude (deg)": locationDict["latitude (deg)"],
var result = innerValue.replace(/"/g, '""'); "longitude (deg)": locationDict["longitude (deg)"],
if (result.search(/("|,|\n)/g) >= 0) result = '"' + result + '"'; };
if (j > 0) finalVal += ","; locationDict["amount (tonne)"].forEach((el, idx) => {
finalVal += result; row[`amount ${idx + 1}`] = el;
} });
return finalVal + "\n"; results.push(row);
};
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);
}
} }
} generateFile(`Initial amounts - ${props.name}.csv`, csvFormat(results));
};
let description = "Not initially available";
const nCenters = Object.keys(props.value["initial amounts"]).length;
if (nCenters > 0) description = `${nCenters} collection centers`;
return (
<>
<Section title={props.name} />
<Card>
<Form>
<h1>General Information</h1>
<FileInputRow
value={description}
label="Initial amounts"
tooltip="A table indicating the amount of this product initially available at each collection center."
accept=".csv"
onFile={onInitialAmountsFile}
onDownload={onInitialAmountsDownload}
onClear={onInitialAmountsClear}
onTemplate={onInitialAmountsTemplate}
/>
<h1>Disposal</h1>
<TextInputRow
label="Disposal cost"
unit="$/tonne"
tooltip="The cost to dispose of one tonne of this product at a collection center, without further processing."
value={props.value["disposal cost ($/tonne)"]}
onChange={(v) => onChange("disposal cost ($/tonne)", v)}
validate="floatList"
/>
<TextInputRow
label="Disposal limit"
unit="tonne"
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)"]}
onChange={(v) => onChange("disposal limit (tonne)", v)}
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>
<TextInputRow
label="Transportation cost"
unit="$/km/tonne"
tooltip="The cost to transport this product."
value={props.value["transportation cost ($/km/tonne)"]}
onChange={(v) => onChange("transportation cost ($/km/tonne)", v)}
validate="floatList"
/>
<TextInputRow
label="Transportation energy"
unit="J/km/tonne"
tooltip="The energy required to transport this product."
value={props.value["transportation energy (J/km/tonne)"]}
onChange={(v) => onChange("transportation energy (J/km/tonne)", v)}
validate="floatList"
/>
<DictInputRow
label="Transportation emissions"
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."
keyPlaceholder="Emission name"
value={props.value["transportation emissions (tonne/km/tonne)"]}
onChange={(v) =>
onChange("transportation emissions (tonne/km/tonne)", v)
}
validate="floatList"
/>
</Form>
</Card>
</>
);
};
export default ProductBlock; export default ProductBlock;

@ -1,7 +1,7 @@
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>;
}; };
export default Section; export default Section;

@ -1,6 +1,6 @@
.Section { .Section {
line-height: 36px; line-height: 36px;
margin: 12px; margin: 12px;
font-size: 16px; font-size: 16px;
font-weight: bold; font-weight: bold;
} }

@ -1,40 +1,44 @@
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 (
<label> <div className={form_styles.FormRow}>
{props.label} {unit} <label>
</label> {props.label} {unit}
<input </label>
type="text" <input
placeholder={props.default} type="text"
disabled={props.disabled} placeholder={props.default}
value={props.value} disabled={props.disabled}
className={className} value={props.value}
onChange={e => props.onChange(e.target.value)} className={className}
/> onChange={(e) => props.onChange(e.target.value)}
{tooltip} ref={ref}
</div>; />
}; {tooltip}
</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" });
});

@ -1,52 +1,52 @@
:root { :root {
--site-width: 1200px; --site-width: 1200px;
--box-border: 1px solid rgba(0, 0, 0, 0.2); --box-border: 1px solid rgba(0, 0, 0, 0.2);
--box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2); --box-shadow: 0px 2px 4px -3px rgba(0, 0, 0, 0.2);
--border-radius: 4px; --border-radius: 4px;
} }
html, body { html,
margin: 0; body {
padding: 0; margin: 0;
border: 0; padding: 0;
border: 0;
} }
body { body {
background-color: #f6f6f6; background-color: #f6f6f6;
color: rgba(0, 0, 0, 0.95); color: rgba(0, 0, 0, 0.95);
} }
#content { #content {
max-width: var(--site-width); max-width: var(--site-width);
min-width: 900px; min-width: 900px;
margin: 0 auto; margin: 0 auto;
padding: 0 6px; padding: 0 6px;
} }
.react-flow__node.selected { .react-flow__node.selected {
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important; box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2) !important;
border-width: 2px !important; border-width: 2px !important;
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;
} }
.react-flow__handle:hover { .react-flow__handle:hover {
background-color: black !important; background-color: black !important;
} }
.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 /> <InputPage />
<div id="content">
<InputPage />
</div>
<Footer />
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById("root")
); );

Loading…
Cancel
Save