def __init__(self, workingFluid: Fluid, massFlowRate: float = float('nan'), massFlowFraction: float = float('nan'), constant_c: bool = False): # Not considering flows with multiple fluids, one flow can contain only one fluid self.workingFluid = workingFluid self.massFR = massFlowRate self.massFF = massFlowFraction # Flow analysis setting self.constant_c = constant_c if self.constant_c: assert isNumeric(self.workingFluid.cp) and isNumeric( self.workingFluid.k) self.items = [] # Items is a list of devices and states making up the flow. # If flow is cyclic, items list should start with a state and end with the same state. self._equations = [] self._updatedUnknowns = set() self._initialSolutionComplete = False
def hasDefined(self, propertyName: Union[str, List]) -> bool: """Returns true if a value for the given property is defined.""" if isinstance(propertyName, str): return isNumeric(getattr(self, propertyName)) elif isinstance(propertyName, list): return all( isNumeric(getattr(self, property)) for property in propertyName)
def get_saturationProperties(materialPropertyDF: DataFrame, P: Union[float, int] = float('nan'), T: Union[float, int] = float('nan')) -> Tuple[StatePure, StatePure]: """Returns saturated liquid and vapor states at the provided pressure or temperature for the material whose materialPropertyDF is provided.""" # at least the T or P must be provided if isNumeric(P): satLiq_atP = materialPropertyDF.query('P == {0} and x == 0'.format(P)) if satLiq_atP.empty: # exact state (saturated liquid at P - state denoted "_f") not found satLiq_atP = interpolate_onSaturationCurve(materialPropertyDF, interpolate_by='P', interpolate_at=P, endpoint='f') materialPropertyDF.append(satLiq_atP.get_asDict_allProperties(), ignore_index=True) # append the calculated (interpolated) state to materialProperty table for future use in runtime if needed - won't have to interpolate again else: # if query found direct match (saturated liquid state at pressure), convert DFRow to a StatePure object satLiq_atP = StatePure().init_fromDFRow(satLiq_atP) satVap_atP = materialPropertyDF.query('P == {0} and x == 1'.format(P)) if satVap_atP.empty: # exact state (saturated vapor at P - state denoted "_g") not found satVap_atP = interpolate_onSaturationCurve(materialPropertyDF, interpolate_by='P', interpolate_at=P, endpoint='g') materialPropertyDF.append(satVap_atP.get_asDict_allProperties(), ignore_index=True) # append the calculated (interpolated) state to materialProperty table for future use in runtime if needed - won't have to interpolate again else: # if query found direct match (saturated vapor state at pressure), convert DFRow to a StatePure object satVap_atP = StatePure().init_fromDFRow(satVap_atP) assert satLiq_atP.isFullyDefined() and satVap_atP.isFullyDefined() assert satLiq_atP.x == 0 and satVap_atP.x == 1 assert satLiq_atP.T == satVap_atP.T if isNumeric(T): assert isWithin(satLiq_atP.T, 3, '%', T), 'InputError: Provided saturation temperature and pressure do not match.' return satLiq_atP, satVap_atP elif isNumeric(T): satLiq_atT = materialPropertyDF.query('T == {0} and x == 0'.format(T)) if satLiq_atT.empty: # exact state (saturated liquid at T - state denoted "_f") not found satLiq_atT = interpolate_onSaturationCurve(materialPropertyDF, interpolate_by='T', interpolate_at=T, endpoint='f') materialPropertyDF.append(satLiq_atT.get_asDict_allProperties(), ignore_index=True) # append the calculated (interpolated) state to materialProperty table for future use in runtime if needed - won't have to interpolate again else: # if query found direct match (saturated liquid state at pressure), convert DFRow to a StatePure object satLiq_atT = StatePure().init_fromDFRow(satLiq_atT) satVap_atT = materialPropertyDF.query('T == {0} and x == 1'.format(T)) if satVap_atT.empty: # exact state (saturated vapor at T - state denoted "_g") not found satVap_atT = interpolate_onSaturationCurve(materialPropertyDF, interpolate_by='T', interpolate_at=T, endpoint='g') materialPropertyDF.append(satVap_atT.get_asDict_allProperties(), ignore_index=True) # append the calculated (interpolated) state to materialProperty table for future use in runtime if needed - won't have to interpolate again else: # if query found direct match (saturated vapor state at pressure), convert DFRow to a StatePure object satVap_atT = StatePure().init_fromDFRow(satVap_atT) assert satLiq_atT.isFullyDefined() and satVap_atT.isFullyDefined() assert satLiq_atT.x == 0 and satVap_atT.x == 1 return satLiq_atT, satVap_atT
def infer_constant_pressure(self): """Sets or verifies pressures of end states to be equal in all lines.""" number_ofNumericPvals = sum(1 for state in self.endStates if isNumeric(getattr(state, 'P'))) endStates = self.endStates if number_ofNumericPvals == 2: assert isWithin(endStates[0].P, 3, '%', endStates[1].P) elif number_ofNumericPvals == 1: state_withNonNumericPval = endStates.itemSatisfying( lambda state: not isNumeric(getattr(state, 'P'))) state_withNumericPval = endStates.other(state_withNonNumericPval) state_withNonNumericPval.P = state_withNumericPval.P
def isFullyDefinable(self): definable = False saturated = (0 <= self.x <= 1) if saturated: if sum(1 for property_regular in self._properties_regular if isNumeric(getattr(self, property_regular))) >= 1: # if saturated mixture, values of quality & 1 other intensive property are enough to fully define state definable = True else: if sum(1 for property_regular in self._properties_regular if isNumeric(getattr(self, property_regular))) >= 2: # if not a saturated mixture, values of 2 intensive properties (other than quality) are necessary to define state definable = True return definable
def solve_workDevice(self, device: WorkDevice): """Determines outlet state based on available inlet state using isentropic efficiency.""" # Find the state_out out of the device IN THIS FLOW - work devices may have multiple states_out (e.g. turbines with many extractions for reheat, regeneration). occurrences_ofDevice = [index for index, item in enumerate(self.items) if item is device] states_afterDevice: List[StatePure] = [self.items[index + 1] for index in occurrences_ofDevice if index + 1 < len(self.items)] # state_afterDevice is a StatePure for sure after the check in _check_itemsConsistency # If isentropic efficiency is 100%, set entropy of all endStates of the device is the same. This will help determine some states. if device.eta_isentropic == 1: numeric_s_values = [state.s for state in device.endStates if isNumeric(state.s)] if len(numeric_s_values) > 0: for endState in device.endStates: # use the first numeric s value as reference to validate others against / or set them if not endState.hasDefined('s'): endState.set({'s': numeric_s_values[0]}) # TODO - ERROR PRONE - added for cengel p10-34, 35 self._defineStates_ifDefinable(device.endStates) # if any states became definable with the above process for state_out in states_afterDevice: if device.state_in.hasDefined('s') and device.state_in.hasDefined('h') and state_out.hasDefined('P'): # going to overwrite state_out - TODO: Need to copy in the first time, then verify in subseqs state_out.copy_fromState(apply_isentropicEfficiency(state_in=device.state_in, state_out_ideal=state_out, eta_isentropic=device.eta_isentropic, fluid=self.workingFluid))
def set_or_verify(self, setDict: Dict): for parameterName in setDict: if hasattr(self, parameterName): if not isNumeric(getattr(self, parameterName)): setattr(self, parameterName, setDict[parameterName]) else: assert isWithin(getattr(self, parameterName), 3, '%', setDict[parameterName])
def update(self): """Iterates over the unknown items in each term, checks if they have become numeric, i.e. have a value now whereas they previously didn't. If so, updates the constant factor by multiplying it with the newly determined value and removes it from the unknowns.""" terms_toRemove = [] for termIndex, [term_constantFactor, term_unknowns_attributeAddresses] in enumerate(self.LHS): # Check if coefficient is 0 - then no need to process any of the unknowns since term will be 0 anyways if term_constantFactor == 0: terms_toRemove.append(termIndex) continue # continue to next term, no need to resolve the unknowns of this term since the product will be 0 anyways # Check if any unknowns became known unknowns_toRemove = [] for unknown_attributeAddress in term_unknowns_attributeAddresses: attribute = getattr_fromAddress(*unknown_attributeAddress) if isNumeric(attribute): # object.attribute which had previously been identified as unknown now has a value, add it to the constant factor product and remove from the unknowns self.LHS[termIndex][0] *= attribute # multiply it with the constant factor product unknowns_toRemove.append([termIndex, unknown_attributeAddress]) for termIndex, unknown_attributeAddress in unknowns_toRemove: # remove unknowns which have become known in the end # removing in the end not to tamper with the iteration of the above loop self.LHS[termIndex][1].remove(unknown_attributeAddress) # Move constants to RHS if self.LHS[termIndex][1] == []: # if term has no unknowns, it is a constant, move to RHS self.RHS -= self.LHS[termIndex][0] self.LHS.pop(termIndex) for termIndex in reversed(terms_toRemove): # reversed - otherwise would tamper with indices of items identified for removal self.LHS.pop(termIndex) self._gatherUnknowns()
def infer_common_exitTemperatures(self): """Sets the exit state temperature of all lines to be equal.""" line_exitStates = [line_endStates[1] for line_endStates in self.lines] numeric_exitStateTvals = [ line_exitState.T for line_exitState in line_exitStates if isNumeric(line_exitState.T) ] if len(numeric_exitStateTvals) > 0: for line_exitState in line_exitStates: line_exitState.set_or_verify({'T': numeric_exitStateTvals[0]})
def _set_endStateEntropiesEqual(self, device: WorkDevice): """If isentropic efficiency is 100%, sets entropy of all endStates of the device to be the same. This will help determine some states.""" numeric_s_values = [ state.s for state in device.endStates if isNumeric(state.s) ] if len(numeric_s_values) > 0: for endState in device.endStates: # use the first numeric s value as reference to validate others against / or set them if not endState.hasDefined('s'): endState.set({ 's': numeric_s_values[0] }) # TODO - ERROR PRONE - added for cengel p10-34, 35
def cQuery(self, conditions: Dict) -> DataFrame: """Custom query method - wrapper around the regular DataFrame.query for convenience.""" queryString_components = [] for columnName, columnValue in conditions.items(): if isinstance(columnValue, tuple): if all(isNumeric(value) for value in columnValue): queryString_components.append('{0} <= {1} <= {2}'.format( columnValue[0], columnName, columnValue[1])) elif isinstance(sign := columnValue[0], str) and isNumeric( columnValue[1]): if any(sign == ltSign for ltSign in ['<', '<=', '>', '>=']): queryString_components.append('{0} {1} {2}'.format( columnName, sign, columnValue[0])) else: raise AssertionError( 'Query string could not be resolved.') elif any(isinstance(columnValue, _type) for _type in [float, int]): queryString_components.append('{0} == {1}'.format( columnName, columnValue))
def solve_mixingChamber(self, device: MixingChamber): """Sets or verifies common mixing pressure on all end states. Does mass & heat balances on flows.""" # Infer constant mixing pressure sampleState_withPressure = None for endState in device.endStates: if isNumeric(endState.P): sampleState_withPressure = endState break if sampleState_withPressure is not None: for endState in [ state for state in device.endStates if state is not sampleState_withPressure ]: endState.set_or_verify({'P': sampleState_withPressure.P}) # Construct the equations # m1 + m2 + m3 - m4 = 0 massBalance_LHS = [] for state_in in device.states_in: massBalance_LHS.append((1, (state_in.flow, 'massFF'))) massBalance_LHS.append((-1, (device.state_out.flow, 'massFF'))) massBalance = LinearEquation(LHS=massBalance_LHS, RHS=0) # m1h1 + m2h2 + m3h3 - m4h4 = 0 heatBalance_LHS = [] for state_in in device.states_in: heatBalance_LHS.append( ((state_in.flow, 'massFF'), (state_in, 'h'))) heatBalance_LHS.append( (-1, (device.state_out.flow, 'massFF'), (device.state_out, 'h'))) heatBalance = LinearEquation(LHS=heatBalance_LHS, RHS=0) for equation in [massBalance, heatBalance]: if equation.isSolvable(): solution = equation.solve() unknownAddress = list(solution.keys())[0] setattr_fromAddress(object=unknownAddress[0], attributeName=unknownAddress[1], value=solution[unknownAddress]) self._updatedUnknowns.add(unknownAddress) else: self._equations.append(equation) equation.source = device
def define_StateIGas(state: StateIGas, fluid: 'IdealGas'): """Tries to fill in the properties of an ideal gas state by applying the ideal gas law and looking up state on the provided mpDF.""" if len(available_TDependentProperties := [propertyName for propertyName in fluid.mpDF.mp.availableProperties if propertyName in state._properties_all and isNumeric(getattr(state, propertyName))]) >= 1: refPropt_name = available_TDependentProperties[0] # Try finding exact state on mpDF state_at_refPropt = fluid.mpDF.cq.cQuery({refPropt_name: getattr(state, refPropt_name)}) try: if state_at_refPropt.empty: # Interpolate in mpDF refPropt_valueBelow, refPropt_valueAbove = get_surroundingValues(fluid.mpDF[refPropt_name], value=getattr(state, refPropt_name)) state_at_refPropt_valueBelow = StateIGas().init_fromDFRow(fluid.mpDF.cq.cQuery({refPropt_name: refPropt_valueBelow})) state_at_refPropt_valueAbove = StateIGas().init_fromDFRow(fluid.mpDF.cq.cQuery({refPropt_name: refPropt_valueAbove})) state_at_refPropt = interpolate_betweenPureStates(state_at_refPropt_valueBelow, state_at_refPropt_valueAbove, interpolate_at={refPropt_name: getattr(state, refPropt_name)}) state.copy_fromState(state_at_refPropt) except NeedsExtrapolationError: pass
def apply_IGasLaw(state: StateIGas, R: float): """Uses Ideal Gas Law to find missing properties, if possible. If all variables in the Ideal Gas Law are already defined, checks consistency""" # P mu = R T IGasLaw_allProperties = ['P', 'mu', 'T'] IGasLaw_availableProperties = [propertyName for propertyName in IGasLaw_allProperties if isNumeric(getattr(state, propertyName))] IGasLaw_missingProperties = [propertyName for propertyName in IGasLaw_allProperties if propertyName not in IGasLaw_availableProperties] if (number_ofMissingProperties := len(IGasLaw_missingProperties)) == 1: assert state.isFullyDefinable() missingProperty = IGasLaw_missingProperties[0] if missingProperty == 'P': state.P = (R * to_Kelvin(state.T) / state.mu) elif missingProperty == 'mu': state.mu = (R * to_Kelvin(state.T) / state.P) elif missingProperty == 'T': state.T = to_deg_C(state.P * state.mu / R)
def fullyDefine_StatePure(state: StatePure, mpDF: DataFrame): """Fully defines StatePure objects by looking them up / interpolating on the material property table.""" assert state.isFullyDefinable(), 'State not fully definable: need at least 2 (independent) intensive properties to be known.' availableProperties = state.get_asDict_definedProperties() availablePropertiesNames = list(availableProperties.keys()) non_referencePropertiesNames = [propertyName for propertyName in availablePropertiesNames if propertyName not in ['P', 'T']] P_available, T_available = ('P' in availablePropertiesNames), ('T' in availablePropertiesNames) # DETERMINE PHASE OF SUBSTANCE isSaturatedMixture = None # If quality is provided, phase is inferred if isNumeric(state.x): isSaturatedMixture = (0 <= state.x <= 1) # If quality is not provided, determine phase, then assign / calculate quality else: # Determine phase: Compare provided T to the saturation T at provided P if P_available and T_available: # If both P & T are provided, check if T is above saturation temperature at that P # Using the fact that P & T are not independent for saturated states - substance is saturated at known temperature and pressure saturationTemperature_atP = get_saturationTemperature_atP(mpDF, P=state.P) # This can handle pressures at which no distinct saturation process exists if state.T == saturationTemperature_atP: # state.x needs to be calculated isSaturatedMixture = True elif state.T > saturationTemperature_atP: state.x = 2 isSaturatedMixture = False elif state.T < saturationTemperature_atP: state.x = -1 isSaturatedMixture = False # Determine phase elif P_available or T_available: # Compare provided P / T to critical P / T of substance - Check if there are even saturation states at the P/T to begin with. If there are, couldBe_saturatedMixture. couldBe_saturatedMixture = False if P_available: if state.P > mpDF.mp.criticalPoint.P: isSaturatedMixture = False state.x = 2 else: couldBe_saturatedMixture = True elif T_available: if state.T > mpDF.mp.criticalPoint.T: isSaturatedMixture = False state.x = 2 else: couldBe_saturatedMixture = True # Determine phase: Saturated states do exist at the provided P / T. Check if saturated mixture with P / T and 1 other property if couldBe_saturatedMixture: # Is provided u/h/s/mu between saturation limits at the provided T/P? satLiq_atRef, satVap_atRef = get_saturationProperties(mpDF, P=state.P, T=state.T) # Define lambda function to check if value of a non-reference property (i.e. property other than P/T) is within the saturated mixture limits isWithinSaturationZone = lambda propertyName, propertyValue: getattr(satLiq_atRef, propertyName) <= propertyValue <= getattr(satVap_atRef, propertyName) isSuperheated = lambda propertyName, propertyValue: getattr(satVap_atRef, propertyName) < propertyValue isSubcooled = lambda propertyName, propertyValue: propertyValue < getattr(satLiq_atRef, propertyName) # Check if the first available non-reference property has value within saturation limits isSaturatedMixture = isWithinSaturationZone(non_referencePropertiesNames[0], getattr(state, non_referencePropertiesNames[0])) # All non-reference properties should give the same result - if the first one is found to be within saturation limits, all should be so. assert all(isSaturatedMixture == isWithinSaturationZone(propertyName, getattr(state, propertyName)) for propertyName in non_referencePropertiesNames), 'ThDataError: While defining state {0}, property {1} suggests saturated state (value within saturation limits), but other properties do not.'.format(state, non_referencePropertiesNames[0]) if isSaturatedMixture: # Calculate state.x using the first available non-reference property calcProptName, calcProptValue = non_referencePropertiesNames[0], availableProperties[non_referencePropertiesNames[0]] state.x = (calcProptValue - getattr(satLiq_atRef, calcProptName))/(getattr(satVap_atRef, calcProptName) - getattr(satLiq_atRef, calcProptName)) else: superheated = isSuperheated(non_referencePropertiesNames[0], getattr(state, non_referencePropertiesNames[0])) # Check if first non-ref propt suggests suph, then assert all other non-ref propts to suggest the same assert all(superheated == isSuperheated(propertyName, getattr(state, propertyName)) for propertyName in non_referencePropertiesNames), 'ThDataError: While defining state {0}, property {1} suggests superheated state (value above saturation limits), but other properties do not.'.format(state, non_referencePropertiesNames[0]) if superheated: state.x = 2 else: subcooled = isSubcooled(non_referencePropertiesNames[0], getattr(state, non_referencePropertiesNames[0])) # Check if first non-ref propt suggests subc, then assert all other non-ref propts to suggest the same assert all(subcooled == isSubcooled(propertyName, getattr(state, propertyName)) for propertyName in non_referencePropertiesNames), 'ThDataError: While defining state {0}, property {1} suggests subcooled state (value below saturation limits), but other properties do not.'.format(state, non_referencePropertiesNames[0]) if subcooled: state.x = -1 else: raise AssertionError('Error: While checking if state is saturated using P or T as reference, could not determine if state is subcooled / saturated / superheated.') # Determine phase: Neither P / T of the state provided else: # Determine if saturated or not using properties other than P/T - P/T not available # TODO isSaturatedMixture = False raise FeatureNotAvailableError('State definition with variables other than at least P / T') # By now, it should have been determined whether state is a saturated (mixture) and state.x should have been defined. assert isSaturatedMixture is not None and isNumeric(state.x) # Fully define state: State is saturated (mixture) if isSaturatedMixture: if P_available or T_available: satLiq_atP, satVap_atP = get_saturationProperties(mpDF, P=state.P, T=state.T) # either state.P or state.T has to be known - pass both, it is ok if one is NaN if state.x == 0: return satLiq_atP elif state.x == 1: return satVap_atP else: # saturated mixture with unique quality (not 0 or 1) return interpolate_betweenPureStates(satLiq_atP, satVap_atP, interpolate_at={'x': state.x}) else: # Define saturated state with properties other than P/T pass # Fully define state: State is not saturated (mixture) else: # Set phase_mpDF: section of main mpDF with states of only the same phase if state.x == 2: phase_mpDF = mpDF.cq.suphVaps phase = 'superheated' elif state.x == -1: phase_mpDF = mpDF.cq.subcLiqs phase = 'subcooled' else: # This would be a coding error - can be removed once confidence is established raise AssertionError('Error: Phase of state could not be determined - x value not -1, 0-1 or 2') refPropt1_name, refPropt2_name = availablePropertiesNames[:2] # first 2 available properties used as reference # TODO: If cannot be interpolated with these 2, can try others if provided refPropt1_queryValue, refPropt2_queryValue = [availableProperties[property] for property in [refPropt1_name, refPropt2_name]] refPropts = [(refPropt1_name, refPropt1_queryValue), (refPropt2_name, refPropt2_queryValue)] # Check if exact state available exactState = mpDF.cq.cQuery({refPropt1_name: refPropt1_queryValue, refPropt2_name: refPropt2_queryValue}) if not exactState.empty: if len(exactState.index) == 1: return StatePure().init_fromDFRow(exactState) else: # Found multiple states with same P & T - need to pick one # TODO - Pick one raise AssertionError('NotImplementedError: Multiple states satisfying same conditions found.') # Exact state not available else: # Check if 1D interpolation possible _1d_interpolationCheck = {} # Check if either refPropt1_queryValue or refPropt2_queryValue has data available for refProptCurrent_index, (refProptCurrent_name, refProptCurrent_queryValue) in enumerate(refPropts): states_at_refProptCurrent = phase_mpDF.cq.cQuery({refProptCurrent_name: refProptCurrent_queryValue}) if not states_at_refProptCurrent.empty and len(states_at_refProptCurrent.index) > 1: # there should be more than one state at refProptCurrent to interpolate between # If so, get refProptOther and its interpolation gap (gap between available values) refProptOther_name, refProptOther_queryValue = refPropts[refProptCurrent_index - 1] values_of_refProptOther = states_at_refProptCurrent[refProptOther_name].to_list() try: refProptOther_valueBelow, refProptOther_valueAbove = get_surroundingValues(values_of_refProptOther, refProptOther_queryValue) except NeedsExtrapolationError: _1d_interpolationCheck.update({refProptCurrent_name: {'1D_interpolatable': False, 'gap': None}}) continue gap_betweenValues = abs(refProptOther_valueAbove - refProptOther_valueBelow) _1d_interpolationCheck.update({refProptCurrent_name: {'1D_interpolatable': True, 'gap': gap_betweenValues, 'refProptOther': {'name': refProptOther_name, 'surroundingValues': (refProptOther_valueBelow, refProptOther_valueAbove)}}}) else: _1d_interpolationCheck.update({refProptCurrent_name: {'1D_interpolatable': False, 'gap': None}}) # Pick reference property to hold constant: pick the one where the interpolation interval for the other refPropt is minimum # Future: consider picking one where the gap between query value and an endpoint is the minimum minimumGap = 10**5 # arbitrary large value refPropt_for_minimumGap_in1Dinterpolation = None for refProptCurrent_name in _1d_interpolationCheck: if _1d_interpolationCheck[refProptCurrent_name]['1D_interpolatable']: if (gap_of_refProptCurrent := _1d_interpolationCheck[refProptCurrent_name]['gap']) < minimumGap: minimumGap = gap_of_refProptCurrent refPropt_for_minimumGap_in1Dinterpolation = refProptCurrent_name if refPropt_for_minimumGap_in1Dinterpolation is not None: # 1D INTERPOLATION # At least one refPropt allows 1D interpolation. If multiple does, the one where the other has the minimum interpolation gap has been picked refPropt_name = refPropt_for_minimumGap_in1Dinterpolation refPropt_value = availableProperties[refPropt_name] refProptOther_name = _1d_interpolationCheck[refPropt_name]['refProptOther']['name'] refProptOther_queryValue = availableProperties[refProptOther_name] refProptOther_valueBelow, refProptOther_valueAbove = _1d_interpolationCheck[refPropt_name]['refProptOther']['surroundingValues'] state_with_refProptOther_valueBelow = StatePure().init_fromDFRow(phase_mpDF.cq.cQuery({refPropt_name: refPropt_value, refProptOther_name: refProptOther_valueBelow})) state_with_refProptOther_valueAbove = StatePure().init_fromDFRow(phase_mpDF.cq.cQuery({refPropt_name: refPropt_value, refProptOther_name: refProptOther_valueAbove})) return interpolate_betweenPureStates(state_with_refProptOther_valueBelow, state_with_refProptOther_valueAbove, interpolate_at={refProptOther_name: refProptOther_queryValue}) else: # DOUBLE INTERPOLATION available_refProptPairs = list(phase_mpDF[[refPropt1_name, refPropt2_name]].itertuples(index=False, name=None)) # refPropt1 -> x, refPropt2 -> y xVals = sorted(set(pair[0] for pair in available_refProptPairs)) xVals_available_yVals = {xVal: set(pair[1] for pair in available_refProptPairs if pair[0] == xVal) for xVal in xVals} xVals_less = xVals[: (index := bisect_left(xVals, refPropt1_queryValue))] # list of available xValues less than query value available in states xVals_more = xVals[index:] # same for available xValues more than query value # Iterate over values of x surrounding the queryValue of x (= refPropt1_queryValue) t1 = time() # Strategy: First find 2 states: # one with x value less than refPropt1_queryValue but with y value = refPropt2_queryValue # one with x value more than refPropt1_queryValue but with y value = refPropt2_queryValue # i.e. two states surround the requested state in terms of x. # To find these 2 states, iterate over available xValues more than and less than the x query value. At each iteration, TRY to get the 2 surrounding values of y available for that x. # There may not be 2 values of y available at each x value surrounding the queried y value. In such case, try/except clause continues iteration with a new x value. # Once at an x value, 2 values of y surrounding the y query value are found, interpolate between the states with (xVal, yVal_below) and (xVal, yVal_above) # The outmost for loop does this for both state with x value less than x query value and state with x value more than x query value. states_at_y_queryValue = [] # list of states at which y = refPropt2 value is the query value, but x = refPropt1 value is not the query value. for available_xValList in [reversed(xVals_less), xVals_more]: for xVal in available_xValList: xVal_available_yVals = xVals_available_yVals[xVal] try: yVal_below, yVal_above = get_surroundingValues(xVal_available_yVals, refPropt2_queryValue) except NeedsExtrapolationError: continue state_at_xVal_less_yVal_below = StatePure().init_fromDFRow(phase_mpDF.cq.cQuery({refPropt1_name: xVal, refPropt2_name: yVal_below})) state_at_xVal_less_yVal_above = StatePure().init_fromDFRow(phase_mpDF.cq.cQuery({refPropt1_name: xVal, refPropt2_name: yVal_above})) states_at_y_queryValue.append(interpolate_betweenPureStates(state_at_xVal_less_yVal_below, state_at_xVal_less_yVal_above, interpolate_at={refPropt2_name: refPropt2_queryValue})) break if len(states_at_y_queryValue) == 2: t2 = time() print('TimeNotification: 2DInterpolation - Time to iteratively find smallest suitable interpolation interval: {0} seconds'.format((t2 - t1) / 1000)) return interpolate_betweenPureStates(states_at_y_queryValue[0], states_at_y_queryValue[1], interpolate_at={refPropt1_name: refPropt1_queryValue}) else: # 2 states to interpolate between could not be found print('ThPrNotification: 2DInterpolation not successful.') if state.x == -1 and T_available and P_available: # SATURATED LIQUID APPROXIMATION AT SAME TEMPERATURE FOR SUBCOOLED LIQUIDS print('ThPrNotification: Applying saturated liquid approximation for subcooled liquid state.') satLiq_atT = get_saturationProperties(mpDF, T=state.T)[0] # returns [satLiq, satVap], pick first # should provide full mpDF and not phase_mpDF - if phase is subcooled, won't find saturated states (x=0) in its phase_mpDF toReturn = satLiq_atT toReturn.P = state.P # Equation 3-8 toReturn.h = toReturn.h + toReturn.mu * (state.P - satLiq_atT.P) # Equation 3-9 return toReturn else: raise NeedsExtrapolationError('DataError InputError: No {0} states with values of {1} lower or higher than the query value of {2} are available in the data table.'.format(phase.upper(), refPropt1_name, refPropt1_queryValue))
def solve(self): # Steps for initialization if not self._initialSolutionComplete: self._convertStates_toFlowPoints( ) # At the cycle level, states become FlowPoints, i.e. states that are aware of the flows they are in. self._deviceDict = self.get_deviceDict() self._regerator_solutionSetups = {} if Regenerator in self._deviceDict: for regenerator in self._deviceDict[Regenerator]: self._regerator_solutionSetups[regenerator] = False # 2 separate loops - device endStates may be from different flows. First set endState references, then work on each device for each flow they are in. for flow in self.flows: flow._set_devices_endStateReferences() for flow in self.flows: flow.solve() # Get updated unknowns (i.e. defined states) from flow solution # TODO ####################################################################### # Identify areas where flows interact, e.g. heat exchangers or flow connections self.intersections = self._get_intersections() if not all(flow.isFullyDefined() for flow in self.flows): for device in self.intersections: self._solveIntersection( device ) # constructs equations for intersections & adds to the pool # Review intersections, construct equations & attempt to solve - if there are undefined states around them # This process needs to be done only once since equations need to be constructed once. Then they are added to the _equations pool. # intersections_attempted_toSolve = [] # list of intersection devices on which the _solveIntersection() has been run - keeping track not to run the _solveIntersection twice on the same device # for state in self.get_undefinedStates(): # surroundingDevices = state.flow.get_surroundingItems(state) # for surroundingDevice in surroundingDevices: # if surroundingDevice in self.intersections and surroundingDevice not in intersections_attempted_toSolve: # self._solveIntersection(surroundingDevice) # intersections_attempted_toSolve.append(surroundingDevice) self._add_net_sPower_relation() self._add_sHeat_relation() self._add_netPowerBalance() self._add_Q_in_relation() if self.type == 'power': self._add_efficiency_relation() elif self.type == 'refrigeration': self._add_COP_relation() self._add_massFlowRelations() updateEquations( self._equations, self._updatedUnknowns ) # updating all equations in case _solveIntersection() above found any of their unknowns self._initialSolutionComplete = True # Initialization steps completed. # Quick and dirty regenerator solution - need to form heat balance equation only once, check each time if equation formed for device. if Regenerator in self._deviceDict: for regenerator in self._deviceDict[Regenerator]: if self._regerator_solutionSetups[regenerator] == False: solution_setup = self.solve_regenerator(regenerator) self._regerator_solutionSetups[ regenerator] = solution_setup for deviceType in [Combustor, GasReheater]: if deviceType in self._deviceDict: for device in self._deviceDict[Combustor]: if isNumeric(device.sHeatSupplied): self._add_sHeatSupplied_relation(device) # Review flow devices again in case some properties of some of their endStates became known above for flow in self.flows: flow.solve() updateEquations(self._equations, self._updatedUnknowns) updatedUnknowns = solve_solvableEquations(self._equations) self._updatedUnknowns.union(updatedUnknowns) updateEquations(self._equations, self._updatedUnknowns) updatedUnknowns = solve_combination_ofEquations(self._equations, number_ofEquations=2) self._updatedUnknowns.union(updatedUnknowns) updateEquations(self._equations, self._updatedUnknowns) updatedUnknowns = solve_combination_ofEquations(self._equations, number_ofEquations=3) self._updatedUnknowns.union(updatedUnknowns) updateEquations(self._equations, self._updatedUnknowns)
def get_mu_R(self, state: StateIGas): assert isNumeric(state.T) return state.T / self.get_P_r(state)
def get_P_r(self, state: StateIGas): assert isNumeric(state.s0) return exp(state.s0 / self.workingFluid.R)
def copy_fromState(self, referenceState: 'StatePure'): for propertyName in self._properties_all: if isNumeric( referenceValue := getattr(referenceState, propertyName)): setattr(self, propertyName, referenceValue) def copy_or_verify_fromState(self, referenceState: 'StatePure', pTolerance: float = 3): """Copies property values from the provided reference state. If property already has a value defined, compares it to the one desired to be assigned, raises error if values do not match. If the values match, still copies the value from the referenceState - decimals might change.""" for propertyName in self._properties_all: if isNumeric( referenceValue := getattr(referenceState, propertyName)): if not isNumeric(getattr(self, propertyName)): setattr(self, propertyName, referenceValue) else: # property has a value defined if not isWithin(getattr(self, propertyName), 3, '%', referenceValue): raise AssertionError def set(self, setDict: Dict): """Sets values of the properties to the values provided in the dictionary.""" for parameterName in setDict: if parameterName in self._properties_all: setattr(self, parameterName, setDict[parameterName]) def set_or_verify(self, setDict: Dict, percentDifference: float = 3): for parameterName in setDict:
def solve_regenerator(self, device: Regenerator): if all( isNumeric(line[0].T) for line in device.lines ): # Need inlet temperatures of both lines to determine in which direction heat will flow warmLine, coldLine = device.lines if device.lines[1][0].T > device.lines[0][ 0].T: # state_in of device.lines[1] coldLine, warmLine = device.lines warm_in, warm_out = warmLine cold_in, cold_out = coldLine assert warm_in.flow.constant_c == cold_in.flow.constant_c, 'solve_regenerator: Flows of the warm and cold lines have different constant_c settings! Not allowed.' constant_c = warm_in.flow.constant_c if device.counterFlow_commonColdTemperature: warm_out.set_or_verify({'T': cold_in.T}) heatBalance_LHS = [] # warm_mFF*(warm_in.h - warm_out.h)*effectiveness = cold_mFF*(cold_out.h - cold_in.h) # warm_mFF*(warm_in.h - warm_out.h)*effectiveness + cold_mFF*(cold_in.h - cold_out.h) = 0 if constant_c: assert isNumeric(warm_in.flow.workingFluid.cp) heatBalance_LHS.append( ((device.effectiveness), (warm_in.flow, 'massFF'), (warm_in.flow.workingFluid.cp), (warm_in, 'T'))) heatBalance_LHS.append( ((device.effectiveness), (-1), (warm_out.flow, 'massFF'), (warm_out.flow.workingFluid.cp), (warm_out, 'T'))) heatBalance_LHS.append( ((cold_in.flow, 'massFF'), (cold_in.flow.workingFluid.cp), (cold_in, 'T'))) heatBalance_LHS.append( ((-1), (cold_out.flow, 'massFF'), (cold_out.flow.workingFluid.cp), (cold_out, 'T'))) else: heatBalance_LHS.append( ((device.effectiveness), (warm_in.flow, 'massFF'), (warm_in, 'h'))) heatBalance_LHS.append( ((device.effectiveness), (-1), (warm_out.flow, 'massFF'), (warm_out, 'h'))) heatBalance_LHS.append( ((cold_in.flow, 'massFF'), (cold_in, 'h'))) heatBalance_LHS.append( ((-1), (cold_out.flow, 'massFF'), (cold_out, 'h'))) heatBalance = LinearEquation(LHS=heatBalance_LHS, RHS=0) if heatBalance.isSolvable( ): # if solvable by itself, there is only one unknown solution = heatBalance.solve() unknownAddress = list(solution.keys())[0] setattr_fromAddress(object=unknownAddress[0], attributeName=unknownAddress[1], value=solution[unknownAddress]) self._updatedUnknowns.add(unknownAddress) else: self._equations.append(heatBalance) heatBalance.source = device return True else: return False
def hasDefined(self, propertyName: str) -> bool: """Returns true if a value for the given property is defined.""" return isNumeric(getattr(self, propertyName))
def organizeTerms_fromOriginal(self): """Iterates over the LHS terms provided in the **original equation description**, replaces variables with their values if they are known, moves constants to the RHS.""" self.LHS = [] # LHS: [ ] # LHS: [ term1, term2, term3 ] # LHS: [ ( ), ( ), ( ) ] # LHS: [ ( item1, item2, item3 ), term2, term3 ] # _______________ _______________ __ # LHS: [ ( (obj1, 'attr1'), (obj2, 'attr2'), 20 ), term2, term3 ] # Expand brackets def someTermsHaveListItems(): for term in self._LHS_original: if any(isinstance(item, list) for item in term): return True return False while someTermsHaveListItems(): for termIndex, Term in enumerate(self._LHS_original): newTerms_toAdd = [] listItems_inTerm = [item for item in Term if isinstance(item, list)] hasListItems = len(listItems_inTerm) > 0 for listItem_inTerm in listItems_inTerm: otherItems_inTerm = tuple(item for item in Term if item is not listItem_inTerm) # otherItems_inTerm are all multiplied items for term in listItem_inTerm: assert isinstance(term, tuple) # isolated term in format (const, [unknowns]) # coeff unknown addresses unpacked from unknowns list newTerms_toAdd.append( otherItems_inTerm + (term[0],) + tuple(unknownAddress for unknownAddress in term[1]) ) break # process one list item in the term at once if hasListItems: for newTerm_toAdd in reversed(newTerms_toAdd): self._LHS_original.insert(termIndex, newTerm_toAdd) self._LHS_original.remove(Term) break # process one term and break # Brackets expanded. for term in self._LHS_original: # each term is a tuple as all brackets expanded above constantFactors = [] unknownFactors = [] for item in term: # items within the term are to be multiplied if isinstance(item, tuple): # item is an object.attribute address, in form (object, 'attribute') assert isinstance(item[1], str) attribute = getattr_fromAddress(*item) if isNumeric(attribute): # If the object.attribute has a value, add it to constant factors constantFactors.append(attribute) else: # If the object.attribute does not have a value, it is an unknownFactor # TODO - the following check may be accommodated to have equation solved if the high-power term is found in another equation assert item not in unknownFactors, 'LinearEquationError: Same unknown appears twice in one term, i.e. a higher power of the unknown encountered - not a linear equation!' unknownFactors.append(item) elif any(isinstance(item, _type) for _type in [float, int]): # item is a number, i.e. a coefficient assert isNumeric(item) constantFactors.append(item) constantFactor = 1 for factor in constantFactors: constantFactor *= factor if len(unknownFactors) != 0: # term has an unknown, e.g. term is in form of "6*x" self.LHS.append([constantFactor, unknownFactors]) else: # term does not have an unknown, e.g. term is in form "6" self.RHS -= constantFactor # move constant term to the RHS self._gatherUnknowns()
def _solveDevice(self, device: Device): endStates = device.endStates if isinstance(device, WorkDevice): # Apply isentropic efficiency relations to determine outlet state self.solve_workDevice(device) # if not self._initialSolutionComplete: # the below processes do not need to be done in each flow solution iteration, but only for the initial one if isinstance(device, HeatDevice): # Setting end state pressures to be the same if device._infer_constant_pressure: device.infer_constant_pressure() if isinstance( device, ReheatBoiler): # reheat boilers can have multiple lines. # Setting up fixed exit temperature if inferring exit temperature from one exit state if device._infer_fixed_exitT: device.infer_fixed_exitT() elif isinstance(device, Intercooler): if device.coolTo == 'ideal': # Cool to the temperature of the compressor inlet state assert isinstance( (compressorBefore := self.get_itemRelative( device, -2)), Compressor ) # before intercooler, there should be compressor exit state, and then a compressor device.state_out.set_or_verify( {'T': compressorBefore.state_in.T}) else: # Cool to specified temperature assert isNumeric(device.coolTo) device.state_out.set_or_verify({'T': device.coolTo}) elif isinstance(device, GasReheater): if device.heatTo == 'ideal': # Heat to the temperature of the turbine inlet state assert isinstance( (turbineBefore := self.get_itemRelative(device, -2)), Turbine) device.state_out.set_or_verify( {'T': turbineBefore.state_in.T}) elif device.heatTo == 'heatSupplied': if not self._initialSolutionComplete: assert isNumeric(device.sHeatSupplied) if not self.constant_c: sHeatSuppliedRelation = LinearEquation( LHS=[(1, (device.state_out, 'h')), (-1, (device.state_in, 'h'))], RHS=device.sHeatSupplied) else: sHeatSuppliedRelation = LinearEquation( LHS=[(1, self.workingFluid.cp, (device.state_out, 'T')), (-1, self.workingFluid.cp, (device.state_in, 'T'))], RHS=device.sHeatSupplied) self._equations.append(sHeatSuppliedRelation) if sHeatSuppliedRelation.isSolvable(): solution = sHeatSuppliedRelation.solve() unknownAddress = list(solution.keys())[0] setattr_fromAddress( object=unknownAddress[0], attributeName=unknownAddress[1], value=solution[unknownAddress]) self._updatedUnknowns.add(unknownAddress) else: sHeatSuppliedRelation.source = device self._equations.append(sHeatSuppliedRelation) else: # Heat to specified temperature assert isNumeric(device.heatTo) device.state_out.set_or_verify({'T': device.heatTo}) elif isinstance(device, HeatExchanger): # Setting end state pressures along the same line if pressures is assumed constant along each line if device._infer_constant_linePressures: device.infer_constant_linePressures() # Setting temperature of exit states equal for all lines # TODO - not the ideal place - inter-flow operation should ideally be in cycle scope if device._infer_common_exitTemperatures: device.infer_common_exitTemperatures() elif isinstance(device, MixingChamber): # Setting pressures of all in / out flows to the same value if device._infer_common_mixingPressure: device.infer_common_mixingPressure() elif isinstance(device, Trap): if device._infer_constant_enthalpy: device.infer_constant_enthalpy() self._defineStates_ifDefinable(endStates)