pflacs: Faster loadcases and parameter studies

enhancing the engineering design process with Python

Stephen McEntee

Python Conference Ireland 2019-10-12

https://qwilka.github.io/PyConIE2019

about me ...

2019-09-30_Visinum_screenshot.jpg

2019-09-30_Visinum_screenshot1.jpg

Overview of presentation

Remarks on the use of computers in engineering design

(based on personal experience working in a small engineering design consultancy in the oil & gas industry since 1990s)

  • "I'm an engineer, not a computer programmer. I want to spend my time engineering, not sitting at a computer."
    • (professor of mechanical engineering, late 1980s)
  • 1990s: a PC on every engineer's desk
    • MS Windows 95, Office
    • GUI software applications
    • familiar environment, intutitive
    • minimal training required
    • productivity boost
  • engineers don't need to learn to program
    • computer programmers write the programs
    • engineers focus on engineering
    • best of both worlds
  • sometimes the software was the worst-of-both-worlds
    • engineers couldn't write a software specification
    • programmers didn't understand how engineers work
    • personal preferences intruding
    • poor interfaces, integration
    • quality and productivity suffered

Typical engineering design process (in brief)

  • Establish the Design Basis from input information & data, specifying:
    • design parameters
    • constraints & assumptions
    • design codes and standards
  • Hierarchical structure of analysis:
    • base-case
      • load cases
        • parameter studies
  • Analyse in accordance with prescibed Design Code
    • levels of analysis: 1, 2 and 3
    • deterministic ('conservative' assumptions used to deal with uncertainty)
  • Final goal: compliance with design code & regulations
    • not an optimisation process

Engineering design process workflow

  • Iterative process (frequent re-work)
  • Typical software tool chain:
    • spreadsheet (level 1 analysis)
    • computational worksheet (level 1 analysis)
    • finite element program (level 3 analysis)
    • word processor for reporting
  • PC on every desk
    • GUI point-n-click environment
    • data entry a cumbersome, repetitive, manual process
    • formatting documentation a major time-sink
    • main productivity tool: copy-n-paste
    • calculations computerized
      • but checking and QA fully manual
  • In effect, manual paper-based workflows were re-implemented on PCs
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

Using Python for engineering computation: a simple example

Calculating water pressure: $P = \rho \cdot g \cdot h $

$\rho$ is water density,    $g$ is gravitational acceleration,    $h$ is water depth ("head")

calculate water pressure with a Python script

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}")
In [1]:
%run water_pressure_script.py
Inputs:
	density = 1025
	g = 9.81
	depth = 10
Output:
	water pressure = 100552.5

calculate water pressure with a Python function

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
In [2]:
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

In [3]:
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.
In [4]:
import matplotlib.pyplot as plt
import numpy
depth = numpy.arange(0, 101, 10)
pressure = water_pressure(depth, 1025) * 10**-5
In [5]:
plt.plot(depth, pressure, "bo-")
plt.xlabel('water depth (m)'); plt.ylabel('pressure (bar)');

calculate water pressure with a Python class

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
In [6]:
from water_pressure_class import Ocean
baltic = Ocean("Baltic", 55.0, 1010.0)
isea = Ocean("Irish Sea", 80.0, 1025.0)
In [7]:
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.

Objectives for computational framework

  • flexibility
    • libraries of plain functions for computations
    • maintains the widest range of possibilities
  • convenience
    • object-orientated framework
    • preferably light-touch and unoptionated
  • ability to utilise external libraries
  • hierarchical
  • scalable
  • automatic reporting

Can we have the best of both worlds: functions and classes?

  • Python is dynamic and flexible
In [8]:
class Pipeline:
    def __init__(self, name, D, t):
        self.name = name    
        self.D = D   # pipeline diameter
        self.t = t   # pipeline wall thickness
In [9]:
# 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) 

The Barlow formula for hoop stress in a pressurised cylinder

$$\sigma_H = \frac{PD}{2t}$$

hoop stress    image: CC BY 3.0 2013 Miltiades C. Elliotis

"pipeline engineering is nothing but PD/2t"

using composition to add a function to a class

In [10]:
def hoop_stress1(P, D, t):
    """Calculate pipe hoop stress in accordance with the Barlow formula."""
    return P * D / 2 / t
In [11]:
Pipeline.hoop_stress = hoop_stress1
In [12]:
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'
In [13]:
Pipeline.hoop_stress = staticmethod(hoop_stress1)
In [14]:
pl_16inch.hoop_stress(100e5,16 * 25.4/1000, 0.5 * 25.4/1000 )
Out[14]:
160000000.0

«patch» a method into to a class

In [15]:
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
In [16]:
# patching hoop stress function into Pipeline class
Pipeline.hoop_stress = hoop_stress2
In [17]:
# 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

"practicality beats purity"

Python is introspective

  • inspect module
    • extract information from live objects
  • inspect.signature
    • get the call signature from a callable object
  • inspect.getargspec
    • "get the names and default values of a Python function’s parameters"
    • Python 2, legacy, depreciated

examining function call signature and source code with the 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 helpis invoked

>>> help(statistics.stdev)

Help on function stdev in module statistics:

stdev(data, xbar=None)
    Return the square root of the sample variance.

some callables may not be introspectable ...

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.'

however, we have been using Python 3.6 in this example

>>> platform.python_version
'3.6.9'

let's upgrade to Python 3.7

>>> 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

Introducing Python module pflacs

  • pflacs «faster load cases and parameter studies»
    • https://github.com/qwilka/pflacs
    • a lightweight, unopinionated framework for carrying out studies involving multiple computations
    • hierarchical organisation, tree data structure
    • provides classes that can incorporate plain Python functions as bound methods at runtime
    • uses inspect.signature to obtain the call signature of the function
    • function arguments can be explicitly applied or obtained from the object attributes
  • pip install pflacs

Basic usage of pflacs

In [18]:
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

In [19]:
def add_nums(a, b, c=0):
    """Function adds 2 or 3 numbers, and returns the sum."""
    return a + b + c
In [20]:
basecase.plugin_func(add_nums)
basecase.add_nums
Out[20]:
<pflacs.pflacs.PflacsFunc at 0x7f8cb25e1400>
In [21]:
pflacs.Premise.add_nums
Out[21]:
<pflacs.pflacs.PflacsFunc at 0x7f8cb25e1400>
In [22]:
basecase.add_nums is add_nums
Out[22]:
False
In [23]:
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.

In [24]:
basecase.add_nums()
Out[24]:
15
In [25]:
basecase.a + basecase.b == basecase.add_nums()
Out[25]:
True
In [26]:
basecase.add_nums(b=-3)
Out[26]:
7
In [27]:
basecase.a + (-3) == basecase.add_nums(b=-3)
Out[27]:
True
In [28]:
basecase.b
Out[28]:
5
In [29]:
basecase.add_nums(5, 4.02, -3)
Out[29]:
6.02
In [30]:
5 + 4.02 + (-3) == basecase.add_nums(5, 4.02, -3)
Out[30]:
True
In [31]:
print(f"basecase.a={basecase.a}, basecase.b={basecase.b}")
basecase.a=10, basecase.b=5

let's plugin another funcion

In [32]:
def sub_nums(x, y, z=0):
    """Function subtracts 2 or 3 numbers, and returns the result."""
    return x - y - z
In [33]:
basecase.plugin_func(sub_nums, argmap={"x":"a", "y":"b", "z":"c"} )
Out[33]:
True
In [34]:
basecase.add_param("c", 6.5)
Out[34]:
True
In [35]:
basecase.c
Out[35]:
6.5
In [36]:
basecase.sub_nums()
Out[36]:
-1.5
In [37]:
basecase.a - basecase.b - basecase.c == basecase.sub_nums()
Out[37]:
True

pflacs.Premise is a tree

In [38]:
import vntree
issubclass(pflacs.Premise, vntree.Node)
Out[38]:
True

let's make a new loadcase, based on basecase

In [39]:
lc1 = pflacs.Premise("Load case 1", parent=basecase, parameters={"a":100})
lc1.parent.name
Out[39]:
'Base case'
In [40]:
print(basecase.to_texttree())
| Base case
+--| Load case 1

pflacs applies argument values are applied in accordance with the following precedence order:

  1. argument explicitly specified in function call,
  2. node instance attribute,
  3. ancestor node attribute,
  4. original function default value.
In [41]:
lc1.add_nums()
Out[41]:
111.5
In [42]:
lc1.a + lc1.b + lc1.c == lc1.add_nums()
Out[42]:
True
In [43]:
print(f"lc1.a = {lc1.a}, basecase.a = {basecase.a}")
lc1.a = 100, basecase.a = 10
In [44]:
print(f"lc1.b = {lc1.b}, basecase.b = {basecase.b}")
lc1.b = 5, basecase.b = 5
In [45]:
lc1.b is basecase.b
Out[45]:
True

introducing pflacs.Calc

In [46]:
issubclass(pflacs.Calc, pflacs.Premise)
Out[46]:
True
In [47]:
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)

In [48]:
lc1_1.a - lc1_1.b - lc1_1.c == lc1_1()
Out[48]:
True
In [49]:
lc1_1._sub_nums
Out[49]:
88.5
In [50]:
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
Out[50]:
111.5

make a Pandas dataframe from the result of the Calc node execution

In [51]:
lc1_2.to_dataframe()   # dataframe stored in lc1_2._df
Out[51]:
a b c add_nums_result
0 100 5 6.5 111.5

let's make a new loadcase

In [52]:
lc2 = basecase.add_child( lc1.copy() )  # method vntree.Node.copy clones a node or branch

changing the attribute values in the new nodes

In [53]:
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()»"

Let's plugin another function

In [54]:
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")
Out[54]:
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"

PEP 3107

let's create another load case with pflacs.Calc

In [55]:
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()
Out[55]:
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

Let’s take a look at the tree structure of the study we have built:

In [56]:
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:

In [57]:
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:

In [58]:
basecase.savefile("basic_usage_example.pflacs")
Out[58]:
True

To re-open the study, we would use the class method pflacs.Premise.openfile that is inherited from vntree.Node

In [59]:
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:

In [60]:
for node in basecase:
    if type(node) == pflacs.Calc:
        node.to_hdf5()

basic_usage_example_HDFView

Using pflacs to automate a pipeline design project

  • Introducing PDover2t a Python package for computational subsea pipeline engineering.
  • https://github.com/qwilka/PDover2t
  • PDover2t is still in early stage development, and not fully implemented yet

function pdover2t.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,
    }

setting up the project tree


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

Using pflacs with 3rd party external libraries

References

  1. seawater module, functions for physical properties of sea water, author: Bjørn Ådlandsvik, https://github.com/bjornaa/seawater
  2. wall module, subsea pipeline wall thickness design to PD 8010-2, author: Ben Randerson, https://github.com/benranderson/wall
  3. numpy.interp function for 1-d linear interpolation https://docs.scipy.org/doc/numpy/reference/generated/numpy.interp.html
In [61]:
import seawater
import wall

extlib = pflacs.Premise("External libraries test",
                       parameters={"water_depth":10.0})

external function 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]
In [62]:
extlib.plugin_func(seawater.dens)
extlib.add_param("S", 35, "water salinity")
extlib.add_param("T", 8, "water temperature (°C)")
Out[62]:
True
In [63]:
density = extlib.dens()
extlib.add_param("rho_seawater", density, "water density (kg/m3)")
extlib.rho_seawater
Out[63]:
1027.2741886990423

external function 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]
In [64]:
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
In [65]:
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)
In [66]:
plt.plot(extlib.salinities, extlib.densities, "bx")
plt.xlabel(extlib.get_param_desc("salinities")); plt.ylabel(extlib.get_param_desc("densities"));

external function 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,
In [67]:
extlib.plugin_func(numpy.interp, 
            argmap={"x":"S", "xp":"salinities", "fp":"densities"}, 
            newname="interp_water_density");
In [68]:
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 
In [69]:
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 

external function 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.

In [70]:
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.
Out[70]:
False

Conclusions

  • pflacs currently at alpha/proof-of-concept stage
  • Future developments and enhancements
    • parameters with units
    • extend to plugin methods and classes
    • automated reporting
  • GUI
    • JupyterLab plugin
  • Scaling
    • move from desktop to data centre
    • MongoDB / PostgreSQL backend storage
    • platform integration

Thank you for listening!