Getting Started
This section presents a first look at an example of a model defined using CSDL. The example shown in this section only shows some basic features of CSDL, and we will only be discussing them at a high level in this section. Later, we'll go over the basic features in more detail, including defining and solving optimization problems, as well as more advanced features.
#
The Compilation ProcessCSDL simulations are written, compiled, and run within a Python script. The code snippet below shows the definition, compilation, and execution of a basic simulation built using CSDL. We will discuss and build upon this example throughout this section. The complete example appears at the end of this section.
from csdl import Modelimport csdl
class Example(Model): def define(self): # model specification (not blank) # ...
from csdl_om import Simulator
sim = Simulator(Example())
sim.run()print(sim[...])print(sim[...])print(sim[...])
First, this script defines a class, Example
, which inherits from
Model
, which is part of the csdl
package.
Second, an object sim
of class Simulator
is constructed from an
instance of Example
.
Third, sim.run()
is executed, and the script prints the result.
Before looking at the definition of Example
or the usage of either
Example
or Simulator
, it is important to understand the role
that these two classes play in this example, and the role that Model
and Simulator
play in all programs written in CSDL.
The Example
class is a user defined class, where the user supplies a
mathematical specification of the model we are interested in simulating.
The Example
class is not capable of running a full simulation or
computing derivatives for use in solving an optimiztion problem -- it is
simply a description of the physical system.
To run a simulation, we need to construct an executable object that
performs the simulation.
As stated in the Installation Instructions, we
need a separate package to provide the Simulator
class, which contains
the compiler back end.
The reason for this is that the CSDL compiler is a three stage compiler.
Three stage compilers are split between a front end, which generates an
intermediate representation of the code, a middle end, which performs
implementation-independent optimizations on the generated intermediate
representation, and a back end, which generates executable code.
The csdl
package implements the front end and middle end in the
Model
class, while a separate package implements the back end in the
Simulator
class.
This creates a clean separation between model specification and
simulation implementation.
This separation is central to creating a language where users can
operate at the highest level of abstraction possible.
- compiler framework figure
important
Although Python is an interpreted language, and CSDL code is written in
Python, CSDL is a compiled language.
The two separate commands to initiate the compilation process and run
the resulting executable object merely takes place within Python, but
the Simulator
class
is not required to be implemented in any language in particular.
Now, let's look at how to specify models in CSDL.
#
Model SpecificationTo specify a model, create a new class that inherits from Model
.
The Model
class is the base class for all classes where a
mathematical model of a system is specified.
The Model.define
method is where the mathematical model is
specified.
This is where most CSDL code is written, except for some advanced use
cases that we'll cover later.
class Example(Model): def define(self): p = self.declare_variable('p', shape=(2,)) x = p[0] y = p[1]
distance = (x**2 + y**2)**(1/2)
self.register_output('distance', distance)
Here we declare a variable named 'p'
with shape (2,)
.
The name 'p'
is the CSDL name of the variable.
This name is the name used to access data within the generated
executable object contained in sim
.
CSDL has no knowledge of Python names, so variables must be given a
name as a string so that the resulting run time values may be accessed
later.
Then we create two new variables, x
and y
from p
.
These variables do not have a name in CSDL.
They will not be accessible via the Simulator
API.
Next, we compute a distance, distance
, using the normal mathematical
operations available in Python (aka special methods).
This variable does not have a name until we register it as an output
using Model.register_output
.
note
We could also compute the distance using
distance = csdl.norm(p)
.
important
When using Python special methods, CSDL variables behave mostly like NumPy arrays, except that CSDL variables are immutable and do not support broadcasting.
Sometimes it is desireable to store values from multiple variables in
one variable.
For this we use the Model.create_output
method:
class Example(Model): def define(self): p = self.declare_variable('p', shape=(2,)) x = p[0] y = p[1]
distance = (x**2 + y**2)**(1/2)
self.register_output('distance', distance)
q = self.create_output('q', shape=(3,)) q[:2] = p q[-1] = distance
Here, q
is named 'q'
, and its value is defined after q
is
constructed.
In this case, q
stores p
(the position vector in coordinates)
and distance
.
important
Variable objects created using Model
methods do not store run time
values, as the Model
class cannot run a full simulation.
Instead, variable objects store a history of operations.
This history is the intermediate representation that Model
constructs
at compile time.
important
Only variables created using Model.create_output
can use indexed
assignment.
The indices from multiple assignments must not overlap.
#
Simulation ImplementationTo construct a simulation implementation from the mathematical
specification defined in Example
, we need to construct an object of
the Simulator
class, which comes from a package separate from csdl
;
in this case, csdl_om
.
The Simulator
class constructor always requires an instance of the
Model
class or any of its subclasses, so we provide it with an
instance of Example
.
Once an object of the Simulator
class is constructed (in this example,
sim
), the compilation process is complete, and we can run a
simulation.
The sim
object can also compute derivatives automatically, so if an
optimization problem is defined (shown later in this tutorial), an
external optimizer can be connected to sim
to solve the optimization
problem.
note
The compilation process can be as simple as a single line of code within a Python script,
sim = Simulator(Example(...))
and running the compiled code is as easy as,
sim.run()
Because of the way the roles are split between Model
and
Simulator
, creating an instance of a Model
(or in this example,
Example
) class does not construct an object that can simulate the
behavior of a physical system.
#
Making Models GenericSo far, the Example
class only defines a single model.
Multiple instances of Example
would result in a simulation that
behaves the exact same way.
We can make Example
more generic by defining model parameters.
To define model parameters, define a Model.initialize
method.
class Example(Model): def initialize(self): self.parameters.declare('scale', types=float, default=1) self.parameters.declare('in_name', types=str) self.parameters.declare('out_name', types=str)
Model parameters are neither inputs to the model, nor are they computed
by the model.
Instead, model parameters make model definitions more generic.
Note that defining an initialize
method is entirely optional.
In this case, users of Example
are free to choose the name of an
input variable and an output variable, as well as the value of some
number called 'scale'
.
If the default
option is not defined, then the parameter is required.
To use the parameters within the model definition,
class Example(Model): def initialize(self): self.parameters.declare('scale', types=float, default=1) self.parameters.declare('in_name', types=str) self.parameters.declare('out_name', types=str)
def define(self): scale = self.parameters['scale'] in_name = self.parameters['in_name'] out_name = self.parameters['out_name']
# now we can use these parameters...
You'll notice that the parameters defined in the initialize
method
are accessed in the define
method.
The initialize
method is always called before the define
method,
and all parameters declared in initialize
are available by the time
define
is called.
That is, you will always have access to parameters within the define
method.
To change the definition of Example
for only one instance of
Example
, pass the values of the parameters as named arguments to the
constructor:
e1 = Example(in_name='a', out_name='b')e2 = Example(in_name='a', out_name='b', scale=10)e3 = Example(in_name='r', out_name='s')e4 = Example(in_name='s', out_name='r', scale=10)
#
The Complete ExampleLooking at the complete example, we see that the variables p
and q
now have CSDL names that are specified only when Example
is
instantiated.
We also see that q[:2]
is equal to the position, scaled by a factor
scale
.
from csdl import Modelimport csdl
class Example(Model): def initialize(self): self.parameters.declare('scale', types=float, default=1) self.parameters.declare('in_name', types=str) self.parameters.declare('out_name', types=str)
def define(self): scale = self.parameters['scale'] in_name = self.parameters['in_name'] out_name = self.parameters['out_name']
p = self.declare_variable(in_name, shape=(2,)) x = p[0] y = p[1]
distance = (x**2 + y**2)**(1/2)
self.register_output('distance', distance)
q = self.create_output(out_name, shape=(3,)) q[:2] = scale*p q[-1] = distance
from csdl_om import Simulator
in_name = 't'out_name = 'u'sim = Simulator(Example(in_name='t', out_name='u'))
sim.run()print(sim[in_name])print(sim['distance'])print(sim[out_name])
#
ConclusionThis is only a taste of what CSDL has to offer, but it should be enough to get you started building basic models that you could otherwise build in Python. In the next section, we'll cover the basic language concepts without relying so much on examples. Later, we'll dive deeper into the basics of CSDL, and move on to more advanced features.