Skip to main content

Models

The Model class is responsible for assembling individual components into a complete optimization problem. It manages component instantiation, variable linking, code generation, and compilation.

Creating a Model​

A model is created with a unique name that will be used for generated C++ files:

import amigo as am

model = am.Model("my_optimization")

The model name should be a valid Python identifier (no spaces or special characters).

Adding Components​

Components are added to the model using add_component, which takes three arguments:

add_component(name, num_instances, component_object)
  • name: Unique identifier for this component group
  • num_instances: Number of instances to create
  • component_object: Instance of your Component class

Single Instance​

For static analysis or single-point optimization:

rosenbrock = Rosenbrock()
model.add_component("opt", 1, rosenbrock)

Multiple Instances​

For time-dependent problems, repeated structures, or spatial discretization:

# Optimal control with 100 time steps
dynamics = DynamicsComponent()
model.add_component("dynamics", 100, dynamics)

# This creates 100 copies of the component

Each instance maintains its own variables, indexed from 0 to num_instances-1.

Variable Linking​

Linking establishes relationships between variables across components. The linking syntax uses scoped names: component_name.variable_name[indices]

Basic Linking​

Link two inputs together (they become the same variable):

# Link inputs from two components
model.link("comp1.x", "comp2.x")

Array Linking​

Link array elements or slices (inputs to inputs):

# Link entire array of inputs
model.link("comp1.forces[:3]", "comp2.forces[:3]")

# Link specific elements
model.link("comp1.load[0]", "comp2.load")

# Link with different indices
model.link("comp1.state[0:3]", "comp2.state[1:4]")

Linking Across Instances​

For time-marching or sequential processes:

num_steps = 50

# Link state at end of step k to state at start of step k+1
for k in range(num_steps - 1):
model.link(f"dynamics.q_end[{k}, :]", f"dynamics.q_start[{k+1}, :]")

Linking Rules​

Variables are linked between components through an explicit linking process. Indices within the model are linked with text-based linking arguments. The names provided to the linking command are scoped by component_name.variable_name.

Inputs to Inputs: Linking establishes that two inputs from different components are the same variable (shared)

model.link("comp1.x", "comp2.x")
# comp1.x and comp2.x are now the same variable

Outputs to Outputs: Linking two outputs together means the sum of the two output values

model.link("comp1.force", "comp2.force")
# The sum: comp1.force + comp2.force

Constraints to Constraints: Linking two constraints together means the sum of the constraint values

model.link("comp1.residual", "comp2.residual")
# The sum: comp1.residual + comp2.residual
warning

You cannot link between different variable types. For instance, you cannot link inputs to constraints or outputs to objectives.

Build and Initialize​

After adding all components and links, compile and initialize:

# Generate and compile C++ code with automatic differentiation
model.build_module()

# Initialize the model (allocate memory, set initial values)
model.initialize()

The Build Process​

When build_module() is called:

  1. Python code in compute() methods is analyzed
  2. Optimized C++ code is generated
  3. Automatic differentiation (A2D) is incorporated
  4. Code is compiled to a Python extension module

Generated files:

  • {model_name}.cpp - C++ source
  • {model_name}.h - Header file
  • {model_name}.pyd/.so - Compiled binary
warning

Whenever you modify compute() methods, you must call build_module() again to recompile.

Complete Example​

import amigo as am

# Define components
class Dynamics(am.Component):
def __init__(self):
super().__init__()
self.add_input("x", shape=2) # Current state
self.add_input("u") # Control input
self.add_input("x_next", shape=2) # Next state
self.add_constraint("residual", shape=2)

def compute(self):
x = self.inputs["x"]
u = self.inputs["u"]
x_next = self.inputs["x_next"]

# Simple dynamics: x_next = f(x, u)
dt = 0.1
self.constraints["residual"] = [
x_next[0] - (x[0] + dt * x[1]),
x_next[1] - (x[1] + dt * u)
]

class Objective(am.Component):
def __init__(self):
super().__init__()
self.add_input("x_final", shape=2)
self.add_objective("cost")

def compute(self):
x = self.inputs["x_final"]
# Minimize distance from origin
self.objective["cost"] = x[0]**2 + x[1]**2

# Build model
model = am.Model("trajectory")

# Add components (10 time steps)
num_steps = 10
model.add_component("dynamics", num_steps, Dynamics())
model.add_component("obj", 1, Objective())

# Link states across time steps (input to input)
for k in range(num_steps - 1):
model.link(f"dynamics.x_next[{k}, :]", f"dynamics.x[{k+1}, :]")

# Link final state to objective (input to input)
model.link(f"dynamics.x[{num_steps-1}, :]", "obj.x_final")

# Build and initialize
model.build_module()
model.initialize()

# Now ready for optimization
opt = am.Optimizer(model)
opt.optimize()

Sub-Models​

You can add sub-models to a model by calling model.add_sub_model("sub_model", sub_model). In this case, the scope becomes sub_model.component_name.variable_name. Any links specified in the sub-model are automatically added to the main model.

# Create sub-model for wing analysis
wing = am.Model("wing")
wing.add_component("aero", 1, AeroComponent())
wing.add_component("structure", 1, StructureComponent())
# Link shared input between components (e.g., both need velocity)
wing.link("aero.velocity", "structure.velocity")

# Create main aircraft model
aircraft = am.Model("aircraft")
aircraft.add_sub_model("left_wing", wing)
aircraft.add_sub_model("right_wing", wing)

# Add a component that takes total weight as input
aircraft.add_component("performance", 1, PerformanceComponent())

# Link wing weight inputs to performance component (input to input)
aircraft.link("left_wing.structure.weight_input", "performance.weight_left")
aircraft.link("right_wing.structure.weight_input", "performance.weight_right")

Variable paths in sub-models use the format: sub_model_name.component_name.variable_name