Overview

As seen in the previous section, inputs to and outputs from Generative Functions can have primitive types such as int, float or str.

For more complex problems or functions, Generative Types provide a way to group related fields, apply validation and limits, and add descriptions.

A Generative Type is a special Python class, created by subclassing GenerativeType, that organizes data in an immutable structure, meaning values can’t be changed once they’re set.

These are similar to Python’s dataclasses in that they’re designed for data storage, but with additional capabilities, like supporting files through the use of Assets and performing validation through Pydantic’s BaseModel.

Generative Types can include nested fields, enabling hierarchical structures.

The Generative Engineering Platform uses Generative Types to organize inputs and outputs for Generative Functions, ensuring the correct data types are used and that each field has a clear name and purpose.

Defining Generative Types

Here is an example of a Generative Function for a cantilever analysis, where Generative Types are defined for its inputs and outputs.

from generative.core import GenerativeType, generative_function

class PointLoadScenario(GenerativeType):
    force_newtons: float
    force_location_proportion: float = 1

class CantileverGeometricProperties(GenerativeType):
    length_m: float = 1
    depth_m: float
    width_m: float
    wall_thickness_m: float

class MaterialProperties(GenerativeType):
    modulus_pascals: float = 7e9

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

@generative_function
def cantilever_point_load_analysis(
    geom: CantileverGeometricProperties,
    load: PointLoadScenario,
    material: MaterialProperties,
) -> CantileverAnalysisOutputs:
    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

First, GenerativeType is imported from generative.core, then the Generative Types are defined by subclassing GenerativeType.

Inputs and outputs are grouped logically, improving usability in the app by organizing data for each Generative Function.

This is similar to the example in the Generative Functions section of the docs, except now the cantilever is hollow, which allows a bigger design space to be explored, and it’s slope is also calculated.

Instead of returning multiple values, we return a CantileverDeflectionOutputs Generative Type, ensuring that each field is clearly labeled and always present in the output.

Syntax rules

Generative Types shouldn’t have any methods that modify their data (because they’re immutable), and in particular the __init__ method is defined automatically and shouldn’t be overridden.

To create an instance of a Generative Type, use keyword arguments, e.g. CantileverDeflectionOutputs(deflection_m=1, slope_radians=2). If you try to provide a field that wasn’t originally defined in the Generative Type, an error will occur.

Default values

Default values can be specified for fields, like force_location_proportion = 1.

If a value for a field isn’t passed to the Generative Type when instantiating, then its default value is used (if one is defined). For example, StructuralMaterialProperties() would create an instance of the class with the default value of 7e9 used for the field youngs_modulus_pa, but CantileverDeflectionOutputs(deflection_m=1) would throw an error, because field slope_radians is missing and has no default value.

These defaults are also used by the app, unless specific values are provided there.

Only set default values on Generative Types which will be used as inputs, and not those used as outputs. Setting a default value on a field used as an output will prevent you from setting constraints or targets on that field from within the app.

Typing

Type hints, like float, are required in Generative Types. The app uses them to check compatibility in experiments, helping to prevent errors.

If a default is provided but no type hint, then the type of that argument is inferred from the type of the default.

Validation

It’s useful to define validation on GenerativeType inputs so that the GenerativeFunction isn’t run with inputs we already know are invalid, preventing wasted computation.

GenerativeType is a subclass of Pydantic’s BaseModel, allowing all of Pydantic’s type-validation capabilities to be used in Generative Types. In practice, only the Field validator is recommended to be used, but refer to Pydantic docs for more information on other validators.

If an instance of a Generative Type is created and the validation is failed, then an error is raised and, unless you catch the error, the Generative Function evaluation fails and no results will be visible in the app. For this reason, it’s often easier not to use validation on Generative Types that are going to be outputs, because it’s easier to let the value be assigned regardless and then spot the error in the data in the app.

Field

Pydantic’s Field function can be used to apply things like bounds and defaults to fields of a Generative Type.

Field should be imported from Pydantic, which will be installed automatically along with the generative.core package.

For example, here is one of the cantilever Generative Types extended as such:

from generative.core import GenerativeType
from pydantic import Field

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

Extra information about the field is passed into Field(...) by keyword.

All the commonly used Field arguments when creating Generative Functions are used in the example.

  • Defaults are provided. The behaviour is the same as when defaults were provided in Defining Generative Types
  • Some bounds are provided using ge=..., le=... and gt=.... Field has gt for “greater-than”, ge for “greater-than-or-equal-to”. lt and le are similar for “less-than”. Any bounds provided will be recognised by the app and used appropriately.

If preferred, Field can be used with Annotated from Python’s typing module to allow standard Python setting of default values. See Pydantic’s docs for implementation.

Handling Complex Data

Often more complex problems will involve inputting or outputting more complex data.

For inputs, integers can be used to represent different options, for example of material or component types, which can then be taken from a dictionary or list defined in the Python code:

from generative.core import generative_function
from dataclasses import dataclass
from pydantic import Field

@dataclass
class Material:
    name: str
    youngs_modulus_pa: float

MATERIALS_CATALOGUE = {
    0: Material(name="Aluminium", youngs_modulus_pa=70e9),
    1: Material(name="Steel", youngs_modulus_pa=210e9),
}

@generative_function
def cantilever_with_material(material_index: int = Field(ge=0, le=1)) -> float:
    material = MATERIALS_CATALOGUE.get(material_index)
    # Further cantilever calculations which involve the material properties
    ...

Visual outputs (e.g. images, 3D models) can also be returned as Assets, which we’ll cover in the next section on Assets.