Skip to content

Edit tutorial contents #253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
f9d5262
Update index.md
BethProedrou Apr 29, 2025
3f47a4f
Update index.md
BethProedrou Apr 29, 2025
ad9bd53
Further updates to the Update index.md
BethProedrou Apr 30, 2025
0aaf778
Improved the "Where to begin" section
BethProedrou Apr 30, 2025
ffff152
improved Installation section
BethProedrou May 1, 2025
12a4821
added some missing commands to install the package
BethProedrou May 1, 2025
9f92084
added link to DAE
BethProedrou May 1, 2025
02f0868
improved the link
BethProedrou May 1, 2025
31f7f96
Improving Mathematical Model
BethProedrou May 1, 2025
27075a5
Impoved the mathematical model text
May 3, 2025
4810a1e
Merge branch 'main' into edit_tutorial_contents
hexaeder May 12, 2025
334d867
remove "diff style" in index.md
hexaeder May 12, 2025
49f8074
replaced the old style of changes with the new one
BethProedrou May 13, 2025
76cf0b4
half-way save
BethProedrou May 13, 2025
8a34eaf
fixed phrasing
BethProedrou May 14, 2025
65dee53
Fixed phrasing
BethProedrou May 14, 2025
728f0b6
improved phrasing
BethProedrou May 14, 2025
f5ba8de
fixed change I made
BethProedrou May 14, 2025
c780541
placed basic complex network systems information in a blockquote
May 26, 2025
5955040
improved readability in IDE
May 26, 2025
c08ceae
improved readability in IDE
May 26, 2025
1f04988
Fixed and added notes
May 26, 2025
ecfac0c
rearranged code to improve readability (work in progress)
May 28, 2025
2e555c6
working
May 28, 2025
bb3993f
working
May 29, 2025
f9e33db
fixed issues causing it not to run
May 29, 2025
c7749de
Merge branch 'main' into edit_tutorial_contents
May 29, 2025
47dfd31
Getting Started Tutorial v.1
May 31, 2025
84f1f2b
address comments
hexaeder Jun 2, 2025
45b002e
address more comments
hexaeder Jun 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 202 additions & 61 deletions docs/examples/getting_started_with_network_dynamics.jl

Large diffs are not rendered by default.

281 changes: 281 additions & 0 deletions docs/src/assets/edgemodel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions docs/src/assets/edgemodel.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
% \documentclass[tikz,convert={outfile=\jobname.svg}]{standalone}
\documentclass[tikz,convert=pdf2svg]{standalone}
%\usetikzlibrary{...}% tikz package already loaded by 'tikz' option
\usetikzlibrary{arrows, arrows.meta}
% \tikzset{
% >=stealth',
% }
\usepackage{amsmath,amssymb}
\usepackage{tikz}
\usepackage{xcolor}

\begin{document}
\begin{tikzpicture}
\node[draw, minimum height=1.5cm](n){
Edge Model
};
\draw[->](n.east)++(0,0.4)--++(1.0,0) node[right]{$y^{\mathrm e}_{\mathrm{src}}$}; \draw[->](n.east)++(0,-0.4)--++(1.0,0) node[right]{$y^{\mathrm e}_{\mathrm{dst}}$};

\draw[<-](n.west)++(0,0.4)--++(-1.0,0) node[left]{$y^{\mathrm v}_{i}=i^{\mathrm e}_{\mathrm{src}}$};
\draw[<-](n.west)++(0,-0.4)--++(-1.0,0) node[left]{$y^{\mathrm v}_{j}=i^{\mathrm e}_{\mathrm{dst}}$};

\node[text width=3.5cm, align=center, font=\scriptsize] at ([xshift=-2.7cm, yshift=-1.2cm]n) {mapping of adjecent node outputs as edge inputs};
\end{tikzpicture}
\end{document}
2 changes: 2 additions & 0 deletions docs/src/assets/makelatexfigs
Original file line number Diff line number Diff line change
@@ -18,4 +18,6 @@ if ! command_exists pdf2svg; then
fi

latexmk -pdflua -shell-escape mathmodel.tex
latexmk -pdflua -shell-escape edgemodel.tex
latexmk -pdflua -shell-escape nodemodel.tex
latexmk -C
254 changes: 254 additions & 0 deletions docs/src/assets/nodemodel.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
34 changes: 34 additions & 0 deletions docs/src/assets/nodemodel.tex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
% \documentclass[tikz,convert={outfile=\jobname.svg}]{standalone}
\documentclass[tikz,convert=pdf2svg]{standalone}
%\usetikzlibrary{...}% tikz package already loaded by 'tikz' option
\usetikzlibrary{arrows, arrows.meta}
% \tikzset{
% >=stealth',
% }
\usepackage{amsmath,amssymb}
\usepackage{tikz}
\usepackage{xcolor}

\begin{document}
\begin{tikzpicture}
\node[draw](n){
Node Model
};
\draw[->](n.east)--++(1.0,0) node[above, pos=.5]{$y^{\mathrm v}$};

% Draw the accumulator circle
\node[draw, circle] (acc) at ([xshift=-1.5cm]n.west) {$\mathrm{acc}$};

% Connect acc to the node
\draw[->] (acc) -- (n.west) node[pos=.5, above]{$i^{v}$};

% Add three arrows to acc with rectangular corners
\draw[<-] (acc) |- ++(-1.0,0.7) node[left](in){$y^e_i$};
\draw[<-] (acc) -- ++(-1.0,0) node[left]{$y^e_j$};
\draw[<-] (acc) |- ++(-1.0,-0.7) node[left]{$\ldots$};

% Add a label for the accumulation function
\node[text width=3cm, align=center, font=\scriptsize] at ([xshift=-.5cm, yshift=-1.2cm]acc) {accumulation of adjacent edge outputs};

\end{tikzpicture}
\end{document}
20 changes: 9 additions & 11 deletions docs/src/data_structure.md
Original file line number Diff line number Diff line change
@@ -4,19 +4,17 @@ A [`Network`](@ref) contains a list of vertex and edge models along with a graph
However, in tight numerical loops, it will never access these lists of models directly.
Instead, the network maintains an internal representation that tracks all symbolic indices, defining the precise ordering of states and parameters in a flat array representation. To optimize performance, especially for heterogeneous networks, the network employs specialized data structures that batch identical models together.

This disconnect between the explicit lists and the internal data structures
can be confusing.
This disconnect between the explicit lists and the internal data structures can be confusing.

## Flat Parameter and State Arrays

The vertex and edge models may contain metadata, such as initial values for states and parameters.
Crucially, this metadata is **only** for building and initialization of the
simulation.
The vertex and edge models may contain metadata, such as the initial values for states and parameters.
Crucially, this metadata is **only** for the building and initialization of the simulation.
During actual simulation, the state and parameters are handled as **flat arrays**, i.e., plain `Vector{Float64}` objects.

[`NWState`](@ref) and [`NWParameter`](@ref) serve as wrappers around flat arrays and the [`Network`](@ref) objects, allowing you to inspect and modify those flat arrays by addressing vertices and edges directly.

A typical workflow is:
A typical workflow is the following:

1. Set default values in the models using the metadata (see [Metadata](@ref)).
2. Create a network (see [Network Construction](@ref)).
@@ -44,26 +42,26 @@ You can access the models using `getindex`/`[]` with `VIndex` or `EIndex`:
```@example data_structure
v1 === nw[VIndex(1)]
```
This can be important when changing metadata of components. i.e., both lines below are equivalent:
This can be important when changing the metadata of components. i.e., both lines below are equivalent:
```@example data_structure
set_position!(v1, (1,0))
set_position!(nw[VIndex(1)], (1,0))
nothing #hide
```

!!! note "Aliasing of component models"
Since components are not copied, multiple entries in vertex and edge lists might point to the same instance of a model.
Since components are not copied, multiple entries in the vertex and edge lists might point to the same instance of a model.
```@example data_structure
nw = Network(complete_graph(3), [v1,v2,v1], e)
v1 === nw[VIndex(1)] === nw[VIndex(3)]
```
Consequently, metadata set for one model might affect another model.
This behavior can be beneficial for performance reasons.
To force copying of components, use the `dealias` keyword:
To force the copying of components, use the `dealias` keyword:
```@example data_structure
nw = Network(complete_graph(3), [v1,v2,v1], e; dealias=true)
nw[VIndex(1)] === nw[VIndex(3)] # neither of them === v1
```

## Extracting `Network`-object from Containers
`NetworkDynamics.jl` provides a [`extract_nw`](@ref) function, to get a reference to the wrapped `Network` object from different containers, such as solution objects or integrator objects.
## Extracting a `Network`-object from Containers
`NetworkDynamics.jl` provides a [`extract_nw`](@ref) function, to get a reference to the wrapped `Network` object from different containers, such as solution objects or [integrator objects](@extref DiffEq.integrator).
64 changes: 49 additions & 15 deletions docs/src/index.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
# NetworkDynamics

A package for working with dynamical systems on complex networks. NetworkDynamics.jl provides an interface between [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) and [DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl).
It allows for high performant simulation of dynamic networks by describing the local dynamics on the edges and vertices of the graph.

The behavior of a node or an edge can be described by algebraic equations, by differential algebraic equation (DAEs) in mass matrix form or by ordinary differential equations (ODE).

The central construction is the function [`Network`](@ref) that receives functions describing the local dynamics on the edges and nodes of
a graph `g` as inputs, and returns a composite function compatible with the
*NetworkDynamics.jl* is a package to simulate dynamical systems within complex networks. It provides an interface
between the [Graphs.jl](https://github.com/JuliaGraphs/Graphs.jl) and the
[DifferentialEquations.jl](https://github.com/SciML/DifferentialEquations.jl) packages and faciliates the simulation of
highly efficient dynamic networks by describing the local dynamics on the edges and vertices of the graph.

!!! note
Complex network systems are composed by the entities that comprise them (the nodes) and the relationships that connect
each entity with one another (the edges). The graphical depictions of such networks are called graphs. The simplest
network (which can be seen in Figure 1) is composed of two entities (so two nodes) who are only connected to each other.
This connection between the two is the edge of the system. Complex networks are composed of multiple nodes and edges,
with most nodes connected to multiple other nodes with multiple edges *(@Hans: can you created the graph of such a
network and place in here?)*

The behavior of a node or an edge can be described through the use of a) algebraic equations, b) differential algebraic
equation (DAEs) in mass matrix form or c) ordinary differential equations (ODE).

The core of the package is the function [`Network`](@ref). It accepts the functions describing the local dynamics on the
edges and nodes of the graph `g` as inputs, and returns a composite function compatible with the
DifferentialEquations.jl calling syntax.

```julia
@@ -15,26 +26,48 @@ nd(dx, x, p, t)
```

Main features:
- Clear separation of local dynamics and topology: you can easily change topology of your system or switching out dynamical components.
- High performance when working with heterogeneous models (which means heaving different local dynamics in different parts of your network).
- [Symbolic Indexing](@ref) into solutions and states: NetworkDynamics keeps track of the states of the individual subsystems.
- Different execution schemes: NetworkDynamics exploits the known inter-dependencies between components to auto parallelize execution, even on GPUs!
- Equation based models: use [ModelingToolkit.jl](https://docs.sciml.ai/ModelingToolkit/dev/) to define local dynamics, use `NetworkDynamics.jl` to combine them into large networks!
- Clear separation of local dynamics and topology: you can easily change the topology of your system or switch out
- dynamic components.
- High performance when working with heterogeneous models: you can have different local dynamics in different parts of
- your network.
- [Symbolic Indexing](@ref) into solutions and states: NetworkDynamics keeps track of the states of each individual
- subsystem.
- Diverse execution schemes: NetworkDynamics exploits the known inter-dependencies between components to auto
- parallelize execution, even on GPUs!
- Equation based models: you can model local dynamics using
- [ModelingToolkit.jl](https://docs.sciml.ai/ModelingToolkit/dev/) and them combine them into larger networks by using
- `NetworkDynamics.jl`!


## Where to begin?
Check out the [Mathematical Model](@ref) to understand the underlying modelling ideas of NetworkDynamics followed by the page on [Network Construction](@ref) to learn how to implement you own models.
To learn how to implement your own models and understand the underlying modelling ideas of NetworkDynamics you should
first read the [Mathematical Model](@ref) section, followed by section [Network Construction](@ref).

If you prefer to look at some concrete code first check out the [Getting Started](@ref) tutorial!


## Installation

Installation is straightforward with Julia's package manager.
Install Julia:
- https://julialang.org/install/
- Find your OS and follow the instructions for the installation

Install NetworkDynamics.jl with Julia's package manager:
```julia-repl
(v1.11) pkg> add NetworkDynamics
```

Next you need to install the Julia package Revise:
```julia-repl
import Pkg; Pkg.add("Revise")
```

Last you need to install the Julia package LiveServer:
```julia-repl
import Pkg; Pkg.add("LiveServer")
```
Comment on lines +60 to +68
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revise is only an optional development dependency, LiveServer is only necessary if you want to observe the docs locally (which most people will never do). Therefore, I'd cut this part.

If you think it would be good to point people to general usage help of Julia (so they learn about Revise, Project Environments and so on) I'd instead suggest to link out to something like https://modernjuliaworkflows.org/




## Reproducibility

@@ -78,7 +111,8 @@ Pkg.status(; mode = PKGMODE_MANIFEST) # hide
```

## Funding
Development of this project was in part funded by the *German Federal Ministry for Economic Affairs and Climate Action* as part of the *OpPoDyn*-Project (Project ID 01258425/1, 2024-2027).
Development of this project was in part funded by the *German Federal Ministry for Economic Affairs and Climate Action*
as part of the *OpPoDyn*-Project (Project ID 01258425/1, 2024-2027).

```@raw html
<img src="assets/bmwk_logo_en.svg" width="300"/>
102 changes: 70 additions & 32 deletions docs/src/mathematical_model.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,25 @@
# Mathematical Model
The basic mathematical model of `NetworkDynamics.jl` splits up the system into two parts: vertex and edge components.
The basic mathematical model of `NetworkDynamics.jl` splits the system in two parts: the vertex and
the edge components.

The main goal of `NetworkDynamics.jl` is to express the overall network dynamics as a
[Differential-Algebraic-Equation (DAE)](https://mathworld.wolfram.com/Differential-AlgebraicEquation.html)

The main goal of `NetworkDynamics.jl` is, to express the overall network dynamics as a Differential-Algebraic-Equation (DAE)
```math
M\,\frac{\mathrm{d}}{\mathrm{d}t}u = f^{\mathrm{nw}}(u, p, t)
```
where M is a (possibly singular) mass matrix, $u$ is the internal state vector of the system, $p$ are the parameters and $t$ is the time.
To make this compatible with the solvers for `OrdinaryDiffEq.jl`, the created [`Network`](@ref) object is a callable object
where $M$ is a (possibly singular) mass matrix, $u$ is the internal state vector of the system, $p$ are the parameters
and $t$ is the time.
To make this compatible with the solvers used in `OrdinaryDiffEq.jl`, the generated
[`Network`](@ref) object is a callable object
```
nw(du, u, p, t) # mutates du
nw(du, u, p, t) # mutates du as an "output"
```
with stored mass matrix information to build an `ODEProblem` based on the `Network`.
which represents the right-hand-side (RHS) of the equation above. The mass-matrix $m$ is stored in the `Network` object as well.

Instead of defining $f^{\mathrm{nw}}$ by hand, `ND.jl` helps you to build it automatically based on a list of decentralized nodal and edge dynamics, so-called `VertexModel` and `EdgeModel` objects.
Each component model $\mathrm c$ is modeled as general input-output-system
Instead of defining $f^{\mathrm{nw}}$ by hand, `ND.jl` helps you to build it automatically based on a list of
decentralized nodal and edge dynamics, the so-called `VertexModel` and `EdgeModel` objects. Each component model
$\mathrm c$ is modeled as a general input-output-system:

```math
\begin{aligned}
@@ -22,25 +28,39 @@ y^{\mathrm c} &= g^{\mathrm c}(x^\mathrm{c}, i_{\mathrm c}, p_{\mathrm c}, t)
\end{aligned}
```

where $M_{\mathrm{c}}$ is the component mass matrix, $x^{\mathrm c}$ are the component states, $i^{\mathrm c}$ are the *inputs* of the component and $y^{\mathrm c}$ is the *output* of the component. It is possible to have $\mathrm{dim}(x^{\mathrm{c}}) = 0$ and thus no internal states.
where $M_{\mathrm{c}}$ is the component mass matrix, $x^{\mathrm c}$ are the component states, $i^{\mathrm c}$ are the
***inputs*** of the component and $y^{\mathrm c}$ is the ***output*** of the component.
If $\mathrm{dim}(x^{\mathrm{c}}) = 0$, the number of internal states is 0.

In the context of the network, the **output of the edges are flow variables** and the **outputs of vertices are
potential variables**.
When the node and edge models are placed on a graph, the inputs and outputs ware connected: the nodes receive the output of the
adjacent edges as inputs and the edges receive the output of the adjecent nodes as inputs.

Thus, the *flow* on the edges depends on the *potentials* at both ends as inputs. The *potentials* of the nodes depend on the
incoming *flows* from all connected edges as an input. (Here, flow and potentials are meant in a conceptional and not
necessarily physical way.)

In the network context, the **output of the edges are flow variables**. The **outputs of vertices are potential variables**. In interconnection, the *flow* on the edges depends on the *potentials* at both ends as inputs. The *potentials* of the nodes depend on the incoming *flows* from all connected edges as an input. (Here, flow and potentials are meant in a conceptional and not necessarily physical way.)
```@raw html
<img src="../assets/mathmodel.svg" width="100%"/>
```
@Hans: the image seems to be missing here
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is not visible in the markdown preview in github, because relative links work different there. But it does show up for me in the rendered documentation.


## Vertex Models
Specifically, a (single-layer) vertex model has one input, and one output.
The input is an aggregation/reduction over all *incident edge outputs*,
```@raw html
<img src="../assets/nodemodel.svg" width="100%"/>
```
A (single-layer) vertex model has one input, and one output.
The input is an aggregation/reduction over all of the *incident edge outputs*,
```math
i^{\mathrm v} = \mathop{\mathrm{agg}}\limits_k^{\text{incident}} y^{\mathrm e}_k \qquad\text{often}\qquad
i^{\mathrm v} = \sum_k^{\text{incident}} y^{\mathrm e}_k
```
The full vertex model
```math
\begin{aligned}
M^{\mathrm v}\,\frac{\mathrm{d}}{\mathrm{d}t}x^{\mathrm v} &= f^{\mathrm v}(u^{\mathrm v}, i^{\mathrm v}, p^{\mathrm v}, t)\\
y^{\mathrm v} &= g^{\mathrm v}(u^{\mathrm v}, i^{\mathrm v}, p^{\mathrm v}, t)
M^{\mathrm v}\,\frac{\mathrm{d}}{\mathrm{d}t}x^{\mathrm v} &= f^{\mathrm v}(x^{\mathrm v}, i^{\mathrm v}, p^{\mathrm v}, t)\\
y^{\mathrm v} &= g^{\mathrm v}(x^{\mathrm v}, i^{\mathrm v}, p^{\mathrm v}, t)
\end{aligned}
```
corresponds to the Julia functions
@@ -57,13 +77,23 @@ vertf = VertexModel(; f=fᵥ, g=gᵥ, mass_matrix=Mᵥ, ...)
```

## Edge Models
In contrast to vertex models, edge models in general have *two* inputs and *two* outputs, both for source and destination end of the edge.
We commonly use `src` and `dst` to describe the source and destination end of an edge respectively.
```@raw html
<img src="../assets/edgemodel.svg" width="100%"/>
```
In contrast to vertex models, edge models in general have *two* inputs and *two* outputs, for both the source and the
destination end of the edge. We commonly use `src` and `dst` to describe the source and destination end of an edge
respectively.

!!! note "On the directionality of edges"
Mathematically, in a system defined on an undirected graph there is no difference between the edge $(1,2)$ and $(2,1)$, the edge has no direction. However, from an implementation point of view we always need to have some kind of ordering for function arguments, state order and so on. For undirected graphs, `Graphs.jl` chooses the direction of an edge `v1->v2` such that `v1 < v2`.
Mathematically, in a system defined on an undirected graph there is no difference between edge $(1,2)$ and
edge $(2,1)$, because the edge has no direction. However, from an implementation point of view we always need to have
some kind of ordering, which is why we introduce the source and destination terminology.

For undirected graphs, `Graphs.jl` chooses the direction of an edge `v1->v2` such that `v1 < v2`, i.e. the edge between vertices 16 and 12 will be always and edge with `src=12` and `dst=16`.

The *inputs* of the edge are just the outputs of the two nodes at both ends. The output is split into two: the `dst` output goes to the input of the vertex at the destination end, the `src` output goes to the input of the vertex at the `src` end.
The *inputs* of the edge are the outputs of the two nodes at both their ends. The output is split into two parts:
the `dst` output goes to the input of the vertex at the destination end, the `src` output goes to the input of the
vertex at the `src` end.

The full model of an edge
```math
@@ -73,7 +103,7 @@ y^{\mathrm e}_{\mathrm{dst}} &= g_\mathrm{dst}^{\mathrm e}(u^{\mathrm e}, y^{\ma
y^{\mathrm e}_{\mathrm{src}} &= g_\mathrm{src}^{\mathrm e}(u^{\mathrm e}, y^{\mathrm v}_{\mathrm{src}}, y^{\mathrm v}_{\mathrm{dst}}, p^{\mathrm e}, t)
\end{aligned}
```
corresponds to the Julia functions
which corresponds to the Julia functions:
```julia
function fₑ(dxₑ, xₑ, v_src, v_dst, pₑ, t)
# mutate dxᵥ
@@ -86,9 +116,9 @@ end
vertf = EdgeModel(; f=fₑ, g=gₑ, mass_matrix=Mₑ, ...)
```

The sign convention for both outputs of an edge must be identical,
typically, a positive flow represents a flow *into* the connected vertex.
This is important, because the vertex only receives the flows, it does not know whether the flow was produce by the source or destination end of an edge.
The sign convention for both outputs of an edge must be identical, so typically, a positive flow represents a flow
*into* the connected vertex. This is important, because the vertex only receives the flows, it does not know whether
the flow was produce by the source or the destination end of an edge.
```
y_src y_dst
V_src o───←─────────→───o V_dst
@@ -97,23 +127,27 @@ This is important, because the vertex only receives the flows, it does not know


### Single Sided Edge Outputs
Often, edge outputs will possess some symmetry which makes it more convenient to define "single sided" edge output functions
Often, edge outputs will possess some symmetry. This makes it more convenient to define
"single sided" edge output functions:
```julia
function g_single(y, xᵥ, v_src, v_dst, pₑ, t)
# mutate y
nothing
end
```
There are multiple wrappers available to automaticially convert them into double-sided edge output functions:
There are multiple wrappers available to automatically convert them into double-sided edge
output functions:

- `Directed(g_single)` builds a double-sided function *which only couples* to the destination side.
- `Symmetric(g_single)` builds a double-sided function in which both ends receive `y`.
- `AntiSymmetric(g_single)` builds a double-sided function where the destination receives `y` and the source receives `-y`.
- `Fiducial(g_single_src, g_singl_dst)` builds a double-sided edge output function based on two single sided functions.


## Feed Forward Behavior
The most general version of the component models can contain direct feed forwards from the input, i.e. the edge output might depend directly on the connected vertices or the vertex output might depend directly on the aggregated edge input.
Component models can show have so-called feed forward behavior. Feed forward means, that there is a direct link from input to output.

The most generic version of the component models can contain direct feed forwards from the input to the output.
This means, the output function depends $g$ directly depends on the component inputs $i$ and not only on the component state $x$.

Whenever possible, you should define output functions without feed forwards, i.e.
```julia
@@ -127,16 +161,19 @@ gₑ([y_src], y_dst, xᵥ, v_src, v_dst, pₑ, t)
```

NetworkDynamics cannot couple two components with feed forward to each other.
It is always possible to transform feed forward behavior to an internal state `x` with mass matrix entry zero to circumvent this problem. This transformation can be performed automatically by using [`ff_to_constraint`](@ref).
But, it is always possible to transform feed forward behavior to an internal state `x` with mass matrix entry zero to
circumvent this problem. This transformation can be performed automatically by using [`ff_to_constraint`](@ref).


!!! warning "Feed Forward Vertices"
As of 11/2024, vertices with feed forward are not supported at all. Use [`ff_to_constraint`](@ref) to transform them into vertex model without FF.
As of 11/2024, vertices with feed forward are not supported at all. Use [`ff_to_constraint`](@ref) to transform them
into vertex model without FF.

Concretely, NetworkDynamics distinguishes between 4 types of feed forward behaviors of `g` functions based on the [`FeedForwardType`](@ref) trait.
The different types the signature of provided function `g`.
Based on the signatures avaialable, ND.jl will try to find the correct type automaticially. Using the `ff`
keyword in the constructors, the user can enforce a specific type.
Concretely, NetworkDynamics distinguishes between 4 types of feed forward behaviors of `g` functions based on the
[`FeedForwardType`](@ref) trait.
The feed forward type is inferred automatically based on the provided function `g`, more concretely it is determined by the signature of that method / the number of arguments it takes.

The code blow shows the different `g` signatures for the different feed forward types
**[`PureFeedForward()`](@ref)**
```julia
g!(outs..., ins..., p, t) # abstractly
@@ -166,3 +203,4 @@ g!(out_src, out_dst, x) # double-sided edge
g!(v_out, x) # single layer vertex
```

If the automatic inference of feed forward type fails, the user may specify it explicitly using the `ff` keyword argument of the Edge/VertexModel constructor.
44 changes: 21 additions & 23 deletions docs/src/network_construction.md
Original file line number Diff line number Diff line change
@@ -2,9 +2,9 @@

## Building a Network
The main type of `NetworkDynamics.jl` is a [`Network`](@ref).
A network bundles various component models (edge and vertex models) together with a graph to form a callable object which represents the RHS of the overall dynamical system, see [Mathematical Model](@ref).
A network bundles various component models (edge and vertex models) together with a graph to form a callable object which represents the right hand side (RHS) of the overall dynamical system, see [Mathematical Model](@ref).

A `Network` is build by passing a graph `g`, vertex models `vertexm` and edge models `edgem`.
A `Network` is build by passing a graph `g`, vertex models `vertexm` and edge models `edgem` to the [`Network`](@ref) constructor:.
```julia
nw = Network(g, vertexm, edgem; kwargs...)
```
@@ -13,20 +13,17 @@ Two important keywords for the [`Network`](@ref) constructor are:

- `execution`:
Defines the [`ExecutionStyle`](@ref) of the coreloop, e.g. `SequentialExecution{true}()`.
A execution style is a special struct which tells the backend how to parallelize for example.
A execution style is a special Julia object, which tells the backend how to parallelize (e.g. `ThreadedExecution{true}()` will use native Julia threads to parallelize the RHS call).
A list of available executions styles can be found under [Execution Types](@ref) in the API.

- `aggregator`:
Tells the backend how to aggregate and which aggregation function to use.
Aggregation is the process of creating a single vertex input by reducing over
the outputs of adjecent edges of said vertex. The `aggregator` contains both the
function and the algorithm. E.g. `SequentialAggregator(+)` is a sequential
aggregation by summation. A list of availabe Aggregators can be found under
[`Aggregators`](@ref) in the API.
- `aggregator`:
Instructs the backend how to perform the aggregation and which aggregation function to use.
Aggregation is the process of creating a single vertex input by reducing over the outputs of adjecent edges of said vertex. The `aggregator` contains both the function and the algorithm. E.g. `SequentialAggregator(+)` is a sequential aggregation by summation. A list of availabe Aggregators can be found under [`Aggregators`](@ref) in the API.

## Building `VertexModel`s
This chapter walks through the most important aspects when defining custom vertex model. For a list of all keyword arguments please check out the docstring of [`VertexModel`](@ref).
As an example, we'll construct an second order kuramoto model, because that's what we do.
This chapter will walk you through the most important aspects of defining a custom vertex model. For a list of all keyword arguments please check out the docstring of [`VertexModel`](@ref).

As an example, we'll construct an second order kuramoto model, because that is what the package does.
```@example construction
using NetworkDynamics #hide
function kuramoto_f!(dv, v, esum, p, t)
@@ -51,30 +48,31 @@ function kuramoto_g_noff!(y, v, p, t)
end
VertexModel(; f=kuramoto_f!, g=kuramoto_g_noff!, dim=2, pdim=3, outdim=1)
```
It is still annoying to explicitly write this trivial output function. You can prevent this by using [`StateMask`](@ref).

To simplify your programming and avoid explicitly writing the above trivial output function you can use [`StateMask`](@ref).
By writing
```@example construction
VertexModel(; f=kuramoto_f!, g=StateMask(1:1), dim=2, pdim=3)
```
we told the vertex model, that the output is part of the states `x[1:1]`.
This enables a few things:
- `outdim` is not needed anymore, can be inferred from `StateMask`
we are instructing the vertex model, that the output is part of the states `x[1:1]`.
This results in the following changes:
- `outdim` is removed because it can be inferred from `StateMask`
- `outsym` is not a generic `:o` any more but inferred from the state symbols.

We can be even less verbose by writing `g=1:1` or just `g=1`.

In a last we define better names for our states and parameters as well as assigning a position in the graph to enable the graphless network construction.
Whenever you provide `sym` keyword the corresponding `dim` keyword is not neccessary anymore. We end up with a relatively short definition
Lastly, we define improved names for our states and parameters as well as assigning a position in the graph to enable the graphless network construction.
Whenever you provide a `sym` keyword the corresponding `dim` keyword stops being neccessary. So, we end up with a relatively short definition
```@example construction
VertexModel(; f=kuramoto_f!, g=1,
sym=[:θ, :ω], psym=[:M=>1, :P=>0.1, :D=>0],
insym=[:P_nw], name=:swing, vidx=1)
```

## Building `EdgeModel`s
This chapter walks through the most important aspects when defining custom edge models. For a list of all keyword arguments please check out the docstring of [`EdgeModel`](@ref).
This chapter walks you through the most important aspects when defining custom edge models. For a list of all keyword arguments please check the docstring of [`EdgeModel`](@ref).

As an example edge model we want to define standard sinusoidal coupling between the vertices in our network. The full definition looks like this:
As an example edge model we define a standard sinusoidal coupling between the vertices in our network. The full definition is:

```@example construction
function edge_f!(de, e, vsrc, vdst, p, t)
@@ -95,15 +93,15 @@ function edge_g_ff!(ysrc, ydst, vsrc, vdst, p, t)
end
EdgeModel(;g=edge_g_ff!, pdim=1, outdim=1)
```
which no classifies as a `PureFeedForward` edge.
In cases like this, where the edge is actually anti symmetric we can alternatively define a single sided output function and wrapping it in an `AntiSymmetric` object
which classifies as a `PureFeedForward` edge.
In cases like this, where the edge is actually anti-symmetrical we can define a single sided output function and wrap it in an `AntiSymmetric` object:
```@example construction
function edge_g_s!(ydst, vsrc, vdst, p, t)
ydst[1] = p[1] * sin(vsrc[1] - vdst[1])
end
EdgeModel(;g=AntiSymmetric(edge_g_ff!), pdim=1, outdim=1)
```
which can also lead to briefer output naming. Available single sided wrappers are
This can also lead to briefer output naming. Available single sided wrappers are:
- [`Directed`](@ref) (no coupling at `src`),
- [`AntiSymmetric`](@ref) (same coupling at `src` and `dst`),
- [`Symmetric`](@ref) (inverse coupling at `dst`) and
22 changes: 12 additions & 10 deletions docs/src/symbolic_indexing.md
Original file line number Diff line number Diff line change
@@ -31,8 +31,11 @@ vertexf = VertexModel(f=_vertexf!, g=1, sym=[:storage])
## Fundamental Symbolic Indices
The default types for this access are the types [`VIndex`](@ref), [`EIndex`](@ref), [`VPIndex`](@ref) and [`EPIndex`](@ref).
Each of those symbolic indices consists of 2 elements: a reference to the network component and a reference to the symbol within that component.
As such, `VIndex(2, :x)` refers to variable with symbolic name `:x` in vertex number 2.
`EPIndex(4, 2)` would refer to the *second* parameter of the edge component for the 4th edge.
As such:
- `VIndex(2, :x)` refers to variable with symbolic name `:x` in vertex number 2.
- `EPIndex(4, 2)` refers to the *second* parameter of the edge component for the 4th edge.
- `VPIndex`() refers to (@Hans please fill in this with an example)
- `EPIndex`() refers to (@Hans please fill in this with an example)
Comment on lines +37 to +38
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You added those examples, neither VPIndex() nor EPIndex() are valid i think. Why would you like to include it?


!!! details "Setup code to make following examples work"
```@example si
@@ -47,13 +50,12 @@ As such, `VIndex(2, :x)` refers to variable with symbolic name `:x` in vertex nu

Those fundamental indices can be used in a lot of scenarios. Most importantly you can use them to
```@example si
sol(sol.t; idxs=VIndex(1, :storage)) # extract timeseries out ouf solution object
sol(sol.t; idxs=VIndex(1, :storage)) # extract timeseries out of a solution object
plot(sol; idxs=[VIndex(1, :storage), VIndex(5,:storage)]) # plot storage of two nodes
```

## Generate Symbolic Indices
Often, you need many individual symbolic indices. For that there are the helper methods [`vidxs`](@ref), [`eidxs`](@ref), [`vpidxs`](@ref) and [`epidxs`](@ref).
With the help of those methods you can generate arrays of symbolic indices:
Often, you need many individual symbolic indices. To achieve this you can use the helper methods [`vidxs`](@ref), [`eidxs`](@ref), [`vpidxs`](@ref) and [`epidxs`](@ref). With their help you can generate arrays of symbolic indices:

```@example si
vidxs(nw, :, :storage) # get variable "storage" for all vertices
@@ -98,7 +100,7 @@ in these flat arrays corresponds exactly to the order returned by [`variable_sym

!!! note
The `NWState` and `NWParameter` wrappers can be constructed from various objects.
Fore example, within a callback you might construct `p = NWParameter(integrator)` to then change the parameters of the network within the callback.
For example, within a callback you might construct `p = NWParameter(integrator)` to then change the parameters of the network within the callback.


## Observables
@@ -109,10 +111,10 @@ the `SciML` ecosystem, these values are called Observables.
A prime example of Observables are edge/vertex-outputs, such as the `flow` in the edge model defined above.
It is also possible to define additional Observables manually by using the `obssym` and `obsf` keyword
on the `EdgeModel`/`VertexModel` constructors.
When building models using ModelingToolkit, the reduced algebraic states will be preserved as observables automatically.

Observables can be accessed like any other state, for example, the flows in the network don't show up in the state array but can be accessed in all the ways discussed above, for example
When building models using ModelingToolkit, the reduced algebraic states will be preserved automatically as observables.

Observables can be accessed like any other state. For example, the flows in the network don't show up in the state array but can be accessed in all the ways discussed above.
For example:
```@example si
plot(sol; idxs=eidxs(nw, :, :flow))
```
@@ -150,7 +152,7 @@ idxs = SII.parameter_index(nw, eidxs(1:2, :K))
pflat(s)[idxs] == s.p.e[1:2, :K]
```

If you need the symbols of all the states/parameters in order, you can use
If you need the symbols of all the states/parameters in order, you can use:
```@example si
SII.variable_symbols(nw)
```