def defineState_ifDefinable(self, state: StatePure): if not state.isFullyDefined() and state.isFullyDefinable(): try: state.copy_fromState(self.define(state)) except NeedsExtrapolationError: print( 'Fluid.defineState_ifDefinable: Leaving state @ {0} not fully defined' .format(state)) return state
def test_satMix_02(self): # From MECH2201 - A9 Q2 - state 4 statePropt = {'P': 5, 's': 7.7622, 'x': 0.92} testState = StatePure(**statePropt) self.assertTrue(testState.isFullyDefinable()) testState = fullyDefine_StatePure(testState, water_mpDF) expected = 2367.8 self.assertTrue(isWithin(testState.h, 3, '%', expected)) print('Expected: {0}'.format(expected)) print('Received: {0}'.format(testState.h))
def test_satMix_01(self): # From MECH2201 - A9 Q3 statePropt = {'P': 6, 's': 6.4855, 'x': 0.7638} testState = StatePure(**statePropt) self.assertTrue(testState.isFullyDefinable()) testState = fullyDefine_StatePure(testState, water_mpDF) expected = 1996.7 self.assertTrue(isWithin(testState.h, 3, '%', expected)) print('Expected: {0}'.format(expected)) print('Received: {0}'.format(testState.h))
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))