Skip to content

Commit

Permalink
adding attrs type declaration file
Browse files Browse the repository at this point in the history
  • Loading branch information
bayc committed Jul 13, 2023
1 parent 86b1087 commit 7527c8b
Show file tree
Hide file tree
Showing 10 changed files with 307 additions and 16 deletions.
1 change: 0 additions & 1 deletion hopp/simulation/hybrid_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
from hopp.simulation.technologies.pv_source import PVPlant
from hopp.simulation.technologies.detailed_pv_plant import DetailedPVPlant
from hopp.simulation.technologies.wind_source import WindPlant
from hopp.simulation.technologies.wind_source_refactor import WindPlantRefactor
from hopp.simulation.technologies.tower_source import TowerPlant
from hopp.simulation.technologies.trough_source import TroughPlant
from hopp.simulation.technologies.battery import Battery
Expand Down
1 change: 0 additions & 1 deletion hopp/simulation/technologies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,3 @@
from hopp.simulation.technologies.trough_source import TroughPlant
from hopp.simulation.technologies.utility_rate import UtilityRate
from hopp.simulation.technologies.wind_source import WindPlant
from hopp.simulation.technologies.wind_source_refactor import WindPlantRefactor
122 changes: 122 additions & 0 deletions hopp/simulation/technologies/hydrogen/desal/desal_model_eco.py
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)
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import json
import os

import hopp.simulation.technologies.hydrogen.h2_storage.pressure_vessel.von_mises as von_mises
from hopp.simulation.technologies.hydrogen.h2_storage.pressure_vessel import von_mises

class MetalMaterial(object):
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import sys
from pathlib import Path
from itertools import product
import multiprocessing_on_dill as mp
import multiprocessing as mp
from typing import Union
import numpy as np
import matplotlib.pyplot as plt
Expand Down
4 changes: 2 additions & 2 deletions hopp/simulation/technologies/wind/floris.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
from floris.tools import FlorisInterface

from hopp.simulation.base import BaseClass
from hopp.simulation.technologies.sites import SiteInfoRefactor
from hopp.simulation.technologies.sites import SiteInfo
from hopp.type_dec import resource_file_converter


@define
class Floris(BaseClass):
config_dict: dict = field(converter=dict)
site: SiteInfoRefactor = field()
site: SiteInfo = field()
timestep: tuple = field(default=(), converter=tuple)

def __attrs_post_init__(self):
Expand Down
172 changes: 172 additions & 0 deletions hopp/type_dec.py
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)
3 changes: 1 addition & 2 deletions tests/hopp/test_clustering.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import pytest
from pytest import approx
import hopp.simulation.technologies.clustering as clustering
from hopp.simulation.technologies import clustering
import numpy as np
import pandas as pd
import copy
Expand Down
Loading

0 comments on commit 7527c8b

Please sign in to comment.