"""
This creates the plot that is used as Fig. 8 in Lyke et al. 2020. This spectrum
file and the catalog file is needed to make this plot. This will not work (and
has no error-trapping) for records that are missing any data from:
UKIDSS, ROSAT, FIRST, GALEX, or WISE. The spectrum file must have a positive
value for flux and ivar at every point. Spectra with bad pixels are NOT supported.

As UKIDSS and 2MASS overlap in wavelength coverage, only one should be used. 2MASS
data conversion is not supported here. This pretty much leaves UKIDSS only (for
this script).

There will be NO updates to this script to include 2MASS conversions.

Gaia data is not converted or plotted as the large time gap between SDSS optical and
Gaia optical observations cannot be plotted together in a meaningful way due to
known quasar flux variability.

The terms 'flux' and 'flux density' are used interchangeably throughout. Context
is key.

Dependencies
----------
spec file : requires the SDSS full spectrum file in ../data/
github repository : https://github.com/bradlyke/utilities
Note: To get these plots, LaTeX must be installed and
      matplotlib must be able to compile LaTeX commands

Input file
----------
The most recent version of the DR16Q quasar-only catalog.

Parameters
----------
input_file : the file name for the DR16Q FITS file in the
             ../data folder. No path needed.
pt,mt,ft : Plate, MJD, and FiberID for a record that has all multiwavelength data
           The values used to make the paper plot are hardcoded in __main__
           No guarantees are made about script viability or plot viability
           for any spectrum other than the hardcoded one.
output_plot_name : the name of the plot written out
plot_fontsize : The fontsize for everything in the plot
               For a 10x4 plot spanning twocolumn, 11 works best.
save_check : 0 - don't save, just plot
             1 - don't plot, just save into ../plots/

Output
----------
If selected, an EPS file of the plot.

"""

from astropy.io import fits
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
matplotlib.rc('text',usetex=True)
import progressBar_fancy as pbf
import cat_tools as ct
from sciCon import mks
import sys

#This function will smooth the SDSS spectrum using a boxcar window of a
#given pixel size (smooth_pct).
#This is a copy of the smoothing function from another of my repositories:
#  https://github.com/bradlyke/ica_5010/rolling_boxcar.ipynb
#Copied over so users don't have to clone another repository for one function.
def boxcar(flux_arr,flux_var,smooth_pct,weighted=False):
    spc = smooth_pct
    num_dpoints = len(flux_arr) #how many data points
    box_flux = np.zeros(num_dpoints,dtype='f8') #This is the smoothed flux data
    box_err = np.zeros(num_dpoints,dtype='f8') #This is the smoother flux error

    #This does the box. 10 pixels wide, so choose 5 on each side of current pixel
    for i in range(num_dpoints):
        #Define range of values to smooth
        if i < int(spc/2):
            lower = 0
        else:
            lower = -int(spc/2) + i
        if ((int(spc/2) + i) > num_dpoints):
            upper = num_dpoints
        else:
            upper = int(spc/2) + i
        #Smooth the values depending on smoothing type
        #Weighted takes each pixels error into account and the pixel flux is inversely
        #proportional to the pixel uncertainty
        if weighted==True:
            noise_temp = np.sqrt(flux_var[lower:upper])
            signal_temp = flux_arr[lower:upper]
            flux_temp = np.average(signal_temp,weights=noise_temp)
            ivar_temp = (np.average((signal_temp-flux_temp)**2, weights=noise_temp))**(-1.0)
        #Unweighted version. Why would you want to use this?
        else:
            flux_temp = np.median(flux_arr[lower:upper])
            ivar_temp = flux_var[i]

        #Save the NEW smoothed flux value
        box_flux[i] = flux_temp
        box_err[i] = ivar_temp

    return box_flux,box_err

"""
To make a Spectral Energy Distribution (SED), all of the different sources
of data have to be on the same flux scale (y-axis). These following functions
will convert the different surveys' data in DR16Q to the standard SDSS flux
units of 10^(-17) erg/s/cm^2/Angstrom.
uk_convert(): UKIDSS data
jy_convert(): Anything in Janskys.
nano_convert(): Anything in nanomaggies (GALEX or WISE data)
xray_convert(): Anything in flux, F (erg/s/cm^2), but NOT flux density, F_lambda (erg/s/cm^2/Ang)
                ROSAT and XMM both use F.

"""
#Remember that the ouput on all of these needs to be:
# 10^(-17) erg/s/cm^2/Ang
def uk_convert(influx,inlam):
    #I will convert W/m^2/Hz from UKIDSS here
    flux_temp = influx*1000 #converts W/m^2/Hz to erg/s/cm^2/Hz
    flux_temp = flux_temp * (mks.c*10**(10)) * inlam**(-2.0) #converts /Hz to /Ang
    flux_temp = flux_temp * 1e17 #convert to SDSS version
    return flux_temp

def jy_convert(fjy,inlam):
    #I will convert Jy or mJy here
    flux_temp = fjy / 1000 #converts mJy to Jy
    flux_temp = flux_temp * 1e-26 #converts Jy to W/m^2/Hz
    flux_temp = uk_convert(flux_temp,inlam) #converts W/m^2/Hz to erg/s/cm^2/Ang
    #uk_convert() already reports in SDSS version
    return flux_temp

def nano_convert(influx,inlam):
    #I will convert nanomaggies from GALEX and WISE here
    #flux_source will be 'GALEX' or 'WISE' as they may be on separate systems
    flux_temp = influx * 3.631e-6 #converts to Jy
    flux_temp = flux_temp * 1e-26 #converts to W/m^2/Hz
    flux_temp = uk_convert(flux_temp,inlam) #converts W/m^2/Hz to erg/s/cm^2/Ang
    #uk_convert() already reports in SDSS version

    return flux_temp

def xray_convert(influx,inlam):
    #I will convert ROSAT/XMM here. They are in Flux, not flux density
    #ROSAT(2RXS) will have one data point. XMM will have 2 (soft/hard) or
    #3 (soft/hard/total). The inlam should be the center wavelength for the range
    #and must be input in Angstroms. Conversion from keV to Ang in sed_plotter()
    flux_temp = influx / inlam
    flux_temp = flux_temp * 1e17 #convert to SDSS scaling

    return flux_temp

#def tmass_convert(influx,inlam):
    #Convert 2MASS magnitudes here. An online program does this, check there:
    # http://ssc.spitzer.caltech.edu/warmmission/propkit/pet/magtojy/
    #2MASS conversions are not supported for DR16Q. The spectrum in the paper
    #used UKIDSS data for the wavelenghts in question.

def sed_plotter(infile,pt,mt,ft,fname,fntsize,write_check):
    dr = fits.open(infile)[1].data #Load the catalog file
    matplotlib.rc('font',size=fntsize)
    matplotlib.rcParams['text.latex.preamble'] = [r'\boldmath']

    drloc = ct.find_rec(dr,pt,mt,ft) #Find record for the specified quasar

    #Load the SDSS spectrum
    spec_iname = '../data/spec-{0}-{1}-{2:04d}.fits'.format(pt,mt,ft)
    spec_data = fits.open(spec_iname)[1].data
    spec_lam = 10**spec_data['loglam'] #wavelength stored as log10(lambda) in file.
    spec_err = spec_data['ivar'] #error stored as inverse variance per pixel
    wspec = np.where(spec_lam <= 10000)[0] #We don't want UKIDSS to overlap SDSS
    #Note that SDSS spectra above lambda=10,000 Ang are typically dominated by sky emission
    spec_flux = spec_data['flux']
    spec_fluxS, spec_errS = boxcar(spec_flux,spec_err,10,weighted=True) #10 pixel window size

    #Converting the GALEX data
    gal_data = np.zeros(2,dtype=[('LAM','float64'),('FLUX_RAW','float64'),('FLUX_CONV','float64'),
                                            ('FLUX_ERR_RAW','float64'),('FLUX_ERR_CONV','float64')])
    gal_data['LAM'][0] = 1550 #Angstroms, FUV bandpass = 1350 - 1750 Ang
    gal_data['LAM'][1] = 2275 #Angstroms, NUV bandpass = 1750 - 2800 Ang
    gal_data['FLUX_RAW'][0] = dr['FUV'][drloc]
    gal_data['FLUX_RAW'][1] = dr['NUV'][drloc]
    #Convert inverse variance to a 1-sigma uncertainty for error bars later
    gal_data['FLUX_ERR_RAW'][0] = 1/np.sqrt(dr['FUV_IVAR'][drloc])
    gal_data['FLUX_ERR_RAW'][1] = 1/np.sqrt(dr['NUV_IVAR'][drloc])
    #Convert GALEX flux and uncertainty to SDSS units
    gal_data['FLUX_CONV'] = nano_convert(gal_data['FLUX_RAW'],gal_data['LAM'])
    gal_data['FLUX_ERR_CONV'] = nano_convert(gal_data['FLUX_ERR_RAW'],gal_data['LAM'])

    #Converting the UKIDSS data
    ukid_data = np.zeros(4,dtype=[('LAM','float64'),('FLUX_RAW','float64'),('FLUX_CONV','float64'),
                                        ('FLUX_ERR_RAW','float64'),('FLUX_ERR_CONV','float64')])
    #Get passband centers in Angstroms (comments are in microns)
    ukid_data['LAM'][0] = 10200 #Y-band, Angstroms, 50% cuton/cutoff = 0.97/1.07 microns Y
    ukid_data['LAM'][1] = 12500 #J 1.17/1.33
    ukid_data['LAM'][2] = 16350 #H 1.49/1.78
    ukid_data['LAM'][3] = 22000 #K 2.03/2.37
    ukid_data['FLUX_RAW'][0] = dr['YFLUX'][drloc]
    ukid_data['FLUX_RAW'][1] = dr['JFLUX'][drloc]
    ukid_data['FLUX_RAW'][2] = dr['HFLUX'][drloc]
    ukid_data['FLUX_RAW'][3] = dr['KFLUX'][drloc]
    ukid_data['FLUX_ERR_RAW'][0] = dr['YFLUX_ERR'][drloc]
    ukid_data['FLUX_ERR_RAW'][1] = dr['JFLUX_ERR'][drloc]
    ukid_data['FLUX_ERR_RAW'][2] = dr['HFLUX_ERR'][drloc]
    ukid_data['FLUX_ERR_RAW'][3] = dr['KFLUX_ERR'][drloc]
    #Convert UKIDSS flux and error to SDSS units
    ukid_data['FLUX_CONV'] = uk_convert(ukid_data['FLUX_RAW'],ukid_data['LAM'])
    ukid_data['FLUX_ERR_CONV'] = uk_convert(ukid_data['FLUX_ERR_RAW'],ukid_data['LAM'])

    #Radio, X-ray, and WISE (infrared) data. Radio/X-ray does not appear on the plot
    #as these are so far out in wavelength space they are difficult to plot even on
    #a log scale. The flux ranges for these are also of significantly
    #lesser (radio)/greater (X-ray) magnitude to SDSS, that the y-axis also won't
    #work well. Converted here for instructional purposes, but not used in the plot.
    radxw_data = np.zeros(4,dtype=[('LAM','float64'),('FLUX_RAW','float64'),('FLUX_CONV','float64'),
                                    ('FLUX_ERR_RAW','float64'),('FLUX_ERR_CONV','float64')])
    #Convert ROSAT data (not used)
    radxw_data['LAM'][0] = ((mks.h * mks.c)/(1250*mks.eV))*(10**10) #Angstroms, ROSAT is 0.5 - 2.0 keV
    radxw_data['FLUX_RAW'][0] = dr['2RXS_SRC_FLUX'][drloc]
    radxw_data['FLUX_CONV'][0] = xray_convert(radxw_data['FLUX_RAW'][0],radxw_data['LAM'][0])
    radxw_data['FLUX_ERR_RAW'][0] = dr['2RXS_SRC_FLUX_ERR'][drloc]
    radxw_data['FLUX_ERR_CONV'][0] = xray_convert(radxw_data['FLUX_ERR_RAW'][0],radxw_data['LAM'][0])
    #Convert WISE infrared data (USED in plot)
    radxw_data['LAM'][1] = 34000 #Angstroms, W1 band center
    radxw_data['LAM'][2] = 46000 #Angstroms, W2 band center
    radxw_data['FLUX_RAW'][1] = dr['W1_FLUX'][drloc]
    radxw_data['FLUX_RAW'][2] = dr['W2_FLUX'][drloc]
    radxw_data['FLUX_CONV'][1:3] = nano_convert(radxw_data['FLUX_RAW'][1:3],radxw_data['LAM'][1:3])
    radxw_data['FLUX_ERR_RAW'][1] = 1/np.sqrt(dr['W1_FLUX_IVAR'][drloc])
    radxw_data['FLUX_ERR_RAW'][2] = 1/np.sqrt(dr['W2_FLUX_IVAR'][drloc])
    radxw_data['FLUX_ERR_CONV'][1:3] = nano_convert(radxw_data['FLUX_ERR_RAW'][1:3],radxw_data['LAM'][1:3])
    #Convert FIRST radio data (not used)
    radxw_data['LAM'][3] = 20e8 #Angstroms, 20cm for FIRST
    radxw_data['FLUX_RAW'][3] = dr['FIRST_FLUX'][drloc]
    radxw_data['FLUX_CONV'][3] = jy_convert(radxw_data['FLUX_RAW'][3],radxw_data['LAM'][3])
    radxw_data['FLUX_ERR_RAW'][3] = (dr['FIRST_FLUX'][drloc]-0.25)/dr['FIRST_SNR'][drloc]
    radxw_data['FLUX_ERR_CONV'][3] = jy_convert(radxw_data['FLUX_ERR_RAW'][3],radxw_data['LAM'][3])

    #Print the converted flux values for each band pass. Using the default spectrum
    #in the paper, this will show why FIRST and ROSAT were not plotted.
    print('\n')
    print('LAMBDA: FUV | FLUX: {:.3e} | ERR: {:.3e}'.format(gal_data['FLUX_CONV'][0],gal_data['FLUX_ERR_CONV'][0]))
    print('LAMBDA: NUV | FLUX: {:.3e} | ERR: {:.3e}'.format(gal_data['FLUX_CONV'][1],gal_data['FLUX_ERR_CONV'][1]))

    print('LAMBDA: Y | FLUX: {:.3e}'.format(ukid_data['FLUX_CONV'][0]))
    print('LAMBDA: J | FLUX: {:.3e}'.format(ukid_data['FLUX_CONV'][1]))
    print('LAMBDA: H | FLUX: {:.3e}'.format(ukid_data['FLUX_CONV'][2]))
    print('LAMBDA: K | FLUX: {:.3e}'.format(ukid_data['FLUX_CONV'][3]))

    print('LAMBDA: ROSAT | FLUX: {:.3e}'.format(radxw_data['FLUX_CONV'][0]))
    print('LAMBDA: W1 | FLUX: {:.3e}'.format(radxw_data['FLUX_CONV'][1]))
    print('LAMBDA: W2 | FLUX: {:.3e}'.format(radxw_data['FLUX_CONV'][2]))
    print('LAMBDA: FIRST | FLUX: {:.3e}'.format(radxw_data['FLUX_CONV'][3]))

    print('\nFIRST RAW: {:.3e}'.format(radxw_data['FLUX_RAW'][3]))

    #The bottom x-axis will be in the observed frame, the top will have the
    #wavelengths in the rest frame. These need different tick labels.
    xtick_obsLabs = np.array([1400,2000,4000,10000,20000,40000])
    xtick_restLabs = np.array([1000,2000,4000,10000,20000])

    #The SDSS name of the quasar and redshift to include in a text box.
    name_str = r'\textbf{SDSS J002912.35+022549.5, z = 0.576}'

    #Now we plot. Error bars for multiwave data may not display as they are
    #very small compared to the flux values (1 to 2 orders of magnitude smaller)
    fig,ax = plt.subplots(figsize=(10,4))
    axT = ax.twiny() #This makes a set of tick labels along the top (twin ALONG y-axis)
    #ax.errorbar(radxw_data['LAM'][0],radxw_data['FLUX_CONV'][0],yerr=radxw_data['FLUX_ERR_CONV'][0],label='ROSAT') #ROSAT DATA
    ax.errorbar(gal_data['LAM'],gal_data['FLUX_CONV'],yerr=gal_data['FLUX_ERR_CONV'],color='blue',alpha=0.7,fmt='d',label=r'\textbf{GALEX}') #GALEX DATA with ERROR
    ax.plot(spec_lam[wspec],spec_flux[wspec],linewidth=0.8,color='0.70',alpha=1.0,label=r'\textbf{SDSS Raw}') #SDSS DATA
    ax.plot(spec_lam[wspec],spec_fluxS[wspec],linewidth=0.6,color='black',alpha=1.0,label=r'\textbf{SDSS Smoothed}') #SDSS DATA
    ax.errorbar(ukid_data['LAM'],ukid_data['FLUX_CONV'],yerr=ukid_data['FLUX_ERR_CONV'],color='darkorange',alpha=1.0,fmt='^',label=r'\textbf{UKIDSS}') #UKIDSS DATA with ERROR
    ax.errorbar(radxw_data['LAM'][1:3],radxw_data['FLUX_CONV'][1:3],yerr=radxw_data['FLUX_ERR_CONV'][1:3],color='red',fmt='o',label=r'\textbf{WISE}') #WISE DATA with ERROR
    #ax.errorbar(radxw_data['LAM'][3],radxw_data['FLUX_CONV'][3],yerr=radxw_data['FLUX_ERR_CONV'][3],label='FIRST') #FIRST DATA

    ax.set_xscale('log') #Lower x-axis scale
    #Convert observed wavelengths to rest by lam_obs / (1+z)
    top_lim = np.array(ax.get_xlim())/1.576
    print('Rest Limits: ',top_lim) #Print these for verification by user
    #Top x-axis labels and scale
    axT.set_xlim(top_lim)
    axT.set_xscale('log')
    #Everything done to the bottom x-axis ticks needs to be done to the top (axT)
    ax.set_xticks(xtick_obsLabs)
    ax.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())
    axT.set_xticks(xtick_restLabs)
    axT.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())
    ax.set_xlabel(r'\textbf{Observed Frame Wavelength (\AA)}')
    axT.set_xlabel(r'\textbf{Rest Frame Wavelength (\AA)}')
    ax.set_ylabel(r'$f_{\lambda}$ ($10^{-17}$ \textbf{erg s}$^{-1}$\, \textbf{cm}$^{-2}$\, \textbf{\AA}$^{-1}$)')

    ##Bottom X-axis and Left/Right Y-axis minor ticks
    ax.tick_params(axis='both',direction='in')
    ax.tick_params(axis='both',which='minor',direction='in')

    #Right-side ticks
    ax.tick_params(right=True)
    ax.tick_params(which='minor',right=True)

    #Top minor ticks
    axT.tick_params(direction='in')
    axT.tick_params(which='minor',direction='in')
    ax.yaxis.set_minor_locator(ticker.MultipleLocator(10)) #Y-axis minor ticks locations
    ax.legend(loc='upper right',facecolor='white',edgecolor='white')

    #This calls the name_str from earlier. Puts it in the bottom left
    ax.text(0.01,0.1,name_str,transform=ax.transAxes, verticalalignment='top',bbox=dict(boxstyle='square,pad=0.2',fc='magenta',alpha=0.0))

    #Write out the plot, or just view it
    if write_check == 1:
        fig.savefig(fname,bbox_inches='tight',pad_inches=0.03,format='eps')
        plt.close()
    else:
        plt.tight_layout()
        plt.show()

#Call from the command line with:
#    python sed_plot.py <DR16Q file name> <plot name> <font size> <save flag>
# Example:
#    python sed_plot.py DR16Q_v3.fits sed.eps 11 1
if __name__=='__main__':
    input_file = '../data/{}'.format(sys.argv[1])
    output_plot_name = '../plots/{}'.format(sys.argv[2])
    plot_fontsize = int(sys.argv[3])
    save_check = int(sys.argv[4])
    pt,mt,ft = 7855,57011,530
    sed_plotter(input_file,pt,mt,ft,output_plot_name,plot_fontsize,save_check)


#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Fri Oct  2 11:08:09 2020

@author: alvarezguido
GITHUB: https://github.com/alvarezguido
"""

"""
SYNOPSIS
----
----
-----

"""

import simpy
import random
import numpy as np
import math
#import sys
#import re
#import matplotlib.pyplot as plt
#import os
#import operator
#from mpl_toolkits import mplot3d
#from mpl_toolkits.mplot3d import Axes3D
#import PIL
import random
import re
import os
import datetime
import sys

name = "ER"
mode_debbug = 0

if not mode_debbug:
    null = open(os.devnull, 'w')
    old_stdout = sys.stdout
    sys.stdout = null
####WE START BY USING SF=12 ADN BW=125 AND CR=1, FOR ALL NODES AND ALL TRANSMISIONS######

if mode_debbug:
    RANDOM_SEED = 5
    chan = 1
    packetlen = 20
    total_data = 60
    beacon_time = 120
    maxBSReceives = 500
    multi_nodes = [1500]
else:
    RANDOM_SEED = int(sys.argv[1])
    chan = int(sys.argv[2])
    packetlen = int(sys.argv[3])   ##NODES SEND PACKETS OF JUST 20 Bytes
    total_data = int(sys.argv[4]) ##TOTAL DATA ON BUFFER, FOR EACH NODE (IT'S THE BUFFER O DATA BEFORE START SENDING)
    beacon_time = int(sys.argv[5]) ###SAT SENDS BEACON EVERY CERTAIN TIME
    maxBSReceives = int(sys.argv[6]) ##MAX NUMBER OF PACKETS THAT BS (ie SATELLITE) CAN RECEIVE AT SAME TIME

    #multi_nodes = [int(sys.argv[7]), int(sys.argv[8]) ,int(sys.argv[9])]
    multi_nodes = [int(sys.argv[7]), int(sys.argv[8]) ,int(sys.argv[9]), int(sys.argv[10]),int(sys.argv[11]),int(sys.argv[12]),int(sys.argv[13]),int(sys.argv[14]),int(sys.argv[15]),int(sys.argv[16]),int(sys.argv[17]),int(sys.argv[18]),int(sys.argv[19]),int(sys.argv[20])]
random.seed(RANDOM_SEED) #RANDOM SEED IS FOR GENERATE ALWAYS THE SAME RANDOM NUMBERS (ie SAME RESULTS OF SIMULATION)
nodesToSend = []
packetsToSend = math.ceil(total_data/packetlen)
###GLOBAL PARAMS ####
bsId = 1 ##ID OF BASE STATION (NOT USED)

avgSendTime = 3  ## NOT USED! --> A NODE SENDS A PACKET EVERY X SECS

back_off = beacon_time * 0.95 ###BACK OFF TIME FOR SEND A PACKET
packetsAtBS = [] ##USED FOR CHEK IF THERE ARE ALREADY PACKETS ON THE SATELLITE
c = 299792.458 ###SPEED LIGHT [km/s]
Ptx = 14
G_device = 0; ##ANTENNA GAIN FOR AN END-DEVICE
G_sat = 12;   ##ANTENNA GAIN FOR SATELLITE
nodes = [] ###EACH NODE WILL BE APPENDED TO THIS VARIABLE
freq =868e6 ##USED FOR PATH LOSS CALCULATION
frequency = [868100000, 868300000, 868500000] ##FROM LORAWAN REGIONAL PARAMETERS EU863-870 / EU868

nrLost = 0 ### TOTAL OF LOST PACKETS DUE Lpl
nrCollisions = 0 ##TOTAL OF COLLIDED PACKETS
nrProcessed = 0 ##TOTAL OF PROCESSED PACKETS
nrReceived = 0 ###TOTAL OF RECEIVED PACKETS
nrNoProcessed = 0 ##TOTAL OF INTRA-PACKETS NO PROCESSED
nrIntraTot = 0
nrLostMaxRec = 0
nrCollFullPacket = 0
nrSentIntra = 0 ##TOTAL OF SENT INTRA-PACKTES
nrReceivedIntra = 0 ##TOTAL OF RECEIVED INTRA-PACKETS

##ARRAY WITH MEASURED VALUES FOR SENSIBILITY, NEW VALUES
##THE FOLLOWING VALUES CORRESPOND TO:
#   - FIRST ELEMENT: IT'S THE SF (NOT USABLE)
#   - SECOND ELEMENT: SENSIBILITY FOR 125KHZ BW
#   - THIRD ELEMENT: SENSIBILITY FOR 250KHZ BW
#   - FOURTH ELEMENT: SENSIBILITY FOR 500KHZ BW
# NOTICE THAT SENSIBILITY DECREASE ALONG BW INCREASES, ALSO WITH LOWER SF
# THIS VALUES RESPONDS TO:
# wf = -174 + 10 log(BW) +NF +SNRf
sf7 = np.array([7,-123,-120,-117.0])
sf8 = np.array([8,-126,-123,-120.0])
sf9 = np.array([9,-129,-126,-123.0])
sf10 = np.array([10,-132,-129,-126.0])
sf11 = np.array([11,-134.53,-131.52,-128.51])
sf12 = np.array([12,-137,-134,-131.0])

sensi = np.array([sf7,sf8,sf9,sf10,sf11,sf12])

#DR = ["dr8","dr9","dr10","dr11"]
DR = [-137,-134.5,-134,-131.5]

## READ PARAMS FROM DIRECTORY ##
path = "./wider_scenario_2/"

### -137dB IS THE MINIMUN TOLERABLE SENSIBILITY, FOR SF=12 AND BW=125KHz ###

leo_pos=np.loadtxt( path + "LEO-XYZ-Pos.csv",skiprows=1,delimiter=',',usecols=(1,2,3))
## WHERE:
    ## leo_pos[i,j]:
        ## i --> the step time in sat pass
        ## j --> 0 for x-position, 1 for y-position, 2 for z-position

sites_pos = np.loadtxt( path + "SITES-XYZ-Pos.csv",skiprows=1,delimiter=',',usecols=(1,2,3))
## WHERE:
    ## sites_pos[i,j]:
        ## i --> the node i
        ## j --> 0 for x-position, 1 for y-position, 2 for z-position

dist_sat = np.zeros((sites_pos.shape[0],3,leo_pos.shape[0]))
t = 0
for i in range(leo_pos.shape[0]):
    t+=1
    dist_sat [:,:,i] = leo_pos[i,:] - sites_pos
## WHERE:
    ## dist_sat[i,j,k]:
        ## i --> the node i
        ## j --> 0 for x-position, 1 for y-position, 2 for z-position
        ## k --> the step time in sat pass

#### FOR COMPUTE DISTANCE MAGNITUDE (ABS) FROM END-DEVICE TO SAT PASSING BY ####
distance = np.zeros((sites_pos.shape[0],leo_pos.shape[0]))
distance[:,:] = (dist_sat[:,0,:]**2 + dist_sat[:,1,:]**2 + dist_sat[:,2,:]**2)**(1/2)
## WHERE:
    ## distance[i,j]:
        ## i --> the node i
        ## j --> the step time in sat pass

##MATRIX FOR LINK BUDGET Lpl ###
Lpl = np.zeros((sites_pos.shape[0],leo_pos.shape[0]))
Lpl = 20*np.log10(distance*1000) + 20*np.log10(freq) - 147.55 #DISTANCE MUST BE IN METERS
## WHERE:
    ## Lpl[i,j]:
        ## i --> the node i
        ## j --> the step time in sat pass

##MATRIX FOR LINK BUDGET, USING Prx ###
Prx = np.zeros((sites_pos.shape[0],leo_pos.shape[0]))
Prx = Ptx + G_sat + G_device -20*np.log10(distance*1000) - 20*np.log10(freq) + 147.55 #DISTANCE IS CONVERTED TO METERS
## WHERE:
    ## Prx[i,j]:
        ## i --> the node i
        ## j --> the step time in sat pass

distance = np.concatenate((distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance,\
                           distance,distance,distance,distance,distance,distance,distance,distance,distance,distance))

Lpl = np.concatenate((Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,\
                      Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl,Lpl))

Prx = np.concatenate((Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,\
                      Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx,Prx))

elev = np.degrees(np.arcsin(599/distance))

# =============================================================================
# IS7 = np.array([1,-8,-9,-9,-9,-9])
# IS8 = np.array([-11,1,-11,-12,-13,-13])
# IS9 = np.array([-15,-13,1,-13,-14,-15])
# IS10 = np.array([-19,-18,-17,1,-17,-18])
# IS11 = np.array([-22,-22,-21,-20,1,-20])
# IS12 = np.array([-25,-25,-25,-24,-23,1])
# IsoThresholds = np.array([IS7,IS8,IS9,IS10,IS11,IS12])
#
# =============================================================================
ISDR8 = np.array([1,-23,-24,-25])
ISDR9 = np.array([-20,1,-20,-21])
ISDR10 = np.array([-18,-17,1,-17])
ISDR11 = np.array([-15,-14,-13,1])

IsoThresholds = np.array([ISDR8,ISDR9,ISDR10,ISDR11])

#THIS IS THE MATRIX OF CROSS INTERFERENCE BETWEEN SF

Collmap = [[0 for i in range(0,6)] for j in range(0,6)]

def simulate_scenario (nrNodes):
    env = simpy.Environment()

    def powerCollision_2(p1, p2):
        #powerThreshold = 6
        global Collmap
        #print ("SF: node {} has {} ; node {} has {}".format(p1.nodeid,p1.sf, p2.nodeid, p2.sf))
        #print ("pwr: node {} with rssi {} dBm and node {} with rssi {} dBm; diff {:3.2f} dBm".format(p1,p1.rssi[math.ceil(env.now)], p2.nodeid,p2.rssi[math.ceil(env.now)], p1.rssi[math.ceil(env.now)] - p2.rssi[math.ceil(env.now)]))
        if True: #p1.sf == p2.sf:
           if abs(p1.rssi[math.ceil(env.now)] - p2.rssi[math.ceil(env.now)]) < IsoThresholds[p1.dr-8][p2.dr-8]:
               print ("collision pwr both node {} and node {}".format(p1.nodeid, p2.nodeid))
               #Collmap[p1.sf-7][p2.sf-7] += 1
               #Collmap[p2.sf-7][p1.sf-7] += 1
               # packets are too close to each other, both collide
               # return both packets as casualties
               return (p1, p2)
           elif p1.rssi[math.ceil(env.now)] - p2.rssi[math.ceil(env.now)] < IsoThresholds[p1.dr-8][p2.dr-8]:
               # p2 overpowered p1, return p1 as casualty
               print ("collision pwr node {} overpowered node {}".format(p2.nodeid, p1.nodeid))
               print ("capture - p2 wins, p1 lost")
               #Collmap[p1.sf-7][p2.sf-7] += 1
               return (p1,)
           print ("capture - p1 wins, p2 lost")
           # p2 was the weaker packet, return it as a casualty
           #Collmap[p2.sf-7][p1.sf-7] += 1
           return (p2,)

    def checkcollision(packet):
        col = 0 # flag needed since there might be several collisions for packet
        processing = 0
        #print ("MAX RECEIVE IS: ", maxBSReceives)
        for i in range(0,len(packetsAtBS)):
            if packetsAtBS[i].header.processed == 1 or packetsAtBS[i].intraPacket.processed == 1:
                processing = processing + 1
        if (processing > maxBSReceives):
            packet.processed = 0
        else:
            packet.processed = 1
        if packetsAtBS:
            #print ("{:3.5f} || >> FOUND overlap... node {} (sf:{} bw:{} freq:{}) others: {}".format(env.now,packet.nodeid, packet.sf, packet.bw,packet.freq,len(packetsAtBS)))
            for other in packetsAtBS:
                if other.nodeid != packet.nodeid:
                   #print ("{:3.5f} || >> node {} overlapped with node {} (sf:{} bw:{} freq:{}). Let's check Freq...".format(env.now,packet.nodeid, other.nodeid, other.packet.sf, other.packet.bw,other.packet.freq))
                   # simple collision
                   #if frequencyCollision(packet, other.packet) and sfCollision(packet, other.packet):
                   if frequencyCollision(packet, other.header):# and timingCollision(packet, other.packet):
                       c = powerCollision_2(packet, other.header)
                       for p in c:
                          p.col = 1
                          if p == packet:
                             col = 1
                   if frequencyCollision(packet, other.intraPacket):# and timingCollision(packet, other.packet):
                       c = powerCollision_2(packet, other.intraPacket)
                       for p in c:
                          p.col = 1
                          if p == packet:
                             col = 1
            return col
        return 0

    def frequencyCollision(p1,p2):
        if (p1.ch == p2.ch):
            #print ("{:3.5f} || >> same channel for header on node {} and node {}.. Let's check sub-channels...".format(env.now,p1.nodeid,p2.nodeid))
            #if (p1.freqHopHeader[replica] == p2.freqHopHeader[replica]):
            if (p1.subCh == p2.subCh):
                #print ("{:3.5f} || >> same sub-channel for header on node {} and node {}".format(env.now,p1.nodeid,p2.nodeid))
                #print ("{:3.5f} || >> Header {} from node {} collided!!!".format(env.now,replica,p1.nodeid))
                return True
            else:
                #print ("{:3.5f} || >> No sub-channel collision".format(env.now))
                return False
        else:
            #print ("{:3.5f} || >> No header channel collision..".format(env.now))
            return False

    class myNode():
        def __init__(self, nodeid, bs, avgSendTime, packetlen, total_data):
            global channel
            global DR
            self.dr = 8
            #carriers = list(range(280))
            #random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
            self.nodeid = nodeid
            self.avgSendTime = avgSendTime
            self.bs = bs
            self.dist = distance[nodeid,:]
            self.elev = elev[nodeid,:]
            self.mindist = np.amin(distance[nodeid,:])
            self.mindist_pos = int(np.where(distance[nodeid,:] == np.amin(distance[nodeid,:]))[0])
            #print('node %d' %nodeid, "dist: ", self.dist[0])
            self.buffer = total_data
            self.packetlen = packetlen
            self.ch = int(random.choice(channel))
            self.packet = myPacket(self.nodeid, packetlen, self.dist)
            #self.freqHop = carriers[0:35]
            self.sent = 0 #INITIAL SENT PACKETS
            self.totalLost = 0 #INITIAL TOTAL LOST FOR PARTICULAR NODE
            self.totalColl = 0
            self.totalRec = 0
            self.totalProc = 0
            if self.dr == 8:
                carriers = list(range(280))
                random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
                self.freqHop = carriers[0:35]
            elif self.dr == 9:
                carriers = list(range(280))
                random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
                self.freqHop = carriers[0:35]
            elif self.dr == 10:
                carriers = list(range(688))
                random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
                self.freqHop = carriers[0:86]
            elif self.dr == 11:
                carriers = list(range(688))
                random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
                self.freqHop = carriers[0:86]

            self.header = myHeader(self.nodeid,self.dist,self.ch,self.freqHop, self.dr)
            self.intraPacket = myIntraPacket(self.nodeid,self.dist,self.ch,self.freqHop,self.dr)

    class myHeader ():
        def __init__(self,nodeid,dist,ch,freqHop,dr):
            global Ptx
            global Prx
            global Lpl
            global c
            global distance
            global channel
            global frequency
            self.nodeid = nodeid
            self.txpow = Ptx
            self.transRange = 150
            self.arriveTime = 0
            self.rssi = Prx[nodeid,:]
            self.rectime = 0.233
            #self.rectime = 1.5
            self.proptime = distance[nodeid,:]*(1/c)
            self.collided = 0
            self.noCollided = 0
            self.processed = 0
            self.noProcessed = 0
            self.ch = ch
            self.lost = bool
            self.Nlost = 0
            self.subCh = 0
            self.sentIntra = 0
            self.dr = dr
            self.col =0
            if dr == "dr8":
                self.freqHopHeader = freqHop[0:3]
            elif dr == "dr9":
                self.freqHopHeader = freqHop[0:2]
            elif dr == "dr10":
                self.freqHopHeader = freqHop[0:3]
            elif dr == "dr11":
                self.freqHopHeader = freqHop[0:2]

    class myIntraPacket ():
        def __init__(self,nodeid,dist,ch,freqHop,dr):
            global Ptx
            global Prx
            global Lpl
            global c
            global distance
            global channel
            global frequency
            self.nodeid = nodeid
            self.txpow = Ptx
            self.transRange = 150
            self.arriveTime = 0
            self.rssi = Prx[nodeid,:]
            self.freqHopIntraPacket = freqHop[3:]
            self.rectime = 50e-3
            #self.rectime = 3
            self.proptime = distance[nodeid,:]*(1/c)
            self.collided = 0
            self.noCollided = 0
            self.nrColl = 0
            self.processed = 0
            self.noProcessed = 0
            self.ch = ch
            self.lost = bool
            self.Nlost = 0
            self.subCh = 0
            self.sentIntra = 0
            self.dr = dr
            self.col =0
            if dr == "dr8":
                self.freqHopIntraPacket = freqHop[3:]
            elif dr == "dr9":
                self.freqHopIntraPacket = freqHop[2:]
            elif dr == "dr10":
                self.freqHopIntraPacket = freqHop[3:]
            elif dr == "dr11":
                self.freqHopIntraPacket = freqHop[2:]

    class myPacket():
        def __init__(self, nodeid, packetlen, dist):
            #global experiment
            global Ptx
            global Prx
            #global gamma
            #global d0
            #global var
            global Lpl
            #global freq
            #global GL
            global c
            global distance
            global channel
            global frequency
            #SF = [7,8,9,10,11,12]
            self.nodeid = nodeid
            self.txpow = Ptx
            #self.sf = random.choice(SF)
            self.sf = 12
            self.cr = 1 ##CODING RATE
            self.bw = 125
            # transmission range, needs update XXX
            self.transRange = 150
            self.pl = packetlen
            self.symTime = (2.0**self.sf)/self.bw
            self.arriveTime = 0
            self.rssi = Prx[nodeid,:]
            self.freq = int(random.choice(frequency))
            self.rectime = airtime(self.sf,self.cr,self.pl,self.bw) ##RECTIME IS THE RECEPTION TIME (ie AIRTIME)
            self.proptime = distance[nodeid,:]*(1/c)
            #print ("rectime node ", self.nodeid, "  ", self.rectime)
            #print ("Airtime for node {} is {} [seconds]".format(self.nodeid,self.rectime)) #from https://www.loratools.nl/#/airtime
            # denote if packet is collided
            self.collided = 0
            self.processed = 0
            self.lost = bool

    def airtime(sf,cr,pl,bw):
        H = 0        # implicit header disabled (H=0) or not (H=1)
        DE = 0       # low data rate optimization enabled (=1) or not (=0)
        Npream = 8   # number of preamble symbol (12.25  from Utz paper)

        if bw == 125 and sf in [11, 12]:
            # low data rate optimization mandated for BW125 with SF11 and SF12
            DE = 1
        if sf == 6:
            # can only have implicit header with SF6
            H = 1

        Tsym = (2.0**sf)/bw
        Tpream = (Npream + 4.25)*Tsym
        #print ("PARAMS FOR TRANSMISION: sf", sf, " cr", cr, "pl", pl, "bw", bw)
        payloadSymbNB = 8 + max(math.ceil((8.0*pl-4.0*sf+28+16-20*H)/(4.0*(sf-2*DE)))*(cr+4),0)
        Tpayload = payloadSymbNB * Tsym
        return ((Tpream + Tpayload)/1000) ##IN SECS

    def selectDR (env,node):
        global DR
        dr = [8,9,10,11]
        node.dr = random.choice(dr)
        node.header.dr = node.dr
        node.intraPacket.dr = node.dr
        if node.dr == 11:
            node.sensi = DR[3]
            carriers = list(range(688))
            random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
            node.freqHop = carriers[0:86]
            node.header.freqHopHeader = node.freqHop[0:2]
            node.intraPacket.freqHopIntraPacket = node.freqHop [2:]
        elif node.dr == 10:
            node.sensi = DR[2]
            carriers = list(range(688))
            random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
            node.freqHop = carriers[0:86]
            node.header.freqHopHeader = node.freqHop[0:3]
            node.intraPacket.freqHopIntraPacket = node.freqHop [3:]
        elif node.dr == 9:
            node.sensi = DR[1]
            carriers = list(range(280))
            random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
            node.freqHop = carriers[0:35]
            node.header.freqHopHeader = node.freqHop[0:2]
            node.intraPacket.freqHopIntraPacket = node.freqHop [2:]
        else:
            node.sensi = DR[0]
            carriers = list(range(280))
            random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
            node.freqHop = carriers[0:35]
            node.header.freqHopHeader = node.freqHop[0:3]
            node.intraPacket.freqHopIntraPacket = node.freqHop [3:]

    def transmit(env,node):
        #while nodes[node.nodeid].buffer > 0.0:
        global wait_min
        global wait_max
        global back_off
        global beacon_time
        global logs
        global nodesToSend
        global DR
        while node.buffer > 0.0:
            #######STARTS TRANSMISSION AS DR8
            node.dr = 8
            node.sensi = -137
            carriers = list(range(280))
            random.shuffle(carriers) #TO CHOOSE THE HOPPING JUMPS
            node.freqHop = carriers[0:35]
            node.header.freqHopHeader = node.freqHop[0:3]
            node.intraPacket.freqHopIntraPacket = node.freqHop [3:]
            #######
            yield env.timeout(node.packet.rectime + float(node.packet.proptime[math.ceil(env.now)])) ##GIVE TIME TO RECEIVE BEACON

            if node in packetsAtBS:
                print ("{:3.5f} || ERROR: packet is already in...".format(env.now))
            else:
                sensibility = sensi[12 - 7, [125,250,500].index(node.packet.bw) + 1]
                if node.packet.rssi[math.ceil(env.now)] < sensibility: #HERE WE ARE CONSIDERING RSSI AT TIME ENV.NOW
                    print ("{:3.5f} || Node {}: Can not reach beacon due Lpl".format(env.now,node.nodeid))
                    wait =0 ##LETS WAIT FOR NEXT BEACON
                    node.header.lost = False
                    node.intraPacket.lost = False
                    trySend = False
                    nIntraPackets = 0
                else:
                    selectDR(env,node)
                    nodesToSend.append(node.nodeid)
                    wait = random.uniform(1,back_off - node.packet.rectime - float(node.packet.proptime[math.ceil(env.now)])) ##TRIGGER BACK-OFF TIME
                    yield env.timeout(wait)
                    #print ("{:3.5f} || Node {} begins to transmit a packet".format(env.now,node.nodeid))
                    trySend = True
                    node.sent = node.sent + 1
                    node.buffer = node.buffer - node.packetlen
                    if node in packetsAtBS:
                        print ("{} || ERROR: packet is already in...".format(env.now))
                    else:
                        #sensibility = sensi[node.packet.sf - 7, [125,250,500].index(node.packet.bw) + 1]
                        sensibility = node.sensi
                        #print ("------Sensi is: ",sensibility)
                        if node.packet.rssi[math.ceil(env.now)] < sensibility: #HERE WE ARE CONSIDERING RSSI AT TIME ENV.NOW
                            print ("{:3.5f} || Node {}: The Packet will be Lost due Lpl".format(env.now,node.nodeid))
                            node.header.lost = True ## LOST ONLY CONSIDERING Lpl
                            node.intraPacket.lost = True ## LOST ONLY CONSIDERING Lpl
                            #nIntraPackets = 0
                            print ("###############lost !!!!!!!!")
                        else:
                            node.header.lost = False ## LOST ONLY CONSIDERING Lpl
                            node.intraPacket.lost = False ## LOST ONLY CONSIDERING Lpl
                            #print ("{:3.5f} || Prx for node {} is {:3.2f} dB".format(env.now, node.nodeid, node.packet.rssi[math.ceil(env.now)]))
                            #print ("Prx for node",node.nodeid, "is: ",node.packet.rssi[math.ceil(env.now)],"at time",env.now)

                        for i in range(len(node.header.freqHopHeader)):
                            ###print ("{:3.5f} || Sending Header replica {} node {}...".format(env.now,i,node.nodeid))
                            ###print ("{:3.5f} || Let's try if there are collisions...".format(env.now))
                            node.header.subCh = node.header.freqHopHeader[i]
                            #print ("SUBCHANELLLL: ",node.header.subCh)
                            node.header.sentIntra +=1
                            isLost =0
                            if node.packet.rssi[math.ceil(env.now)] < sensibility:
                                node.header.Nlost +=1
                                isLost =1
                            if (checkcollision(node.header)==1):
                                #pass
                                if node.header.col == 1:
                                    if isLost == 0:
                                        node.header.collided +=1
                                #node.packet.collided = 1
                                #print ("---{:3.5f} || Collision for Header replica {} node {} !!!".format(env.now,i,node.nodeid))
                                #node.packet.collided = 1
                                #node.header.collided +=1 #ALREADY COUNTED IN FUNCTION
                            else:
                                ###print ("{:3.5f} || ...No Collision for Header replica {} node {}!".format(env.now,i,node.nodeid))
                                #node.packet.collided = 0
                                node.header.noCollided = 1 ##ALMOST ONE HEADER IS OK, THEN HEADER IS OK
                            packetsAtBS.append(node)
                            node.packet.addTime = env.now
                            isLost =0
                            yield env.timeout(node.header.rectime)
                            if (node in packetsAtBS):
                                packetsAtBS.remove(node)

                        ##CALCULATE N OF INTRAPACKETS BASED ON PACKETLEN
                        #airtime(12,1,node.packetlen,125)
                        if node.dr == 8 or node.dr == 10:
                            payloadTime = 1.85 - 0.233*3
                        elif node.dr == 9 or node.dr ==11:
                            payloadTime = 1.07 - 0.233*2

                        nIntraPackets = math.ceil(payloadTime / 50e-3)
                        #print ("NUMBER OF INTRA PACKETSSSS",nIntraPackets)

                        for j in range (nIntraPackets):
                            ###print ("{:3.5f} || Sending intra-packet {} of {} for node {}...".format(env.now,j,nIntraPackets-1,node.nodeid))
                            ###print ("{:3.5f} || Let's try if there are collisions...".format(env.now))
                            node.intraPacket.subCh = node.intraPacket.freqHopIntraPacket[j]
                            node.intraPacket.sentIntra +=1
                            isLost =0
                            if node.packet.rssi[math.ceil(env.now)] < sensibility:
                                node.intraPacket.Nlost +=1
                                isLost =1
                            #print ("INTRA-PACKT SUB CHANNELLLL", node.intraPacket.subCh)
                            if (checkcollision(node.intraPacket)==1):
                                #pass
                                if node.intraPacket.col ==1:
                                    if isLost == 0:
                                        node.intraPacket.collided+=1
                                #print ("---{:3.5f} || Collision for intra-packet {} for node {} !!!".format(env.now,j,node.nodeid))
                                #node.intraPacket.collided+=1 #ALREADY COUNTED ON FUNCTION
                            else:
                                ###print ("{:3.5f} || ...No Collision for intra-packet {} for node {}!".format(env.now,j,node.nodeid))
                                node.intraPacket.noCollided +=1
                                pass
                            packetsAtBS.append(node)
                            node.packet.addTime = env.now
                            yield env.timeout(node.intraPacket.rectime)
                            if (node in packetsAtBS):
                                packetsAtBS.remove(node)
                            #print ("INTRA-PACKET NO-PROCESEDDD",node.intraPacket.noProcessed)
                            isLost =0

            node.header.noCollided = len(node.header.freqHopHeader)-node.header.Nlost-node.header.collided
            node.intraPacket.noCollided = nIntraPackets-node.intraPacket.Nlost-node.intraPacket.collided

            if node.header.noCollided <0:
                node.header.noCollided = 0
            if node.intraPacket.noCollided <0:
                node.intraPacket.noCollided = 0

            if trySend == 1:
                #print ("----count intra-packet collided", node.intraPacket.collided)
                if node.header.lost or node.intraPacket.lost:
                    logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},PL,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                else:
                    if node.dr ==8 or node.dr==10:
                        if node.header.collided == 3:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},PCh,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        elif node.intraPacket.collided > (1/3)*nIntraPackets:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},PCp,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        elif node.header.noProcessed == 3:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},NP,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        elif node.intraPacket.noProcessed > (1/3)*nIntraPackets:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},NP,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        else:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},PE,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))

                    elif node.dr==9 or node.dr==11:
                        if node.header.collided == 2:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},PCh,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        elif node.intraPacket.collided > (2/3)*nIntraPackets:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},PCp,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        elif node.header.noProcessed == 2:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},NP,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        elif node.intraPacket.noProcessed > (2/3)*nIntraPackets:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},NP,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))
                        else:
                            logs.append("{:3.3f},{},{:3.3f},{:3.3f},{},PE,#{},#{},#{},#{}".format(env.now,node.nodeid,node.dist[math.ceil(env.now)],node.elev[math.ceil(env.now)],node.dr,nIntraPackets,node.intraPacket.noCollided,len(node.header.freqHopHeader),node.header.noCollided))

            ##RESET
            node.header.collided = 0
            node.header.processed = 0
            node.header.noProcessed = 0
            node.header.lost = False
            node.header.noCollided =0
            node.intraPacket.nrColl = 0
            node.intraPacket.collided = 0
            node.intraPacket.processed = 0
            node.intraPacket.noProcessed = 0
            node.intraPacket.lost = False
            node.intraPacket.noCollided = 0
            node.header.sentIntra = 0
            node.intraPacket.sentIntra = 0
            node.header.Nlost =0
            node.intraPacket.Nlost = 0

            if trySend:
                #print ("BEACON TIMEEE",beacon_time)
                #print ("WAITTT",wait)
                #print ("NODE HEADER TIME",node.header.rectime)
                #print ("ONE INTRA-PACKET TIMEE",node.intraPacket.rectime)
                #yield env.timeout(beacon_time-wait)
                yield env.timeout(beacon_time-wait-2*3*node.header.rectime-2*nIntraPackets*node.intraPacket.rectime)
            else:
                nIntraPackets = 0
                yield env.timeout(beacon_time-wait-3*node.header.rectime-nIntraPackets*node.intraPacket.rectime)

    def beacon (env):
        global beacon_time
        global nodesToSend
        global logs
        i = 0
        while True:
            if i == 0:
                yield env.timeout(0)
            else:
                yield env.timeout(beacon_time-2)
            i=i+1
            print ("{:3.5f} || ***A new beacon has been sended from Satellite***".format(env.now))
            yield env.timeout(2)
            logs.append("{:3.3f},B,{}".format(env.now,nodesToSend))
            nodesToSend = []

    env.process(beacon(env)) ##BEACON SENDER

    ### THIS IS GOING TO CREATE NODES AND DO TRAMSMISIONS. IS THE MAIN PROGRAM ###
    for i in range(nrNodes):
        node = myNode(i,bsId, avgSendTime, packetlen, total_data)
        nodes.append(node)
        env.process(transmit(env,node))

    env.run(until=600*2)

    sent = sum(n.sent for n in nodes)
    return ([sent,nrCollFullPacket,None,None,nrReceived],logs)

#########################################################################
if chan == 1:
    ###SCENARIO 1 CHANNEL###
    channel = [0]

    nodes = [] ###EACH NODE WILL BE APPENDED TO THIS VARIABLE
    nrLost = 0 ### TOTAL OF LOST PACKETS DUE Lpl
    nrCollisions = 0 ##TOTAL OF COLLIDED PACKETS
    nrProcessed = 0 ##TOTAL OF PROCESSED PACKETS
    nrReceived = 0 ###TOTAL OF RECEIVED PACKETS
    nrNoProcessed = 0 ##TOTAL OF INTRA-PACKETS NO PROCESSED
    nrIntraTot = 0
    nrLostMaxRec = 0
    nrCollFullPacket = 0
    nrSentIntra = 0 ##TOTAL OF SENT INTRA-PACKTES
    nrReceivedIntra = 0 ##TOTAL OF RECEIVED INTRA-PACKETS

    i =0
    scenario_1ch = np.zeros((len(multi_nodes),5))
    results = []
    ## WHERE:
        ## scenario_1ch[i,j]:
            ## i --> the node i
            ## j --> [sent, nrCollisions, nrLost, nrProcessed, nrReceived]

    for nrNodes in multi_nodes:
        print ("\n\n***NEW SCENARIO BEGINS***\n")
        logs = []
        results,logs = simulate_scenario(nrNodes)
        scenario_1ch[i,:] = results
        folder = name+'_1CH_s'+str(RANDOM_SEED)+'_p'+str(packetsToSend)
        if not os.path.exists(folder):
            os.makedirs(folder)
        fname = "./"+folder+"/" + str(name+"_"+str(nrNodes)+"_1CH_"+str(maxBSReceives)+"_s"+str(RANDOM_SEED)+"_p"+str(packetsToSend)) + ".csv"
        with open(fname,"w") as myfile:
            myfile.write("\n".join(logs))
        myfile.close()
        i=i+1
        if not mode_debbug:
            nodes = [] ###EACH NODE WILL BE APPENDED TO THIS VARIABLE
        nrLost = 0 ### TOTAL OF LOST PACKETS DUE Lpl
        nrCollisions = 0 ##TOTAL OF COLLIDED PACKETS
        nrProcessed = 0 ##TOTAL OF PROCESSED PACKETS
        nrReceived = 0 ###TOTAL OF RECEIVED PACKETS
        nrNoProcessed = 0 ##TOTAL OF INTRA-PACKETS NO PROCESSED
        nrIntraTot = 0
        nrLostMaxRec = 0
        nrCollFullPacket = 0
        nrSentIntra = 0 ##TOTAL OF SENT INTRA-PACKTES
        nrReceivedIntra = 0 ##TOTAL OF RECEIVED INTRA-PACKETS

#########################################################################
if chan ==3:
    ###SCENARIO 3 CHANNELS###
    channel = [0,1,2]
    nodes = [] ###EACH NODE WILL BE APPENDED TO THIS VARIABLE
    nrLost = 0 ### TOTAL OF LOST PACKETS DUE Lpl
    nrCollisions = 0 ##TOTAL OF COLLIDED PACKETS
    nrProcessed = 0 ##TOTAL OF PROCESSED PACKETS
    nrReceived = 0 ###TOTAL OF RECEIVED PACKETS
    nrNoProcessed = 0 ##TOTAL OF INTRA-PACKETS NO PROCESSED
    nrIntraTot = 0
    nrLostMaxRec = 0
    nrCollFullPacket = 0
    nrSentIntra = 0 ##TOTAL OF SENT INTRA-PACKTES
    nrReceivedIntra = 0 ##TOTAL OF RECEIVED INTRA-PACKETS
    i =0
    scenario_3ch = np.zeros((len(multi_nodes),5))
    results = []

    for nrNodes in multi_nodes:
        print ("\n\n***NEW SCENARIO BEGINS***\n")
        logs = []
        results,logs = simulate_scenario(nrNodes)
        scenario_3ch[i,:] = results
        folder = name+'_3CH_s'+str(RANDOM_SEED)+'_p'+str(packetsToSend)
        if not os.path.exists(folder):
            os.makedirs(folder)
        fname = "./"+folder+"/" + str(name+"_"+str(nrNodes)+"_3CH_"+str(maxBSReceives)+"_s"+str(RANDOM_SEED)+"_p"+str(packetsToSend)) + ".csv"
        with open(fname,"w") as myfile:
            myfile.write("\n".join(logs))
        myfile.close()
        i=i+1
        if not mode_debbug:
            nodes = [] ###EACH NODE WILL BE APPENDED TO THIS VARIABLE
        nrLost = 0 ### TOTAL OF LOST PACKETS DUE Lpl
        nrCollisions = 0 ##TOTAL OF COLLIDED PACKETS
        nrProcessed = 0 ##TOTAL OF PROCESSED PACKETS
        nrReceived = 0 ###TOTAL OF RECEIVED PACKETS
        nrNoProcessed = 0 ##TOTAL OF INTRA-PACKETS NO PROCESSED
        nrIntraTot = 0
        nrLostMaxRec = 0
        nrCollFullPacket = 0
        nrSentIntra = 0 ##TOTAL OF SENT INTRA-PACKTES
        nrReceivedIntra = 0 ##TOTAL OF RECEIVED INTRA-PACKETS

if not mode_debbug:
    sys.stdout = old_stdout
    print("done ",name)


"""Implementation on top of MultiNest via PyMultiNest.

This module defines the class for Nested Sampling using the MultiNest
program via its Python wrapper PyMultiNest. Note that PyMultiNest and MultiNest
have to be built and installed separately (from gleipnir) before this module
can be used.

PyMultiNest: https://github.com/JohannesBuchner/PyMultiNest
MultiNest: https://github.com/JohannesBuchner/MultiNest

References:
    MultiNest:
    1. Feroz, Farhan, and M. P. Hobson. "Multimodal nested sampling: an
        efficient and robust alternative to Markov Chain Monte Carlo
        methods for astronomical data analyses." Monthly Notices of the
        Royal Astronomical Society 384.2 (2008): 449-463.
    2. Feroz, F., M. P. Hobson, and M. Bridges. "MultiNest: an efficient
        and robust Bayesian inference tool for cosmology and particle
        physics." Monthly Notices of the Royal Astronomical Society 398.4
        (2009): 1601-1614.
    3. Feroz, F., et al. "Importance nested sampling and the MultiNest
        algorithm." arXiv preprint arXiv:1306.2144 (2013).
    PyMultiNest:
    4. Buchner, J., et al. "X-ray spectral modelling of the AGN obscuring
        region in the CDFS: Bayesian model selection and catalogue."
        Astronomy & Astrophysics 564 (2014): A125.

"""

import numpy as np
import warnings
from .nsbase import NestedSamplingBase

try:
    import pymultinest
    from pymultinest.solve import solve
    from pymultinest.analyse import Analyzer
except ImportError as err:
    #print(err)
    raise err

class MultiNestNestedSampling(NestedSamplingBase):
    """Nested Sampling using MultiNest.
    PyMultiNest: https://github.com/JohannesBuchner/PyMultiNest
    MultiNest: https://github.com/JohannesBuchner/MultiNest

    Attributes:
        sampled_parameters (list of :obj:gleipnir.sampled_parameter.SampledParameter):
            The parameters that are being sampled during the Nested Sampling
            run.
        loglikelihood (function): The log-likelihood function to use for
            assigning a likelihood to parameter vectors during the sampling.
        population_size (int): The number of points to use in the Nested
            Sampling active population.
        multinest_kwargs (dict): Additional keyword arguments that should be
            passed to the PyMultiNest MultiNest solver. Available options are:
                importance_nested_sampling (bool): Should MultiNest use
                    Importance Nested Sampling (INS). Default: True
                constant_efficiency_mode (bool): Should MultiNest run in
                    constant sampling efficiency mode. Default: False
	            sampling_efficiency (float): Set the MultiNest sampling
                    efficiency. 0.3 is recommended for evidence evaluation,
                    while 0.8 is recommended for parameter estimation.
                    Default: 0.8
                resume (bool): Resume from a previous MultiNest run (using
                    the last saved checkpoint in the MultiNest output files).
                    Default: True
                write_output (bool): Specify whether MultiNest should write
                    to output files. True is required for additional
                    analysis. Default: True
                multimodal (bool): Set whether MultiNest performs mode
                    separation. Default: True
                max_mode (int): Set the maximum number of modes allowed in
                    mode separation (if multimodal=True). Default: 100
                mode_tolerance (float): A lower bound for which MultiNest will
                    use to separate mode samples and statistics with
                    log-evidence value greater the given value.
                    Default: -1e90
                n_clustering_params (int): If multimodal=True, set the number
                    of parameters to use in clustering during mode separation.
                    If None, then MultiNest will use all the paramters for
                    clustering during mode separation. If
                    n<(number of sampled parameters), then MultiNest will only
                    use a subset composed of the first n parameters for
                    clustering during mode separation. Default: None
                null_log_evidence (float): If multimodal=True, a lower bound
                    for which MultiNest can use to separte mode samples and
                    statistics with a local log-evidence value greater than the
                    given bound. Default: -1.e90
                log_zero (float): Set a threshold value for which points with
                    a loglikelihood less than the given value will be ignored
                    by MultiNest. Default: -1e100
                max_iter (int): Set the maximum number of nested sampling
                    iterations performed by MultiNest. If 0, then it is
                    unlimited and MultiNest will stop using a different
                    criterion. Default: 0

    References:
        1. Feroz, Farhan, and M. P. Hobson. "Multimodal nested sampling: an
            efficient and robust alternative to Markov Chain Monte Carlo
            methods for astronomical data analyses." Monthly Notices of the
            Royal Astronomical Society 384.2 (2008): 449-463.
        2. Feroz, F., M. P. Hobson, and M. Bridges. "MultiNest: an efficient
            and robust Bayesian inference tool for cosmology and particle
            physics." Monthly Notices of the Royal Astronomical Society 398.4
            (2009): 1601-1614.
        3. Feroz, F., et al. "Importance nested sampling and the MultiNest
            algorithm." arXiv preprint arXiv:1306.2144 (2013).
        4. Buchner, J., et al. "X-ray spectral modelling of the AGN obscuring
            region in the CDFS: Bayesian model selection and catalogue."
            Astronomy & Astrophysics 564 (2014): A125.
    """

    def __init__(self, sampled_parameters, loglikelihood, population_size,
                 **multinest_kwargs):
        """Initialize the MultiNest Nested Sampler."""
        self.sampled_parameters = sampled_parameters
        self.loglikelihood = loglikelihood
        self.population_size = population_size
        self.multinest_kwargs = multinest_kwargs

        self._nDims = len(sampled_parameters)
        self._nDerived = 0
        self._output = None
        self._post_eval = False
        #if self.population_size is None:
        #    self.population_size = 25*self._nDims

        # Make the prior function for PyMultiNest.
        def prior(hypercube):
            return np.array([self.sampled_parameters[i].invcdf(value) for i,value in enumerate(hypercube)])

        self._prior = prior
        # multinest settings
        self._file_root = 'multinest_run_' #string

        return

    def run(self, verbose=False):
        """Initiate the MultiNest Nested Sampling run."""
        output = solve(LogLikelihood=self.loglikelihood, Prior=self._prior,
                       n_dims = self._nDims,
                       n_live_points=self.population_size,
                       outputfiles_basename=self._file_root,
                       verbose=verbose,
                       **self.multinest_kwargs)
        self._output = output
        return self.log_evidence, self.log_evidence_error

    @property
    def evidence(self):
        """float: Estimate of the Bayesian evidence, or Z."""
        return np.exp(self._output['logZ'])
    @evidence.setter
    def evidence(self, value):
        warnings.warn("evidence is not settable")

    @property
    def evidence_error(self):
        """float: Estimate (rough) of the error in the evidence, or Z."""
        return np.exp(self._output['logZerr'])
    @evidence_error.setter
    def evidence_error(self, value):
        warnings.warn("evidence_error is not settable")

    @property
    def log_evidence(self):
        """float: Estimate of the natural logarithm of the Bayesian evidence, or ln(Z).
        """
        return self._output['logZ']
    @log_evidence.setter
    def log_evidence(self, value):
        warnings.warn("log_evidence is not settable")

    @property
    def log_evidence_error(self):
        """float: Estimate of the error in the natural logarithm of the evidence.
        """
        return self._output['logZerr']
    @log_evidence_error.setter
    def log_evidence_error(self, value):
        warnings.warn("log_evidence_error is not settable")

    @property
    def information(self):
        """None: Not implemented yet->Estimate of the Bayesian information, or H."""
        return None
    @information.setter
    def information(self, value):
        warnings.warn("information is not settable")

    @property
    def multinest_file_root(self):
        """str. The root name used for MultNest output files."""
        return self._file_root
    @multinest_file_root.setter
    def multinest_file_root(self, value):
        self._file_root = value
        return

    def posteriors(self, nbins=None):
        """Estimates of the posterior marginal probability distributions of each parameter.
        Returns:
            dict of tuple of (numpy.ndarray, numpy.ndarray, numpy.ndarray): The
                histogram estimates of the posterior marginal probability
                distributions. The returned dict is keyed by the sampled
                parameter names and each element is a tuple with
                (marginal_weights, bin_edges, bin_centers).
        """
        # Lazy evaluation at first call of the function and store results
        # so that subsequent calls don't have to recompute.
        #print('nbins', nbins)
        if not self._post_eval:
            # Here the samples are samples directly from the posterior
            # (i.e. equal weights).
            samples = self._output['samples']
            # Rice bin count selection
            if nbins is None:
                nbins = 2 * int(np.cbrt(len(samples)))
            # print('nbins', nbins)
            nd = samples.shape[1]
            self._posteriors = dict()
            for ii in range(nd):
                marginal, edge = np.histogram(samples[:,ii], density=True, bins=nbins)
                center = (edge[:-1] + edge[1:])/2.
                self._posteriors[self.sampled_parameters[ii].name] = (marginal, edge, center)
            self._post_eval = True

        return self._posteriors

    def max_loglikelihood(self):
        mn_data = Analyzer(len(self.sampled_parameters), self._file_root, verbose=False).get_data()
        log_ls = -0.5*mn_data[:,1]
        ml = log_ls.max()
        return ml

    def deviance_ic(self):
        """Estimate Deviance Information Criterion.
        This function estimates the Deviance Information Criterion (DIC) for the
        model simulated with Nested Sampling (NS). It does so by using the
        posterior distribution estimates computed from the NS outputs.
        The DIC formula is given by:
            DIC = p_D + D_bar,
        where p_D = D_bar - D(theta_bar), D_bar is the posterior average of
        the deviance D(theta)= -2*ln(L(theta)) with L(theta) the likelihood
        of parameter set theta, and theta_bar is posterior average parameter set.

        Returns:
            float: The DIC estimate.
        """
        mn_data = Analyzer(len(self.sampled_parameters), self._file_root, verbose=False).get_data()
        params = mn_data[:,2:]
        log_likelihoods = -0.5*mn_data[:,1]
        prior_mass = mn_data[:,0]
        norm_weights = (prior_mass*np.exp(log_likelihoods))/self.evidence
        nw_mask = np.isnan(norm_weights)
        if  np.any(nw_mask):
            return np.inf
        D_of_theta = -2.*log_likelihoods
        D_bar = np.average(D_of_theta, weights=norm_weights)
        theta_bar = np.average(params, axis=0, weights=norm_weights)
        D_of_theta_bar = -2. * self.loglikelihood(theta_bar)
        p_D = D_bar - D_of_theta_bar
        return p_D + D_bar

    def best_fit_likelihood(self):
        """Parameter vector with the maximum likelihood.
        Returns:
            numpy.array: The parameter vector.
        """
        mn_data = Analyzer(len(self.sampled_parameters), self._file_root, verbose=False).get_data()
        log_ls = -0.5*mn_data[:,1]
        midx = np.argmax(log_ls)
        ml = mn_data[midx][2:]
        return ml


# python
import os

# site-packages
import numpy as np

# local imports
import utils, tasks

class simulation( object ):

    def __init__(self, dir_name):

        # intialize data structure to store simulation data
        self.data = {
          # from simulation configuration file
          'name': None,
          'friction': None,
          'rough_fault_seed': None,
          'fault_roughness': None,
          'ihypo': None,

          # calculated from simulation output
          'rupture_length'  : None,
          'rupture_area'    : None,
          'avg_stress_drop' : None,
          'avg_slip': None,
          'avg_psv' : None,
          'avg_surf_slip': None,
          'avg_surf_psv': None,
          'max_slip' : None,
          'max_psv' : None,
          'max_surf_slip' : None,
          'max_surf_psv' : None,
          'moment': None,
          'mw': None,
        }

        # populated from parsing the parameter file
        self.params = {}

        # hardcoded
        self.__config_file = 'meta.py'

        # simulation directory
        self.dir = os.path.expanduser( dir_name )

        # read simulation details and update data
        self.data.update( self.parse_simulation_details() )

        # add some parameters to top level class for easy access
        self.dt = self.params['dt']
        self.dx = self.params['dx'][0]
        self.nx = int(self.params['nn'][0])
        self.nz = int(self.params['nn'][1])
        self.ihypo = self.params['ihypo']
        self.name = self.params['name']

        # load slip, psv, and trup
        nx = int(self.params['shape']['su1'][0])
        nz = int(self.params['shape']['su1'][1])
        self.dtype = self.params['dtype']

        su1 = np.fromfile(os.path.join(self.dir, 'out/su1'), dtype=self.dtype).reshape([nz,nx])
        su2 = np.fromfile(os.path.join(self.dir, 'out/su2'), dtype=self.dtype).reshape([nz,nx])
        self.slip = np.sqrt( su1**2 + su2**2 )
        self.trup = np.fromfile(os.path.join(self.dir, 'out/trup'), dtype=self.dtype).reshape([nz,nx])
        self.psv = np.fromfile(os.path.join(self.dir, 'out/psv'), dtype=self.dtype).reshape([nz,nx])

        # load shear stress
        nx = int(self.params['shape']['tsm'][0])
        nz = int(self.params['shape']['tsm'][1])
        nt = int(self.params['shape']['tsm'][2]) # some models didn't write out all timesteps

        self.tsm = np.fromfile( os.path.join(self.dir, 'out/tsm'), dtype=self.dtype )

        # sometimes the simulations finish early
        try:
            self.tsm = self.tsm.reshape([nt,nz,nx])
        except ValueError:
            n = len( self.tsm )
            sdim = int(n / ( nx * nz ))
            self.tsm = self.tsm.reshape([sdim,nz,nx])

    def parse_simulation_details( self ):
        """
        parse parameters from meta.py file. will populate specifics for database.

        inputs
            config_file (string of file object) : configuration file for simulation

        returns
            params (dict) : params.keys() = ['name', 'friction', rough_fault_seed', 'fault_roughness']

        """
        # read meta.py file
        meta = utils.parse( os.path.join( self.dir, self.__config_file ) )

        # update simulation object with all parameters
        self.params.update( meta )

        # extract parameters
        data = {}
        data['name'] = meta['name']
        data['friction'] = meta['friction']
        data['rough_fault_seed'] = utils.extract_from_string( data['name'], ['rf','seed'] )
        data['fault_roughness'] = utils.extract_from_string( data['name'], 'a' )
        data['ihypo'] = str(meta['ihypo'])
        return data

    def get_masked_field(self, field_name, tind = None, mask='psv', tol=0.001, hypo_tol=4000):
        """
        masks a rupture field around hypocenter, velocity strengthening region, and based on
        field values.  currently, masking where psv < 0.001 seems to be the most general approach.

        notes: returns the mask that is the same shape as 'field'

        inputs
            sim : simulation object
            field : string of field in simulation object to use.
            hypo_tol : radius to mask around hypocenter.
            vel_str_tol : velocity strengthening tolerance.
            field_tol : tolerance of simulation field.

        return:
            field (ndarray.masked)

        """

        # get reference to fields
        field = getattr(self, field_name)
        mask_field = getattr(self, mask)

        nskipx_mask = self.params['indices'][mask][0][2]
        if field_name is 'slip':
            nskipx_field = self.params['indices']['su1'][0][2]
        else:
            nskipx_field = self.params['indices'][field_name][0][2]

        # get dims of field
        nt = 0
        try:
            nx,nz,nt = field.shape
        except ValueError:
            nx,nz = field.shape

        # some sanity checks
        if len(mask_field.shape) > len(field.shape):
            raise ValueError("Mask should not have higher dimensionality that field being masked.")

        if mask_field.shape != (self.nz, self.nx):
            raise ValueError("Masking field should cover entire fault plane.  Try trup, or psv, or slip.")

        if nskipx_mask > nskipx_field:
            raise ValueError("Mask has less resolution that field. Unable to mask field.")

        # calculate hypocentral mask
        hx = int(self.ihypo[0])
        hz = int(self.ihypo[1])
        r = int(hypo_tol / self.dx)
        z,x = np.ogrid[-hz:self.nz-hz, -hx:self.nx-hx]
        mask1 = x*x + z*z <= r*r

        # mask unruptured area based on mask and tol
        mask2 = np.zeros( mask_field.shape, dtype=bool )
        mask2[np.where( mask_field < tol )] = True

        # decimate mask if necessary
        ratio = 1
        if nskipx_field != nskipx_mask:
            ratio = int(nskipx_field / nskipx_mask)

        # combine masks using logical or
        mask = np.ma.mask_or(mask1, mask2)[::ratio, ::ratio]

        # return at time
        if tind is not None:
            masked = np.ma.array( np.squeeze(field[tind, :, :]), mask = mask )
        else:
            masked = np.ma.array( field, mask = mask )

        return masked

    def process( self ):
        """ process the simulation from folders in tasks. make sure to update data as we go along. """
        print(f'processing sim: {self.name}')

        self.data['rupture_length'] = tasks.get_fault_extent( self )
        self.data['rupture_area'] = tasks.get_area( self )
        self.data['rupture_area_bbox'] = tasks.get_area_bbox( self )
        self.data['avg_stress_drop'] = tasks.compute_stress_drop( self )

        avg, avg_surf, mx, mx_surf = tasks.rupture_field_stats( self , 'slip' )
        self.data['avg_slip'] = avg
        self.data['avg_surf_slip']= avg_surf
        self.data['max_slip'] = mx
        self.data['max_surf_slip'] = mx_surf

        avg, avg_surf, mx, mx_surf = tasks.rupture_field_stats( self , 'psv' )
        self.data['avg_psv'] = avg
        self.data['avg_surf_psv'] = avg_surf
        self.data['max_psv'] = mx
        self.data['max_surf_psv'] = mx_surf

        self.data['mw'] = tasks.get_mw( self, dtype=self.dtype )
        self.data['moment'] = utils.mw_to_m0( self.data['mw'] )

        return self