Qwilka
Visinum
data management and analytics platform pflacs
faster load-cases and paramter studies
vntree
tree data structure
PDover2t
functions for pipelines design
(based on personal experience working in a small engineering design consultancy in the oil & gas industry since 1990s)
Design Basis
from input information & data, specifying:The design process needs to be made more efficient, less costly, and less time-consuming
...
it is not beyond reason to think of design being made essentially automatic,
and the design being documented automatically.
Andrew Palmer & Roger King, Subsea Pipeline Engineering, 2nd ed. PennWell Books, 2008
Calculating water pressure: $P = \rho \cdot g \cdot h $
$\rho$ is water density, $g$ is gravitational acceleration, $h$ is water depth ("head")
water_pressure_script.py
rho = 1025
g = 9.81
h = 10
pressure = rho * g * h
print(f"Inputs:\n\tdensity = {rho}")
print(f"\tg = {g}")
print(f"\tdepth = {h}")
print(f"Output:\n\twater pressure = {pressure}")
%run water_pressure_script.py
Inputs: density = 1025 g = 9.81 depth = 10 Output: water pressure = 100552.5
water_pressure_function.py
def water_pressure(h, rho, g=9.81):
"""Calculate hydrostatic water pressure.
:param h: water depth or head
:param rho: water density
:param g: gravitational acceleration
:returns: hydrostatic water pressure
"""
return rho * g * h
from water_pressure_function import water_pressure
help(water_pressure)
Help on function water_pressure in module water_pressure_function: water_pressure(h, rho, g=9.81) Calculate hydrostatic water pressure. :param h: water depth or head :param rho: water density :param g: gravitational acceleration :returns: hydrostatic water pressure
depth=10.0; density=1025.0
pressure = water_pressure(depth, density)
print(f"water pressure is {pressure} at {depth} depth.")
print(f"water pressure is {water_pressure(90, 1027.5, 9.79)} at 90 depth.")
water pressure is 100552.5 at 10.0 depth. water pressure is 905330.2499999999 at 90 depth.
import matplotlib.pyplot as plt
import numpy
depth = numpy.arange(0, 101, 10)
pressure = water_pressure(depth, 1025) * 10**-5
plt.plot(depth, pressure, "bo-")
plt.xlabel('water depth (m)'); plt.ylabel('pressure (bar)');
water_pressure_class.py
class Ocean:
def __init__(self, name, h, rho):
self.name = name
self.h = h
self.rho = rho
def water_pressure(self, h=None, rho=None, verbose=True):
"""Calculate hydrostatic water pressure.
Note: gravitational acceleration is assumed to be 9.81 m/s2
:param h: water depth or head
:param rho: water density :math:`(\rho)`
:param verbose: print result message
:returns: hydrostatic water pressure
"""
_h = self.h if h is None else h
_rho = self.rho if rho is None else rho
g=9.81
p = _rho * g * _h
if verbose: print(f"{self.name} pressure is {p} at {_h} depth.")
return p
from water_pressure_class import Ocean
baltic = Ocean("Baltic", 55.0, 1010.0)
isea = Ocean("Irish Sea", 80.0, 1025.0)
baltic.water_pressure()
baltic.water_pressure(49.0)
baltic.water_pressure(10.0, rho=1005.0)
isea.water_pressure();
Baltic pressure is 544945.5 at 55.0 depth. Baltic pressure is 485496.9 at 49.0 depth. Baltic pressure is 98590.50000000001 at 10.0 depth. Irish Sea pressure is 804420.0 at 80.0 depth.
class Pipeline:
def __init__(self, name, D, t):
self.name = name
self.D = D # pipeline diameter
self.t = t # pipeline wall thickness
# 16inch diameter pipeline with 1/2 inch wall thickness
# using consistent SI units (metre, newton, Pa, etc...)
pl_16inch = Pipeline('16" pipeline', 16 * 25.4/1000, 0.5 * 25.4/1000)
Barlow
formula for hoop stress in a pressurised cylinder¶image: CC BY 3.0 2013 Miltiades C. Elliotis
def hoop_stress1(P, D, t):
"""Calculate pipe hoop stress in accordance with the Barlow formula."""
return P * D / 2 / t
Pipeline.hoop_stress = hoop_stress1
try:
pl_16inch.hoop_stress()
except Exception as err:
print("Error in Pipeline object:", err)
Error in Pipeline object: hoop_stress1() missing 2 required positional arguments: 'D' and 't'
Pipeline.hoop_stress = staticmethod(hoop_stress1)
pl_16inch.hoop_stress(100e5,16 * 25.4/1000, 0.5 * 25.4/1000 )
160000000.0
def hoop_stress2(obj, P, D=None, t=None):
_D = obj.D if D is None else D
_t = obj.t if t is None else t
return P * _D / 2 / _t
# patching hoop stress function into Pipeline class
Pipeline.hoop_stress = hoop_stress2
# calculate hoop stress at 100 bar pressure
pres = 100 * 10**5 # convert bar into Pa
sigma_H = pl_16inch.hoop_stress(pres)
print(f"{pl_16inch.name} hoop stress is {sigma_H * 10**-6} MPa at pressure {pres * 10**-5} bar")
16" pipeline hoop stress is 160.0 MPa at pressure 100.00000000000001 bar
Definition: «monkey-patch»
dynamic modification of a class or module at runtime
inspect
module¶>>> import inspect, statistics
>>> inspect.signature(statistics.stdev)
<Signature (data, xbar=None)>
>>> inspect.getsource(statistics.stdev)
'def stdev(data, xbar=None):\n """Return the square root of the sample variance.\n\n See ``variance`` for arguments and other details.\n\n >>> stdev([1.5, 2.5, 2.5, 2.75, 3.25, 4.75])\n 1.0810874155219827\n\n """\n var = variance(data, xbar)\n try:\n return var.sqrt()\n except AttributeError:\n return math.sqrt(var)\n'
pydoc
uses inspect.signature
when help
is invoked¶>>> help(statistics.stdev)
Help on function stdev in module statistics:
stdev(data, xbar=None)
Return the square root of the sample variance.
import math
inspect.signature(math.sqrt)
Traceback (most recent call last):
( ... trace details suppressed ... )
ValueError: no signature found for builtin <built-in function sqrt>
>>> inspect.getsource(math.sqrt)
Traceback (most recent call last):
( ... trace details suppressed ... )
TypeError: <built-in function sqrt> is not a module, class, method, function, traceback, frame, or code object
https://docs.python.org/3/library/math.html
CPython implementation detail: The math module consists mostly of
thin wrappers around the platform C math library functions.
>>> help(math.sqrt)
Help on built-in function sqrt in module math:
sqrt(...)
sqrt(x)
Return the square root of x.
(END)
>>> math.sqrt.__doc__
'sqrt(x)\n\nReturn the square root of x.'
>>> platform.python_version
'3.6.9'
>>> platform.python_version()
'3.7.3'
>>> import inspect, math
>>> inspect.signature(math.sqrt)
<Signature (x, /)>
>>> help(math.sqrt)
Help on built-in function sqrt in module math:
sqrt(x, /)
Return the square root of x.
(END)
>>> inspect.getsource(math.sqrt)
Traceback (most recent call last):
( ... trace details suppressed ... )
TypeError: module, class, method, function, traceback, frame, or code object was expected, got builtin_function_or_method
pflacs
¶pflacs
«faster load cases and parameter studies» inspect.signature
to obtain the call signature of the functionpflacs
¶import pflacs
basecase = pflacs.Premise("Base case",
parameters={"a":10, "b":5} )
print(f"basecase.a={basecase.a} basecase.b={basecase.b}")
basecase.a=10 basecase.b=5
pflacs.Premise.plugin_func
used to 'plugin' or patch a function¶def add_nums(a, b, c=0):
"""Function adds 2 or 3 numbers, and returns the sum."""
return a + b + c
basecase.plugin_func(add_nums)
basecase.add_nums
<pflacs.pflacs.PflacsFunc at 0x7f8cb25e1400>
pflacs.Premise.add_nums
<pflacs.pflacs.PflacsFunc at 0x7f8cb25e1400>
basecase.add_nums is add_nums
False
help(basecase.add_nums)
Help on PflacsFunc in module __main__: add_nums(a, b, c=0) Function adds 2 or 3 numbers, and returns the sum.
basecase.add_nums()
15
basecase.a + basecase.b == basecase.add_nums()
True
basecase.add_nums(b=-3)
7
basecase.a + (-3) == basecase.add_nums(b=-3)
True
basecase.b
5
basecase.add_nums(5, 4.02, -3)
6.02
5 + 4.02 + (-3) == basecase.add_nums(5, 4.02, -3)
True
print(f"basecase.a={basecase.a}, basecase.b={basecase.b}")
basecase.a=10, basecase.b=5
def sub_nums(x, y, z=0):
"""Function subtracts 2 or 3 numbers, and returns the result."""
return x - y - z
basecase.plugin_func(sub_nums, argmap={"x":"a", "y":"b", "z":"c"} )
True
basecase.add_param("c", 6.5)
True
basecase.c
6.5
basecase.sub_nums()
-1.5
basecase.a - basecase.b - basecase.c == basecase.sub_nums()
True
pflacs.Premise
is a tree¶import vntree
issubclass(pflacs.Premise, vntree.Node)
True
let's make a new loadcase, based on basecase
lc1 = pflacs.Premise("Load case 1", parent=basecase, parameters={"a":100})
lc1.parent.name
'Base case'
print(basecase.to_texttree())
| Base case +--| Load case 1
pflacs
applies argument values are applied in accordance with the following precedence order:
lc1.add_nums()
111.5
lc1.a + lc1.b + lc1.c == lc1.add_nums()
True
print(f"lc1.a = {lc1.a}, basecase.a = {basecase.a}")
lc1.a = 100, basecase.a = 10
print(f"lc1.b = {lc1.b}, basecase.b = {basecase.b}")
lc1.b = 5, basecase.b = 5
lc1.b is basecase.b
True
pflacs.Calc
¶issubclass(pflacs.Calc, pflacs.Premise)
True
lc1_1 = pflacs.Calc("Load case 1-1 «sub_nums()»", lc1, funcname="sub_nums")
print(lc1_1._root.to_texttree(func=lambda n: f" {n.name} (node type: {n.__class__.__name__})"))
| Base case (node type: Premise) +--| Load case 1 (node type: Premise) . +--| Load case 1-1 «sub_nums()» (node type: Calc)
lc1_1.a - lc1_1.b - lc1_1.c == lc1_1()
True
lc1_1._sub_nums
88.5
lc1_2 = pflacs.Calc("Load case 1-2 «add_nums()»",
lc1,
funcname="add_nums",
argmap={"return":"add_nums_result"})
lc1_2()
lc1_2.add_nums_result
111.5
Calc
node execution¶lc1_2.to_dataframe() # dataframe stored in lc1_2._df
a | b | c | add_nums_result | |
---|---|---|---|---|
0 | 100 | 5 | 6.5 | 111.5 |
lc2 = basecase.add_child( lc1.copy() ) # method vntree.Node.copy clones a node or branch
lc2.name = "Load case 2"
lc2.a = 200
lc2_1 = lc2.get_child_by_name("Load case 1-1 «sub_nums()»") # using vntree.Node.get_child_by_name
lc2_1.name = "Load case 2-1 «sub_nums()»»"
lc2_2 = lc2.get_child_by_name("Load case 1-2 «add_nums()»")
lc2_2.name = "Load case 2-2 «add_nums()»"
def multipily_xyz(k:"a", l:"b", m:"c" = 1) -> "mult_nums_result":
"""Function multiplies 2 or 3 numbers, and returns the product."""
return k * l * m
basecase.plugin_func(multipily_xyz, newname="mult_nums")
True
Using function annotations instead of pflacs.Premise.plugin_func
argument argmap
to re-map argument names and return attribute
"Python does not attach any particular meaning or significance to annotations"
"annotation consumers can do anything they want with a function's annotations"
let's create another load case with pflacs.Calc
lc3 = pflacs.Calc("Load case 3 «mult_nums»", basecase, funcname="mult_nums")
import numpy
lc3.b = numpy.linspace(0,10,5)
lc3()
lc3.to_dataframe()
a | b | c | mult_nums_result | |
---|---|---|---|---|
0 | 10 | 0.0 | 6.5 | 0.0 |
1 | 10 | 2.5 | 6.5 | 162.5 |
2 | 10 | 5.0 | 6.5 | 325.0 |
3 | 10 | 7.5 | 6.5 | 487.5 |
4 | 10 | 10.0 | 6.5 | 650.0 |
print(basecase.to_texttree())
| Base case +--| Load case 1 . +--| Load case 1-1 «sub_nums()» . . | Load case 1-2 «add_nums()» . | Load case 2 . +--| Load case 2-1 «sub_nums()»» . . | Load case 2-2 «add_nums()» . | Load case 3 «mult_nums»
Each vntree.Node
instance is a iterator, so the tree can be traversed simply by interating over the root node.
To re-execute the Calc nodes:
for node in basecase:
if type(node) == pflacs.Calc:
result = node()
print(f"{node.name} calculated {result}")
Load case 1-1 «sub_nums()» calculated 88.5 Load case 1-2 «add_nums()» calculated 111.5 Load case 2-1 «sub_nums()»» calculated 188.5 Load case 2-2 «add_nums()» calculated 211.5 Load case 3 «mult_nums» calculated [ 0. 162.5 325. 487.5 650. ]
Now that our study has been re-calculated, we will save it as Pickle
file:
basecase.savefile("basic_usage_example.pflacs")
True
To re-open the study, we would use the class method pflacs.Premise.openfile
that is inherited from vntree.Node
new_study = pflacs.Premise.openfile("basic_usage_example.pflacs")
print(new_study.to_texttree())
| Base case +--| Load case 1 . +--| Load case 1-1 «sub_nums()» . . | Load case 1-2 «add_nums()» . | Load case 2 . +--| Load case 2-1 «sub_nums()»» . . | Load case 2-2 «add_nums()» . | Load case 3 «mult_nums»
For large projects, saving results in a Pickle
file could be inconvenient, so the results from pflacs.Calc
nodes can be saved in a HDF5
file:
for node in basecase:
if type(node) == pflacs.Calc:
node.to_hdf5()
pflacs
to automate a pipeline design project¶PDover2t
a Python package for computational subsea pipeline engineering.PDover2t
is still in early stage development, and not fully implemented yetpdover2t.dnvgl_st_f101.pressure_containment_all
¶
def pressure_containment_all(p_d, D, t, t_corr, t_fab,
h_l, h_ref, rho_cont, rho_water,
gamma_m, gamma_SCPC, alpha_U, alpha_spt, alpha_mpt,
SMYS, SMTS, T, material, f_ytemp=None, p_t=None, rho_t=None,
gamma_inc=1.1, g=9.81) -> "{}":
"""Pressure containment resistance in accordance with DNVGL-ST-F101.
Reference:
DNVGL-ST-F101 (2017-12) sec:5.4.2.2 eq:5.8 page:94 $p_{b}(t)$
"""
p_cont_res_uty = press_contain_resis_unity(p_li, p_e, p_b)
p_lt_uty = local_test_press_unity(p_li, p_e, p_lt)
p_mpt_uty = mill_test_press_unity(p_li, p_e, p_mpt)
p_cont_uty = press_contain_unity(p_cont_res_uty, p_lt_uty,
p_mpt_uty)
return {
"p_cont_res_uty": p_cont_res_uty,
"p_lt_uty": p_lt_uty,
"p_mpt_uty": p_mpt_uty,
"p_cont_uty": p_cont_uty,
}
rootnode = Premise("Pflacs oil&gas field, subsea", parameters={ **field_params, **constants, **env_params },
data={"desc": "Top-level field details, environmental and universal parameters."})
P01 = Premise("P-01 gas pipeline", parent=rootnode, parameters={ **pipeline_params,
**process_params, **design_params }, data={"desc": "P-01 gas pipeline."})
rootnode.plugin_func("pressure_containment_all", "pdover2t.dnvgl_st_f101")
rootnode.plugin_func("pipe_collapse_all", "pdover2t.dnvgl_st_f101") # ... & other functions to be added...
lc1_cont = Calc("Calc: pressure containment", parent=P01_1, parameters={ "h_l": -370, },
data={"desc": "pressure containment calcs."}, funcname="pressure_containment_all")
P07 = rootnode.add_child(P01.copy())
P07.name = "P-07 oil pipeline"
P07.childs[0].name = "P-07 pipeline section 1, KP 0-1.5"
for _n in rootnode:
if callable(_n):
_n()
print(rootnode.to_texttree())
| Pflacs oil&gas field, subsea
+--| P-01 gas pipeline
. +--| P-01 pipeline section 1, KP 0-0.3
. . +--| Calc: pipe properties
. . . | Calc: pressure containment
. . . | Calc: pipe collapse
. . | P-01 pipeline section 2, KP 0.3-15
. . +--| Calc: pipe properties
. . . | Calc: pressure containment
. . . | Calc: pipe collapse
. . | P-01 pipeline section 3, KP 15-79.7
. . +--| Calc: pipe properties
. . . | Calc: pressure containment
. . . | Calc: pipe collapse
. | P-07 oil pipeline
. +--| P-07 pipeline section 1, KP 0-1.5
. . +--| Calc: pipe properties
. . . | Calc: pressure containment
. . . | Calc: pipe collapse
. . | P-07 pipeline section 2, KP 1.5-6.9
. . +--| Calc: pipe properties
. . . | Calc: pressure containment
. . . | Calc: pipe collapse
pflacs
with 3rd party external libraries¶References
seawater
module, functions for physical properties of sea water, author: Bjørn Ådlandsvik, https://github.com/bjornaa/seawater wall
module, subsea pipeline wall thickness design to PD 8010-2, author: Ben Randerson, https://github.com/benranderson/wallnumpy.interp
function for 1-d linear interpolation https://docs.scipy.org/doc/numpy/reference/generated/numpy.interp.htmlimport seawater
import wall
extlib = pflacs.Premise("External libraries test",
parameters={"water_depth":10.0})
seawater.dens
¶help(seawater.dens)
Help on function dens in module seawater.density:
dens(S, T, P=0)
Compute density of seawater from salinity, temperature, and pressure
Input:
S = Salinity, [PSS-78]
T = Temperature, [�C]
P = Pressure, [dbar = 10**4 Pa]
Output:
Density, [kg/m**3]
extlib.plugin_func(seawater.dens)
extlib.add_param("S", 35, "water salinity")
extlib.add_param("T", 8, "water temperature (°C)")
True
density = extlib.dens()
extlib.add_param("rho_seawater", density, "water density (kg/m3)")
extlib.rho_seawater
1027.2741886990423
wall.pressure_head
¶help(wall.pressure_head)
Help on function pressure_head in module wall.wall:
pressure_head(h, rho, g=9.81)
Calculate the fluid pressure head.
Parameters
h : array : Water depth [m]
rho : array : Fluid density [kg/m^3]
g : float : Acceleration of gravitiy [m/s/s]
Returns
P_h : array : Pressure head [Pa]
extlib.plugin_func(wall.pressure_head,
argmap={"h":"water_depth", "rho":"rho_seawater"});
extlib.add_param("pressure", desc="pressure (Pa)")
extlib.pressure = extlib.pressure_head()
print(f'«{extlib.name}» pressure = {extlib.pressure} at water depth {extlib.water_depth}')
«External libraries test» pressure = 100775.59791137604 at water depth 10.0
extlib.add_param("salinities", desc="water salinity (g/kg)")
extlib.add_param("densities", desc="water density (kg/m^3)")
extlib.salinities = numpy.linspace(30, 40, 11)
extlib.densities = extlib.dens(S=extlib.salinities)
plt.plot(extlib.salinities, extlib.densities, "bx")
plt.xlabel(extlib.get_param_desc("salinities")); plt.ylabel(extlib.get_param_desc("densities"));
numpy.interp
¶help(numpy.interp)
Python Library Documentation: function interp in module numpy
interp(x, xp, fp, left=None, right=None, period=None)
One-dimensional linear interpolation.
Parameters
x : array_like
The x-coordinates at which to evaluate the interpolated values.
xp : 1-D sequence of floats
The x-coordinates of the data points,
fp : 1-D sequence of float or complex
The y-coordinates of the data points,
extlib.plugin_func(numpy.interp,
argmap={"x":"S", "xp":"salinities", "fp":"densities"},
newname="interp_water_density");
rho_interp = extlib.interp_water_density()
print(f"Interpolated water density = {rho_interp}, for salinity = {extlib.S} ")
Interpolated water density = 1027.2741886990423, for salinity = 35
rho_interp = extlib.interp_water_density(S=39.5)
print(f"Interpolated water density = {rho_interp}, for salinity = 39.5 ")
Interpolated water density = 1030.81293808007, for salinity = 39.5
math.sqrt
¶help(math.sqrt)
Help on built-in function sqrt in module math:
sqrt(x, /)
Return the square root of x.
Functions with positional only arguments not currently supported by pflacs
.
import math
extlib.plugin_func(math.sqrt)
Premise.plugin_func: args «func»=«<built-in function sqrt>» «module»=«math» pflacs does not currently support functions with POSITIONAL_ONLY arguments.
False
pflacs
currently at alpha/proof-of-concept stage