From 03beee456a46cb86fdce9b195e8c1571fb8221cc Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Tue, 25 Aug 2026 14:02:03 +0200 Subject: [PATCH 01/12] Add Timelines section in describing_models section --- docs/describing_models.rst | 138 +++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 531ab77..ceeae46 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -132,6 +132,144 @@ separated by periods. Depending on the context, this may represent a name in a n or an attribute of an object (as we will see below with Conduits). +Timelines +````````` + +Different components of a model run at their own rate, which is expressed by putting them +on a timeline. A model's timelines must follow a few rules: + +- A component that is not called by any other component has its ``f_init`` and ``o_f`` + ports on the root timeline, written ``:``. +- A component's ``o_i``/``s`` ports belong to a timeline nested inside the component's own + timeline. +- A component whose ``f_init``/``o_f`` ports are connected by a conduit is on the same + timeline as the port on the other end of that conduit. + +:func:`.ymmsl.v0_2.resolve_timelines` works out the timeline of every component and port +automatically, purely from how ``f_init``/``o_f`` and ``o_i``/``s`` ports are wired together +with conduits. It raises a :class:`.ymmsl.v0_2.ResolveTimelineException` if the conduits +don't describe a consistent set of timelines. + +Take a macro model that calls a micro model in a loop, without naming any timelines: + +.. code-block:: yaml + :caption: A macro-micro coupling, timelines left to the default + + components: + macro: + ports: + o_i: bc_out + s: bc_in + description: '' + micro: + ports: + f_init: init_in + o_f: final_out + description: '' + conduits: + macro.bc_out: micro.init_in + micro.final_out: macro.bc_in + +Since ``macro``'s ``o_i``/``s`` ports aren't given a name, they default to a +subtimeline named after ``macro`` itself, and ``micro`` ends up on that same +subtimeline: + +.. code-block:: python + :caption: Resolving the default timelines in python code + + from pathlib import Path + import ymmsl + from ymmsl.v0_2 import resolve_timelines + + config = ymmsl.load(Path('macro_micro.ymmsl')) + model = config.models['macro_micro'] + resolve_timelines(model) + + print(model.components['macro'].timeline) # output: : + print(model.components['micro'].timeline) # output: :macro + +A timeline is written as a colon-separated list of names, starting with the root +timeline ``:`` for the outermost level of the model, and growing by one name +for each level of nesting. + +You could also name the timeline yourself: + +.. code-block:: yaml + :caption: The same coupling, with an explicit timeline name + + components: + macro: + ports: + timeline tl1: + o_i: bc_out + s: bc_in + description: '' + micro: + ports: + f_init: init_in + o_f: final_out + description: '' + conduits: + macro.bc_out: micro.init_in + micro.final_out: macro.bc_in + +now puts ``micro`` on ``:tl1`` instead of ``:macro``. Note that this absolute timeline +is only stored on ``micro`` as a whole; ``macro``'s own port just gets the new name as +its (relative) timeline, since it is still part of ``macro`` itself: + +.. code-block:: python + :caption: Resolving the explicitly named timeline + + print(model.components['macro'].timeline) # output: : + print(model.components['micro'].timeline) # output: :tl1 + print(model.components['macro'].ports['bc_out'].timeline) # output: tl1 + +A component can also be connected to more than one timeline, for example when it calls +two other components at different rates. In that case, its ``o_i``/``s`` ports must be +grouped explicitly by timeline name, as in the previous example, rather than left to +default grouping. Extending the example with an extra ``micro2`` component that ``macro`` +calls at a different rate than ``micro1``: + +.. code-block:: yaml + :caption: One component with two subtimelines + + components: + macro: + ports: + timeline tl1: + o_i: micro1_out + s: micro1_in + timeline tl2: + o_i: micro2_out + s: micro2_in + description: '' + micro1: + ports: + f_init: init_in + o_f: final_out + description: '' + micro2: + ports: + f_init: init_in + o_f: final_out + description: '' + conduits: + macro.micro1_out: micro1.init_in + micro1.final_out: macro.micro1_in + macro.micro2_out: micro2.init_in + micro2.final_out: macro.micro2_in + +``macro`` itself stays on the root timeline, but ``micro1`` and ``micro2`` end up on the two +different subtimelines it calls them on: + +.. code-block:: python + :caption: Resolving multiple subtimelines from one component + + print(model.components['macro'].timeline) # output: : + print(model.components['micro1'].timeline) # output: :tl1 + print(model.components['micro2'].timeline) # output: :tl2 + + Conduits ```````` From 79abc5d36495bcccedcc8f677cc4c605ba53141f Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 31 Aug 2026 09:38:56 +0200 Subject: [PATCH 02/12] Timelines documentation --- docs/describing_models.rst | 175 ++++++++------------------ docs/timelines_macro_meso_micro.svg | 1 + docs/timelines_macro_meso_micro.ymmsl | 29 +++++ docs/timelines_two_subtimelines.svg | 1 + docs/timelines_two_subtimelines.ymmsl | 31 +++++ 5 files changed, 111 insertions(+), 126 deletions(-) create mode 100644 docs/timelines_macro_meso_micro.svg create mode 100644 docs/timelines_macro_meso_micro.ymmsl create mode 100644 docs/timelines_two_subtimelines.svg create mode 100644 docs/timelines_two_subtimelines.ymmsl diff --git a/docs/describing_models.rst b/docs/describing_models.rst index ceeae46..47b3945 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -135,139 +135,62 @@ or an attribute of an object (as we will see below with Conduits). Timelines ````````` -Different components of a model run at their own rate, which is expressed by putting them -on a timeline. A model's timelines must follow a few rules: - -- A component that is not called by any other component has its ``f_init`` and ``o_f`` - ports on the root timeline, written ``:``. -- A component's ``o_i``/``s`` ports belong to a timeline nested inside the component's own - timeline. -- A component whose ``f_init``/``o_f`` ports are connected by a conduit is on the same - timeline as the port on the other end of that conduit. - -:func:`.ymmsl.v0_2.resolve_timelines` works out the timeline of every component and port -automatically, purely from how ``f_init``/``o_f`` and ``o_i``/``s`` ports are wired together -with conduits. It raises a :class:`.ymmsl.v0_2.ResolveTimelineException` if the conduits -don't describe a consistent set of timelines. - -Take a macro model that calls a micro model in a loop, without naming any timelines: - -.. code-block:: yaml - :caption: A macro-micro coupling, timelines left to the default - - components: - macro: - ports: - o_i: bc_out - s: bc_in - description: '' - micro: - ports: - f_init: init_in - o_f: final_out - description: '' - conduits: - macro.bc_out: micro.init_in - micro.final_out: macro.bc_in - -Since ``macro``'s ``o_i``/``s`` ports aren't given a name, they default to a -subtimeline named after ``macro`` itself, and ``micro`` ends up on that same -subtimeline: - -.. code-block:: python - :caption: Resolving the default timelines in python code - - from pathlib import Path - import ymmsl - from ymmsl.v0_2 import resolve_timelines - - config = ymmsl.load(Path('macro_micro.ymmsl')) - model = config.models['macro_micro'] - resolve_timelines(model) - - print(model.components['macro'].timeline) # output: : - print(model.components['micro'].timeline) # output: :macro - -A timeline is written as a colon-separated list of names, starting with the root -timeline ``:`` for the outermost level of the model, and growing by one name -for each level of nesting. - -You could also name the timeline yourself: - -.. code-block:: yaml - :caption: The same coupling, with an explicit timeline name - - components: - macro: - ports: - timeline tl1: - o_i: bc_out - s: bc_in - description: '' - micro: - ports: - f_init: init_in - o_f: final_out - description: '' - conduits: - macro.bc_out: micro.init_in - micro.final_out: macro.bc_in - -now puts ``micro`` on ``:tl1`` instead of ``:macro``. Note that this absolute timeline -is only stored on ``micro`` as a whole; ``macro``'s own port just gets the new name as -its (relative) timeline, since it is still part of ``macro`` itself: - -.. code-block:: python - :caption: Resolving the explicitly named timeline +Different components of a coupled simulation typically run at their own pace: a fast, +detailed micro model may take many small steps for every single step of the macro model +driving it, and a meso model may sit somewhere in between the two. yMMSL captures this +idea of "running at a different pace" as a *timeline*. Wiring a component's ``o_i``/``s`` +ports (the ports it uses to run a loop) to another component's ``f_init``/``o_f`` ports +puts that other component, and anything it in turn drives, on a timeline nested inside the +first. yMMSL works this out automatically from how components are wired together with +conduits, so in most models you never have to declare a timeline explicitly. + +A component that nobody calls sits on the outermost, root timeline, written ``:``. Every +level of nesting adds one more name, giving each timeline in the model an addressable +path, a bit like a folder structure. + +Take a macro model that calls a meso model in a loop, and where that meso model in turn +calls a micro model in its own loop. This produces three levels of timelines: the root +timeline for ``macro``, a timeline nested inside it for ``meso`` (which runs once for +every step ``macro`` takes), and a timeline nested inside *that* for ``micro`` (which runs +once for every step ``meso`` takes): + +.. literalinclude:: timelines_macro_meso_micro.ymmsl + :caption: ``docs/timelines_macro_meso_micro.ymmsl`` + :language: yaml - print(model.components['macro'].timeline) # output: : - print(model.components['micro'].timeline) # output: :tl1 - print(model.components['macro'].ports['bc_out'].timeline) # output: tl1 +.. figure:: timelines_macro_meso_micro.svg + :alt: macro connects to meso through F_INIT/O_F and O_I/S ports, and meso connects to + micro the same way, producing three nested timelines. -A component can also be connected to more than one timeline, for example when it calls -two other components at different rates. In that case, its ``o_i``/``s`` ports must be -grouped explicitly by timeline name, as in the previous example, rather than left to -default grouping. Extending the example with an extra ``micro2`` component that ``macro`` -calls at a different rate than ``micro1``: + The same model, visualized with `ymmsl2svg + `_. Nesting in the figure mirrors nesting in + time: ``meso``'s box sits inside ``macro``'s, and ``micro``'s sits inside ``meso``'s. -.. code-block:: yaml - :caption: One component with two subtimelines +A single component can also be connected to more than one timeline at once, for example +when it drives two other components that run at different rates. ``macro`` calling +``micro1`` in one loop and ``micro2`` in a separate loop puts ``micro1`` and ``micro2`` on +two independent timelines nested inside ``macro``'s own, rather than on a shared one. +Since there's more than one loop to keep apart, each one needs an explicit name: group +the ports that belong together under a ``timeline :`` heading, one per loop: - components: - macro: - ports: - timeline tl1: - o_i: micro1_out - s: micro1_in - timeline tl2: - o_i: micro2_out - s: micro2_in - description: '' - micro1: - ports: - f_init: init_in - o_f: final_out - description: '' - micro2: - ports: - f_init: init_in - o_f: final_out - description: '' - conduits: - macro.micro1_out: micro1.init_in - micro1.final_out: macro.micro1_in - macro.micro2_out: micro2.init_in - micro2.final_out: macro.micro2_in +.. literalinclude:: timelines_two_subtimelines.ymmsl + :caption: ``docs/timelines_two_subtimelines.ymmsl`` + :language: yaml -``macro`` itself stays on the root timeline, but ``micro1`` and ``micro2`` end up on the two -different subtimelines it calls them on: +.. figure:: timelines_two_subtimelines.svg + :alt: macro has two separate pairs of O_I/S ports, one connecting down to micro1 and + one connecting down to micro2, side by side. -.. code-block:: python - :caption: Resolving multiple subtimelines from one component + The same model, visualized with `ymmsl2svg + `_. ``macro``'s two named timelines are drawn + side by side beneath it, each with its own pair of ports, one leading to ``micro1`` + and the other to ``micro2``. - print(model.components['macro'].timeline) # output: : - print(model.components['micro1'].timeline) # output: :tl1 - print(model.components['micro2'].timeline) # output: :tl2 +This is the pattern to reach for whenever a single component acts as the driver for +more than one independently-paced loop, for example a component that advances a coarse +grid with one fast inner solver and a separate, differently-paced inner solver for a +refined region, or a driver that runs an ensemble of replicas at one rate while also +maintaining some shared bookkeeping process at another. Conduits diff --git a/docs/timelines_macro_meso_micro.svg b/docs/timelines_macro_meso_micro.svg new file mode 100644 index 0000000..f19f6b6 --- /dev/null +++ b/docs/timelines_macro_meso_micro.svg @@ -0,0 +1 @@ +Model: macro_meso_micro_modelmacrobc_outbc_inmesoinit_infinal_outbc_outbc_inmicroinit_infinal_out diff --git a/docs/timelines_macro_meso_micro.ymmsl b/docs/timelines_macro_meso_micro.ymmsl new file mode 100644 index 0000000..f815651 --- /dev/null +++ b/docs/timelines_macro_meso_micro.ymmsl @@ -0,0 +1,29 @@ +ymmsl_version: v0.2 + +description: A macro-meso-micro model, three levels of timelines + +models: + macro_meso_micro_model: + components: + macro: + ports: + o_i: bc_out + s: bc_in + description: '' + meso: + ports: + f_init: init_in + o_i: bc_out + s: bc_in + o_f: final_out + description: '' + micro: + ports: + f_init: init_in + o_f: final_out + description: '' + conduits: + macro.bc_out: meso.init_in + meso.final_out: macro.bc_in + meso.bc_out: micro.init_in + micro.final_out: meso.bc_in diff --git a/docs/timelines_two_subtimelines.svg b/docs/timelines_two_subtimelines.svg new file mode 100644 index 0000000..4126c23 --- /dev/null +++ b/docs/timelines_two_subtimelines.svg @@ -0,0 +1 @@ +Model: two_subtimelines_modelmacromicro1_outmicro2_outmicro2_inmicro1_inmicro1init_infinal_outmicro2init_infinal_out diff --git a/docs/timelines_two_subtimelines.ymmsl b/docs/timelines_two_subtimelines.ymmsl new file mode 100644 index 0000000..221da3d --- /dev/null +++ b/docs/timelines_two_subtimelines.ymmsl @@ -0,0 +1,31 @@ +ymmsl_version: v0.2 + +description: One component (macro) connected to two independent timelines + +models: + two_subtimelines_model: + components: + macro: + ports: + timeline tl1: + o_i: micro1_out + s: micro1_in + timeline tl2: + o_i: micro2_out + s: micro2_in + description: '' + micro1: + ports: + f_init: init_in + o_f: final_out + description: '' + micro2: + ports: + f_init: init_in + o_f: final_out + description: '' + conduits: + macro.micro1_out: micro1.init_in + micro1.final_out: macro.micro1_in + macro.micro2_out: micro2.init_in + micro2.final_out: macro.micro2_in From 46c43275382e304003910c267511b5b8cb8a19d9 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 31 Aug 2026 12:00:30 +0200 Subject: [PATCH 03/12] add conduit filter documentation --- docs/conduit_filters_bypass.svg | 1 + docs/conduit_filters_bypass.ymmsl | 31 ++++++++++++++++ docs/describing_models.rst | 62 +++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 docs/conduit_filters_bypass.svg create mode 100644 docs/conduit_filters_bypass.ymmsl diff --git a/docs/conduit_filters_bypass.svg b/docs/conduit_filters_bypass.svg new file mode 100644 index 0000000..aa13003 --- /dev/null +++ b/docs/conduit_filters_bypass.svg @@ -0,0 +1 @@ +Model: macro_meso_micro_filteredmacrobypass_outbc_outbc_inbypass_inmesoinit_infinal_outbc_outbc_inmicroinit_inbypass_infinal_outbypass_out diff --git a/docs/conduit_filters_bypass.ymmsl b/docs/conduit_filters_bypass.ymmsl new file mode 100644 index 0000000..7275f25 --- /dev/null +++ b/docs/conduit_filters_bypass.ymmsl @@ -0,0 +1,31 @@ +ymmsl_version: v0.2 + +description: macro-meso-micro with a bypass conduit needing filters + +models: + macro_meso_micro_filtered: + components: + macro: + ports: + o_i: bc_out bypass_out + s: bc_in bypass_in + description: '' + meso: + ports: + f_init: init_in + o_i: bc_out + s: bc_in + o_f: final_out + description: '' + micro: + ports: + f_init: init_in bypass_in + o_f: bypass_out final_out + description: '' + conduits: + macro.bc_out: meso.init_in + macro.bypass_out: repeat micro.bypass_in + meso.final_out: macro.bc_in + meso.bc_out: micro.init_in + micro.final_out: meso.bc_in + micro.bypass_out: last macro.bypass_in diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 47b3945..9226052 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -247,6 +247,68 @@ sender: print(conduits[0]) # output: Conduit(sender.port -> receiver1.port) print(conduits[1]) # output: Conduit(sender.port -> receiver2.port) +Conduit filters +^^^^^^^^^^^^^^^ + +As explained in :ref:`Timelines` above, a conduit that connects a port on one timeline +to a port on another must bridge the difference in how often either side sends or +receives, using a filter: + +- ``repeat`` and ``pad`` cross from an outer timeline into a nested one: a single + message sent on the outer timeline (e.g. an initial state) is repeated, or followed + by empty messages, to match every receive on the nested timeline. +- ``last`` crosses from a nested timeline back out to its parent: of the many messages + sent on the nested timeline, only the last one (e.g. a final result) is passed on. + +Filters are written in front of the receiver and may be combined: + +.. code-block:: yaml + :caption: Specifying conduit filters in yMMSL + + conduits: + macro.init_out: repeat micro.init_in + micro.state_out: last macro.final_in + +A filter is only needed where a conduit *skips* a level of nesting. Extending the +macro-meso-micro example from :ref:`Timelines`: ``macro`` lives on the root timeline +``:``, ``meso`` on ``:macro``, and ``micro`` on ``:macro:meso``. The conduits that call +``meso`` and that call ``micro`` from ``meso`` don't need a filter, since each one only +crosses a single level of nesting that it also establishes. A conduit that goes directly +from ``macro`` to ``micro``, bypassing ``meso``, skips a level, and does need one: + +.. literalinclude:: conduit_filters_bypass.ymmsl + :caption: ``docs/conduit_filters_bypass.ymmsl`` + :language: yaml + +.. figure:: conduit_filters_bypass.svg + :alt: macro and micro have an extra pair of ports directly connecting them, + bypassing meso, labeled "repeat" and "last". + + The same model, visualized with `ymmsl2svg + `_. Besides the normal call/release + conduits, ``macro`` sends directly into ``micro``'s timeline with ``repeat`` (since + ``macro`` only sends once per ``meso`` step, but ``micro`` runs several times per + ``meso`` step), and ``micro`` reports back to ``macro`` with ``last`` (since only + the final value of several ``micro`` runs is meaningful once you're back up at + ``macro``'s level). + +This is represented in Python by the ``filters`` attribute of +:class:`.ymmsl.v0_2.Conduit`, a list of :class:`.ymmsl.v0_2.ConduitFilter` values: + +.. code-block:: python + :caption: Conduit filters in python code + + from pathlib import Path + import ymmsl + + config = ymmsl.load(Path('conduit_filters_bypass.ymmsl')) + model = config.models['macro_meso_micro_filtered'] + + conduits = model.conduits + print(conduits[1]) # output: Conduit(macro.bypass_out -> repeat -> micro.bypass_in) + print(conduits[1].filters) # output: [] + + Nesting models `````````````` From e855954efe599c2d8ed11c2e188d67e2879e41fa Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 31 Aug 2026 12:18:08 +0200 Subject: [PATCH 04/12] update conduit filters --- docs/describing_models.rst | 53 +++++++++++++------------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 9226052..0537fe2 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -139,10 +139,10 @@ Different components of a coupled simulation typically run at their own pace: a detailed micro model may take many small steps for every single step of the macro model driving it, and a meso model may sit somewhere in between the two. yMMSL captures this idea of "running at a different pace" as a *timeline*. Wiring a component's ``o_i``/``s`` -ports (the ports it uses to run a loop) to another component's ``f_init``/``o_f`` ports -puts that other component, and anything it in turn drives, on a timeline nested inside the -first. yMMSL works this out automatically from how components are wired together with -conduits, so in most models you never have to declare a timeline explicitly. +ports to another component's ``f_init``/``o_f`` ports puts that other component, and anything +it in turn drives, on a timeline nested inside the first. yMMSL works this out automatically +from how components are wired together with conduits, so in most models you never have +to declare a timeline explicitly. A component that nobody calls sits on the outermost, root timeline, written ``:``. Every level of nesting adds one more name, giving each timeline in the model an addressable @@ -250,9 +250,14 @@ sender: Conduit filters ^^^^^^^^^^^^^^^ -As explained in :ref:`Timelines` above, a conduit that connects a port on one timeline -to a port on another must bridge the difference in how often either side sends or -receives, using a filter: +A conduit filter lets a conduit skip past an in-between timeline(s): a component can send +directly to (or receive directly from) another one further down or up the nesting, +without the message being relayed through whatever sits between them. Because the +deeper side of such a conduit still gets called multiple times for every step the +shallower side takes, skipping down needs a filter that produces enough messages to +match (``repeat``/``pad``), and skipping back up needs one that picks a single message +out of the many produced (``last``). A conduit between directly connected timelines (a +component and its own caller) doesn't skip anything, so it can't take a filter at all. - ``repeat`` and ``pad`` cross from an outer timeline into a nested one: a single message sent on the outer timeline (e.g. an initial state) is repeated, or followed @@ -269,12 +274,11 @@ Filters are written in front of the receiver and may be combined: macro.init_out: repeat micro.init_in micro.state_out: last macro.final_in -A filter is only needed where a conduit *skips* a level of nesting. Extending the -macro-meso-micro example from :ref:`Timelines`: ``macro`` lives on the root timeline -``:``, ``meso`` on ``:macro``, and ``micro`` on ``:macro:meso``. The conduits that call -``meso`` and that call ``micro`` from ``meso`` don't need a filter, since each one only -crosses a single level of nesting that it also establishes. A conduit that goes directly -from ``macro`` to ``micro``, bypassing ``meso``, skips a level, and does need one: +Extending the macro-meso-micro example from :ref:`Timelines`: ``macro`` lives on the +root timeline ``:``, ``meso`` on ``:macro``, and ``micro`` on ``:macro:meso``. The +conduits that call ``meso`` and that call ``micro`` from ``meso`` are each a direct +connection, so neither needs a filter. A conduit that goes directly from ``macro`` to +``micro``, bypassing ``meso``, does: .. literalinclude:: conduit_filters_bypass.ymmsl :caption: ``docs/conduit_filters_bypass.ymmsl`` @@ -285,28 +289,7 @@ from ``macro`` to ``micro``, bypassing ``meso``, skips a level, and does need on bypassing meso, labeled "repeat" and "last". The same model, visualized with `ymmsl2svg - `_. Besides the normal call/release - conduits, ``macro`` sends directly into ``micro``'s timeline with ``repeat`` (since - ``macro`` only sends once per ``meso`` step, but ``micro`` runs several times per - ``meso`` step), and ``micro`` reports back to ``macro`` with ``last`` (since only - the final value of several ``micro`` runs is meaningful once you're back up at - ``macro``'s level). - -This is represented in Python by the ``filters`` attribute of -:class:`.ymmsl.v0_2.Conduit`, a list of :class:`.ymmsl.v0_2.ConduitFilter` values: - -.. code-block:: python - :caption: Conduit filters in python code - - from pathlib import Path - import ymmsl - - config = ymmsl.load(Path('conduit_filters_bypass.ymmsl')) - model = config.models['macro_meso_micro_filtered'] - - conduits = model.conduits - print(conduits[1]) # output: Conduit(macro.bypass_out -> repeat -> micro.bypass_in) - print(conduits[1].filters) # output: [] + `_. Nesting models From f5b29423a4d575074e5a4ec007c46bad2025f219 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 31 Aug 2026 13:07:53 +0200 Subject: [PATCH 05/12] rewrite timelines --- docs/describing_models.rst | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 0537fe2..7ebe78d 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -141,18 +141,16 @@ driving it, and a meso model may sit somewhere in between the two. yMMSL capture idea of "running at a different pace" as a *timeline*. Wiring a component's ``o_i``/``s`` ports to another component's ``f_init``/``o_f`` ports puts that other component, and anything it in turn drives, on a timeline nested inside the first. yMMSL works this out automatically -from how components are wired together with conduits, so in most models you never have -to declare a timeline explicitly. +from how components are wired together with conduits. A component that nobody calls sits on the outermost, root timeline, written ``:``. Every level of nesting adds one more name, giving each timeline in the model an addressable -path, a bit like a folder structure. +path, a bit like a folder structure. Take a macro model that calls a meso model in a loop, and where that meso model in turn calls a micro model in its own loop. This produces three levels of timelines: the root -timeline for ``macro``, a timeline nested inside it for ``meso`` (which runs once for -every step ``macro`` takes), and a timeline nested inside *that* for ``micro`` (which runs -once for every step ``meso`` takes): +timeline for ``macro``, a timeline nested inside it for ``meso``, and a timeline nested +inside *that* for ``micro``: .. literalinclude:: timelines_macro_meso_micro.ymmsl :caption: ``docs/timelines_macro_meso_micro.ymmsl`` @@ -186,12 +184,6 @@ the ports that belong together under a ``timeline :`` heading, one per loo side by side beneath it, each with its own pair of ports, one leading to ``micro1`` and the other to ``micro2``. -This is the pattern to reach for whenever a single component acts as the driver for -more than one independently-paced loop, for example a component that advances a coarse -grid with one fast inner solver and a separate, differently-paced inner solver for a -refined region, or a driver that runs an ensemble of replicas at one rate while also -maintaining some shared bookkeeping process at another. - Conduits ```````` From 8f0ce696b730019784e19e9370cba5a938c8a177 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Tue, 1 Sep 2026 16:05:29 +0200 Subject: [PATCH 06/12] include comments Timelines --- docs/describing_models.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 7ebe78d..e44278e 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -157,25 +157,28 @@ inside *that* for ``micro``: :language: yaml .. figure:: timelines_macro_meso_micro.svg + :align: center :alt: macro connects to meso through F_INIT/O_F and O_I/S ports, and meso connects to micro the same way, producing three nested timelines. The same model, visualized with `ymmsl2svg - `_. Nesting in the figure mirrors nesting in - time: ``meso``'s box sits inside ``macro``'s, and ``micro``'s sits inside ``meso``'s. + `_. The order of the boxes in the figure, + from top to bottom, mirrors the nesting in time: ``macro`` first, then ``meso`` + below it, then ``micro`` below ``meso``. A single component can also be connected to more than one timeline at once, for example when it drives two other components that run at different rates. ``macro`` calling ``micro1`` in one loop and ``micro2`` in a separate loop puts ``micro1`` and ``micro2`` on -two independent timelines nested inside ``macro``'s own, rather than on a shared one. -Since there's more than one loop to keep apart, each one needs an explicit name: group -the ports that belong together under a ``timeline :`` heading, one per loop: +two independent sub-timelines of ``macro``. The following example shows how you can +use ``timeline :`` to indicate that the ports connecting to ``micro1`` belong to +a different subtimeline than the ports connecting to ``micro2``: .. literalinclude:: timelines_two_subtimelines.ymmsl :caption: ``docs/timelines_two_subtimelines.ymmsl`` :language: yaml .. figure:: timelines_two_subtimelines.svg + :align: center :alt: macro has two separate pairs of O_I/S ports, one connecting down to micro1 and one connecting down to micro2, side by side. From a267190ac86a32dff31eaf62dbcf6e7e723072b2 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Tue, 1 Sep 2026 16:27:35 +0200 Subject: [PATCH 07/12] rewrite conduit filters --- docs/describing_models.rst | 70 ++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 29 deletions(-) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index e44278e..cb7ebcd 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -245,47 +245,59 @@ sender: Conduit filters ^^^^^^^^^^^^^^^ -A conduit filter lets a conduit skip past an in-between timeline(s): a component can send -directly to (or receive directly from) another one further down or up the nesting, -without the message being relayed through whatever sits between them. Because the -deeper side of such a conduit still gets called multiple times for every step the -shallower side takes, skipping down needs a filter that produces enough messages to -match (``repeat``/``pad``), and skipping back up needs one that picks a single message -out of the many produced (``last``). A conduit between directly connected timelines (a -component and its own caller) doesn't skip anything, so it can't take a filter at all. - -- ``repeat`` and ``pad`` cross from an outer timeline into a nested one: a single - message sent on the outer timeline (e.g. an initial state) is repeated, or followed - by empty messages, to match every receive on the nested timeline. -- ``last`` crosses from a nested timeline back out to its parent: of the many messages - sent on the nested timeline, only the last one (e.g. a final result) is passed on. - -Filters are written in front of the receiver and may be combined: - -.. code-block:: yaml - :caption: Specifying conduit filters in yMMSL - - conduits: - macro.init_out: repeat micro.init_in - micro.state_out: last macro.final_in - -Extending the macro-meso-micro example from :ref:`Timelines`: ``macro`` lives on the -root timeline ``:``, ``meso`` on ``:macro``, and ``micro`` on ``:macro:meso``. The -conduits that call ``meso`` and that call ``micro`` from ``meso`` are each a direct -connection, so neither needs a filter. A conduit that goes directly from ``macro`` to -``micro``, bypassing ``meso``, does: +A conduit connects two components that call each other directly, for example ``macro`` +and ``meso``, or ``meso`` and ``micro``. ``macro`` and ``micro`` are not directly +connected in this sense: ``meso`` sits between them. Connecting ``macro`` and ``micro`` +directly, bypassing ``meso``, means their pace no longer matches: ``micro`` is still +called many times for every step ``macro`` takes, and still produces a message on +every one of those calls, even though there is no longer a ``meso`` in between to +absorb the difference. A conduit filter reconciles that mismatch. + +Extending the macro-meso-micro example from :ref:`Timelines` with a conduit that +bypasses ``meso`` to connect ``macro`` and ``micro`` directly shows both filters in +use: .. literalinclude:: conduit_filters_bypass.ymmsl :caption: ``docs/conduit_filters_bypass.ymmsl`` :language: yaml .. figure:: conduit_filters_bypass.svg + :align: center :alt: macro and micro have an extra pair of ports directly connecting them, bypassing meso, labeled "repeat" and "last". The same model, visualized with `ymmsl2svg `_. +``macro`` produces the ``bypass_out`` message once, but ``micro`` is called many times +for every step of ``macro`` and needs the message on each of those calls. The conduit +from ``macro.bypass_out`` to ``micro.bypass_in`` uses a ``repeat`` filter for this: the +single message ``macro`` sends is resent to ``micro`` every time it runs, without +``meso`` having to relay it. + +The reverse happens on the way back: ``micro`` produces a ``bypass_out`` message on +every one of its many runs, but ``macro`` still expects only one message per call. The +conduit from ``micro.bypass_out`` to ``macro.bypass_in`` uses a ``last`` filter to +reduce those many messages down to the single most recently produced one. + +- ``repeat`` and ``pad`` go from the shallower side to the deeper one: a single message + is repeated, or followed by empty messages, to match every time the deeper side + receives. +- ``last`` goes from the deeper side back to the shallower one: of the many messages + produced, only the last one is passed on. + +A conduit between two components that call each other directly doesn't skip anything, +so it can't take a filter at all. + +Filters are written in front of the receiver and may be combined: + +.. code-block:: yaml + :caption: Specifying conduit filters in yMMSL + + conduits: + macro.init_out: repeat micro.init_in + micro.state_out: last macro.final_in + Nesting models `````````````` From a56a97850d69fede24ed1e78977391f12fbc1304 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Tue, 22 Sep 2026 11:03:50 +0200 Subject: [PATCH 08/12] Add matching timelines to the timelines documentation --- docs/describing_models.rst | 65 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index cb7ebcd..54afcc2 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -187,6 +187,71 @@ a different subtimeline than the ports connecting to ``micro2``: side by side beneath it, each with its own pair of ports, one leading to ``micro1`` and the other to ``micro2``. +Matching timelines +^^^^^^^^^^^^^^^^^^^ + +The timeline hierarchy above is worked out automatically from how ``f_init``/``o_f`` +and ``o_i``/``s`` ports are wired together, and a conduit can only connect ports whose +timelines match. Sometimes, though, two components are expected to produce matching +time points without one being nested inside the other's timeline this way, for example +two components that call each other directly and are expected to stay in lock-step, or +a component that adapts its output to another component's timeline, as time bridges do. +``matching_timelines`` lets you declare such timelines equivalent, so that a conduit can +still connect ports on them directly: + +.. code-block:: yaml + :caption: Declaring matching timelines + + components: + left: + ports: + o_i: out + s: in + description: Left side of the domain + right: + ports: + o_i: out + s: in + description: Right side of the domain + + matching_timelines: + left: right + + conduits: + left.out: right.in + right.out: left.in + +``left`` and ``right`` call each other directly rather than through a shared driver, so +their O_I and S ports live on their own default timelines, ``:left`` and ``:right``, +named after the component as usual. A conduit between these ports would therefore not be +allowed. The entry under ``matching_timelines`` declares ``left`` and ``right``'s +timelines equivalent, so that the conduits connecting them are valid after all. + +On the Python side, ``matching_timelines`` is a list of +:class:`.ymmsl.v0_2.MatchingTimelines` objects, each representing a set of equivalent +timelines, with a ``head`` attribute and a ``matches`` attribute holding the full set, +including the head. + +A head can have more than one match, for example if ``left`` is expected to stay in +lock-step with both ``right`` and ``top``. The matches can then be written as a +whitespace-separated string: + +.. code-block:: yaml + :caption: A head with multiple matches + + matching_timelines: + left: right top + +or, equivalently, as a YAML list: + +.. code-block:: yaml + :caption: The same, as a YAML list + + matching_timelines: + left: + - right + - top + Conduits ```````` From 0ff0cb10b2207112e73c161900ba4e87c18c1a79 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 28 Sep 2026 13:27:57 +0200 Subject: [PATCH 09/12] Update first part Timelines section --- docs/describing_models.rst | 57 ++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 54afcc2..8379dfb 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -138,19 +138,41 @@ Timelines Different components of a coupled simulation typically run at their own pace: a fast, detailed micro model may take many small steps for every single step of the macro model driving it, and a meso model may sit somewhere in between the two. yMMSL captures this -idea of "running at a different pace" as a *timeline*. Wiring a component's ``o_i``/``s`` -ports to another component's ``f_init``/``o_f`` ports puts that other component, and anything -it in turn drives, on a timeline nested inside the first. yMMSL works this out automatically -from how components are wired together with conduits. - -A component that nobody calls sits on the outermost, root timeline, written ``:``. Every -level of nesting adds one more name, giving each timeline in the model an addressable -path, a bit like a folder structure. +idea of "running at a different pace" as a *timeline*. + +Timelines are determined separately for each model under ``models``, and are named +relative to that model. Each component has two timelines associated with it: + +- Its *parent timeline* is the timeline of whatever calls it, i.e. the timeline on which + the messages to its ``f_init`` ports are sent and the messages from its ``o_f`` ports + are received. For a component that isn't called by any other component in the model, + the parent timeline is empty. +- Its *component timeline* is the timeline it runs on itself. Its name is the name of + the parent timeline followed by the name of the component, joined with a colon. A + component that isn't called by anything therefore gets a timeline named after itself. + +A component is called by another one through a call-and-release coupling, in which the +caller's ``o_i`` port sends to the callee's ``f_init`` port and the callee's ``o_f`` +port sends back to the caller's ``s`` port. The caller's component timeline then becomes +the callee's parent timeline, so the callee's component timeline is nested inside the +caller's. Every level of nesting adds one more name, giving each timeline in the model +an addressable path, a bit like a folder structure. A dispatch coupling, in which one +component's ``o_f`` port sends to the next component's ``f_init`` port, does not add a +level: the second component gets the same parent timeline as the first, so the two end +up side by side. + +Components and their ports are related to timelines in slightly different ways. A +component's ``o_i`` and ``s`` ports send and receive during its run, so they are on its +component timeline. Its ``f_init`` and ``o_f`` ports sit at the beginning and the end of +the component timeline, where the component hands over to and from its caller, so the +messages they receive and send belong to the parent timeline. + +To make a valid conduit, you should connect two ports whose messages live on the same +timeline. :ref:`Conduit filters` and :ref:`Matching timelines` relax this rule +in specific cases. Take a macro model that calls a meso model in a loop, and where that meso model in turn -calls a micro model in its own loop. This produces three levels of timelines: the root -timeline for ``macro``, a timeline nested inside it for ``meso``, and a timeline nested -inside *that* for ``micro``: +calls a micro model in its own loop: .. literalinclude:: timelines_macro_meso_micro.ymmsl :caption: ``docs/timelines_macro_meso_micro.ymmsl`` @@ -166,6 +188,19 @@ inside *that* for ``micro``: from top to bottom, mirrors the nesting in time: ``macro`` first, then ``meso`` below it, then ``micro`` below ``meso``. +``macro`` isn't called by anything, so its parent timeline is empty and its +component timeline is ``macro``. ``macro`` calls ``meso``, so ``meso``'s parent +timeline is ``macro`` and its component timeline is ``macro:meso``. Likewise, +``micro`` has parent timeline ``macro:meso`` and component timeline +``macro:meso:micro``. + +The conduit from ``macro.bc_out`` to ``meso.init_in`` isvalid because ``bc_out`` is an +``o_i`` port on ``macro``'s component timeline ``macro``, and the messages received by +the ``f_init`` port ``init_in`` are on ``meso``'s parent timeline, which is also +``macro``. The same reasoning applies to the other three conduits. +None of these timelines are written in the yMMSL file itself: yMMSL works them out +automatically from how the components are wired together with conduits. + A single component can also be connected to more than one timeline at once, for example when it drives two other components that run at different rates. ``macro`` calling ``micro1`` in one loop and ``micro2`` in a separate loop puts ``micro1`` and ``micro2`` on From 0b6366c51a5da51e8002a4416523350ad0ceae20 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 28 Sep 2026 15:02:33 +0200 Subject: [PATCH 10/12] Update the Timelines documentation + Matching Timelines --- docs/describing_models.rst | 138 +++++++++++++++++++++++++++------- docs/timelines_dispatch.svg | 1 + docs/timelines_dispatch.ymmsl | 26 +++++++ 3 files changed, 137 insertions(+), 28 deletions(-) create mode 100644 docs/timelines_dispatch.svg create mode 100644 docs/timelines_dispatch.ymmsl diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 8379dfb..0f503b2 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -198,6 +198,31 @@ The conduit from ``macro.bc_out`` to ``meso.init_in`` isvalid because ``bc_out`` ``o_i`` port on ``macro``'s component timeline ``macro``, and the messages received by the ``f_init`` port ``init_in`` are on ``meso``'s parent timeline, which is also ``macro``. The same reasoning applies to the other three conduits. + +In a dispatch coupling, by contrast, no extra level is added. Here, ``macro`` calls +``solver``, which hands its result over to ``analysis``, which in turn returns to +``macro``: + +.. literalinclude:: timelines_dispatch.ymmsl + :caption: ``docs/timelines_dispatch.ymmsl`` + :language: yaml + +.. figure:: timelines_dispatch.svg + :align: center + :alt: macro connects through its O_I port to solver's F_INIT port, solver's O_F port + connects to analysis's F_INIT port, and analysis's O_F port connects back to + macro's S port. solver and analysis are drawn side by side below macro. + + The same model, visualized with `ymmsl2svg + `_. ``solver`` and ``analysis`` are drawn + side by side below ``macro``, since they share the same parent timeline. + +``solver`` is called by ``macro``, so its parent timeline is ``macro``. ``analysis`` +receives its ``f_init`` message from ``solver``'s ``o_f`` port, and those messages are on +``solver``'s parent timeline, so ``analysis`` gets that same parent timeline ``macro``. +The two components therefore end up side by side, on component timelines +``macro:solver`` and ``macro:analysis``. + None of these timelines are written in the yMMSL file itself: yMMSL works them out automatically from how the components are wired together with conduits. @@ -222,17 +247,21 @@ a different subtimeline than the ports connecting to ``micro2``: side by side beneath it, each with its own pair of ports, one leading to ``micro1`` and the other to ``micro2``. +A named sub-timeline is written as the component name, a period, and the annotation. +Here, ``macro``'s first pair of O_I and S ports is on timeline ``macro.tl1`` and its +second pair on ``macro.tl2``, which puts ``micro1`` on ``macro.tl1:micro1`` and +``micro2`` on ``macro.tl2:micro2``. A timeline annotation must be a single name without +periods, so ``timeline tl1:`` is fine but ``timeline sub.tl1:`` is not. + Matching timelines ^^^^^^^^^^^^^^^^^^^ -The timeline hierarchy above is worked out automatically from how ``f_init``/``o_f`` -and ``o_i``/``s`` ports are wired together, and a conduit can only connect ports whose -timelines match. Sometimes, though, two components are expected to produce matching -time points without one being nested inside the other's timeline this way, for example -two components that call each other directly and are expected to stay in lock-step, or -a component that adapts its output to another component's timeline, as time bridges do. -``matching_timelines`` lets you declare such timelines equivalent, so that a conduit can -still connect ports on them directly: +The timeline hierarchy above is worked out automatically from how the ``f_init``/``o_f`` +and ``o_i``/``s`` ports are wired together, and a conduit is only valid if the messages +on both of its ends are on the same timeline. Sometimes, though, two components step +through the same time points without one being nested inside the other's timeline. +``matching_timelines`` lets you declare the timelines of such components equivalent, so +that a conduit can still connect their ports directly: .. code-block:: yaml :caption: Declaring matching timelines @@ -250,42 +279,95 @@ still connect ports on them directly: description: Right side of the domain matching_timelines: - left: right + main: left right conduits: left.out: right.in right.out: left.in -``left`` and ``right`` call each other directly rather than through a shared driver, so -their O_I and S ports live on their own default timelines, ``:left`` and ``:right``, -named after the component as usual. A conduit between these ports would therefore not be -allowed. The entry under ``matching_timelines`` declares ``left`` and ``right``'s -timelines equivalent, so that the conduits connecting them are valid after all. +``left`` and ``right`` each run on their own component timeline, ``left`` and +``right``, and their ``o_i`` and ``s`` ports are on those timelines. While running, they +exchange messages with each other directly through these ports, in an interact coupling. +Because the two timelines are different, a conduit between these ports would not be +allowed. The components do however step through the same time points, so their timelines +are equivalent even though they are not the same. The entry under ``matching_timelines`` +declares exactly that, so that the conduits connecting them are valid after all. + +Each entry has a *head*, written on the left of the colon, and the timelines that match +it, written on the right. The head doesn't have to be one of the component timelines: +here it is a separate name, ``main``, so that neither ``left`` nor ``right`` is singled +out as the one the other follows. If one of the timelines does lead, as in the time +bridge example below, you can use that timeline as the head instead. + +Matching timelines are also what makes a time bridge work. A time bridge lets two +components ``a`` and ``b`` that take different time steps exchange messages while they +run. The bridge sits in between and converts the messages from one side to the other, +so that each component receives messages with time stamps it can use. To do so, the +bridge runs on timelines of its own: a named sub-timeline for each side, on which it +follows the time points of the component on that side: + +.. code-block:: yaml + :caption: Two components on different timelines, connected through a time bridge + + components: + a: + ports: + o_i: state_out + s: state_in + description: A model with its own time steps + b: + ports: + o_i: state_out + s: state_in + description: A model with different time steps + bridge: + ports: + timeline a_side: + o_i: a_out + s: a_in + timeline b_side: + o_i: b_out + s: b_in + description: Time bridge that converts messages between a and b + + matching_timelines: + a: bridge.a_side + b: bridge.b_side + + conduits: + a.state_out: bridge.a_in + bridge.a_out: a.state_in + b.state_out: bridge.b_in + bridge.b_out: b.state_in + +Like any component, the bridge runs on its own timelines, ``bridge.a_side`` and +``bridge.b_side``, which differ from those of ``a`` and ``b``. Without +``matching_timelines``, none of the conduits in this example would therefore be valid. +But each side of the bridge does step through the same time points as the component on +that side, and the two entries under ``matching_timelines`` declare exactly that: +``bridge.a_side`` is equivalent to ``a``, and ``bridge.b_side`` to ``b``. + +Deeper timelines are matched by writing out their full path, e.g. +``macro1:micro1: macro2:micro2``. Matching timelines are taken into account after +applying any conduit filters, so a conduit with a ``repeat`` or ``last`` filter can also +connect to a timeline that matches the one it is filtered to. On the Python side, ``matching_timelines`` is a list of :class:`.ymmsl.v0_2.MatchingTimelines` objects, each representing a set of equivalent timelines, with a ``head`` attribute and a ``matches`` attribute holding the full set, including the head. -A head can have more than one match, for example if ``left`` is expected to stay in -lock-step with both ``right`` and ``top``. The matches can then be written as a -whitespace-separated string: +A head can have more than one match, as ``main`` does in the first example. The matches +can be written as a whitespace-separated string, as above, or, equivalently, as a YAML +list: .. code-block:: yaml - :caption: A head with multiple matches + :caption: The same matching timelines, as a YAML list matching_timelines: - left: right top - -or, equivalently, as a YAML list: - -.. code-block:: yaml - :caption: The same, as a YAML list - - matching_timelines: - left: + main: + - left - right - - top Conduits diff --git a/docs/timelines_dispatch.svg b/docs/timelines_dispatch.svg new file mode 100644 index 0000000..edadc4b --- /dev/null +++ b/docs/timelines_dispatch.svg @@ -0,0 +1 @@ +Model: dispatch_modelmacrostate_outstate_insolverinit_infinal_outanalysisinit_infinal_out diff --git a/docs/timelines_dispatch.ymmsl b/docs/timelines_dispatch.ymmsl new file mode 100644 index 0000000..7f51dcd --- /dev/null +++ b/docs/timelines_dispatch.ymmsl @@ -0,0 +1,26 @@ +ymmsl_version: v0.2 + +description: A dispatch coupling inside a call-and-release loop + +models: + dispatch_model: + components: + macro: + ports: + o_i: state_out + s: state_in + description: Macro model + solver: + ports: + f_init: init_in + o_f: final_out + description: Computes a new state + analysis: + ports: + f_init: init_in + o_f: final_out + description: Analyses the new state + conduits: + macro.state_out: solver.init_in + solver.final_out: analysis.init_in + analysis.final_out: macro.state_in From 228061792b53d3a48f35119834fa07a6557434d5 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Mon, 28 Sep 2026 15:40:22 +0200 Subject: [PATCH 11/12] Add multiple filters example --- docs/conduit_filters_bypass.svg | 2 +- docs/conduit_filters_bypass.ymmsl | 23 ++++++++++++----- docs/describing_models.rst | 42 ++++++++++++++++--------------- 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/docs/conduit_filters_bypass.svg b/docs/conduit_filters_bypass.svg index aa13003..d72f01b 100644 --- a/docs/conduit_filters_bypass.svg +++ b/docs/conduit_filters_bypass.svg @@ -1 +1 @@ -Model: macro_meso_micro_filteredmacrobypass_outbc_outbc_inbypass_inmesoinit_infinal_outbc_outbc_inmicroinit_inbypass_infinal_outbypass_out +Model: macro_meso_micro_pico_filteredmacroto_picoto_microbc_outbc_infrom_microfrom_picomesoinit_infinal_outbc_outbc_inmicroinit_inbypass_inbypass_outfinal_outbc_outbc_inpicoinit_inbypass_inbypass_outfinal_out diff --git a/docs/conduit_filters_bypass.ymmsl b/docs/conduit_filters_bypass.ymmsl index 7275f25..4e46bc6 100644 --- a/docs/conduit_filters_bypass.ymmsl +++ b/docs/conduit_filters_bypass.ymmsl @@ -1,14 +1,14 @@ ymmsl_version: v0.2 -description: macro-meso-micro with a bypass conduit needing filters +description: macro-meso-micro-pico with bypass conduits needing filters models: - macro_meso_micro_filtered: + macro_meso_micro_pico_filtered: components: macro: ports: - o_i: bc_out bypass_out - s: bc_in bypass_in + o_i: bc_out to_micro to_pico + s: bc_in from_micro from_pico description: '' meso: ports: @@ -18,14 +18,25 @@ models: o_f: final_out description: '' micro: + ports: + f_init: init_in bypass_in + o_i: bc_out + s: bc_in + o_f: bypass_out final_out + description: '' + pico: ports: f_init: init_in bypass_in o_f: bypass_out final_out description: '' conduits: macro.bc_out: meso.init_in - macro.bypass_out: repeat micro.bypass_in meso.final_out: macro.bc_in meso.bc_out: micro.init_in micro.final_out: meso.bc_in - micro.bypass_out: last macro.bypass_in + micro.bc_out: pico.init_in + pico.final_out: micro.bc_in + macro.to_micro: repeat micro.bypass_in + micro.bypass_out: last macro.from_micro + macro.to_pico: repeat repeat pico.bypass_in + pico.bypass_out: last last macro.from_pico diff --git a/docs/describing_models.rst b/docs/describing_models.rst index 0f503b2..ea79411 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -435,9 +435,9 @@ called many times for every step ``macro`` takes, and still produces a message o every one of those calls, even though there is no longer a ``meso`` in between to absorb the difference. A conduit filter reconciles that mismatch. -Extending the macro-meso-micro example from :ref:`Timelines` with a conduit that -bypasses ``meso`` to connect ``macro`` and ``micro`` directly shows both filters in -use: +Extending the macro-meso-micro example from :ref:`Timelines` with a fourth level, +``pico``, called by ``micro``, and adding conduits that bypass the levels in between +shows both kinds of filters in use, including combinations of them: .. literalinclude:: conduit_filters_bypass.ymmsl :caption: ``docs/conduit_filters_bypass.ymmsl`` @@ -445,21 +445,22 @@ use: .. figure:: conduit_filters_bypass.svg :align: center - :alt: macro and micro have an extra pair of ports directly connecting them, - bypassing meso, labeled "repeat" and "last". + :alt: macro, meso, micro and pico are nested inside each other. Extra pairs of + conduits connect macro directly to micro, bypassing meso, and macro directly + to pico, bypassing meso and micro. The same model, visualized with `ymmsl2svg `_. -``macro`` produces the ``bypass_out`` message once, but ``micro`` is called many times +``macro`` produces the ``to_micro`` message once, but ``micro`` is called many times for every step of ``macro`` and needs the message on each of those calls. The conduit -from ``macro.bypass_out`` to ``micro.bypass_in`` uses a ``repeat`` filter for this: the +from ``macro.to_micro`` to ``micro.bypass_in`` uses a ``repeat`` filter for this: the single message ``macro`` sends is resent to ``micro`` every time it runs, without ``meso`` having to relay it. The reverse happens on the way back: ``micro`` produces a ``bypass_out`` message on -every one of its many runs, but ``macro`` still expects only one message per call. The -conduit from ``micro.bypass_out`` to ``macro.bypass_in`` uses a ``last`` filter to +every one of its many runs, but ``macro`` still expects only one message per step. The +conduit from ``micro.bypass_out`` to ``macro.from_micro`` uses a ``last`` filter to reduce those many messages down to the single most recently produced one. - ``repeat`` and ``pad`` go from the shallower side to the deeper one: a single message @@ -468,17 +469,18 @@ reduce those many messages down to the single most recently produced one. - ``last`` goes from the deeper side back to the shallower one: of the many messages produced, only the last one is passed on. -A conduit between two components that call each other directly doesn't skip anything, -so it can't take a filter at all. - -Filters are written in front of the receiver and may be combined: - -.. code-block:: yaml - :caption: Specifying conduit filters in yMMSL - - conduits: - macro.init_out: repeat micro.init_in - micro.state_out: last macro.final_in +Filters are written in front of the receiver, and each filter bridges exactly one level +of nesting. To skip more than one level, you combine multiple filters on the same +conduit. The conduits between ``macro`` and ``pico`` skip both ``meso`` and ``micro``, +so each of them needs two filters. On the way down, ``repeat repeat`` repeats +``macro``'s single message for every call of ``micro`` inside ``meso``, and then again +for every call of ``pico`` inside ``micro``. On the way back up, ``last last`` first +reduces ``pico``'s many messages to the last one per call of ``micro``, and then those +to the last one per call of ``meso``, so that ``macro`` again receives a single message. + +The number of filters has to equal the number of levels skipped: with only a single +``repeat`` on the conduit to ``pico``, the timelines on its two ends would not match and +the model would be rejected. Nesting models From 2810fb3aa2b4be55e28da03302e2042576b89991 Mon Sep 17 00:00:00 2001 From: Iris van der Werf Date: Tue, 29 Sep 2026 13:38:24 +0200 Subject: [PATCH 12/12] include comments --- docs/describing_models.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/describing_models.rst b/docs/describing_models.rst index ea79411..56ce615 100644 --- a/docs/describing_models.rst +++ b/docs/describing_models.rst @@ -194,7 +194,7 @@ timeline is ``macro`` and its component timeline is ``macro:meso``. Likewise, ``micro`` has parent timeline ``macro:meso`` and component timeline ``macro:meso:micro``. -The conduit from ``macro.bc_out`` to ``meso.init_in`` isvalid because ``bc_out`` is an +The conduit from ``macro.bc_out`` to ``meso.init_in`` is valid because ``bc_out`` is an ``o_i`` port on ``macro``'s component timeline ``macro``, and the messages received by the ``f_init`` port ``init_in`` are on ``meso``'s parent timeline, which is also ``macro``. The same reasoning applies to the other three conduits. @@ -347,8 +347,10 @@ But each side of the bridge does step through the same time points as the compon that side, and the two entries under ``matching_timelines`` declare exactly that: ``bridge.a_side`` is equivalent to ``a``, and ``bridge.b_side`` to ``b``. -Deeper timelines are matched by writing out their full path, e.g. -``macro1:micro1: macro2:micro2``. Matching timelines are taken into account after +Deeper timelines are matched by writing out their full path, relative to the model that +contains the ``matching_timelines`` declaration. In the two-subtimelines example above, +for instance, ``micro1`` and ``micro2`` could be declared equivalent from within +``two_subtimelines_model`` with ``macro.tl1:micro1: macro.tl2:micro2``. Matching timelines are taken into account after applying any conduit filters, so a conduit with a ``repeat`` or ``last`` filter can also connect to a timeline that matches the one it is filtered to.