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 1/8] 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 2/8] 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 3/8] 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 4/8] 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 5/8] 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 6/8] 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 7/8] 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 8/8] 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 ````````