(git:f2099e5)
Loading...
Searching...
No Matches
realspace_grid_openpmd.F
Go to the documentation of this file.
1!--------------------------------------------------------------------------------------------------!
2! CP2K: A general program to perform molecular dynamics simulations !
3! Copyright 2000-2026 CP2K developers group <https://cp2k.org> !
4! !
5! SPDX-License-Identifier: GPL-2.0-or-later !
6!--------------------------------------------------------------------------------------------------!
7
8! **************************************************************************************************
9!> \brief Generate Gaussian cube files
10! **************************************************************************************************
12
13#ifdef __OPENPMD
14 USE cp_files, ONLY: close_file, &
19 USE kinds, ONLY: default_string_length, &
20 dp
21 USE message_passing, ONLY: &
25 USE physcon, ONLY: a_bohr, e_charge, seconds
26 USE openpmd_api, ONLY: &
27 openpmd_attributable_type, &
28 openpmd_dynamic_memory_view_type_1d, &
29 openpmd_dynamic_memory_view_type_3d, &
30 openpmd_mesh_type, &
31 openpmd_particle_species_type, &
32 openpmd_record_component_type, &
33 openpmd_record_type, openpmd_type_double, openpmd_type_int
35 USE pw_types, ONLY: pw_r3d_rs_type
36 USE util, ONLY: sort_unique
37
38#else
39
40 USE pw_types, ONLY: pw_r3d_rs_type
41 USE kinds, ONLY: dp
42
43#endif
44
45#include "../base/base_uses.f90"
46
47 IMPLICIT NONE
48
49 PRIVATE
50
51 PUBLIC :: pw_to_openpmd
52
53#ifdef __OPENPMD
54 CHARACTER(len=*), PARAMETER, PRIVATE :: moduleN = 'realspace_grid_openpmd'
55 LOGICAL, PARAMETER, PRIVATE :: debug_this_module = .false.
56
57 TYPE cp_openpmd_write_buffer_1d
58 REAL(KIND=dp), DIMENSION(:), POINTER :: buffer => null()
59 END TYPE cp_openpmd_write_buffer_1d
60#endif
61
62CONTAINS
63
64#ifdef __OPENPMD
65! **************************************************************************************************
66!> \brief ...
67!> \param particles_z ...
68!> \param res_atom_types ...
69!> \param res_atom_counts ...
70!> \param res_len ...
71! **************************************************************************************************
72 SUBROUTINE pw_get_atom_types(particles_z, res_atom_types, res_atom_counts, res_len)
73 INTEGER, DIMENSION(:), INTENT(IN) :: particles_z
74 INTEGER, ALLOCATABLE, DIMENSION(:), INTENT(OUT) :: res_atom_types, res_atom_counts
75 INTEGER, INTENT(OUT) :: res_len
76
77 INTEGER :: current_atom_number, i
78 INTEGER, ALLOCATABLE, DIMENSION(:) :: particles_z_sorted
79 LOGICAL :: unique
80
81 ALLOCATE (particles_z_sorted(SIZE(particles_z)))
82 particles_z_sorted(:) = particles_z(:)
83 CALL sort_unique(particles_z_sorted, unique)
84
85 ALLOCATE (res_atom_types(min(118, SIZE(particles_z))))
86 ALLOCATE (res_atom_counts(min(118, SIZE(particles_z))))
87 current_atom_number = -1
88 res_len = 0
89 DO i = 1, SIZE(particles_z_sorted)
90 IF (particles_z_sorted(i) /= current_atom_number) THEN
91 res_len = res_len + 1
92 current_atom_number = particles_z_sorted(i)
93 res_atom_types(res_len) = current_atom_number
94 res_atom_counts(res_len) = 1
95 ELSE
96 res_atom_counts(res_len) = res_atom_counts(res_len) + 1
97 END IF
98 END DO
99
100 END SUBROUTINE pw_get_atom_types
101
102! **************************************************************************************************
103!> \brief ...
104!> \param particles_z ...
105!> \param particles_r ...
106!> \param particles_zeff ...
107!> \param atom_type ...
108!> \param atom_count ...
109!> \param openpmd_data ...
110!> \param do_write_data ...
111! **************************************************************************************************
112 SUBROUTINE pw_write_particle_species( &
113 particles_z, &
114 particles_r, &
115 particles_zeff, &
116 atom_type, &
117 atom_count, &
118 openpmd_data, &
119 do_write_data &
120 )
121 INTEGER, DIMENSION(:), INTENT(IN) :: particles_z
122 REAL(KIND=dp), DIMENSION(:, :), INTENT(IN) :: particles_r
123 REAL(KIND=dp), DIMENSION(:), INTENT(IN), OPTIONAL :: particles_zeff
124 INTEGER, INTENT(IN) :: atom_type, atom_count
125 TYPE(cp_openpmd_per_call_value_type) :: openpmd_data
126 LOGICAL :: do_write_data
127
128 CHARACTER(len=1), DIMENSION(3), PARAMETER :: dims = ["x", "y", "z"]
129
130 CHARACTER(len=3) :: atom_type_as_string
131 CHARACTER(len=default_string_length) :: species_name
132 INTEGER :: i, j, k
133 INTEGER, DIMENSION(1) :: global_extent, global_offset, &
134 local_extent
135 TYPE(cp_openpmd_write_buffer_1d) :: charge_write_buffer
136 TYPE(cp_openpmd_write_buffer_1d), DIMENSION(3) :: write_buffers
137 TYPE(openpmd_attributable_type) :: attr
138 TYPE(openpmd_dynamic_memory_view_type_1d) :: unresolved_charge_write_buffer
139 TYPE(openpmd_dynamic_memory_view_type_1d), &
140 DIMENSION(3) :: unresolved_write_buffers
141 TYPE(openpmd_particle_species_type) :: species
142 TYPE(openpmd_record_component_type) :: charge_component, position_component, &
143 position_offset_component
144 TYPE(openpmd_record_type) :: charge, position, position_offset
145
146! TODO: The charge is probably constant per species?
147! If yes, we could use a constant component and save storage space
148
149 global_extent(1) = atom_count
150 IF (do_write_data) THEN
151 global_offset(1) = 0
152 local_extent(1) = atom_count
153 ELSE
154 global_offset(1) = 0
155 local_extent(1) = 0
156 END IF
157
158 WRITE (atom_type_as_string, '(I3)') atom_type
159 species_name = trim(openpmd_data%name_prefix)//"-"//adjustl(atom_type_as_string)
160
161 CALL openpmd_data%iteration%open()
162 species = openpmd_data%iteration%get_particle_species(trim(species_name))
163
164 position_offset = species%get_record("positionOffset")
165 position = species%get_record("position")
166 ! length
167 CALL position%set_unit_dimension([1.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp, 0.0_dp])
168 DO k = 1, SIZE(dims)
169 position_offset_component = position_offset%get_component(dims(k))
170 CALL position_offset_component%make_constant_zero(openpmd_type_int, global_extent)
171 CALL position_offset_component%set_unit_SI(a_bohr) ! doesnt really matter as it is zero
172 position_component = position%get_component(dims(k))
173 CALL position_component%reset_dataset(openpmd_type_double, global_extent)
174 CALL position_component%set_unit_SI(a_bohr)
175 unresolved_write_buffers(k) = &
176 position_component%store_chunk_span_1d_double(global_offset, local_extent)
177 write_buffers(k)%buffer => unresolved_write_buffers(k)%resolve_double(deallocate=.false.)
178 END DO
179
180 IF (PRESENT(particles_zeff)) THEN
181 charge = species%get_record("charge")
182 charge_component = charge%as_record_component()
183 ! charge ~ time x current
184 CALL charge%set_unit_dimension([0.0_dp, 0.0_dp, 1.0_dp, 1.0_dp, 0.0_dp, 0.0_dp, 0.0_dp])
185 CALL charge_component%reset_dataset(openpmd_type_double, global_extent)
186 CALL charge_component%set_unit_SI(e_charge)
187 unresolved_charge_write_buffer = charge_component%store_chunk_span_1d_double(global_offset, local_extent)
188 charge_write_buffer%buffer => unresolved_charge_write_buffer%resolve_double(deallocate=.false.)
189 END IF
190
191 ! Resolve Spans for a second time to allow for internal reallocations in BP4 engine of ADIOS2
192 DO k = 1, SIZE(dims)
193 write_buffers(k)%buffer = unresolved_write_buffers(k)%resolve_double(deallocate=.true.)
194 END DO
195 IF (PRESENT(particles_zeff)) THEN
196 charge_write_buffer%buffer = unresolved_charge_write_buffer%resolve_double(deallocate=.true.)
197 END IF
198 IF (do_write_data) THEN
199 j = 1
200 DO i = 1, SIZE(particles_z)
201 IF (particles_z(i) == atom_type) THEN
202 DO k = 1, 3
203 write_buffers(k)%buffer(j) = particles_r(k, i)
204 END DO
205 IF (PRESENT(particles_zeff)) THEN
206 charge_write_buffer%buffer(j) = particles_zeff(i)
207 END IF
208 j = j + 1
209 END IF
210 END DO
211 END IF
212 attr = openpmd_data%iteration%as_attributable()
213 CALL attr%series_flush("hdf5.independent_stores = true")
214 END SUBROUTINE pw_write_particle_species
215
216! **************************************************************************************************
217!> \brief ...
218!> \param particles_z ...
219!> \param particles_r ...
220!> \param particles_zeff ...
221!> \param atom_types ...
222!> \param atom_counts ...
223!> \param num_atom_types ...
224!> \param openpmd_data ...
225!> \param gid ...
226! **************************************************************************************************
227 SUBROUTINE pw_write_particles( &
228 particles_z, &
229 particles_r, &
230 particles_zeff, &
231 atom_types, &
232 atom_counts, &
233 num_atom_types, &
234 openpmd_data, &
235 gid &
236 )
237 INTEGER, DIMENSION(:), INTENT(IN) :: particles_z
238 REAL(KIND=dp), DIMENSION(:, :), INTENT(IN) :: particles_r
239 REAL(KIND=dp), DIMENSION(:), INTENT(IN), OPTIONAL :: particles_zeff
240 INTEGER, DIMENSION(:), INTENT(IN) :: atom_types, atom_counts
241 INTEGER, INTENT(IN), TARGET :: num_atom_types
242 TYPE(cp_openpmd_per_call_value_type) :: openpmd_data
243 TYPE(mp_comm_type), OPTIONAL :: gid
244
245 INTEGER :: i, mpi_rank
246 LOGICAL :: do_write_data
247
248 IF (PRESENT(gid)) THEN
249 CALL gid%get_rank(mpi_rank)
250 do_write_data = mpi_rank == 0
251 ELSE
252 do_write_data = .true.
253 END IF
254 DO i = 1, num_atom_types
255 CALL pw_write_particle_species( &
256 particles_z, &
257 particles_r, &
258 particles_zeff, &
259 atom_types(i), &
260 atom_counts(i), &
261 openpmd_data, &
262 do_write_data &
263 )
264 END DO
265 END SUBROUTINE pw_write_particles
266
267! **************************************************************************************************
268!> \brief ...
269!> \param pw ...
270!> \param unit_nr ...
271!> \param title ...
272!> \param particles_r ...
273!> \param particles_z ...
274!> \param particles_zeff ...
275!> \param stride ...
276!> \param zero_tails ...
277!> \param silent ...
278!> \param mpi_io ...
279! **************************************************************************************************
280 SUBROUTINE pw_to_openpmd( &
281 pw, &
282 unit_nr, &
283 title, &
284 particles_r, &
285 particles_z, &
286 particles_zeff, &
287 stride, &
288 zero_tails, &
289 silent, &
290 mpi_io &
291 )
292 TYPE(pw_r3d_rs_type), INTENT(IN) :: pw
293 INTEGER :: unit_nr
294 CHARACTER(*), INTENT(IN), OPTIONAL :: title
295 REAL(KIND=dp), DIMENSION(:, :), INTENT(IN), &
296 OPTIONAL :: particles_r
297 INTEGER, DIMENSION(:), INTENT(IN), OPTIONAL :: particles_z
298 REAL(KIND=dp), DIMENSION(:), INTENT(IN), OPTIONAL :: particles_zeff
299 INTEGER, DIMENSION(:), OPTIONAL, POINTER :: stride
300 LOGICAL, INTENT(IN), OPTIONAL :: zero_tails, silent, mpi_io
301
302 CHARACTER(len=*), PARAMETER :: routineN = 'pw_to_openpmd'
303
304 CHARACTER(LEN=default_string_length) :: my_title
305 INTEGER :: count1, count2, count3, handle, i, I1, &
306 I2, I3, iat, L1, L2, L3, my_rank, &
307 my_stride(3), np, num_atom_types, &
308 num_pe, U1, U2, U3
309 INTEGER, ALLOCATABLE, DIMENSION(:) :: atom_counts, atom_types
310 INTEGER, DIMENSION(3) :: global_extent, local_extent, offset
311 LOGICAL :: be_silent, my_zero_tails, parallel_write
312 REAL(KIND=dp), DIMENSION(3) :: grid_spacing
313 REAL(KIND=dp), POINTER :: write_buffer(:, :, :)
314 TYPE(cp_openpmd_per_call_value_type) :: openpmd_data
315 TYPE(mp_comm_type) :: gid
316 TYPE(openpmd_attributable_type) :: attr
317 TYPE(openpmd_dynamic_memory_view_type_3d) :: unresolved_write_buffer
318 TYPE(openpmd_mesh_type) :: mesh
319 TYPE(openpmd_record_component_type) :: scalar_mesh
320
321 CALL timeset(routinen, handle)
322
323 my_zero_tails = .false.
324 be_silent = .false.
325 parallel_write = .false.
326 gid = pw%pw_grid%para%group
327 IF (PRESENT(title)) my_title = trim(title)
328 IF (PRESENT(zero_tails)) my_zero_tails = zero_tails
329 IF (PRESENT(silent)) be_silent = silent
330 IF (PRESENT(mpi_io)) parallel_write = mpi_io
331 my_stride = 1
332 IF (PRESENT(stride)) THEN
333 IF (SIZE(stride) /= 1 .AND. SIZE(stride) /= 3) THEN
334 CALL cp_abort(__location__, "STRIDE keyword can accept only 1 "// &
335 "(the same for X,Y,Z) or 3 values. Correct your input file.")
336 END IF
337 IF (SIZE(stride) == 1) THEN
338 DO i = 1, 3
339 my_stride(i) = stride(1)
340 END DO
341 ELSE
342 my_stride = stride(1:3)
343 END IF
344 cpassert(my_stride(1) > 0)
345 cpassert(my_stride(2) > 0)
346 cpassert(my_stride(3) > 0)
347 END IF
348
349 openpmd_data = cp_openpmd_get_value_unit_nr(unit_nr)
350
351 cpassert(PRESENT(particles_z) .EQV. PRESENT(particles_r))
352 np = 0
353 IF (PRESENT(particles_z)) THEN
354 CALL pw_get_atom_types(particles_z, atom_types, atom_counts, num_atom_types)
355 cpassert(SIZE(particles_z) == SIZE(particles_r, dim=2))
356 np = SIZE(particles_z)
357 END IF
358
359 DO i = 1, 3
360 ! Notes:
361 ! 1. This loses information on the rotation of the mesh, the mesh is stored
362 ! without reference to a global coordinate system
363 ! 2. This assumes that the coordinate system is not sheared
364 grid_spacing(i) = sqrt(sum(pw%pw_grid%dh(:, i)**2))*real(my_stride(i), dp)
365 END DO
366
367 IF (PRESENT(particles_z)) THEN
368 IF (parallel_write) THEN
369 CALL pw_write_particles( &
370 particles_z, &
371 particles_r, &
372 particles_zeff, &
373 atom_types, &
374 atom_counts, &
375 num_atom_types, &
376 openpmd_data, &
377 gid &
378 )
379 ELSE
380 CALL pw_write_particles( &
381 particles_z, &
382 particles_r, &
383 particles_zeff, &
384 atom_types, &
385 atom_counts, &
386 num_atom_types, &
387 openpmd_data &
388 )
389 END IF
390 END IF
391
392 DO iat = 1, 3
393 global_extent(iat) = (pw%pw_grid%npts(iat) + my_stride(iat) - 1)/my_stride(iat)
394 ! '- 1' for upper gaussian bracket
395 offset(iat) = ((pw%pw_grid%bounds_local(1, iat) - pw%pw_grid%bounds(1, iat) + my_stride(iat) - 1)/my_stride(iat))
396 ! '+ 1' because upper end is inclusive, '- 1' for upper gaussian bracket
397 ! refer local_extent to the global offset first in order to have consistent rounding
398 local_extent(iat) = ((pw%pw_grid%bounds_local(2, iat) + 1 - pw%pw_grid%bounds(1, iat) + my_stride(iat) - 1)/my_stride(iat))
399 END DO
400 local_extent = local_extent - offset
401
402 mesh = openpmd_data%iteration%get_mesh(trim(openpmd_data%name_prefix))
403 CALL mesh%set_axis_labels(["x", "y", "z"])
404 CALL mesh%set_position([0.5_dp, 0.5_dp, 0.5_dp])
405 CALL mesh%set_grid_global_offset([ &
406 pw%pw_grid%bounds(1, 1)*grid_spacing(1), &
407 pw%pw_grid%bounds(1, 2)*grid_spacing(2), &
408 pw%pw_grid%bounds(1, 3)*grid_spacing(3)])
409 CALL mesh%set_grid_spacing(grid_spacing)
410 CALL mesh%set_grid_unit_SI(a_bohr)
411 CALL mesh%set_unit_dimension(openpmd_data%unit_dimension)
412 scalar_mesh = mesh%as_record_component()
413 CALL scalar_mesh%set_unit_SI(openpmd_data%unit_si)
414 CALL scalar_mesh%reset_dataset(openpmd_type_double, global_extent)
415
416 ! shortcut
417 ! need to adjust L1/U1 for uneven distributions across MPI ranks
418 ! (when working with a stride, we might have to skip the first n values)
419 ! so keep this consistent with the offset and local_extent computed above
420 ! L1 = pw%pw_grid%bounds_local(1, 1)
421 l1 = pw%pw_grid%bounds(1, 1) + offset(1)*my_stride(1)
422 l2 = pw%pw_grid%bounds_local(1, 2)
423 l3 = pw%pw_grid%bounds_local(1, 3)
424 ! offset + local_extent is the start index for the next rank already
425 ! since the indexes are inclusive, subtract 1 from the boundary index
426 u1 = pw%pw_grid%bounds(1, 1) + (offset(1) + local_extent(1) - 1)*my_stride(1)
427 u2 = pw%pw_grid%bounds_local(2, 2)
428 u3 = pw%pw_grid%bounds_local(2, 3)
429
430 my_rank = pw%pw_grid%para%group%mepos
431 num_pe = pw%pw_grid%para%group%num_pe
432
433 IF (all(my_stride == 1)) THEN
434 CALL scalar_mesh%store_chunk(pw%array(l1:u1, l2:u2, l3:u3), offset)
435 ! Are there some conditions under which we can skip this flush?
436 attr = openpmd_data%iteration%as_attributable()
437 CALL attr%series_flush("hdf5.independent_stores = false")
438 ELSE
439 count3 = 0
440 DO i3 = l3, u3, my_stride(3)
441 ! maybe add an overload to provide `buf` here for HDF5, might have better performance
442 ! for intermittent flushing
443 ! or just call the buffer in the outer function if memory is no problem...
444 unresolved_write_buffer = scalar_mesh%store_chunk_span_3d_double( &
445 [offset(1), offset(2), offset(3) + count3], &
446 [local_extent(1), local_extent(2), 1])
447 write_buffer => unresolved_write_buffer%resolve_double(deallocate=.true.)
448
449 ! Sanity checks: ensure buffer is associated and matches expected shape
450 cpassert(ASSOCIATED(write_buffer))
451 cpassert(SIZE(write_buffer, 1) == local_extent(1))
452 cpassert(SIZE(write_buffer, 2) == local_extent(2))
453 cpassert(SIZE(write_buffer, 3) == 1)
454
455 count2 = 0
456 DO i2 = l2, u2, my_stride(2)
457 ! This loop deals with ray (:, count2, count3) of the local subspace
458 ! The write buffer itself has been allocated for slice (:, :, count3)
459 count1 = 0
460 DO i1 = l1, u1, my_stride(1)
461 write_buffer(count1 + 1, count2 + 1, 1) = pw%array(i1, i2, i3)
462 ! Debug: print the target indices in write_buffer to the command line
463 ! WRITE(*,*) 'write_buffer index:', count1 + 1, ',', count2 + 1, ',', 1
464 count1 = count1 + 1
465 END DO
466 count2 = count2 + 1
467 END DO
468 count3 = count3 + 1
469 END DO
470 END IF
471
472 CALL timestop(handle)
473
474 END SUBROUTINE pw_to_openpmd
475
476#else
477
478! **************************************************************************************************
479!> \brief ...
480!> \param pw ...
481!> \param unit_nr ...
482!> \param title ...
483!> \param particles_r ...
484!> \param particles_z ...
485!> \param particles_zeff ...
486!> \param stride ...
487!> \param zero_tails ...
488!> \param silent ...
489!> \param mpi_io ...
490! **************************************************************************************************
491 SUBROUTINE pw_to_openpmd( &
492 pw, &
493 unit_nr, &
494 title, &
495 particles_r, &
496 particles_z, &
497 particles_zeff, &
498 stride, &
499 zero_tails, &
500 silent, &
501 mpi_io &
502 )
503 TYPE(pw_r3d_rs_type), INTENT(IN) :: pw
504 INTEGER :: unit_nr
505 CHARACTER(*), INTENT(IN), OPTIONAL :: title
506 REAL(kind=dp), DIMENSION(:, :), INTENT(IN), &
507 OPTIONAL :: particles_r
508 INTEGER, DIMENSION(:), INTENT(IN), OPTIONAL :: particles_z
509 REAL(kind=dp), DIMENSION(:), INTENT(IN), OPTIONAL :: particles_zeff
510 INTEGER, DIMENSION(:), OPTIONAL, POINTER :: stride
511 LOGICAL, INTENT(IN), OPTIONAL :: zero_tails, silent, mpi_io
512
513 mark_used(pw)
514 mark_used(unit_nr)
515 mark_used(title)
516 mark_used(particles_r)
517 mark_used(particles_z)
518 mark_used(particles_zeff)
519 mark_used(stride)
520 mark_used(zero_tails)
521 mark_used(silent)
522 mark_used(mpi_io)
523 cpabort("CP2K compiled without the openPMD-api")
524
525 END SUBROUTINE pw_to_openpmd
526
527#endif
528
529END MODULE realspace_grid_openpmd
Define the atom type and its sub types.
Definition atom_types.F:15
Utility routines to open and close files. Tracking of preconnections.
Definition cp_files.F:16
subroutine, public open_file(file_name, file_status, file_form, file_action, file_position, file_pad, unit_number, debug, skip_get_unit_number, file_access)
Opens the requested file using a free unit number.
Definition cp_files.F:323
subroutine, public close_file(unit_number, file_status, keep_preconnection)
Close an open file given by its logical unit number. Optionally, keep the file and unit preconnected.
Definition cp_files.F:123
various routines to log and control the output. The idea is that decisions about where to log should ...
integer function, public cp_logger_get_default_io_unit(logger)
returns the unit nr for the ionode (-1 on all other processors) skips as well checks if the procs cal...
routines to handle the output, The idea is to remove the decision of wheter to output and what to out...
type(cp_openpmd_per_call_value_type) function, public cp_openpmd_get_value_unit_nr(key)
Defines the basic variable types.
Definition kinds.F:23
integer, parameter, public dp
Definition kinds.F:34
integer, parameter, public default_string_length
Definition kinds.F:57
Interface to the message passing library MPI.
type(mp_file_descriptor_type) function, public mp_file_type_hindexed_make_chv(count, lengths, displs)
Creates an indexed MPI type for arrays of strings using bytes for spacing (hindexed type).
subroutine, public mp_file_type_free(type_descriptor)
Releases the type used for MPI I/O.
integer, parameter, public mpi_character_size
integer, parameter, public file_offset
integer, parameter, public file_amode_rdonly
subroutine, public mp_file_type_set_view_chv(fh, offset, type_descriptor)
Uses a previously created indexed MPI character type to tell the MPI processes how to partition (set_...
Definition of physical constants:
Definition physcon.F:68
real(kind=dp), parameter, public a_bohr
Definition physcon.F:136
real(kind=dp), parameter, public e_charge
Definition physcon.F:106
real(kind=dp), parameter, public seconds
Definition physcon.F:150
integer, parameter, public pw_mode_local
Generate Gaussian cube files.
subroutine, public pw_to_openpmd(pw, unit_nr, title, particles_r, particles_z, particles_zeff, stride, zero_tails, silent, mpi_io)
...
All kind of helpful little routines.
Definition util.F:14