Skip to content

Commit d82ebc6

Browse files
committed
Squashed commit of the following:
commit 629ead573f5a306a23a5153eb02c6cf0523dd436 Merge: 16387d0 7284f25 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Fri Mar 14 00:26:41 2025 +0000 Merge branch 'master' into initial-final-graph commit 16387d0 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Fri Feb 14 00:45:31 2025 +0000 Always start new graph from flow 1. commit 2c95d33 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Thu Feb 13 23:32:20 2025 +0000 Extend a functional test. commit a0b65ba Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Mon Feb 10 09:59:06 2025 +0000 Post-merge tweaks. commit 3bed101 Merge: db03e50 4f11996 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Fri Feb 7 00:35:13 2025 +0000 Merge branch 'master' into initial-final-graph commit db03e50 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Mon Sep 4 17:49:47 2023 +1200 Post-rebase tweakage. commit af0bdc1 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Mon Mar 20 10:32:09 2023 +1300 Squashed commit of the following: commit ad8231a8f22dd3cd8d887774474f97df2c83e429 Merge: acc0ca3 6fc3c58 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Mon Mar 20 10:27:19 2023 +1300 Merge branch 'master' into initial-final-graph commit acc0ca3 Author: Hilary Oliver <hilary.j.oliver@gmail.com> Date: Wed Oct 26 12:00:00 2022 +1300 Style fix. commit a7170fb Author: Hilary Oliver <hilary.j.oliver@gmail.com> Date: Wed Oct 26 11:53:31 2022 +1300 Update change log. commit 9024065 Merge: 858a8c1 b0ff549 Author: Hilary Oliver <hilary.j.oliver@gmail.com> Date: Wed Oct 26 11:32:26 2022 +1300 Merge branch 'master' into initial-final-graph commit 858a8c1 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Mon Sep 26 23:27:01 2022 +1300 Fix alpha/omega graph runahead release. commit b581ab2 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Mon Sep 26 23:26:33 2022 +1300 Add new func tests. commit 8d09900 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Mon Sep 26 15:53:42 2022 +1300 Revert graph sorting change. commit 75c1763 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Fri Sep 23 22:44:08 2022 +1200 Adapt integration tests. commit 1b685a0 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Fri Sep 23 15:55:57 2022 +1200 Tidy up. commit 7a62450 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Fri Sep 23 13:20:51 2022 +1200 Switch to alpha and omega. commit 129c2e3 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Fri Sep 23 10:32:54 2022 +1200 Implicit integer ICP if only nocycle graphs. commit a53ac82 Author: Hilary James Oliver <hilary.j.oliver@gmail.com> Date: Thu Sep 22 16:23:16 2022 +1200 startup and shutdown graphs; needs tidying
1 parent 7284f25 commit d82ebc6

29 files changed

+823
-110
lines changed

changes.d/5090.feat.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Distinct initial and final graphs, separated from the main cycling graph,
2+
to make it easier to configure special behaviour at startup and shutdown.

cylc/flow/config.py

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
get_sequence, get_sequence_cls, init_cyclers, get_dump_format,
5555
INTEGER_CYCLING_TYPE, ISO8601_CYCLING_TYPE
5656
)
57+
from cylc.flow.cycling.nocycle import (
58+
NocycleSequence,
59+
NOCYCLE_SEQ_ALPHA,
60+
NOCYCLE_SEQ_OMEGA
61+
)
5762
from cylc.flow.id import Tokens
5863
from cylc.flow.cycling.integer import IntegerInterval
5964
from cylc.flow.cycling.iso8601 import ingest_time, ISO8601Interval
@@ -270,6 +275,7 @@ def __init__(
270275
self.start_point: 'PointBase'
271276
self.stop_point: Optional['PointBase'] = None
272277
self.final_point: Optional['PointBase'] = None
278+
self.nocycle_sequences: Set['NocycleSequence'] = set()
273279
self.sequences: List['SequenceBase'] = []
274280
self.actual_first_point: Optional['PointBase'] = None
275281
self._start_point_for_actual_first_point: Optional['PointBase'] = None
@@ -604,8 +610,13 @@ def _warn_if_queues_have_implicit_tasks(
604610
)
605611

606612
def prelim_process_graph(self) -> None:
607-
"""Ensure graph is not empty; set integer cycling mode and icp/fcp = 1
608-
for simplest "R1 = foo" type graphs.
613+
"""Error if graph empty; set integer cycling and icp/fcp = 1,
614+
if those settings are omitted and the graph is acyclic graphs.
615+
616+
Somewhat relevant notes:
617+
- The default (if not set) cycling mode, gregorian, requires an ICP.
618+
- cycling mode is not stored in the DB, so recompute for restarts.
619+
609620
"""
610621
graphdict = self.cfg['scheduling']['graph']
611622
if not any(graphdict.values()):
@@ -614,9 +625,21 @@ def prelim_process_graph(self) -> None:
614625
if (
615626
'cycling mode' not in self.cfg['scheduling'] and
616627
self.cfg['scheduling'].get('initial cycle point', '1') == '1' and
617-
all(item in ['graph', '1', 'R1'] for item in graphdict)
628+
all(
629+
seq in [
630+
'R1',
631+
str(NOCYCLE_SEQ_ALPHA),
632+
str(NOCYCLE_SEQ_OMEGA),
633+
'graph', # Cylc 7 back-compat
634+
'1' # Cylc 7 back-compat?
635+
]
636+
for seq in graphdict
637+
)
618638
):
619639
# Pure acyclic graph, assume integer cycling mode with '1' cycle
640+
# Note typos in "alpha", "omega", or "R1" will appear as cyclic
641+
# here, but will be fatal later during proper recurrance checking.
642+
620643
self.cfg['scheduling']['cycling mode'] = INTEGER_CYCLING_TYPE
621644
for key in ('initial cycle point', 'final cycle point'):
622645
if key not in self.cfg['scheduling']:
@@ -2249,15 +2272,25 @@ def load_graph(self):
22492272
try:
22502273
seq = get_sequence(section, icp, fcp)
22512274
except (AttributeError, TypeError, ValueError, CylcError) as exc:
2252-
if cylc.flow.flags.verbosity > 1:
2253-
traceback.print_exc()
2254-
msg = 'Cannot process recurrence %s' % section
2255-
msg += ' (initial cycle point=%s)' % icp
2256-
msg += ' (final cycle point=%s)' % fcp
2257-
if isinstance(exc, CylcError):
2258-
msg += ' %s' % exc.args[0]
2259-
raise WorkflowConfigError(msg) from None
2260-
self.sequences.append(seq)
2275+
try:
2276+
# is it an alpha or omega graph?
2277+
seq = NocycleSequence(section)
2278+
except ValueError:
2279+
if cylc.flow.flags.verbosity > 1:
2280+
traceback.print_exc()
2281+
msg = (
2282+
f"Cannot process recurrence {section}"
2283+
f" (initial cycle point={icp})"
2284+
f" (final cycle point={fcp})"
2285+
)
2286+
if isinstance(exc, CylcError):
2287+
msg += ' %s' % exc.args[0]
2288+
raise WorkflowConfigError(msg) from None
2289+
else:
2290+
self.nocycle_sequences.add(seq)
2291+
else:
2292+
self.sequences.append(seq)
2293+
22612294
parser = GraphParser(
22622295
family_map,
22632296
self.parameters,

cylc/flow/cycling/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,11 @@ def TYPE_SORT_KEY(self) -> int:
349349
@classmethod
350350
@abstractmethod # Note: stacked decorator not strictly enforced in Py2.x
351351
def get_async_expr(cls, start_point=0):
352-
"""Express a one-off sequence at the initial cycle point."""
352+
"""Express a one-off sequence at the initial cycle point.
353+
354+
Note "async" has nothing to do with asyncio. It was a (bad)
355+
name for one-off (non-cycling) graphs in early Cylc versions.
356+
"""
353357
pass
354358

355359
@abstractmethod

cylc/flow/cycling/integer.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121
import re
2222

2323
from cylc.flow.cycling import (
24-
PointBase, IntervalBase, SequenceBase, ExclusionBase, parse_exclusion, cmp
24+
PointBase,
25+
IntervalBase,
26+
SequenceBase,
27+
ExclusionBase,
28+
parse_exclusion,
29+
cmp
2530
)
2631
from cylc.flow.exceptions import (
2732
CylcMissingContextPointError,

cylc/flow/cycling/loader.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@
2121

2222
from typing import Optional, Type, overload
2323

24-
from cylc.flow.cycling import PointBase, integer, iso8601
24+
from cylc.flow.cycling import PointBase, integer, iso8601, nocycle
2525
from metomi.isodatetime.data import Calendar
2626

2727

2828
ISO8601_CYCLING_TYPE = iso8601.CYCLER_TYPE_ISO8601
2929
INTEGER_CYCLING_TYPE = integer.CYCLER_TYPE_INTEGER
30+
NOCYCLE_CYCLING_TYPE = nocycle.CYCLER_TYPE_NOCYCLE
31+
3032

3133
IS_OFFSET_ABSOLUTE_IMPLS = {
3234
INTEGER_CYCLING_TYPE: integer.is_offset_absolute,

cylc/flow/cycling/nocycle.py

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# THIS FILE IS PART OF THE CYLC WORKFLOW ENGINE.
2+
# Copyright (C) NIWA & British Crown (Met Office) & Contributors.
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License
15+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
17+
"""
18+
Cycling logic for isolated non-cycling startup and shutdown graphs.
19+
"""
20+
21+
from cylc.flow.cycling import PointBase, SequenceBase
22+
23+
# cycle point values
24+
NOCYCLE_PT_ALPHA = "alpha"
25+
NOCYCLE_PT_OMEGA = "omega"
26+
27+
NOCYCLE_POINTS = (
28+
NOCYCLE_PT_ALPHA,
29+
NOCYCLE_PT_OMEGA
30+
)
31+
32+
CYCLER_TYPE_NOCYCLE = "nocycle"
33+
CYCLER_TYPE_SORT_KEY_NOCYCLE = 1
34+
35+
# Unused abstract methods below left to raise NotImplementedError.
36+
37+
38+
class NocyclePoint(PointBase):
39+
"""A non-advancing string-valued cycle point."""
40+
41+
TYPE = CYCLER_TYPE_NOCYCLE
42+
TYPE_SORT_KEY = CYCLER_TYPE_SORT_KEY_NOCYCLE
43+
44+
__slots__ = ('value')
45+
46+
def __init__(self, value: str) -> None:
47+
"""Initialise a nocycle point.
48+
49+
>>> NocyclePoint(NOCYCLE_PT_ALPHA)
50+
alpha
51+
>>> NocyclePoint("beta")
52+
Traceback (most recent call last):
53+
ValueError: Illegal Nocycle value 'beta'
54+
"""
55+
if value not in [NOCYCLE_PT_ALPHA, NOCYCLE_PT_OMEGA]:
56+
raise ValueError(f"Illegal Nocycle value '{value}'")
57+
self.value = value
58+
59+
def __hash__(self):
60+
"""Hash it.
61+
62+
>>> bool(hash(NocyclePoint(NOCYCLE_PT_ALPHA)))
63+
True
64+
"""
65+
return hash(self.value)
66+
67+
def __eq__(self, other):
68+
"""Equality.
69+
70+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) == NocyclePoint(NOCYCLE_PT_ALPHA)
71+
True
72+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) == NocyclePoint(NOCYCLE_PT_OMEGA)
73+
False
74+
"""
75+
return str(other) == str(self.value)
76+
77+
def __le__(self, other):
78+
"""Less than or equal (only if equal).
79+
80+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) <= NocyclePoint(NOCYCLE_PT_ALPHA)
81+
True
82+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) <= NocyclePoint(NOCYCLE_PT_OMEGA)
83+
False
84+
"""
85+
return str(other) == self.value
86+
87+
def __lt__(self, other):
88+
"""Less than (never).
89+
90+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) < NocyclePoint(NOCYCLE_PT_ALPHA)
91+
False
92+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) < NocyclePoint(NOCYCLE_PT_OMEGA)
93+
False
94+
"""
95+
return False
96+
97+
def __gt__(self, other):
98+
"""Greater than (never).
99+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) > NocyclePoint(NOCYCLE_PT_ALPHA)
100+
False
101+
>>> NocyclePoint(NOCYCLE_PT_ALPHA) > NocyclePoint(NOCYCLE_PT_OMEGA)
102+
False
103+
"""
104+
return False
105+
106+
def __str__(self):
107+
"""
108+
>>> str(NocyclePoint(NOCYCLE_PT_ALPHA))
109+
'alpha'
110+
>>> str(NocyclePoint(NOCYCLE_PT_OMEGA))
111+
'omega'
112+
"""
113+
return self.value
114+
115+
def _cmp(self, other):
116+
raise NotImplementedError
117+
118+
def add(self, other):
119+
# Not used.
120+
raise NotImplementedError
121+
122+
def sub(self, other):
123+
# Not used.
124+
raise NotImplementedError
125+
126+
127+
class NocycleSequence(SequenceBase):
128+
"""A single point sequence."""
129+
130+
def __init__(self, dep_section, p_context_start=None, p_context_stop=None):
131+
"""Workflow cycling context is ignored.
132+
133+
>>> NocycleSequence("alpha").point
134+
alpha
135+
"""
136+
self.point = NocyclePoint(dep_section)
137+
138+
def __hash__(self):
139+
"""Hash it.
140+
141+
>>> bool(hash(NocycleSequence("alpha")))
142+
True
143+
"""
144+
return hash(str(self.point))
145+
146+
def is_valid(self, point):
147+
"""Is point on-sequence and in-bounds?
148+
149+
>>> NocycleSequence("alpha").is_valid("alpha")
150+
True
151+
>>> NocycleSequence("alpha").is_valid("omega")
152+
False
153+
"""
154+
return str(point) == str(self.point)
155+
156+
def get_first_point(self, point):
157+
"""First point is the only point.
158+
159+
>>> NocycleSequence("alpha").get_first_point("omega")
160+
alpha
161+
"""
162+
return self.point
163+
164+
def get_start_point(self, point):
165+
"""First point is the only point."""
166+
# Not used.
167+
raise NotImplementedError
168+
return self.point
169+
170+
def get_next_point(self, point):
171+
"""There is no next point.
172+
173+
>>> NocycleSequence("alpha").get_next_point("alpha")
174+
"""
175+
return None
176+
177+
def get_next_point_on_sequence(self, point):
178+
"""There is no next point.
179+
180+
>>> NocycleSequence("alpha").get_next_point_on_sequence("alpha")
181+
"""
182+
return None
183+
184+
def __eq__(self, other):
185+
"""Equality.
186+
187+
>>> NocycleSequence("alpha") == NocycleSequence("alpha")
188+
True
189+
>>> NocycleSequence("alpha") == NocycleSequence("omega")
190+
False
191+
"""
192+
try:
193+
return str(other.point) == str(self.point)
194+
except AttributeError:
195+
# (other has not .point)
196+
return False
197+
198+
def __str__(self):
199+
"""String.
200+
201+
>>> str(NocycleSequence("alpha"))
202+
'alpha'
203+
"""
204+
return str(self.point)
205+
206+
def TYPE(self):
207+
raise NotImplementedError
208+
209+
def TYPE_SORT_KEY(self):
210+
raise NotImplementedError
211+
212+
def get_async_expr(cls, start_point=0):
213+
raise NotImplementedError
214+
215+
def get_interval(self):
216+
"""Return the cycling interval of this sequence."""
217+
raise NotImplementedError
218+
219+
def get_offset(self):
220+
"""Deprecated: return the offset used for this sequence."""
221+
raise NotImplementedError
222+
223+
def set_offset(self, i_offset):
224+
"""Deprecated: alter state to offset the entire sequence."""
225+
raise NotImplementedError
226+
227+
def is_on_sequence(self, point):
228+
"""Is point on-sequence, disregarding bounds?"""
229+
raise NotImplementedError
230+
231+
def get_prev_point(self, point):
232+
"""Return the previous point < point, or None if out of bounds."""
233+
raise NotImplementedError
234+
235+
def get_nearest_prev_point(self, point):
236+
"""Return the largest point < some arbitrary point."""
237+
raise NotImplementedError
238+
239+
def get_stop_point(self):
240+
"""Return the last point in this sequence, or None if unbounded."""
241+
raise NotImplementedError
242+
243+
244+
NOCYCLE_SEQ_ALPHA = NocycleSequence(NOCYCLE_PT_ALPHA)
245+
NOCYCLE_SEQ_OMEGA = NocycleSequence(NOCYCLE_PT_OMEGA)

0 commit comments

Comments
 (0)