-
Notifications
You must be signed in to change notification settings - Fork 45
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
307 additions
and
16 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
122 changes: 122 additions & 0 deletions
122
hopp/simulation/technologies/hydrogen/desal/desal_model_eco.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,122 @@ | ||
################## needed addition ###################### | ||
""" | ||
Description: This file already contains a desal model, but we need an estimate of the desal unit size, particularly mass and footprint (m^2) | ||
Sources: | ||
- [1] Singlitico 2021 (use this as a jumping off point, I think there may be other good sources available) | ||
- [2] See sources in existing model below and the model itself | ||
Args: | ||
- electrolyzer_rating (float): electrolyzer rating in MW | ||
- input and output values from RO_desal() below | ||
- others may be added as needed | ||
Returns (can be from separate functions and/or methods as it makes sense): | ||
- mass (float): approximate mass of the desalination system (kg or tonnes) | ||
- footprint (float): approximate area required for the desalination system (m^2) | ||
""" | ||
|
||
|
||
#################### existing model ######################## | ||
|
||
## High-Pressure Reverse Osmosis Desalination Model | ||
""" | ||
Python model of High-Pressure Reverse Osmosis Desalination (HPRO). | ||
Reverse Osmosis (RO) is a membrane separation process. No heating or phase change is necessary. | ||
The majority of energy required is for pressurizing the feed water. | ||
A typical RO system is made up of the following basic components: | ||
Pre-treatment: Removes suspended solids and microorganisms through sterilization, fine filtration and adding chemicals to inhibit precipitation. | ||
High-pressure pump: Supplies the pressure needed to enable the water to pass through the membrane (pressure ranges from 54 to 80 bar for seawater). | ||
Membrane Modules: Membrane assembly consists of a pressure vessel and the membrane. Either sprial wound membranes or hollow fiber membranes are used. | ||
Post-treatment: Consists of sterilization, stabilization, mineral enrichment and pH adjustment of product water. | ||
Energy recovery system: A system where a portion of the pressure energy of the brine is recovered. | ||
Costs are in 2013 dollars | ||
""" | ||
import sys | ||
import numpy as np | ||
from examples.H2_Analysis.simple_cash_annuals import simple_cash_annuals | ||
|
||
|
||
def RO_desal_eco(freshwater_kg_per_hr, salinity): | ||
""" | ||
param: freshwater_kg_per_hr: Maximum freshwater requirements of system [kg/hr] | ||
param: salinity: (str) "Seawater" >18,000 ppm or "Brackish" <18,000 ppm | ||
output: feedwater_m3_per_hr: feedwater flow rate [m^3/hr] | ||
output: desal_power: reqired power [kW] | ||
output: desal_capex: Capital cost [USD] | ||
output: desal_opex: OPEX (USD/yr) | ||
Costs from: https://pdf.sciencedirectassets.com/271370/1-s2.0-S0011916408X00074/1-s2.0-S0011916408002683/main.pdf?X-Amz-Security-Token=IQoJb3JpZ2luX2VjEEcaCXVzLWVhc3QtMSJGMEQCIBNfL%2Frp%2BWpMGUW7rWBm3dkXztvOFIdswOdqI23VkBTGAiALG4NJuAiUkzKnukw233sXHF1OFBPnogJP1ZkboPkaiSrVBAjA%2F%2F%2F%2F%2F%2F%2F%2F%2F%2F8BEAUaDDA1OTAwMzU0Njg2NSIMWZ%2Fh3cDnrPjUJMleKqkELlVPKjinHYk85KwguMS3panLr1RRD9qkoxIASocYCbkvKLE9xW%2BT8QMCtEaH3Is7NRZ2Efc6YFQiO0DHbRzXYTfgz6Er5qqvSAFTrfgp%2B5bB3NYvtDI3kEGH%2F%2BOrEiL8iDK9TmgUjojvnKt86zidswBSDWrzclxcLrw6dfsqZf6dVjJT2g3Cyy8LKnP9vc33tCbACRLeszW1Zce%2BTlBbON22W%2FJq0qLcXDxI9JpRDqL8T%2Fo7SsetEif2DWovTLnv%2B%2FX2tJotFp630ZTVpd37ukGtanjAr5pl0nHgjnUtOJVtNksHQwc8XElFpBGKEXmvRo2uZJFd%2BeNtPEB1dWIZlZul6B8%2BJ7D%2FSPJsclPfpkMU92YUStQpw4Mc%2FOJFCILFyb4416DsL6PVWsdcYu9bbry8c0hQGZlE7oXTFoUy9SKdpEOguXAUi3X4JxjZisy3esVH8zNS3%2FiFsNr2FkTB6MLaSjSKj344AuDCkQYZ7CnenAiCHgf4a2tSnfiXzAvAFnpeQkr4iCnZOQ4Eis6L3fVRpWlluX5HUpbvUMN6rvtmAzq0APJn1b3NmFHy4ORoemTGvmI%2FHTRYKuAu257XBMe7X1qAJlnmpt6yGXrelXCz%2FmUvmbT1SzxETA5ss4KR0OM4YdXNnFLUrsV44ZkUM%2B8FlwZr%2F%2FePjz4QeG4ApR821IYTyre3%2FY%2BBZxaMs5AcXKiTHGwfE7CDi%2BQQ7CnDKk0lleZcas6kxzDl9%2BmeBjqqAeZhBVwd5sEx6aDGxAQC0eWpux6HauoVfuPOCkkv621szF0kTBqcoOlJlJav4eUPW4efAzBremirjiRLI2GdP72lVqXz9oaCg5NFXeKJAWbWkLdzHnDOu8ecSUPn%2F0jcR2IO2mznLspx6wKQA%2BAPEVGgptkwZtDqHcw8FNx7Q8tWJ1C4qL1bEMl0%2FatDXOHiJfuzCFp4%2B4uijTNfpVXO%2BzYQuNJA7ZNUMroa&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230201T155950Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=ASIAQ3PHCVTY7RLVF2MG%2F20230201%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Signature=a3770ee910f7f78c94bb84206538810ca03f7a653183191b3794c633b9e3f08f&hash=2e8904ff0d2a6ef567a5894d5bb773524bf1a90bc3ed88d8592e3f9d4cc3c531&host=68042c943591013ac2b2430a89b270f6af2c76d8dfd086a07176afe7c76c2c61&pii=S0011916408002683&tid=spdf-27339dc5-0d03-4078-a244-c049a9bb014d&sid=50eb5802654ba84dc80a5675e9bbf644ed4dgxrqa&type=client&tsoh=d3d3LnNjaWVuY2VkaXJlY3QuY29t&ua=0f1650585c05065559515c&rr=792be5868a1a8698&cc=us | ||
A desal system capacity is given as desired freshwater flow rate [m^3/hr] | ||
""" | ||
|
||
freshwater_density = 997 #[kg/m^3] | ||
freshwater_m3_per_hr = freshwater_kg_per_hr / freshwater_density | ||
desal_capacity = freshwater_m3_per_hr | ||
|
||
if salinity == "Seawater": | ||
# SWRO: Sea Water Reverse Osmosis, water >18,000 ppm | ||
# Water recovery | ||
recovery_ratio = 0.5 #https://www.usbr.gov/research/dwpr/reportpdfs/report072.pdf | ||
feedwater_m3_per_hr = freshwater_m3_per_hr / recovery_ratio | ||
|
||
# Power required | ||
energy_conversion_factor = 4.0 #[kWh/m^3] SWRO energy_conversion_factor range 2.5 to 4.0 kWh/m^3 | ||
#https://www.sciencedirect.com/science/article/pii/S0011916417321057 | ||
desal_power = freshwater_m3_per_hr * energy_conversion_factor | ||
|
||
|
||
elif salinity == "Brackish": | ||
# BWRO: Brakish water Reverse Osmosis, water < 18,000 ppm | ||
# Water recovery | ||
recovery_ratio = 0.75 #https://www.usbr.gov/research/dwpr/reportpdfs/report072.pdf | ||
feedwater_m3_per_hr = freshwater_m3_per_hr / recovery_ratio | ||
|
||
# Power required | ||
energy_conversion_factor = 1.5 #[kWh/m^3] BWRO energy_conversion_factor range 1.0 to 1.5 kWh/m^3 | ||
#https://www.sciencedirect.com/science/article/pii/S0011916417321057 | ||
|
||
desal_power = freshwater_m3_per_hr * energy_conversion_factor | ||
|
||
else: | ||
raise Exception("Salinity parameter must be set to Brackish or Seawater") | ||
|
||
# Costing | ||
# https://www.nrel.gov/docs/fy16osti/66073.pdf | ||
desal_capex = 32894 * (freshwater_density * desal_capacity / 3600) # [USD] | ||
|
||
desal_opex = 4841 * (freshwater_density * desal_capacity / 3600) # [USD/yr] | ||
|
||
'''Mass and Footprint | ||
Based on Commercial Industrial RO Systems | ||
https://www.appliedmembranes.com/s-series-seawater-reverse-osmosis-systems-2000-to-100000-gpd.html | ||
All Mass and Footprint Estimates are estimated from Largest RO System: | ||
S-308F | ||
-436 m^3/day | ||
-6330 kg | ||
-762 cm (L) x 112 cm (D) x 183 cm (H) | ||
436 m^3/day = 18.17 m^3/hr = 8.5 m^2, 6330 kg | ||
1 m^3/hr = .467 m^2, 346.7 kg | ||
Voltage Codes | ||
460 or 480v/ 3ph/ 60 Hz | ||
''' | ||
desal_mass_kg = freshwater_m3_per_hr * 346.7 #[kg] | ||
desal_size_m2 = freshwater_m3_per_hr * .467 #[m^2] | ||
|
||
|
||
return desal_capacity, feedwater_m3_per_hr, desal_power, desal_capex, desal_opex, desal_mass_kg, desal_size_m2 | ||
|
||
if __name__ == '__main__': | ||
desal_freshwater_kg_hr = 75000 | ||
salinity = 'Brackish' | ||
test = RO_desal(desal_freshwater_kg_hr,salinity) | ||
print(test) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
# Copyright 2022 NREL | ||
|
||
# Licensed under the Apache License, Version 2.0 (the "License"); you may not | ||
# use this file except in compliance with the License. You may obtain a copy of | ||
# the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
|
||
# Unless required by applicable law or agreed to in writing, software | ||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT | ||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the | ||
# License for the specific language governing permissions and limitations under | ||
# the License. | ||
|
||
from typing import Any, Iterable, Tuple, Union, Callable | ||
|
||
import attrs | ||
from attrs import define, Attribute | ||
from pathlib import Path | ||
import numpy as np | ||
import numpy.typing as npt | ||
import os.path | ||
|
||
from hopp.utilities.log import hybrid_logger as logger | ||
|
||
|
||
### Define general data types used throughout | ||
|
||
hopp_path = Path(__file__).parent.parent | ||
hopp_float_type = np.float64 | ||
|
||
NDArrayFloat = npt.NDArray[hopp_float_type] | ||
NDArrayInt = npt.NDArray[np.int_] | ||
NDArrayFilter = Union[npt.NDArray[np.int_], npt.NDArray[np.bool_]] | ||
NDArrayObject = npt.NDArray[np.object_] | ||
|
||
|
||
### Custom callables for attrs objects and functions | ||
|
||
def hopp_array_converter(data: Iterable) -> np.ndarray: | ||
try: | ||
a = np.array(data, dtype=hopp_float_type) | ||
except TypeError as e: | ||
raise TypeError(e.args[0] + f". Data given: {data}") | ||
return a | ||
|
||
def resource_file_converter(resource_file: str) -> None: | ||
# If the default value of an empty string is supplied, just pass through the default | ||
if resource_file == "": | ||
return resource_file | ||
|
||
# Check the path relative to the hopp directory for the resource file and return if it exists | ||
resource_file_path = str(hopp_path / resource_file) | ||
resolved_path = convert_to_path(resource_file_path) | ||
file_exists = os.path.isfile(resolved_path) | ||
if file_exists: | ||
return resolved_path | ||
else: # If path doesn't exist, check for absolute path | ||
resolved_path = convert_to_path(resource_file_path) | ||
file_exists = os.path.isfile(resolved_path) | ||
if file_exists: | ||
return resolved_path | ||
else: | ||
raise FileNotFoundError ( | ||
f"Resource file path is not resolvable: {resource_file}. " | ||
"The resource file path needs to be relative to the root HOPP directory " | ||
"or be absolute." | ||
) | ||
|
||
def attr_serializer(inst: type, field: Attribute, value: Any): | ||
if isinstance(value, np.ndarray): | ||
return value.tolist() | ||
return value | ||
|
||
def attr_hopp_filter(inst: Attribute, value: Any) -> bool: | ||
if inst.init is False: | ||
return False | ||
if value is None: | ||
return False | ||
if isinstance(value, np.ndarray): | ||
if value.size == 0: | ||
return False | ||
return True | ||
|
||
def iter_validator(iter_type, item_types: Union[Any, Tuple[Any]]) -> Callable: | ||
"""Helper function to generate iterable validators that will reduce the amount of | ||
boilerplate code. | ||
Parameters | ||
---------- | ||
iter_type : any iterable | ||
The type of iterable object that should be validated. | ||
item_types : Union[Any, Tuple[Any]] | ||
The type or types of acceptable item types. | ||
Returns | ||
------- | ||
Callable | ||
The attr.validators.deep_iterable iterable and instance validator. | ||
""" | ||
validator = attrs.validators.deep_iterable( | ||
member_validator=attrs.validators.instance_of(item_types), | ||
iterable_validator=attrs.validators.instance_of(iter_type), | ||
) | ||
return validator | ||
|
||
def convert_to_path(fn: str | Path) -> Path: | ||
"""Converts an input string or pathlib.Path object to a fully resolved ``pathlib.Path`` | ||
object. | ||
Args: | ||
fn (str | Path): The user input file path or file name. | ||
Raises: | ||
TypeError: Raised if :py:attr:`fn` is neither a :py:obj:`str`, nor a :py:obj:`pathlib.Path`. | ||
Returns: | ||
Path: A resolved pathlib.Path object. | ||
""" | ||
if isinstance(fn, str): | ||
fn = Path(fn) | ||
|
||
if isinstance(fn, Path): | ||
fn.resolve() | ||
else: | ||
raise TypeError(f"The passed input: {fn} could not be converted to a pathlib.Path object") | ||
return fn | ||
|
||
@define | ||
class FromDictMixin: | ||
""" | ||
A Mixin class to allow for kwargs overloading when a data class doesn't | ||
have a specific parameter definied. This allows passing of larger dictionaries | ||
to a data class without throwing an error. | ||
""" | ||
|
||
@classmethod | ||
def from_dict(cls, data: dict): | ||
"""Maps a data dictionary to an `attr`-defined class. | ||
TODO: Add an error to ensure that either none or all the parameters are passed in | ||
Args: | ||
data : dict | ||
The data dictionary to be mapped. | ||
Returns: | ||
cls | ||
The `attr`-defined class. | ||
""" | ||
# Check for any inputs that aren't part of the class definition | ||
class_attr_names = [a.name for a in cls.__attrs_attrs__] | ||
extra_args = [d for d in data if d not in class_attr_names] | ||
if len(extra_args): | ||
raise AttributeError(f"The initialization for {cls.__name__} was given extraneous inputs: {extra_args}") | ||
|
||
kwargs = {a.name: data[a.name] for a in cls.__attrs_attrs__ if a.name in data and a.init} | ||
|
||
# Map the inputs must be provided: 1) must be initialized, 2) no default value defined | ||
required_inputs = [a.name for a in cls.__attrs_attrs__ if a.init and a.default is attrs.NOTHING] | ||
undefined = sorted(set(required_inputs) - set(kwargs)) | ||
|
||
if undefined: | ||
raise AttributeError(f"The class defintion for {cls.__name__} is missing the following inputs: {undefined}") | ||
return cls(**kwargs) | ||
|
||
def as_dict(self) -> dict: | ||
"""Creates a JSON and YAML friendly dictionary that can be save for future reloading. | ||
This dictionary will contain only `Python` types that can later be converted to their | ||
proper `Turbine` formats. | ||
Returns: | ||
dict: All key, vaue pais required for class recreation. | ||
""" | ||
return attrs.asdict(self, filter=attr_hopp_filter, value_serializer=attr_serializer) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.