General data: User defined results

User defined results allow you to extend OrcaFlex by defining, using Python scripts, additional results. The data are described below and are specified on the general data form.

In essence, user defined results are a convenience feature for post-processing. There is nothing that can be done with user defined results that could not be done with other forms of post-processing. The convenience arises because the user defined results can be used in exactly the same way as any pre-defined results. For example, you can plot graphs of the user defined results in the OrcaFlex GUI. You can extract user defined results using any of the various post-processing interfaces available to OrcaFlex. And so on.

User defined results take advantage of the Python interface to OrcaFlex. The code to calculate the results is run inside the OrcaFlex process, by an embedded Python interpreter. The script has direct access to the underlying model, and typical usage pattern is for the script to first extract some pre-defined results, and then calculate the additional results as a function of those pre-defined results.

Note: See Python Interface: Installation for more information on installing Python, and on the embedded Python distribution that can be automatically installed as part of the OrcaFlex installation process.

Data

The additional results are defined by one or more user defined results Python objects which specify the following data.

Source

Specifies whether the script is embedded directly in the data, or defined in an external file.

Script (embedded scripts only)

The script.

Script file name (external files only)

The file name of the script. You can give either its full path or a relative path.

Parameters (external files only)

Text which is passed to the user defined result function. This text can be used for whatever purposes you like.

User defined results scripts

To illustrate the process of creating a user defined result we will show how to add a new result for 3D buoys named depth. This is defined to be the vertical depth of the buoy below the instantaneous sea surface. The script is as follows:

import numpy

def Depth(info):
    SeaSurfaceZ = info.modelObject.TimeHistory("Sea surface Z", info.period, info.objectExtra)
    Z = info.modelObject.TimeHistory("Z", info.period, info.objectExtra)
    return SeaSurfaceZ - Z

def UserDefinedResults(model):
    return (
        {
            "ObjectType": OrcFxAPI.ObjectType.Buoy3D,
            "Name": "Depth",
            "Units": "LL",
            "TimeDomainFunction": Depth
        },
    )

The download .zip file contains an OrcaFlex model (.dat file) and the Python script (.py file) that defines the additional results. When you open the OrcaFlex model, look at the user defined results page of the general data form to see how the Python script is associated with the model.

Note: You must not explicitly import the OrcFxAPI module in user defined results code. This import is performed automatically by the host OrcaFlex process, to ensure that the imported OrcFxAPI module correctly references the host OrcaFlex process. The user defined results code can access this implicitly imported OrcFxAPI module in the usual way, as is demonstrated in this example.

Broadly speaking, there are two distinct parts to the user defined results script: the results are (i) declared and (ii) calculated.

Declaration

In order to declare the result, the Python script must contain a function named UserDefinedResults. OrcaFlex will load the Python script that you provide, at the beginning of the statics calculation, call this function whose job it is to declare the names (and other information) of the results, and register these results internally.

Because the declaration function is not called until the beginning of the statics calculation, user defined results are not available on the results form when the model is in reset state.

The UserDefinedResults function must return a tuple or list object, with each element being a dict describing one result. In the example above a tuple is returned containing just a single element, so only one new result is defined.

The dict which defines a result must contain, at minimum, the following items:

Further items may also be specified, as will be illustrated in the more complex examples below.

The UserDefinedResults function receives a single parameter containing a reference to the model (an instance of the Model class). In the example above the model parameter is not used, and typically user defined result scripts do not need to refer to it. In some scenarios it may be desirable to customise which results are registered depending on the data present in the model, and this parameter allows you to do that.

Calculation

In our example script above, we declared the result with TimeDomainFunction defined to be the function named Depth. This is the calculation function, which is called when OrcaFlex needs to obtain time history values (or indeed static state results) for the user defined result.

The calculation function is passed a single parameter (by convention it is named info) whose attributes contain additional information required to calculate the time history. The return value must be a numpy array containing the time history values. The example script imports the numpy module even though it does not explicitly use it. User defined results scripts rely on numpy being available, and explicitly importing the module is a way to ensure that a meaningful error message is returned in the event that that numpy is not available.

As a minimum the info object contains the following attributes:

There are additional optional attributes that may be defined, as will be illustrated by the more complex examples below.

The typical usage pattern is to call modelObject.TimeHistory() to extract one or more time histories of pre-defined results, and then to combine them to calculate the user defined result. The period and objectExtra attributes are commonly passed directly to modelObject.TimeHistory(). That is the case in the example above where the Z and sea surface Z results are first extracted, and then one subtracted from the other to calculate the depth.

Note: The above example defines a result for 3D buoy objects, for which the objectExtra never contains any information. So it would be perfectly reasonable to omit the objectExtra parameter when calling modelObject.TimeHistory() inside the calculation function. However, it does no harm to include it, and should we attempt to re-use this function for other object types which do have meaningful objectExtra, then we would need to pass it on to modelObject.TimeHistory(). We will see just such an example below.

Examples

The example above is the simplest possible non-trivial example of a user defined result. We will now demonstrate the more advanced features with some more complex examples.

Results which require object extra fields

As mentioned above the 3D buoy object has no results that require objectExtra to be specified. But suppose we take that example, and try to apply it to the 6D buoy object. The code will work largely unchanged because 6D buoy objects also offer results named Z and sea surface Z. But we do need to make some minor alterations. The script is as follows:

import numpy

def Depth(info):
    SeaSurfaceZ = info.modelObject.TimeHistory("Sea surface Z", info.period, info.objectExtra)
    Z = info.modelObject.TimeHistory("Z", info.period, info.objectExtra)
    return SeaSurfaceZ - Z

def UserDefinedResults(model):
    return (
        {
            "ObjectType": OrcFxAPI.ObjectType.Buoy6D,
            "Name": "Depth",
            "Units": "LL",
            "ObjectExtraFields": [OrcFxAPI.ObjectExtraField.RigidBodyPos],
            "TimeDomainFunction": Depth
        },
    )

The calculation code is unchanged from the 3D buoy example, and the only changes are in the declaration code. The first change is that ObjectType is OrcFxAPI.ObjectType.Buoy6D in order that the result applies to 6D buoy objects.

The only other change is the addition of ObjectExtraFields. The ObjectExtra class contains a number of fields, only some of which are used by each object type; the ObjectExtraFields declaration allows you to specify which of these fields are required by your user defined result. For a 6D buoy, both Z and sea surface Z are reported at a user-specified point $\vec{p}$ on the buoy. It therefore makes sense to treat our depth result in exactly the same way.

By specifying OrcFxAPI.ObjectExtraField.RigidBodyPos we ensure that the OrcaFlex results form displays the data entry control in which a value for $\vec{p}$ can be entered. If we do not specify OrcFxAPI.ObjectExtraField.RigidBodyPos then that control will not be shown, and no value for $\vec{p}$ will be provided to the user defined result calculation function.

The full list of object extra field names can be found in the OrcFxAPI.py source file which defines the Python interface. This file can be found in the OrcaFlex installation directory under OrcFxAPI\Python.

Note that you may wish to specify multiple object extra fields. For instance, if your result requires both support index and supported line name, then you would list both when declaring your result:

"ObjectExtraFields": [OrcFxAPI.ObjectExtraField.SupportIndex, OrcFxAPI.ObjectExtraField.SupportedLineName]

Results for line objects

Line objects introduce an additional layer of complexity because different results are reported at different locations along the line. To illustrate the various options:

This list includes only a small selection of the available results, but it does encompass all the possibilities for result reporting locations. When declaring user defined results for line objects you must define the locations at which each result is to be reported. This is done by including the LineResultPoints item in the declaration. This can take one of the following values:

To illustate this, consider the following example which replicates the pre-defined result Pm, the primary membrane stress.

import numpy

def Pm(info):
    # Pm is defined to be the ZZ stress at the mid-wall
    info.objectExtra.RadialPos = OrcFxAPI.RadialPos.Mid
    return info.modelObject.TimeHistory("ZZ stress", info.period, info.objectExtra)

def UserDefinedResults(model):
    return (
        {
            "ObjectType": OrcFxAPI.ObjectType.Line,
            "Name": "My Pm",
            "Units": "$S",
            "ObjectExtraFields": [OrcFxAPI.ObjectExtraField.Theta],
            "LineResultPoints": OrcFxAPI.LineResultPoints.MidSegmentsAndEnds,
            "TimeDomainFunction": Pm
        },
    )

The code at the start of Pm() modifies the provided object extra by setting the RadialPos attribute to specify the pipe mid-wall. The result is then calculated by returning the time history of ZZ stress for the mid-wall, and for the specified value of Theta. Although the theta value is never explicitly mentioned in the code, it is passed to the user defined result code inside info.objectExtra and then passed on to the TimeHistory() function call.

The ZZ stress result is reported at mid-segment points and the two line ends. Hence we specify LineResultPoints as OrcFxAPI.LineResultPoints.MidSegmentsAndEnds when declaring the result Most user defined results will be calculated from other pre-defined results in this way, so the usual practice is to specify LineResultPoints corresponding to the pre-defined results used in the calculation.

Results for turbine objects

Turbine results can be reported for the entire turbine by omitting the ObjectExtraFields item when declaring the result. Alternatively, for turbine blade results, include OrcFxAPI.ObjectExtraField.BladeIndex in the ObjectExtraFields value.

Results for turbine blades are similar to those for line objects because they can be reported at different locations along the blade. Just as for line objects, this is done by including the LineResultPoints item in the declaration. For blade results this can take one of the following values:

Note that OrcFxAPI.LineResultPoints.WholeLine is used for blade results that do not depend on the location along the blade.

Results for Morison elements

Morison element results for vessels and 6D buoys require OrcFxAPI.ObjectExtraField.ElementIndex to be included in the ObjectExtraFields value.

Results for Morison elements are similar to those for line objects because they can be reported at different locations along the element. Just as for line objects, this is done by including the LineResultPoints item in the declaration. For Morison element results this can take one of the following values:

Note that OrcFxAPI.LineResultPoints.WholeLine is used for Morison element results that do not depend on the location along the element.

Range jump suppression

Range jump suppression is applied to built-in angle results variables. For user defined results, range jump suppression will be applied if SuppressRangeJumps is defined as True in the result variable declaration.

The following example illustrates this with a somewhat artificial result variable that reports the angle formed by a horizonal line between the vessel origin and the global origin. Two variants of the result variable are declared, one with range jumps suppressed, one without. Compare time histories of the two variables to see the effect of suppressing range jumps.

import numpy

def test(info):
    X = info.modelObject.TimeHistory("X", info.period, info.objectExtra)
    Y = info.modelObject.TimeHistory("Y", info.period, info.objectExtra)
    return numpy.degrees(numpy.arctan2(Y, X))

def UserDefinedResults(model):
    return (
        {
            "ObjectType": OrcFxAPI.ObjectType.Vessel,
            "Name": "Test (without suppression)",
            "Units": "deg",
            "TimeDomainFunction": test
        },
        {
            "ObjectType": OrcFxAPI.ObjectType.Vessel,
            "Name": "Test (with suppression)",
            "Units": "deg",
            "SuppressRangeJumps": True,
            "TimeDomainFunction": test
        },
    )

Using object tags

Pre-defined results are calculated from the OrcaFlex model input data and the model's calculated response. Sometimes when creating a user defined result, you will need to use input data that are not part of the OrcaFlex model data. Rather than hard-code these additional values in the Python script, it may be preferable to instead define them in the OrcaFlex model. Doing so promotes better QA: all the data are held together in the OrcaFlex data file, and scripts can be re-used without modification. Object tags provide a mechanism by which such additional data can be included in an OrcaFlex model.

We will demonstrate this by creating a rather artificial result named my normalised tension, defined to be $T_e / T_{max}$ where $T_{max}$ is a user defined normalising constant, specified in the line type object tags.

import numpy

def NormalisedTension(info):
    Te = info.modelObject.TimeHistory("Effective tension", info.period, info.objectExtra)
    Tmax = float(info.lineType.tags.Tmax)
    return Te / Tmax

def UserDefinedResults(model):
    return (
        {
            "ObjectType": OrcFxAPI.ObjectType.Line,
            "Name": "My normalised tension",
            "Units": "",
            "LineResultPoints": OrcFxAPI.LineResultPoints.MidSegmentsAndEnds,
            "TimeDomainFunction": NormalisedTension
        },
    )

The only novel aspect of this example is the way the code obtains Tmax. In the example OrcaFlex data file, each line type has a tag defined with the name Tmax. For Line type1 the associated value is 200 and for Line type2 it is 300. These values are read in the calculation code: info.lineType.tags.Tmax. Note that tag values are always strings, so the text value must be converted to a floating point value using float().

Additional attributes of the info parameter

In the previous example we used info.lineType to obtain a reference to the line type object associated with each line results point. When we introduced the info object above, we mentioned that it sometimes has additional attributes, and this is one such attribute. These additional attributes are provided to result calculation functions to make available values that are otherwise hard to obtain.

For line results, info.modelObject is the line object, and if you wish to obtain data or tag values for that object, that is straightforward. For instance, you might read the contents density using info.modelObject.ContentsDensity.

However, many of the line's properties are defined by the line types. Rather than expect your calculation code to find out which line type corresponds to the result point, that information is provided in an additional info attribute. Furthermore, some line data can be variable (e.g. stiffness, diameter, etc.) and again for convenience some commonly used values are provided as additional info attributes.

The full list of additional attributes for line objects is:

Notes: The stressID and stressOD attributes are provided for two reasons. First of all, the stress diameters are often specified in the data as ~ which means to use the same value as the pipe diameter. Using the additional attributes allows you to ignore that complexity because the attribute contains the actual numerical value used by OrcaFlex. Similarly, if the line has a profiled section, with variable diameter, then rather than reading the profile data and interpolating it, the additional attributes provide the interpolated value directly.
The EA, EIx, EIy, GJ and E attributes are expected to be useful when the underlying data can be variable. For a variable data item, the value provided in the attribute is the nominal value. For instance, for variable axial stiffness, EA contains the slope of the axial stiffness profile evaluated at zero strain.

For turbine blade results that are reported at different locations along the blade a similar set of attributes are provided:

Flex joint and drag chain objects also provide additional attributes, flexJointType and dragChainType respectively. These provide access to the associated attachment type objects.

Frequency domain results

All the examples so far have been limited to time domain results, but it is also possible to implement user defined results for frequency domain analysis. Instead of returning a time history of values, the job of the calculation function is to return an array of complex-valued results process components. Typically, you will do this by calculating the process components for one or more pre-defined results which are, in turn, used to calculate the process components for the final user defined result.

We illustrate this by extending the normalised tension example above to support frequency domain analysis.

import numpy

def getTmax(lineType):
    return float(lineType.tags.Tmax)

def NormalisedTensionTD(info):
    Te = info.modelObject.TimeHistory("Effective tension", info.period, info.objectExtra)
    return Te / getTmax(info.lineType)

def NormalisedTensionFD(info):
    Te = info.modelObject.FrequencyDomainResultsProcess("Effective tension", info.objectExtra)
    return Te / getTmax(info.lineType)

def UserDefinedResults(model):
    return (
        {
            "ObjectType": OrcFxAPI.ObjectType.Line,
            "Name": "My normalised tension",
            "Units": "",
            "LineResultPoints": OrcFxAPI.LineResultPoints.MidSegmentsAndEnds,
            "TimeDomainFunction": NormalisedTensionTD,
            "FrequencyDomainFunction": NormalisedTensionFD
        },
    )

When declaring the result, in addition to TimeDomainFunction we also specify FrequencyDomainFunction. This is the calculation function whose job it is to return the complex-valued results process components for the user defined result. In this very simple example the code is almost identical to that for the time domain function, with the call to TimeHistory() replaced by a call to FrequencyDomainResultsProcess.

Even if you intend to use the result exclusively with frequency domain analyses, you must still provide a time domain calculation function: this is needed to calculate the static state result.

Debugging

A very simple but effective way to debug user defined results code is to print to the external code output window. Any Python print functions in your code are redirected to this window.

Another useful trick is to declare results to report intermediate values. You might remove these results from final versions of your script, but they can be useful while developing the code. Sometimes these values might not be time-varying, but it can still be useful to report a time history, even if all the values are the same. Use numpy.full() to achieve this. For example:

def CalcIntermediateValue(info):
    value = getIntermediateValue(info)
    return numpy.full(info.model.SampleCount(info.period), value)

It can sometimes be more productive to use a debugger, e.g. from your chosen IDE, in which case it is usually simplest to invoke the user defined results code from a small host script rather than from the OrcaFlex GUI. For example, with the 3DbuoyDepth example above, you could use this host script.

import OrcFxAPI

model = OrcFxAPI.Model("3DbuoyDepth.dat")
model.RunSimulation()
depth = model["3D Buoy1"].TimeHistory("Depth")

Load this host script into your IDE and set breakpoints in the user defined result code. When you debug the host script, the breakpoints will be triggered and you can then debug your code.