Skip to content
MANUAL

Syntax ​

This section provides an overview of Dyad's syntax and language constructs. It only briefly touches on how to use them, and should be read as reference material for what you can do.

Dyad files (.dyad) typically include component definitions and analysis definitions. They may also includes using statements when referencing external Julia packages.

In this syntax guide, we have placed all optional keywords in square brackets, with | separating the options. For example, [structural | final] parameter means that the following syntax is supported:

  • parameter

  • structural parameter

  • final parameter

Components ​

Components are the fundamental building blocks in Dyad. The are meant to capture mathematical behavior (causal, acausal or discrete-time) as a reusable component. They may contain variables, parameters, connectors, subcomponents, and relations. Furthermore, they may extend from other components.

The component declaration may be prefixed with:

  • partial to indicate that it is a partial component, which can be used as a base for other components,

  • example to indicate that it is an example component to show what you can do with other components,

  • test to indicate that it is a component used only for testing, and should not be exported.

Here is an example component definition:

dyad
# Sample component
component MyComponent
  # A component can have one or more extends clauses, like this one...
  extends BaseComponent
  # A normal parameter only results in a parametric change, not a structural change,
  parameter y::Real
  # A structural parameter is a parameter changes the number of equations or variables.
  structural parameter N::Integer = 10
  # Variables and parameters can have a number of different attributes
  variable x::Real(min = 0, max = 10, guess = 5, units = "m")
  # Subcomponents are components nested inside other components forming hierarchical models
  subcomponent = SomeComponent()
  # Connectors represent points of interaction between the components
  p = Pin()
relations
  # One type of relation is an equation, like this one
  der(x) = y
  # Another type of relation is a connection
  connect(subcomponent.p, p)
end

Component metadata ​

Components can also have metadata, which is used by the UI, documentation tools, and codegen for some specific keys. For example, you can specify the icon for a component in the metadata.

dyad
component MyComponent
  # ...
metadata {
  "Dyad": {"icon": "dyad://YourComponentLibrary/assets/icon.svg"}
}
end

The Dyad namespace in metadata is reserved. But all other namespaces can be used. This allows application/user/customer specific metadata to be included in models. An example use of metadata might be to include the corresponding physical part number in the component metadata.

Analyses ​

Analyses describe workflows that can be performed. One way to think about this is the a component describes a problem while an analysis is something that ultimately leads to a "result" (some kind of computation typically performed on a component or perhaps even another analysis).

Examples of analyses might include:

  • simulate a model over time with a TransientAnalysis,

  • run a sweep over some parameters with a ParameterSweepAnalysis,

  • run a Monte Carlo analysis with a MonteCarloAnalysis,

  • or even define and run a custom analysis using your own Julia code (see the advanced users guide).

To write an analysis in Dyad you must extend from some existing analysis; ultimately all analyses are implemented in Julia. See the custom analysis tutorial for more details.

dyad
analysis MyAnalysis
  extends TransientAnalysis(alg="Rodas5P", abstol=0.001)
  model = CircuitModel()
relations
  # Relations between model and data
end

Importing libraries ​

The using statement imports components, types, or other definitions from other packages. Crucially, these can be functions or variables from Julia as well as Dyad component libraries.

A Julia function defined in a component library's own dyad/definitions.jl is available to that library's .dyad sources directly, with no using statement — see Julia-Based Component Libraries.

To import a Julia function from another package you must specify its signature. This is done in two steps: declare a named function type with func, then import the symbol at that type. Suppose we have a simple Julia function myfunc(x, y) = x + y:

dyad
type BinaryOp = func(::Real, ::Real)::Real
using MyLibrary: myfunc::BinaryOp

The func signature lists argument types only (::Real, ::Real), with the return type after the closing parenthesis. A using statement takes a plain name or a name::Type pair — the signature cannot be written inline in the using itself.

The types can be any Dyad type; the most permissive is Native, which translates to Julia's Any. Several symbols may be imported at once, with or without types:

dyad
using Mechanical: Flange, Spring
using .: density::DensityFunction

To import a component from another component library, it's sufficient to fully specify the component library and the name, like so:

dyad
subcomponent = BlockComponents.Sources.Constant(k=1.0)

All the external libraries you pull in must be in your Project.toml file. You can add to this file in the Julia REPL, by calling using Pkg; Pkg.add("MyLibrary"), or going into "Pkg-mode" by typing ], and then running add MyLibrary. See Julia's package manager documentation for more details.

Variables ​

Variables represent quantities that can change during simulation. For variables of type Real you can define what units are associated with that variable, in which case Dyad will automatically check that the units are correct in every relation it is involved in.

dyad
component MyComponent
  variable position::Real
  variable velocity::Real(units="m/s")
relations
  # ...
end

Parameters ​

Parameters are values that remain fixed during a simulation, but can be changed between simulations.

dyad
component MyComponent
  parameter mass::Mass = 1.0
  parameter length::Length = 0.5
  # Cannot be modified at all, even when constructing a new component
  final parameter id::Integer = 12345  
  # See below for more on structural parameters.
  structural parameter N::Integer = 3  
  # ...
end

Structural parameters ​

Structural parameters are parameters whose changes might imply structural changes in the model. They are fixed at component creation time, and cannot be changed after the component is created.

The size of an array in a component can be defined by a literal integer or a structural parameter.

dyad
component MyComponent
  structural parameter N::Integer = 3  # Number of elements
  variable x::Real[N]  # Array size depends on N
relations
  # ...
end

Relations ​

The relations block contains equations and other statements that define a component's behavior.

You can use initial in front of an equation to set the initial value of a variable (at the start time of the simulation).

Loops via for are supported, and can contain other relations within them. Note that they will be unrolled in Julia, so you shouldn't have extremely long loops here (O(10_000) and above) for performance reasons (But they will still work!).

You can also use connect to connect connectors. A connect call can have any number of arguments, and will connect all the listed connectors to each other.

dyad
relations
  # Equations
  initial x = 0.0  # Initial condition
  der(x) = v      # Differential equation (dx/dt = v)
  F = m * der(v)  # Newton's law

  # Loops
  for i in 1:5
    initial array_of_components[i].x = 0.0
  end

  for i in 1:4
    connect(array_of_components[i].y, array_of_components[i+1].x)
  end
  
  # Connections
  connect(source.p, resistor.p)
  connect(resistor.n, ground.g)
end

Expressions ​

Expressions are used to compute values in equations and other contexts. They can be used in most any context where a value is expected (except for array sizes, which require literals, or parameter guesses).

Most expressions are pretty similar to Julia syntax - you can use + for addition, * for multiplication, ^ for exponentiation, etc. as you would in most programming or modeling languages.

Operators ​

Dyad supports a variety of operators for arithmetic and logical operations. The operator precedence here is the same as in Julia.

dyad
a + b            # Addition
a - b            # Subtraction
a * b            # Multiplication
a / b            # Division
a ^ b            # Power
a % b            # Modulo

a > b            # Greater than
a < b            # Less than
a >= b           # Greater than or equal
a <= b           # Less than or equal
a == b           # Equal
a != b           # Not equal

a and b          # Logical AND
a or b           # Logical OR
!a               # Logical NOT

a = b            # Assignment

Function calls ​

Dyad supports calling functions with arguments. You can also provide default values for arguments.

You can call any Julia function that is available in your component library. That means anything from Julia Base, or any packages you load within that module (using using), or any functions you define within that module.

You must import a function as shown in the importing libraries section.

dyad
sin(angle)
atan2(y, x)
custom_func(data, tolerance=0.01)

Conditional expressions ​

If statements are available in Dyad, but you may also want to see enumerations for more complex cases.

dyad
y = if x > 0 then x else 0  # Inline if expression

Literals ​

dyad
x = 10           # Integer
y = 3.14         # Real number
z = 1.5e-3       # Scientific notation
R = 10k          # With metric prefix (10 kilo => 10 * 10e3 => 10,000)
text = "Hello"   # String
flag = true      # Boolean

Time ​

Dyad supports a time variable, which is a special variable that represents the current time of the simulation.

Here is a step function that changes at t=10:

dyad
v = if time < 10 then 0 else 1

You can use time anywhere you could use a variable. For example, here's an expression that switches from sine to cosine at t=10:

dyad
v = if time < 10 then sin(time) else cos(time)

Arrays ​

Arrays can be used for variables, parameters, and other data structures.

dyad
parameter vector::Real[3] = [1, 2, 3]
variable matrix::Real[2, 2]
variable pos::Position[3]  # 3D position vector

Array literals are comma-separated, and a matrix literal is a list of rows. There is no ; row separator and no space-separated column syntax, so [1.0 2.0; 3.0 4.0] is a parse error:

dyad
parameter A::Real[2, 2] = [[1.0, 2.0], [3.0, 4.0]]
parameter col::Real[2, 1] = [[1.0], [2.0]]
# `fill` is available for uniform arrays
parameter zeros_2x2::Real[2, 2] = fill(0.0, 2, 2)

Array comprehensions can initialize arrays of components:

dyad
resistors = [Resistor(R=i*10) for i in 1:5]  # Array of 5 resistors

The size of an array can be defined by a literal integer or a structural parameter, but cannot vary at runtime and any change to a structural parameter will require that the model undergo symbolic processing again.

Clocks ​

A clock is an event source: when it ticks, the discrete-time logic associated with it runs. Variables that only have a value at the tick times of a clock are called clocked. This section is a syntax reference only — for the semantics of clocks, sampling and discrete-time modeling, see Discrete-Time Modeling and Sampled-Data Systems.

Clock parameters on a component ​

A component may be parameterized by one or more clocks, listed in @[...] after the component name. Each entry is [input | output] <name> [extends <ClockType>]. An input clock is supplied by the surrounding context; an output clock is defined by the component itself, i.e. the component is a clock source.

dyad
# Clock-agnostic: works on whatever discrete-time clock it is connected to.
component MyFilter@[input clk extends Discrete]
  # ...
end

# A clock source that defines a periodic clock.
component MyClockSource@[output clk extends Periodic]
  # ...
end

# Two clock parameters, both supplied by the context: this component straddles a
# continuous-time clock and a discrete-time one, which is how a sampler is declared.
component MySampler@[input inclk extends Continuous, input outclk extends Discrete]
  # ...
end

The clock types available in extends are Continuous, Discrete and Periodic, ordered from least to most specific: Periodic is a Discrete clock that additionally ticks at a fixed rate. Constraining a parameter to Discrete therefore accepts a periodic clock too.

Leaving out extends places no constraint on the kind of clock:

dyad
component MyBlock@[input clk]
  # ...
end

Clock annotations on connectors and variables ​

Connectors and variables are placed on a clock with the same @[...] syntax. Writing @[clk] is shorthand for binding the element's clock to the component's clock parameter of that name; the long form @[<param> = <clock>] names the binding explicitly, which is what you need when the two names differ.

dyad
component MyFilter@[input clk extends Discrete, input aux extends Discrete]
  u = RealInput@[clk]()
  y = RealOutput@[clk]()
  # Long form: this connector goes on `aux`, not on `clk`
  trim = RealInput@[clk = aux]()
  # A clocked variable
  variable x::Real@[clk = clk]
  # A conditional clocked connector
  structural parameter with_ff::Boolean = false
  u_ff = RealInput@[clk]() if with_ff
  # An array of clocked connectors
  structural parameter nu::Integer = 2
  v = [RealInput@[clk]() for i in 1:nu]
relations
  # ...
end

Connectors in library sources usually also carry a {^name} marker, as in y = Dyad.RealOutput@[clk]() {^y}. That is unrelated to clocks — it attaches diagram metadata by reference, see Linked metadata. Connector types are written Dyad.RealInput / Dyad.RealOutput when the Dyad library is not already in scope.

A component may mix clocked and unclocked connectors — this is how a component that converts between the continuous-time and discrete-time domains is declared.

Binding a clock when instantiating a subcomponent ​

A clock parameter is supplied at instantiation with @[<param> = <clock>], including inside an array comprehension:

dyad
component MyOuter@[input clk extends Discrete]
  structural parameter N::Integer = 2
  inner = MyFilter@[clk = clk]()
  cells = [MyCell@[clk = clk]() for i in 1:N]
relations
  # ...
end

Shift expressions ​

x@clk refers to the value of x at the current tick of clk, and x@(clk-n) to its value n ticks ago. A clocked equation with no annotation is implicitly at the current tick, so y = x means y@clk = x@clk.

dyad
relations
  # x(k) = 0.5 x(k-1) + u(k-1)
  x@clk = 0.5 * x@(clk-1) + u@(clk-1)
  y = x

Initial conditions for clocked variables refer to the past, because the update at the first tick needs the previous value. One initial equation is required for every shift depth the component uses:

dyad
relations
  initial x@(clk-1) = 0.0
  initial x@(clk-2) = 0.0
  x@clk = a1 * x@(clk-1) + a2 * x@(clk-2) + u@clk

SampleTime() ​

SampleTime() returns the tick interval of the clock the component ends up on, which lets a component be written without committing to a sample rate:

dyad
component MyDerivative@[input clk extends Discrete]
  u = RealInput@[clk]()
  y = RealOutput@[clk]()
  structural parameter Ts::Real = SampleTime()
relations
  y@clk = (u@clk - u@(clk-1)) / Ts
end

Control flow ​

Enums and switch-case ​

Enums define a type with a finite set of named values.

dyad
enum InitOptions =
  | FixedPosition(s0::Position)
  | Equilibrium
  | None

Switch statements allow different equations based on enum values:

dyad
  structural parameter init_option::InitOptions
...
relations
  switch init_option
    case FixedPosition
      initial s = init_option.s0
      initial v = 0
    case Equilibrium
      initial der(s) = 0
      initial der(v) = 0
    default
      initial s = 0
      initial v = 0
  end

If statements ​

If statements provide conditional logic in relations. You can encode an if statement via the ifelse function:

dyad
heat_output = ifelse(
  (temperature < 0),
  1000.0,
  ifelse((temperature > 25),
      0.0,
      500.0
  )
)

Not that the number of equations must match for each contingency unless the expression depends only on constants or structural parameters.

Metadata ​

Metadata attaches additional information to model elements for documentation, UI hints, or tool-specific data. Metadata is organized into namespaces. The Dyad namespace is reserved, but other namespaces can be used to manage any kind of structured (JSON) data, e.g.,

dyad
component MyComponent
  # ...
metadata {
  "Dyad": {
    "icons": {
      "default": "dyad://MyLibrary/my_component.svg"
    }
  },
  "ACME Enterprises": {
    "Author": "Wile E. Coyote",
    "Part Number": "XRocketA/30/F"
  }
}
end

For associating metadata with a definition, or...

dyad
p = Pin() [{ "Dyad": { "iconName": "pos" } }]

For associating metadata with individual components, connectors, variables, etc.

Linked metadata ​

Inline metadata becomes unwieldy when a graphical editor stores diagram placement and connection routing for every element. The {^tag} form attaches metadata by reference instead: it looks tag up in the enclosing definition's _links object and uses the object found there as that element's metadata.

dyad
component MyComponent
  u = RealInput() {^u}
  y = RealOutput() {^y}
relations
  connect(u, y) {^id1}
metadata {
  "_links": {
    "u": {"Dyad": {"placement": {"diagram": {"x1": 0, "y1": 0, "x2": 100, "y2": 100}}}},
    "y": {"Dyad": {"placement": {"diagram": {"x1": 300, "y1": 0, "x2": 400, "y2": 100}}}},
    "id1": {"Dyad": {"edges": [{"S": 1, "M": [], "E": 2}]}}
  }
}
end

The tag is an arbitrary identifier — it does not have to match the element's name, though the editor usually makes it do so. If the enclosing definition has no _links object, or it lacks the tag, the reference is reported as an invalid-metadata-link problem. _links itself is stripped from the definition's own metadata.

These markers are written and maintained by the graphical editor, so you rarely type them by hand, but you will see them throughout generated and library sources.