0

Hi there smart people!

I am trying to implement a 1D, steady-state, real gas (compressibility factor) pipe flow model in Python using Pyomo + SCIP. It basically amounts to solving a DAE system. The formulation is an adopted version from chapter 10 in Koch, T.; Hiller, B.; Pfetsch, M.E.; Schewe, L. Evaluating Gas Network Capacities; Series on Optimization, MOS-SIAM, 2015.

However, I am encountering several problems:

  1. The problem seems to be numerically sensitive to a combination of the discretization step length and input parameters (mass flow, pipe length).
  2. Does not solve for any other model but ideal gas.

With a suitable discretization, and an ideal gas law, I get a result that seems reasonable (see example). In all other cases it turns out to be infeasible. I may be overlooking something here, but I do not see it. Therefore, if anyone is inclined to try and help me out here, I would be thankful.

The example below should produce a valid output.

Edit: I realized I had one false constraint in there belonging to another model. The energy equation works now. However, the problems mentioned above remain.

from pyomo.dae import *
from pyomo.environ import *
import matplotlib.pyplot as plt
from math import pi
from dataclasses import dataclass


@dataclass
class ShomateParameters:
    A: float
    B: float
    C: float
    D: float
    E: float

    def specific_isobaric_heat_capacity(self, temperature):

        # in J/(mol*K)
        return self.A + self.B * (temperature/1000) + self.C * (temperature/1000)**2 + self.D * (temperature/1000)**3 + self.E/(temperature/1000)**2

    def plot(self, temperature_start, temperature_end, points_to_mark=None):
        assert temperature_start <= temperature_end, "temperature_start <= temperature_end must hold."

        temperatures = [i for i in range(temperature_start, temperature_end+1)]
        values = [self.specific_isobaric_heat_capacity(temp) for temp in temperatures]
        fig, ax = plt.subplots()
        ax.plot(temperatures, values)
        if points_to_mark is not None:
            ax.scatter(points_to_mark, [self.specific_isobaric_heat_capacity(temp) for temp in points_to_mark])
        ax.set(xlabel='temperature [K]', ylabel='specific isobaric heat capacity [J/(mol*K)]',
               title='Shomate equation:\n A + B*T + C*T^2 + D*T^3 + E/T^2')
        ax.grid()
        plt.show()


@dataclass
class Species:
    MOLAR_MASS: float  # kg/mol
    CRITICAL_TEMPERATURE: float  # Kelvin
    CRITICAL_PRESSURE: float  # Pa
    DYNAMIC_VISCOSITY: float  # Pa*s
    SHOMATE_PARAMETERS: ShomateParameters


METHANE = Species(MOLAR_MASS=0.016043,
                  CRITICAL_TEMPERATURE=190.56,
                  CRITICAL_PRESSURE=4599000,
                  DYNAMIC_VISCOSITY=1.0245e-5,
                  SHOMATE_PARAMETERS=ShomateParameters(
                      A=-0.703029,
                      B=108.4773,
                      C=-42.52157,
                      D=5.862788,
                      E=0.678565))

# select gas species
gas = METHANE

# select equation of state ('ideal', 'AGA' or 'Papay')
formula = 'ideal'

PIPE_LENGTH = 24000  # meter
start = 0  # meter
end = start + PIPE_LENGTH

MASS_FLOW = 350  # kg/s

PIPE_SLOPE = 0.0
PIPE_DIAMETER = 1.0  # meter
PIPE_INNER_ROUGHNESS = 6e-5  # 15e-6  # meter 6e-6  # meter


# gravitational acceleration
G = 9.81  # meter**2/s**2

# gas temperature at entry
TEMPERATURE = 283.15

# temperature ambient soil
TEMPERATURE_SOIL = 283.15  # Kelvin

# gas pressure at entry
PRESSURE = 4.2e6  # Pa

GAS_CONSTANT = 8.314  # J/(mol*K)

print(gas.SHOMATE_PARAMETERS)
print(gas.SHOMATE_PARAMETERS.specific_isobaric_heat_capacity(TEMPERATURE))
gas.SHOMATE_PARAMETERS.plot(273, 400, points_to_mark=[TEMPERATURE])

##################################################################################
# Variable bounds
##################################################################################

pressure_bounds = (0, 1e7)  # Pa
temperature_bounds = (0, 650)  # Kelvin
density_bounds = (0, 100)
idealMolarIsobaricHeatCapacityBounds = (0, 200)
correctionIdealMolarIsobaricHeatCapacityBounds = (-250, 250)
velocity_bounds = (0, 300)

##################################################################################
# Create model
##################################################################################

m = ConcreteModel()

##################################################################################
# Parameters
##################################################################################

m.criticalPressure = Param(initialize=gas.CRITICAL_PRESSURE)
m.criticalTemperature = Param(initialize=gas.CRITICAL_TEMPERATURE)
m.molarMass = Param(initialize=gas.MOLAR_MASS)
m.dynamicViscosity = Param(initialize=gas.DYNAMIC_VISCOSITY)

m.gravitationalAcceleration = Param(initialize=G)
m.pipeSlope = Param(initialize=PIPE_SLOPE)
m.innerPipeRoughness = Param(initialize=PIPE_INNER_ROUGHNESS)
m.c_HT = Param(initialize=2)
m.pi = Param(initialize=pi)
m.temperatureSoil = Param(initialize=TEMPERATURE_SOIL)
m.gasConstantR = Param(initialize=GAS_CONSTANT)
m.massFlow = Param(initialize=MASS_FLOW)
m.pipeDiameter = Param(initialize=PIPE_DIAMETER)
m.crossSectionalArea = Param(initialize=m.pi * m.pipeDiameter**2 / 4)

m.alpha = Param(initialize=3.52)
m.beta = Param(initialize=-2.26)
m.gamma = Param(initialize=0.274)
m.delta = Param(initialize=-1.878)

m.e = Param(initialize=2.2)
m.d = Param(initialize=2.2)


##################################################################################
# Variables
##################################################################################

m.x = ContinuousSet(bounds=(start, end))
m.pressure = Var(m.x, bounds=pressure_bounds)  #
m.dpressuredx = DerivativeVar(m.pressure, wrt=m.x, initialize=0, bounds=(-100, 100))
m.temperature = Var(m.x, bounds=temperature_bounds)  #
m.dtemperaturedx = DerivativeVar(m.temperature, wrt=m.x, initialize=0, bounds=(-100, 100))
m.density = Var(m.x, bounds=density_bounds)
m.ddensitydx = DerivativeVar(m.density, wrt=m.x, initialize=0, bounds=(-100, 100))
m.z = Var(m.x, bounds=(-10, 10))
m.specificIsobaricHeatCapacity = Var(m.x)
m.idealMolarIsobaricHeatCapacity = Var(m.x, bounds=idealMolarIsobaricHeatCapacityBounds)
m.correctionIdealMolarIsobaricHeatCapacity = Var(m.x, bounds=correctionIdealMolarIsobaricHeatCapacityBounds)
m.mue_jt = Var(bounds=(-100, 100))
m.velocity = Var(m.x, bounds=velocity_bounds)
m.phiVar = Var()
##################################################################################
# Constraint rules
##################################################################################

# compressibility factor z and its derivatives; (pV/(nRT)=z


def z(p,
      T,
      p_c,
      T_c,
      formula=None):

    p_r = p/p_c
    T_r = T/T_c
    if formula is None:
        raise ValueError("formula must be equal to 'AGA' or 'Papay' or 'ideal'")
    elif formula == 'AGA':
        return 1 + 0.257 * p_r - 0.533 * p_r/T_r
    elif formula == 'Papay':
        return 1-3.52 * p_r * exp(-2.26 * T_r) + 0.247 * p_r**2 * exp(-1.878 * T_r)
    elif formula == 'ideal':
        return 1
    else:
        raise ValueError("formula must be equal to 'AGA' or 'Papay' or 'ideal'")


def dzdT(p,
         T,
         p_c,
         T_c,
         formula=None):

    p_r = p/p_c
    T_r = T/T_c
    if formula is None:
        raise ValueError("formula must be equal to 'AGA' or 'Papay'")
    elif formula == 'AGA':
        return 0.533 * p/p_c * T_c * 1/T**2
    elif formula == 'Papay':
        return 3.52 * p_r * (2.26/T_c) * exp(-2.26 * T_r) + 0.247 * p_r**2 * (-1.878/T_c) * exp(-1.878 * T_r)
    elif formula == 'ideal':
        return 0
    else:
        raise ValueError("formula must be equal to 'AGA' or 'Papay' or 'ideal'")


def dzdp(p,
         T,
         p_c,
         T_c,
         formula=None):

    p_r = p/p_c
    T_r = T/T_c
    if formula is None:
        raise ValueError("formula must be equal to 'AGA' or 'Papay' or 'ideal'")
    elif formula == 'AGA':
        return 0.257 * 1/p_c - 0.533 * (1/p_c)/T_r
    elif formula == 'Papay':
        return -3.52 * 1/p_c * exp(-2.26 * T_r) + 0.274 * 1/(p_c**2) * 2 * p * exp(-1.878 * T_r)
    elif formula == 'ideal':
        return 0
    else:
        raise ValueError("formula must be equal to 'AGA' or 'Papay' or 'ideal'")


def make_c_compr(formula):

    assert formula == 'AGA' or formula == 'Papay' or formula == 'ideal'

    def _c_compr(z_var,
                 p,
                 T,
                 p_c,
                 T_c):
        return z_var - z(p, T, p_c, T_c, formula=formula)
    return _c_compr


_c_compr = make_c_compr(formula)


def _c_compr_rule(m, x):
    return 0 == _c_compr(m.z[x],
                         m.pressure[x],
                         m.temperature[x],
                         m.criticalPressure,
                         m.criticalTemperature)


m.c_compr = Constraint(m.x, rule=_c_compr_rule)

# specific isobaric heat capacity: ideal + correction term

def _c_mhc_real(molarMass,
                specificIsobaricHeatCapacity,
                idealMolarIsobaricHeatCapacity,
                correctionIdealMolarIsobaricHeatCapacity):

    return molarMass * specificIsobaricHeatCapacity - (idealMolarIsobaricHeatCapacity +
                                                       correctionIdealMolarIsobaricHeatCapacity)


def _c_mhc_real_rule(m, x):
    return 0 == _c_mhc_real(m.molarMass,
                            m.specificIsobaricHeatCapacity[x],
                            m.idealMolarIsobaricHeatCapacity[x],
                            m.correctionIdealMolarIsobaricHeatCapacity[x])


m.c_mhc_real = Constraint(m.x, rule=_c_mhc_real_rule)

# _c_mhc_ideal_Shomate


def _c_mhc_ideal_Shomate(idealMolarIsobaricHeatCapacity, A, B, C, D, E, T):
    return idealMolarIsobaricHeatCapacity - (A + B * (T/1000) + C * (T/1000)**2 + D * (T/1000)**3 + E/(T/1000)**2)


def _c_mhc_ideal_Shomate_rule(m, x):
    return 0 == _c_mhc_ideal_Shomate(m.idealMolarIsobaricHeatCapacity[x],
                                     gas.SHOMATE_PARAMETERS.A,
                                     gas.SHOMATE_PARAMETERS.B,
                                     gas.SHOMATE_PARAMETERS.C,
                                     gas.SHOMATE_PARAMETERS.D,
                                     gas.SHOMATE_PARAMETERS.E,
                                     m.temperature[x])


m.c_mhc_ideal_Shomate = Constraint(m.x, rule=_c_mhc_ideal_Shomate_rule)

# _c_mhc_corr


def make_c_mhc_corr(formula):
    assert formula == 'AGA' or formula == 'Papay' or formula == 'ideal'

    if formula == 'AGA':
        def _c_mhc_corr(correctionIdealMolarIsobaricHeatCapacity, alpha, beta, gamma, delta, p, T, p_c, T_c, R):
            return correctionIdealMolarIsobaricHeatCapacity
    elif formula == 'Papay':
        def _c_mhc_corr(correctionIdealMolarIsobaricHeatCapacity, alpha, beta, gamma, delta, p, T, p_c, T_c, R):
            # m.alpha = 3.52
            # m.beta = -2.26
            # m.gamma = 0.274
            # m.delta = -1.878
            p_r = p/p_c
            T_r = T/T_c

            return correctionIdealMolarIsobaricHeatCapacity + R * (
                (gamma * delta + 0.5 * gamma * delta**2 * T_r) * p_r**2 * T_r * exp(delta * T_r) -
                (2 * alpha * beta + alpha * beta**2 * T_r) * p_r * T_r * exp(beta * T_r))
    elif formula == 'ideal':
        def _c_mhc_corr(correctionIdealMolarIsobaricHeatCapacity, alpha, beta, gamma, delta, p, T, p_c, T_c, R):
            return correctionIdealMolarIsobaricHeatCapacity
    return _c_mhc_corr


_c_mhc_corr = make_c_mhc_corr(formula)


def _c_mhc_corr_rule(m, x):
    return 0 == _c_mhc_corr(m.correctionIdealMolarIsobaricHeatCapacity[x],
                            m.alpha,
                            m.beta,
                            m.gamma,
                            m.delta,
                            m.pressure[x],
                            m.temperature[x],
                            m.criticalPressure,
                            m.criticalTemperature,
                            m.gasConstantR)


m.c_mhc_corr = Constraint(m.x, rule=_c_mhc_corr_rule)

# equation of state

def _c_eos(p, T, rho, molarMass, R, z):
    return rho * z * R * T - p * molarMass


def _c_eos_rule(m, x):
    return 0 == _c_eos(m.pressure[x],
                       m.temperature[x],
                       m.density[x],
                       m.molarMass,
                       m.gasConstantR,
                       m.z[x])


m.c_eos = Constraint(m.x, rule=_c_eos_rule)

# flow velocity equation

def _c_vel_flow(q, v, rho, A):
    return A * rho * v - q


def _c_vel_flow_rule(m, x):
    return 0 == _c_vel_flow(m.massFlow,
                            m.velocity[x],
                            m.density[x],
                            m.crossSectionalArea)


m.c_vel_flow = Constraint(m.x, rule=_c_vel_flow_rule)


# a smooth reformulation of the flow term with friction: lambda(q)|q|q (=phi)


def _c_friction(phi, q, k, D, e, d, A, eta):
    beta = k/(3.71 * D)
    lambda_slant = 1/(2*log10(beta))**2
    alpha = 2.51 * A * eta / D
    delta = 2 * alpha/(beta*log(10))
    b = 2 * delta
    c = (log(beta) + 1) * delta**2 - (e**2 / 2)

    root1 = sqrt(q**2 + e**2)
    root2 = sqrt(q**2 + d**2)

    return phi - lambda_slant * (root1 + b + c/root2) * q


def _c_friction_rule(m):
    return 0 == _c_friction(m.phiVar,
                            m.massFlow,
                            m.innerPipeRoughness,
                            m.pipeDiameter,
                            m.e,
                            m.d,
                            m.crossSectionalArea,
                            m.dynamicViscosity)


m.c_friction = Constraint(rule=_c_friction_rule)

# energy balance


def _diffeq_energy(q, specificIsobaricHeatCapacity, dTdx, T, rho, z, dzdT, dpdx, g, s, pi, D, c_HT, T_soil):
    return q * specificIsobaricHeatCapacity * dTdx - (q * T / (rho * z) * dzdT * dpdx) + (q * g * s) + (pi * D * c_HT * (T - T_soil))


def _diffeq_energy_rule(m, x):
    # if x == start:
    #     return Constraint.Skip
    return 0 == _diffeq_energy(m.massFlow,
                               m.specificIsobaricHeatCapacity[x],
                               m.dtemperaturedx[x],
                               m.temperature[x],
                               m.density[x],
                               m.z[x],
                               dzdT(m.pressure[x],
                                    m.temperature[x],
                                    m.criticalPressure,
                                    m.criticalTemperature,
                                    formula=formula),
                               m.dpressuredx[x],
                               m.gravitationalAcceleration,
                               m.pipeSlope,
                               m.pi,
                               m.pipeDiameter,
                               m.c_HT,
                               m.temperatureSoil)


m.diffeq_energy = Constraint(m.x, rule=_diffeq_energy_rule)

# momentum balance


def _diffeq_momentum(rho, dpdx, q, A, drhodx, g, s, phi, D):
    return rho * dpdx - q**2 / (A**2) * drhodx / rho + g * rho**2 * s + phi / (2 * A**2 * D)


def _diffeq_momentum_rule(m, x):
    # if x == start:
    #     return Constraint.Skip
    return 0 == _diffeq_momentum(m.density[x],
                                 m.dpressuredx[x],
                                 m.massFlow,
                                 m.crossSectionalArea,
                                 m.ddensitydx[x],
                                 m.gravitationalAcceleration,
                                 m.pipeSlope,
                                 m.phiVar,
                                 m.pipeDiameter)


m.diffeq_momentum = Constraint(m.x, rule=_diffeq_momentum_rule)

##################################################################################
# Discretization
##################################################################################

discretizer = TransformationFactory('dae.finite_difference')
discretizer.apply_to(m, nfe=60, wrt=m.x, scheme='BACKWARD')

##################################################################################
# Initial conditions
##################################################################################

m.pressure[start].fix(PRESSURE)
m.temperature[start].fix(TEMPERATURE)

##################################################################################
# Objective
##################################################################################
# constant
m.obj = Objective(expr=1)

m.pprint()
##################################################################################
# Solve
##################################################################################

solver = SolverFactory('scip')
# solver = SolverFactory('scip', executable="C:/Users/t.triesch/Desktop/scipampl-7.0.0.win.x86_64.intel.opt.spx2.exe")
results = solver.solve(m, tee=True, logfile="pipe.log")

##################################################################################
# Plot
##################################################################################

distance = [i/1000 for i in list(m.x)]
p = [value(m.pressure[x])/1e6 for x in m.x]
t = [value(m.temperature[x]) for x in m.x]
rho = [value(m.density[x]) for x in m.x]
v = [value(m.velocity[x]) for x in m.x]


fig = plt.figure()
gs = fig.add_gridspec(4, hspace=0.5)
axs = gs.subplots(sharex=True)
fig.suptitle('p[start] = {0} [MPa], p[end] = {1} [MPa],\n T[start]= {2} [K],\n massFlow[:]= {3} [kg/s],\n total length: {4} m'.format(
    p[0], p[-1], t[0], m.massFlow.value, PIPE_LENGTH))
axs[0].plot(distance, p, '-')
axs[0].set(ylabel='p [MPa]')
axs[0].set_ylim([0, 10])
axs[0].grid()
axs[0].set_yticks([i for i in range(0, 11)])
axs[1].plot(distance, t, '-')
axs[1].set(ylabel='T [K]')
axs[1].set_ylim([250, 350])
axs[1].grid()
axs[2].plot(distance, rho, '-')
axs[2].set(ylabel='rho [kg/m^3]')
axs[2].grid()
axs[3].plot(distance, v, '-')
axs[3].set(ylabel='v [m/s]')
axs[3].grid()

for ax in axs.flat:
    ax.set(xlabel='distance [km]')

plt.show()
Tobias Triesch
  • 152
  • 1
  • 9

0 Answers0