Automating Load Calculations with Spawn-wind

spawn_logo.png

It's a very common task in the wind industry to run large sets of aeroelastic simulations as part of wind turbine design and certification processes and site-specific loading assessments. In fact, this type of analysis is not unique to the wind industry and it is common amongst many areas of engineering to run thousands of similar simulations to see how a car, aeroplane or building responds in a variety of circumstances and environments. So it is somewhat of a surprise, due to the ubiquity of this task amongst wind turbine designers, consultants and other engineers that there is a shortage of cross-organisation codes that automate the creation and control of these large sets of simulations.

Therefore, last year we put our heads together to come up with a modular and flexible library that could solve this problem in a generic way. We wrote a package in Python 3 and named it spawn (and its wind specific plugin spawn-wind) and we released it open-source under the GPL licence. In this post, I introduce the concept and specifically how it can be applied to automating a large set of wind turbine aeroelastic simulation using NREL's simulation tool FAST.

High-Level Design

The design consists of a front-end - spawn, a general purpose processor of complex parameter sets and combinations - and a back-end - the "spawner", which is responsible for interpreting the parameters (turbine and wind properties) and defining the processes (simulations) and is specific to the simulation tool that you are using. In spawn-wind, there is a spawner ready-built for FAST. After the spawner has defined the processes and their dependencies they are then controlled by spawn using luigi, a workflow management package open-sourced by spotify.

spawn-basic-design.png

Calculation sets and their parameter combinations are defined in the input file, based on JSON format. In itself, the input file specification represents a sort of basic declarative language that defines the set of simulations, designed with the following key principles in mind:

  • Capability of defining complex parameter combinations and inter-dependencies
  • Human readability
  • Conciseness
  • Flexibility

In the rest of this post, I'll introduce you to how the input file works, the features it has and how they apply to creating sets of aeroelastic simulations.

Defining the simulation set

All spawn simulation sets start from the point of having a "baseline" input file (or set of input files) - all the parameter variations are then editions of this baseline. The basics of the input file is that in the JSON format, the keys represent the names of the parameters to be changed.

So to start with a simple case, let's just do one simulation where we change the simulation time to 600s. The spawn input file would be like so:

{
    "spec": {
        "simulation_time": 600.0
    }
}

Let's say we want to do three runs with different wind speeds (all with the new simulation time):

{
    "spec": {
        "simulation_time": 600.0,
        "wind_speed": [6.0, 8.0, 10.0]
    }
}

In spawn-wind, in the case of turbulent wind, the spawner automatically takes care of turbulent wind file generation and will create the prerequisite wind file for each simulation. If in each of these we also want to simulate at two different turbulence intensities (six runs in total), adjacent arrays invoke all combinations (a Cartesian outer-product):

{
    "spec": {
        "simulation_time": 600.0,
        "wind_speed": [6.0, 8.0, 10.0],
        "turbulence_intensity": [12.0, 16.0]
    }
}

If instead of simulating all combinations of the two arrays, we actually want to map one value turbulence intensity to each wind speed, there's a special operation for that called a "combinator" of type "zip":

{
    "spec": {
        "combine:zip": {
            "wind_speed": [6.0, 8.0, 10.0],
            "turbulence_intensity": [16.0, 14.0, 12.0]
        }
    }
}

Better still, if we're running IEC load calculation, we might want to use the "normal turbulence model", often abbreviated NTM. For this, we introduce the concept of "evaluators", prefixed with either eval: or # that consist of built-in functions or simply an equation written inline (anything python evaluable, using ! to reference other parameters):

{
    "spec": {
        "wind_speed": [6.0, 8.0, 10.0],
        "turbulence_intensity": "eval:14.0 * (0.75 * !wind_speed + 5.6) / !wind_speed"
    }
}

The 14.0 is the "reference turbulence intensity" and is a constant for all simulations, so we could declare this up front in what we call a "macro". Whilst we're at it, we'll also standardise the wind speeds using the inbuilt "range" evaluator. Macros are used by using the macro: or $ prefix:

{
    "macros": {
        "Iref": 14.0,
        "WindSpeeds": "#range(4.0, 25.0, 2.0)"
    },
    "spec": {
        "wind_speed": "$WindSpeeds",
        "turbulence_intensity": "eval:$Iref * (0.75 * !wind_speed + 5.6) / !wind_speed"
    }
}

IEC load calculations typically require six turbulence seeds per wind speed. To do this, we could add a field such as "turbulence_seed": [1, 2, 3, 4, 5, 6] and then make sure each part of our spec uses different seeds. But that's not really very practical to keep track of. Therefore, we developed the concept of "generators" with methods such as IncrementalInt (1, 2, 3 etc...) and RandomInt (pseudo-random integers), which produce a new value each time they are referenced in the spec according to their given method. We can combine this with the #repeat evaluator to give us six seeds, and within it invoke the generator by using the gen: or @ prefix:

{
    "macros": {
        "Iref": 14.0,
        "WindSpeeds": "#range(4.0, 25.0, 2.0)"
    },
    "generators": {
        "TurbulenceSeed": {
            "method": "IncrementalInt"
        }
    },
    "spec": {
        "wind_speed": "$WindSpeeds",
        "turbulence_intensity": "eval:$Iref * (0.75 * !wind_speed + 5.6) / !wind_speed",
        "turbulence_seed": "#repeat(@TurbulenceSeed, 6)"
    }
}

Each leaf of the simulation tree will have a path assigned to it, which when running locally indicates the directory into which the simulation result is output. By default, paths will just increment by letter (a-z, aa-zz etc...), but the path can be controlled according to the parameters by using the policy:path feature, and curly braces are used to format according to parameters. The following will create paths such as fatigue\WS1\a (the last directory being a-f for the turbulence seed):

{
    "macros": {
        "Iref": 14.0,
        "WindSpeeds": "#range(4.0, 25.0, 2.0)"
    },
    "generators": {
        "TurbulenceSeed": {
            "method": "IncrementalInt"
        }
    },
    "spec": {
        "policy:path": "fatigue/WS{wind_speed:1}",
        "wind_speed": "$WindSpeeds",
        "turbulence_intensity": "eval:$Iref * (0.75 * !wind_speed + 5.6) / !wind_speed",
        "turbulence_seed": "#repeat(@TurbulenceSeed, 6)"
    }
}

To create different branches (such as different design load cases), parameters can be put into separate JSON objects; the key of each object is irrelevant and is just for readability:

{
    "macros": {
        "Iref": 14.0,
        "WindSpeeds": "#range(4.0, 25.0, 2.0)"
    },
    "generators": {
        "TurbulenceSeed": {
            "method": "IncrementalInt"
        }
    },
    "spec": {
        "dlc1.1": {
            "policy:path": "extreme/WS{wind_speed:1}",
            "wind_speed": "$WindSpeeds",
            "turbulence_intensity": "eval:$Iref * (0.75 * !wind_speed + 5.6) / !wind_speed",
            "turbulence_seed": "#repeat(@TurbulenceSeed, 6)"
        },
        "dlc1.2": {
            "policy:path": "fatigue/WS{wind_speed:1}",
            ...
        }
    }
}

It's starting to get more and more complex, so it's probably time to just point you straight to what a full IEC load calculation specification looks like (depends on your team's interpretation and implementation of course...).

Spawn-wind is easy to install into your python environment with pip install spawn-wind. All these example specs can tried out and played around with by using the spawnwind inspect [SPECFILE] command, which will print out the parameters of all the simulations created and their paths.

BlogPhilip Bradstock