About#
In this document we will try to explain the process that is happening behind the scenes when an .ode
file is parsed and code is generated.
To do this exercise we will consider the following ODE file called lorentz.ode
ode_string = """
# This is the Lorentz system
# And it is part of this tutorial
parameters(
sigma=12.0,
rho=21.0,
beta=2.4
)
states(x=ScalarParam(1.0, unit="m", description="x variable"), y=2.0,z=3.05)
dx_dt = sigma * (y - x) # The derivative of x
dy_dt = x * (rho - z) - y # m/s
dz_dt = x * y - beta * z
"""
from gotranx.load import ode_from_string
ode = ode_from_string(ode_string, name="Lorentz")
print(ode)
2024-11-05 20:05:02 [info ] Num states 3
2024-11-05 20:05:02 [info ] Num parameters 3
ODE(Lorentz, num_states=3, num_parameters=3)
To see what output is generated from this ODE you can check out Your first ODE file.
Atoms#
This ODE has 3 parameters (sigma
, rho
and beta
), 3 states (x
, y
and z
) and 3 assignments (dx_dt
, dy_dt
and dz_dt
). Parameters, states and assignments are subclasses of Atom
which is the most primitive building block in gotranx
classDiagram Atom <|-- Parameter Atom <|-- State Atom <|-- Assignment class Atom{ name: str symbol: sympy.Symbol components: tuple[str, ...] description: str unit_str: str unit: pint.Unit } class Parameter{ value: sympy.Number } class State{ value: sympy.Number } class Assignment{ value: Expression expr: sympy.Expression comment: Comment } Assignment <|-- StateDerivative Assignment <|-- Intermediate class Expression{ tree: lark.Tree dependencies: set[str] } class StateDerivative{ state: State }
An Atom
contains a number of different fields
name
is the name of the variable represented as a string. For example of the name of the parameterrho
is the string"rho"
symbol
is similar toname
this this is asympy
object and this is used within the expressions in the assignments.components
is just a list of components that a given atom belongs to. In this simple ODE we don’t have any components (or practically speaking we have one component). However in lager ODE systems it might be useful to group parameters, states and assignments into different components. In these cases it is possible for an atom to be part of several components.description
is just a string with some information. In our example, the statex
has the description"x variable"
anddx_dt
has the description"The derivative of x"
.unit_str
is a string representation of a unit, for examplex
has the unit string"m"
whiledy_dt
has the unit stringm/s
. If no using is provided this is set toNone
unit
is a pint unit and this can be used e.g to convert to and from different units.
We note that the 3 assignments are a special type of assignment called a StateDerivative
. There is also another type of assignment called and Intermediate
. The StateDerivative
is special because it is associated with a given State
and represents the temporal derivative of the state variable. For example dx_dt
is associated with the state x
.
Whenever you have a state variable, there has to be a corresponding state derivative. We can try to create an ODE with a missing derivative
from gotranx.load import ode_from_string
ode_from_string("states(x=1, y=2)\ndx_dt = x + y")
---------------------------------------------------------------------------
ComponentNotCompleteError Traceback (most recent call last)
Cell In[2], line 3
1 from gotranx.load import ode_from_string
----> 3 ode_from_string("states(x=1, y=2)\ndx_dt = x + y")
File /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/gotranx/load.py:35, in ode_from_string(text, name)
32 if not isinstance(result, LarkODE):
33 raise exceptions.InvalidODEException(text=text, atoms=result)
---> 35 ode = make_ode(
36 components=result.components,
37 name=name,
38 comments=result.comments,
39 )
40 logger.info(f"Num states {ode.num_states}")
41 logger.info(f"Num parameters {ode.num_parameters}")
File /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/gotranx/ode.py:229, in make_ode(components, comments, name)
203 def make_ode(
204 components: Sequence[Component],
205 comments: Sequence[atoms.Comment] | None = None,
206 name: str = "ODE",
207 ) -> ODE:
208 """Create an ODE from a list of components
209
210 Parameters
(...)
227 If a symbol is duplicated
228 """
--> 229 check_components(components=components)
230 t = sp.Symbol("t")
231 # components = add_temporal_state(components, t)
File /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/gotranx/ode.py:39, in check_components(components)
37 for comp in components:
38 if not comp.is_complete():
---> 39 raise exceptions.ComponentNotCompleteError(
40 component_name=comp.name,
41 missing_state_derivatives=[state.name for state in comp.states_without_derivatives],
42 )
ComponentNotCompleteError: Component '' is not complete. Missing state derivatives for ['y']
Parsing an ODE file#
When a file is parsed it is first opened, turned into a string and then sent to the lark parser
Lark then tries to parse the text using the grammar and turns each object into a tree which is in turn assembled into components using the transformer class
.
flowchart LR FILE[.ode file] --> PARSER[Lark Parser] PARSER --> TRANS[Transformer] TRANS --> DESC[Description] TRANS --> TREE[Syntax Tree] ATOMS --> COMP[Components] --> ODE TRANS --> COMP TRANS --> ODE TREE --> ATOMS[Atom] DESC --> ODE
Code generation#
To generate code we pass the ODE to a code generator object. A code generator object inherits from the gotranx.codegen.CodeGenerator
class. For example we could create a PythonCodeGenerator by passing in the ode, i.e
from gotranx.codegen.python import PythonCodeGenerator
codegen = PythonCodeGenerator(ode)
We can now generate code for the different methods. For example we can generate code for the initial_state_values
print(codegen.initial_state_values())
def init_state_values(**values):
"""Initialize state values"""
# x=1.0, y=2.0, z=3.05
states = numpy.array([1.0, 2.0, 3.05], dtype=numpy.float64)
for key, value in values.items():
states[state_index(key)] = value
return states
Each code generator is associated with a template
that follows the gotranx.templates.Template
protocol.
flowchart TD subgraph gen [Code generators] PYTHON_CODEGEN[python] C_CODEGEN[C] end ODE[ODE file] --> gen subgraph tem [Templates] PY_TEMP[Python template] --> PYTHON_CODEGEN C_TEMP[C template] --> C_CODEGEN end subgraph code [Code] PYTHON_CODEGEN -. Specific methods .-> PY_CODE[Python Code] C_CODEGEN -. Specific methods .-> C_CODE[C Code] end
You can checkout Adding a new language to see how to add support for a new language.