Peano
Loading...
Searching...
No Matches
PropagateUseStatements.py
Go to the documentation of this file.
1# This file is part of the Peano project. For conditions of distribution and
2# use, please see the copyright notice at www.peano-framework.org
3from typing import Dict, List, Optional, Tuple
4
5import peano4.api
6
7from ..Task import Task
8
9
10def propagate_use_statements(
11 sequences: List[List["peano4.api.solversteps.Step"]],
12 edges: Optional[List[Tuple[List["peano4.api.solversteps.Step"], List["peano4.api.solversteps.Step"]]]] = None,
13) -> bool:
14 """!
15
16 Derive whichever further use_cell()/use_face()/use_vertex() declarations
17 a set of compiled step sequences need so that every submodel they write
18 is safely handed on to whoever reads it next.
19
20 This is a postprocessing step that runs on the output of a task graph
21 compiler (@ref peano4.api.taskgraph.compiler), i.e. on the sequences of
22 peano4.api.solversteps.Step instances (Observers, aka mesh traversals)
23 a compiler such as peano4.api.taskgraph.compiler.Sequential.translate()
24 or peano4.api.taskgraph.compiler.FuseSteps.translate() yields - one
25 sequence per task graph. All distances below therefore count mesh
26 traversals, not tasks: a compiler that fuses several tasks into one
27 Step changes these distances, and only the compiled Steps know them.
28
29 Every Step carries the Task.DataAccess qualifier of each submodel it
30 declares in its cell_data_access/face_data_access/vertex_data_access
31 (populated by the compiler). This function reads and amends these
32 qualifiers, and it adds use_cell()/use_face()/use_vertex() declarations
33 to the Steps where required.
34
35 ## Creating submodels that are read before they are ever written
36
37 The very first pass looks for submodels that a step loads
38 (READ_AND_DISCARD or READ_AND_WRITE) before any step has ever written
39 them. Peano would then try to load data that does not exist yet and
40 stop with an assertion on its stacks. A sequence without any incoming
41 edge (from another sequence or itself) is an entry sequence, i.e. it
42 runs first, such as a setup phase. The pass searches forward from the
43 first step of every entry sequence, following edges, and stops each
44 path at the first step that declares the submodel (TEMPORARY is
45 transparent). If this step reads the submodel, the pass adds a WRITE
46 declaration of the submodel to the last step of the entry sequence:
47 this step then creates the submodel and stores it, and the passes
48 below make it reach the reader. The created submodel holds whatever
49 its default constructor yields, i.e. the steps reading it have to
50 cope with uninitialised values - typically through guards that ensure
51 that they only read it once a "real" WRITE has happened. The WRITE
52 goes into the entry sequence, as this one runs only once: a WRITE
53 within a repeating sequence would overwrite the submodel's value every
54 time the sequence runs. A read within the entry sequence itself cannot
55 be fixed this way and is only logged. @see
56 _create_submodels_read_before_written().
57
58 ## Keeping submodels that a READ_AND_DISCARD would discard too early
59
60 The second pass looks for READ_AND_DISCARD declarations after which
61 another step still reads the submodel (READ_AND_DISCARD or
62 READ_AND_WRITE) before any step writes it again. This happens, for
63 example, if two sibling tasks both consume the same submodel and the
64 compiler maps them onto two subsequent mesh traversals: each task is
65 done with the submodel, but only the latter traversal may discard it.
66 The pass searches forward from every READ_AND_DISCARD, following
67 edges, and stops each path at the first step that declares the
68 submodel (TEMPORARY is transparent). If this step reads the submodel,
69 the READ_AND_DISCARD is upgraded to READ_AND_WRITE, i.e. the submodel
70 is stored again. The pass runs to a fixed point, and the passes below
71 treat the upgraded declaration like any other one. @see
72 _upgrade_discards_followed_by_reads().
73
74 ## Upgrading a WRITE that immediately follows a READ_AND_WRITE
75
76 Before any of the below runs, a narrower, separate pass first upgrades
77 every WRITE declaration that sits immediately (distance 1, following
78 edges the same way as everything else here) after a READ_AND_WRITE
79 declaration of the same submodel into a READ_AND_WRITE of its own -
80 @see _upgrade_writes_immediately_following_read_and_write() for why
81 (in short: trusting the WRITE there would silently discard whatever
82 the READ_AND_WRITE step explicitly asked to keep). This is logged via
83 peano4.api.Logger().log() whenever it happens. A WRITE immediately
84 after another plain WRITE is deliberately left alone by this - @see
85 that function's own docstring for why that case is different.
86
87 ## The rule
88
89 Every use_cell()/use_face()/use_vertex() declaration carries a
90 Task.DataAccess qualifier - READ_AND_DISCARD, WRITE, READ_AND_WRITE or
91 TEMPORARY. Peano only relays a submodel from one Observer to the next
92 through its stacks if *both* declare it, so a submodel a step WRITEs
93 is lost unless some later step actually reads it, and every step in
94 between has to relay it there. Precisely:
95
96 Only a READ_AND_DISCARD or READ_AND_WRITE ever "picks up" a submodel a
97 WRITE produced; a WRITE or READ_AND_WRITE is what obliges anything
98 downstream in the first place (a plain READ_AND_DISCARD, or leaving
99 the submodel alone entirely, imposes no obligation of its own). Two
100 other qualifiers get special treatment when they turn up *downstream*
101 of such an obligation:
102
103 - A bare WRITE encountered downstream is a fresh, independent
104 production - it does not read what came before, it simply
105 supersedes it, so there is nothing left to protect on the way to
106 it. The search stops there with no constraint at all (that WRITE is
107 instead - separately - checked as an origin of its own, once the
108 surrounding loop reaches it in turn).
109 - A TEMPORARY use is transparent: it is treated exactly like a step
110 that does not mention the submodel at all (@see
111 Task.DataAccess.TEMPORARY) - the search simply continues straight
112 through it.
113
114 That leaves READ_AND_DISCARD and READ_AND_WRITE as the only uses that
115 actually stop the search and matter. For every WRITE or
116 READ_AND_WRITE use at some step, let *distance* be how many steps
117 separate it from the next such READ_AND_DISCARD/READ_AND_WRITE use of
118 the same submodel (distance 1 means "the very next step"):
119
120 - an odd distance (1, 3, 5, ...): always fine, whatever lies strictly
121 in between (nothing, in the sense above - every step there is
122 either genuinely silent or a WRITE/TEMPORARY, neither of which
123 counts).
124 - an even distance (2, 4, 6, ...): not admissible as it stands, but
125 always fixable: insert a READ_AND_WRITE declaration of the submodel
126 on the step exactly one before the next READ_AND_DISCARD/
127 READ_AND_WRITE (an odd distance away from the origin, so the two
128 legs it creates - origin-to-relay and relay-to-next-use - are each
129 individually valid by the rule above). This is the *only* thing
130 this function ever does to fix a violation.
131 - no such READ_AND_DISCARD/READ_AND_WRITE reachable at all (every path
132 from here either dead-ends or runs into a superseding WRITE first):
133 nothing to do - there is nothing downstream to protect.
134
135 The freshly-inserted READ_AND_WRITE is itself fed back through the very
136 same check (it is, for this purpose, exactly as much of an origin as a
137 plain WRITE), so a single call already iterates this to a fixed point
138 - one call is enough, calling it again changes nothing further
139 (idempotent).
140
141 ### Worked examples (a single sequence of steps 0, 1, 2, 3, 4, ...)
142
143 - Step N has a WRITE, step N+2 a READ_AND_DISCARD, nothing in between
144 mentions the submodel: distance 2 (even) - not admissible as it
145 stands. Fixed by inserting a READ_AND_WRITE declaration on step N+1.
146 - Step N has a WRITE, step N+3 a READ_AND_DISCARD: distance 3 (odd) -
147 already fine, nothing to do.
148 - Step N has a WRITE, step N+4 a READ_AND_DISCARD: distance 4 (even) -
149 not admissible. Fixed by inserting a READ_AND_WRITE declaration on
150 step N+3 (one before the READ_AND_DISCARD) - not on step N+1:
151 inserting right after step N would put the relay at distance 1 from
152 the origin, and the *next* leg (relay to the eventual
153 READ_AND_DISCARD) would then be distance 3 away, still fine, but the
154 relay would sit doing nothing useful for two further steps;
155 inserting right before the READ_AND_DISCARD instead keeps both legs
156 minimal.
157 - Step N has a WRITE, step N+1 WRITEs the same submodel again: N+1's
158 WRITE supersedes N's - nothing to protect, nothing to do (even
159 though the two are adjacent).
160
161 ## Sequences, and how edges connect them
162
163 Within one sequence, "the next step" is simply the next one in the
164 list. Once a sequence's own steps run out, "the next step" continues
165 into whichever sequence(s) edges declares reachable from it - a
166 sequence can have several such successors (e.g. a time-stepping
167 sequence that may be followed by either another time step or a plot,
168 decided only at runtime), in which case the rule is checked, and fixed
169 if need be, independently along *every* such continuation, since we
170 cannot know which one will actually be taken. Two different
171 continuations can legitimately reach the very same later step at
172 distances of different parity (e.g. straight from setup into
173 time-stepping versus via a plot detour first, if plotting happens to
174 be due immediately) - both are checked and, if need be, independently
175 fixed; a silent step is only ever revisited once per parity of how far
176 away it was reached, which is all this rule ever cares about, so a
177 cyclic edge structure (e.g. time-stepping and plotting following one
178 another indefinitely) is still only walked a bounded number of times,
179 not indefinitely. A sequence that is meant to repeat itself declares a
180 (sequence, sequence) self-edge - the successor of its own last step is
181 (among others, if it is also reachable from elsewhere) its own first
182 step. A sequence with no outgoing edges at all simply ends there -
183 whatever it last wrote and never got read is not this function's
184 concern (it might be handed to a caller entirely outside the
185 task-graph machinery, e.g. plotted from C++ code this Python layer does
186 not see).
187
188 @param sequences: [[peano4.api.solversteps.Step]]
189 Every step sequence to consider, typically the outcome of one
190 compiler call per task graph. A sequence has to be listed here to be
191 analysed at all, whether or not it appears in edges (a sequence with
192 no edges is simply analysed on its own). The steps are modified in
193 place.
194
195 @param edges: [([peano4.api.solversteps.Step], [peano4.api.solversteps.Step])]
196 Declares which sequences may run immediately after which other ones -
197 each (predecessor, successor) pair means "successor's first step may
198 immediately follow predecessor's last step". Sequences are
199 identified by identity, i.e. pass the very list objects that are
200 also handed over through sequences. Defaults to none (every sequence
201 analysed purely on its own).
202
203 @return bool
204 True if this call added or upgraded at least one declaration anywhere,
205 False if everything was already admissible.
206
207 """
208 if edges is None:
209 edges = []
210
211 # Lists are not hashable, so we identify each sequence by its position
212 # within sequences and translate the edges accordingly.
213 def index_of_sequence(sequence) -> int:
214 for index, candidate in enumerate(sequences):
215 if candidate is sequence:
216 return index
217 raise ValueError("propagate_use_statements(): edge refers to a step sequence not passed through sequences")
218
219 steps_of_sequence: Dict[int, List] = dict(enumerate(sequences))
220 index_edges = [(index_of_sequence(predecessor), index_of_sequence(successor)) for predecessor, successor in edges]
221
222 something_changed = False
223 for attribute_name, access_attribute_name, use_method_name in (
224 ("cell_data", "cell_data_access", "use_cell"),
225 ("face_data", "face_data_access", "use_face"),
226 ("vertex_data", "vertex_data_access", "use_vertex"),
227 ):
228 something_changed = (
230 steps_of_sequence, index_edges, attribute_name, access_attribute_name, use_method_name
231 )
232 or something_changed
233 )
234 something_changed = (
236 steps_of_sequence, index_edges, attribute_name, access_attribute_name
237 )
238 or something_changed
239 )
240 something_changed = (
242 steps_of_sequence, index_edges, attribute_name, access_attribute_name
243 )
244 or something_changed
245 )
246 something_changed = (
248 steps_of_sequence, index_edges, attribute_name, access_attribute_name, use_method_name
249 )
250 or something_changed
251 )
252 return something_changed
253
254
256 steps_of_sequence: Dict[int, List],
257 edges: List[Tuple[int, int]],
258 attribute_name: str,
259 access_attribute_name: str,
260 use_method_name: str,
261) -> bool:
262 """!
263
264 Add a WRITE declaration to the last step of an entry sequence for every
265 submodel that can be read before it is ever written - @see the
266 corresponding section of propagate_use_statements().
267
268 @param steps_of_sequence: Dict[int, [peano4.api.solversteps.Step]]
269 The sequences handed over to propagate_use_statements(), each
270 sequence identified by its index.
271
272 @param edges: [(int, int)]
273 The edges handed over to propagate_use_statements(), translated into
274 pairs of sequence indices.
275
276 @param attribute_name: str
277 Which kind of submodel (cell_data, face_data or vertex_data) to
278 analyse.
279
280 @param access_attribute_name: str
281 The access dictionary matching attribute_name (cell_data_access,
282 face_data_access or vertex_data_access).
283
284 @param use_method_name: str
285 The Step method that declares a submodel of this kind (use_cell,
286 use_face or use_vertex).
287
288 @return bool
289 True if this pass added at least one declaration.
290
291 """
292 successors_of_sequence: Dict[int, List[int]] = {sequence: [] for sequence in steps_of_sequence}
293 has_predecessor = set()
294 for predecessor, successor in edges:
295 successors_of_sequence[predecessor].append(successor)
296 has_predecessor.add(successor)
297 entry_sequences = [
298 sequence for sequence, steps in steps_of_sequence.items() if steps and sequence not in has_predecessor
299 ]
300
301 submodels = []
302 for steps in steps_of_sequence.values():
303 for step in steps:
304 for submodel in getattr(step, attribute_name):
305 if submodel not in submodels:
306 submodels.append(submodel)
307
308 something_changed = False
309 for entry_sequence in entry_sequences:
310 last_entry_step = steps_of_sequence[entry_sequence][-1]
311 for submodel in submodels:
312 # Breadth-first search from the entry sequence's first step,
313 # each path stopping at the first real use of the submodel
314 frontier = [(entry_sequence, 0)]
315 visited = {(entry_sequence, 0)}
316 readers = []
317 while frontier:
318 sequence, index = frontier.pop(0)
319 step = steps_of_sequence[sequence][index]
320 qualifier = getattr(step, access_attribute_name).get(submodel)
321 if qualifier in (Task.DataAccess.READ_AND_DISCARD, Task.DataAccess.READ_AND_WRITE):
322 readers.append((sequence, step))
323 continue
324 if qualifier == Task.DataAccess.WRITE:
325 continue
326 for next_sequence, next_index in _step_forward(sequence, index, steps_of_sequence, successors_of_sequence):
327 if (next_sequence, next_index) not in visited:
328 visited.add((next_sequence, next_index))
329 frontier.append((next_sequence, next_index))
330
331 for sequence, step in readers:
332 if sequence == entry_sequence:
333 peano4.api.Logger().log(
334 f"propagate_use_statements(): step '{step.name}' reads {attribute_name} submodel "
335 f"'{submodel.name}' before any step has written it, and there is no earlier "
336 f"sequence which could create it."
337 )
338 elif getattr(last_entry_step, access_attribute_name).get(submodel) != Task.DataAccess.WRITE:
339 if getattr(last_entry_step, access_attribute_name).get(submodel) is None:
340 getattr(last_entry_step, use_method_name)(submodel)
341 getattr(last_entry_step, access_attribute_name)[submodel] = Task.DataAccess.WRITE
342 peano4.api.Logger().log(
343 f"propagate_use_statements(): step '{step.name}' reads {attribute_name} submodel "
344 f"'{submodel.name}' before any step has written it - adding a WRITE declaration "
345 f"to step '{last_entry_step.name}', which creates the submodel."
346 )
347 something_changed = True
348
349 return something_changed
350
351
353 steps_of_sequence: Dict[int, List],
354 edges: List[Tuple[int, int]],
355 attribute_name: str,
356 access_attribute_name: str,
357) -> bool:
358 """!
359
360 Upgrade every READ_AND_DISCARD declaration to READ_AND_WRITE if a later
361 step still reads the submodel before any step writes it again - @see
362 the corresponding section of propagate_use_statements().
363
364 @param steps_of_sequence: Dict[int, [peano4.api.solversteps.Step]]
365 The sequences handed over to propagate_use_statements(), each
366 sequence identified by its index.
367
368 @param edges: [(int, int)]
369 The edges handed over to propagate_use_statements(), translated into
370 pairs of sequence indices.
371
372 @param attribute_name: str
373 Which kind of submodel (cell_data, face_data or vertex_data) to
374 analyse.
375
376 @param access_attribute_name: str
377 The access dictionary matching attribute_name (cell_data_access,
378 face_data_access or vertex_data_access).
379
380 @return bool
381 True if this pass upgraded at least one declaration.
382
383 """
384 successors_of_sequence: Dict[int, List[int]] = {sequence: [] for sequence in steps_of_sequence}
385 for predecessor, successor in edges:
386 successors_of_sequence[predecessor].append(successor)
387
388 something_changed = False
389 changed_this_pass = True
390 while changed_this_pass:
391 changed_this_pass = False
392 for sequence, steps in steps_of_sequence.items():
393 for index, step in enumerate(steps):
394 access = getattr(step, access_attribute_name)
395 for submodel in list(getattr(step, attribute_name)):
396 if access[submodel] != Task.DataAccess.READ_AND_DISCARD:
397 continue
398 readers = [
399 next_step
400 for next_step, _, _ in _find_next_consecutive_uses(
401 sequence,
402 index,
403 submodel,
404 attribute_name,
405 access_attribute_name,
406 steps_of_sequence,
407 successors_of_sequence,
408 )
409 if getattr(next_step, access_attribute_name)[submodel]
410 in (Task.DataAccess.READ_AND_DISCARD, Task.DataAccess.READ_AND_WRITE)
411 ]
412 if readers:
413 access[submodel] = Task.DataAccess.READ_AND_WRITE
414 peano4.api.Logger().log(
415 f"propagate_use_statements(): step '{step.name}' declared {attribute_name} "
416 f"submodel '{submodel.name}' as READ_AND_DISCARD, but step '{readers[0].name}' "
417 f"reads it afterwards - upgrading '{step.name}' to READ_AND_WRITE to keep it."
418 )
419 changed_this_pass = True
420 something_changed = something_changed or changed_this_pass
421
422 return something_changed
423
424
425def _step_backward(sequence, index, steps_of_sequence, predecessors_of_sequence):
426 """!
427
428 The exact reverse of _step_forward(): yield every (sequence, index)
429 that can immediately reach (sequence, index) - the previous step in the
430 same sequence if there is one, or otherwise the last step of every
431 sequence declared (via edges) to lead into this one.
432
433 """
434 if index > 0:
435 yield (sequence, index - 1)
436 else:
437 for predecessor in predecessors_of_sequence.get(sequence, []):
438 predecessor_steps = steps_of_sequence[predecessor]
439 if predecessor_steps:
440 yield (predecessor, len(predecessor_steps) - 1)
441
442
444 steps_of_sequence: Dict[int, List],
445 edges: List[Tuple[int, int]],
446 attribute_name: str,
447 access_attribute_name: str,
448) -> bool:
449 """!
450
451 A plain WRITE is normally treated as a fresh, independent production
452 that supersedes whatever came before (@see _fix_forward_gap) - correct
453 whenever a step genuinely does not care what the submodel held before
454 it ran. But a WRITE immediately (distance 1) after a READ_AND_WRITE of
455 the very same submodel is a different, narrower situation: the
456 READ_AND_WRITE step explicitly asked for its result to be kept around,
457 and the very next step then declares itself uninterested in reading it
458 - one of these two declarations has to be wrong, and silently trusting
459 the WRITE would mean the READ_AND_WRITE step's stack storage is
460 populated (predicateForStore true) and then never loaded again
461 (predicateForLoad false for a plain WRITE - @see
462 peano4.api.taskgraph.compiler.DeriveLoadStoreComputeFlag), which is
463 at best wasted storage and at worst - if the very next step's own
464 computation actually does depend on that value, just under the wrong
465 qualifier - silently wrong numerics.
466
467 This is therefore auto-corrected rather than merely reported: every
468 step whose own declaration of a submodel is WRITE, and that has at
469 least one immediate predecessor (@see _step_backward - several, if the
470 step is reachable from more than one sequence) that declares the same
471 submodel READ_AND_WRITE, has its own declaration silently upgraded to
472 READ_AND_WRITE, logged via peano4.api.Logger().log() so it is visible
473 without changing behaviour otherwise. Run to a fixed point first,
474 before _propagate_use_statements_for_one_kind()'s own relay-insertion
475 pass: an upgraded declaration is exactly as valid an origin as one a
476 task graph author wrote by hand, and doing this first means the relay
477 pass never has to reason about a WRITE that is about to change.
478
479 This is deliberately narrower than "any WRITE reachable from an
480 earlier WRITE/READ_AND_WRITE": a WRITE immediately after another
481 WRITE of the same submodel is left alone, since that is exactly the
482 legitimate "recompute this fresh every time" pattern
483 _fix_forward_gap() already has to support (@see there) - only a
484 READ_AND_WRITE predecessor, which explicitly asked for its value to
485 survive, triggers the upgrade.
486
487 @param steps_of_sequence: Dict[int, [peano4.api.solversteps.Step]]
488 The sequences handed over to propagate_use_statements(), each
489 sequence identified by its index.
490
491 @param edges: [(int, int)]
492 The edges handed over to propagate_use_statements(), translated into
493 pairs of sequence indices.
494
495 @param attribute_name: str
496 Which kind of submodel (cell_data, face_data or vertex_data) to
497 analyse.
498
499 @param access_attribute_name: str
500 The access dictionary matching attribute_name (cell_data_access,
501 face_data_access or vertex_data_access).
502
503 @return bool
504 True if this pass upgraded at least one declaration.
505
506 """
507 predecessors_of_sequence: Dict[int, List[int]] = {sequence: [] for sequence in steps_of_sequence}
508 for predecessor, successor in edges:
509 predecessors_of_sequence[successor].append(predecessor)
510
511 something_changed = False
512 changed_this_pass = True
513 while changed_this_pass:
514 changed_this_pass = False
515 for sequence, steps in steps_of_sequence.items():
516 for index, step in enumerate(steps):
517 access = getattr(step, access_attribute_name)
518 for submodel in list(getattr(step, attribute_name)):
519 if access[submodel] != Task.DataAccess.WRITE:
520 continue
521 for predecessor_sequence, predecessor_index in _step_backward(
522 sequence, index, steps_of_sequence, predecessors_of_sequence
523 ):
524 predecessor_step = steps_of_sequence[predecessor_sequence][predecessor_index]
525 predecessor_access = getattr(predecessor_step, access_attribute_name)
526 if predecessor_access.get(submodel) == Task.DataAccess.READ_AND_WRITE:
527 access[submodel] = Task.DataAccess.READ_AND_WRITE
528 peano4.api.Logger().log(
529 f"propagate_use_statements(): step '{step.name}' declared {attribute_name} "
530 f"submodel '{submodel.name}' as WRITE, but its predecessor step "
531 f"'{predecessor_step.name}' declares it READ_AND_WRITE - upgrading "
532 f"'{step.name}' to READ_AND_WRITE to avoid silently discarding it."
533 )
534 changed_this_pass = True
535 break
536 something_changed = something_changed or changed_this_pass
537
538 return something_changed
539
540
542 steps_of_sequence: Dict[int, List],
543 edges: List[Tuple[int, int]],
544 attribute_name: str,
545 access_attribute_name: str,
546 use_method_name: str,
547) -> bool:
548 successors_of_sequence: Dict[int, List[int]] = {sequence: [] for sequence in steps_of_sequence}
549 for predecessor, successor in edges:
550 successors_of_sequence[predecessor].append(successor)
551
552 something_changed = False
553 changed_this_pass = True
554 while changed_this_pass:
555 changed_this_pass = False
556 for sequence, steps in steps_of_sequence.items():
557 for index, step in enumerate(steps):
558 access = getattr(step, access_attribute_name)
559 for submodel in list(getattr(step, attribute_name)):
560 if access[submodel] in (Task.DataAccess.WRITE, Task.DataAccess.READ_AND_WRITE):
562 sequence,
563 index,
564 submodel,
565 steps_of_sequence,
566 successors_of_sequence,
567 attribute_name,
568 access_attribute_name,
569 use_method_name,
570 ):
571 changed_this_pass = True
572 something_changed = something_changed or changed_this_pass
573
574 return something_changed
575
576
577def _step_forward(sequence, index, steps_of_sequence, successors_of_sequence):
578 """!
579
580 Yield every (sequence, index) immediately reachable from (sequence,
581 index) - the next step in the same sequence if there is one, or
582 otherwise the first step of every declared successor sequence
583 (several, if a sequence may be followed by more than one other).
584
585 """
586 steps = steps_of_sequence[sequence]
587 if index + 1 < len(steps):
588 yield (sequence, index + 1)
589 else:
590 for successor in successors_of_sequence.get(sequence, []):
591 if steps_of_sequence[successor]:
592 yield (successor, 0)
593
594
596 origin_sequence, origin_index, submodel, attribute_name, access_attribute_name, steps_of_sequence, successors_of_sequence
597):
598 """!
599
600 Breadth-first search forward from (origin_sequence, origin_index),
601 stopping each path the moment it reaches a step that declares a
602 READ_AND_DISCARD, WRITE or READ_AND_WRITE use of submodel (a TEMPORARY
603 declaration is transparent to this search - @see
604 Task.DataAccess.TEMPORARY - so a path continues straight through it,
605 exactly as if it were silent). Returns one (step, distance,
606 one_before_step) tuple per such path (several if the search branches
607 across more than one successor sequence) - one_before_step is the step
608 exactly one before the found one *along that path* (possibly in a
609 different, preceding sequence), i.e. where a READ_AND_WRITE relay has
610 to go if distance turns out to be even.
611
612 """
613 results = []
614 # Frontier entries: (sequence, index, distance, previous_step). Distance
615 # counts steps away from the origin; the origin itself is distance 0
616 # and is never itself reported as a result.
617 frontier = [(origin_sequence, origin_index, 0, None)]
618 # Whether a gap is admissible depends only on the *parity* of its
619 # distance, never its exact length, so a silent (or TEMPORARY-only)
620 # node only ever needs to be expanded once per parity of how far it
621 # was reached - but it does need both: which sequence runs next (e.g.
622 # Plot immediately after Setup, or TimeStep immediately after Setup)
623 # is a runtime decision we cannot see, and different such choices can
624 # genuinely reach the same later step at distances of different parity
625 # (e.g. straight from Setup into TimeStep versus via a Plot detour
626 # first). Deduplicating on the plain (sequence, index) alone would
627 # silently drop whichever parity is not reached first, hiding a real
628 # violation reachable via the other path.
629 visited = set()
630
631 while frontier:
632 sequence, index, distance, previous_step = frontier.pop(0)
633 for next_sequence, next_index in _step_forward(sequence, index, steps_of_sequence, successors_of_sequence):
634 next_distance = distance + 1
635 next_step = steps_of_sequence[next_sequence][next_index]
636 access = getattr(next_step, access_attribute_name)
637 if submodel in access and access[submodel] != Task.DataAccess.TEMPORARY:
638 results.append((next_step, next_distance, previous_step))
639 # This path stops here - do not look past the first real use.
640 else:
641 state = (next_sequence, next_index, next_distance % 2)
642 if state not in visited:
643 visited.add(state)
644 frontier.append((next_sequence, next_index, next_distance, next_step))
645
646 return results
647
648
650 origin_sequence,
651 origin_index,
652 submodel,
653 steps_of_sequence,
654 successors_of_sequence,
655 attribute_name,
656 access_attribute_name,
657 use_method_name,
658) -> bool:
659 changed = False
660 for next_step, distance, previous_step in _find_next_consecutive_uses(
661 origin_sequence, origin_index, submodel, attribute_name, access_attribute_name, steps_of_sequence, successors_of_sequence
662 ):
663 qualifier = getattr(next_step, access_attribute_name)[submodel]
664 if qualifier == Task.DataAccess.WRITE:
665 # A bare WRITE is a fresh, independent production of the
666 # submodel - it does not read what the origin wrote, it simply
667 # supersedes it. There is therefore nothing to protect on the
668 # way there: this path ends here with no constraint at all
669 # (next_step's own forward gap is - separately - checked once
670 # the surrounding loop reaches it as an origin in its own
671 # right, since it is itself a WRITE).
672 continue
673 # Only a READ_AND_DISCARD or READ_AND_WRITE reaches this point
674 # (TEMPORARY is transparent to _find_next_consecutive_uses and
675 # never appears here). Any odd distance - including 1, the
676 # smallest one - is then automatically fine: an even distance is
677 # not, but always fixable by inserting a READ_AND_WRITE relay one
678 # step before it.
679 if distance % 2 == 0:
680 # previous_step always exists here: distance >= 2 means at
681 # least one step lies strictly in between the origin and
682 # next_step, and that is exactly what previous_step is.
683 # A TEMPORARY declaration on previous_step is upgraded, as a
684 # TEMPORARY use does not relay the submodel.
685 if getattr(previous_step, access_attribute_name).get(submodel) is None:
686 getattr(previous_step, use_method_name)(submodel)
687 if getattr(previous_step, access_attribute_name).get(submodel) != Task.DataAccess.READ_AND_WRITE:
688 getattr(previous_step, access_attribute_name)[submodel] = Task.DataAccess.READ_AND_WRITE
689 changed = True
690
691 return changed
Singleton logger for the Peano 4 Python API.
Definition Logger.py:37
bool _fix_forward_gap(origin_sequence, origin_index, submodel, steps_of_sequence, successors_of_sequence, attribute_name, access_attribute_name, use_method_name)
_step_backward(sequence, index, steps_of_sequence, predecessors_of_sequence)
The exact reverse of _step_forward(): yield every (sequence, index) that can immediately reach (seque...
bool _create_submodels_read_before_written(Dict[int, List] steps_of_sequence, List[Tuple[int, int]] edges, str attribute_name, str access_attribute_name, str use_method_name)
Add a WRITE declaration to the last step of an entry sequence for every submodel that can be read bef...
_step_forward(sequence, index, steps_of_sequence, successors_of_sequence)
Yield every (sequence, index) immediately reachable from (sequence, index) - the next step in the sam...
bool _upgrade_discards_followed_by_reads(Dict[int, List] steps_of_sequence, List[Tuple[int, int]] edges, str attribute_name, str access_attribute_name)
Upgrade every READ_AND_DISCARD declaration to READ_AND_WRITE if a later step still reads the submodel...
bool _upgrade_writes_immediately_following_read_and_write(Dict[int, List] steps_of_sequence, List[Tuple[int, int]] edges, str attribute_name, str access_attribute_name)
A plain WRITE is normally treated as a fresh, independent production that supersedes whatever came be...
bool _propagate_use_statements_for_one_kind(Dict[int, List] steps_of_sequence, List[Tuple[int, int]] edges, str attribute_name, str access_attribute_name, str use_method_name)
_find_next_consecutive_uses(origin_sequence, origin_index, submodel, attribute_name, access_attribute_name, steps_of_sequence, successors_of_sequence)
Breadth-first search forward from (origin_sequence, origin_index), stopping each path the moment it r...