import numpy as np
import pandas as pd
import datetime
from ......utils.datastores.empty import init_empty
from ..utils.point_data import read_point_output, read_point_input, calc_and_write_point_dep
# JvP 20210705: added import statements, MODULE_NAME module level logger
import logging
import os
import sys
import netCDF4 as nc4
from ......utils.check.errclass import CifError, CifRuntimeError
MODULE_NAME = __name__[__name__.index(
'TM5'):] if 'TM5' in __name__ else __name__
logger = logging.getLogger(MODULE_NAME)
# Original make_obs function by A. Berchet
[docs]
def make_obs_AB(self, datastore, runsubdir, mode, tracer, do_simu=True):
"""Write the TM5 observation file for one sub-period and one tracer.
Formats observation metadata (station ID, level, coordinates, parameter,
time step) and writes to the TM5 point-data observation file under
*runsubdir*. For adjoint mode, also writes the departure vector.
Args:
self: TM5 model plugin instance.
datastore: CIF data-store for the observation tracer.
runsubdir (str): path to the period run directory.
mode (str): ``'fwd'``, ``'tl'``, or ``'adj'``.
tracer (str): tracer / species name.
do_simu (bool): if ``False``, skip writing and only update bookkeeping.
"""
# If empty datastore, do nothing
if datastore.size == 0:
return
# # JvP 20210602, telecon with AB: only relevant for multiple species
# and/or multiple datastreams (e.g. point and sat obs)
# # Otherwise, crop the datastore to active species
# # Save it to the model in case it is needed later
# if not hasattr(self, "dataobs") or getattr(self, "reset_obs", True):
# self.dataobs = {spec: init_empty()
# for spec in self.chemistry.acspecies.attributes}
#
# self.dataobs[tracer] = pd.concat([self.dataobs[tracer], datastore],
# axis=0, sort=False)
# If do not need to do CHIMERE simulation, just update obs datastore
if not do_simu:
return
# Write only species simulated by the model
mask = (
# AB to JvP (dd.20210602): replace self.dataobs[tracer] with
# datastore from here on...
# self.dataobs[tracer]["parameter"]
datastore["parameter"]
.str.upper()
.isin(["CH4"])
)
# AB to JvP (dd.20210602): replace self.dataobs[tracer] with datastore...
# data2write = self.dataobs[tracer].loc[mask]
data2write = datastore.loc[mask]
# For adjoint, check that there is no NaN values
if mode == "adj":
if not np.all(~np.isnan(data2write["maindata"].loc[:, "adj_out"])):
raise CifError("WARNING: pycif will drive TM5 adjoint "
"with NaNs values! Check prior informations")
# Then write the data
# pyCIF needs observations overlapping several model time steps to be
# unfolded
# That mean that if "dtstep" > 1, you need to create as many virtual
# observations
# in the observation file as dtstep
# Refer to CHIMERE or LMDZ for examples
# New make_obs function by J.C.A. van Peet
[docs]
def make_obs(self, datastore, runsubdir, mode, tracer, ddi, do_simu=True):
"""
PURPOSE
Write point observations to file readable by TM5
ARGS
No idea...
KWARGS
No idea...
NOTE
This function is called from the function outputs2native_adj, which is
defined in pycif/plugins/models/TM5/io/outputs2native_adj.py
VERSION HISTORY
2.3 28-10-2021 by J.C.A. van Peet
*) Updated determination of iter_nr.
*) When mode=adj, the CIF observation increments ('adj_out') are now
written to the point departure file instead of the TM5 forcings
(which are retained for comparison purposes).
2.2 22-10-2021 by J.C.A. van Peet
Antoine Berchet added ddi to the function header. No idea what it does
though...
2.1 20-07-2021 by J.C.A. van Peet
*) Updated the way time is selected from the data2write variable
*) Added on-the-fly generation of stationlist_CH4.dat, which is
then used in params.py for the variable station.list.file.
2.0 05-07-2021 by J.C.A. van Peet
Adapted for TM5.
1.0 ??-??-???? by A. Berchet
See original code above.
"""
# Set the name of this function
PROG_NAME = MODULE_NAME+".make_obs"
# Local logger
logger = logging.getLogger(PROG_NAME)
logger.setLevel(logging.DEBUG)
# Some debug statements...
logger.debug("")
logger.debug("*"*30)
logger.debug(PROG_NAME+" => DEBUG:")
logger.debug(f" datastore = {datastore}")
logger.debug(f" runsubdir = {runsubdir}")
logger.debug(f" mode = {mode}")
logger.debug(f" tracer = {tracer}")
logger.debug(f" kwarg do_simu = {do_simu}")
# If empty datastore, do nothing
if datastore.size == 0:
return
# JvP 20210705: based on a telecon I had with AB (dd. 2-6-2021), I commented
# out some parts of the original code. I did not copy those parts here, but
# they are included for future reference in the original code above.
# No idea what do_simu does. In the original code it's just checked if it
# is True. If not, you return to the calling function. However, I would like
# to know if that ever happens, so I raise an error to stop CIF if that ever
# happens.
if not do_simu:
# Original
# return
# JvP 20211022: Originial code. Judging by the code by Antoine below,
# you should just return if do_simu == False. But I still would like
# to know if that ever happens, so I reverted back to my old code.
logger.critical("*"*30)
logger.critical(PROG_NAME+" => ERROR: do_simu is False.")
logger.critical(" How to proceed???")
logger.critical(" Computer says no...")
logger.critical("*"*30)
raise CifRuntimeError
#
# Code Antoine used to replace my code above.
# logger.critical("")
# logger.critical("*"*30)
# logger.critical(PROG_NAME + " => ERROR: do_simu is False.")
# logger.critical(" Do nothing")
# logger.critical("*"*30)
# logger.critical("")
# return
# end if do_simu
# Get a mask with only species simulated by the model (i.e. CH4). Then use
# the dataframe.loc method/object/property (whatever, see
# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.loc.html
# for more info) to select only the rows from the dataframe where the
# "parameter" is CH4. I think that the resulting data2write is another
# dataframe...
mask = datastore["metadata"]["parameter"].str.upper().isin(["CH4"])
data2write = datastore.loc[mask]
# >>> JvP 20211022
# The following lines were added by Antoine. No idea what they do...
# Unfold data2write depending on the duration of each observation
inds = [0] + list(np.cumsum(
data2write[("metadata", "dtstep")].values[:-1]))
ref_dtstep = data2write["metadata"]["dtstep"].values
data2write = data2write.reset_index().loc[
data2write.index.repeat(data2write["metadata"]["dtstep"])]
idx = np.zeros(len(data2write), dtype=int)
idx[inds] = inds
np.maximum.accumulate(idx, out=idx)
data2write.loc[:, ("metadata", "tstep")
] += np.arange(len(data2write)) - idx
data2write[("metadata", "date")] = \
self.tstep_dates[ddi][data2write["metadata"]["tstep"].astype(int)] \
+ datetime.timedelta(minutes=30)
# <<< JvP 20211022.
# Number of observations in data2write
n_obs = len(data2write)
# Apparently, in the adjoint run, there can be NaN values in the data.
# So for the adjoint, check that there are no NaN values and raise a
# RuntimeError if there are.
if mode == "adj":
if not np.all(pd.notnull(data2write["maindata"].loc[:, "adj_out"])):
raise CifError("WARNING: pycif will drive TM5 adjoint "
"with NaNs values! Check prior informations")
# end if
# end if
# Generate station data from data2write:
# *) station names can be any length
# *) station codes are strings of 11 characters (e.g. ALT_NOA_000), which
# for now I'll just set to 'STN_XXX_000'
# *) station_type seems to be 'FM' for all stations (Flask, M...?)
# *) station_id is a 1 based index linking the measurements to the
# unique stations
# *) station_tower seems to be 0 for all stations (no tower measurements
# assimilated...)
# *) Use dtype='S' to be able to write to netCDF as character arrays,
# i.e. something that Fortran can understand...
# If you don't use 'S' as dtype, in Python3 the strings will be
# unicode by default, and that messes up the stringtochar function
# somehow.
#
# **************************************************************************
# * According to a mail by Antoine (dd. 15-07-2021), the format of the *
# * "station" column in data2write changes according to the version of *
# * CIF that you're using: *
# * Do you generate observations from random values? or from a monitor? *
# * With the random values, stations just have a number (in your *
# * version it is indeed an integer, which is a problem; in newest *
# * version, it is a number but as a string) *
# * When you lad from a custom monitor, you can give any name to your *
# * station. *
# * In other words, the following may crash after some CIF update... *
# **************************************************************************
#
# This is fixed below:
metadata = data2write["metadata"]
station_full = pd.Series(data=np.asarray(metadata['station']), dtype=str)
station_list = pd.Series(index=metadata['station'].drop_duplicates().values,
data=range(len(metadata['station'].drop_duplicates())))
station_name = np.array("station_" + station_full, dtype="S")
station_code = np.array(
("STN_" + station_full + "_000").str.zfill(11), dtype="S")
station_type = np.array(len(metadata) * ['FM'], dtype="S")
station_id = np.array(station_list[metadata['station']] + 1)
station_tower = np.array(len(metadata) * [0])
logger.debug(f" station_name = {station_name}")
logger.debug(f" station_code = {station_code}")
logger.debug(f" station_type = {station_type}")
logger.debug(f" station_id = {station_id}")
logger.debug(f" station_tower = {station_tower}")
# Unique stations in station_name
unique_stn, unique_stn_idx = np.unique(station_name, return_index=True)
unique_stn_alt = metadata['alt'].iloc[unique_stn_idx].to_numpy()
unique_stn_code = station_code[unique_stn_idx]
unique_stn_lat = metadata['lat'].iloc[unique_stn_idx].to_numpy()
unique_stn_lon = metadata['lon'].iloc[unique_stn_idx].to_numpy()
unique_stn_name = station_name[unique_stn_idx]
unique_stn_tower = station_tower[unique_stn_idx]
unique_stn_type = station_type[unique_stn_idx]
logger.debug(f" unique_stn = {unique_stn}")
logger.debug(f" unique_stn_idx = {unique_stn_idx}")
logger.debug(f" unique_stn_alt = {unique_stn_alt}")
logger.debug(f" unique_stn_code = {unique_stn_code}")
logger.debug(f" unique_stn_lat = {unique_stn_lat}")
logger.debug(f" unique_stn_lon = {unique_stn_lon}")
logger.debug(f" unique_stn_name = {unique_stn_name}")
logger.debug(f" unique_stn_tower = {unique_stn_tower}")
logger.debug(f" unique_stn_type = {unique_stn_type}")
# Original comment by Antoine... I guess I'll check for dtstep > 1 and
# just raise an exception if that occurs...
#
# Then write the data
# pyCIF needs observations overlapping several model time steps to be unfolded
# That mean that if "dtstep" > 1, you need to create as many virtual observations
# in the observation file as dtstep
# Refer to CHIMERE or LMDZ for examples
# Set the name of the file
point_file = runsubdir + '/point_input.nc4'
logger.debug(" point_file = " + point_file)
# Delete any pre-existing files, and open the output file. Usually,
# selecting "w" when opening a file should create a new file overwriting
# any existing file. But if the file is opened in hdfview (which happens
# regularly), this issues an error. Deleting the file explicitly supresses
# that error. But os.remove raises an error if the file does not exist
# (at least in some Python versions I've tried this), so wrap it in a try
# statement to suppress it.
try:
os.remove(point_file)
except:
pass
# end try
# Open file
nc_id = nc4.Dataset(point_file, 'w')
# Global attributes
nc_id.listfile = 'Generated by pyCIF'
nc_id.obsdatadir = 'Generated by pyCIF'
# >>> Create dimensions
# idate = 6: year, month, day, hour, minute, second
nc_id.createDimension('idate', 6)
# id = n_obs: an integer array with indices of the observations...
nc_id.createDimension('id', n_obs)
id_var = nc_id.createVariable('id', np.int32, ('id',))
id_var[()] = np.arange(n_obs, dtype=np.int32) + 1
id_var.longname = 'observation index (1-based)'
# Note that max(...,key=len) gives you the longest string in the array,
# and len returns the length of that string
nc_id.createDimension('len_station_code', len(max(station_code, key=len)))
nc_id.createDimension('len_station_name', len(max(station_name, key=len)))
nc_id.createDimension('len_station_type', len(max(station_type, key=len)))
nc_id.createDimension('len_tracer', len(tracer))
nc_id.createDimension('station', unique_stn.size)
# <<< Create dimensions
# Create variables
alt_var = nc_id.createVariable('alt', np.float64, ('id',))
bias_id_var = nc_id.createVariable(
'bias_id', np.int32, ('id',))
date_components_var = nc_id.createVariable(
'date_components', np.int16, ('id', 'idate'))
lat_var = nc_id.createVariable('lat', np.float64, ('id',))
lon_var = nc_id.createVariable('lon', np.float64, ('id',))
mixing_ratio_var = nc_id.createVariable(
'mixing_ratio', np.float64, ('id',))
mixing_ratio_error_var = nc_id.createVariable(
'mixing_ratio_error', np.float64, ('id',))
mixing_ratio_stdv_var = nc_id.createVariable(
'mixing_ratio_stdv', np.float64, ('id',))
sampling_strategy_var = nc_id.createVariable(
'sampling_strategy', np.int16, ('id',))
station_alt_var = nc_id.createVariable(
'station_alt', np.float32, ('station',))
station_code_var = nc_id.createVariable(
'station_code', 'S1', ('station', 'len_station_code'))
station_id_var = nc_id.createVariable(
'station_id', np.int32, ('id',))
station_lat_var = nc_id.createVariable(
'station_lat', np.float32, ('station',))
station_lon_var = nc_id.createVariable(
'station_lon', np.float32, ('station',))
station_name_var = nc_id.createVariable(
'station_name', 'S1', ('station', 'len_station_name'))
station_tower_var = nc_id.createVariable(
'station_tower', np.int32, ('station',))
station_type_var = nc_id.createVariable(
'station_type', 'S1', ('station', 'len_station_type'))
tower_var = nc_id.createVariable('tower', np.int32, ('id',))
tracer_var = nc_id.createVariable(
'tracer', 'S1', ('len_tracer',))
# Fill variables
alt_var[()] = metadata['alt']
bias_id_var[()] = np.ones((n_obs,), dtype=np.int32)
#
# JvP 20210720
# Apparently, the date column contains np.datetime64[ns] objects, so that
# does include a time value. I don't know why I set the time to 12 to start
# with, but you can just use i.[hour|minute|second] in the command below.
# More info on np.datetime64: https://numpy.org/doc/stable/reference/arrays.datetime.html
# Note that this interface is stable only since the latest numpy version
# (1.21), and that older versions considered this interface as experimental
# https://numpy.org/doc/1.20/reference/arrays.datetime.html.
# date_components_var [()] = np.array( [[i.year, i.month, i.day, 12, 0, 0] for i in data2write['date']] )
date_components_var[()] = np.array(
[[i.year, i.month, i.day, i.hour, i.minute, i.second]
for i in metadata['date']])
# Here, replace lat/lon by the middle of grid cells, as the rest of the CIF does
# the interpolation
lat_var[()] = self.domain.zlat[
metadata["i"].astype(int), metadata["j"].astype(int)]
lon_var[()] = self.domain.zlon[
metadata["i"].astype(int), metadata["j"].astype(int)]
mixing_ratio_var[()] = data2write["maindata"]['obs'] # should be in ppbv
#
# NOTE: MIXING_RATIO_ERROR SEEMS TO BE A CONSTANT 3.0 IN THE POINT_INPUT FILE...
mixing_ratio_error_var[()] = np.full((n_obs,), 3.0)
#
# NOTE: THERE'S NO ERROR IN THE OBSERVATIONS, SO I USE CONSTANT OF 0.0001 FOR NOW...
mixing_ratio_stdv_var[()] = 0.001 * data2write["maindata"]['obs']
#
# NOTE: SAMPLING_STRATEGY SEEMS TO BE A CONSTANT 4 IN THE POINT_INPUT FILE...
sampling_strategy_var[()] = np.full((n_obs,), 4)
#
station_alt_var[()] = unique_stn_alt
station_alt_var.long_name = 'station altitude'
station_code_var[()] = nc4.stringtochar(unique_stn_code)
station_code_var.long_name = 'station code'
station_id_var[()] = station_id
station_id_var.long_name = 'station index (1-based)'
station_lat_var[()] = unique_stn_lat
station_lat_var.long_name = 'station latitude'
station_lon_var[()] = unique_stn_lon
station_lon_var.long_name = 'station longitude'
station_name_var[()] = nc4.stringtochar(unique_stn_name)
station_name_var.long_name = 'station name'
station_tower_var[()] = unique_stn_tower
station_tower_var.long_name = 'tower flag: 1=tower, 0=surface'
station_type_var[()] = nc4.stringtochar(unique_stn_type)
station_type_var.long_name = 'station observation type'
tower_var[()] = station_tower
tracer_var[()] = nc4.stringtochar(np.array(tracer, dtype='S'))
# Close file
nc_id.close()
# *********************************************************
# * QUESTIONS TO ANTOINE: *
# * -) Is there a way to get a name for the stations? *
# * Currently, there are only numbers in the station *
# * column which I convert to the name "station_00X". *
# * -) What is the unit of the 'obs' column in datastore? *
# * -) Is there an error for the 'obs' column? *
# * -) What is the time of the observations? Currently, *
# * only the date is given. I've set the time to 12 *
# * for now. *
# * -) How to access input_dir or other variables from *
# * the yaml file? I now copied the global input_dir *
# to the model plugin input_dir variable *
# *********************************************************
# >>> JvP 20210720
# Write station locations to a stationlist_CH4[...].dat file that is
# also used in params.py for the station.list.file variable.
# Set the name of the stationlist file
stationlist_file = runsubdir + '/stationlist_CH4.dat'
# Write data to the stationlist file
with open(stationlist_file, 'w') as f_id:
# Write header line
print(
'ID_OBS LAT LON ALT TP TO LT_min LT_max STATIONNAME', file=f_id)
# For each of the stations, write the corresponding variables
# Notes:
# *) the columns 'TP' through 'LT_max' have fixed values, corresponding
# to those in the original stationlist file.
# *) the arrays unique_stn_code and unique_stn_name are converted to
# unicode with 'astype(str)', because else their contents in the
# file look like "b'STN_000_000'"
for stn_id, stn_lat, stn_lon, stn_alt, stn_name in \
zip(unique_stn_code.astype(str), unique_stn_lat,
unique_stn_lon, unique_stn_alt, unique_stn_name.astype(str)):
print("%s %7.2f %7.2f %7.1f %s %2i %6.1f %6.1f %s" %
(stn_id, stn_lat, stn_lon, stn_alt, 'FM', 0, 0.0, 24.0, stn_name), file=f_id)
# end for
# end with
# <<< JvP 20210720
# Some debug statements...
logger.debug("*"*30)
logger.debug("")
# Now should write the departure file when in adjoint mode
if mode == "adj":
# >>> JvP 20211022
# After a successful forward run, you have to make a "point_departures.nc4"
# file. In TM5, this file is made in the "dep" job by TMVar.py, which calls
# Observations_JRC.PointObs_JRC.applyPointObs(...) as:
# point.applyPointObs( trackfile=trackfile, trackfile_apri=trackfile_apri,
# mismatchfile=departurefile,
# Params=None, montecarlo=False )
# where:
# trackfile = some/path/to/point_output.nc4 (written by TM5)
# trackile_apri = some/path/to/point_output.nc4 from the apri run (i.e. iter 1)
# departurefile = some/path/to/point_departures.nc4 (which is read by the
# adjoint run of TM5, and which is set in params.py when
# the config file is written).
# Message
logger.info(PROG_NAME+" => writing point departure file.")
# >>> JvP 20211028
# Get the iteration number from the runsubdir, since it is not stored
# somewhere easily accessible. According to Antoine (TM5-CIF telecon
# 27-10-2021) the iteration number is -1 for the apriori run (or first
# iteration), -2 for the aposterior run (or final iteration) and 2,3,...
# for the 'normal' iterations. This means that when you convert the
# iteration number to a string with '%04i'%iter_nr, the apriori iter_nr
# number will become '-001', the aposteriori iter_nr will become '-002',
# and the rest will become '0002', '0003' etc. If you combine the iter_nr
# with a prefix like 'fwd_', the strings become (in chronological order):
# 'fwd_-001', 'fwd_0002', 'fwd_0003', ..., 'fwd_-002'.
# Notes:
# *) Luckily, you only need the string representation of this number, so
# it is sufficient to extract the part after '[fwd|adj]_'.
# *) Similar code is used in params.write_tm5_rc(...)
# Split runsubdir into its consituent parts
runsubdir_parts = runsubdir.split(sep='/')
# Check if runsubdir_parts[-2] starts with either 'fwd_' or 'adj_'.
if (not runsubdir_parts[-2].startswith(('fwd_', 'adj_'))):
logger.critical("")
logger.critical("*"*30)
logger.critical(
PROG_NAME+" => ERROR: path component does not start with 'fwd_' or 'adj_'!")
logger.critical(" runsubdir = "+runsubdir)
logger.critical(f" runsubdir parts = {runsubdir_parts}")
logger.critical(" Computer says no...")
logger.critical("*"*30)
logger.critical("")
raise CifRuntimeError
# end if
# Get the string representing the iteration number, and log a message.
iter_nr = runsubdir_parts[-2][4:]
logger.info(PROG_NAME+f" => iter_nr = {iter_nr}")
# <<< JvP 20211028
# Get the a-priori, forward, and adjoint directories
# Here, use self.adj_refdir defined in run.py, to be sure that it is
# the last computed forward
apri_dir = '/'.join(runsubdir_parts[0:-2]) + \
'/fwd_-001/' + runsubdir_parts[-1]+'/'
# fwd_dir = '/'.join(runsubdir_parts[0:-2])+'/fwd_'+iter_nr+'/'+runsubdir_parts[-1]+'/'
fwd_dir = f'{self.adj_refdir}/{runsubdir_parts[-1]}/'
adj_dir = '/'.join(runsubdir_parts[0:-2]) + \
'/adj_'+iter_nr+'/'+runsubdir_parts[-1]+'/'
#
logger.info(PROG_NAME+f" => apri_dir = {apri_dir}")
logger.info(PROG_NAME+f" => fwd_dir = {fwd_dir}")
logger.info(PROG_NAME+f" => adj_dir = {adj_dir}")
# Read the point_output.nc4 file for the current iteration.
# You can check if your functions work the same as the originals by
# using point files from a TM5 run.
# point_output_file = '/home/jpt930/NOBACKUP/tm5/20210222-tm5_sf-18m_points_only-ERA5-spiv/tmvar/var4d/iter-0001/fwd/output/point_output.nc4'
point_output_file = fwd_dir+'output/point_output.nc4'
logger.info(PROG_NAME+f" => point_output_file = {point_output_file}")
point_output_data = read_point_output(point_output_file)
# Read the point_output.nc4 file from the a-priori (i.e. iteration 1)
# You can check if your functions work the same as the originals by
# using point files from a TM5 run.
# apri_point_output_file = '/home/jpt930/NOBACKUP/tm5/20210222-tm5_sf-18m_points_only-ERA5-spiv/tmvar/var4d/iter-0001/fwd/output/point_output.nc4'
# AB: Is this part needed? we can simply take point_output_data?
# apri_point_output_file = apri_dir+'output/point_output.nc4'
# logger.info(PROG_NAME+" => apri_point_output_file = %s" % apri_point_output_file)
# apri_point_output_data = read_point_output(apri_point_output_file)
#
# # Check if the sample_ids match
# if( not np.array_equal( point_output_data['sample_id'], apri_point_output_data['sample_id'] ) ):
# logger.critical("")
# logger.critical("*"*30)
# logger.critical(PROG_NAME+" => ERROR: Something wrong with sample_id!")
# logger.critical(" point_output_data['sample_id'] = %s" % point_output_data['sample_id'] )
# logger.critical(" apri_point_output_data['sample_id'] = %s" % apri_point_output_data['sample_id'] )
# logger.critical(" mode = %s" % mode )
# logger.critical(" runsubdir = %s" % runsubdir )
# logger.critical(" iter_nr = %s" % iter_nr )
# logger.critical(" Computer says no...")
# logger.critical("*"*30)
# logger.critical("")
# raise RuntimeError(PROG_NAME+" => ERROR: something wrong with sample_id!")
# # end if
# Read the point_input.nc4 file from the last forward
# You can check if your functions work the same as the originals by
# using point files from a TM5 run.
# apri_point_input_file = '/home/jpt930/NOBACKUP/tm5/20210222-tm5_sf-18m_points_only-ERA5-spiv/input/point_input.nc4'
fwd_point_input_file = fwd_dir+'point_input.nc4'
logger.info(PROG_NAME+f" => fwd_point_input_file = {fwd_point_input_file}")
fwd_point_input_data = read_point_input(
fwd_point_input_file, point_output_data['sample_id'])
# Set point departure filename
point_dep_file = runsubdir+'/point_departures.nc4'
logger.info(PROG_NAME+f" => point_dep_file = {point_dep_file}")
# Calculate the departure data
#
# JvP 20211028: Antoine entered the following example code when he made
# some modifications to the TM5 branch:
# print(__file__)
# import code
# code.interact(local=dict(locals(), **globals()))
#
# # At this point the variable data2write["obs_incr"], includes:
# # R^(-1) x (H(xb) - y)
# The first three lines just exit to an interactive prompt, including
# all variables and their values at that point in the code. Nice
# debugging tip. The value of obs_incr is probably the same as the
# TM5 'forcing' calculated in the function calc_and_write_point_dep in
# .../pycif/plugins/models/TM5/io/utils/point_data.py. So I added
# obs_incr as keyword to the function call. Inside the function,
# obs_incr is written to file as 'forcing' while the original TM5
# focings are retained in the 'forcing_TM5' variable for comparison
# purposes.
#
# calc_and_write_point_dep( point_output_data, apri_point_output_data,
# apri_point_input_data, point_dep_file )
#
calc_and_write_point_dep(point_output_data, point_output_data,
fwd_point_input_data, point_dep_file,
adj_out=data2write["maindata"]["adj_out"])
# <<< JvP 20211022
# end if (mode == 'adj')
# logger.debug("")
# logger.debug("*"*30)
# logger.debug(PROG_NAME+" => Computer says no!")
# logger.debug("*"*30)
# logger.debug("")
# try:
# raise RuntimeError
# except RuntimeError as e:
# #logger.exception("OOPS!")
# logger.critical(e, exc_info=True)
# #raise # => Will display the traceback on screen a second time
# sys.exit() # => Just exit.
# end try
# end function make_obs