diff --git a/docs/modules/fluid_properties.rst b/docs/modules/fluid_properties.rst index eb1366578..8fa02f8ba 100644 --- a/docs/modules/fluid_properties.rst +++ b/docs/modules/fluid_properties.rst @@ -184,7 +184,7 @@ isentropic change of pressure for an ideal gas. ... + self.coefficients[5] * y ** 2 ... ) / self.coefficients[6] ... - ... def h_pT(self, p, T): + ... def h_pT(self, p, T, **kwargs): ... return self._h_pT(p, T) - self.h_ref ... ... def _h_pT(self, p, T): diff --git a/pyproject.toml b/pyproject.toml index 3f0de59ed..55ac0e9a2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ exclude = ["docs/_build"] [project] name = "tespy" -version = "0.7.3" +version = "0.8" description = "Thermal Engineering Systems in Python (TESPy)" readme = "README.rst" authors = [ diff --git a/src/tespy/components/component.py b/src/tespy/components/component.py index 4e5937786..f179f99a5 100644 --- a/src/tespy/components/component.py +++ b/src/tespy/components/component.py @@ -21,6 +21,7 @@ from tespy.tools.data_containers import ComponentCharacteristicMaps as dc_cm from tespy.tools.data_containers import ComponentCharacteristics as dc_cc from tespy.tools.data_containers import ComponentProperties as dc_cp +from tespy.tools.data_containers import ComponentPropertiesArray as dc_cpa from tespy.tools.data_containers import GroupedComponentCharacteristics as dc_gcc from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp from tespy.tools.data_containers import SimpleDataContainer as dc_simple @@ -209,6 +210,22 @@ def set_attr(self, **kwargs): logger.error(msg) raise TypeError(msg) + elif isinstance(data, dc_cpa): + floats = [isinstance(f, float) for f in kwargs[key]] + vars = [f == 'var' for f in kwargs[key]] + is_numeric = any(floats) + is_var = any(vars) + num_eq = floats.count(True) + + if is_numeric or is_var: + data.set_attr(val=kwargs[key], is_set=floats, is_var=vars, num_eq = num_eq) + else: + msg = ( + 'Bad datatype for keyword argument ' + key + + ' at ' + self.label + '.') + logger.error(msg) + raise TypeError(msg) + elif key in ['design', 'offdesign']: if not isinstance(kwargs[key], list): msg = ( diff --git a/src/tespy/components/nodes/base.py b/src/tespy/components/nodes/base.py index 55a22b82d..9a85ed428 100644 --- a/src/tespy/components/nodes/base.py +++ b/src/tespy/components/nodes/base.py @@ -171,6 +171,8 @@ def initialise_source(c, key): return 1e5 elif key == 'h': return 5e5 + elif key == 'T': # maybe add more parameters + return 300 @staticmethod def initialise_target(c, key): @@ -201,6 +203,8 @@ def initialise_target(c, key): return 1e5 elif key == 'h': return 5e5 + elif key == 'T': # maybe add more parameters + return 300 def propagate_to_target(self, branch): diff --git a/src/tespy/components/nodes/merge.py b/src/tespy/components/nodes/merge.py index b3b286cb2..217ef1018 100644 --- a/src/tespy/components/nodes/merge.py +++ b/src/tespy/components/nodes/merge.py @@ -112,7 +112,7 @@ class Merge(NodeBase): >>> inc1.set_attr(fluid={'O2': 0.23, 'N2': 0.77}, p=1, T=T, m=5) >>> inc2.set_attr(fluid={'O2': 1}, T=T, m=5) >>> inc3.set_attr(fluid={'N2': 1}, T=T) - >>> outg.set_attr(fluid={'N2': 0.4}) + >>> outg.set_attr(fluid={'N2': 0.4, 'O2': 0.6}) >>> nw.solve('design') >>> round(inc3.m.val_SI, 2) 0.25 diff --git a/src/tespy/components/nodes/separator.py b/src/tespy/components/nodes/separator.py index bf8a7be85..b2000249b 100644 --- a/src/tespy/components/nodes/separator.py +++ b/src/tespy/components/nodes/separator.py @@ -127,7 +127,7 @@ class Separator(NodeBase): mass fraction for this outlet. >>> outg1.set_attr(m=None) - >>> outg2.set_attr(fluid={'O2': 0.3}) + >>> outg2.set_attr(fluid={'O2': 0.3, 'N2': 0.7}) >>> nw.solve('design') >>> outg2.fluid.val['O2'] 0.3 @@ -350,8 +350,10 @@ def energy_balance_deriv(self, increment_filter, k): # for fluid in i.fluid.is_var: # self.jacobian[k, i.fluid.J_col[fluid]] = dT_dfluid_in[fluid] args = (o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule) - self.jacobian[k, o.p.J_col] = -dT_mix_dph(*args) - self.jacobian[k, o.h.J_col] = -dT_mix_pdh(*args) + if self.is_variable(o.p): + self.jacobian[k, o.p.J_col] = -dT_mix_dph(*args) + if self.is_variable(o.h): + self.jacobian[k, o.h.J_col] = -dT_mix_pdh(*args) # for fluid in o.fluid.is_var: # self.jacobian[k, o.fluid.J_col[fluid]] = -dT_mix_ph_dfluid(o) k += 1 diff --git a/src/tespy/connections/connection.py b/src/tespy/connections/connection.py index b60261628..79b08f314 100644 --- a/src/tespy/connections/connection.py +++ b/src/tespy/connections/connection.py @@ -215,18 +215,15 @@ class Connection: Specify the state keyword: The fluid will be forced to liquid or gaseous state in this case. - >>> so_si2.set_attr(state='l') - >>> so_si2.state.is_set - True - >>> so_si2.set_attr(state=None) - >>> so_si2.state.is_set - False - >>> so_si2.set_attr(state='g') - >>> so_si2.state.is_set - True - >>> so_si2.set_attr(state=None) - >>> so_si2.state.is_set - False + >>> so_si2.set_attr(force_state='l') + >>> so_si2.force_state + 'l' + >>> so_si2.set_attr(force_state='g') + >>> so_si2.force_state + 'g' + >>> so_si2.set_attr(force_state=None) + >>> so_si2.force_state + """ def __init__(self, source, outlet_id, target, inlet_id, @@ -260,13 +257,15 @@ def __init__(self, source, outlet_id, target, inlet_id, self.local_offdesign = False self.printout = True + self.force_state = None + self.good_starting_values = None + # set default values for kwargs self.property_data = self.get_parameters() self.parameters = { k: v for k, v in self.get_parameters().items() if hasattr(v, "func") and v.func is not None } - self.state = dc_simple() self.property_data0 = [x + '0' for x in self.property_data.keys()] self.__dict__.update(self.property_data) self.mixing_rule = None @@ -407,14 +406,12 @@ def set_attr(self, **kwargs): elif key in self.property_data or key in self.property_data0: self._parameter_specification(key, kwargs[key]) - elif key == 'state': - if kwargs[key] in ['l', 'g']: - self.state.set_attr(val=kwargs[key], is_set=True) - elif kwargs[key] is None: - self.state.set_attr(is_set=False) + elif key == 'force_state': + if kwargs[key] in ['l', 'g', None]: + self.force_state = kwargs[key] else: msg = ( - 'Keyword argument "state" must either be ' + 'Keyword argument "force_state" must either be ' '"l" or "g" or be None.' ) logger.error(msg) @@ -459,6 +456,9 @@ def set_attr(self, **kwargs): elif key == "mixing_rule": self.mixing_rule = kwargs[key] + elif key == "good_starting_values": + self.good_starting_values = kwargs[key] + # invalid keyword else: msg = 'Connection has no attribute ' + key + '.' @@ -488,7 +488,12 @@ def _fluid_specification(self, key, value): self.fluid.back_end[fluid] = back_end elif key == "fluid0": - self.fluid.val0.update(value) + for fluid, fraction in value.items(): + if "::" in fluid: + back_end, fluid = fluid.split("::") + else: + back_end = None + self.fluid.val0.update({fluid:fraction}) elif key == "fluid_engines": self.fluid.engine = value @@ -496,6 +501,9 @@ def _fluid_specification(self, key, value): elif key == "fluid_balance": self.fluid_balance.is_set = value + elif key == "fluid_coefs": + self.fluid.fluid_coefs = value + else: msg = f"Connections do not have an attribute named {key}" logger.error(msg) @@ -585,8 +593,6 @@ def _serialize(self): data = self.get_attr(k) export.update({k: data._serialize()}) - export.update({"state": self.state._serialize()}) - return {self.label: export} @staticmethod @@ -594,7 +600,7 @@ def _serializable(): return [ "source_id", "target_id", "design_path", "design", "offdesign", "local_design", "local_design", - "printout", "mixing_rule" + "printout", "mixing_rule", "good_starting_values", "force_state" ] def _create_fluid_wrapper(self): @@ -610,7 +616,11 @@ def _create_fluid_wrapper(self): else: self.fluid.back_end[fluid] = None - self.fluid.wrapper[fluid] = self.fluid.engine[fluid](fluid, back_end) + if self.fluid.engine[fluid].__name__ == 'MyWrapper': + self.fluid.wrapper[fluid] = self.fluid.engine[fluid](fluid, back_end, coefs=self.fluid.fluid_coefs) + else: + self.fluid.fluid_coefs[fluid] = None + self.fluid.wrapper[fluid] = self.fluid.engine[fluid](fluid, back_end) def preprocess(self): self.num_eq = 0 @@ -652,23 +662,22 @@ def simplify_specifications(self): if not self.h.is_set and self.p.is_set: if self.T.is_set: - self.h.val_SI = h_mix_pT(self.p.val_SI, self.T.val_SI, self.fluid_data, self.mixing_rule) + self.h.val_SI = h_mix_pT(self.p.val_SI, self.T.val_SI, self.fluid_data, self.mixing_rule, self.force_state) self.h._solved = True self.T._solved = True elif self.Td_bp.is_set: - T_sat = T_sat_p(self.p.val_SI, self.fluid_data) - self.h.val_SI = h_mix_pT(self.p.val_SI, T_sat + self.Td_bp.val, self.fluid_data) + T_sat = T_sat_p(self.p.val_SI, self.fluid_data, self.mixing_rule) + self.h.val_SI = h_mix_pT(self.p.val_SI, T_sat + self.Td_bp.val, self.fluid_data, self.force_state) self.h._solved = True self.Td_bp._solved = True elif self.x.is_set: - self.h.val_SI = h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + self.h.val_SI = h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data, self.mixing_rule) self.h._solved = True self.x._solved = True - elif not self.h.is_set and not self.p.is_set: if self.T.is_set and self.x.is_set: - self.p.val_SI = p_sat_T(self.T.val_SI, self.fluid_data) - self.h.val_SI = h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + self.p.val_SI = p_sat_T(self.T.val_SI, self.fluid_data, self.mixing_rule) + self.h.val_SI = h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data, self.mixing_rule) self.T._solved = True self.x._solved = True self.p._solved = True @@ -741,23 +750,23 @@ def primary_ref_deriv(self, k, **kwargs): def calc_T(self, T0=None): if T0 is None: T0 = self.T.val_SI - return T_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=T0) + return T_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=T0, force_state=self.force_state) def T_func(self, k, **kwargs): - self.residual[k] = self.calc_T() - self.T.val_SI + self.residual[k] = self.calc_T(T0=self.T.val_SI) - self.T.val_SI def T_deriv(self, k, **kwargs): if self.p.is_var: self.jacobian[k, self.p.J_col] = ( - dT_mix_dph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, self.T.val_SI) + dT_mix_dph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, self.T.val_SI, force_state=self.force_state) ) if self.h.is_var: self.jacobian[k, self.h.J_col] = ( - dT_mix_pdh(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, self.T.val_SI) + dT_mix_pdh(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, self.T.val_SI, force_state=self.force_state) ) for fluid in self.fluid.is_var: self.jacobian[k, self.fluid.J_col[fluid]] = dT_mix_ph_dfluid( - self.p.val_SI, self.h.val_SI, fluid, self.fluid_data, self.mixing_rule + self.p.val_SI, self.h.val_SI, fluid, self.fluid_data, self.mixing_rule, force_state=self.force_state ) def T_ref_func(self, k, **kwargs): @@ -793,7 +802,7 @@ def calc_viscosity(self, T0=None): def calc_vol(self, T0=None): try: - return v_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=T0) + return v_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=T0, force_state=self.force_state) except NotImplementedError: return np.nan @@ -837,17 +846,17 @@ def v_ref_deriv(self, k, **kwargs): def calc_x(self): try: - return Q_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data) + return Q_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, self.force_state) except NotImplementedError: return np.nan def x_func(self, k, **kwargs): # saturated steam fraction - self.residual[k] = self.h.val_SI - h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + self.residual[k] = self.h.val_SI - h_mix_pQ(self.p.val_SI, self.x.val_SI, self.fluid_data, self.mixing_rule) def x_deriv(self, k, **kwargs): if self.p.is_var: - self.jacobian[k, self.p.J_col] = -dh_mix_dpQ(self.p.val_SI, self.x.val_SI, self.fluid_data) + self.jacobian[k, self.p.J_col] = -dh_mix_dpQ(self.p.val_SI, self.x.val_SI, self.fluid_data, self.mixing_rule) if self.h.is_var: self.jacobian[k, self.h.J_col] = 1 @@ -889,7 +898,7 @@ def fluid_balance_deriv(self, k, **kwargs): def calc_s(self): try: - return s_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=self.T.val_SI) + return s_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data, self.mixing_rule, T0=self.T.val_SI, force_state=self.force_state) except NotImplementedError: return np.nan @@ -904,11 +913,12 @@ def solve(self, increment_filter): data.deriv(k, **data.func_params) def calc_results(self): - self.T.val_SI = self.calc_T() + if not self.T.is_set: + self.T.val_SI = self.calc_T() number_fluids = get_number_of_fluids(self.fluid_data) _converged = True - if number_fluids > 1: - h_from_T = h_mix_pT(self.p.val_SI, self.T.val_SI, self.fluid_data, self.mixing_rule) + if number_fluids > 1 and not "HEOS" in [self.fluid_data[f]["wrapper"].back_end for f in self.fluid_data] and not "Water" in [self.fluid_data[f]["wrapper"].fluid for f in self.fluid_data]: + h_from_T = h_mix_pT(self.p.val_SI, self.T.val_SI, self.fluid_data, self.mixing_rule, force_state=self.force_state) if abs(h_from_T - self.h.val_SI) > ERR ** .5: self.T.val_SI = np.nan self.vol.val_SI = np.nan @@ -942,7 +952,7 @@ def calc_results(self): if not self.Td_bp.is_set: self.Td_bp.val_SI = self.calc_Td_bp() except ValueError: - self.x.val_SI = np.nan + self.Td_bp.val_SI = np.nan if _converged: self.vol.val_SI = self.calc_vol() @@ -978,7 +988,7 @@ def check_enthalpy_bounds(self, fluid): # enthalpy try: hmin = self.fluid.wrapper[fluid].h_pT( - self.p.val_SI, self.fluid.wrapper[fluid]._T_min + 1e-1 + self.p.val_SI, self.fluid.wrapper[fluid]._T_min + 1e-1,force_state=self.force_state ) except ValueError: f = 1.05 @@ -996,7 +1006,7 @@ def check_enthalpy_bounds(self, fluid): T = self.fluid.wrapper[fluid]._T_max while True: try: - hmax = self.fluid.wrapper[fluid].h_pT(self.p.val_SI, T) + hmax = self.fluid.wrapper[fluid].h_pT(self.p.val_SI, T, force_state=self.force_state) break except ValueError as e: T *= 0.99 @@ -1009,18 +1019,18 @@ def check_enthalpy_bounds(self, fluid): def check_two_phase_bounds(self, fluid): - if (self.Td_bp.val_SI > 0 or (self.state.val == 'g' and self.state.is_set)): + if (self.Td_bp.val_SI > 0 or self.force_state == 'g'): h = self.fluid.wrapper[fluid].h_pQ(self.p.val_SI, 1) if self.h.val_SI < h: self.h.val_SI = h * 1.01 logger.debug(self._property_range_message('h')) - elif (self.Td_bp.val_SI < 0 or (self.state.val == 'l' and self.state.is_set)): + elif (self.Td_bp.val_SI < 0 or self.force_state == 'l'): h = self.fluid.wrapper[fluid].h_pQ(self.p.val_SI, 0) if self.h.val_SI > h: self.h.val_SI = h * 0.99 logger.debug(self._property_range_message('h')) - def check_temperature_bounds(self): + def check_temperature_bounds(self, iter): r""" Check if temperature is within user specified limits. @@ -1029,14 +1039,21 @@ def check_temperature_bounds(self): c : tespy.connections.connection.Connection Connection to check fluid properties. """ - Tmin = max( - [w._T_min for f, w in self.fluid.wrapper.items() if self.fluid.val[f] > ERR] - ) * 1.01 - Tmax = min( - [w._T_max for f, w in self.fluid.wrapper.items() if self.fluid.val[f] > ERR] - ) * 0.99 - hmin = h_mix_pT(self.p.val_SI, Tmin, self.fluid_data, self.mixing_rule) - hmax = h_mix_pT(self.p.val_SI, Tmax, self.fluid_data, self.mixing_rule) + Tminlist=[] + Tmaxlist=[] + for f, w in self.fluid.wrapper.items(): + if self.fluid.val[f] > ERR and self.fluid.val[f] < 1-ERR: + Tminlist.append(w._T_min) + Tmaxlist.append(w._T_max) + + if iter < 8: + Tmin = max(Tminlist) * 1.01 + Tmax = min(Tmaxlist) * 0.99 + else: + Tmin = max(Tminlist) * (1+ERR) + Tmax = min(Tmaxlist) * (1-ERR) + hmin = h_mix_pT(self.p.val_SI, Tmin, self.fluid_data, self.mixing_rule, force_state=self.force_state) + hmax = h_mix_pT(self.p.val_SI, Tmax, self.fluid_data, self.mixing_rule, force_state=self.force_state) if self.h.val_SI < hmin: self.h.val_SI = hmin diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py index 7c5f22923..bf36fc20d 100644 --- a/src/tespy/networks/network.py +++ b/src/tespy/networks/network.py @@ -33,6 +33,7 @@ from tespy.tools.data_containers import GroupedComponentCharacteristics as dc_gcc from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp from tespy.tools.global_vars import ERR +from tespy.tools.global_vars import component_property_data as cpd from tespy.tools.global_vars import fluid_property_data as fpd # Only require cupy if Cuda shall be used @@ -212,6 +213,10 @@ def set_defaults(self): # standard unit set self.__dict__.update({prop + '_unit': data['SI_unit']}) msg += data['text'] + ': ' + data['SI_unit'] + '\n' + for prop, data in cpd.items(): + # standard unit set + self.__dict__.update({prop + '_unit': data['SI_unit']}) + msg += data['text'] + ': ' + data['SI_unit'] + '\n' # don't need the last newline logger.debug(msg[:-1]) @@ -219,7 +224,7 @@ def set_defaults(self): # generic value range self.m_range_SI = np.array([-1e12, 1e12]) self.p_range_SI = np.array([2e2, 300e5]) - self.h_range_SI = np.array([1e3, 7e6]) + self.h_range_SI = np.array([-5e5, 7e6]) for prop in ['m', 'p', 'h']: limits = self.get_attr(prop + '_range_SI') @@ -270,6 +275,10 @@ def set_attr(self, **kwargs): vol_unit : str Specify the unit for specific volume: 'm3 / kg', 'l / kg'. + + Q_unit : str + Specify the unit for heat flow rate: 'W', 'kW', 'MW'. + """ # unit sets for prop in fpd.keys(): @@ -285,6 +294,24 @@ def set_attr(self, **kwargs): logger.error(msg) raise ValueError(msg) + for prop in cpd.keys(): + unit = prop + '_unit' + if unit in kwargs: + if kwargs[unit] in cpd[prop]['units']: + self.__dict__.update({unit: kwargs[unit]}) + msg = ( + 'Setting ' + cpd[prop]['text'] + + ' unit: ' + kwargs[unit] + '.') + logger.debug(msg) + else: + keys = ', '.join(cpd[prop]['units'].keys()) + msg = ( + 'Allowed units for ' + + cpd[prop]['text'] + ' are: ' + keys) + logger.error(msg) + raise ValueError(msg) + + for prop in ['m', 'p', 'h']: if f'{prop}_range' in kwargs: if isinstance(kwargs[f'{prop}_range'], list): @@ -722,6 +749,17 @@ def create_massflow_and_fluid_branches(self): for start in start_components: self.branches.update(start.start_branch()) + self.branchesNames = {} + msg = ("Branched the following components and connections:") + logger.debug(msg) + for k,v in self.branches.items(): + self.branchesNames[k] = v['components'][0].label + for conn,comp in zip(v['connections'],v['components'][1:]): + #self.branchesNames[k] += " -> " + conn.label + " -> " + comp.label + self.branchesNames[k] += " -> " + comp.label + msg = (self.branchesNames[k]) + logger.debug(msg) + self.massflow_branches = hlp.get_all_subdictionaries(self.branches) self.fluid_branches = {} @@ -734,13 +772,24 @@ def create_fluid_wrapper_branches(self): self.fluid_wrapper_branches = {} mask = self.comps["comp_type"].isin( - ["Source", "CycleCloser", "WaterElectrolyzer", "FuelCell"] + ["Source", "SourceEnergy", "CycleCloser", "WaterElectrolyzer", "FuelCell"] ) start_components = self.comps["object"].loc[mask] for start in start_components: self.fluid_wrapper_branches.update(start.start_fluid_wrapper_branch()) + self.branchesNames = {} + msg = ("Wrapped the following components and connections:") + logger.debug(msg) + for k,v in self.fluid_wrapper_branches.items(): + self.branchesNames[k] = v['components'][0].label + for conn,comp in zip(v['connections'],v['components'][1:]): + #self.branchesNames[k] += " -> " + conn.label + " -> " + comp.label + self.branchesNames[k] += " -> " + comp.label + msg = (self.branchesNames[k]) + logger.debug(msg) + merged = self.fluid_wrapper_branches.copy() for branch_name, branch_data in self.fluid_wrapper_branches.items(): if branch_name not in merged: @@ -758,7 +807,8 @@ def create_fluid_wrapper_branches(self): merged[branch_name]["components"] = list( set(branch_data["components"] + ob_data["components"]) ) - del merged[ob_name] + if merged.get(ob_name,False): + del merged[ob_name] break self.fluid_wrapper_branches = merged @@ -871,10 +921,10 @@ def initialise(self): if self.conns.loc[first_conn.label, "object"] != first_conn: self.create_massflow_and_fluid_branches() self.create_fluid_wrapper_branches() + self.propagate_fluid_wrappers() self.presolve_massflow_topology() self.presolve_fluid_topology() - self.init_set_properties() if self.mode == 'offdesign': @@ -909,6 +959,7 @@ def propagate_fluid_wrappers(self): any_fluids_set = [] engines = {} back_ends = {} + fluid_coefss = {} for c in all_connections: for f in c.fluid.is_set: any_fluids_set += [f] @@ -916,6 +967,8 @@ def propagate_fluid_wrappers(self): engines[f] = c.fluid.engine[f] if f in c.fluid.back_end: back_ends[f] = c.fluid.back_end[f] + if f in c.fluid.fluid_coefs: + fluid_coefss[f] = c.fluid.fluid_coefs[f] mixing_rules = [ c.mixing_rule for c in all_connections @@ -961,7 +1014,8 @@ def propagate_fluid_wrappers(self): c.fluid.engine[f] = engine for f, back_end in back_ends.items(): c.fluid.back_end[f] = back_end - + for f, fluid_coefs in fluid_coefss.items(): + c.fluid.fluid_coefs[f] = fluid_coefs c._create_fluid_wrapper() def presolve_massflow_topology(self): @@ -1068,17 +1122,17 @@ def presolve_fluid_topology(self): # remaining fluids are variable, create wrappers for them all_fluids = main_conn.fluid.val.keys() num_remaining_fluids = len(all_fluids) - len(fixed_fractions) - if num_remaining_fluids == 1: - missing_fluid = list( - main_conn.fluid.val.keys() - fixed_fractions.keys() - )[0] - fixed_fractions[missing_fluid] = 1 - mass_fraction_sum - variable = set() - else: - missing_fluids = ( - main_conn.fluid.val.keys() - fixed_fractions.keys() - ) - variable = {f for f in missing_fluids} + # if num_remaining_fluids == 1: + # missing_fluid = list( + # main_conn.fluid.val.keys() - fixed_fractions.keys() + # )[0] + # fixed_fractions[missing_fluid] = 1 - mass_fraction_sum + # variable = set() + # else: + missing_fluids = ( + main_conn.fluid.val.keys() - fixed_fractions.keys() + ) + variable = {f for f in missing_fluids} else: # fluid mass fraction is 100 %, all other fluids are 0 % @@ -1108,7 +1162,7 @@ def presolve_fluid_topology(self): main_conn.fluid.is_var = variable num_var = len(variable) for f in variable: - main_conn.fluid.val[f]: (1 - mass_fraction_sum) / num_var + main_conn.fluid.val[f] = (1 - mass_fraction_sum) / num_var [c.build_fluid_data() for c in all_connections] for fluid in main_conn.fluid.is_var: @@ -1229,6 +1283,15 @@ def init_design(self): respective :code:`design_path`. In this case, the design values are unset, the offdesign values set. """ + for c in self.comps['object']: + for prop in cpd.keys(): + if c.parameters.get(prop,False): + c.parameters[prop].unit = self.get_attr(prop + '_unit') + if c.parameters[prop].is_set: + # we simply overwrite to begin with.. because all model do not use val_SI + c.parameters[prop].val = hlp.convert_comp_to_SI(prop, c.parameters[prop].val, c.parameters[prop].unit) + # we then convert back again upon solution + # connections self._conn_variables = [] _local_designs = {} @@ -1671,6 +1734,12 @@ def init_properties(self): """ if self.init_path is not None: df = self.init_read_connections(self.init_path) + # read val0 for fluids first, before build_fluid_data below + for c in self.conns['object']: + for key in ['fluid']: + if c.get_attr(key).is_var: + for k,v in c.get_attr(key).val0.items(): + c.get_attr(key).val[k] = v # improved starting values for referenced connections, # specified vapour content values, temperature values as well as # subccooling/overheating and state specification @@ -1717,14 +1786,12 @@ def init_properties(self): # and state specification. These should be recalculated even with # good starting values, for example, when one exchanges enthalpy # with boiling point temperature difference. - if (c.Td_bp.is_set or c.state.is_set) and c.h.is_var: - if ((c.Td_bp.val_SI > 0 and c.Td_bp.is_set) or - (c.state.val == 'g' and c.state.is_set)): + if (c.Td_bp.is_set or c.force_state) and c.h.is_var: + if ((c.Td_bp.val_SI > 0 and c.Td_bp.is_set) or c.force_state == 'g'): h = fp.h_mix_pQ(c.p.val_SI, 1, c.fluid_data) if c.h.val_SI < h: c.h.val_SI = h * 1.001 - elif ((c.Td_bp.val_SI < 0 and c.Td_bp.is_set) or - (c.state.val == 'l' and c.state.is_set)): + elif ((c.Td_bp.val_SI < 0 and c.Td_bp.is_set) or c.force_state == 'l'): h = fp.h_mix_pQ(c.p.val_SI, 0, c.fluid_data) if c.h.val_SI > h: c.h.val_SI = h * 0.999 @@ -1785,11 +1852,19 @@ def init_precalc_properties(self, c): if c.T.is_set: try: - c.h.val_SI = fp.h_mix_pT(c.p.val_SI, c.T.val_SI, c.fluid_data, c.mixing_rule) + c.h.val_SI = fp.h_mix_pT(c.p.val_SI, c.T.val_SI, c.fluid_data, c.mixing_rule, force_state=c.force_state) except ValueError: pass - def init_val0(self, c, key): + if c.T.val0: + if not np.isnan(c.T.val0): + try: + c.h.val_SI = fp.h_mix_pT(c.p.val_SI, c.T.val0 + 273.15, c.fluid_data, c.mixing_rule, force_state=c.force_state) + except ValueError: + pass + + + def init_val0(self, c: con.Connection, key: str): r""" Set starting values for fluid properties. @@ -1847,7 +1922,8 @@ def init_read_connections(base_path): def solve(self, mode, init_path=None, design_path=None, max_iter=50, min_iter=4, init_only=False, init_previous=True, - use_cuda=False, print_results=True, prepare_fast_lane=False): + use_cuda=False, print_results=True, prepare_fast_lane=False, + robust_relaxation=False): r""" Solve the network. @@ -1898,6 +1974,7 @@ def solve(self, mode, init_path=None, design_path=None, """ ## to own function self.new_design = False + self.robust_relaxation = robust_relaxation if self.design_path == design_path and design_path is not None: for c in self.conns['object']: if c.new_design: @@ -2024,12 +2101,20 @@ def solve_loop(self, print_results=True): for self.iter in range(self.max_iter): self.increment_filter = np.absolute(self.increment) < ERR ** 2 self.solve_control() + # self.residual_history = np.append( + # self.residual_history, norm(self.residual) + # ) + + # if self.iterinfo: + # self.iterinfo_body(print_results) + + # must always call this one to add increments residual to the solver + residual_norm = self.iterinfo_body(print_results) + self.residual_history = np.append( - self.residual_history, norm(self.residual) + self.residual_history, residual_norm ) - if self.iterinfo: - self.iterinfo_body(print_results) if ( (self.iter >= self.min_iter - 1 @@ -2160,18 +2245,30 @@ def iterinfo_body(self, print_results=True): fluid = 'NaN' component = 'NaN' - progress_val = -1 + if not self.lin_dep and not np.isnan(residual_norm): + norm_massflow = norm(self.increment[m]) / fpd['m']['units'][self.m_unit] # scale with mass unit + norm_pressure = norm(self.increment[p]) / 1e5 # scale with 1 bar + norm_enthalpy = norm(self.increment[h]) / 1e5 # scale with enthalpy + norm_fluid = norm(self.increment[fl]) / fpd['m']['units'][self.m_unit] # scale with mass unit + norm_component = norm(self.increment[cp]) + + massflow = '{:.2e}'.format(norm_massflow) + pressure = '{:.2e}'.format(norm_pressure) + enthalpy = '{:.2e}'.format(norm_enthalpy) + fluid = '{:.2e}'.format(norm_fluid) + component = '{:.2e}'.format(norm_component) + + residual_norm = norm( + np.append( + residual_norm, + np.array([norm_massflow, norm_pressure, norm_enthalpy, norm_fluid, norm_component]) + ) + ) + progress_val = -1 if not np.isnan(residual_norm): residual = '{:.2e}'.format(residual_norm) - if not self.lin_dep: - massflow = '{:.2e}'.format(norm(self.increment[m])) - pressure = '{:.2e}'.format(norm(self.increment[p])) - enthalpy = '{:.2e}'.format(norm(self.increment[h])) - fluid = '{:.2e}'.format(norm(self.increment[fl])) - component = '{:.2e}'.format(norm(self.increment[cp])) - # This should not be hardcoded here. if residual_norm > np.finfo(float).eps * 100: progress_min = np.log(ERR) @@ -2190,20 +2287,21 @@ def iterinfo_body(self, print_results=True): progress = '{:d} %'.format(progress_val) - msg = self.iterinfo_fmt.format( - iter=iter_str, - residual=residual, - progress=progress, - massflow=massflow, - pressure=pressure, - enthalpy=enthalpy, - fluid=fluid, - component=component - ) - logger.progress(progress_val, msg) - if print_results: - print(msg) - return + if self.iterinfo: + msg = self.iterinfo_fmt.format( + iter=iter_str, + residual=residual, + progress=progress, + massflow=massflow, + pressure=pressure, + enthalpy=enthalpy, + fluid=fluid, + component=component + ) + logger.progress(progress_val, msg) + if print_results: + print(msg) + return residual_norm def iterinfo_tail(self, print_results=True): """Print tail of convergence progress.""" @@ -2238,36 +2336,72 @@ def matrix_inversion(self): except np.linalg.linalg.LinAlgError: self.increment = self.residual * 0 + def _limit_increments(self, valmin, valmax, val, increment): + inc_min = valmin - val + inc_max = valmax - val + + if increment < inc_min: + # need to limit the increment + if inc_min < -0.01 * (valmax - valmin): + # if we are not close the the bound we limit it half way to the bound + increment = inc_min / 2 + else: + # othervice we set the increment to the bound + increment = inc_min + + elif increment > inc_max: + # need to limit the increment + if inc_max > 0.01 * (valmax - valmin): + # if we are not close the the bound we limit it half way to the bound + increment = inc_max / 2 + else: + # othervice we set the increment to the bound + increment = inc_max + return increment + def update_variables(self): + + robust_relax = 1 + if self.robust_relaxation: + if self.iter < 2: + robust_relax = 0.1 + elif self.iter < 4: + robust_relax = 0.25 + elif self.iter < 6: + robust_relax = 0.5 + # add the increment for data in self.variables_dict.values(): - if data["variable"] in ["m", "h"]: + if data["variable"] == "m": container = data["obj"].get_attr(data["variable"]) - container.val_SI += self.increment[container.J_col] + increment = self.increment[container.J_col] + container.val_SI += robust_relax * self._limit_increments( + self.m_range_SI[0], self.m_range_SI[1], container.val_SI, increment + ) + elif data["variable"] == "h": + container = data["obj"].get_attr(data["variable"]) + increment = self.increment[container.J_col] + container.val_SI += robust_relax * increment elif data["variable"] == "p": container = data["obj"].p increment = self.increment[container.J_col] + # prevents negative values relax = max(1, -2 * increment / container.val_SI) - container.val_SI += increment / relax + container.val_SI += robust_relax * increment / relax elif data["variable"] == "fluid": container = data["obj"].fluid - container.val[data["fluid"]] += self.increment[ - container.J_col[data["fluid"]] - ] - - if container.val[data["fluid"]] < ERR : - container.val[data["fluid"]] = 0 - elif container.val[data["fluid"]] > 1 - ERR : - container.val[data["fluid"]] = 1 + increment = self.increment[container.J_col[data["fluid"]]] + val = container.val[data["fluid"]] + container.val[data["fluid"]] += robust_relax * self._limit_increments( + 0, 1, val, increment + ) else: - # add increment - data["obj"].val += self.increment[data["obj"].J_col] - - # keep value within specified value range - if data["obj"].val < data["obj"].min_val: - data["obj"].val = data["obj"].min_val - elif data["obj"].val > data["obj"].max_val: - data["obj"].val = data["obj"].max_val + # component variables + increment = self.increment[data["obj"].J_col] + val = data["obj"].val + data["obj"].val += robust_relax * self._limit_increments( + data["obj"].min_val, data["obj"].max_val, val, increment + ) def check_variable_bounds(self): @@ -2334,11 +2468,11 @@ def check_connection_properties(self, c): c.check_enthalpy_bounds(fl) # two-phase related - if (c.Td_bp.is_set or c.state.is_set) and self.iter < 3: + if (c.Td_bp.is_set or c.force_state) and self.iter < 3: c.check_two_phase_bounds(fl) # mixture - elif self.iter < 4 and not c.good_starting_values: + else: #if self.iter < 4 and not c.good_starting_values: # pressure if c.p.is_var: if c.p.val_SI <= self.p_range_SI[0]: @@ -2351,17 +2485,17 @@ def check_connection_properties(self, c): # enthalpy if c.h.is_var: - if c.h.val_SI < self.h_range_SI[0]: + if c.h.val_SI <= self.h_range_SI[0]: c.h.val_SI = self.h_range_SI[0] logger.debug(c._property_range_message('h')) - elif c.h.val_SI > self.h_range_SI[1]: + elif c.h.val_SI >= self.h_range_SI[1]: c.h.val_SI = self.h_range_SI[1] logger.debug(c._property_range_message('h')) # temperature if c.T.is_set: - c.check_temperature_bounds() + c.check_temperature_bounds(self.iter) # mass flow if c.m.val_SI <= self.m_range_SI[0] and c.m.is_var: @@ -2455,6 +2589,15 @@ def postprocessing(self): def process_connections(self): """Process the Connection results.""" for c in self.conns['object']: + + # fluid mass fractions sometimes end up with tiny deviations from + # the bound, e.g. mass fractions of 1e-12 or similar. + for fluid in c.fluid.val: + if c.fluid.val[fluid] > 1 - ERR ** 2: + c.fluid.val[fluid] = 1 + elif c.fluid.val[fluid] < ERR ** 2: + c.fluid.val[fluid] = 0 + c.good_starting_values = True c.calc_results() @@ -2475,6 +2618,12 @@ def process_components(self): cp.calc_parameters() cp.check_parameter_bounds() + for prop in cpd.keys(): + if cp.parameters.get(prop,False): + # we simply overwrite to begin with.. because all model do not use val_SI + cp.parameters[prop].val_SI = cp.parameters[prop].val # delete this when proper use of val_SI is done + cp.parameters[prop].val = hlp.convert_comp_from_SI(prop, cp.parameters[prop].val, cp.parameters[prop].unit) + key = cp.__class__.__name__ for param in self.results[key].columns: p = cp.get_attr(param) diff --git a/src/tespy/networks/network_reader.py b/src/tespy/networks/network_reader.py index ecbe2bbac..f2e81771b 100644 --- a/src/tespy/networks/network_reader.py +++ b/src/tespy/networks/network_reader.py @@ -15,8 +15,6 @@ import json import os -import pandas as pd - from tespy.components import CombustionChamber from tespy.components import CombustionEngine from tespy.components import Compressor @@ -81,7 +79,7 @@ 'WaterElectrolyzer': WaterElectrolyzer, 'Compressor': Compressor, 'Pump': Pump, - 'Turbine': Turbine + 'Turbine': Turbine, } ENGINE_TARGET_CLASSES = { @@ -336,9 +334,11 @@ def construct_components(component, data): container = instances[cp].get_attr(param) if isinstance(container, dc): if isinstance(container, dc_cc): - param_data["char_func"] = CharLine(**param_data["char_func"]) + if 'char_func' in param_data.keys(): + param_data["char_func"] = CharLine(**param_data["char_func"]) elif isinstance(container, dc_cm): - param_data["char_func"] = CharMap(**param_data["char_func"]) + if 'char_func' in param_data.keys(): + param_data["char_func"] = CharMap(**param_data["char_func"]) if isinstance(container, dc_prop): param_data["val0"] = param_data["val"] container.set_attr(**param_data) diff --git a/src/tespy/tools/data_containers.py b/src/tespy/tools/data_containers.py index fae107d4d..167e1ba11 100644 --- a/src/tespy/tools/data_containers.py +++ b/src/tespy/tools/data_containers.py @@ -112,8 +112,14 @@ def set_attr(self, **kwargs): # specify values for key in kwargs: if key in var: - self.__dict__.update({key: kwargs[key]}) - + if key == "split_fluid" and kwargs[key]: + if "::" in kwargs[key]: + _, fluid = kwargs[key].split("::") + else: + fluid = kwargs[key] + self.__dict__.update({key: fluid}) + else: + self.__dict__.update({key: kwargs[key]}) else: msg = ( f"Datacontainer of type {self.__class__.__name__} has no " @@ -284,6 +290,13 @@ class ComponentProperties(DataContainer): max_val : float Maximum value for this attribute, used if attribute is part of the system variables, default: max_val=1e12. + + unit : str + Unit for this property, default: ref=None. + + unit : boolean + Has the unit for this property been specified manually by the user? + default: unit_set=False. """ @staticmethod @@ -298,7 +311,7 @@ def attr(): values. """ return { - 'val': 1, 'val_SI': 0, 'is_set': False, 'd': 1e-4, + 'val': 1, 'val_SI': 0, 'is_set': False, 'd': 1e-4, 'unit': None, 'min_val': -1e12, 'max_val': 1e12, 'is_var': False, 'design': np.nan, 'is_result': False, 'num_eq': 0, 'func_params': {}, 'func': None, 'deriv': None, @@ -312,7 +325,7 @@ def _serialize(self): @staticmethod def _serializable_keys(): return [ - "val", "val_SI", "is_set", "d", "min_val", "max_val", "is_var", + "val", "val_SI", "is_set", "d", "min_val", "max_val", "is_var", "unit" ] @@ -359,6 +372,7 @@ def attr(): 'wrapper': dict(), 'back_end': dict(), 'engine': dict(), + 'fluid_coefs': dict(), "is_var": set(), "J_col": dict(), } @@ -577,3 +591,18 @@ def attr(): def _serialize(self): return {"val": self.val, "is_set": self.is_set} + +class ComponentPropertiesArray(DataContainer): + """ + Data container for arrays. + """ + @staticmethod + def attr(): + """ + """ + return { + 'val': [], 'val_SI': [], 'is_set': False, 'd': 1e-4, + 'min_val': -1e12, 'max_val': 1e12, 'is_var': False, + 'val_ref': 1, 'design': np.nan, 'is_result': False, + 'num_eq': 0, 'func_params': {}, 'func': None, 'deriv': None, + 'latex': None} diff --git a/src/tespy/tools/fluid_properties/functions.py b/src/tespy/tools/fluid_properties/functions.py index f97ce42d1..93cf728e5 100644 --- a/src/tespy/tools/fluid_properties/functions.py +++ b/src/tespy/tools/fluid_properties/functions.py @@ -85,50 +85,58 @@ def calc_chemical_exergy(pamb, Tamb, fluid_data, Chem_Ex, mixing_rule=None, T0=N return EXERGY_CHEMICAL[mixing_rule](pamb, Tamb, fluid_data, Chem_Ex) -def T_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None): +def T_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None, force_state=None): if get_number_of_fluids(fluid_data) == 1: pure_fluid = get_pure_fluid(fluid_data) return pure_fluid["wrapper"].T_ph(p, h) else: + if "Water" in fluid_data and not force_state: + if fluid_data["Water"]["wrapper"].back_end == "HEOS": + Tsat = fluid_data["Water"]["wrapper"].T_sat(p) + T = min([fluid_data[f]["wrapper"]._T_max for f in fluid_data]+[Tsat]) + hL = h_mix_pT(p, T, fluid_data, mixing_rule, force_state='l') + hV = h_mix_pT(p, T, fluid_data, mixing_rule, force_state='g') + if h>hL and h=hV: + return 1.0 + else: + return (h-hL) / (hV-hL) + if force_state == 'l': + return 0.0 + elif force_state == 'g': + return 1.0 + msg = "Saturation function cannot be called on mixtures, unless there is HEOS::Water" raise ValueError(msg) - def p_sat_T(T, fluid_data, mixing_rule=None): if get_number_of_fluids(fluid_data) == 1: pure_fluid = get_pure_fluid(fluid_data) return pure_fluid["wrapper"].p_sat(T) else: - msg = "Saturation function cannot be called on mixtures." + if "Water" in fluid_data: + if fluid_data["Water"]["wrapper"].back_end == "HEOS": + return fluid_data["Water"]["wrapper"].p_sat(T) + msg = "Saturation function cannot be called on mixtures, unless there is HEOS::Water" raise ValueError(msg) @@ -170,7 +200,10 @@ def T_sat_p(p, fluid_data, mixing_rule=None): pure_fluid = get_pure_fluid(fluid_data) return pure_fluid["wrapper"].T_sat(p) else: - msg = "Saturation function cannot be called on mixtures." + if "Water" in fluid_data: + if fluid_data["Water"]["wrapper"].back_end == "HEOS": + return fluid_data["Water"]["wrapper"].T_sat(p) + msg = "Saturation function cannot be called on mixtures, unless there is HEOS::Water" raise ValueError(msg) @@ -181,26 +214,35 @@ def dT_sat_dp(p, fluid_data, mixing_rule=None): return (upper - lower) / (2 * d) -def s_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None): +def s_mix_ph(p, h, fluid_data, mixing_rule=None, T0=None, force_state=None): if get_number_of_fluids(fluid_data) == 1: pure_fluid = get_pure_fluid(fluid_data) return pure_fluid["wrapper"].s_ph(p, h) else: - T = T_mix_ph(p, h , fluid_data, mixing_rule, T0) - return s_mix_pT(p, T, fluid_data, mixing_rule) - - - -def s_mix_pT(p, T, fluid_data, mixing_rule=None): + if "Water" in fluid_data and not force_state: + if fluid_data["Water"]["wrapper"].back_end == "HEOS": + Tsat = fluid_data["Water"]["wrapper"].T_sat(p) + hL = h_mix_pT(p, Tsat, fluid_data, mixing_rule, force_state='l') + hV = h_mix_pT(p, Tsat, fluid_data, mixing_rule, force_state='g') + if h>hL and hhL and h self.AS.p(): + try: + self.AS.update(CP.PT_INPUTS, p, T) + except: + self.AS.update(CP.QT_INPUTS, 0, T) + else: + self.AS.update(CP.QT_INPUTS, 0, T) + else: + if (kwargs.get('force_state', False) == "l") and not (T > self.AS.T_critical()): + self.AS.update(CP.QT_INPUTS, 0, T) + if p > self.AS.p(): + try: + self.AS.update(CP.PT_INPUTS, p, T) + except: + self.AS.update(CP.QT_INPUTS, 0, T) + elif kwargs.get('force_state',False) == "g" and not (T > self.AS.T_critical()): + self.AS.update(CP.QT_INPUTS, 1, T) + if p < self.AS.p(): + try: + self.AS.update(CP.PT_INPUTS, p, T) + except: + self.AS.update(CP.QT_INPUTS, 1, T) + else: + self.AS.update(CP.PT_INPUTS, p, T) + + def h_pT(self, p, T, **kwargs): + self._check_imposed_state(p, T, **kwargs) return self.AS.hmass() def h_QT(self, Q, T): @@ -223,8 +256,8 @@ def d_ph(self, p, h): self.AS.update(CP.HmassP_INPUTS, h, p) return self.AS.rhomass() - def d_pT(self, p, T): - self.AS.update(CP.PT_INPUTS, p, T) + def d_pT(self, p, T, **kwargs): + self._check_imposed_state(p, T, **kwargs) return self.AS.rhomass() def d_QT(self, Q, T): @@ -235,16 +268,16 @@ def viscosity_ph(self, p, h): self.AS.update(CP.HmassP_INPUTS, h, p) return self.AS.viscosity() - def viscosity_pT(self, p, T): - self.AS.update(CP.PT_INPUTS, p, T) + def viscosity_pT(self, p, T, **kwargs): + self._check_imposed_state(p, T, **kwargs) return self.AS.viscosity() def s_ph(self, p, h): self.AS.update(CP.HmassP_INPUTS, h, p) return self.AS.smass() - def s_pT(self, p, T): - self.AS.update(CP.PT_INPUTS, p, T) + def s_pT(self, p, T, **kwargs): + self._check_imposed_state(p, T, **kwargs) return self.AS.smass() @@ -321,7 +354,7 @@ def h_pQ(self, p, Q): def h_ps(self, p, s): return self.AS(P=p / 1e6, s=s / 1e3).h * 1e3 - def h_pT(self, p, T): + def h_pT(self, p, T, **kwargs): return self.AS(P=p / 1e6, T=T).h * 1e3 def h_QT(self, Q, T): @@ -421,7 +454,7 @@ def T_ph(self, p, h): def T_ps(self, p, s): return self.AS.T(p=p, s=s)[0] - def h_pT(self, p, T): + def h_pT(self, p, T, **kwargs): return self.AS.h(p=p, T=T)[0] def h_ps(self, p, s): diff --git a/src/tespy/tools/global_vars.py b/src/tespy/tools/global_vars.py index 8cd773f1a..d13152a27 100644 --- a/src/tespy/tools/global_vars.py +++ b/src/tespy/tools/global_vars.py @@ -14,13 +14,76 @@ gas_constants = {} gas_constants['uni'] = 8.314462618 +component_property_data = { + 'Q': { + 'text': 'heat flow', + 'SI_unit': 'W', + 'units': { + 'W': 1, 'kW': 1000, 'MW': 1e6, 'GW': 1e9, 'TW': 1e12, + 'J / s': 1 , 'J / h': 1 / 3.6e3, 'J / y': 1 / (3.6e3*24*365), + 'kJ / s': 1e3, 'kJ / h': 1e3 / 3.6e3, 'kJ / y': 1e3 / (3.6e3*24*365), + 'MJ / s': 1e6, 'MJ / h': 1e6 / 3.6e3, 'MJ / y': 1e6 / (3.6e3*24*365), + 'GJ / s': 1e9, 'GJ / h': 1e9 / 3.6e3, 'GJ / y': 1e9 / (3.6e3*24*365), + 'TJ / s': 1e12, 'TJ / h': 1e12 / 3.6e3, 'TJ / y': 1e12 / (3.6e3*24*365), + 'Wh / s': 3.6e3 , 'Wh / h': 3.6e3 / 3.6e3, 'Wh / y': 3.6e3 / (3.6e3*24*365), + 'kWh / s': 3.6e6, 'kWh / h': 3.6e6 / 3.6e3, 'kWh / y': 3.6e6 / (3.6e3*24*365), + 'MWh / s': 3.6e9, 'MWh / h': 3.6e9 / 3.6e3, 'MWh / y': 3.6e9 / (3.6e3*24*365), + 'GWh / s': 3.6e12, 'GWh / h': 3.6e12 / 3.6e3, 'GWh / y': 3.6e12 / (3.6e3*24*365), + 'TWh / s': 3.6e15, 'TWh / h': 3.6e15 / 3.6e3, 'TWh / y': 3.6e15 / (3.6e3*24*365), + }, + #'latex_eq': r'0 = \dot{m} - \dot{m}_\mathrm{spec}', + #'documentation': {'float_fmt': '{:,.3f}'} + }, + 'kA': { + 'text': 'heat transfer conductance', + 'SI_unit': 'W / K', + 'units': { + 'W / K': 1, 'kW / K': 1000, 'MW / K': 1e6, 'GW / K': 1e9, 'TW / K': 1e12 + }, + #'latex_eq': r'0 = \dot{m} - \dot{m}_\mathrm{spec}', + #'documentation': {'float_fmt': '{:,.3f}'} + }, + 'KPI': { + 'text': 'KPI scaling with Q', + 'SI_unit': 'J / kg', + 'units': { + 'J / kg': 1 , 'J / t': 1 / 1e3, + 'kJ / kg': 1e3 , 'kJ / t': 1e3 / 1e3, + 'MJ / kg': 1e6 , 'MJ / t': 1e6 / 1e3, + 'GJ / kg': 1e9 , 'GJ / t': 1e9 / 1e3, + 'TJ / kg': 1e12 , 'TJ / t': 1e12 / 1e3, + + 'Wh / kg': 3.6e3 , 'Wh / t': 3.6e3 / 1e3, + 'kWh / kg': 3.6e6 , 'kWh / t': 3.6e6 / 1e3, + 'MWh / kg': 3.6e9 , 'MWh / t': 3.6e9 / 1e3, + 'GWh / kg': 3.6e12 , 'GWh / t': 3.6e12 / 1e3, + 'TWh / kg': 3.6e15 , 'TWh / t': 3.6e15 / 1e3, + }, + #'latex_eq': r'0 = \dot{m} - \dot{m}_\mathrm{spec}', + #'documentation': {'float_fmt': '{:,.3f}'} + }, + 'SF': { + 'text': 'species split is a mass flow', + 'SI_unit': 'kg / s', + 'units': { + 'kg / s': 1, 'kg / min': 1 / 60, 'kg / h': 1 / 3.6e3, + 't / h': 1 / 3.6, 'g / s': 1 / 1e3, 't / y': 1e3 / (3600*24*365), 't / s': 1e3 / 1 + }, + #'latex_eq': r'0 = \dot{m} - \dot{m}_\mathrm{spec}', + #'documentation': {'float_fmt': '{:,.3f}'} + } + +} +component_property_data['Q_loss'] = component_property_data['Q'] +component_property_data['Q_total'] = component_property_data['Q'] + fluid_property_data = { 'm': { 'text': 'mass flow', 'SI_unit': 'kg / s', 'units': { - 'kg / s': 1, 'kg / min': 1 / 60, 'kg / h': 1 / 3.6e3, - 't / h': 1 / 3.6, 'g / s': 1 / 1e3 + 'kg / s': 1, 'kg / min': 1 / 60, 'kg / h': 1 / 3.6e3, 'kg / y': 1 / (3600*24*365), + 't / h': 1 / 3.6, 'g / s': 1 / 1e3, 't / y': 1e3 / (3600*24*365), 't / s': 1e3 / 1 }, 'latex_eq': r'0 = \dot{m} - \dot{m}_\mathrm{spec}', 'documentation': {'float_fmt': '{:,.3f}'} diff --git a/src/tespy/tools/helpers.py b/src/tespy/tools/helpers.py index d7dab3f10..8b4eabe78 100644 --- a/src/tespy/tools/helpers.py +++ b/src/tespy/tools/helpers.py @@ -19,6 +19,7 @@ from tespy import __datapath__ from tespy.tools import logger from tespy.tools.global_vars import ERR +from tespy.tools.global_vars import component_property_data from tespy.tools.global_vars import fluid_property_data @@ -109,6 +110,17 @@ def convert_to_SI(property, value, unit): else: return value * fluid_property_data[property]['units'][unit] +def convert_comp_to_SI(property, value, unit): + r""" + Convert a value to its SI value. + """ + return value * component_property_data[property]['units'][unit] + +def convert_comp_from_SI(property, value, unit): + r""" + Convert a value to its SI value. + """ + return value / component_property_data[property]['units'][unit] def convert_from_SI(property, SI_value, unit): r""" diff --git a/tests/test_errors.py b/tests/test_errors.py index b71d53fb2..9199f3bba 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -110,7 +110,7 @@ def test_set_attr_errors(): set_attr_TypeError(conn, local_design=5) set_attr_TypeError(conn, local_offdesign=5) set_attr_TypeError(conn, printout=5) - set_attr_TypeError(conn, state=5) + set_attr_TypeError(conn, force_state=5) set_attr_TypeError(nw, m_range=5) set_attr_TypeError(nw, p_range=5) diff --git a/tests/test_models/test_solar_energy_generating_system.py b/tests/test_models/test_solar_energy_generating_system.py index 5dcfcacbf..3db9b4ba0 100644 --- a/tests/test_models/test_solar_energy_generating_system.py +++ b/tests/test_models/test_solar_energy_generating_system.py @@ -316,10 +316,10 @@ def setup_method(self): c14.set_attr(p=0.29) # preheater pressure values - c19.set_attr(p=14.755, state='l') - c21.set_attr(p=9.9975, state='l') - c23.set_attr(p=8.7012, state='l') - c25.set_attr(state='l') + c19.set_attr(p=14.755, force_state='l') + c21.set_attr(p=9.9975, force_state='l') + c23.set_attr(p=8.7012, force_state='l') + c25.set_attr(force_state='l') c27.set_attr(p=125) c29.set_attr(p=112) @@ -365,8 +365,8 @@ def setup_method(self): # specification of missing parameters c19.set_attr(p=14.755) - c21.set_attr(p=9.9975, state='l') - c23.set_attr(p=8.7012, state='l') + c21.set_attr(p=9.9975, force_state='l') + c23.set_attr(p=8.7012, force_state='l') c27.set_attr(p=125) c29.set_attr(p=112) diff --git a/tests/test_networks/test_network.py b/tests/test_networks/test_network.py index e9c92e52a..4595c8bdd 100644 --- a/tests/test_networks/test_network.py +++ b/tests/test_networks/test_network.py @@ -13,7 +13,6 @@ import os import shutil -import numpy as np from pytest import mark from pytest import raises @@ -199,8 +198,10 @@ def test_Network_missing_data_in_design_case_files(self): def test_Network_missing_data_in_individual_design_case_file(self): """Test for missing data in individual design case files.""" pi = Pipe('pipe', Q=0, pr=0.95, design=['pr'], offdesign=['zeta']) - a = Connection(self.source, 'out1', pi, 'in1', m=1, p=1, T=293.15, - fluid={'water': 1}) + a = Connection( + self.source, 'out1', pi, 'in1', m=1, p=1, T=293.15, + fluid={'water': 1} + ) b = Connection(pi, 'out1', self.sink, 'in1', design_path='tmp2') self.nw.add_conns(a, b) self.nw.solve('design') @@ -224,8 +225,10 @@ def test_Network_missing_data_in_individual_design_case_file(self): def test_Network_missing_connection_in_design_path(self): """Test for missing connection data in design case files.""" pi = Pipe('pipe', Q=0, pr=0.95, design=['pr'], offdesign=['zeta']) - a = Connection(self.source, 'out1', pi, 'in1', m=1, p=1, T=293.15, - fluid={'water': 1}) + a = Connection( + self.source, 'out1', pi, 'in1', m=1, p=1, T=293.15, + fluid={'water': 1} + ) b = Connection(pi, 'out1', self.sink, 'in1') self.nw.add_conns(a, b) self.nw.solve('design') @@ -287,16 +290,20 @@ def setup_Network_individual_offdesign(self): me = Merge('merge', num_in=2) si = Sink('sink') - self.pump1.set_attr(eta_s=0.8, design=['eta_s'], - offdesign=['eta_s_char']) - self.pump2.set_attr(eta_s=0.8, design=['eta_s'], + self.pump1.set_attr( + eta_s=0.8, design=['eta_s'], offdesign=['eta_s_char'] + ) + self.pump2.set_attr( + eta_s=0.8, design=['eta_s'], offdesign=['eta_s_char']) - self.sc1.set_attr(pr=0.95, lkf_lin=3.33, lkf_quad=0.011, A=1252, E=700, - Tamb=20, eta_opt=0.92, design=['pr'], - offdesign=['zeta']) - self.sc2.set_attr(pr=0.95, lkf_lin=3.5, lkf_quad=0.011, A=700, E=800, - Tamb=20, eta_opt=0.92, design=['pr'], - offdesign=['zeta']) + self.sc1.set_attr( + pr=0.95, lkf_lin=3.33, lkf_quad=0.011, A=1252, E=700, Tamb=20, + eta_opt=0.92, design=['pr'], offdesign=['zeta'] + ) + self.sc2.set_attr( + pr=0.95, lkf_lin=3.5, lkf_quad=0.011, A=700, E=800, Tamb=20, + eta_opt=0.92, design=['pr'], offdesign=['zeta'] + ) fl = {'H2O': 1} inlet = Connection(so, 'out1', sp, 'in1', T=50, p=3, fluid=fl) @@ -312,8 +319,10 @@ def setup_Network_individual_offdesign(self): self.sc2_v2 = Connection(self.sc2, 'out1', v2, 'in1', p=3.1, m=0.1) v2_me = Connection(v2, 'out1', me, 'in2') - self.nw.add_conns(inlet, outlet, self.sp_p1, self.p1_sc1, self.sc1_v1, - v1_me, self.sp_p2, self.p2_sc2, self.sc2_v2, v2_me) + self.nw.add_conns( + inlet, outlet, self.sp_p1, self.p1_sc1, self.sc1_v1, + v1_me, self.sp_p2, self.p2_sc2, self.sc2_v2, v2_me + ) def test_individual_design_path_on_connections_and_components(self): """Test individual design path specification.""" @@ -327,7 +336,7 @@ def test_individual_design_path_on_connections_and_components(self): v1_design = self.sc1_v1.v.val_SI zeta_sc1_design = self.sc1.zeta.val - self.sc2_v2.set_attr(T=95, state='l', m=None) + self.sc2_v2.set_attr(T=95, force_state='l', m=None) self.sc1_v1.set_attr(m=0.001, T=None) self.nw.solve('design') self.nw._convergence_check() @@ -336,8 +345,8 @@ def test_individual_design_path_on_connections_and_components(self): zeta_sc2_design = self.sc2.zeta.val self.sc1_v1.set_attr(m=None) - self.sc1_v1.set_attr(design=['T'], offdesign=['v'], state='l') - self.sc2_v2.set_attr(design=['T'], offdesign=['v'], state='l') + self.sc1_v1.set_attr(design=['T'], offdesign=['v'], force_state='l') + self.sc2_v2.set_attr(design=['T'], offdesign=['v'], force_state='l') self.sc2.set_attr(design_path='design2') self.pump2.set_attr(design_path='design2') @@ -395,8 +404,8 @@ def test_local_offdesign_on_connections_and_components(self): self.nw._convergence_check() self.nw.save('design1') - self.sc1_v1.set_attr(design=['T'], offdesign=['v'], state='l') - self.sc2_v2.set_attr(design=['T'], offdesign=['v'], state='l') + self.sc1_v1.set_attr(design=['T'], offdesign=['v'], force_state='l') + self.sc2_v2.set_attr(design=['T'], offdesign=['v'], force_state='l') self.sc1.set_attr(local_offdesign=True, design_path='design1') self.pump1.set_attr(local_offdesign=True, design_path='design1') @@ -412,9 +421,11 @@ def test_local_offdesign_on_connections_and_components(self): # connections and components on side 1 must have switched to offdesign - msg = ('Solar collector outlet temperature must be different from ' + - 'design value ' + str(round(self.sc1_v1.T.design - 273.15, 1)) + - ', is ' + str(round(self.sc1_v1.T.val, 1)) + '.') + msg = ( + 'Solar collector outlet temperature must be different from ' + f'design value {round(self.sc1_v1.T.design - 273.15, 1)}, is ' + f'{round(self.sc1_v1.T.val, 1)}.' + ) assert self.sc1_v1.T.design > self.sc1_v1.T.val, msg msg = "Parameter eta_s_char must be set for pump one." @@ -439,8 +450,8 @@ def test_missing_design_path_local_offdesign_on_connections(self): self.nw._convergence_check() self.nw.save('design1') - self.sc1_v1.set_attr(design=['T'], offdesign=['v'], state='l') - self.sc2_v2.set_attr(design=['T'], offdesign=['v'], state='l') + self.sc1_v1.set_attr(design=['T'], offdesign=['v'], force_state='l') + self.sc2_v2.set_attr(design=['T'], offdesign=['v'], force_state='l') self.sc1.set_attr(local_offdesign=True, design_path='design1') self.pump1.set_attr(local_offdesign=True, design_path='design1') @@ -520,7 +531,10 @@ def test_linear_branch_massflow_presolve(self): b.set_attr(pr=1) self.nwk.solve("design") self.nwk._convergence_check() - variables = [data["obj"].get_attr(data["variable"]) for data in self.nwk.variables_dict.values()] + variables = [ + data["obj"].get_attr(data["variable"]) + for data in self.nwk.variables_dict.values() + ] # no mass flow is variable assert c1.m not in variables assert c2.m not in variables