Skip to content

Isolated start-up and shut-down graphs. #5090

New issue

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

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

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changes.d/5090.feat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Distinct initial and final graphs, separated from the main cycling graph,
to make it easier to configure special behaviour at startup and shutdown.
59 changes: 46 additions & 13 deletions cylc/flow/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@
get_sequence, get_sequence_cls, init_cyclers, get_dump_format,
INTEGER_CYCLING_TYPE, ISO8601_CYCLING_TYPE
)
from cylc.flow.cycling.nocycle import (
NocycleSequence,
NOCYCLE_SEQ_STARTUP,
NOCYCLE_SEQ_SHUTDOWN
)
from cylc.flow.id import Tokens
from cylc.flow.cycling.integer import IntegerInterval
from cylc.flow.cycling.iso8601 import ingest_time, ISO8601Interval
Expand Down Expand Up @@ -270,6 +275,7 @@
self.start_point: 'PointBase'
self.stop_point: Optional['PointBase'] = None
self.final_point: Optional['PointBase'] = None
self.nocycle_sequences: Set['NocycleSequence'] = set()
self.sequences: List['SequenceBase'] = []
self.actual_first_point: Optional['PointBase'] = None
self._start_point_for_actual_first_point: Optional['PointBase'] = None
Expand Down Expand Up @@ -604,8 +610,13 @@
)

def prelim_process_graph(self) -> None:
"""Ensure graph is not empty; set integer cycling mode and icp/fcp = 1
for simplest "R1 = foo" type graphs.
"""Error if graph empty; set integer cycling and icp/fcp = 1,
if those settings are omitted and the graph is acyclic graphs.

Somewhat relevant notes:
- The default (if not set) cycling mode, gregorian, requires an ICP.
- cycling mode is not stored in the DB, so recompute for restarts.

"""
graphdict = self.cfg['scheduling']['graph']
if not any(graphdict.values()):
Expand All @@ -614,9 +625,21 @@
if (
'cycling mode' not in self.cfg['scheduling'] and
self.cfg['scheduling'].get('initial cycle point', '1') == '1' and
all(item in ['graph', '1', 'R1'] for item in graphdict)
all(
seq in [
'R1',
str(NOCYCLE_SEQ_STARTUP),
str(NOCYCLE_SEQ_SHUTDOWN),
'graph', # Cylc 7 back-compat
'1' # Cylc 7 back-compat?
]
for seq in graphdict
)
):
# Pure acyclic graph, assume integer cycling mode with '1' cycle
# Non-cycling graph, assume integer cycling mode with '1' cycle.
# Typos in "startup", "shutdown", or "R1" will appear as cycling
# here, but will be fatal later during proper recurrance checking.

self.cfg['scheduling']['cycling mode'] = INTEGER_CYCLING_TYPE
for key in ('initial cycle point', 'final cycle point'):
if key not in self.cfg['scheduling']:
Expand Down Expand Up @@ -2249,15 +2272,25 @@
try:
seq = get_sequence(section, icp, fcp)
except (AttributeError, TypeError, ValueError, CylcError) as exc:
if cylc.flow.flags.verbosity > 1:
traceback.print_exc()
msg = 'Cannot process recurrence %s' % section
msg += ' (initial cycle point=%s)' % icp
msg += ' (final cycle point=%s)' % fcp
if isinstance(exc, CylcError):
msg += ' %s' % exc.args[0]
raise WorkflowConfigError(msg) from None
self.sequences.append(seq)
try:

Check warning on line 2275 in cylc/flow/config.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/config.py#L2275

Added line #L2275 was not covered by tests
# is it a startup or shutdown graph?
seq = NocycleSequence(section)
except ValueError:

Check warning on line 2278 in cylc/flow/config.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/config.py#L2277-L2278

Added lines #L2277 - L2278 were not covered by tests
if cylc.flow.flags.verbosity > 1:
traceback.print_exc()
msg = (

Check warning on line 2281 in cylc/flow/config.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/config.py#L2280-L2281

Added lines #L2280 - L2281 were not covered by tests
f"Cannot process recurrence {section}"
f" (initial cycle point={icp})"
f" (final cycle point={fcp})"
)
if isinstance(exc, CylcError):
msg += ' %s' % exc.args[0]
raise WorkflowConfigError(msg) from None

Check warning on line 2288 in cylc/flow/config.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/config.py#L2287-L2288

Added lines #L2287 - L2288 were not covered by tests
else:
self.nocycle_sequences.add(seq)

Check warning on line 2290 in cylc/flow/config.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/config.py#L2290

Added line #L2290 was not covered by tests
else:
self.sequences.append(seq)

parser = GraphParser(
family_map,
self.parameters,
Expand Down
6 changes: 5 additions & 1 deletion cylc/flow/cycling/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,11 @@ def TYPE_SORT_KEY(self) -> int:
@classmethod
@abstractmethod # Note: stacked decorator not strictly enforced in Py2.x
def get_async_expr(cls, start_point=0):
"""Express a one-off sequence at the initial cycle point."""
"""Express a one-off sequence at the initial cycle point.

Note "async" has nothing to do with asyncio. It was a (bad)
name for one-off (non-cycling) graphs in early Cylc versions.
"""
pass

@abstractmethod
Expand Down
7 changes: 6 additions & 1 deletion cylc/flow/cycling/integer.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
import re

from cylc.flow.cycling import (
PointBase, IntervalBase, SequenceBase, ExclusionBase, parse_exclusion, cmp
PointBase,
IntervalBase,
SequenceBase,
ExclusionBase,
parse_exclusion,
cmp
)
from cylc.flow.exceptions import (
CylcMissingContextPointError,
Expand Down
4 changes: 3 additions & 1 deletion cylc/flow/cycling/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@

from typing import Optional, Type, overload

from cylc.flow.cycling import PointBase, integer, iso8601
from cylc.flow.cycling import PointBase, integer, iso8601, nocycle
from metomi.isodatetime.data import Calendar


ISO8601_CYCLING_TYPE = iso8601.CYCLER_TYPE_ISO8601
INTEGER_CYCLING_TYPE = integer.CYCLER_TYPE_INTEGER
NOCYCLE_CYCLING_TYPE = nocycle.CYCLER_TYPE_NOCYCLE


IS_OFFSET_ABSOLUTE_IMPLS = {
INTEGER_CYCLING_TYPE: integer.is_offset_absolute,
Expand Down
253 changes: 253 additions & 0 deletions cylc/flow/cycling/nocycle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,253 @@
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

"""
Cycling logic for isolated non-cycling startup and shutdown graphs.
"""

from cylc.flow.cycling import PointBase, SequenceBase

# cycle point values
NOCYCLE_PT_STARTUP = "startup"
NOCYCLE_PT_SHUTDOWN = "shutdown"

NOCYCLE_POINTS = (
NOCYCLE_PT_STARTUP,
NOCYCLE_PT_SHUTDOWN
)

CYCLER_TYPE_NOCYCLE = "nocycle"
CYCLER_TYPE_SORT_KEY_NOCYCLE = 1

# Unused abstract methods below left to raise NotImplementedError.


class NocyclePoint(PointBase):
"""A non-advancing string-valued cycle point."""

TYPE = CYCLER_TYPE_NOCYCLE
TYPE_SORT_KEY = CYCLER_TYPE_SORT_KEY_NOCYCLE

__slots__ = ('value')

def __init__(self, value: str) -> None:
"""Initialise a nocycle point.

>>> NocyclePoint(NOCYCLE_PT_STARTUP)
startup
>>> NocyclePoint("beta")
Traceback (most recent call last):
ValueError: Illegal Nocycle value 'beta'
"""
if value not in [NOCYCLE_PT_STARTUP, NOCYCLE_PT_SHUTDOWN]:
raise ValueError(f"Illegal Nocycle value '{value}'")
self.value = value

def __hash__(self):
"""Hash it.

>>> bool(hash(NocyclePoint(NOCYCLE_PT_STARTUP)))
True
"""
return hash(self.value)

def __eq__(self, other):
"""Equality.

>>> (NocyclePoint(NOCYCLE_PT_STARTUP) ==
... NocyclePoint(NOCYCLE_PT_STARTUP))
True
>>> (NocyclePoint(NOCYCLE_PT_STARTUP) ==
... NocyclePoint(NOCYCLE_PT_SHUTDOWN))
False
"""
return str(other) == str(self.value)

def __le__(self, other):
"""Less than or equal (only if equal).

>>> (NocyclePoint(NOCYCLE_PT_STARTUP) <=
... NocyclePoint(NOCYCLE_PT_STARTUP))
True
>>> (NocyclePoint(NOCYCLE_PT_STARTUP) <=
... NocyclePoint(NOCYCLE_PT_SHUTDOWN))
False
"""
return str(other) == self.value

def __lt__(self, other):
"""Less than (never).

>>> (NocyclePoint(NOCYCLE_PT_STARTUP) <
... NocyclePoint(NOCYCLE_PT_STARTUP))
False
>>> (NocyclePoint(NOCYCLE_PT_STARTUP) <
... NocyclePoint(NOCYCLE_PT_SHUTDOWN))
False
"""
return False

def __gt__(self, other):
"""Greater than (never).
>>> (NocyclePoint(NOCYCLE_PT_STARTUP) >
... NocyclePoint(NOCYCLE_PT_STARTUP))
False
>>> (NocyclePoint(NOCYCLE_PT_STARTUP) >
... NocyclePoint(NOCYCLE_PT_SHUTDOWN))
False
"""
return False

def __str__(self):
"""
>>> str(NocyclePoint(NOCYCLE_PT_STARTUP))
'startup'
>>> str(NocyclePoint(NOCYCLE_PT_SHUTDOWN))
'shutdown'
"""
return self.value

def _cmp(self, other):
raise NotImplementedError

def add(self, other):
# Not used.
raise NotImplementedError

def sub(self, other):
# Not used.
raise NotImplementedError


class NocycleSequence(SequenceBase):
"""A single point sequence."""

def __init__(self, dep_section, p_context_start=None, p_context_stop=None):
"""Workflow cycling context is ignored.

>>> NocycleSequence("startup").point
startup
"""
self.point = NocyclePoint(dep_section)

def __hash__(self):
"""Hash it.

>>> bool(hash(NocycleSequence("startup")))
True
"""
return hash(str(self.point))

def is_valid(self, point):
"""Is point on-sequence and in-bounds?

>>> NocycleSequence("startup").is_valid("startup")
True
>>> NocycleSequence("startup").is_valid("shutdown")
False
"""
return str(point) == str(self.point)

def get_first_point(self, point):
"""First point is the only point.

>>> NocycleSequence("startup").get_first_point("shutdown")
startup
"""
return self.point

def get_start_point(self, point):
"""First point is the only point."""
# Not used.
raise NotImplementedError
return self.point

Check warning on line 176 in cylc/flow/cycling/nocycle.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/cycling/nocycle.py#L176

Added line #L176 was not covered by tests

def get_next_point(self, point):
"""There is no next point.

>>> NocycleSequence("startup").get_next_point("startup")
"""
return None

def get_next_point_on_sequence(self, point):
"""There is no next point.

>>> NocycleSequence("startup").get_next_point_on_sequence("startup")
"""
return None

def __eq__(self, other):
"""Equality.

>>> NocycleSequence("startup") == NocycleSequence("startup")
True
>>> NocycleSequence("startup") == NocycleSequence("shutdown")
False
"""
try:
return str(other.point) == str(self.point)
except AttributeError:

Check warning on line 202 in cylc/flow/cycling/nocycle.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/cycling/nocycle.py#L202

Added line #L202 was not covered by tests
# (other has not .point)
return False

Check warning on line 204 in cylc/flow/cycling/nocycle.py

View check run for this annotation

Codecov / codecov/patch

cylc/flow/cycling/nocycle.py#L204

Added line #L204 was not covered by tests

def __str__(self):
"""String.

>>> str(NocycleSequence("startup"))
'startup'
"""
return str(self.point)

def TYPE(self):
raise NotImplementedError

def TYPE_SORT_KEY(self):
raise NotImplementedError

def get_async_expr(cls, start_point=0):
raise NotImplementedError

def get_interval(self):
"""Return the cycling interval of this sequence."""
raise NotImplementedError

def get_offset(self):
"""Deprecated: return the offset used for this sequence."""
raise NotImplementedError

def set_offset(self, i_offset):
"""Deprecated: alter state to offset the entire sequence."""
raise NotImplementedError

def is_on_sequence(self, point):
"""Is point on-sequence, disregarding bounds?"""
raise NotImplementedError

def get_prev_point(self, point):
"""Return the previous point < point, or None if out of bounds."""
raise NotImplementedError

def get_nearest_prev_point(self, point):
"""Return the largest point < some arbitrary point."""
raise NotImplementedError

def get_stop_point(self):
"""Return the last point in this sequence, or None if unbounded."""
raise NotImplementedError


NOCYCLE_SEQ_STARTUP = NocycleSequence(NOCYCLE_PT_STARTUP)
NOCYCLE_SEQ_SHUTDOWN = NocycleSequence(NOCYCLE_PT_SHUTDOWN)
Loading
Loading