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
예제 #2
0
    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
예제 #3
0
    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)
예제 #5
0
    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
예제 #9
0
    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
예제 #10
0
    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}.")
예제 #11
0
    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))
예제 #12
0
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)
예제 #13
0
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)
예제 #14
0
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
예제 #15
0
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
예제 #16
0
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,
                    ))