Skip to content

Commit e02a56c

Browse files
authored
feat(prt): add developer option for cycle detection (#2435)
Cyclic particle pathlines may be legitimate over multiple time steps, but within a given application of the tracking method i.e. in a single time step they so far reliably indicate a programming error. Add a developer option DEV_CYCLE_DETECTION_WINDOW to PRT PRP enabling cycle detection. Disabled (set to 0) by default. When enabled, look at the last DEV_CYCLE_DETECTION_WINDOW events and terminate the simulation with error if a cycle is found. A cycle is defined as a duplicate cell exit (i.e. through the same face) within a single application of the tracking method. For a speed improvement we could keep a HashTable alongside the list of recent events, where the key is the event string, then just a single lookup instead of iterating over the full window. But since this is meant for infrequent use as a developer debugging tool not a user option, and the window will be small, I will punt performance optimization for now. Consequentially, this PR also makes it the responsibility of the event firer to deallocate the event when it's no longer needed. The event firing routines generally now deallocate the event after dispatch/handling. When cycle detection is off, the cell exit event firing routine does the same. When cycle detection is on, events cannot be deallocated until they are popped from the history, so they are cleaned up at that time by the list node's value destruction mechanism.
1 parent 7bce99e commit e02a56c

File tree

12 files changed

+165
-17
lines changed

12 files changed

+165
-17
lines changed

doc/ReleaseNotes/develop.toml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ description = "Fixed a memory access exception that can occur when writing conve
5151
[[items]]
5252
section = "features"
5353
subsection = "solution"
54-
description = "Added an optional new COORDINATE\\_CHECK\\_METHOD option to the PRT model's Particle Release Package, accepting values 'EAGER' (default) or 'NONE'. 'NONE' disables release-time verification that release point coordinates and release point cell IDs match, which can significantly reduce runtime when releasing many particles."
54+
description = "Added an optional new COORDINATE\\_CHECK\\_METHOD option to the PRT model's Particle Release Package, accepting values EAGER (default) or NONE. NONE disables release-time verification that release point coordinates and release point cell IDs match, which can significantly reduce runtime when releasing many particles."
5555

5656
[[items]]
5757
section = "features"
@@ -72,3 +72,8 @@ description = "Previously the Particle Tracking (PRT) Model's generalized DISV t
7272
section = "fixes"
7373
subsection = "basic"
7474
description = "Previously the NOCHECK option was used to directly set the internal isimcheck variable. When NOCHECK is specified in the simulation name file isimcheck should be set to 0, otherwise isimcheck should be 1. The code has been fixed to implement the intended behaviour."
75+
76+
[[items]]
77+
section = "features"
78+
subsection = "solution"
79+
description = "Added a developer option DEV\\_CYCLE\\_DETECTION\\_WINDOW to the PRT Model's Particle Release (PRP) Package. When enabled, this feature maintains a history of recent cell transitions, the size of which is configurable. If a cycle is detected, the program will terminate with an error. A cycle is identified as a duplicate particle event within a single application of the tracking loop, respective to certain particle and cell properties, e.g., exiting through the same cell face twice. This option defaults to 0, disabling cycle detection by default."

doc/mf6io/mf6ivar/dfn/prt-prp.dfn

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,15 @@ longname coordinate checking method
293293
description approach for verifying that release point coordinates are in the cell with the specified ID. By default, release point coordinates are checked at release time.
294294
default eager
295295

296+
block options
297+
name dev_cycle_detection_window
298+
type integer
299+
reader urword
300+
optional true
301+
mf6internal icycwin
302+
longname cycle detection window size
303+
description integer value defining the size of the window (number of consecutive exit events) used for cycle detection. Defaults to 0, disabling cycle detection. With detection enabled, particle pathlines with duplicate cell exit events (i.e., exiting the same cell through the same face twice) will cause the program to terminate with an error. A larger window size provides more robust cycle detection at the cost of more runtime operations per cell exit.
304+
296305
# --------------------- prt prp dimensions ---------------------
297306

298307
block dimensions

make/makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,6 @@ $(OBJDIR)/ParticleEvent.o \
315315
$(OBJDIR)/sort.o \
316316
$(OBJDIR)/FlowModelInterface.o \
317317
$(OBJDIR)/Cell.o \
318-
$(OBJDIR)/FeatExitEvent.o \
319318
$(OBJDIR)/WeakSinkEvent.o \
320319
$(OBJDIR)/UserTimeEvent.o \
321320
$(OBJDIR)/TimeStepEvent.o \
@@ -325,20 +324,21 @@ $(OBJDIR)/Subcell.o \
325324
$(OBJDIR)/ReleaseEvent.o \
326325
$(OBJDIR)/prt-fmi.o \
327326
$(OBJDIR)/ParticleEvents.o \
328-
$(OBJDIR)/SubcellExitEvent.o \
327+
$(OBJDIR)/FeatExitEvent.o \
329328
$(OBJDIR)/Method.o \
329+
$(OBJDIR)/CellExitEvent.o \
330+
$(OBJDIR)/SubcellExitEvent.o \
331+
$(OBJDIR)/MethodCell.o \
330332
$(OBJDIR)/TernarySolveUtils.o \
331333
$(OBJDIR)/SubcellTri.o \
332334
$(OBJDIR)/MethodSubcell.o \
333335
$(OBJDIR)/SubcellRect.o \
334336
$(OBJDIR)/MethodSubcellTernary.o \
335337
$(OBJDIR)/MethodSubcellPollock.o \
336-
$(OBJDIR)/CellExitEvent.o \
337338
$(OBJDIR)/TimeStepSelect.o \
338339
$(OBJDIR)/LinearAlgebraUtils.o \
339340
$(OBJDIR)/SfrCrossSectionUtils.o \
340341
$(OBJDIR)/MethodSubcellPool.o \
341-
$(OBJDIR)/MethodCell.o \
342342
$(OBJDIR)/CellPoly.o \
343343
$(OBJDIR)/CellRectQuad.o \
344344
$(OBJDIR)/CellRect.o \

src/Idm/prt-prpidm.f90

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ module PrtPrpInputModule
4141
logical :: rttol = .false.
4242
logical :: rtfreq = .false.
4343
logical :: ichkmeth = .false.
44+
logical :: icycwin = .false.
4445
logical :: nreleasepts = .false.
4546
logical :: nreleasetimes = .false.
4647
logical :: irptno = .false.
@@ -589,6 +590,24 @@ module PrtPrpInputModule
589590
.false. & ! timeseries
590591
)
591592

593+
type(InputParamDefinitionType), parameter :: &
594+
prtprp_icycwin = InputParamDefinitionType &
595+
( &
596+
'PRT', & ! component
597+
'PRP', & ! subcomponent
598+
'OPTIONS', & ! block
599+
'DEV_CYCLE_DETECTION_WINDOW', & ! tag name
600+
'ICYCWIN', & ! fortran variable
601+
'INTEGER', & ! type
602+
'', & ! shape
603+
'cycle detection window size', & ! longname
604+
.false., & ! required
605+
.false., & ! multi-record
606+
.false., & ! preserve case
607+
.false., & ! layered
608+
.false. & ! timeseries
609+
)
610+
592611
type(InputParamDefinitionType), parameter :: &
593612
prtprp_nreleasepts = InputParamDefinitionType &
594613
( &
@@ -909,6 +928,7 @@ module PrtPrpInputModule
909928
prtprp_rttol, &
910929
prtprp_rtfreq, &
911930
prtprp_ichkmeth, &
931+
prtprp_icycwin, &
912932
prtprp_nreleasepts, &
913933
prtprp_nreleasetimes, &
914934
prtprp_irptno, &

src/Model/ParticleTracking/prt-prp.f90

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ module PrtPrpModule
5454
integer(I4B), pointer :: ifrctrn => null() !< force ternary solution for quad grids
5555
integer(I4B), pointer :: iexmeth => null() !< method for iterative solution of particle exit location and time in generalized Pollock's method
5656
integer(I4B), pointer :: ichkmeth => null() !< method for checking particle release coordinates are in the specified cells, 0 = none, 1 = eager
57+
integer(I4B), pointer :: icycwin => null() !< cycle detection window size
5758
real(DP), pointer :: extol => null() !< tolerance for iterative solution of particle exit location and time in generalized Pollock's method
5859
real(DP), pointer :: rttol => null() !< tolerance for coincident particle release times
5960
real(DP), pointer :: rtfreq => null() !< frequency for regularly spaced release times
@@ -168,6 +169,7 @@ subroutine prp_da(this)
168169
call mem_deallocate(this%ifrctrn)
169170
call mem_deallocate(this%iexmeth)
170171
call mem_deallocate(this%ichkmeth)
172+
call mem_deallocate(this%icycwin)
171173
call mem_deallocate(this%extol)
172174
call mem_deallocate(this%rttol)
173175
call mem_deallocate(this%rtfreq)
@@ -259,6 +261,7 @@ subroutine prp_allocate_scalars(this)
259261
call mem_allocate(this%ifrctrn, 'IFRCTRN', this%memoryPath)
260262
call mem_allocate(this%iexmeth, 'IEXMETH', this%memoryPath)
261263
call mem_allocate(this%ichkmeth, 'ICHKMETH', this%memoryPath)
264+
call mem_allocate(this%icycwin, 'ICYCWIN', this%memoryPath)
262265
call mem_allocate(this%extol, 'EXTOL', this%memoryPath)
263266
call mem_allocate(this%rttol, 'RTTOL', this%memoryPath)
264267
call mem_allocate(this%rtfreq, 'RTFREQ', this%memoryPath)
@@ -283,6 +286,7 @@ subroutine prp_allocate_scalars(this)
283286
this%ifrctrn = 0
284287
this%iexmeth = 0
285288
this%ichkmeth = 1
289+
this%icycwin = 0
286290
this%extol = DEM5
287291
this%rttol = DSAME * DEP9
288292
this%rtfreq = DZERO
@@ -540,6 +544,7 @@ subroutine initialize_particle(this, particle, ip, trelease)
540544
particle%ifrctrn = this%ifrctrn
541545
particle%iexmeth = this%iexmeth
542546
particle%iextend = this%iextend
547+
particle%icycwin = this%icycwin
543548
particle%extol = this%extol
544549
end subroutine initialize_particle
545550

@@ -712,6 +717,7 @@ subroutine prp_options(this)
712717
found%iexmeth)
713718
call mem_set_value(this%ichkmeth, 'ICHKMETH', this%input_mempath, &
714719
coorcheck_method, found%ichkmeth)
720+
call mem_set_value(this%icycwin, 'ICYCWIN', this%input_mempath, found%icycwin)
715721

716722
! update internal state and validate input
717723
if (found%idrymeth) then
@@ -757,6 +763,11 @@ subroutine prp_options(this)
757763
end if
758764
end if
759765

766+
if (found%icycwin) then
767+
if (this%icycwin < 0) &
768+
call store_error('CYCLE_DETECTION_WINDOW MUST BE NON-NEGATIVE')
769+
end if
770+
760771
! fileout options
761772
if (found%trackfile) then
762773
this%itrkout = getunit()

src/Model/ParticleTracking/prt.f90

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,7 @@ subroutine prt_solve(this)
989989
end do
990990
end select
991991
end do
992+
call particle%destroy()
992993
deallocate (particle)
993994
end subroutine prt_solve
994995

src/Solution/ParticleTracker/Method/Method.f90

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ subroutine release(this, particle)
185185

186186
allocate (ReleaseEventType :: event)
187187
call this%events%dispatch(particle, event)
188+
deallocate (event)
188189
end subroutine release
189190

190191
!> @brief Particle terminates.
@@ -198,6 +199,7 @@ subroutine terminate(this, particle, status)
198199
if (present(status)) particle%istatus = status
199200
allocate (TerminationEventType :: event)
200201
call this%events%dispatch(particle, event)
202+
deallocate (event)
201203
end subroutine terminate
202204

203205
!> @brief Time step ends.
@@ -208,6 +210,7 @@ subroutine timestep(this, particle)
208210

209211
allocate (TimeStepEventType :: event)
210212
call this%events%dispatch(particle, event)
213+
deallocate (event)
211214
end subroutine timestep
212215

213216
!> @brief Particle leaves a weak sink.
@@ -218,6 +221,7 @@ subroutine weaksink(this, particle)
218221

219222
allocate (WeakSinkEventType :: event)
220223
call this%events%dispatch(particle, event)
224+
deallocate (event)
221225
end subroutine weaksink
222226

223227
!> @brief User-defined tracking time occurs.
@@ -228,6 +232,7 @@ subroutine usertime(this, particle)
228232

229233
allocate (UserTimeEventType :: event)
230234
call this%events%dispatch(particle, event)
235+
deallocate (event)
231236
end subroutine usertime
232237

233238
end module MethodModule

src/Solution/ParticleTracker/Method/MethodCell.f90

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
module MethodCellModule
22

33
use KindModule, only: DP, I4B, LGP
4+
use ErrorUtilModule, only: pstop
45
use ConstantsModule, only: DONE, DZERO
56
use MethodModule, only: MethodType
6-
use ParticleModule, only: ParticleType
7+
use ParticleModule, only: ParticleType, TERM_NO_EXITS, TERM_BOUNDARY
78
use ParticleEventModule, only: ParticleEventType
89
use CellExitEventModule, only: CellExitEventType
910
use CellDefnModule, only: CellDefnType
11+
use IteratorModule, only: IteratorType
1012
implicit none
1113

1214
private
@@ -16,6 +18,8 @@ module MethodCellModule
1618
contains
1719
procedure, public :: assess
1820
procedure, public :: cellexit
21+
procedure, public :: forms_cycle
22+
procedure, public :: store_event
1923
end type MethodCellType
2024

2125
contains
@@ -32,8 +36,7 @@ subroutine assess(this, particle, cell_defn, tmax)
3236
use TdisModule, only: endofsimulation, totimc, totim
3337
use ParticleModule, only: TERM_WEAKSINK, TERM_NO_EXITS, &
3438
TERM_STOPZONE, TERM_INACTIVE
35-
use ParticleEventModule, only: FEATEXIT, TERMINATE, &
36-
TIMESTEP, WEAKSINK, USERTIME
39+
use ParticleEventModule, only: TERMINATE, TIMESTEP, WEAKSINK, USERTIME
3740
! dummy
3841
class(MethodCellType), intent(inout) :: this
3942
type(ParticleType), pointer, intent(inout) :: particle
@@ -172,13 +175,87 @@ subroutine cellexit(this, particle)
172175
class(MethodCellType), intent(inout) :: this
173176
type(ParticleType), pointer, intent(inout) :: particle
174177
class(ParticleEventType), pointer :: event
178+
! local
179+
integer(I4B) :: i, nhist
180+
class(*), pointer :: prev
175181

176182
allocate (CellExitEventType :: event)
177183
select type (event)
178184
type is (CellExitEventType)
179185
event%exit_face = particle%iboundary(2)
180186
end select
181187
call this%events%dispatch(particle, event)
188+
if (particle%icycwin == 0) then
189+
deallocate (event)
190+
return
191+
end if
192+
if (this%forms_cycle(particle, event)) then
193+
! print event history
194+
print *, "Cyclic pathline detected"
195+
nhist = particle%history%Count()
196+
do i = 1, nhist
197+
prev => particle%history%GetItem(i)
198+
select type (prev)
199+
class is (ParticleEventType)
200+
print *, "Back ", nhist - i + 1, ": ", prev%get_text()
201+
end select
202+
end do
203+
print *, "Current :", event%get_text()
204+
call pstop(1, 'Cyclic pathline detected, aborting')
205+
else
206+
call this%store_event(particle, event)
207+
end if
182208
end subroutine cellexit
183209

210+
!> @brief Check if the event forms a cycle in the particle path.
211+
function forms_cycle(this, particle, event) result(found_cycle)
212+
! dummy
213+
class(MethodCellType), intent(inout) :: this
214+
type(ParticleType), pointer, intent(inout) :: particle
215+
class(ParticleEventType), pointer, intent(in) :: event
216+
! local
217+
class(IteratorType), allocatable :: itr
218+
logical(LGP) :: found_cycle
219+
220+
found_cycle = .false.
221+
select type (event)
222+
type is (CellExitEventType)
223+
itr = particle%history%Iterator()
224+
do while (itr%has_next())
225+
call itr%next()
226+
select type (prev => itr%value())
227+
class is (CellExitEventType)
228+
if (event%icu == prev%icu .and. &
229+
event%ilay == prev%ilay .and. &
230+
event%izone == prev%izone .and. &
231+
event%exit_face == prev%exit_face .and. &
232+
event%exit_face /= 0) then
233+
found_cycle = .true.
234+
exit
235+
end if
236+
end select
237+
end do
238+
end select
239+
end function forms_cycle
240+
241+
!> @brief Save the event in the particle's history.
242+
!! Acts like a queue, the oldest event is removed
243+
!! when the event count exceeds the maximum size.
244+
subroutine store_event(this, particle, event)
245+
! dummy
246+
class(MethodCellType), intent(inout) :: this
247+
type(ParticleType), pointer, intent(inout) :: particle
248+
class(ParticleEventType), pointer, intent(in) :: event
249+
! local
250+
class(*), pointer :: p
251+
252+
select type (event)
253+
type is (CellExitEventType)
254+
p => event
255+
call particle%history%Add(p)
256+
if (particle%history%Count() > particle%icycwin) &
257+
call particle%history%RemoveNode(1, .true.)
258+
end select
259+
end subroutine store_event
260+
184261
end module MethodCellModule

src/Solution/ParticleTracker/Method/MethodCellPassToBot.f90

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,11 @@ subroutine apply_ptb(this, particle, tmax)
6464
type is (DisvType)
6565
nlay = dis%nlay
6666
end select
67-
if (particle%ilay == nlay) &
67+
if (particle%ilay == nlay) then
6868
call this%terminate(particle, status=TERM_NO_EXITS)
69-
70-
call this%cellexit(particle)
69+
else
70+
call this%cellexit(particle)
71+
end if
7172
end subroutine apply_ptb
7273

7374
end module MethodCellPassToBotModule

src/Solution/ParticleTracker/Method/MethodSubcell.f90

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
module MethodSubcellModule
22
use KindModule, only: DP, I4B
3-
use MethodModule, only: MethodType
3+
use MethodCellModule, only: MethodCellType
44
use ParticleModule, only: ParticleType
55
use CellDefnModule, only: CellDefnType
66
use ParticleEventModule, only: ParticleEventType
7-
use SubcellExitEventModule, only: SubcellExitEventType
7+
use SubCellExitEventModule, only: SubCellExitEventType
88

99
private
1010
public :: MethodSubcellType
1111

12-
type, abstract, extends(MethodType) :: MethodSubcellType
12+
type, abstract, extends(MethodCellType) :: MethodSubcellType
1313
contains
1414
procedure, public :: assess
1515
procedure, public :: subcellexit
@@ -32,9 +32,9 @@ subroutine subcellexit(this, particle)
3232
type(ParticleType), pointer, intent(inout) :: particle
3333
class(ParticleEventType), pointer :: event
3434

35-
allocate (SubcellExitEventType :: event)
35+
allocate (SubCellExitEventType :: event)
3636
select type (event)
37-
type is (SubcellExitEventType)
37+
type is (SubCellExitEventType)
3838
event%isc = particle%idomain(3)
3939
event%exit_face = particle%iboundary(3)
4040
end select

0 commit comments

Comments
 (0)