Menu

python scripting of petroleum characterization

Tom
2018-10-25
2018-11-09
  • Tom

    Tom - 2018-10-25

    Hello

    Has anyone had experience scripting the creation of a MaterialStream with a composition that was generated using the Petroleum Characterization utility? If so, I’d be grateful to see an example.

    I’m trying to create a custom tool that accepts, for example, a given bulk MW and SG, generates an oil composition (pseudocomponents), creates a MaterialStream with a given flow rate, and flashes that stream to a given T and P to arrive at liquid and vapor phase flow rates. Ultimately, I will need to mix in the C1 – C6 light ends prior to the flash as well.

    Thanks in advance for your help.

    Tom

     
  • Daniel Medeiros

    Daniel Medeiros - 2018-10-25

    Hi Tom,

    While I was working with the new cross-platform user interface, I've created a helper function which takes these bulk parameters and returns a list of compounds:

    https://github.com/DanWBR/dwsim5/blob/windows/DWSIM.UI.Desktop.Editors/Compounds/BulkC7.cs#L173

    The remaining work depends if you're going to start with a predefined simulation or from scratch.

    Daniel

     

    Last edit: Daniel Medeiros 2018-10-25
  • Tom

    Tom - 2018-10-25

    Thanks, Daniel. Has this new helper function made it into DTL_v5.2.6836? I tried reproducing the oil characterization screenshot example from the DWSIM User Guide, and I receive a "No method matches given arguments" error when calling the helper function. Snippets of my code and the error are below.

    import clr
    clr.AddReference("DWSIM.Thermodynamics.StandaloneLibrary")
    from DWSIM.Thermodynamics.Utilities.PetroleumCharacterization import GenerateCompounds

    class DWSIMWrapper():
    @classmethod
    def flash_t_p(cls, flash_to_temperature, flash_to_pressure, overall_molar_flowrate, overall_composition):
    assay_name = "Heavy Ends"
    ncomps = 10
    t1 = None
    t2 = None
    v1 = None
    v2 = None
    mw = None
    sg = 0.87
    nbp = None
    mw0 = 80.0
    sg0 = 0.70
    nbp0 = 333.0
    Tccorr = "Riazi-Daubert (1985)"
    Pccorr = "Riazi-Daubert (1985)"
    AFcorr = "Lee-Kesler (1976)"
    MWcorr = "Winn (1956)"
    adjustAf = True
    adjustZR = True
    comps = GenerateCompounds()
    comp_results = comps.GenerateCompounds(assay_name, ncomps, Tccorr, Pccorr, AFcorr, MWcorr, adjustAf, adjustZR, mw, sg, nbp, v1, v2, t1, t2, mw0, sg0, nbp0)

    running thermo tests E............ ====================================================================== ERROR: test_flash_t_p (tests.TestDWSIMWrapper) ---------------------------------------------------------------------- Traceback (most recent call last): File "T:\PETMIN\TOrtiz\projects\audit-excel-spreadsheets\thermo\tests.py", line 54, in test_flash_t_p DWSIMWrapper.flash_t_p(self.temperature, self.pressure, self.molar_flowrate, self.composition) File "T:\PETMIN\TOrtiz\projects\audit-excel-spreadsheets\thermo\dwsim_wrapper.py", line 45, in flash_t_p comp_results = comps.GenerateCompounds(assay_name, ncomps, Tccorr, Pccorr, AFcorr, MWcorr, adjustAf, adjustZR, mw, sg, nbp, v1, v2, t1, t2, mw0, sg0, nbp0) TypeError: No method matches given arguments

     
  • Daniel Medeiros

    Daniel Medeiros - 2018-10-25

    Are you running this from inside DWSIM itself? It is on DTL v5.2 anyway...

     
  • Tom

    Tom - 2018-10-25

    No, I'm running this from a standalone Python script using pythonnet.

     
  • Tom

    Tom - 2018-10-25

    Yes, I can cast those values to Nullable[Double], but I can't set them to None (i.e. I can't nullify them), because then their type is converted to NoneType and the call to GenerateCompounds fails. The following snippet works, but values of 0.0 are not the same as None (a.k.a. Nothing in VB), so is that safe behavior? I wouldn't think so.

    from System import Double, Array, Nullable

        assay_name = "Heavy Ends"
        # Number of pseudocomponents
        ncomps = 10
        # Viscosity data
        t1 = Nullable[Double](0.0)
        t2 = Nullable[Double](0.0)
        v1 = Nullable[Double](0.0)
        v2 = Nullable[Double](0.0)
        # Assay properties
        mw = Nullable[Double](0.0)
        sg = Nullable[Double](0.87)
        nbp = Nullable[Double](0.0)
        # Properties of lightest component in stream
        mw0 = 80.0
        sg0 = 0.70
        nbp0 = 333.0
        # Correlations
        Tccorr = "Riazi-Daubert (1985)"
        Pccorr = "Riazi-Daubert (1985)"
        AFcorr = "Lee-Kesler (1976)"
        MWcorr = "Winn (1956)"
        # Adjust acentric factors to match NBP
        adjustAf = True
        # Adjust Rackett factors to match SG
        adjustZR = True
        comps = GenerateCompounds()
        # t1 = t2 = v1 = v2 = mw = nbp = None
        comp_results = comps.GenerateCompounds(assay_name, ncomps, Tccorr, Pccorr, AFcorr, MWcorr, adjustAf, adjustZR, mw, sg, nbp, v1, v2, t1, t2, mw0, sg0, nbp0)
        comp_keys = list(comp_results.Keys)
        comp_values = list(comp_results.Values)
    

    (Pdb) pp(comp_values)
    [<dwsim.thermodynamics.baseclasses.compound object="" at="" 0x000001d6581e17b8="">,
    <dwsim.thermodynamics.baseclasses.compound 0x000001d6581e17f0="" object="" at="">,
    <dwsim.thermodynamics.baseclasses.compound object="" at="" 0x000001d6581e1828="">,
    <dwsim.thermodynamics.baseclasses.compound 0x000001d6581e1860="" object="" at="">,
    <dwsim.thermodynamics.baseclasses.compound 0x000001d6581e1898="" object="" at="">,
    <dwsim.thermodynamics.baseclasses.compound 0x000001d6581e18d0="" object="" at="">,
    <dwsim.thermodynamics.baseclasses.compound object="" at="" 0x000001d6581e1908="">,
    <dwsim.thermodynamics.baseclasses.compound 0x000001d6581e1940="" object="" at="">,
    <dwsim.thermodynamics.baseclasses.compound 0x000001d6581e1978="" object="" at="">,
    <dwsim.thermodynamics.baseclasses.compound 0x000001d6581e19b0="" object="" at="">]</dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound></dwsim.thermodynamics.baseclasses.compound>

    (Pdb) pp(comp_values[0])
    <dwsim.thermodynamics.baseclasses.compound object="" at="" 0x000001d6581e17b8="">
    (Pdb) pp(comp_values[0].Name)
    'Heavy Ends_NBP431'
    (Pdb) pp(comp_values[0].MoleFraction)
    0.10004146986435093
    (Pdb)</dwsim.thermodynamics.baseclasses.compound>

     
  • Daniel Medeiros

    Daniel Medeiros - 2018-10-25

    Does it work if you cast to Nullable[Double]()?

    Anyway, for this specific function there is no problem on having a value as long as it is zero (https://github.com/DanWBR/dwsim5/blob/windows/DWSIM.Thermodynamics/PetroleumCharacterization/GenerateCompounds.vb#L43)

     

    Last edit: Daniel Medeiros 2018-10-25
  • Tom

    Tom - 2018-10-26

    I can't assign to Nullable[Double] (), because Nullable< T > doesn't have a default, no-arg constructor: https://docs.microsoft.com/en-us/dotnet/api/system.nullable-1?view=netframework-4.7.2 . The only available constructor takes a single, value type argument. See also error below.

    However, if 0.0 is a safe default value for the nullable arguments of GenerateCompounds, then that will work for me. Thanks for your help.

    Traceback (most recent call last):
    File "T:\PETMIN\TOrtiz\projects\audit-excel-spreadsheets\thermo\tests.py", line 54, in test_flash_t_p
    DWSIMWrapper.flash_t_p(self.temperature, self.pressure, self.molar_flowrate, self.composition)
    File "T:\PETMIN\TOrtiz\projects\audit-excel-spreadsheets\thermo\dwsim_wrapper.py", line 23, in flash_t_p
    t1 = Nullable[Double] ()
    SystemError: <class culture="neutral," 'system.0,="" publickeytoken="b77a5c561934e089]]'"> returned NULL without setting an error</class>

     

    Last edit: Tom 2018-10-26
  • Tom

    Tom - 2018-10-29

    Still working on this a little bit at a time. May I offer a couple of observations?

    First, it appears that the no arg MaterialStream () constructor does not initialize the Phases attribute; one must call MaterialStream (String, String) instead. That fact might be good to note in the API documentation. If the instantiation of mixture (see below) were made using the no arg MaterialStream () constructor, then no pseudocomponents would be added to mixture, because material_stream.Phases.get_Count() would equal 0, and any subsequent flash (not shown here) would fail. The following snippets do appear to work correctly.

        if overall_composition:
            compound_names = [components[0] for components in overall_composition]
            mole_fractions = [components[1] for components in overall_composition]
            mixture = flash_calculator.CreateMaterialStream(compound_names, mole_fractions)
        elif oil_characterization:
            compounds, assay = oil_characterization
            compound_names = [compound.Name for compound in compounds]
            mole_fractions = [compound.MoleFraction for compound in compounds]
            mixture = MaterialStream("", "")
            DWSIMWrapper._add_pseudocomponents_to_material_stream(compounds, assay, mixture)
            mixture.SetOverallComposition(Array[Double](mole_fractions))
    
    @classmethod
    def _add_pseudocomponents_to_material_stream(cls, pseudocomponents, assay, material_stream):
        try:
            # add pseudocomponents as new Compound objects to each phase
            for component in pseudocomponents:
                for phase_index in range(0, material_stream.Phases.get_Count()):
                    compound = Compound(component.Name, "")
                    compound.ConstantProperties = component.ConstantProperties
                    material_stream.Phases[phase_index].Compounds.Add(compound.Name, compound)
            # Quality check requires flowsheet object to exist
            # qc = QualityCheck(assay, material_stream)
            # qc.DoQualityCheck()
        except Exception as e:
            raise e
    

    This leads me to my second observation: It appears that DoQualityCheck () was designed specifically to be used within the GUI. It might be useful to make the raw error calculations available as a standalone method, and then have something like ReportQualityCheck () write the comparison to a string/report/screen.

     

    Last edit: Tom 2018-10-29
  • Daniel Medeiros

    Daniel Medeiros - 2018-10-31

    Hi Tom,

    Yes, there is no empty MS constructor as DWSIM thermo wasn't originally designed to be used from outside the GUI. That is easily fixed, as you may know.

    Regarding the DoQualityCheck() issue, the answer is yes again for the same reasons. Also easily fixed.

    I'll reply to this post as soon as I have updated the code with your suggestions.

     
  • Daniel Medeiros

    Daniel Medeiros - 2018-10-31

    Here's an updated build: https://gum.co/VHBNe

     
  • Tom

    Tom - 2018-11-02

    Thanks, Daniel.

    I'm now working on mixing the light ends into the oil characterization. I see from Mixer.vb that the StreamMixer unit operation is also fairly tightly coupled with UI code. I was, therefore, planning to just create my own Python method to mix the streams. However, if you have an example of how this should be done most efficiently in Python, that would be very helpful.

     
  • Daniel Medeiros

    Daniel Medeiros - 2018-11-05

    I believe that the current method on Mixer.vb is the most efficient one. Create a new stream, set its composition and mass flow through a mass balance. Calculate and set enthalpy through an energy balance and calculate the temperature using a PH flash, with the pressure being an assumed value. Maybe in your case you don't even have to calculate temperature, just set it to a default value...

     
  • Tom

    Tom - 2018-11-05

    OK. Is the Mixer class exposed in DTL_v5.4.6878.2 ? I was having trouble importing it from DWSIM.UnitOperations.UnitOperations .

     
  • Daniel Medeiros

    Daniel Medeiros - 2018-11-05

    Nops. it is on DWSIM.UnitOperations.dll. You may try importing it from a DWSIM installation, but it will probably ask for some additional DLLs.

     
  • Tom

    Tom - 2018-11-05

    Would that alter the licensing terms for any code I write if I were to link to any DWSIM libraries other than the Standalone Thermodynamics Library?

     

    Last edit: Tom 2018-11-05
  • Daniel Medeiros

    Daniel Medeiros - 2018-11-05

    In this case, yes. You'll have to write your own code. What's your difficulty on writing the mixer code? Something specific?

     
  • Tom

    Tom - 2018-11-07

    No, no problem. I was just trying to avoid reinventing the wheel. The following appears to be working for me:

    @classmethod
    def _mix_material_streams(cls, material_streams, property_package):
        # Sets pressure of mixed stream equal to lowest of inlet stream pressures
        mixture = None
        compounds = []
        stream_mole_fractions = []
        stream_flow_rates = []
        stream_enthalpies = []
        stream_pressures = []
        mixture_mole_fractions = []
        mixture_flow_rate = 0.0
        mixture_enthalpy = 0.0
        mixture_pressure = 0.0
        try:
            mixture = MaterialStream("", "")
            for material_stream in material_streams:
                stream_flow_rates.append(material_stream.GetProp("totalFlow", "Overall", None, "", "mole")[0])
                stream_enthalpies.append(material_stream.GetProp("enthalpy", "Overall", None, "", "mole")[0])
                stream_pressures.append(material_stream.GetProp("pressure", "Overall", None, "", "")[0])
                DWSIMWrapper._add_components_to_material_stream(list(material_stream.Phases[0].Compounds.Values), mixture)
                stream_mole_fractions = [compound.MoleFraction for compound in list(material_stream.Phases[0].Compounds.Values)]
                mixture_mole_fractions.extend(stream_mole_fractions)
            # mixture.SetOverallComposition(Array[Double](mixture_mole_fractions))
            mixture.SetPropertyPackage(property_package)
            for stream_index in range(0, len(stream_flow_rates)):
                flow_energy = stream_flow_rates[stream_index]*stream_enthalpies[stream_index]
                mixture_enthalpy += flow_energy
                mixture_flow_rate += stream_flow_rates[stream_index]
            mixture_enthalpy /= mixture_flow_rate
            mixture_pressure = min(stream_pressures)
            mixture.ClearAllProps()
            mixture.SetProp("enthalpy", "Overall", None, "", "", [mixture_enthalpy])
            mixture.SetProp("pressure", "Overall", None, "", "", [mixture_pressure])
            mixture.SetProp("totalFlow", "Overall", None, "", "mole", [mixture_flow_rate])
            mixture.SetProp("fraction", "Overall", None, "", "mole", mixture_mole_fractions)
            mixture.NormalizeOverallMoleComposition()
            # Perform PH flash on mixed stream and return it
            mixture.SpecType = StreamSpec.Pressure_and_Enthalpy
            mixture.Calculate(True, True)
        except Exception as e:
            raise e
        return mixture
    
     
Want the latest updates on software, tech news, and AI?
Get latest updates about software, tech news, and AI from SourceForge directly in your inbox once a month.