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)
2025-11-18 05:46:57 [info ] Num states 3
2025-11-18 05:46:57 [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
nameis the name of the variable represented as a string. For example of the name of the parameterrhois the string"rho"symbolis similar tonamethis this is asympyobject and this is used within the expressions in the assignments.componentsis 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.descriptionis just a string with some information. In our example, the statexhas the description"x variable"anddx_dthas the description"The derivative of x".unit_stris a string representation of a unit, for examplexhas the unit string"m"whiledy_dthas the unit stringm/s. If no using is provided this is set toNoneunitis 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.19/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.19/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.19/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.