In this tutorial, a Generative Function will be built-up step-by-step for exploring tradeoffs. An airfoil will be used as an example, taking the flow conditions and shape parameters as inputs, and outputting an image of the resultant airfoil and its performance (e.g. lift and drag).

The tutorial will focus on writing and using a Generative Function, rather than the details of creating and analysing airfoils.

Final outcome of the tutorial.

You can also see the code defining the Generative Function and the finished project in the app.

Setup

Prerequisites: Python + an IDE (a code editor)

Optionally, to more easily manage Python packages, you’ll need a Python package manager.

See the Setup page if you’re missing any of these.

Initialising a code repository

To create and run a Generative Function you need somewhere to store and run the code. This is simply a folder anywhere on your computer that has access to Python.

1

Create an empty folder

If you are using an IDE (code editor), you can probably do this step there.

We recommend you do not create a venv, which some IDEs will do by default when making a new project, unless you’re already comfortable with virtual environments.

If you want to manage different Python packages per repository, we recommend you use a package manager - see the ‘Advanced’ track in the Setup guide for how to set one up.

2

Install the Generative packages

Install Generative packages generative-core and generative-server from pypi.generative.vision.

Open a terminal window, navigate to the folder you created, and run:

pip
pip install --extra-index-url https://pypi.generative.vision/simple generative-core generative-server
python -m generative.server --version

If this is successful, the version of generative.server will be printed.

If you’re not using a virtual environment (venv), this installs Generative packages globally - so you’d only need to do this once per machine. If you are using a venv then you will need to install generative server inside the venv.

Installing other dependencies

This tutorial uses Python packages numpy, Matplotlib and Neural Foil. Install them by running in your terminal:

pip install numpy matplotlib neuralfoil

Writing the Generative Function

In this section, the Generative Function will be defined in Python code.

You can see the complete code which we’ll build up through this section.

Defining input and output Generative types

Generative Types are Python classes subclassing GenerativeType, which is imported from generative.core. They let you structure your data with clearly defined fields, types, and validation. Here, several Generative Types are defined that describe both the input parameters that will be varied and the output results that will be returned.

Create a Python file in your folder. We’ll call the file airfoil.py, but it can be anything you like. Copy the below code to that file.

airfoil.py
from generative.core import Asset, GenerativeType
from pydantic import Field

# Inputs:

class ParsecParams(GenerativeType):
    """PARSEC airfoil parametrisation: http://pubs.sciepub.com/ajme/2/4/1. Defaults to NACA0012."""
    leading_edge_radius: float = Field(ge=0.001, le=0.999, default=0.0155)
    upper_crest_x_location: float = Field(ge=0.001, le=0.999, default=0.2966)
    upper_crest_y_location: float = Field(ge=0, le=0.4, default=0.06002)
    upper_crest_curvature: float = Field(ge=-30, le=30, default=-0.4515)
    lower_crest_x_location: float = Field(ge=0.001, le=0.999, default=0.2966)
    lower_crest_y_location: float = Field(ge=-0.4, le=0, default=-0.06002)
    lower_crest_curvature: float = Field(ge=-30, le=30, default=0.4515)
    trailing_edge_vertical_offset: float = Field(ge=-0.3, le=0.3, default=0)
    trailing_edge_thickness: float = Field(ge=0.0, le=0.2, default=0.0025)
    trailing_edge_angle_degrees: float = Field(ge=-30, le=30, default=0)
    trailing_edge_wedge_angle_degrees: float = Field(ge=0.001, lt=90, default=12.89)

class FlowConditions(GenerativeType):
    angle_of_attack: float = 0.0
    reynolds_number: float = Field(gt=0.0, default=1_000_000)
    mach: float = Field(gt=0.0, default=0.4)

class Config(GenerativeType):
    n_points_per_side: int = 100
    model_size: str = "large"


# Outputs:

class AnalysisOutputs(GenerativeType):
    lift_coefficient: float
    drag_coefficient: float
    moment_coefficient: float
    lift_drag_ratio: float
    analysis_confidence: float

class Outputs(GenerativeType):
    plot: Asset
    analysis: AnalysisOutputs

Inputs are grouped together into three Generative Types, one for the airfoil parameterisation, another for the flow conditions and a final one for the configuration. The airfoil parameterisation uses the PARSEC method, which defines the airfoil’s shape using 11 parameters.

All outputs are collected into one Generative Type, Outputs. The plot field in this class has an Asset type annotation, which allows non-numeric data (such as videos, 3D geometry, or images) to be passed to the app. Outputs also contains a field AnalysisOutputs, which is another Generative Type containing the analysis results.

Setting defaults and bounds

Default values and bounds are set for inputs using Pydantic’s Field.

For example:

  • leading_edge_radius: float = Field(ge=0.001, le=0.999, default=0.0155) means that the value for leading_edge_radius must be greater-than-or-equal-to 0.001, less-than-or-equal-to 0.999, and has a default value of 0.0155.
  • mach: float = Field(gt=0.0, default=0.4) means that the value for mach must be greater than 0, and has a default of 0.4.

These defaults and bounds not mandatory, but they help to make sure the Generative Function operates within reasonable ranges and provide a starting point for each parameter - you’ll see later exactly how the app treats these, or you can learn more in the concepts section.

Generative Function definition

With the Generative Types defined, it’s now possible to write a Generative Function using the @generative_function decorator, which is imported from generative.core. Add the function below to your Python file. It takes three of the Generative Types we defined earlier as inputs and returns the Outputs Generative Type. Internally the code uses other normal Python functions for validation and calculations. We’ll define those functions in the next few steps.

from generative.core import generative_function

@generative_function
def airfoil(airfoil_params: ParsecParams, conditions: FlowConditions, config: Config) -> Outputs:
    airfoil_coords = parsec_airfoil(airfoil_params, config.n_points_per_side)
    check_surface_overlap(airfoil_coords)
    plot = plot_airfoil(airfoil_coords)
    analysis_output = neuralfoil_analysis(airfoil_coords, conditions, model_size=config.model_size)
    return Outputs(plot=plot, analysis=analysis_output)

The generative function has been called airfoil, which is the name that will appear in the app. Its inputs are three variables of the input types, which were defined above. Each field of the input Generative Types can be accessed in the function. For example config.n_points_per_side gives the integer value input for the number of discrete points to use when generating the airfoil shape.

On the last line, an instance of Outputs is created, passing in all its fields by keyword (Outputs(plot=plot, analysis=analysis_output)), and then returned.

Generating the airfoil

The Generative Function calls a function parsec_airfoil(), which should create a list of coordinates defining the upper and lower surfaces of the airfoil using the PARSEC method.

This function is normal Python, it has no special Generative properties, and should be defined as follows.

from enum import Enum
import numpy as np

def parsec_airfoil(params, n_points_per_side):
    """Produce airfoil from PARSEC parameters (http://pubs.sciepub.com/ajme/2/4/1)"""
    upper = _parsec_airfoil_surface_coords(params, Surface.UPPER, n_points_per_side)
    lower = _parsec_airfoil_surface_coords(params, Surface.LOWER, n_points_per_side)
    # Build coords anti-clockwise from upper trailing edge
    return list(reversed(upper)) + lower[1:]

def degrees_to_radians(value: float) -> float:
    return value * np.pi / 180.0

class Surface(Enum):
    UPPER = 0
    LOWER = 1

def _parsec_airfoil_surface_coords(params, surface, n_points):
    """Produce coordinates of a surface running from LE to TE. All angles in radians."""

    # Use cosine spacing to group points at the curvy bits of the airfoil
    x_coords = 0.5 * (1 - np.cos(np.linspace(0, np.pi, n_points)))

    x_matrix = np.zeros([n_points, 6], dtype=np.float64)
    for i in range(n_points):
        for j in range(6):
            x_matrix[i, j] = x_coords[i] ** (j + 0.5)

    if surface == Surface.UPPER:
        crest_x_loc = params.upper_crest_x_location
        crest_y_loc = params.upper_crest_y_location
        crest_curv = params.upper_crest_curvature
        te_y_coord = params.trailing_edge_vertical_offset + 0.5 * params.trailing_edge_thickness
        te_angle = degrees_to_radians(
            params.trailing_edge_angle_degrees - 0.5 * params.trailing_edge_wedge_angle_degrees
        )
        le_fac = 1.0
    else:
        crest_x_loc = params.lower_crest_x_location
        crest_y_loc = params.lower_crest_y_location
        crest_curv = params.lower_crest_curvature
        te_y_coord = params.trailing_edge_vertical_offset - 0.5 * params.trailing_edge_thickness
        te_angle = degrees_to_radians(
            params.trailing_edge_angle_degrees + 0.5 * params.trailing_edge_wedge_angle_degrees
        )
        le_fac = -1.0

    c = np.ones([6, 6], dtype=np.float64)
    for i in range(6):
        c[1, i] = crest_x_loc ** (0.5 + i)
        c[2, i] = 0.5 + i
        c[3, i] = crest_x_loc ** (-0.5 + i)
        c[4, i] = crest_x_loc ** (-1.5 + i)
        if i != 0:
            c[5, i] = 0.0
    c[3, :] = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5] * c[3, :]
    c[4, :] = [-0.25, 0.75, 3.75, 3.75, 15.75, 24.75] * c[4, :]

    le_term = le_fac * (2 * params.leading_edge_radius) ** 0.5

    b = np.array(
        [te_y_coord, crest_y_loc, np.tan(te_angle), 0.0, crest_curv, le_term], dtype=np.float64
    )

    a = np.linalg.solve(c, b)
    y_coords = np.dot(x_matrix, a)

    return list(zip(x_coords, y_coords, strict=True))

Validation

Validation of input data is useful to prevent calculation of the outputs when it’s already clear that the design isn’t viable. We’ve already defined some validation using bounds, but due to the complex nature of the PARSEC parameterisation these bounds on individual parameters don’t guarantee that the resulting airfoil in valid. In particular, it’s possible for the upper and lower surfaces of the airfoil to overlap, producing an invalid geometry.

To catch and reject these malformed airfoils, add the following function to your file. This raises an Exception if the upper and lower airfoil surfaces overlap. Any Exceptions raised during the evaluation of a Generative Function will be caught and logged, and the app will show a failed design which has inputs but no outputs.

import numpy as np

def _separate_surfaces(coordinates):
    """Separate upper and lower surfaces, returning tuple of (upper surface, lower surface)"""
    try:
        leading_edge_idx = next(i for i, (x, _) in enumerate(coordinates) if x == 0.0)
    except StopIteration as e:
        raise ValueError("No leading edge found") from e

    s1 = coordinates[leading_edge_idx:]
    s2 = list(reversed(coordinates[: leading_edge_idx + 1]))
    return (s1, s2) if s1[1][1] > s2[1][1] else (s2, s1)

def check_surface_overlap(airfoil_coords):
    """Raise `ValueError` if the upper and lower surfaces overlap at any point. Return `None`."""
    tol = 1e-6
    upper_surface, lower_surface = _separate_surfaces(airfoil_coords)
    upper_x_vals = [x for x, _ in upper_surface]
    upper_y_vals = [y for _, y in upper_surface]
    for x_low, y_low in lower_surface:
        y_up = np.interp(x_low, upper_x_vals, upper_y_vals)
        if y_low - tol > y_up:
            raise ValueError("Lower and upper surfaces overlap")

Creating an Asset

A function plot_airfoil() needs to be defined which returns an image of the airfoil as an Asset. This will allow us to see a plot of the airfoil’s upper and lower surfaces in the app.

The function creates a plot of the airfoil using Matplotlib then, in the highlighted lines at the end of the function, saves this plot as an Asset.

The image is a file, rather than a digital asset at a URL, so the type FileAsset must be used (from generative.core). First a FileAsset instance is created - asset = FileAsset(extension="png") - which makes an empty temporary file with extension .png. Then the path of the asset is converted to a string and passed to Matplotlib’s function to save the figure.

from matplotlib.figure import Figure
from generative.core import FileAsset

def plot_airfoil(airfoil_coords):
    # Close the trailing edge before plotting
    x_vals = [p[0] for p in airfoil_coords] + [airfoil_coords[0][0]]
    y_vals = [p[1] for p in airfoil_coords] + [airfoil_coords[0][1]]

    fig = Figure()
    ax = fig.subplots()
    ax.plot(x_vals, y_vals, linewidth=2.5, solid_capstyle="round", color="#ECEEED")
    ax.set_aspect("equal", "box")
    ax.axis("off")

    asset = FileAsset(extension="svg")
    fig.savefig(asset.path, transparent=True)

    return asset

Tools that rely on global state, like Matplotlib’s pyplot, can cause memory leaks and crashes when handling used in Generative Functions. Make sure an alternative is used.

For example for Matplotlib, initialise each figure with Figure() to ensure each Generative Function evaluation gets an isolated, reliable plot without interfering with others.

FileAsset is a subclass of Asset, so type of the plot field in the Generative Type Outputs should still be Asset. Learn more in the concepts section.

Airfoil analysis

Finally, add the following function to perform airfoil analysis. This uses Neural Foil to estimate various coefficients from the airfoil’s shape and flow conditions, and returns those coefficients in Generative Type AnalysisOutputs.

import neuralfoil
import numpy as np

def neuralfoil_analysis(airfoil_coords, flow_conditions, model_size):
    aero = neuralfoil.get_aero_from_coordinates(
        coordinates=np.array(airfoil_coords),
        alpha=flow_conditions.angle_of_attack,
        Re=flow_conditions.reynolds_number,
        model_size=model_size,
    )
    return AnalysisOutputs(
        lift_coefficient=aero.get("CL"),
        drag_coefficient=aero.get("CD"),
        moment_coefficient=aero.get("CM"),
        lift_drag_ratio=aero.get("CL") / aero.get("CD"),
        analysis_confidence=aero.get("analysis_confidence"),
    )

Type Hints for input and output parameters are only used for the Generative Function, where they are required, however Type Hints on the other functions could also be used to increase clarity and help catch errors earlier

That's all the coding done!

You can see the complete Generative Function here.

Connecting to the app

Great! Now you have finished writing a Generative Function, the next step is to connect to the app to start generating designs.

1

Start up a local function server

Run the following command from a terminal at the root of your repository or in the folder your Generative Function is in:

python -m generative.server start --no-reload

When you make changes to your function, manually stop the server (e.g. Ctrl-C in Powershell) and run the above command again. If you want the server to automatically restart when you make changes to your function, you can remove the --no-reload option or use --reload, but on Windows this is known to not work as expected so we recommend using --no-reload.

To learn more, see the concepts section.

2

Login to the app

Account is required to proceed. If you haven’t already, sign up at generative.vision.

Open app

3

Create an empty project

Create and navigate to an empty project. Under the ‘Experiment’ tab, your local function server will be automatically detected if it’s running on port 3000.

If your function server is not detected, check the logs in your terminal where you’re running the server. If the server started correctly, you should see a link, which will take you the server’s API docs page.

Generating designs

You should now have a project open in the app connected to your Generative Function on the Experiment page.

If you haven’t got the Generative Function running on a Function Server, you can go to the app and open a copy of the tutorial project ''.

From here you can use the Generative Function running in our cloud to continue with the rest of the tutorial.

Set up Experiment parameters

Follow the below steps to add parameters to the Experiment and set values for them.

Experiment with parameters set up

1

Add parameters

  • Add the parameters from the image above to the Experiment by clicking Add and selecting them. Dismiss the selector box by clicking outside of it.
  • You’ll now see the input parameters you selected on the left hand side of the screen, and the outputs on the right.

Any inputs not selected will be kept constant at their default value set in the Python code. Any outputs not selected will still be calculated.

2

Set input parameter values from the image above

  • The airfoil shape parameters will be varied between the defined ranges, whilst the Mach number will be held at 0.4 and the angle of attack will be held at 2 degrees.
  • These input values were chosen as they allow a reasonable variation on each parameter around the default values, which are of a common basic airfoil shape - NACA0012.

Ranges are inclusive, so setting a range from 0 - 1 means 0x10\leq x\leq 1.

3

Set output parameter values from the image above

  • The output values set mean the exploration will aim to find designs with an analysis confidence greater than 0.95, and a moment coefficient between -0.15 and 0.15, whilst maximising lift-to-drag ratio, minimising drag.

Adjust the Experiment settings

The exploration algorithm is automatically configured for optimization when targets are set for output parameters.

Ensure the algorithm settings are as follows:

  • Algorithm: Particle swarm
  • Designs: 500
  • Particles: 50

Adjust algorithm settings

Increasing the number of particles from the default value enables the algorithm to explore a broader range of the design space before focusing on a specific region. As a rule of thumb, the number of designs should be at least 10x the number of particles to provide the algorithm with enough iterations to explore the design space.

These adjustments are particularly beneficial for the airfoil, as its parameters are highly interdependent.

Click Generate

The Experiment is now set up and the Generate button should be activated. Press Generate and watch designs being generated!

Failed designs

With these inputs, a few of the designs fail the validation we wrote. These designs appear with warning triangles showing that they failed to generate outputs.

Viewing results

Navigate to the ‘Discover’ tab to start viewing results.

If you copied the existing tutorial project from within the app rather than making your own, an Insight tab will already exist. Click the plus at the bottom of the page to create a new Insight tab to follow the rest of the tutorial.

Optimisation results

From the ‘Discover’ tab, follow the prompts on screen to add a visualisation of the latest experiment with parameters ‘lift-to-drag ratio’ and ‘analysis confidence’.

Then add another view by clicking on the plus icon + on the right of the screen. Add parameter ‘plot’ to that view, and choose selected design from the dropdown.

Select a design furthest to the right in the graph to view the airfoil with the highest lift-to-drag ratio.

Optimisation results on Discover page

You can see on the left side of the graph some of the early designs generated whilst the algorithm was widely exploring the design space. These include some rather strange looking airfoils.

The algorithm soon identifies some more promising geometries and gradually starts generating designs further towards the top right of the graph - ones with higher lift-to-drag ratio and higher analysis confidence.

The exploration breifly explores the less good areas of the designs space

The algorithm starts with randomly chosen designs and iteratively refines them to meet the specified targets. It doesn’t guarantee that the most optimal design for the given input criteria has been found, and if you run the same exploration twice, you’ll get slightly different results.

Exploring Tradeoffs

Add a new Insight tab by clicking on the + button next to the Insight tabs at the bottom of the page.

Add a visualisation of the latest experiment with parameters ‘lift-to-drag ratio’, ‘moment coefficient’ and ‘drag coefficient’.

As before, add another view by clicking on the plus icon + on the right of the screen. Add parameters ‘plot’ and ‘analysis confidence’ to that view, and choose selected design from the dropdown.

Zooming in on the data and rotating the 3D plot to look at ‘lift-to-drag ratio’ and ‘drag coefficient’, you can see that there’s a tradeoff between these parameters - designs which have higher lift-to-drag ratio tend to also have a slightly higher drag coefficient, with the best designs furthest towards the bottom right hand corner of the plot.

A high lift-to-drag ratio increases aerodynamic efficiency, while a low drag coefficient lowers overall energy consumption. Designers must balance these two factors to achieve optimal performance.

Tradeoffs on Discover page - the orange dashed line superimposed on the image shows the tradeoff curve.

Rotating the plot, you can see that most of the designs have met the target for the moment coefficient to be between -0.15 and 0.15.

A moment coefficient close to 0 is desirable as it is directly correlated with the size of the horizontal stabiliser, often a tail wing, that is required.

In the right hand view you see the analysis confidence of the selected design to verify that it’s high enough.

Further Experiments

If you want to experiment further, you could try calculating the internal area of the airfoil in the Python code for the Generative Function and add that to the Experiment.

This would allow you to explore further design tradeoffs by setting a Range constraint on the internal area to ensure that it remains above a certain size. Airfoils need to have sufficient internal area to accommodate fuel storage and structural supports.

Finished project

Congratulations, you finished the tutorial!

To see the finished project:

  1. Go to the app
  2. Open a copy of the tutorial project ''
  3. Navigate to the ‘Discover’ tab to view generated designs