Create the NetCDF file, define dims + vars for every
currently-registered diag var, and bind the post-fire emit hook.
Must be called AFTER all register calls land — vars registered
later get no NetCDF binding and are not written.
deflate_level (0-9, default 0 = uncompressed, bit-identical)
enables gzip compression on data vars; level 1 is usually
optimal (3-5× compression, near-zero CPU).
The optional decomposition attrs (px, py, i_start, j_start,
nx_global, ny_global, nx_local, ny_local, nghost) are
written as global integer attributes when present, making the file
mergeable by tools/merge_output.py. Absent => no attrs written;
the file is bit-identical to the pre-MPI single-rank output.
ierr (P7 F1 fix): every internal nc_* call used to error
stop unconditionally — a first-run create() against a fresh
checkout with no ./output/ directory killed the whole host
process from nc_create_file. When ierr is present, any
failure (from nc_create_file itself through to the last
attribute write) closes the partially-created file, logs +
rings the specific NetCDF reason (via nc_check/fail), sets
ierr = OCEAN_STATUS_ERR_IO, and returns with is_open left
.false. — never error stops. Absent ierr preserves the
legacy abort behaviour byte-for-byte.
| Type | Intent | Optional | Attributes | Name | ||
|---|---|---|---|---|---|---|
| class(ocean_diag_t), | intent(inout) | :: | diag | |||
| character(len=*), | intent(in) | :: | filename | |||
| integer, | intent(in), | optional | :: | deflate_level | ||
| integer, | intent(in), | optional | :: | px |
Number of ranks in the x direction (process grid). |
|
| integer, | intent(in), | optional | :: | py |
Number of ranks in the y direction (process grid). |
|
| integer, | intent(in), | optional | :: | i_start |
1-based global column index of this rank’s first physical cell. |
|
| integer, | intent(in), | optional | :: | j_start |
1-based global row index of this rank’s first physical cell. |
|
| integer, | intent(in), | optional | :: | nx_global |
Total number of physical columns in the global domain. |
|
| integer, | intent(in), | optional | :: | ny_global |
Total number of physical rows in the global domain. |
|
| integer, | intent(in), | optional | :: | nx_local |
Number of physical columns on this rank (excluding ghost cells). |
|
| integer, | intent(in), | optional | :: | ny_local |
Number of physical rows on this rank (excluding ghost cells). |
|
| integer, | intent(in), | optional | :: | nghost |
Number of ghost cells on each side of the local domain. |
|
| character(len=*), | intent(in), | optional | :: | output_precision |
|
|
| integer, | intent(out), | optional | :: | ierr |
Non-zero ( |
| Type | Visibility | Attributes | Name | Initial | |||
|---|---|---|---|---|---|---|---|
| integer, | private | :: | dl | ||||
| integer, | private | :: | i | ||||
| integer, | private | :: | local_ierr | ||||
| integer, | private | :: | n1 | ||||
| integer, | private | :: | n2 | ||||
| integer, | private | :: | n3 | ||||
| integer, | private | :: | n3_max | ||||
| character(len=80), | private | :: | time_dim_name | ||||
| character(len=80), | private | :: | z_dim_name |
subroutine open_stream(diag, filename, deflate_level, & px, py, i_start, j_start, & nx_global, ny_global, nx_local, ny_local, nghost, & output_precision, ierr) !! Create the NetCDF file, define dims + vars for every !! currently-registered diag var, and bind the post-fire emit hook. !! Must be called AFTER all `register` calls land — vars registered !! later get no NetCDF binding and are not written. !! !! `deflate_level` (0-9, default 0 = uncompressed, bit-identical) !! enables gzip compression on data vars; level 1 is usually !! optimal (3-5× compression, near-zero CPU). !! !! The optional decomposition attrs (`px`, `py`, `i_start`, `j_start`, !! `nx_global`, `ny_global`, `nx_local`, `ny_local`, `nghost`) are !! written as global integer attributes when present, making the file !! mergeable by `tools/merge_output.py`. Absent => no attrs written; !! the file is bit-identical to the pre-MPI single-rank output. !! !! `ierr` (P7 F1 fix): every internal `nc_*` call used to `error !! stop` unconditionally — a first-run `create()` against a fresh !! checkout with no `./output/` directory killed the whole host !! process from `nc_create_file`. When `ierr` is present, any !! failure (from `nc_create_file` itself through to the last !! attribute write) closes the partially-created file, logs + !! rings the specific NetCDF reason (via `nc_check`/`fail`), sets !! `ierr = OCEAN_STATUS_ERR_IO`, and returns with `is_open` left !! `.false.` — never `error stop`s. Absent `ierr` preserves the !! legacy abort behaviour byte-for-byte. class(ocean_diag_t), intent(inout) :: diag character(len=*), intent(in) :: filename integer, intent(in), optional :: deflate_level integer, intent(in), optional :: px !! Number of ranks in the x direction (process grid). integer, intent(in), optional :: py !! Number of ranks in the y direction (process grid). integer, intent(in), optional :: i_start !! 1-based global column index of this rank's first physical cell. integer, intent(in), optional :: j_start !! 1-based global row index of this rank's first physical cell. integer, intent(in), optional :: nx_global !! Total number of physical columns in the global domain. integer, intent(in), optional :: ny_global !! Total number of physical rows in the global domain. integer, intent(in), optional :: nx_local !! Number of physical columns on this rank (excluding ghost cells). integer, intent(in), optional :: ny_local !! Number of physical rows on this rank (excluding ghost cells). integer, intent(in), optional :: nghost !! Number of ghost cells on each side of the local domain. character(len=*), intent(in), optional :: output_precision !! `"double"` (default / absent) or `"single"` — element type of !! the DATA variables. Absent => `NC_WP` => byte-identical to the !! pre-knob writer. Time coordinate variables stay `NC_WP` !! regardless: they are a handful of values per frame and are !! routinely differenced. integer, intent(out), optional :: ierr !! Non-zero (`OCEAN_STATUS_ERR_IO`) on any NetCDF failure when !! present; absent behaves as today (`error stop`). integer :: i, n1, n2, n3, dl, n3_max integer :: local_ierr character(len=80) :: time_dim_name, z_dim_name if (present(ierr)) ierr = OCEAN_STATUS_OK dl = 0 if (present(deflate_level)) dl = max(0, min(9, deflate_level)) if (diag%nc_stream%is_open) return diag%nc_stream%filename = filename diag%nc_stream%xtype = NC_WP if (present(output_precision)) then diag%nc_stream%xtype = diag_xtype_from_name(output_precision) end if call nc_create_file(filename, diag%nc_stream%ncid, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr)) return call nc_put_att_global(diag%nc_stream%ncid, "Conventions", "CF-1.8", ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call nc_put_att_global(diag%nc_stream%ncid, "title", & "Roundabout ocean-path diagnostics", ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return ! Decomposition window attrs — written only in multi-rank runs so ! single-rank files stay bit-identical. The merge tool uses these to ! place each rank's physical-cell slice into the global array, trimming ! the `nghost` halo on each side before stitching. if (present(px)) then call nc_put_att_global_int(diag%nc_stream%ncid, "px", px, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(py)) then call nc_put_att_global_int(diag%nc_stream%ncid, "py", py, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(i_start)) then call nc_put_att_global_int(diag%nc_stream%ncid, "i_start", i_start, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(j_start)) then call nc_put_att_global_int(diag%nc_stream%ncid, "j_start", j_start, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(nx_global)) then call nc_put_att_global_int(diag%nc_stream%ncid, "nx_global", nx_global, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(ny_global)) then call nc_put_att_global_int(diag%nc_stream%ncid, "ny_global", ny_global, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(nx_local)) then call nc_put_att_global_int(diag%nc_stream%ncid, "nx_local", nx_local, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(ny_local)) then call nc_put_att_global_int(diag%nc_stream%ncid, "ny_local", ny_local, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (present(nghost)) then call nc_put_att_global_int(diag%nc_stream%ncid, "nghost", nghost, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if ! Spatial dims from var 1's output buffer (all vars share the ! same horizontal extent). if (diag%nvars > 0) then n1 = size(diag%vars(1)%output_buffer, 1) n2 = size(diag%vars(1)%output_buffer, 2) call nc_def_dim(diag%nc_stream%ncid, "x", n1, diag%nc_stream%x_dimid, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call nc_def_dim(diag%nc_stream%ncid, "y", n2, diag%nc_stream%y_dimid, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if do i = 1, diag%nvars associate (v => diag%vars(i)) n3 = size(v%output_buffer, 3) ! Per-var time dim + coord var (unlimited). write (time_dim_name, "(A,A)") "time_", trim(v%name) call nc_def_dim(diag%nc_stream%ncid, trim(time_dim_name), & nf90_unlimited, v%nc_time_dimid, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call rdb_def_var_1d(diag%nc_stream%ncid, trim(time_dim_name), & v%nc_time_dimid, v%nc_time_varid, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call nc_put_att(diag%nc_stream%ncid, v%nc_time_varid, & "units", "seconds since simulation start", ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call nc_put_att(diag%nc_stream%ncid, v%nc_time_varid, & "long_name", "time", ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return ! Data var: (x, y, time) for 2D; (x, y, z_<name>, time) for 3D. if (n3 == 1) then call nc_def_var_3d(diag%nc_stream%ncid, trim(v%name), & [diag%nc_stream%x_dimid, diag%nc_stream%y_dimid, & v%nc_time_dimid], v%nc_varid, & deflate_level=dl, xtype=diag%nc_stream%xtype, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return else write (z_dim_name, "(A,A)") "z_", trim(v%name) call nc_def_dim(diag%nc_stream%ncid, trim(z_dim_name), & n3, v%nc_z_dimid, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call nc_def_var_4d(diag%nc_stream%ncid, trim(v%name), & [diag%nc_stream%x_dimid, diag%nc_stream%y_dimid, & v%nc_z_dimid, v%nc_time_dimid], v%nc_varid, & deflate_level=dl, xtype=diag%nc_stream%xtype, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (len_trim(v%units) > 0) then call nc_put_att(diag%nc_stream%ncid, v%nc_varid, "units", trim(v%units), ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (len_trim(v%long_name) > 0) then call nc_put_att(diag%nc_stream%ncid, v%nc_varid, "long_name", trim(v%long_name), ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (len_trim(v%standard_name) > 0) then call nc_put_att(diag%nc_stream%ncid, v%nc_varid, & "standard_name", trim(v%standard_name), ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if if (v%has_missing) then ! Vanished (no-water) target cells carry the sentinel; advertise ! it both ways so CF-aware (_FillValue) and legacy ! (missing_value) tools mask below-bottom / dry cells. ! ! NetCDF REJECTS a `_FillValue` whose type differs from the ! variable's, so the attribute has to follow `xtype`. The ! real32 branch writes `real(DIAG_MISSING_VALUE, real32)` — ! the exact same value the data conversion produces for a ! sentinel cell, so masking still matches bit-for-bit. if (diag%nc_stream%xtype == NC_R4) then call nc_put_att_real_r4(diag%nc_stream%ncid, v%nc_varid, & "_FillValue", real(DIAG_MISSING_VALUE, real32), ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call nc_put_att_real_r4(diag%nc_stream%ncid, v%nc_varid, & "missing_value", real(DIAG_MISSING_VALUE, real32), ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return else call nc_put_att_real(diag%nc_stream%ncid, v%nc_varid, & "_FillValue", DIAG_MISSING_VALUE, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return call nc_put_att_real(diag%nc_stream%ncid, v%nc_varid, & "missing_value", DIAG_MISSING_VALUE, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return end if end if v%nc_time_index = 0 end associate end do call nc_enddef(diag%nc_stream%ncid, ierr=local_ierr) if (.not. diag_io_ok(local_ierr, ierr, diag%nc_stream%ncid)) return ! Single-precision staging buffer. One shared scratch covering the ! largest registered `output_buffer`: every var shares (n1, n2), so ! only the vertical extent varies and `n3_max` bounds them all. This ! is why `open_stream` must run after every `register` — the same ! precondition the per-var NetCDF binding above already imposes. if (diag%nc_stream%xtype == NC_R4 .and. diag%nvars > 0) then n3_max = 1 do i = 1, diag%nvars n3_max = max(n3_max, size(diag%vars(i)%output_buffer, 3)) end do if (allocated(diag%nc_stream%stage)) deallocate (diag%nc_stream%stage) allocate (diag%nc_stream%stage(n1, n2, n3_max)) end if diag%nc_stream%is_open = .true. diag%emit_post_fire => nc_emit_post_fire end subroutine open_stream