# Python code: 3dmesh_py.py
# Damaris example of obtaining data from a simulation via Python
#
# mpirun --oversubscribe --host ubu20-hvvm-c -np 5 ./3dmesh_py 3dmesh_py.xml -i 3 -v 2 -r
# 
# Example output:
# ScriptManager has found a script which has a file field named: 3dmesh_py.py
# Input paramaters found: v=2 r=1 (0 is not found)
# Input paramaters found: v=2 r=1 (0 is not found)
# Input paramaters found: v=2 r=1 (0 is not found)
# Input paramaters found: v=2 r=1 (0 is not found)
# Iteration 0 Rank 0 Sum =        0
# Iteration 0 Rank 1 Sum =    16384
# Iteration 0 Rank 2 Sum =    32768
# Iteration 0 Rank 3 Sum =    49152
# Iteration 0 done in 1.001990 seconds
# cube_i
# cube_f
# Info from Python: Iteration  0  Data found: cube_i[ P3_B0 ].sum() =  49152
# Info from Python: Iteration  0  Data found: cube_i[ P1_B0 ].sum() =  16384
# Info from Python: Iteration  0  Data found: cube_i[ P0_B0 ].sum() =  0
# Info from Python: Iteration  0  Data found: cube_i[ P2_B0 ].sum() =  32768
# for variable cube_i the number of domains (== clients x blocks_per_client) for variable last_iter:  4
# for variable cube_i block sources list:  [3, 1, 0, 2]

import numpy as np
np.set_printoptions(threshold=np.inf)


# DD (AKA Damaris Data) is a dictionary that has been filled by the 
# Damaris server process with NumPy arraysthat point to the data variables 
# that is exposed in the simulation. The Damaris source file that implements 
# thisis PyAction, found in the src/scripts/ and include/damaris/scripts 
# directories. Damaris <variables> must be exposed to the Python <pyscript>
# XML element by including its name i.e. MyPyAction in the following example:
# 
#    <variable name="cube_i" type="scalar" layout="cells_whd_wf" mesh="mesh" 
#                                                  centering="nodal" script="MyPyAction" />
#
#    <scripts>
#       <pyscript name="MyPyAction" file="3dmesh_py.py" language="python" frequency="1"
#            scheduler-file="/home/user/dask_file.json" nthreads="1" keep-workers="no" 
#            timeout="4" />
#    </scripts>
#
# The Damaris server processes also present three dictionaries, damaris_env, dask_env and iteration_data 
# containing various data about the simulation as well as the variable data itself, packaged as Numpy arrays.
# DD['damaris_env'].keys()     - The global Damaris environment data
#       (['is_dedicated_node',    # 
#         'is_dedicated_core',    # 
#         'servers_per_node',     # Number of Damaris server ranks per node
#         'clients_per_node',     # Number of Damaris client ranks per node
#         'ranks_per_node',       # Total number of ranks per node
#         'cores_per_node',       # Total number of ranks per node (yes, the same as above)
#         'number_of_nodes',      # The total number of nodes used in the simulation
#         'simulation_name',      # The name of the simulation (specified in Damaris XML file)
#         'simulation_magic_number' # Unique number for a simulation run (used in constructing name of Dask workers.)
#       ])
#
# DD['dask_env'].keys()  - The Dask environment data, 
#       (['dask_scheduler_file',    # if an empty string then no Dask scheduler was found
#         'dask_workers_name',      # Each simulation has a uniquely named set of workers
#         'dask_nworkers',          # The total number of dask workers (== 'servers_per_node' x 'number_of_nodes')
#         'dask_threads_per_worker' # Dask workers can have their own threads. Specify as nthreads="1" in Damris XML file
#       ]) 
#
# DD['iteration_data'].keys() - A single simulation iteration. 
#                        Contains the iteration number and a list of sub-dictionaries, 
#                        one for each *Damaris variable* that has been exposed to the Python 
#                        interface. i.e. specified with the script="MyAction" as in the example above
#       (['iteration',              # The iteration number as an integer.
#          'cube_i',                # A Damaris variable dictionary - the name relates to the variable name used in the Damaris XML file
#          '...',                   # A Damaris variable dictionary
#          '...'                    # A Damaris variable dictionary
#       ])
#
# A Damaris variable dictionary has the following structure
# DD['iteration_data']['cube_i'].keys()
#       (['numpy_data', 
#         'sort_data',             
#         'type_string'           # possibly to be removed as this information can be obtained from the NumPy array itself
#        ])
# 
# DD['iteration_data']['cube_i']['sort_data']
# sort_data is a list, that can be sorted on (possibly required to be transformed to tuple) which
# when sorted, the list values can be used to reconstruct the whole array using Dask:
#    ['string', 'string', [ <block_offset values> ]]
#   A specific example:
#     ['S0_I1_<simulation_magic_number>', 'P0_B0', [ 0, 9, 12 ]]
#   The string 'S0_I1_<simulation_magic_number>' indicates 'S' for server and 'I' for iteration. 
#                                                The magic number is needed as the data is published to a Dask server
#   The string 'P0_B0' indciates the dictionary key of Numpy data (see next description for explanation of 'P' and 'B')
#   The list [ 0, 9, 12 ] indicates the offestes into the global array from where the NumPy data is mapped 
#                         (The size of the NumPy array inicates the block size of the data)
#                         
#
# And, finally, the NumPy data is present in blocks, given by keys constructed as described below
# DD['iteration_data']['cube_i']['numpy_data'].keys()
#        (['P0_B0',  
#          'P1_B0'
#        ])

#  Damaris NumPy data keys: 'P' + damaris client number + '_B' + domain number
#  The client number is the source of the data (i.e. it is the Damaris client number that wrote the data)
#  The domain number is the result of multiple calls to damaris_write_block()
#  or 0 if only damaris_write() API is used or a single block only was written.
# 
# N.B. Only the data for the current iteration is available - and it is Read Only. 
#      If it is needed later it needs to be saved somehow and re-read on the 
#      next iteration. When connected to a Dask scheduler then the data can be saved on 
#      the distributed workers.



def main(DD):
    try:
        # These two dictionaries are set up in the PyAction constructor 
        # and are static.
        damaris_dict = DD['damaris_env']
        dask_dict    = DD['dask_env']
        # This third dictionary is set up in PyAction::PassDataToPython() and is 
        # typically different each iteration.
        iter_dict    = DD['iteration_data']   
        # This is the iteration value the from Damaris perspective
        # i.e. It is the number of times damaris_end_iteration() has been called
        it       = iter_dict['iteration']

        if it == 0:
            print('The DamarisData dictionaries are:')
            keys = list(DD.keys())
            print(keys)
            
            # These are the variables that have been published to Python by the Damaris server process:
            print('The DamarisData variables available are :')
            keys = list(iter_dict.keys())
            for data_key in keys :
                if (data_key != 'iteration'):
                    print(data_key)
                   
        # We know the variable names as they match what is in the Damaris XML file
        cube_i =  iter_dict['fields/PRESSURE']
        # There will be one key and corresponding NumPy array for each block of the variable
        total_sum = 0
        for key in cube_i['numpy_data'].keys() :
            cube_i_numpy = cube_i['numpy_data'][key]  # This is our NumPy array
            print('Python iteration ', it, ', Data found: fields/PRESSURE[',key,'].sum() = ', cube_i_numpy.sum() )
            total_sum += cube_i_numpy.sum()
        
        print('Python iteration ', it, ', Sum() = ', total_sum )


        
    except KeyError as err: 
        print('KeyError: No damaris data of name: ', err)
    except PermissionError as err:
        print('PermissionError!: ', err)
    except ValueError as err:
        print('Damaris Data is read only!: ', err)
    except UnboundLocalError as err:
        print('Damaris data not assigned!: ', err)   
    finally:
        pass


if __name__ == '__main__':
    main(DamarisData)