Language Concepts
This section covers the basic concepts of CSDL that you will need to
understand in order to write effective CSDL code.
CSDL code is entirely written inside of user defined classes that
inherit from Model
, CustomExplicitOperation
, and
CustomImplicitOperation
.
The Model
class is the main container for all CSDL code, while
CustomExplicitOperation
, and CustomImplicitOperation
offer more
advanced control over program execution and the ability to integrate
external analysis tools into your design optimization workflow.
Later, this tutorial will refer back to and build upon the concepts
presented in this section.
#
ModelA CSDL model is a mathematical specification of a physical system.
The majority of user code is located in the definition of a user
defined class that inherits from the Model
class.
Users can supply a mathematical specification using two styles of model
composition corresponding to two different programming language
paradigms.
Users can use a functional programming style, which resembles
mathematical notation to define a model as a composition of functions,
or an object oriented programming style to define a model as a
hierarchical composition of models.
Users are also free to define a model specification using a mix of the
functional and object oriented paradigms.
Model specifications are defined within the user defined
Model.define
method:
from csdl import Model
class Example(Model): def define(self): # mathematical specification goes here
The functional programming style can be seen in Getting Started, and use of all functions in the Standard Library is considered part of the functional programming style of model composition.
The object oriented programming style is covered in Object Oriented Programming, and use of all functions in the Standard Library is considered part of the functional programming style of model composition.
important
The Model
class and user defined subclasses
do not provide an implementation of a simulation of the behavior of a
physical system, only a representation of the physical system.
To generate an implementation, an object of the Simulator
class must
be constructed from a Model
object.
The csdl
package supplies the Model
class, while a separate package
chosen by the user supplies the Simulator
class.
#
VariableThe concept of a variable in CSDL differs from the concept of a variable
in Python.
In Python, variables store run time values, e.g. x=3
, or
a=np.eye(3)
.
In CSDL, a variable stores the history of previous operations and
variables used to define that variable.
The histories form a structure called a Directed Acyclic Graph (DAG).
The nodes of the DAG are variables and operations.
This DAG structure forms and intermediate representation (IR) of the
program.
Storing an IR of the program comes with many benfits.
The CSDL IR can store properties of variables and
operations that the compiler can exploit, either before generating the
final executable, or during code generation.
For example, the compiler back end is free to implement derivative
computation for each operation, requiring no additional input from the
user.
The DAG structure also encodes a partial ordering of the operations,
which enables computing the execution order of the operations within the
final simulation via a topological sort.
important
Because CSDL variables do not store run time values, printing a CSDL
variable will not show its run time value.
In order to print a run time value for a variable, use the
Model.print_var
method.
CSDL variables can be used the same way Python variables are used. For example,
x = self.declare_variable('x')m = self.declare_variable('m')b = self.declare_variable('b')y = m*x + bz = csdl.sin(x)
important
CSDL expressions can also mix Python int
, float
, and numpy.ndarray
types with CSDL variables.
All Python types that are not CSDL variables are
computed at compile time and may not be part of the final executable
object, possibly resulting in more efficient code.
The next section, Variable Types, will cover
Model.declare_variable
and
Model.register_output
into
more detail.
important
Unlike NumPy arrays, CSDL variables do not support
broadcasting. In order to perform
elementwise multiplication, for example, a scalar variable (array with
shape (1,)
) by a variable that represents a vector, tensor, or matrix,
users must use the csdl.reshape
or csdl.expand
functions to make the
shapes of the operands compatible.
As mentioned in Model, CSDL users can use a functional or
object oriented style to define a model specification.
Defining variables in terms of other variables, or as functions of
variables, is how users define a model specification using the
functional programming style.
The simplest user defined models will use only the functional
programming style, as there is only one Model
in the model hierarchy.
The functional programming style shows how data flows throughout the
simulation, which is consistent with the DAG structure of the IR
Part of the functional style to keep in mind is that CSDL variables are
immutable; once a variable is defined, there is no way to express a
change (or mutation) in that variable's value at run time.
The only case where the definition of a variable's computation can be
modified after the variable has been created is with variables created
using Model.create_output
, which is exclusively used for defining a
concatenation of variable values.
important
Python does not enforce immutability, so if a variable is redefined (or in Python parlance, if a variable name is bound to a new reference), then the object in memory storing the variable data is not in use in later parts of the code. See Best Practices.
#
OperationThe concept of an operation in CSDL also serves to construct the IR.
Users do not interact with operations except when defining
Custom Operations, which is
covered in the Advanced section of this
tutorial, but it is still useful at this stage to understand how the IR
is constructed when Python executes Model.define
.
As mentioned in Variable, the IR has the structure of a
DAG, where nodes represent either variables or operations.
Each operation node has at least one predecessor node of variable type,
and at least one successor node of variable type.
Each variable node has at most one predecessor node of operation type,
and zero or more successor nodes of operation type.
This leads to the DAG having a bipartite structure.
Operations can store properties, such as whether or not they are linear operations, elementwise operations, posynomials, etc. These properties are used for performing implementation-independent optimizations/graph transformations on the IR.
The standard library contains functions that add StandardOperation
objects to the IR.
The CSDL compiler back end uses the StandardOperation
classes to
generate the executable code for each StandardOperation
object.
Operation properties and derivatives are hidden from the user so that
code optimization and derivative implementation are automated, ensuring
that user code is clear and easily maintainable without sacrificing
performance of generated code.
#
SimulatorThe previous subsections describe (at a high level) how users define a
model specification using the CSDL language.
The csdl
package provides classes that represent all of the concepts
presented up to this point.
The model specification is contained within a single top level, or
"main" model, represented by a user defined subclass of Model
.
The Model
class, however, contains only a specification, not an
implementation.
That is, there is no way of simulating the model, given only a Model
definition.
In order to simulate the model, CSDL requires the Simulator
class,
which the csdl
package does not provide.
When a user defines an objective (and usually constraints) within a
Model
, the Simulator
class makes the objective, constriants, and
their derivatives available.
These values can be used to solve a nonlinear program (NLP).
note
The user does not need to provide derivatives for any operation except
for the subclasses of the
CustomOperation
class.
The Simulator
class provides a Python interface to the generated
executable object.
#
ParametersThe CSDL Model
class has an attribute called parameters
, which
gives users some power over a Model
subclass definition without
defining a new Model
subclass.
Parameters are compile time constants that can be set by the user to
change a model definition per instance of a model.
For example, if a model requires time integration, the number of time
steps and the step size can be set without creating a new model
definition.
Parameters are also useful for setting the resolution of a mesh grid, or
the number of instances of another model to add as children in the model
hierarchy (e.g. the number of satellites in a swarm).
Parameters make model definitions more generic, and thus reusable,
reducing overhead for users of your library.
Parameters can be of any Python type, and a Model
definition can
restrict parameters to a set of types.
Parameters were introduced briefly in
Getting Started, and are covered in
more detail in
Advanced.
note
Parameters in CSDL can best be compared to value/non-type template parameters in C++ and const generics in Rust.