source: trunk/LMDZ.COMMON/libf/evolution/output.F90

Last change on this file was 4065, checked in by jbclement, 2 weeks ago

PEM:
Major refactor following the previous ones (r3989 and r3991) completing the large structural reorganization and cleanup of the PEM codebase. This revision introduces newly designed modules, standardizes interfaces with explicit ini/end APIs and adds native NetCDF I/O together with explicit PCM/PEM adapters. In detail:

  • Some PEM models were corrected or improved:
    • Frost/perennial ice semantics are clarified via renaming;
    • Soil temperature remapping clarified, notably by removing the rescaling of temperature deviation;
    • Geothermal flux for the PCM is computed based on the PEM state;
  • New explicit PEM/PCM adapters ("set_*"/"build4PCM_*") to decouple PEM internal representation from PCM file layouts and reconstruct consistent fields returned to the PCM;
  • New explicit build/teardown routines that centralize allocation and initialization ordering, reducing accidental use of uninitialized data and making the model lifecycle explicit;
  • Add native read/write helpers for NetCDF that centralize all low-level NetCDF interactions with major improvements (and more simplicity) compared to legacy PEM/PCM I/O (see the modules "io_netcdf" and "output"). They support reading, creation and writing of "diagevol.nc" (renamed from "diagpem.nc") and start/restart files;
  • Provide well-focused modules ("numerics"/"maths"/"utility"/"display") to host commonly-used primitives:
    • "numerics" defines numerical types and constants for reproducibility, portability across compilers and future transitions (e.g. quadruple precision experiments);
    • "display" provides a single controlled interface for runtime messages, status output and diagnostics, avoiding direct 'print'/'write' to enable silent mode, log redirection, and MPI-safe output in the future.
    • "utility" (new module) hosts generic helpers used throughout the code (e.g. "int2str" or "real2str");
  • Add modules "clim_state_init"/"clim_state_rec" which provide robust read/write logic for "start/startfi/startpem", including 1D fallbacks, mesh conversions and dimension checks. NetCDF file creation is centralized and explicit. Restart files are now self-consistent and future-proof, requiring changes only to affected variables;
  • Add module "atmosphere" which computes pressure fields, reconstructs potential temperature and air mass. It also holds the whole logic to define sigma or hybrid coordinates for altitudes;
  • Add module "geometry" to centrilize dimensions logic and grid conversions routines (including 2 new ones "dyngrd2vect"/"vect2dyngrd");
  • Add module "slopes" to isolate slopes handling;
  • Add module "surface" to isolate surface management. Notably, albedo and emissivity are now fully reconstructed following the PCM settings;
  • Add module "allocation" to check the dimension initialization and centrilize allocation/deallocation;
  • Finalize module decomposition and renaming to consolidate domain-based modules, purpose-based routines and physics/process-based variables;
  • The main program now drives a clearer sequence of conceptual steps (initialization / reading / evolution / update / build / writing) and fails explicitly instead of silently defaulting;
  • Ice table logic is made restart-safe;
  • 'Open'/'read' intrinsic logic is made safe and automatic;
  • Improve discoverability and standardize the data handling (private vs protected vs public);
  • Apply consistent documentation/header style already introduced;
  • Update deftank scripts to reflect new names and launch wrappers;

This revision is a structural milestone aiming to be behavior-preserving where possible. It has been tested via compilation and short integration runs. However, due to extensive renames, moves, and API changes, full validation is still ongoing.
Note: the revision includes one (possibly two) easter egg hidden in the code for future archaeologists of the PEM. No physics were harmed.
JBC

File size: 19.0 KB
Line 
1MODULE output
2!-----------------------------------------------------------------------
3! NAME
4!     output
5!
6! DESCRIPTION
7!     Tools to write PEM diagnostic outputs.
8!
9! AUTHORS & DATE
10!     JB Clement, 12/2025
11!
12! NOTES
13!
14!-----------------------------------------------------------------------
15
16! DEPENDENCIES
17! ------------
18use numerics, only: dp, di, k4, minieps
19
20! DECLARATION
21! -----------
22implicit none
23
24! PARAMETERS
25! ----------
26character(12),                             parameter :: diagdef_name = 'diagevol.def'
27integer(di),                               parameter :: dim_ngrid = -1        ! Dimension ID to identify 'ngrid' for "diagevol.nc" logic
28logical(k4),                               protected :: is_diagdef = .false.  ! Flag to know if "diagevol.def" exists
29logical(k4),                               protected :: is_diagevol = .false. ! Flag to know if "diagevol.nc" exists
30character(256), dimension(:), allocatable, protected :: outputs_name          ! Names of output variables defined in "diagevol.def"
31integer(di),                               protected :: n_outputs             ! Number of output variables defined in "diagevol.def"
32integer(di),                               protected :: output_rate           ! Output rate
33
34! VARIABLES
35! ---------
36integer(di) :: writing_idt = -1                                                  ! Current writing number of timestep
37integer(di) :: itime                                                             ! Current time index to record variables
38integer(di) :: dim_nlon, dim_nlat, dim_nalt, dim_interlayer, dim_nsoil, dim_time ! Dimensions ID to write in "diagevol.nc"
39
40contains
41!+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
42
43!=======================================================================
44SUBROUTINE ini_output()
45!-----------------------------------------------------------------------
46! NAME
47!     ini_output
48!
49! DESCRIPTION
50!     Initialize the parameters of module 'output'.
51!
52! AUTHORS & DATE
53!     JB Clement, 12/2025
54!
55! NOTES
56!     At first call.
57!-----------------------------------------------------------------------
58
59! DECLARATION
60! -----------
61implicit none
62
63! CODE
64! ----
65n_outputs = 0
66if (.not. allocated(outputs_name)) allocate(outputs_name(0))
67call read_diagdef()
68
69END SUBROUTINE ini_output
70!=======================================================================
71
72!=======================================================================
73SUBROUTINE end_output()
74!-----------------------------------------------------------------------
75! NAME
76!     end_output
77!
78! DESCRIPTION
79!     Deallocate output arrays.
80!
81! AUTHORS & DATE
82!     JB Clement, 01/2026
83!
84! NOTES
85!
86!-----------------------------------------------------------------------
87
88! DECLARATION
89! -----------
90implicit none
91
92! CODE
93! ----
94if (allocated(outputs_name)) deallocate(outputs_name)
95
96END SUBROUTINE end_output
97!=======================================================================
98
99!=======================================================================
100SUBROUTINE set_output_config(output_rate_in)
101!-----------------------------------------------------------------------
102! NAME
103!     set_output_config
104!
105! DESCRIPTION
106!     Setter for 'output' configuration parameters.
107!
108! AUTHORS & DATE
109!     JB Clement, 02/2026
110!
111! NOTES
112!
113!-----------------------------------------------------------------------
114
115! DEPENDENCIES
116! ------------
117use utility, only: int2str
118use display, only: print_msg
119
120! DECLARATION
121! -----------
122implicit none
123
124! ARGUMENTS
125! ---------
126integer(di), intent(in) :: output_rate_in
127
128! CODE
129! ----
130output_rate = output_rate_in
131call print_msg('output_rate = '//int2str(output_rate))
132
133END SUBROUTINE set_output_config
134!=======================================================================
135
136!=======================================================================
137SUBROUTINE append_name(var_name)
138!-----------------------------------------------------------------------
139! NAME
140!     append_name
141!
142! DESCRIPTION
143!     Append the variable name read in "diagevol.def" to 'outputs_name'.
144!
145! AUTHORS & DATE
146!     JB Clement, 01/2026
147!
148! NOTES
149!
150!-----------------------------------------------------------------------
151
152! DECLARATION
153! -----------
154implicit none
155
156! ARGUMENTS
157! ---------
158character(*), intent(in) :: var_name
159
160! LOCAL VARIABLES
161! ---------------
162character(256), dimension(:), allocatable :: tmp
163
164! CODE
165! ----
166allocate(tmp(n_outputs + 1))
167if (n_outputs > 0) tmp(1:n_outputs) = outputs_name
168tmp(n_outputs + 1) = var_name
169
170call move_alloc(tmp,outputs_name)
171n_outputs = n_outputs + 1
172
173END SUBROUTINE append_name
174!=======================================================================
175
176!=======================================================================
177SUBROUTINE read_diagdef()
178!-----------------------------------------------------------------------
179! NAME
180!     read_diagdef
181!
182! DESCRIPTION
183!     Read the variables names defined in "diagevol.def" to ouput.
184!
185! AUTHORS & DATE
186!     JB Clement, 01/2026
187!
188! NOTES
189!     > If no "diagevol.def", then no output.
190!     > If "diagevol.def" is empty, then all the possible coded outputs.
191!     > If "diagevol.def" holds a list of variable names, then these are
192!     outputted.
193!-----------------------------------------------------------------------
194
195! DEPENDENCIES
196! ------------
197use stoppage, only: stop_clean
198use utility,  only: is_id_1st_char, is_id_char, int2str
199use display, only: print_msg
200
201! DECLARATION
202! -----------
203implicit none
204
205! LOCAL VARIABLES
206! ---------------
207integer(di)    :: i, iline, ierr, funit
208character(256) :: line
209
210! CODE
211! ----
212inquire(file = diagdef_name,exist = is_diagdef)
213if (is_diagdef) then
214    call print_msg('> Reading "'//diagdef_name//'"')
215    open(newunit = funit,file = diagdef_name,status = 'old',form = 'formatted',action = 'read',iostat = ierr)
216    if (ierr /= 0) call stop_clean(__FILE__,__LINE__,'error opening file "'//diagdef_name//'"!',ierr)
217    iline = 1
218    do
219        ! Read the line
220        read(funit,'(a)',iostat = ierr) line
221        if (ierr /= 0) exit ! Empty file or end of file
222        iline = iline + 1
223
224        ! Examine the line
225        line = trim(line)
226        if (len(line) == 0) cycle ! Ignore empty lines
227        if (.not. is_id_1st_char(line(1:1))) call print_msg('Invalid output name at line '//int2str(iline)//' in '//diagdef_name//'"!')
228        do i = 2,len(line)
229            if (.not. is_id_char(line(i:i))) call print_msg('Invalid output name at line '//int2str(iline)//' in '//diagdef_name//'"!')
230        end do
231
232        ! Fill in the table 'outputs_name'
233        call append_name(line)
234    end do
235    close(funit)
236    if (n_outputs == 0) call print_msg('There is a "'//diagdef_name//'" which is is empty. So all the possible coded variables are outputted.')
237    ! Create the output file
238    call create_diagevol()
239else
240    call print_msg('There is no "'//diagdef_name//'". So no variable is outputted.')
241end if
242
243END SUBROUTINE read_diagdef
244!=======================================================================
245
246!=======================================================================
247FUNCTION is_output_selected(var_name) RESULT(output_var)
248!-----------------------------------------------------------------------
249! NAME
250!     is_output_selected
251!
252! DESCRIPTION
253!     Read the variables names to output defined in "diagevol.def".
254!
255! AUTHORS & DATE
256!     JB Clement, 01/2026
257!
258! NOTES
259!     > If no "diagevol.def", then no output.
260!     > If "diagevol.def" is empty, then all the possible coded outputs.
261!     > If "diagevol.def" holds a list of variable names, then these are
262!     outputted.
263!-----------------------------------------------------------------------
264
265! DECLARATION
266! -----------
267implicit none
268
269! ARGUMENTS
270! ---------
271character(*), intent(in) :: var_name
272logical(k4)              :: output_var
273
274! LOCAL VARIABLES
275! ---------------
276integer(di) :: i
277
278! CODE
279! ----
280output_var = .false.
281
282! If diagdef exists but is empty, output everything
283if (n_outputs == 0) then
284    output_var = .true.
285    return
286end if
287
288do i = 1,n_outputs
289    if (trim(outputs_name(i)) == trim(var_name)) then
290        output_var = .true.
291        return
292    end if
293end do
294
295END FUNCTION is_output_selected
296!=======================================================================
297
298!=======================================================================
299SUBROUTINE create_diagevol()
300!-----------------------------------------------------------------------
301! NAME
302!     create_diagevol
303!
304! DESCRIPTION
305!     Create a NetCDF file "diagevol.nc" containing the diagnostic
306!     variables.
307!
308! AUTHORS & DATE
309!     JB Clement, 01/2026
310!
311! NOTES
312!
313!-----------------------------------------------------------------------
314
315! DEPENDENCIES
316! ------------
317use netcdf,     only: nf90_double, nf90_clobber, nf90_unlimited, nf90_global, &
318                      nf90_create, nf90_enddef, nf90_def_dim, nf90_def_var, nf90_put_att
319use io_netcdf,  only: check_nc, open_nc, close_nc, put_var_nc, diagevol_name, ncid_diagevol
320use geometry,   only: dim_init, ngrid, nlon, nlat, nlayer, nsoil, cell_area, longitudes, latitudes, vect2dyngrd
321use stoppage,   only: stop_clean
322use soil,       only: mlayer
323use atmosphere, only: ap, bp
324use display,    only: print_msg
325
326! DECLARATION
327! -----------
328implicit none
329
330! LOCAL VARIABLES
331! ---------------
332integer(di)                           :: varid ! Variable ID
333integer(di)                           :: nlon_loc
334real(dp), dimension(:,:), allocatable :: var_dyn
335
336! CODE
337! ----
338! Check if dimensions are well initialized
339if (.not. dim_init) call stop_clean(__FILE__,__LINE__,'dimensions are not initilized in the PEM!',1)
340if (ngrid == 1) then ! 1D
341    nlon_loc = 1
342else ! 3D
343    nlon_loc = nlon + 1
344end if
345
346! Create file
347call print_msg('> Creating "'//diagevol_name//'"')
348call check_nc(nf90_create(diagevol_name,nf90_clobber,ncid_diagevol),'creating '//diagevol_name) ! Enter define mode
349
350! Define dimensions
351call check_nc(nf90_def_dim(ncid_diagevol,'Time',NF90_UNLIMITED,dim_time),'defining dimension Time')
352call check_nc(nf90_def_dim(ncid_diagevol,'longitude',nlon_loc,dim_nlon),'defining dimension longitude')
353call check_nc(nf90_def_dim(ncid_diagevol,'latitude',nlat,dim_nlat),'defining dimension latitude')
354call check_nc(nf90_def_dim(ncid_diagevol,'altitude',nlayer,dim_nalt),'defining dimension altitude')
355call check_nc(nf90_def_dim(ncid_diagevol,'interlayer',nlayer + 1,dim_interlayer),'defining dimension interlayer')
356call check_nc(nf90_def_dim(ncid_diagevol,'subsurface_layers',nsoil,dim_nsoil),'defining dimension subsurface_layers')
357
358! Define variables
359call check_nc(nf90_def_var(ncid_diagevol,'Time',nf90_double,(/dim_time/),varid),'defining variable Time')
360call check_nc(nf90_put_att(ncid_diagevol,varid,'title','Year of run'),'putting title attribute for Time')
361call check_nc(nf90_put_att(ncid_diagevol,varid,'units','Planetary year'),'putting units attribute for Time')
362
363call check_nc(nf90_def_var(ncid_diagevol,'longitude',nf90_double,(/dim_nlon/),varid),'defining variable longitude')
364call check_nc(nf90_put_att(ncid_diagevol,varid,'title','Longitudes of the grid'),'putting title attribute for longitude')
365
366call check_nc(nf90_def_var(ncid_diagevol,'latitude',nf90_double,(/dim_nlat/),varid),'defining variable latitude')
367call check_nc(nf90_put_att(ncid_diagevol,varid,'title','Latitudes of the grid'),'putting title attribute for latitude')
368
369call check_nc(nf90_def_var(ncid_diagevol,'ap',nf90_double,(/dim_interlayer/),varid),'defining variable ap')
370call check_nc(nf90_put_att(ncid_diagevol,varid,'title','Hybrid pressure at interlayers'),'putting title attribute for ap')
371
372call check_nc(nf90_def_var(ncid_diagevol,'bp',nf90_double,(/dim_interlayer/),varid),'defining variable bp')
373call check_nc(nf90_put_att(ncid_diagevol,varid,'title','Hybrid sigma at interlayers'),'putting title attribute for bp')
374
375call check_nc(nf90_def_var(ncid_diagevol,'soildepth',nf90_double,(/dim_nsoil/),varid),'defining variable soildepth')
376call check_nc(nf90_put_att(ncid_diagevol,varid,'title','Depths of soil layers'),'putting title attribute for soildepth')
377
378call check_nc(nf90_def_var(ncid_diagevol,'cell_area',nf90_double,(/dim_nlon,dim_nlat/),varid),'defining variable cell_area')
379call check_nc(nf90_put_att(ncid_diagevol,varid,'title','Cell area'),'putting title attribute for cell_area')
380
381! Global attributes
382call check_nc(nf90_put_att(ncid_diagevol,nf90_global,'title','PEM diagnostic file'),'putting global attribute')
383
384! Leave define mode and putting variables defining dimensions
385call check_nc(nf90_enddef(ncid_diagevol),'leaving define mode')
386allocate(var_dyn(nlon_loc,nlat))
387call vect2dyngrd(nlon + 1,nlat,ngrid,longitudes,var_dyn)
388call put_var_nc('longitude',var_dyn)
389call vect2dyngrd(nlon + 1,nlat,ngrid,latitudes,var_dyn)
390call put_var_nc('latitude',var_dyn)
391call put_var_nc('ap',ap)
392call put_var_nc('bp',bp)
393call put_var_nc('soildepth',mlayer)
394call vect2dyngrd(nlon + 1,nlat,ngrid,cell_area,var_dyn)
395call put_var_nc('cell_area',var_dyn)
396deallocate(var_dyn)
397call close_nc(diagevol_name)
398
399! File creation done
400is_diagevol = .true.
401
402END SUBROUTINE create_diagevol
403!=======================================================================
404
405!=======================================================================
406SUBROUTINE write_diagevol(var_name,title,units,var,dimids_in)
407!-----------------------------------------------------------------------
408! NAME
409!     write_diagevol
410!
411! DESCRIPTION
412!     Write selected diagnostic variables to NetCDF file "diagevol.nc".
413!
414! AUTHORS & DATE
415!     JB Clement, 01/2026
416!
417! NOTES
418!     > Output rate controlled by 'output_rate' from "run.def" (PEM).
419!     > Variable selection controlled by the file "diagevol.def".
420!         - If no "diagevol.def", then no output.
421!         - If "diagevol.def" is empty, then all the possible coded
422!           outputs are done.
423!         - If "diagevol.def" holds a list of variable names, then these
424!           are outputted.
425!     > If 'var' is an array, then the argument 'dimids_in' is mandatory.
426!       'dimids_in' contains the dimension IDs of the variable in order.
427!       If the variable holds a dimension 'ngrid', then it must appear
428!       in the first position.
429!-----------------------------------------------------------------------
430
431! DEPENDENCIES
432! ------------
433use io_netcdf, only: open_nc, close_nc, def_var_nc, put_var_nc, diagevol_name
434use evolution, only: idt, n_yr_run
435use stoppage,  only: stop_clean
436use geometry,  only: vect2dyngrd, ngrid, nlon, nlat
437
438! DECLARATION
439! -----------
440implicit none
441
442! ARGUMENTS
443! ---------
444character(*),               intent(in)           :: var_name, title, units
445real(dp),    dimension(..), intent(in)           :: var ! Assumed-rank array
446integer(di), dimension(:),  intent(in), optional :: dimids_in
447
448! LOCAL VARIABLES
449! ---------------
450integer(di), dimension(:),       allocatable :: dimids
451integer(di)                                  :: idim_ngrid, nlon_loc, ndim, i, j
452real(dp),    dimension(:,:),     allocatable :: var_dyn
453real(dp),    dimension(:,:,:),   allocatable :: var1d_dyn
454real(dp),    dimension(:,:,:,:), allocatable :: var2d_dyn
455
456! CODE
457! ----
458! If no diagdef file, no output at all
459if (.not. is_diagdef) return
460
461! If variable not selected, return
462! Empty diagdef file means all the variables are ouputted
463if (.not. is_output_selected(var_name)) return
464
465! If output timing not met, return
466if (abs(mod(idt,output_rate)) > minieps) return
467
468! If it is the first writing of the current timestep
469if (writing_idt /= idt) then
470    writing_idt = idt
471    call open_nc(diagevol_name,'write',itime) ! Get the current time index
472    call put_var_nc('Time',n_yr_run,itime)
473end if
474
475! Write the variable
476call open_nc(diagevol_name,'write') ! Open diagevol_name only it is not done yet
477select rank (var) ! Assumed-rank array removes compile-time rank information which is required by the following subroutines using generic interface overload
478    rank(0) ! Scalar
479        call def_var_nc(var_name,title,units,(/dim_time/)) ! Define the variable only it is not done yet
480        call put_var_nc(var_name,var,itime)
481    rank default ! Arrays
482        if (.not. present(dimids_in)) call stop_clean(__FILE__,__LINE__,'The argument ''dimids_in'' must be present to output arrays!',1)
483        ndim = size(dimids_in)
484        if (any(dimids_in == dim_ngrid)) then
485            idim_ngrid = findloc(dimids_in,dim_ngrid,dim = 1)
486            if (idim_ngrid /= 1) call stop_clean(__FILE__,__LINE__,'The dimension ''ngrid'' must be the first one to output the array!',1)
487            allocate(dimids(ndim + 2))
488            dimids(1) = dim_nlon
489            dimids(2) = dim_nlat
490            dimids(3:ndim + 1) = dimids_in(2:ndim)
491            dimids(ndim + 2) = dim_time
492            if (ngrid == 1) then ! 1D
493                nlon_loc = 1
494            else ! 3D
495                nlon_loc = nlon + 1
496            end if
497            call def_var_nc(var_name,title,units,dimids) ! Define the variable only it is not done yet
498            select rank (var) ! Assumed-rank array removes compile-time rank information which is required by the following subroutines using generic interface overload
499                rank(1)
500                    allocate(var_dyn(nlon_loc,nlat))
501                    call vect2dyngrd(nlon + 1,nlat,ngrid,var,var_dyn)
502                    call put_var_nc(var_name,var_dyn,itime)
503                    deallocate(var_dyn)
504                rank(2)
505                    allocate(var1d_dyn(nlon_loc,nlat,size(var,2)))
506                    do i = 1,size(var,2)
507                        call vect2dyngrd(nlon + 1,nlat,ngrid,var(:,i),var1d_dyn(:,:,i))
508                    end do
509                    call put_var_nc(var_name,var1d_dyn,itime)
510                    deallocate(var1d_dyn)
511                rank(3)
512                    allocate(var2d_dyn(nlon_loc,nlat,size(var,2),size(var,3)))
513                    do i = 1,size(var,2)
514                        do j = 1,size(var,3)
515                            call vect2dyngrd(nlon + 1,nlat,ngrid,var(:,i,j),var2d_dyn(:,:,i,j))
516                        end do
517                    end do
518                    call put_var_nc(var_name,var2d_dyn,itime)
519                    deallocate(var2d_dyn)
520                rank default
521                    call stop_clean(__FILE__,__LINE__,'unsupported rank for the input array!',1)
522            end select
523        else ! No 'ngrid' dimension
524            allocate(dimids(ndim + 1))
525            dimids(1:ndim) = dimids_in(:)
526            dimids(ndim + 1) = dim_time
527            call def_var_nc(var_name,title,units,dimids) ! Define the variable only it is not done yet
528            select rank (var) ! Assumed-rank array removes compile-time rank information which is required by the following subroutines using generic interface overload
529                rank(1)
530                    call put_var_nc(var_name,var_dyn,itime)
531                rank(2)
532                    call put_var_nc(var_name,var1d_dyn,itime)
533                rank(3)
534                    call put_var_nc(var_name,var2d_dyn,itime)
535                rank(4)
536                    call put_var_nc(var_name,var2d_dyn,itime)
537                rank default
538                    call stop_clean(__FILE__,__LINE__,'unsupported rank for the input array!',1)
539            end select
540        end if
541        deallocate(dimids)
542end select
543call close_nc(diagevol_name)
544
545END SUBROUTINE write_diagevol
546!=================================================================
547
548END MODULE output
Note: See TracBrowser for help on using the repository browser.