Overview

This guide will show step-by-step how to transform an existing script into a Generative Function.

We’ll use a script which generates and analyses an airfoil as an example to work from, but the same technique can be used to convert any script you have which generates and analyses a single design or set of data of any kind.

You can find the code before and after the conversion here:

Airfoil background

This guide is showing how to convert any script to a Generative Function. As a concrete example, it uses a script which generates airfoils.

In the example original script, airfoil outlines (cross-sections of a wing) are generated using the PARSEC parameterisation technique described in this publication. From the initial parameterisation, (x,y)(x,y) coordinates of a few hundred points along the airfoil perimeter are calculated. These coordinates are used to plot the airfoil using matplotlib. The Neural Foil Python package is then used to estimate the performance of the airfoil using neural networks, for example its Lift-Drag ratio.

Converting the script into a Generative Function

Scripts take inputs and present outputs in many ways, for example reading and writing to the command line or files. In the example original script here, the inputs were defined as constants at the top of the script, the numeric outputs are printed to the command line, and the visual output (plot) is displayed on the screen.

To convert a script into a Generative Function, first all the inputs and outputs of the script are collected into Generative Types, using the generative.core package. Then a Generative Function is created using those Generative Types and the rest of the logic defined in the script.

Setup

Make sure your setup is complete, and you’ve initialised a repository with the generative.core and generative.server packages installed.

Wrapping inputs and outputs in Generative Types

The inputs and outputs to the Generative Function define what you’ll be able to interact with from the app. It’s often useful to wrap most of the inputs and outputs in GenerativeTypes to group related parameters together.

Inputs

All the variables defined at the top of the script are wrapped in Generative Types (classes which subclass GenerativeType). Field from pydantic is used to store the descriptions, and to add some bounds to each input where we already know some sensible limits.

from generative.core import GenerativeType
from pydantic import Field

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

class FlowConditions(GenerativeType):
    angle_of_attack: float = 0.0
    ...

class Config(GenerativeType):
    n_points_per_side: int = 100
    ...

The inputs are grouped into airfoil parameters and air flow parameters.

Default values are set, either using normal Python syntax, angle_of_attack: float = 0.0, or using Field, upper_crest_x_location: float = Field(..., default=0.2966).

Keyword arguments ge and le are used passed to the Field functions to provide greater-than-or-equal and less-than-or-equal bounds where appropriate. These aren’t mandatory, but are recommend to make sure the Generative Function operates within reasonable parameter ranges.

These bounds and defaults are used by the Generative Engineering Platform when running the Generative Function.

Outputs

Outputs are defined similarly for each of the items which are printed, plotted or saved to file in the original script. An Asset, which is the way to store digital assets (e.g. files) and pass them to the app where they can be visualised, is used for the plot.

from generative.core import GenerativeType, Asset

class AnalysisOutputs(GenerativeType):
    lift_coefficient: float
    drag_coefficient: float
    ...

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

Turn code blocks into Python functions

The Generative Function will need to execute each step in the script. To make this possible, each of the three code blocks which create the airfoil coordinates, plot it, and analyse its performance are turned into normal Python functions.

For example, for analysing the airfoil, the code block in the original script below gets turned into a Python function, taking as input some airfoil coordinates and the flow conditions, and returning the numeric output values stored in the GenerativeType named 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"),
    )

If any of the coefficients like "CL" aren’t in the dictionary aero, then the get() function will return None, and an Exception will be thrown. To make debugging easier, this exception could be caught and dealt with.

Create Assets from files

Any files which should be visible from the app are turned into Assets,

For example, the original code which plots the airfoil is converted into function which saves the plot as a FileAsset (which is a type of Asset):

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

def plot_airfoil(airfoil_coords):
    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()
    ...
    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.

These Python functions aren’t typed, but to keep the code clear and allow type checkers to find errors before runtime, it would be beneficial to add typing to them too. For example, the signature for the above function could become def plot_airfoil(airfoil_coords: list[tuple[float, float]]) -> Asset:

Create the Generative Function

Once all steps from the script have been converted into function, create a function that performs all intended steps. Add the @generative_function decorator to turn it into a Generative Function.

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 function signature (def airfoil(airfoil_params: ParsecParams, conditions: FlowConditions, config: Config) -> Outputs:) defines the type of each input and the output. For example, variable airfoil_params has the type ParsecParams. This is required to allow the Generative Engineering Platform to understand the inputs and outputs of the function.

A new function has also been added, check_surface_overlap(), which throws an exception if the airfoil upper and lower surfaces overlap each other. This ensures that no computation is wasted plotting and analysing any non-physical airfoils.

Start generating designs

Great! Now you have finished converting your script to 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.

You should now have a project open in the app connected to your Generative Function on the Experiment page. Set up the parameters you want to explore in your Experiment and press Generate!

Finished project

Congratulations, you’ve converted your script to a Generative Function!

If you didn’t follow along, but want to see the finished Generative Function in action:

  1. Go to the app
  2. Open a copy of the tutorial project ‘Airfoil’
  3. Navigate to the ‘Discover’ tab