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 groupnum_instances: Number of instances to createcomponent_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
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:
- Python code in
compute()methods is analyzed - Optimized C++ code is generated
- Automatic differentiation (A2D) is incorporated
- 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
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