Source code for pycif.plugins.obsoperators.standard.transforms.utils.check_adjtltest

import os
from logging import info
import itertools

import numpy as np
import pandas as pd


[docs] def check_adjtltest(self, period_order, mapper, transform_pipe): """Run the adjoint / tangent-linear dot-product test on each transform. For every ``(date, transform)`` pair processed in the forward direction, computes the two sides of the dot-product identity used to validate the adjoint of a transform:: <dx | H^T(H(dx))> vs <H(dx) | H(dx)> ``dx_in`` (left-hand side) is accumulated from the transform's inputs, by summing the element-wise product of the tangent-linear increment (``incr``) and the adjoint sensitivity (``delta_adj_out`` if present, else ``adj_out``) of each precursor. ``dx_out`` (right-hand side) is accumulated the same way from the transform's outputs and successors. The two quantities should be equal, up to floating-point round-off, if the transform's adjoint is correctly implemented; the relative difference is reported in units of machine epsilon. Results for all checked transforms are logged and written to ``{self.workdir}/check_transforms.log``. Args: self: obsoperator instance exposing ``data_tl`` and ``data_adj`` (datastores keyed by transform name, then by sub-simulation date/id ``ddi``, holding the tangent-linear increments and adjoint outputs for each input/output tracer ``trid``) as well as ``workdir``. period_order (list[tuple]): ordered ``(ddi, transform, direction)`` triplets describing the transforms scheduled to run, as built by the transform pipeline. Only ``direction == "forward"`` entries are checked here (the tangent-linear/adjoint pair for a transform is compared once, not once per direction). mapper (dict): per-transform metadata (``inputs``, ``outputs``, ``precursors``, ``successors``, ``subsimus``) describing how sub-simulations and tracers (``trid``) are linked across transforms. transform_pipe: container exposing each transform instance as an attribute (``getattr(transform_pipe, transform)``); used here only to retrieve the plugin name of transforms and their precursors/successors. Returns: None. Results are emitted via the ``info`` logger and written to ``check_transforms.log`` in ``self.workdir``. """ df = None # Check the adjoint for each transformations for ddi, transform, direction in period_order: if transform not in self.data_adj \ or transform not in self.data_tl \ or direction != "forward": continue data_tl = self.data_tl[transform] data_adj = self.data_adj[transform] dx_in = 0 dx_out = 0 if ddi not in data_tl: continue # dx_in = <dx | H^T(H(dx))>, accumulated over the transform's # inputs and their precursors list_trids = data_tl.get(ddi, {"inputs": {}})["inputs"] for trid in list_trids: # Skip if data not in adj or tl datastore if ddi not in data_adj: continue if data_tl[ddi]["inputs"].get(trid, {}) == {} \ and data_adj[ddi]["inputs"].get(trid, {}) == {}: continue # Force using the transform input itself if no precursor auto_precursor = False if trid not in mapper[transform]["precursors"]: precursors = [transform] auto_precursor = False list_di = mapper[transform]["subsimus"][ddi]["outputs"][trid] else: precursors = mapper[transform]["precursors"][trid] list_di = mapper[transform]["subsimus"][ddi]["inputs"][trid] for di, precursor in itertools.product(list_di, precursors): # Get value from dump2input transform if force_dump precursor_name = getattr( transform_pipe, precursor).plugin.name if precursor_name == "dump2inputs": if precursor in self.data_tl \ and precursor in self.data_adj: tl = self.data_tl[precursor][di][ "outputs"][trid][di][transform] adj = self.data_adj[precursor][di][ "outputs"][trid][di][transform] else: continue else: tl = data_tl[ddi]["inputs"][trid][di].get( precursor, {ddi: data_tl[ddi]["inputs"][trid][di]}) adj = data_adj[ddi]["inputs"][trid][di].get( precursor, {ddi: data_adj[ddi]["inputs"][trid][di]}) # Loop over precursor sub-simulations subsimus_in = mapper[precursor]["subsimus"] for precursor_di in subsimus_in: # Skip if subsimu not in precursor subsimulations if di not in subsimus_in[precursor_di]['outputs'].get(trid, []): continue tl_precursor = tl[precursor_di] adj_precursor = adj[precursor_di] # Deal with sparse data if isinstance(tl_precursor, pd.DataFrame) \ and isinstance(adj_precursor, pd.DataFrame): tl_precursor = tl_precursor["maindata"] adj_precursor = adj_precursor["maindata"] if "incr" not in tl_precursor or "adj_out" not in adj_precursor: continue data_tl_in = tl_precursor["incr"] data_adj_in = adj_precursor.get( "delta_adj_out", adj_precursor["adj_out"]) dx_in += np.float64((data_tl_in * data_adj_in).sum()) # dx_out = <H(dx) | H(dx)>, accumulated over the transform's # outputs and their successors for trid in mapper[transform]["outputs"]: if ddi not in data_tl or ddi not in data_adj: continue if trid not in data_tl[ddi]["outputs"] \ or trid not in data_adj[ddi]["outputs"]: continue for di in mapper[transform]["subsimus"][ddi]["outputs"][trid]: for successor in mapper[transform]["successors"][trid]: # Get value from loadfromoutputs transform if force_loadin successor_name = getattr( transform_pipe, successor).plugin.name if successor_name == "loadfromoutputs" \ and successor in self.data_tl \ and successor in self.data_adj: if di not in self.data_tl[successor] \ or di not in self.data_adj[successor]: continue tl = self.data_tl[successor][di]["inputs"][trid][di][ transform] adj = self.data_adj[successor][di]["inputs"][trid][di][ transform] else: tl = data_tl[ddi]["outputs"][trid][di][successor] adj = data_adj[ddi]["outputs"][trid][di][successor] # Loop over successor sub-simulations subsimus_in = mapper[successor]["subsimus"] for successor_di in subsimus_in: # Skip if subsimu not in successor subsimulations if di not in subsimus_in[successor_di]['inputs'].get(trid, []): continue tl_successor = tl[successor_di] adj_successor = adj[successor_di] # Deal with sparse data if isinstance(tl_successor, pd.DataFrame) \ and isinstance(adj_successor, pd.DataFrame): tl_successor = tl_successor["maindata"] adj_successor = adj_successor["maindata"] if "incr" not in tl_successor or "adj_out" not in adj_successor: continue data_tl_out = tl_successor["incr"] data_adj_out = adj_successor["adj_out"] dx_out += np.float64( (data_tl_out * data_adj_out).sum()) # Duplicate input / output for toobsvect and fromcontrol transf_name = getattr(transform_pipe, transform).plugin.name if transf_name == "toobsvect": dx_out = dx_in # elif transf_name == "fromcontrol": # print(__file__) # import code # code.interact(local=dict(locals(), **globals())) # dx_in = dx_out # Skip transforms for which neither side of the test could be # computed for this sub-simulation (nothing to report) if dx_in == dx_out == 0: continue # Relative difference between the two sides of the dot-product # test, expressed in units of machine epsilon diff = ( (dx_in / dx_out - 1) / np.finfo(np.float64).eps if dx_out != 0 else np.nan ) df_tr = pd.DataFrame( data=[[transform, transf_name, ddi, dx_in, dx_out, diff]], columns=["id", "name", "date", "dx_in", "dx_out", "diff"] ) df = df_tr if df is None else pd.concat([df, df_tr], ignore_index=True) # Format the results table and write it to the log / logfile df["date"] = df["date"].map(lambda x: x.isoformat(sep=" ")) df["dx_in"] = df["dx_in"].map(lambda x: f"{x:.15f}") df["dx_out"] = df["dx_out"].map(lambda x: f"{x:.15f}") df["diff"] = df["diff"].map(lambda x: f"{x:.1E}") df.columns = ["transform_ID", "transform_name", "sub-simulation", "<dx|H*(H(dx))>", "<H(dx)|H(dx)>", "difference"] df_str = df.to_string(index=False) # Write to logfile info("check transforms results:\n" + df_str) adjtl_log = os.path.join(self.workdir, "check_transforms.log") with open(adjtl_log, "w") as f: f.write(df_str)