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/user-event": "^13.5.0",
"d3": "^7.3.0",
"dagre": "^0.8.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-flow-renderer": "^9.7.4",
@ -6266,6 +6267,15 @@
"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": {
"version": "1.0.8",
"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",
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",
@ -21012,6 +21030,15 @@
"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": {
"version": "1.0.8",
"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",
"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": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz",

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

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

@ -11,10 +11,7 @@
text-transform: uppercase;
font-weight: bold;
font-size: 12px;
background: linear-gradient(
rgb(255, 255, 255) 25%,
rgb(245, 245, 245) 100%
)
background: linear-gradient(rgb(255, 255, 255) 25%, rgb(245, 245, 245) 100%);
}
.Button:hover {
@ -52,7 +49,7 @@
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.25);
line-height: 18px;
padding: 6px;
transition: opacity .5s;
transition: opacity 0.5s;
font-weight: normal;
text-align: left;
padding: 6px 12px;
@ -61,5 +58,5 @@
.Button:hover .tooltip {
visibility: visible;
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) => {
return (<div className={styles.Card}>{props.children}</div>);
return <div className={styles.Card}>{props.children}</div>;
};
export default Card;

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

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

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

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

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

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

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

@ -1,7 +1,7 @@
import Section from './Section';
import Card from './Card';
import Form from './Form';
import TextInputRow from './TextInputRow';
import Section from "./Section";
import Card from "./Card";
import Form from "./Form";
import TextInputRow from "./TextInputRow";
const ParametersBlock = (props) => {
const onChangeField = (field, val) => {
@ -18,7 +18,7 @@ const ParametersBlock = (props) => {
unit="years"
tooltip="Number of years in the simulation."
value={props.value["time horizon (years)"]}
onChange={v => onChangeField("time horizon (years)", v)}
onChange={(v) => onChangeField("time horizon (years)", v)}
validate="int"
/>
<TextInputRow
@ -26,17 +26,17 @@ const ParametersBlock = (props) => {
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."
value={props.value["building period (years)"]}
onChange={v => onChangeField("building period (years)", v)}
onChange={(v) => onChangeField("building period (years)", v)}
validate="intList"
/>
<TextInputRow
{/* <TextInputRow
label="Annual inflation rate"
unit="%"
tooltip="Rate of inflation applied to all costs."
value={props.value["annual inflation rate (%)"]}
onChange={v => onChangeField("annual inflation rate (%)", v)}
onChange={(v) => onChangeField("annual inflation rate (%)", v)}
validate="float"
/>
/> */}
</Form>
</Card>
</>

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

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

@ -1,20 +1,104 @@
import Section from './Section';
import Card from './Card';
import Form from './Form';
import TextInputRow from './TextInputRow';
import FileInputRow from './FileInputRow';
import DictInputRow from './DictInputRow';
import Section from "./Section";
import Card from "./Card";
import Form from "./Form";
import TextInputRow from "./TextInputRow";
import FileInputRow from "./FileInputRow";
import DictInputRow from "./DictInputRow";
import { csvFormat, csvParse, generateFile } from "./csv";
const PlantBlock = (props) => {
const emissions = {
"CO2": "0.05",
"CH4": "0.01",
"N2O": "0.04",
const onChange = (val, field1, field2, field3) => {
const newPlant = { ...props.value };
if (field3 !== undefined) {
newPlant[field1][field2][field3] = val;
} else if (field2 !== undefined) {
newPlant[field1][field2] = val;
} else {
newPlant[field1] = val;
}
props.onChange(newPlant);
};
const output = {
"Nickel": "0.5",
"Metal casing": "0.35",
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",
},
])
);
};
const onCandidateLocationsFile = (contents) => {
const data = csvParse({
contents: contents,
requiredCols: [
"name",
"latitude (deg)",
"longitude (deg)",
"area cost factor",
],
});
const result = {};
data.forEach((el) => {
result[el["name"]] = {
"latitude (deg)": el["latitude (deg)"],
"longitude (deg)": el["longitude (deg)"],
"area cost factor": el["area cost factor"],
};
});
onChange(result, "locations");
};
const onCandidateLocationsDownload = () => {
const result = [];
for (const [locationName, locationDict] of Object.entries(
props.value["locations"]
)) {
result.push({
name: locationName,
"latitude (deg)": locationDict["latitude (deg)"],
"longitude (deg)": locationDict["longitude (deg)"],
"area cost factor": locationDict["area cost factor"],
});
}
generateFile(`Candidate locations - ${props.name}.csv`, csvFormat(result));
};
const onCandidateLocationsClear = () => {
onChange({}, "locations");
};
let description = "No locations set";
const nCenters = Object.keys(props.value["locations"]).length;
if (nCenters > 0) description = `${nCenters} locations`;
const shouldDisableMaxCap =
props.value["minimum capacity (tonne)"] ===
props.value["maximum capacity (tonne)"];
return (
<>
<Section title={props.name} />
@ -23,74 +107,109 @@ const PlantBlock = (props) => {
<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."
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. Only one input is accepted per plant."
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 tonnes of input. If the plant does not output anything, this key may be omitted."
value={output}
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}
default="0"
validate="float"
/>
<h1>Capacity & costs</h1>
<h1>Capacity & Costs</h1>
<TextInputRow
label="Minimum capacity"
unit="tonne"
tooltip="The minimum size of the plant."
default="0"
value={props.value["minimum capacity (tonne)"]}
onChange={(v) => onChange(v, "minimum capacity (tonne)")}
validate="float"
/>
<TextInputRow
label="Opening cost (min capacity)"
unit="$"
tooltip="The cost to open the plant at minimum capacity."
default="0.00"
value={props.value["opening cost (min capacity) ($)"]}
onChange={(v) => onChange(v, "opening cost (min capacity) ($)")}
validate="float"
/>
<TextInputRow
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"
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."
default="0"
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."
default="0.00"
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."
default="0.00"
value={
shouldDisableMaxCap
? ""
: props.value["fixed operating cost (max capacity) ($)"]
}
onChange={(v) =>
onChange(v, "fixed operating cost (max capacity) ($)")
}
validate="float"
disabled={shouldDisableMaxCap}
/>
<TextInputRow
label="Variable operating cost"
unit="$"
tooltip="The cost that the plant incurs to process each tonne of input."
default="0.00"
value={props.value["variable operating cost ($/tonne)"]}
onChange={(v) => onChange(v, "variable operating cost ($/tonne)")}
validate="float"
/>
<TextInputRow
label="Energy expenditure"
unit="GJ/tonne"
tooltip="The energy required to process 1 tonne of the input."
default="0"
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>
@ -98,13 +217,15 @@ const PlantBlock = (props) => {
label="Storage cost"
unit="$/tonne"
tooltip="The cost to store a tonne of input product for one time period."
default="0.00"
value={props.value["storage"]["cost ($/tonne)"]}
onChange={(v) => onChange(v, "storage", "cost ($/tonne)")}
/>
<TextInputRow
label="Storage limit"
unit="tonne"
tooltip="The maximum amount of input product this plant can have in storage at any given time."
default="0"
value={props.value["storage"]["limit (tonne)"]}
onChange={(v) => onChange(v, "storage", "limit (tonne)")}
/>
<h1>Disposal</h1>
@ -112,15 +233,18 @@ const PlantBlock = (props) => {
label="Disposal cost"
unit="$/tonne"
tooltip="The cost to dispose of the product."
value={output}
value={props.value["disposal cost ($/tonne)"]}
onChange={(v) => onChange(v, "disposal cost ($/tonne)")}
disableKeys={true}
/>
<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}
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>
@ -128,11 +252,11 @@ const PlantBlock = (props) => {
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={emissions}
value={props.value["emissions (tonne/tonne)"]}
onChange={(v) => onChange(v, "emissions (tonne/tonne)")}
keyPlaceholder="Emission name"
valuePlaceholder="0"
/>
</Form>
</Card>
</>

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save