This tutorial provides a step-by-step guide for generating designs using the Generative Platform. It uses design of a cantilever as an example. The inputs will be the cantilever’s dimensions, the material it is made from and the load applied. The outputs will be its deflection under this load, approximate construction cost and a 3D model.

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 trimesh, shapely and triangle to create a 3D model of the cantilever. Install them by running in your terminal:

pip install trimesh shapely triangle

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. Below, several Generative Types are defined that describe both the inputs and outputs for our cantilever example.

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

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

# Inputs:

class CantileverGeometricProperties(GenerativeType):
    length_m: float = Field(default=1, gt=0)
    depth_m: float = Field(default=0.1, gt=0)
    width_m: float = Field(default=0.2, gt=0)
    wall_thickness_m: float = Field(default=0.02, gt=0)

class PointLoadScenario(GenerativeType):
    force_newtons: float = Field(default=10)
    force_location_proportion: float = Field(default=1, ge=0, le=1)

class CantileverInputs(GenerativeType):
    geometry: CantileverGeometricProperties
    material_idx: int = Field(default=0, ge=0, le=1)
    load_case: PointLoadScenario


# Outputs:

class CantileverAnalysisOutputs(GenerativeType):
    slope_radians: float
    deflection_m: float

class CantileverCostOutputs(GenerativeType):
    total_cost_gbp: float
    material_cost_gbp: float
    fabrication_cost_gbp: float

class CantileverOutputs(GenerativeType):
    cost: CantileverCostOutputs
    structural: CantileverAnalysisOutputs
    model: Asset

CantileverGeometricProperties groups together all geometry-related fields (length, depth, width, wall thickness). PointLoadScenario includes the load magnitude and where it’s applied along the beam. CantileverInputs collects these together, along with a material_idx to decide which material to use.

Outputs of cost, structural response and a 3D model are combined into more Generative Types. An Asset type is used for the model field. This allows non-numeric data to be passed to the app, for example images, videos, or 3D geometry.

Setting defaults and bounds

Default values and bounds are set for inputs using Pydantic’s Field - you’ll see later how the app treats these, or you can learn more in the concepts section.

For example:

  • length_m: float = Field(default=1, gt=0) means that the value for length_m must be greater than 0, and has a default value of 1
  • force_location_proportion: float = Field(default=1, ge=0, le=1) means that force_location_proportion must be between 0 and 1 (inclusive) and has a default value of 1.

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 the CantileverInputs Generative Type we defined earlier as its input, and returns the CantileverOutputs Generative Type. Internally it uses other normal Python functions for validation and calculations. We’ll define these in the following sections.

from generative.core import generative_function

@generative_function
def cantilever(inputs: CantileverInputs) -> CantileverOutputs:
    validate_inputs(inputs)

    material = material_database(inputs.material_idx)
    cost = cantilever_cost_model(inputs.geometry, material)
    analysis = cantilever_point_load_analysis(inputs.geometry, inputs.load_case, material)
    model = cantilever_3d_model(inputs.geometry)
    return CantileverOutputs(cost=cost, structural=analysis, model=model)

The generative function has been called cantilever, which is the name that will appear in the app. Each field of CantileverInputs can be accessed in the function, for example inputs.material_idx gives the integer value input for the material index.

On the last line, an instance of CantileverOutputs is created by passing in all its fields by keyword, and this instance is then returned.

Validating the input data

The function validate_inputs() is called at the start of the generative function. This raises an Exception if it finds any non-physical characteristics in the inputs. Copy the code below into your Python file.

def validate_inputs(inputs: CantileverInputs) -> None:
    max_possible_wall_thickness = 0.5 * min(inputs.geometry.width_m, inputs.geometry.depth_m)
    if inputs.geometry.wall_thickness_m >= max_possible_wall_thickness:
        raise ValueError("Total wall thickness larger than width or depth")

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.

Creating an Asset

The 3D model of the cantilever must be an Asset so that can be viewed in the app. The cantilever_3d_model function that constructs the 3D model is defined below. Add it to your Python file. This function is called from the cantilever Generative Function defined above, and creates an Asset from a GLB file (which is a universal mesh file format).

from generative.core import Asset, FileAsset
import trimesh
import shapely

def cantilever_3d_model(geometry: CantileverGeometricProperties) -> Asset:
    half_width, half_depth = geometry.width_m / 2, geometry.depth_m / 2
    wall_offset = geometry.wall_thickness_m
    polygon = shapely.Polygon(
        shell=[
            (-half_width, -half_depth),
            (half_width, -half_depth),
            (half_width, half_depth),
            (-half_width, half_depth),
        ],
        holes=[
            [
                (-half_width + wall_offset, -half_depth + wall_offset),
                (half_width - wall_offset, -half_depth + wall_offset),
                (half_width - wall_offset, half_depth - wall_offset),
                (-half_width + wall_offset, half_depth - wall_offset),
            ]
        ],
    )
    cantilever_mesh = trimesh.creation.extrude_polygon(
        polygon, height=geometry.length_m, engine="triangle"
    )

    asset = FileAsset()
    cantilever_mesh.export(str(asset.path), file_type="glb")
    return asset

Most of the function is creating a mesh of the cantilever using the Python packages Trimesh, Shapely and Triangle.

Other geometry creating packages could be used instead. For example you could use the Python package CadQuery or connect to a traditional CAD package. Get in touch if you need any help doing this.

The highlighted lines at the end of the function create the Asset. FileAsset (from generative.core) is used because our 3D model is a file, rather than a digital asset at a URL. First a FileAsset instance is created - asset = FileAsset() - which makes an empty temporary file. Then the path of the asset is converted to a string and passed to Trimesh’s normal export function.

FileAsset is a type of Asset, so the return type of the function should still be Asset. Learn more in the concepts section.

Getting material data from the material index

The material index is an integer input to the Generative Function. When defining the Generative Types it was defined to have a value between 0 and 1.

This index is used to get relevant material data from a table, defined in Python as a dictionary. Another Generative Type is used to store the data associated with each material, though a dataclass or dictionary could also be used. Add this to your Python file.

from generative.core import GenerativeType
from pydantic import Field

class MaterialProperties(GenerativeType):
    material_name: str
    modulus_pascals: float = Field(gt=0)
    cost_gbp_per_kg: float = Field(gt=0)
    density_kg_per_m3: float = Field(gt=0)

MATERIAL_DATA = {
    0: MaterialProperties(material_name="steel", modulus_pascals=210e9, cost_gbp_per_kg=10, density_kg_per_m3=78500),
    1: MaterialProperties(material_name="aluminium", modulus_pascals=70e9, cost_gbp_per_kg=100, density_kg_per_m3=2700),
}


def material_database(material_idx: int) -> MaterialProperties:
    try:
        return MATERIAL_DATA[material_idx]
    except KeyError as e:
        raise ValueError(
            f"Unknown material index '{material_idx}', try {list(MATERIAL_DATA.keys())}"
        ) from e

Filling in the rest

The rest of the functions used in the cantilever Generative Function are defined below. They calculate the deflection and slope of the cantilever using standard formulas and produce a rough estimate of the cantilever cost for demonstration purposes.

Copy these functions into your Python file.

def cantilever_point_load_analysis(
    geom: CantileverGeometricProperties,
    load: PointLoadScenario,
    material: MaterialProperties,
) -> CantileverAnalysisOutputs:
    """Calculate cantilever performance using standard formulas: https://w.wiki/D2ue"""
    second_mom_area_m4 = second_moment_of_area(geom.width_m, geom.depth_m, geom.wall_thickness_m)
    load_location = load.force_location_proportion * geom.length_m

    slope_numerator = load.force_newtons * load_location**2
    slope = slope_numerator / (2 * material.modulus_pascals * second_mom_area_m4)
    def_numerator = load.force_newtons * load_location**2 * (3 * geom.length_m - load_location)
    deflection = def_numerator / (6 * material.modulus_pascals * second_mom_area_m4)
    return CantileverAnalysisOutputs(slope_radians=slope, deflection_m=deflection)


def second_moment_of_area(outer_width: float, outer_depth: float, wall_thickness: float) -> float:
    inner_width = outer_width - 2 * wall_thickness
    inner_depth = outer_depth - 2 * wall_thickness
    return (outer_width * outer_depth**3 - inner_width * inner_depth**3) / 12

def cantilever_cost_model(
    geometry: CantileverGeometricProperties,
    material: MaterialProperties,
) -> CantileverCostOutputs:
    outer_cross_section = geometry.width_m * geometry.depth_m
    inner_cross_section = (geometry.width_m - 2 * geometry.wall_thickness_m) * (
        geometry.depth_m - 2 * geometry.wall_thickness_m
    )
    volume = geometry.length_m * (outer_cross_section - inner_cross_section)
    material_cost = material.cost_gbp_per_kg * volume * material.density_kg_per_m3
    fabrication_cost = 10_000 + 100 * volume

    return CantileverCostOutputs(
        total_cost_gbp=fabrication_cost + material_cost,
        fabrication_cost_gbp=fabrication_cost,
        material_cost_gbp=material_cost,
    )

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.

Add parameters to control

Add some parameters to the Experiment by clicking Add parameters and selecting the ones you want. Add depth, width, length, wall thickness, force and material index, and then close the selector box by clicking outside of it.

Any inputs not selected here will be kept constant, at their default value set in the Python code.

Set values for the parameters

You’ll now see the input parameters you selected on the left hand side of the screen.

We haven’t added any output parameters for this tutorial as we’re only interested in generating designs. The outputs will still be calculated, but no optimisation (like trying to minimise or maximise different outputs) will take place.

Set the following for the input parameters:

  • Length to exactly 1
  • Force to exactly 100
  • Depth to range from 0.2 to 0.5
  • Width to range from 0.2 to 0.5
  • Wall thickness to range from 0.02 to 0.1
  • Material index to range from 0 to 1

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

The Experiment is now set up and the Generate button should be activated

The Experiment is now set up and the Generate button is activated

Click Generate

Press Generate and watch designs being generated!

If you left the settings at default, 100 designs will be generated at random in the specified input ranges.

Failed designs

With our inputs, a few of the designs might fail the validation we wrote - the wall thickness is more than twice the width or depth. If this happens these designs appear with warning triangles showing that they failed to generate outputs.

Viewing results

Compare all

Click ‘Compare All’ to navigate to the Compare page and see a tabular view of all the designs you’ve just generated.

Compare all results from an Experiment

Any designs which failed validation show values for their inputs, but no values are shown for their output parameters because an Exception was raised before the Generative Function returned.

If you click the Add button and add parameter force_location_proportion, you’ll can see that it’s always equal to 1 (the default value we set in the Python code) because it wasn’t given a different value or range on the Experiment page.

Discover different views

Navigate to the ‘Discover’ tab and follow the guidance there to add a plot viewing the results from the latest experiment with parameters depth, width and deflection.

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 this step of the tutorial.

Click the plus symbol on the right hand side to add another plot, this time adding parameter ‘model’. Change the collection being plotted in the dropdown menu from all designs to selected design. Now as you select different designs on the 3D plot on the left hand plot, you’ll see the 3D model for that design on the right.

Custom plots on Discover page

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