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 scalar variables:
# Output of comp1 becomes input to comp2
model.link("comp1.output", "comp2.input")
Array Linking
Link array elements or slices:
# Link entire array
model.link("comp1.forces[:3]", "comp2.loads[:3]")
# Link specific elements
model.link("comp1.stress[0]", "comp2.load")
# Link with different indices
model.link("comp1.state[0:3]", "comp2.input[1:4]")
Linking Across Instances
For time-marching or sequential processes:
num_steps = 50
# Link state at time k+1 to previous state at time k
for k in range(num_steps):
model.link(f"dynamics.q[{k+1}, :]", f"dynamics.q_prev[{k}, :]")
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 Source(am.Component):
def __init__(self):
super().__init__()
self.add_input("x", value=1.0, lower=0.0, upper=10.0)
self.add_output("y")
def compute(self):
self.outputs["y"] = self.inputs["x"]**2
class Processor(am.Component):
def __init__(self):
super().__init__()
self.add_input("z")
self.add_output("w")
self.add_objective("cost")
def compute(self):
z = self.inputs["z"]
self.outputs["w"] = am.sqrt(z)
self.objective["cost"] = z
# Build model
model = am.Model("pipeline")
# Add components
model.add_component("source", 1, Source())
model.add_component("proc", 3, Processor())
# Link components
model.link("source.y", "proc.z[0]")
for i in range(2):
model.link(f"proc.w[{i}]", f"proc.z[{i+1}]")
# 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())
wing.link("aero.pressure", "structure.loads")
# Create main aircraft model
aircraft = am.Model("aircraft")
aircraft.add_sub_model("left_wing", wing)
aircraft.add_sub_model("right_wing", wing)
# Link between sub-models
aircraft.link("left_wing.structure.weight", "total_weight")
aircraft.link("right_wing.structure.weight", "total_weight")
Variable paths in sub-models use the format: sub_model_name.component_name.variable_name