def convert_to(self, new_unit): """ Convert this Measurement object to the specified new unit The object is mutated in place. If the conversion can not be performed, an Exception will be raised, and the object not altered. This will also return the object (self) -- but that is a deprecated feature -- do not use it! If you want a new object, use `converted_to` instead """ new_vals = {att: None for att in ('value', 'min_value', 'max_value', 'standard_deviation')} for attr in new_vals.keys(): val = getattr(self, attr) if val is not None: new_val = convert(self.unit_type, self.unit, new_unit, val) new_vals[attr] = new_val # if this was all successful new_vals['unit'] = new_unit self.__dict__.update(new_vals) return None
def find_density_at_60F(self): """ Returns the density (in kg/m3) It will interpolate and extrapolate as needed """ try: density = Density(self.oil) have_data = False if self.oil.metadata.product_type in CRUDE_PRODUCTS: have_data = True else: temps = density.temps\ if len(temps) == 1: t = temps[0] if 286 < t < 291: # 55F, 65F (is the value near 60F?) have_data = True else: # check if ref temps are anywhere near 60F if (max(temps) >= 286 and min(temps) <= 291): have_data = True if have_data: return density.at_temp(uc.convert("F", "K", 60)) except Exception: # something went wrong, and we don't want it to barf return None
def at_temp(self, temp, unit='K'): """ density(s) at the provided temperature(s) :param temp: scalar or sequence of temp in K :param unit='K': unit of temperature densities will be returned as kg/m^3 """ temp = np.asarray(temp) scaler = True if temp.shape == () else False temp.shape = (-1, ) if unit != 'K': temp = uc.convert(unit, 'K', temp) densities = np.interp(temp, self.temps, self.densities, left=-np.inf, right=np.inf) left = (densities == -np.inf) densities[left] = (self.densities[0] + (self.k_rho_default * (temp[left] - self.temps[0]))) right = (densities == np.inf) densities[right] = (self.densities[-1] + (self.k_rho_default * (temp[right] - self.temps[-1]))) return densities if not scaler else densities[0]
def test_dist_cuts(self, samp_ind, cut_index, fraction, temp_f): samples = ExxonMapper(self.record).sub_samples cut = samples[samp_ind].distillation_data.cuts[cut_index] assert cut.fraction.value == fraction assert isclose(cut.vapor_temp.value, sigfigs(uc.convert("F", "C", temp_f), 5), rel_tol=1e-4)
def at_temp(self, temp, kvis_units='m^2/s', temp_units="K"): """ Compute the kinematic viscosity of the oil as a function of temperature :param temp_k: temperatures to compute at: can be scalar or array of values. Should be in Kelvin viscosity as a function of temp is given by: v = A exp(k_v2 / T) with constants determined from measured data """ temp = np.asarray(temp) temp = uc.convert('temperature', temp_units, 'K', temp) kvisc = self._visc_A * np.exp(self._k_v2 / temp) kvisc = uc.convert('kinematic viscosity', 'm^2/s', kvis_units, kvisc) return kvisc
def test_dist_end_point(self, sample_idx, expected): samples = ExxonMapper(self.record).sub_samples if expected is None: assert samples[sample_idx].distillation_data.end_point is None else: expected_c = sigfigs(uc.convert("F", "C", expected), 5) end_point = samples[sample_idx].distillation_data.end_point assert isclose(end_point.value, expected_c, rel_tol=1e-4) assert end_point.unit == 'C'
def convert_to(self, new_unit): # need to do the "right thing" with standard deviation if self.standard_deviation is None: # no need for anything special super().convert_to(new_unit) else: new_std = convert("deltatemperature", self.unit, new_unit, self.standard_deviation) super().convert_to(new_unit) self.standard_deviation = new_std return self
def validate(self): msgs = [] if self is None: # how can this happen?!?! -- but it does. return msgs # only do this for C or K if (self.unit is not None) and (self.unit.upper() not in {'C', 'K'}): return msgs for val in (self.value, self.min_value, self.max_value): if val is not None: val_in_C = convert(self.unit, "C", val) decimal = val_in_C % 1 if isclose(decimal, 0.15) or isclose(decimal, 0.85): msgs.append(WARNINGS['W010'].format( f"{val:.2f} {self.unit} ({val_in_C:.2f} C)", f"{round(val_in_C):.2f} C")) return msgs
def check_for_valid_api(self): """ Check is the API value is already valid """ API = self.oil.metadata.API if API is None: return False density_at_60F = self.find_density_at_60F() if density_at_60F is None: return None computed_API = uc.convert("density", "kg/m^3", "API", density_at_60F) if abs(API - computed_API) <= 0.2: return True else: return False
def cleanup(self): """ run this particular cleanup option :param oil: an Oil object to act on :param do_it=False: flag to tell the cleanup to do its thing. If False, the method returns a message. If True, the action is taken, and the Oil object is altered. :returns: a message of what could be done, or what was done. """ density_at_60 = self.find_density_at_60F() if density_at_60: API = uc.convert("density", "kg/m^3", "API", density_at_60) self.oil.metadata.API = round(API, 2) return (f"Cleanup: {self.ID}: " f"Set API for {self.oil.oil_id} to {API}.")
def validate(self): """ validation specific to the Oil object itself validation of sub-objects is automatically applied """ try: # See if it can be used as a GNOME oil # NOTE: This is an odd one, as it puts the information in a # different place # NOTE: Make a copy, as make_gnome_oil might change it in place. # NOTE: If it barfs for any reason it's not suitable make_gnome_oil(copy.deepcopy(self)) self.metadata.gnome_suitable = True except Exception: self.metadata.gnome_suitable = False msgs = [] try: self._validate_id(self.oil_id) except ValueError: msgs.append(ERRORS["E011"].format(self.oil_id)) API = self.metadata.API if API is not None: try: density_at_60F = (physical_properties.Density(self).at_temp( 60, 'F')) calculatedAPI = uc.convert('kg/m^3', 'API', density_at_60F) if abs(API - round(calculatedAPI, 3)) > 0.2: msgs.append(ERRORS["E043"].format(API, calculatedAPI)) except (IndexError, ValueError): pass # always add these: msgs.extend("W000: " + m for m in self.permanent_warnings) return sorted(set(msgs))
def test_API_density_missmatch(minimal_oil): oil = minimal_oil minimal_oil.metadata.API = 32.2 # too far from 32.0 density = DensityPoint( # API 32.0 converted density=Density(value=0.86469, unit='g/cm^3'), ref_temp=Temperature(value=60, unit='F'), ) oil.sub_samples[0].physical_properties.densities.append(density) density_at_60F = physical_properties.Density(oil).at_temp(60, 'F') API = uc.convert('kg/m^3', 'API', density_at_60F) print(f"{density_at_60F=}") print(f"{API=}") print(f"{minimal_oil.metadata.API=}") validate(oil) print(oil.status) assert snippet_in_oil_status("E043", oil)
def test_API_density_match(minimal_oil): oil = minimal_oil minimal_oil.metadata.API = 32.1 # close enough to 32.0 density = DensityPoint( density=Density(value=0.86469, unit='g/cm^3'), ref_temp=Temperature(value=60, unit='F'), ) oil.sub_samples[0].physical_properties.densities.append(density) density_at_60F = physical_properties.Density(oil).at_temp(60, 'F') API = uc.convert('kg/m^3', 'API', density_at_60F) print(density_at_60F) print(API) assert math.isclose(API, oil.metadata.API, rel_tol=1e3) validate(oil) print(oil.status) assert snippet_not_in_oil_status("E043", oil)
def make_gnome_oil(oil): """ Make a dict that a GnomeOil can be built from A GnomeOil needs: "name, "# Physical properties "api, "pour_point, "solubility, # kg/m^3 "# emulsification properties "bullwinkle_fraction, "bullwinkle_time, "emulsion_water_fraction_max, "densities, "density_ref_temps, "density_weathering, "kvis, "kvis_ref_temps, "kvis_weathering, "# PCs: "mass_fraction, "boiling_point, "molecular_weight, "component_density, "sara_type, "adios_oil_id=None, """ # make sure we don't change the original oil object oil = copy.deepcopy(oil) # metadata: go = get_empty_dict() go['name'] = oil.metadata.name go['adios_oil_id'] = oil.oil_id dens = Density(oil) ref_density = dens.at_temp(288.7) # 60F in K go['api'] = uc.convert('kg/m^3', 'API', ref_density) # for gnome_oil we don't treat api as data, only api from density oil.metadata.API = go['api'] # Physical properties phys_props = oil.sub_samples[0].physical_properties pour_point = phys_props.pour_point if pour_point is None: go['pour_point'] = estimate_pour_point(oil) else: pp = phys_props.pour_point.measurement.converted_to('K') if pp.max_value is not None: go['pour_point'] = pp.max_value elif pp.value is not None: go['pour_point'] = pp.value elif pp.min_value is not None: go['pour_point'] = pp.min_value else: go['pour_point'] = None # fixme: We need to get the weathered densities, if they are there. densities = get_density_data(oil, units="kg/m^3", temp_units="K") go['densities'], go['density_ref_temps'] = zip(*densities) go['density_weathering'] = [0.0] * len(go['densities']) viscosities = get_kinematic_viscosity_data(oil, units="m^2/s", temp_units="K") if viscosities: go['kvis'], go['kvis_ref_temps'] = zip(*viscosities) go['kvis_weathering'] = [0.0] * len(go['kvis']) else: raise ValueError("Gnome oil needs at least one viscosity value") bullwinkle = None for sub_sample in oil.sub_samples: try: frac_weathered = (sub_sample.metadata.fraction_weathered. converted_to('fraction').value) if bullwinkle is None or frac_weathered > bullwinkle: bullwinkle = frac_weathered except Exception: frac_weathered = None if bullwinkle is None: go['bullwinkle_fraction'] = bullwinkle_fraction(oil) else: go['bullwinkle_fraction'] = bullwinkle go['emulsion_water_fraction_max'] = max_water_fraction_emulsion(oil) go['solubility'] = 0 # k0y is not currently used -- not sure what it is? # go['k0y'] = 2.024e-06 #do we want this included? # pseudocomponents cut_temps, _frac_evap = normalized_cut_values(oil) mass_fraction = component_mass_fractions(oil) mask = np.where(mass_fraction == 0) mol_wt = np.delete(component_mol_wt(cut_temps), mask) comp_dens = np.delete(component_densities(cut_temps), mask) boiling_pt = np.delete(component_temps(cut_temps), mask) sara_type = np.delete(np.array(component_types(cut_temps)), mask) mass_frac = np.delete(mass_fraction, mask) go['molecular_weight'] = mol_wt.tolist() go['component_density'] = comp_dens.tolist() go['mass_fraction'] = mass_frac.tolist() go['boiling_point'] = boiling_pt.tolist() go['sara_type'] = sara_type.tolist() return go
def process_cut_table(oil, samples, cut_table): """ process the parts that aren't a simple map """ # API -- odd because we only need one! row = cut_table[norm("API Gravity,")] # pull API from first value try: # stored as full precision double oil.metadata.API = round(float(row[0]), 1) except Exception: oil.metadata.API = None # use specific gravity to get density row = cut_table[norm("Specific Gravity (60/60F)")] for sample, val in zip(samples, row): try: rho = uc.convert("SG", "g/cm^3", val) sample.physical_properties.densities.append( DensityPoint( density=Density(value=sigfigs(rho, 5), unit="g/cm^3"), ref_temp=Temperature(value=15.6, unit="C"), )) except Exception: pass # viscosity for lbl in ("Viscosity at 20C/68F, cSt", "Viscosity at 40C/104F, cSt", "Viscosity at 50C/122F, cSt"): row = cut_table[norm(lbl)] temps = re.compile(r'\d+C').findall(lbl) if len(temps) > 0: temp_c = float(temps[0][:-1]) else: temp_c = None for sample, val in zip(samples, row): try: sample.physical_properties.kinematic_viscosities.append( KinematicViscosityPoint( viscosity=KinematicViscosity(value=sigfigs(val, 5), unit="cSt"), ref_temp=Temperature(value=temp_c, unit="C"), )) except Exception: pass # distillation data if norm("Distillation type, TBP") not in cut_table: raise ValueError("I don't recognise this distillation data. \n" 'Expected: "Distillation type, TBP"') for sample in samples: sample.distillation_data.type = 'volume fraction' for name, row in cut_table.items(): if norm("vol%, F") in name or name == norm("IBP, F"): # looks like a distillation cut. percent = 0.0 if "ibp" in name else float(name.split("vol")[0]) for sample, val in zip(samples, row): if val is not None: val = sigfigs(uc.convert("F", "C", val), 5) sample.distillation_data.cuts.append( DistCut(fraction=MassFraction(value=percent, unit="%"), vapor_temp=Temperature(value=val, unit="C"))) elif name == norm('EP, F'): for sample, val in zip(samples, row): if val is not None: val = sigfigs(uc.convert("F", "C", val), 5) sample.distillation_data.end_point = Temperature(value=val, unit="C") # sort them for sample in samples: sample.distillation_data.cuts.sort(key=lambda c: c.fraction.value) oil.sub_samples = samples return oil
def set_sample_property(samples, row, attr, unit, cls, unit_type=None, convert_from=None, element_of=None, num_digits=5): """ reads a row from the spreadsheet, and sets the sample properties Notes: - optional rounding to "num_digits" digits - optional converting to unit from convert_from (if the data aren't in the right units) - These values are now kept in a list of compounds held by the bulk_composition attribute - The name & groups of each compound should be match the ADIOS data model controlled vocabulary """ for sample, val in zip(samples, row): if val is not None and val not in ('NotAvailable', ): if convert_from is not None: val = uc.convert(convert_from, unit, val) if element_of is None: # assign to an attribute if isinstance(attr, str): attr = attr.split('.') if len(attr) > 1: for a in attr[:-1]: child_value = getattr(sample, a) if child_value is None: # get a default value from the parent dataclass # annotation child_value = sample.__dataclass_fields__[a].type() setattr(sample, a, child_value) sample = child_value setattr(sample, attr[-1], cls(sigfigs(val, num_digits), unit=unit)) else: # add to a list attribute compositions = getattr(sample, element_of) measurement = cls(sigfigs(val, num_digits), unit=unit, unit_type=unit_type) item_cls = compositions.item_type compositions.append( item_cls( name=attr, measurement=measurement, ))