Final task where final cycle point may not be congruent with cycle period

Let’s say I want to run a simulation from 2000-01-01 to an arbitrary date, with a period of several days (here three days). After the simulation is finished, I want to post-process the results.

The flow may look like:

[scheduling]
    initial cycle point = 20000101
    final cycle point = {{end_date}}
    [[graph]]
        P3D = compute_period[-P3D] => compute_period
        R1/$ = compute_period => post-proc

My problem: If the difference between the final point and the initial point is not a multiple of the cycle period (3D), then R1/$ will launch a compute_period task without any dependencies.

How can I have the task run at the final cycle, regardless of the period ?


(Using Cylc 8.2.4 with python3.9 on ubuntu 22)

Hi there,

You’ve spotted something that’s a little tricky to achieve with Cylc.

The simplest solution is to use final cycle point constraints to ensure that the final cycle point is always set to a value that works for your workflow. This avoids the issue of the “ragged” end cycle.

If the “ragged” end cycle is a requirement of the workflow, there is a sneaky solution that is suitable if the task is trivial, use the shutdown handler. Not generally reccomended, but sometimes used to generate workflow reports.

Beyond that there is a Jinja2 solution but it’s a little more advanced. Let me know if you would be interested in that, if so I’ll write it up tomorrow.

Cheers,
Oliver

The simplest solution is to use final cycle point constraints to ensure that the final cycle point is always set to a value that works for your workflow.

I’m not sure I understand how that works (and the documentation doesn’t give an example). If the end date is several months/years in the future, even I don’t know what a valid end date will be.

Beyond that there is a Jinja2 solution but it’s a little more advanced. Let me know if you would be interested in that, if so I’ll write it up tomorrow.

Sure, thanks !

I think by “valid” @oliver.sanders means the inter-cycle trigger has to be valid for cycle interval.

An example using integer cycles for simplicity:

[scheduling]
   cycling mode = integer
   initial cycle point = 1
   final cycle point = 22
   [[graph]]
      P10 = "foo[-P10] => foo"  # 1 => 11 => 21
      R1/$/P0 = "foo[-P1] => foo"  # 21 => 22 !!
[runtime]
   [[foo]]

gr3

That final inter-cycle trigger is what tells the final foo which previous task to wait on. If your final cycle point is variable, you’ll have to compute the final trigger offset using Jinja2 (I presume that’s what Oliver will suggest).

Another suggestion - leave your final point on the P3D sequence. At runtime the task job can check if it is at the final point or not (compare $CYLC_TASK_CYCLE_POINT with $CYLC_TASK_FINAL_CYCLE_POINT) and if it is, just use the “correct” value instead for any cycle point based computations, filenames, or whatever. (Of course the Cylc job log directory path will still reflect the automatic “wrong” value).

(As an aside, a future Cylc release will allow a distinct shutdown graph that runs after the main cycling graph finishes, regardless of dependencies - that work was done a while ago but it remains trumped by higher priorities for now).

Hi,

There are examples for the initial cycle point constraints in the docs, but not the final cycle point constraints, but the syntax is the same. The constraints allow you to set rules which the initial/final cycle points will be validated against e.g:

# FCP must be on the first of Jan at 00:00
final cycle point constraints = 0101T00

# FCP must be the 1st, 4th or 7th of the month
final cycle point constraints = 01, 04, 07

Here’s the Jinja2 solution:

flow.cylc

#!Jinja2

{% set start_date = '2000-01-01T00Z' %}
{% set end_date = '2000-01-08T00Z' %}
{% set recurrence = 'P3D' %}

{% from "offset" import get_fcp_offset %}
{% set fcp_offset = get_fcp_offset(start_date, end_date, recurrence) %}

[scheduler]
    allow implicit tasks = True

[scheduling]
    initial cycle point = {{ start_date }}
    final cycle point = {{end_date}}
    [[graph]]
        {{ recurrence }} = compute_period[-{{ recurrence }}] => compute_period
        R1/$ = compute_period[-{{ fcp_offset }}] => post-proc

lib/python/offset.py

from metomi.isodatetime.datetimeoper import DateTimeOperator


def get_fcp_offset(start_date, end_date, recurrence):
    """Return the offset between the final cycle of the recurrence and the
    final cycle point.

    Args:
        start_date:
            The start point of the recurrence, e.g. the workflow's
            initial cycle point, in ISO8601 format.
        end_date:
            The end point of the recurrence, e.g. the workflow's
            final cycle point, in ISO8601 format.
        recurrence:
            The cycling recurrence in ISO8601 format, e.g. P3D

    Returns:
        An ISO8601 offset string.
            
    """
    oper = DateTimeOperator()
    start_date = oper.time_point_parser.parse(start_date)
    end_date = oper.time_point_parser.parse(end_date)
    cycle_generator = iter(
        oper.recurrence_parser.parse(
            f'R/{start_date}/{recurrence}'
        )
    )
    date = last_date = start_date
    while date <= end_date:
        last_date = date
        date = next(cycle_generator)

    return end_date - last_date
2 Likes

This works wonders, many thanks !

ps: It would be wonderful if metomi.isodatetime had a documentation outside of the very sparse examples on the github README :wink:

ps 2 for future readers: you can pass a calendar_mode argument to DateTimeOperator to change the calendar used