import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.nn.functional as F
import random
import numpy as np

PAD, CLS = '[PAD]', '[CLS]'
SEP = '[SEP]'

class Attention(nn.Module):
    r"""
    Applies an attention mechanism on the output features from the decoder.

    .. math::
            \begin{array}{ll}
            x = context*output \\
            attn = exp(x_i) / sum_j exp(x_j) \\
            output = \tanh(w * (attn * context) + b * output)
            \end{array}

    Args:
        dim(int): The number of expected features in the output

    Inputs: output, context
        - **output** (batch, output_len, dimensions): tensor containing the output features from the decoder.
        - **context** (batch, input_len, dimensions): tensor containing features of the encoded input sequence.

    Outputs: output, attn
        - **output** (batch, output_len, dimensions): tensor containing the attended output features from the decoder.
        - **attn** (batch, output_len, input_len): tensor containing attention weights.

    Attributes:
        linear_out (torch.nn.Linear): applies a linear transformation to the incoming data: :math:`y = Ax + b`.
        mask (torch.Tensor, optional): applies a :math:`-inf` to the indices specified in the `Tensor`.

    Examples::

         >>> attention = seq2seq.models.Attention(256)
         >>> context = Variable(torch.randn(5, 3, 256))
         >>> output = Variable(torch.randn(5, 5, 256))
         >>> output, attn = attention(output, context)

    """
    def __init__(self, dim):
        super(Attention, self).__init__()
        self.linear_out = nn.Linear(dim*2, dim)
        self.mask = None

    def set_mask(self, mask):
        """
        Sets indices to be masked

        Args:
            mask (torch.Tensor): tensor containing indices to be masked
        """
        self.mask = mask

    def forward(self, output, context):
        batch_size = output.size(0)
        hidden_size = output.size(2)
        input_size = context.size(1)
        # (batch, out_len, dim) * (batch, in_len, dim) -> (batch, out_len, in_len)
        attn = torch.bmm(output, context.transpose(1, 2))
        if self.mask is not None:
            attn.data.masked_fill_(self.mask, -float('inf'))
        attn = F.softmax(attn.view(-1, input_size), dim=1).view(batch_size, -1, input_size)

        # (batch, out_len, in_len) * (batch, in_len, dim) -> (batch, out_len, dim)
        mix = torch.bmm(attn, context)

        # concat -> (batch, out_len, 2*dim)
        combined = torch.cat((mix, output), dim=2)
        # output -> (batch, out_len, dim)
        output = F.tanh(self.linear_out(combined.view(-1, 2 * hidden_size))).view(batch_size, -1, hidden_size)

        return output, attn

class BaseRNN(nn.Module):
    r"""
    Applies a multi-layer RNN to an input sequence.
    Note:
        Do not use this class directly, use one of the sub classes.
    Args:
        vocab_size (int): size of the vocabulary
        max_len (int): maximum allowed length for the sequence to be processed
        hidden_size (int): number of features in the hidden state `h`
        input_dropout_p (float): dropout probability for the input sequence
        dropout_p (float): dropout probability for the output sequence
        n_layers (int): number of recurrent layers
        rnn_cell (str): type of RNN cell (Eg. 'LSTM' , 'GRU')

    Inputs: ``*args``, ``**kwargs``
        - ``*args``: variable length argument list.
        - ``**kwargs``: arbitrary keyword arguments.

    Attributes:
        SYM_MASK: masking symbol
        SYM_EOS: end-of-sequence symbol
    """
    SYM_MASK = "MASK"
    SYM_EOS = "EOS"

    def __init__(self, vocab_size, max_len, hidden_size, input_dropout_p, dropout_p, n_layers, rnn_cell):
        super(BaseRNN, self).__init__()
        self.vocab_size = vocab_size
        self.max_len = max_len
        self.hidden_size = hidden_size
        self.n_layers = n_layers
        self.input_dropout_p = input_dropout_p
        self.input_dropout = nn.Dropout(p=input_dropout_p)
        if rnn_cell.lower() == 'lstm':
            self.rnn_cell = nn.LSTM
        elif rnn_cell.lower() == 'gru':
            self.rnn_cell = nn.GRU
        else:
            raise ValueError("Unsupported RNN Cell: {0}".format(rnn_cell))

        self.dropout_p = dropout_p

    def forward(self, *args, **kwargs):
        raise NotImplementedError()

class DecoderRNN(BaseRNN):
    r"""
    Provides functionality for decoding in a seq2seq framework, with an option for attention.

    Args:
        vocab_size (int): size of the vocabulary
        max_len (int): a maximum allowed length for the sequence to be processed
        hidden_size (int): the number of features in the hidden state `h`
        sos_id (int): index of the start of sentence symbol
        eos_id (int): index of the end of sentence symbol
        n_layers (int, optional): number of recurrent layers (default: 1)
        rnn_cell (str, optional): type of RNN cell (default: gru)
        bidirectional (bool, optional): if the encoder is bidirectional (default False)
        input_dropout_p (float, optional): dropout probability for the input sequence (default: 0)
        dropout_p (float, optional): dropout probability for the output sequence (default: 0)
        use_attention(bool, optional): flag indication whether to use attention mechanism or not (default: false)

    Attributes:
        KEY_ATTN_SCORE (str): key used to indicate attention weights in `ret_dict`
        KEY_LENGTH (str): key used to indicate a list representing lengths of output sequences in `ret_dict`
        KEY_SEQUENCE (str): key used to indicate a list of sequences in `ret_dict`

    Inputs: inputs, encoder_hidden, encoder_outputs, function, teacher_forcing_ratio
        - **inputs** (batch, seq_len, input_size): list of sequences, whose length is the batch size and within which
          each sequence is a list of token IDs.  It is used for teacher forcing when provided. (default `None`)
        - **encoder_hidden** (num_layers * num_directions, batch_size, hidden_size): tensor containing the features in the
          hidden state `h` of encoder. Used as the initial hidden state of the decoder. (default `None`)
        - **encoder_outputs** (batch, seq_len, hidden_size): tensor with containing the outputs of the encoder.
          Used for attention mechanism (default is `None`).
        - **function** (torch.nn.Module): A function used to generate symbols from RNN hidden state
          (default is `torch.nn.functional.log_softmax`).
        - **teacher_forcing_ratio** (float): The probability that teacher forcing will be used. A random number is
          drawn uniformly from 0-1 for every decoding token, and if the sample is smaller than the given value,
          teacher forcing would be used (default is 0).

    Outputs: decoder_outputs, decoder_hidden, ret_dict
        - **decoder_outputs** (seq_len, batch, vocab_size): list of tensors with size (batch_size, vocab_size) containing
          the outputs of the decoding function.
        - **decoder_hidden** (num_layers * num_directions, batch, hidden_size): tensor containing the last hidden
          state of the decoder.
        - **ret_dict**: dictionary containing additional information as follows {*KEY_LENGTH* : list of integers
          representing lengths of output sequences, *KEY_SEQUENCE* : list of sequences, where each sequence is a list of
          predicted token IDs }.
    """

    KEY_ATTN_SCORE = 'attention_score'
    KEY_LENGTH = 'length'
    KEY_SEQUENCE = 'sequence'

    def __init__(self, vocab_size, max_len, hidden_size,
                 sos_id, eos_id,
                 n_layers=1, rnn_cell='gru', bidirectional=False,
                 input_dropout_p=0, dropout_p=0, use_attention=False):
        super(DecoderRNN, self).__init__(vocab_size, max_len, hidden_size,
                                         input_dropout_p, dropout_p,
                                         n_layers, rnn_cell)

        self.bidirectional_encoder = bidirectional
        self.rnn = self.rnn_cell(hidden_size, hidden_size, n_layers, batch_first=True, dropout=dropout_p)
        self.hidden_size = hidden_size
        self.output_size = vocab_size
        self.max_length = max_len
        self.use_attention = use_attention
        self.eos_id = eos_id
        self.sos_id = sos_id

        self.init_input = None

        self.embedding = nn.Embedding(self.output_size, self.hidden_size)
        if use_attention:
            self.attention = Attention(self.hidden_size)

        self.out = nn.Linear(self.hidden_size, self.output_size)
        self.hidden_change = nn.Linear(768, self.hidden_size)

    def forward_step(self, input_var, hidden, encoder_outputs, function):
        batch_size = input_var.size(0)
        output_size = input_var.size(1)
        embedded = self.embedding(input_var)
        embedded = self.input_dropout(embedded)

        output, hidden = self.rnn(embedded, hidden)

        attn = None
        if self.use_attention:
            output, attn = self.attention(output, encoder_outputs)

        predicted_softmax = function(self.out(output.contiguous().view(-1, self.hidden_size)), dim=1).view(batch_size,
                                                                                output_size,
                                                                                -1)
        return predicted_softmax, hidden, attn

    def forward(self, inputs=None, encoder_hidden=None, encoder_outputs=None,
                function=F.log_softmax, teacher_forcing_ratio=0):
        ret_dict = dict()
        if self.use_attention:
            ret_dict[DecoderRNN.KEY_ATTN_SCORE] = list()

        inputs, batch_size, max_length = self._validate_args(inputs, encoder_hidden, encoder_outputs,
                                                             function, teacher_forcing_ratio)
        decoder_hidden = self._init_state(encoder_hidden)

        use_teacher_forcing = True if random.random() < teacher_forcing_ratio else False

        decoder_outputs = []
        sequence_symbols = []
        lengths = np.array([max_length] * batch_size)

        def decode(step, step_output, step_attn):
            decoder_outputs.append(step_output)
            if self.use_attention:
                ret_dict[DecoderRNN.KEY_ATTN_SCORE].append(step_attn)
            symbols = decoder_outputs[-1].topk(1)[1]
            sequence_symbols.append(symbols)

            eos_batches = symbols.data.eq(self.eos_id)
            if eos_batches.dim() > 0:
                eos_batches = eos_batches.cpu().view(-1).numpy()
                update_idx = ((lengths > step) & eos_batches) != 0
                lengths[update_idx] = len(sequence_symbols)
            return symbols

        # Manual unrolling is used to support random teacher forcing.
        # If teacher_forcing_ratio is True or False instead of a probability, the unrolling can be done in graph
        if use_teacher_forcing:
            decoder_input = inputs[:, :-1]
            decoder_output, decoder_hidden, attn = self.forward_step(decoder_input, decoder_hidden, encoder_outputs,
                                                                     function=function)

            for di in range(decoder_output.size(1)):
                step_output = decoder_output[:, di, :]
                if attn is not None:
                    step_attn = attn[:, di, :]
                else:
                    step_attn = None
                decode(di, step_output, step_attn)
        else:
            decoder_input = inputs[:, 0].unsqueeze(1)
            for di in range(max_length):
                decoder_output, decoder_hidden, step_attn = self.forward_step(decoder_input, decoder_hidden,
                                                                              encoder_outputs,
                                                                              function=function)
                step_output = decoder_output.squeeze(1)
                symbols = decode(di, step_output, step_attn)
                decoder_input = symbols

        ret_dict[DecoderRNN.KEY_SEQUENCE] = sequence_symbols
        ret_dict[DecoderRNN.KEY_LENGTH] = lengths.tolist()

        return decoder_outputs, decoder_hidden, ret_dict

    def _init_state(self, encoder_hidden):
        """ Initialize the encoder hidden state. """
        encoder_hidden = torch.tanh(self.hidden_change(encoder_hidden)).unsqueeze(dim=0)
        if encoder_hidden is None:
            return None
        if isinstance(encoder_hidden, tuple):
            encoder_hidden = tuple([self._cat_directions(h) for h in encoder_hidden])
        else:
            encoder_hidden = self._cat_directions(encoder_hidden)
        return encoder_hidden

    def _cat_directions(self, h):
        """ If the encoder is bidirectional, do the following transformation.
            (#directions * #layers, #batch, hidden_size) -> (#layers, #batch, #directions * hidden_size)
        """
        if self.bidirectional_encoder:
            h = torch.cat([h[0:h.size(0):2], h[1:h.size(0):2]], 2)
        return h

    def _validate_args(self, inputs, encoder_hidden, encoder_outputs, function, teacher_forcing_ratio):
        if self.use_attention:
            if encoder_outputs is None:
                raise ValueError("Argument encoder_outputs cannot be None when attention is used.")

        # inference batch size
        if inputs is None and encoder_hidden is None:
            batch_size = 1
        else:
            if inputs is not None:
                batch_size = inputs.size(0)
            else:
                if self.rnn_cell is nn.LSTM:
                    batch_size = encoder_hidden[0].size(0)
                elif self.rnn_cell is nn.GRU:
                    batch_size = encoder_hidden.size(0)

        # set default input and max decoding length
        if inputs is None:
            if teacher_forcing_ratio > 0:
                raise ValueError("Teacher forcing has to be disabled (set 0) when no inputs is provided.")
            inputs = torch.LongTensor([self.sos_id] * batch_size).view(batch_size, 1)
            if torch.cuda.is_available():
                inputs = inputs.cuda()
            max_length = self.max_length
        else:
            max_length = inputs.size(1) - 1  # minus the start of sequence symbol

        return inputs, batch_size, max_length

class Seq2seq(nn.Module):
    """ Standard sequence-to-sequence architecture with configurable encoder
    and decoder.

    Args:
        encoder (EncoderRNN): object of EncoderRNN
        decoder (DecoderRNN): object of DecoderRNN
        decode_function (func, optional): function to generate symbols from output hidden states (default: F.log_softmax)

    Inputs: input_variable, input_lengths, target_variable, teacher_forcing_ratio
        - **input_variable** (list, option): list of sequences, whose length is the batch size and within which
          each sequence is a list of token IDs. This information is forwarded to the encoder.
        - **input_lengths** (list of int, optional): A list that contains the lengths of sequences
            in the mini-batch, it must be provided when using variable length RNN (default: `None`)
        - **target_variable** (list, optional): list of sequences, whose length is the batch size and within which
          each sequence is a list of token IDs. This information is forwarded to the decoder.
        - **teacher_forcing_ratio** (int, optional): The probability that teacher forcing will be used. A random number
          is drawn uniformly from 0-1 for every decoding token, and if the sample is smaller than the given value,
          teacher forcing would be used (default is 0)

    Outputs: decoder_outputs, decoder_hidden, ret_dict
        - **decoder_outputs** (batch): batch-length list of tensors with size (max_length, hidden_size) containing the
          outputs of the decoder.
        - **decoder_hidden** (num_layers * num_directions, batch, hidden_size): tensor containing the last hidden
          state of the decoder.
        - **ret_dict**: dictionary containing additional information as follows {*KEY_LENGTH* : list of integers
          representing lengths of output sequences, *KEY_SEQUENCE* : list of sequences, where each sequence is a list of
          predicted token IDs, *KEY_INPUT* : target outputs if provided for decoding, *KEY_ATTN_SCORE* : list of
          sequences, where each list is of attention weights }.

    """

    def __init__(self, encoder, decoder, decode_function=F.log_softmax):
        super(Seq2seq, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.decode_function = decode_function

    def flatten_parameters(self):
        self.encoder.rnn.flatten_parameters()
        self.decoder.rnn.flatten_parameters()

    def forward(self, batch_src, batch_tar=None,
                teacher_forcing_ratio=0):

        encoder_outputs, encoder_hidden = self.encoder(batch_src)

        target_variable = batch_tar
        result = self.decoder(inputs=target_variable,
                              encoder_hidden=encoder_hidden,
                              # encoder_outputs=combined_embeddings,
                              encoder_outputs = encoder_outputs,
                              function=self.decode_function,
                              teacher_forcing_ratio=teacher_forcing_ratio)
        return result


__author__ = 'Jiashun'
import re
import numpy as np
from collections import Counter
from math import ceil
from math import floor

# compare single base
def SingleBaseCompare(seq1,seq2,i,j):
    if seq1[i] == seq2[j]:
        return 2
    else:
        return -1

# Smith–Waterman Alignment
def SMalignment(seq1, seq2):
    m = len(seq1)
    n = len(seq2)
    g = -3
    matrix = []
    for i in range(0, m):
        tmp = []
        for j in range(0, n):
            tmp.append(0)
        matrix.append(tmp)
    for sii in range(0, m):
        matrix[sii][0] = sii*g
    for sjj in range(0, n):
        matrix[0][sjj] = sjj*g
    for siii in range(1, m):
        for sjjj in range(1, n):
            matrix[siii][sjjj] = max(matrix[siii-1][sjjj] + g, matrix[siii - 1][sjjj - 1] + SingleBaseCompare(seq1,seq2,siii, sjjj), matrix[siii][sjjj-1] + g)
    sequ1 = [seq1[m-1]]
    sequ2 = [seq2[n-1]]
    while m > 1 and n > 1:
        if max(matrix[m-1][n-2], matrix[m-2][n-2], matrix[m-2][n-1]) == matrix[m-2][n-2]:
            m -= 1
            n -= 1
            sequ1.append(seq1[m-1])
            sequ2.append(seq2[n-1])
        elif max(matrix[m-1][n-2], matrix[m-2][n-2], matrix[m-2][n-1]) == matrix[m-1][n-2]:
            n -= 1
            sequ1.append('-')
            sequ2.append(seq2[n-1])
        else:
            m -= 1
            sequ1.append(seq1[m-1])
            sequ2.append('-')
    sequ1.reverse()
    sequ2.reverse()
    align_seq1 = ''.join(sequ1)
    align_seq2 = ''.join(sequ2)
    align_score = 0.
    for k in range(0, len(align_seq1)):
        if align_seq1[k] == align_seq2[k]:
            align_score += 1
    align_score = float(align_score)/len(align_seq1)
    return align_seq1, align_seq2, align_score

# Display BlAST result
def Display(seque1, seque2):
    le = 60
    while len(seque1)-le >= 0:
        print('sequence1: ',end='')
        for a in list(seque1)[le-40:le]:
            print(a,end='')
        print("\n")
        print('           ',end='')
        for k in range(le-40, le):
            if seque1[k] == seque2[k]:
                print('|',end='')
            else:
                print(' ',end='')
        print("\n")
        print('sequence2: ',end='')
        for b in list(seque2)[le-40:le]:
            print(b,end='')
        print("\n")
        le += 40
    if len(seque1) > le-40:
        print('sequence1: ',end='')
        for a in list(seque1)[le-40:len(seque1)]:
            print(a,end='')
        print("\n")
        print('           ',end='')
        for k in range(le-40, len(seque1)):
            if seque1[k] == seque2[k]:
                print('|',end='')
            else:
                print(' ',end='')
        print("\n")
        print('sequence2: ',end='')
        for b in list(seque2)[le-40:len(seque2)]:
            print(b,end='')
        print("\n")

# transform base to numeric value
def WordToNum(word):
    tmp = []
    trans = {'A':1,'C':2,'G':3,'T':4}
    for w in word:
        tmp.append(trans[w])
    return tmp

# transform word with 11 bases to its index
def WordToIndex(word,word_len):
    tmp = 0
    word_num = WordToNum(word)
    for i,v in enumerate(word_num):
        tmp += (v-1)*4**(word_len-i)
    return tmp

# Get word's postion in genome from library
def GetWordPos(word):
    assert len(word)== 11
    seek_index = WordToIndex(word,11-1)
    positions = []
    for chr_name in chr_names:
        chr_seq = open('/home/jxiaoae/class/blast/chromosome_{}_library.txt'.format(chr_name),'r')
        seeks = np.load("chromosome_{}_library_seeks.npy".format(chr_name))
        chr_seq.seek(seeks[seek_index,0])
        position = chr_seq.read(seeks[seek_index,1])
        try:
            positions.append(list(map(int, position[:-1].split(","))))
        except:
            positions.append([])
    return positions

# Extract subsequence from GRCh37 file
def ExtractSeq(chr_index,pos,length):
    pos = pos+floor(pos/60)
    hg19.seek(chrom_seek_index[chr_index,1]+pos-1)
    return re.sub(r'\n', '', hg19.read(length))

# main blast function
def Blast(query_seq):
    i = 0
    query_words = []
    query_seq_length = len(query_seq)
    words_length = query_seq_length-11+1
    while i < words_length:
        query_words.append(query_seq[i:i+11])
        i += 1
    words_positions = []
    for word in query_words:
        words_positions.append(GetWordPos(word))
    for chr_index in range(24):
        for word_index in range(words_length):
            for pos in range(len(words_positions[word_index][chr_index])):
                words_positions[word_index][chr_index][pos] += words_length - word_index - 1

        words_positions_corrects = []
        for word_index in range(words_length):
            words_positions_corrects += words_positions[word_index][chr_index]

        words_positions_corrects_count = Counter(words_positions_corrects)
        finded_postions = []
        for count_ in words_positions_corrects_count:
            # we can select the bigger threshold of words_positions_corrects_count[count_] just
            # like we select the highly similar sequence in NCBI BLAST
            if words_positions_corrects_count[count_] > 5:
                finded_postions.append(count_)
        if finded_postions:
            for finded_postion in finded_postions:
                candidate_seq_pos = finded_postion - query_seq_length + 11 - 5
                candidate_seq_length = query_seq_length + 11
                candidate_sequence = ExtractSeq(chr_index,candidate_seq_pos,candidate_seq_length)
                i_start_indexs = []
                for i_start in range(15):
                    _,_,score = SMalignment(candidate_sequence[i_start:],query_seq)
                    i_start_indexs.append(score)
                i_start = np.array(i_start_indexs).argmax()
                i_end_indexs = []
                for i_end in range(1,16):
                    _,_,score = SMalignment(candidate_sequence[:-i_end],query_seq)
                    i_end_indexs.append(score)
                i_end = np.array(i_end_indexs).argmax()+1
                candidate_sequence = candidate_sequence[i_start:-i_end]
                align_seq1,align_seq2,align_score = SMalignment(candidate_sequence,query_seq)
                if align_score>0.7:
                    print("find in chromosome "+chr_names[chr_index]+": "+str(candidate_seq_pos+i_start)+' ---> '+str(candidate_seq_pos+i_start+len(candidate_sequence)-1)+", align score: "+str(align_score))
                    Display(align_seq1, align_seq2)
    return None

if __name__ == "__main__":
    chr_names = np.load('/home/jxiaoae/class/blast/GRCh37_chr_names.npy')
    chrom_seek_index = np.load('/home/jxiaoae/class/blast/GRCh37_chrom_seek_index.npy')
    hg19 = open("/home/share/GRCh37/human_g1k_v37.fasta")
    query_sequence = 'GTATCGGAACTTCCAACTTGTAGGCAAAATAGATATGCTTCATATTCTTAAAAACCACAAGAAA'
    Blast(query_sequence)


# -*- coding: utf-8 -*-
"""
Module to simulate OH sky lines spectra.
"""

import numpy as np
import pandas as pd
from pathlib import Path

from .constants import Constants as cs
from .spectra import Spectra
from .simSpec import SpecUtil as spec_util

_PARENT_DIR = Path(__file__).resolve().parents[1]

class SkyLines(object):
    """
    Contains everything related to OH sky lines.
    """

    MAX_V = 13  #fa maximum vibrational quantum level

    def __init__(self):
        """
        Initiate `SkyLines`. Load data containing line wavelengths, transition
        levels, Einstein's A, etc. The data is from Brooke et al. (2016).
        """
        self.temperature_rot = 190.
        self.temperature_vib = 9000.

        self._wavelength_start = 0.
        self._wavelength_end = 0.
        self._wavelengths = []

        self._qn_vs = pd.DataFrame(columns=['v', 'Q', 'N'],
                                   index=np.arange(self.MAX_V+1))

        self.line_list = pd.DataFrame()  # stores line data
        self._line_list_v = []  # list of lines (dataframes) indexed with v
        self._lines_in_range = pd.DataFrame()  # lines in wavelength range

        self.load_data()  # loads data into `self.line_list`

    def load_data(self):
        """
        Load necessary data files.
        """
        data_file_path = _PARENT_DIR / 'data' / 'OH-XX-Line_list.txt'
        self.line_list = pd.read_csv(data_file_path,
                                     delim_whitespace=True, skiprows=33)

        for v in range(self.MAX_V+1):
            # subset of line_list with v' = v
            v_lines = self.line_list[
                self.line_list["v'"].values == v
                ][["J'", "F'", "p'", "E''", 'Calculated']].drop_duplicates(
            ).reset_index()
            self._line_list_v.append(v_lines)

    def reset_temperatures(self, temperature_rot=190, temperature_vib=9000):
        """
        Reset the temperatures without reloading data.
        :param temperature_rot: Rotational temperature.
        :param temperature_vib: Vibrational temperature.
        """
        self.temperature_rot = temperature_rot
        self.temperature_vib = temperature_vib

        # Resetting all the values in the dataframe, but keeping the same
        # dataframe object for efficient memory allocation.
        self._qn_vs.loc[:] = np.nan
        self._lines_in_range.loc[:, ('Q_v', 'N_v', 'Intensity')] = np.nan

    def setup_wavelengths(self, start=None, end=None, resolution=0.01,
                          wavelengths=None):
        """
        :param start: Beginning wavelength in nm.
        :param end: Ending wavelength in nm.
        :param resolution: Resolution element in nm.
        :param wavelengths: Wavelengths for computing the sky spectra. If
        provided, `start` and `end` are not needed.
        """
        if wavelengths is None:
            assert start is not None and end is not None, 'You need to ' \
                                                          'provide either ' \
                                                          'the wavelengths ' \
                                                          'or the range.'
            self._wavelength_start = start
            self._wavelength_end = end
            self._wavelengths = np.arange(start, end+resolution/2., resolution)
        else:
            self._wavelengths = wavelengths
            self._wavelength_start = wavelengths[0]
            self._wavelength_end = wavelengths[-1]

        k_start = 1e7 / spec_util.air_to_vacuum_wavelength(
            self._wavelength_end)  # in /cm
        k_end = 1e7 / spec_util.air_to_vacuum_wavelength(
            self._wavelength_start)  # in /cm

        self._lines_in_range = self.line_list[
            self.line_list["Calculated"].between(k_start, k_end)]
        self._lines_in_range = self._lines_in_range.assign(
            Q_v=np.nan,
            N_v=np.nan,
            Intensity=np.nan)

    def get_sky_spectra(self, temperature_rot=None, temperature_vib=None):
        """
        Generate the sky spectra between start and end wavelengths.
        :param temperature_rot: Rotational temperature, in K.
        :param temperature_vib: Vibrational temperature, in K.
        """
        if temperature_rot is not None and temperature_vib is not None:
            self.reset_temperatures(temperature_rot=temperature_rot,
                                    temperature_vib=temperature_vib)
        elif temperature_rot is not None:
            self.reset_temperatures(temperature_rot=temperature_rot,
                                    temperature_vib=self.temperature_vib)
        elif temperature_vib is not None:
            self.reset_temperatures(temperature_rot=self.temperature_rot,
                                    temperature_vib=temperature_vib)

        flux = np.zeros_like(self._wavelengths)

        intensities = self._get_line_intensities_in_range()
        line_wavelengths = spec_util.vacuum_to_air_wavelength_mathar(
            1e7/self._lines_in_range['Calculated'].values)  # nm

        pixel_size = self._wavelengths[1] - self._wavelengths[0]
        for line_wavelength, intensity in zip(line_wavelengths, intensities):
            idx = int((line_wavelength - self._wavelength_start)/pixel_size)
            if 0 <= idx < len(flux):
                flux[idx] += intensity

        return Spectra(self._wavelengths, flux, wavelength_unit='nm')

    def _get_qn_v(self, v):
        """
        Compute partition function Q_v (T_rot) and N_v (T_vib). Q_v (T_rot) is
        computed using equation (39) from Mies (1974). Simply, N_v (T_vib) =
        Q_v (T_vib).
        :param v: Vibrational quantum number.
        :type v: int
        """
        if v in self._qn_vs.v.values:
            idx = np.where(self._qn_vs.v.values == v)[0]
            return (self._qn_vs.Q.values[idx],
                    self._qn_vs.N.values[idx])
        else:
            v_lines = self._line_list_v[v]

            wave_n = v_lines["E''"].values + v_lines['Calculated'].values
            j = v_lines["J'"].values

            q = np.sum((4*j+2) * np.exp(-self._get_beta(wave_n,
                                                        self.temperature_rot)
                                        ))
            n = np.sum((4*j+2) * np.exp(-self._get_beta(wave_n,
                                                        self.temperature_vib)
                                        ))

            self._qn_vs.loc[v] = [v, q, n]

            return q, n

    @staticmethod
    def _get_beta(wavenumber, temperature):
        """
        Compute $\beta(E)$ from wavenumber k (cm^-1).
        :param wavenumber: Wavenumber in cm^-1.
        :param temperature: Temperature in K.
        """
        return cs.h * cs.c * wavenumber / cs.k_b / temperature

    @staticmethod
    def _wavenumber2energy(wavenumber):
        """
        Convert wavenumber to energy.
        :param wavenumber: Wavenumber in cm^-1.
        :return:
        """
        return cs.h * cs.c * wavenumber

    def _populate_qn_in_line_list(self):
        """
        Fills up [Q_v, N_v] columns of  `SkyLines._lines_in_range` with
        calculated values.
        :return:
        :rtype:
        """
        for v in range(self.MAX_V+1):
            q_v, n_v = self._get_qn_v(v)
            row_slice = self._lines_in_range["v'"] == v
            self._lines_in_range.loc[row_slice,
                                     'Q_v'] = q_v
            self._lines_in_range.loc[row_slice,
                                     'N_v'] = n_v

    def _get_line_intensities_in_range(self):
        """
        Compute line intensities for lines in `self._lines_in_range`.
        Computed intensity is unit of ergs/cm^3/s. Equation (41) from Mies
        (1974) is used to derive the intensities in photon number, then the
        energy of the line is multiplied to get the intensities in unit of
        energy.
        :param index:
        """
        if np.isnan(self._lines_in_range['Intensity'].values).any():
            self._populate_qn_in_line_list()

            v = self._lines_in_range["v'"].values
            wavenumber = self._lines_in_range['Calculated'].values
            einstein_a = self._lines_in_range['A'].values
            j = self._lines_in_range["J'"].values
            energy_lower = self._lines_in_range["E''"].values
            q_v = self._lines_in_range['Q_v'].values
            n_v = self._lines_in_range['N_v'].values

            intensity = self._wavenumber2energy(wavenumber) * n_v \
                        * einstein_a * (4*j+2) / q_v * np.exp(
                -self._get_beta(energy_lower+wavenumber, self.temperature_rot))

            self._lines_in_range.loc[:, 'Intensity'] = pd.Series(intensity,
                                            index=self._lines_in_range.index)

            return intensity
        else:
            return self._lines_in_range['Intensity'].values


import numpy as np
import itertools
import math

class reax_forcefield:
    """
    ReaxFF forcefield class. Used for generating ReaxFF templates
    """
    def __init__(self, filename = None, filestring = None, template = 'ff.template.generated', ranges = 'param_ranges', bo_threshold = 1e-8):
        """
        :param filename: ReaxFF forcefield filename
        :type filename: str

        :param filestring: ReaxFF forcefield filestring
        :type filestring: str

        :param template: ReaxFF forcefield template filename
        :type template: str

        :param ranges: File containing the lower and upper bounds for decision variables
        :type ranges: str
        """
        self.params_write = []
        self.template = template
        self.ranges = ranges
        self.bo_threshold = bo_threshold
        if not filename is None:
            self.read_forcefield_from_file(filename)
        elif not filestring is None:
            self.read_forcefield_from_string(filestring)

    def read_forcefield_from_file(self,filename):
        """
        Read ReaxFF forcefield from external file

        :param filename: ReaxFF forcefield filename
        :type filename: str
        """
        fffile = open(filename,'r')
        ff = fffile.read()
        fffile.close()

        # Parse forcefield from read-in string
        self.read_forcefield_from_string(ff)
        return

    def read_forcefield_from_string(self,filestring):
        """
        Read ReaxFF forcefield from a given forcefield string

        :param filestring: ReaxFF forcefield string
        :type filestring: str
        """
        list_of_strings = [line+'\n' for line in filestring.split('\n')]

        self.full = list_of_strings
        #print(self.full)
        # Split forcefield
        self._split_forcefield()
        return

    def _split_forcefield(self):
        """
        Split ReaxFF forcefield into sections corresponding to general, one-body, two-body, three-body, four-body, offdiagonal and H-bond sections
        """
        header, general, onebody, twobody, offdiagonal, threebody, fourbody, hbond = [], [], [], [], [], [], [], []
        counter = 0
        ff = self.full

        # Read HEADER line
        header = ff[0]
        counter += 1

        num_general = int(ff[counter].strip().split()[0])
        general_string = ff[counter:counter+num_general+1] # one for the header another for the number of parameters
        for line in general_string:
            general.append(line.strip().split())
        counter += (num_general + 1)

        num_onebody = int(ff[counter].strip().split()[0])
        onebody_string = ff[counter:counter+(num_onebody*4)+4] # one for the header another for the number of parameters
        for line in onebody_string:
            onebody.append(line.strip().split())
        counter += ((num_onebody*4) + 4)

        num_twobody = int(ff[counter].strip().split()[0])
        twobody_string = ff[counter:counter+(num_twobody*2)+2] # one for the header another for the number of parameters
        for line in twobody_string:
            twobody.append(line.strip().split())
        counter += ((num_twobody*2) + 2)

        num_offdiagonal = int(ff[counter].strip().split()[0])
        offdiagonal_string = ff[counter:counter+(num_offdiagonal*1)+1] # one for the header another for the number of parameters
        for line in offdiagonal_string:
            offdiagonal.append(line.strip().split())
        counter += ((num_offdiagonal*1) + 1)

        num_threebody = int(ff[counter].strip().split()[0])
        threebody_string = ff[counter:counter+(num_threebody*1)+1] # one for the header another for the number of parameters
        for line in threebody_string:
            threebody.append(line.strip().split())
        counter += ((num_threebody*1) + 1)

        num_fourbody = int(ff[counter].strip().split()[0])
        fourbody_string = ff[counter:counter+(num_fourbody*1)+1] # one for the header another for the number of parameters
        for line in fourbody_string:
            fourbody.append(line.strip().split())
        counter += ((num_fourbody*1) + 1)

        num_hbond = int(ff[counter].strip().split()[0])
        hbond_string = ff[counter:counter+(num_hbond*1)+1] # one for the header another for the number of parameters
        for line in hbond_string:
            hbond.append(line.strip().split())
        counter += ((num_hbond*1) + 1)

        self.header = header
        self.general = general
        self.onebody = onebody
        self.twobody = twobody
        self.offdiagonal = offdiagonal
        self.threebody = threebody
        self.fourbody = fourbody
        self.hbond = hbond

    def _get_element_number(self,element):
        """
        Get the numerical index of an element in the ReaxFF forcefield file

        :param element: Chemical symbol for the element
        :type element: str
        """
        new_onebody = self.onebody[4::4]
        for i in range(len(new_onebody)):
            if new_onebody[i][0].lower().upper() == element.lower().upper():
                return i+1
        return 0

    def _template_qeq(self, e1, bounds):
       """
       Generate decision variable for electrostatic energy equation for a particular element

       :param e1: Chemical symbol for element 1
       :type e1: str

       :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
       :type bounds: float

       """
       ie1 = self._get_element_number(e1)
       if ie1 == 0: return

       # gamma, chi and eta
       for index, line in enumerate(self.onebody[4::4]):
           if (line[0] == e1):
               break
       line_number = 3 + (4*index) + 1

       gamma = float(self.onebody[line_number][7-1]) #  6th term, -1 for 0 indexing
       chi   = float(self.onebody[line_number+1][14-8-1]) # 14th term, -8 for previous line, -1 for 0 indexing
       eta   = float(self.onebody[line_number+1][15-8-1]) # 15th term, -8 for previous line, -1 for 0 indexing

       # gamma
       self.onebody[line_number][7-1] = '<<gam_'+e1+'>>'
       delta = bounds * np.absolute(gamma)
       self.params_write.append(['gam_'+e1, str(gamma-delta), str(gamma+delta)])

       # chi
       self.onebody[line_number+1][14-8-1] = '<<chi_'+e1+'>>'
       delta = bounds * np.absolute(chi)
       self.params_write.append(['chi_'+e1, str(chi-delta), str(chi+delta)])

       # eta
       self.onebody[line_number+1][15-8-1] = '<<eta_'+e1+'>>'
       delta = bounds * np.absolute(eta)
       self.params_write.append(['eta_'+e1, str(eta-delta), str(eta+delta)])

       return

    def _template_bond_order(self, e1, e2, double_bond = False, triple_bond = False, bounds = 0.1):
        """
        Generate decision variables in the bond-order equation for bonds between two elements

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float

        :param double_bond: Flag for the presence of a double-bond between elements e1 and e2
        :type double_bond: bool

        :param triple_bond: Flag for the presence of a triple-bond between elements e1 and e2
        :type triple_bond: bool

        """
        ie1, ie2 = self._get_element_number(e1), self._get_element_number(e2)

        #-------------------#
        #--- SINGLE BOND ---#
        #-------------------#

        # PBO1 and PBO2
        for index, line in enumerate(self.twobody[2::2]):
            if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                break
        line_number = (2 + (2*index) + 1)

        #PBO1
        PBO1 = float(self.twobody[line_number][13-8-1]) # 13th term, -8 for previous line, -1 for 0 indexing
        delta = bounds * np.absolute(PBO1)
        self.twobody[line_number][13-8-1] = '<<PBO1_'+e1+'_'+e2+'>>'
        self.params_write.append(['PBO1_'+e1+'_'+e2, str(PBO1-delta), str(PBO1+delta)])

        #PBO2
        PBO2 = float(self.twobody[line_number][14-8-1]) # 14th term, -8 for previous line, -1 for 0 indexing
        delta = bounds * np.absolute(PBO2)
        self.twobody[line_number][14-8-1] = '<<PBO2_'+e1+'_'+e2+'>>'
        self.params_write.append(['PBO2_'+e1+'_'+e2, str(PBO2-delta), str(PBO2+delta)])

        # ro_sigma
        for index, line in enumerate(self.offdiagonal[1:]):
            if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                break
        line_number = (1 + (1*index))

        # ro_sigma
        ro_sigma = float(self.offdiagonal[line_number][4+2-1])  # 4th term, +2 for atom indices, -1 for 0 indexing
        delta = bounds * np.absolute(ro_sigma)
        self.offdiagonal[line_number][4+2-1] = '<<ro_sigma_'+e1+'_'+e2+'>>'
        self.params_write.append(['ro_sigma_'+e1+'_'+e2, str(ro_sigma-delta), str(ro_sigma+delta)])

        #-------------------#
        #--- DOUBLE BOND ---#
        #-------------------#
        if double_bond:
            # PBO1 and PBO2
            for index, line in enumerate(self.twobody[2::2]):
                if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                    break
            line_number = (2 + (2*index) + 1)

            #PBO3
            PBO3 = float(self.twobody[line_number][10-8-1]) # 10th term, -8 for previous line, -1 for 0 indexing
            delta = bounds * np.absolute(PBO3)
            self.twobody[line_number][10-8-1] = '<<PBO3_'+e1+'_'+e2+'>>'
            self.params_write.append(['PBO3_'+e1+'_'+e2, str(PBO3-delta), str(PBO3+delta)])

            #PBO4
            PBO4 = float(self.twobody[line_number][11-8-1]) # 11th term, -8 for previous line, -1 for 0 indexing
            delta = bounds * np.absolute(PBO4)
            self.twobody[line_number][11-8-1] = '<<PBO4_'+e1+'_'+e2+'>>'
            self.params_write.append(['PBO4_'+e1+'_'+e2, str(PBO4-delta), str(PBO4+delta)])

            # ro_pi
            for index, line in enumerate(self.offdiagonal[1:]):
                if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                    break
            line_number = (1 + (1*index))

            # ro_pi
            ro_pi = float(self.offdiagonal[line_number][5+2-1])  # 4th term, +2 for atom indices, -1 for 0 indexing
            delta = bounds * np.absolute(ro_pi)
            self.offdiagonal[line_number][5+2-1] = '<<ro_pi_'+e1+'_'+e2+'>>'
            self.params_write.append(['ro_pi_'+e1+'_'+e2, str(ro_pi-delta), str(ro_pi+delta)])

        #-------------------#
        #--- TRIPLE BOND ---#
        #-------------------#
        if triple_bond:
            # PBO5 and PBO6
            for index, line in enumerate(self.twobody[2::2]):
                if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                    break
            line_number = (2 + (2*index))

            #PBO5
            PBO5 = float(self.twobody[line_number][5+2-1]) # 5th term, +2 for atom indices, -1 for 0 indexing
            delta = bounds * np.absolute(PBO5)
            self.twobody[line_number][5+2-1] = '<<PBO5_'+e1+'_'+e2+'>>'
            self.params_write.append(['PBO5_'+e1+'_'+e2, str(PBO5-delta), str(PBO5+delta)])

            #PBO6
            PBO6 = float(self.twobody[line_number][8+2-1]) # 8th term, +2 for atom indices, -1 for 0 indexing
            delta = bounds * np.absolute(PBO6)
            self.twobody[line_number][8+2-1] = '<<PBO6_'+e1+'_'+e2+'>>'
            self.params_write.append(['PBO6_'+e1+'_'+e2, str(PBO6-delta), str(PBO6+delta)])

            # ro_pipi
            for index, line in enumerate(self.offdiagonal[1:]):
                if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                    break
            line_number = (1 + (1*index))

            # ro_pipi
            ro_pipi = float(self.offdiagonal[line_number][6+2-1]) # 6th term, +2 for atom indices, -1 for 0 indexing
            delta = bounds * np.absolute(ro_pipi)
            self.offdiagonal[line_number][6+2-1] = '<<ro_pipi_'+e1+'_'+e2+'>>'
            self.params_write.append(['ro_pipi_'+e1+'_'+e2, str(ro_pipi-delta), str(ro_pipi+delta)])

        return

    def _template_bond_energy_attractive(self, e1, e2, double_bond = False, triple_bond = False, bounds = 0.1):
        """
        Generate decision variables related to the two-body attractive term

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float

        :param double_bond: Flag for the presence of a double-bond between elements e1 and e2
        :type double_bond: bool

        :param triple_bond: Flag for the presence of a triple-bond between elements e1 and e2
        :type triple_bond: bool
        """
        ie1, ie2 = self._get_element_number(e1), self._get_element_number(e2)

        #-------------------#
        #--- SINGLE BOND ---#
        #-------------------#

        # De_sigma
        for index, line in enumerate(self.twobody[2::2]):
            if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                break
        line_number = (2 + (2*index))

        #De_sigma
        De_sigma = float(self.twobody[line_number][1+2-1]) # 1st term, +2 for atom indices, -1 for 0 indexing
        delta = bounds * np.absolute(De_sigma)
        self.twobody[line_number][1+2-1] = '<<De_sigma_'+e1+'_'+e2+'>>'
        self.params_write.append(['De_sigma_'+e1+'_'+e2, str(De_sigma-delta), str(De_sigma+delta)])

        #PBE1
        PBE1 = float(self.twobody[line_number][4+2-1]) # 4th term, +2 for atom indices, -1 for 0 indexing
        delta = bounds * np.absolute(PBE1)
        self.twobody[line_number][4+2-1] = '<<PBE1_'+e1+'_'+e2+'>>'
        self.params_write.append(['PBE1_'+e1+'_'+e2, str(PBE1-delta), str(PBE1+delta)])

        line_number = (2 + (2*index) + 1)  # PBE2 is on the next line

        #PBE2
        PBE2 = float(self.twobody[line_number][9-8-1]) # 9th term, -8 for previous line, -1 for 0 indexing
        delta = bounds * np.absolute(PBE2)
        self.twobody[line_number][9-8-1] = '<<PBE2_'+e1+'_'+e2+'>>'
        self.params_write.append(['PBE2_'+e1+'_'+e2, str(PBE2-delta), str(PBE2+delta)])

        #-------------------#
        #--- DOUBLE BOND ---#
        #-------------------#
        # De_pi
        for index, line in enumerate(self.twobody[2::2]):
            if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                break
        line_number = (2 + (2*index))
        De_pi = float(self.twobody[line_number][2+2-1]) # 2nd term, +2 for atom indices, -1 for 0 indexing

        if double_bond:
            delta = bounds * np.absolute(De_pi)
            self.twobody[line_number][2+2-1] = '<<De_pi_'+e1+'_'+e2+'>>'
            self.params_write.append(['De_pi_'+e1+'_'+e2, str(De_pi-delta), str(De_pi+delta)])
        else:
            if De_pi != 0.0:
                print('Double bond parameters for ' + e1 + '-' + e2 + ' bond will not be optimized. Current non-zero values in the template forcefield will be retained.')

        #-------------------#
        #--- TRIPLE BOND ---#
        #-------------------#
        # De_pipi
        for index, line in enumerate(self.twobody[2::2]):
            if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                break
        line_number = (2 + (2*index))
        De_pipi = float(self.twobody[line_number][3+2-1]) # 3rd term, +2 for atom indices, -1 for 0 indexing

        if triple_bond:
            delta = bounds * np.absolute(De_pipi)
            self.twobody[line_number][3+2-1] = '<<De_pipi_'+e1+'_'+e2+'>>'
            self.params_write.append(['De_pipi_'+e1+'_'+e2, str(De_pipi-delta), str(De_pipi+delta)])
        else:
            if De_pipi != 0.0:
                print('Triple bond parameters for ' + e1 + '-' + e2 + ' bond will not be optimized. Current non-zero values in the template forcefield will be retained.')

    def _template_bond_energy_vdW(self, e1, e2, f13 = False, bounds = 0.1):
        """
        Generate decision variables related to the two-body repulsive (i.e. van der Waals) term

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float

        :param f13: Flag for the optimization of common variables
        :type f13: bool
        """
        ie1, ie2 = self._get_element_number(e1), self._get_element_number(e2)

        # Dij, rvdWm alpha_ij in off-diagonal
        for index, line in enumerate(self.offdiagonal[1:]):
            if (int(line[0]) == ie1 and int(line[1]) == ie2) or (int(line[0]) == ie2 and int(line[1]) == ie1):
                break
        line_number = (1 + (1*index))

        # Dij
        Dij = float(self.offdiagonal[line_number][1+2-1]) # 1st term, +2 for atom indices, -1 for 0 indexing
        delta = bounds * np.absolute(Dij)
        self.offdiagonal[line_number][1+2-1] = '<<Dij_'+e1+'_'+e2+'>>'
        self.params_write.append(['Dij_'+e1+'_'+e2, str(Dij-delta), str(Dij+delta)])

        # rvdW
        rvdW = float(self.offdiagonal[line_number][2+2-1]) # 2nd term, +2 for atom indices, -1 for 0 indexing
        delta = bounds * np.absolute(rvdW)
        self.offdiagonal[line_number][2+2-1] = '<<rvdW_'+e1+'_'+e2+'>>'
        self.params_write.append(['rvdW_'+e1+'_'+e2, str(rvdW-delta), str(rvdW+delta)])

        # alpha_ij
        alpha_ij = float(self.offdiagonal[line_number][3+2-1]) # 3rd term, +2 for atom indices, -1 for 0 indexing
        delta = bounds * np.absolute(alpha_ij)
        self.offdiagonal[line_number][3+2-1] = '<<alpha_ij_'+e1+'_'+e2+'>>'
        self.params_write.append(['alpha_ij_'+e1+'_'+e2, str(alpha_ij-delta), str(alpha_ij+delta)])

        ### WRITE PARAMETER IN F13
        if f13:
            # gamma_w in element 1 in onebody
            for index, line in enumerate(self.onebody[4::4]):
                if line[0].lower().upper() == e1.lower().upper():
                    break
            line_number = (4 + (4*index) + 1)

            # gamma_w
            gamma_w = float(self.onebody[line_number][10-8-1]) # 10th term, -8 for previous line, -1 for 0 indexing
            delta = bounds * np.absolute(gamma_w)
            self.onebody[line_number][3+2-1] = '<<gamma_w_'+e1+'>>'
            self.params_write.append(['gamma_w_'+e1, str(gamma_w-delta), str(gamma_w+delta)])

            # gamma_w in element 2 in onebody
            for index, line in enumerate(self.onebody[4::4]):
                if line[0].lower().upper() == e2.lower().upper():
                    break
            line_number = (4 + (4*index) + 1)

            # gamma_w
            gamma_w = float(self.onebody[line_number][10-8-1]) # 10th term, -8 for previous line, -1 for 0 indexing
            delta = bounds * np.absolute(gamma_w)
            self.onebody[line_number][3+2-1] = '<<gamma_w_'+e2+'>>'
            self.params_write.append(['gamma_w_'+e2, str(gamma_w-delta), str(gamma_w+delta)])

            # Pvdw1 in general parameters
            line_number = 1 + 29  # 29th parameter, +1 for header line
            P_vdW1 = float(self.general[line_number][0])
            delta = bounds * np.absolute(P_vdW1)
            self.general[line_number][0] = '<<PvdW>>'
            self.params_write.append(['PvdW', str(P_vdW1-delta), str(P_vdW1+delta)])

    def _template_threebody_energy(self, e1, e2, e3, bounds = 0.1):
        """
        Generate decision variables related to the three-body angle term

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param e3: Chemical symbol for element 3
        :type e3: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float
        """
        # theta0, Pval1, Pval2 in threebody
        for triplet in list(set(list(itertools.permutations([e1,e2,e3])))):
            ie1 = self._get_element_number(triplet[0])
            ie2 = self._get_element_number(triplet[1])
            ie3 = self._get_element_number(triplet[2])
            for index, line in enumerate(self.threebody[1:]):
                if int(line[0]) == ie1 and int(line[1]) == ie2 and int(line[2]) == ie3:
                    line_number = (1 + (1*index))

                    theta0 = float(self.threebody[line_number][1+3-1]) # 1st term, +3 for atom indices, -1 for 0 indexing
                    delta = bounds * np.absolute(theta0)
                    self.threebody[line_number][1+3-1] = '<<theta0_'+triplet[0]+'_'+triplet[1]+'_'+triplet[2]+'>>'
                    self.params_write.append(['theta0_'+triplet[0]+'_'+triplet[1]+'_'+triplet[2], str(theta0-delta), str(theta0+delta)])

                    Pval1 = float(self.threebody[line_number][2+3-1]) # 2nd term, +3 for atom indices, -1 for 0 indexing
                    delta = bounds * np.absolute(Pval1)
                    self.threebody[line_number][2+3-1] = '<<Pval1_'+triplet[0]+'_'+triplet[1]+'_'+triplet[2]+'>>'
                    self.params_write.append(['Pval1_'+triplet[0]+'_'+triplet[1]+'_'+triplet[2], str(Pval1-delta), str(Pval1+delta)])

                    Pval2 = float(self.threebody[line_number][3+3-1]) # 3rd term, +3 for atom indices, -1 for 0 indexing
                    delta = bounds * np.absolute(Pval2)
                    self.threebody[line_number][3+3-1] = '<<Pval2_'+triplet[0]+'_'+triplet[1]+'_'+triplet[2]+'>>'
                    self.params_write.append(['Pval2_'+triplet[0]+'_'+triplet[1]+'_'+triplet[2], str(Pval2-delta), str(Pval2+delta)])

    def _template_fourbody_energy(self, e1, e2, e3, e4, bounds = 0.1):
        """
        Generate decision variables related to the four-body dihedral term

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param e3: Chemical symbol for element 3
        :type e3: str

        :param e4: Chemical symbol for element 4
        :type e4: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float
        """
        # V1, V2, V3, Ptor1 in fourbody
        for quartet in list(set(list(itertools.permutations([e1,e2,e3,e4])))):
            ie1 = self._get_element_number(quartet[0])
            ie2 = self._get_element_number(quartet[1])
            ie3 = self._get_element_number(quartet[2])
            ie4 = self._get_element_number(quartet[3])

            for index, line in enumerate(self.fourbody[1:]):
                if int(line[0]) == ie1 and int(line[1]) == ie2 and int(line[2]) == ie3 and int(line[3]) == ie4:
                    line_number = (1 + (1*index))

                    V1 = float(self.fourbody[line_number][1+4-1]) # 1st term, +3 for atom indices, -1 for 0 indexing
                    delta = bounds * np.absolute(V1)
                    self.fourbody[line_number][1+4-1] = '<<V1_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3]+'>>'
                    self.params_write.append(['V1_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3], str(V1-delta), str(V1+delta)])

                    V2 = float(self.fourbody[line_number][2+4-1]) # 2nd term, +3 for atom indices, -1 for 0 indexing
                    delta = bounds * np.absolute(V2)
                    self.fourbody[line_number][2+4-1] = '<<V2_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3]+'>>'
                    self.params_write.append(['V2_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3], str(V2-delta), str(V2+delta)])

                    V3 = float(self.fourbody[line_number][3+4-1]) # 3rd term, +3 for atom indices, -1 for 0 indexing
                    delta = bounds * np.absolute(V3)
                    self.fourbody[line_number][3+4-1] = '<<V3_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3]+'>>'
                    self.params_write.append(['V3_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3], str(V3-delta), str(V3+delta)])

                    Ptor1 = float(self.fourbody[line_number][4+4-1]) # 4th term, +3 for atom indices, -1 for 0 indexing
                    delta = bounds * np.absolute(Ptor1)
                    self.fourbody[line_number][4+4-1] = '<<Ptor1_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3]+'>>'
                    self.params_write.append(['Ptor1_'+quartet[0]+'_'+quartet[1]+'_'+quartet[2]+'_'+quartet[3], str(Ptor1-delta), str(Ptor1+delta)])

    def generate_templates(self):
        """
        Function to write-out the current modified forcefield sections into a forcefield template file
        """
        with open(self.ranges, 'w') as ranges_file:
            for parameter in self.params_write:
                ranges_file.write(' '.join(parameter) + '\n')

        with open(self.template,'w') as template:
            template.write(self.header)
            for line in self.general:
                template.write(' '.join(line)+'\n')
            for line in self.onebody:
                template.write(' '.join(line)+'\n')
            for line in self.twobody:
                template.write(' '.join(line)+'\n')
            for line in self.offdiagonal:
                template.write(' '.join(line)+'\n')
            for line in self.threebody:
                template.write(' '.join(line)+'\n')
            for line in self.fourbody:
                template.write(' '.join(line)+'\n')
            for line in self.hbond:
                template.write(' '.join(line)+'\n')

    def make_template_qeq(self, e1, bounds=0.1):
        """
        Function to generate decision variable for Charge Equilibration (QEq) terms

        : param e1 : Chemical symbol for element 1
        : type e1  : str

        : param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        : type bounds: float

        """
        # GET ONE_BODY_PARAMETERS SPECIFIC TO QEQ
        self._template_qeq(e1,bounds)
        return

    def make_template_twobody(self, e1, e2, double_bond = False, triple_bond = False, bounds = 0.1, common = False):
        """
        Function to generate decision variables for all two-body terms (i.e. bond-order, attractive and vdW) between two given elements

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float

        :param double_bond: Flag for the presence of a double-bond between elements e1 and e2
        :type double_bond: bool

        :param triple_bond: Flag for the presence of a triple-bond between elements e1 and e2
        :type triple_bond: bool

        :param common: Flag for the optimization of common parameters
        :type common: bool
        """
        # GET BOND_ORDER_PARAMETERS
        self._template_bond_order(e1,e2,double_bond = double_bond, triple_bond = triple_bond, bounds = bounds)
        self._template_bond_energy_attractive(e1,e2,double_bond = double_bond, triple_bond = triple_bond, bounds = bounds)
        self._template_bond_energy_vdW(e1,e2, f13 = common, bounds = bounds)
        return

    def make_template_threebody(self, e1, e2, e3, bounds = 0.1, common = False):
        """
        Function to generate decision variables for all three-body terms for a given triplet of elements

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param e3: Chemical symbol for element 3
        :type e3: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float

        :param common: Flag for the optimization of common parameters
        :type common: bool
        """
        self._template_threebody_energy(e1, e2, e3, bounds = bounds)
        return

    def make_template_fourbody(self, e1, e2, e3, e4, bounds = 0.1, common = False):
        """
        Function to generate decision variables for all four-body terms for a given quartet of elements

        :param e1: Chemical symbol for element 1
        :type e1: str

        :param e2: Chemical symbol for element 2
        :type e2: str

        :param e3: Chemical symbol for element 3
        :type e3: str

        :param e4: Chemical symbol for element 4
        :type e4: str

        :param bounds: Maximum deviation allowed for each decision variable from its current value in the forcefield
        :type bounds: float

        :param common: Flag for the optimization of common parameters
        :type common: bool
        """
        self._template_fourbody_energy(e1, e2, e3, e4, bounds = bounds)
        return

    def write_formatted_forcefields(self):
        """
        Function to write-out the current forcefield with correct ReaxFF formatting

        :param outfilename: File to which formatted forcefield to be written to
        :type outfilename: str
        """
        string = self.header

        # Write general parameters
        for lineno, line in enumerate(self.general):
            if lineno == 0:
                string += ' %2d       %s\n' %(int(line[0]), ' '.join(line[1:]))
            else:
                string += '%10.4f %s\n' %(float(line[0]), ' '.join(line[1:]))

        # One-body term
        for lineno, line in enumerate(self.onebody[:4]):
            if lineno == 0:
                string += '%3d    %s\n' %(int(line[0]), ' '.join(line[1:]))
            else:
                string += '            %s\n' %(' '.join(line))

        for lineno, line in enumerate(self.onebody[4:]):
            if lineno % 4 == 0:
                string += ' %-2s' % line[0] + ''.join(['%9.4f' % float(val) for val in line[1:]]) + '\n'
            else:
                string += '   ' + ''.join(['%9.4f' % float(val) for val in line]) + '\n'

        # Two-body terms
        for lineno, line in enumerate(self.twobody[:2]):
            if lineno == 0:
                string += '%3d      %s\n' %(int(line[0]), ' '.join(line[1:]))
            else:
                string += '            %s\n' %(' '.join(line))

        for lineno, line in enumerate(self.twobody[2:]):
            if lineno % 2 == 0:
                string += '%3d' % int(line[0]) + '%3d' % int(line[1]) + ''.join(['%9.4f' % float(val) for val in line[2:]]) + '\n'
            else:
                string += '      ' + ''.join(['%9.4f' % float(val) for val in line]) + '\n'

        # Off-diagonal
        for lineno, line in enumerate(self.offdiagonal):
            if lineno == 0:
                string += '%3d    ' % int(line[0]) + ' '.join(line[1:]) + '\n'
            else:
                string += '%3d' % int(line[0]) + '%3d' % int(line[1]) + ''.join(['%9.4f' % float(val) for val in line[2:]]) + '\n'

        # Threebody
        for lineno, line in enumerate(self.threebody):
            if lineno == 0:
                string += '%3d    ' % int(line[0]) + ' '.join(line[1:]) + '\n'
            else:
                string += '%3d' % int(line[0]) + '%3d' % int(line[1]) + '%3d' % int(line[2]) + ''.join(['%9.4f' % float(val) for val in line[3:]]) + '\n'

        # Fourbody
        for lineno, line in enumerate(self.fourbody):
            if lineno == 0:
                string += '%3d    ' % int(line[0]) + ' '.join(line[1:]) + '\n'
            else:
                string += '%3d' % int(line[0]) + '%3d' % int(line[1]) + '%3d' % int(line[2]) + '%3d' % int(line[3]) + ''.join(['%9.4f' % float(val) for val in line[4:]])+ '\n'

        # Hbond
        for lineno, line in enumerate(self.hbond):
            if lineno == 0:
                string += '%3d    ' % int(line[0]) + ' '.join(line[1:]) + '\n'
            else:
                string += '%3d' % int(line[0]) + '%3d' % int(line[1]) + '%3d' % int(line[2]) + ''.join(['%9.4f' % float(val) for val in line[3:]]) + '\n'

        return string

    def write_gulp_library(self, outfilename = None):
        """
        Function to write-out the forcefield in the GULP library format

        :param outfilename: File to which the GULP ReaxFF forcefield library
        :type outfilename: str
        """
        # HEADER
        string = ''
        string += '#\n'
        string += '#  ReaxFF force field\n'
        string += '#\n'
        string += '#  Original paper:\n'
        string += '#\n'
        string += '#  A.C.T. van Duin, S. Dasgupta, F. Lorant and W.A. Goddard III,\n'
        string += '#  J. Phys. Chem. A, 105, 9396-9409 (2001)\n'
        string += '#\n'
        string += '#\n'

        # CUTOFFS
        string += '#  Cutoffs for VDW & Coulomb terms\n'
        string += '#\n'
        string += 'reaxFFvdwcutoff %12.4f\n' % float(self.general[13][0])
        string += 'reaxFFqcutoff   %12.4f\n' % float(self.general[13][0])
        string += '#\n'

        #BOND ORDER THRESHOLD
        string += '#  Bond order threshold - check anglemin as this is cutof2 given in control file\n'
        string += '#\n'
        string += 'reaxFFtol       %12.10f 0.001\n' % (float(self.general[30][0])*0.01)
        string += '#\n'

        #SPECIES INDEPENDENT PARAMETERS
        string += '#  Species independent parameters \n'
        string += '#\n'
        string += 'reaxff0_bond     %12.6f %12.6f\n' %(float(self.general[1][0]), float(self.general[2][0]))
        string += 'reaxff0_over     %12.6f %12.6f %12.6f %12.6f %12.6f\n' %(float(self.general[33][0]), float(self.general[32][0]), float(self.general[7][0]), float(self.general[9][0]), float(self.general[10][0]))
        string += 'reaxff0_valence  %12.6f %12.6f %12.6f %12.6f\n' %(float(self.general[15][0]), float(self.general[34][0]), float(self.general[17][0]), float(self.general[18][0]))
        string += 'reaxff0_penalty  %12.6f %12.6f %12.6f\n' %(float(self.general[20][0]), float(self.general[21][0]), float(self.general[22][0]))
        string += 'reaxff0_torsion  %12.6f %12.6f %12.6f %12.6f\n' %(float(self.general[24][0]), float(self.general[25][0]), float(self.general[26][0]), float(self.general[28][0]))
        string += 'reaxff0_vdw      %12.6f\n' % float(self.general[29][0])
        string += 'reaxff0_lonepair %12.6f\n' % float(self.general[16][0])
        string += '#\n'

        #SPECIES PARAMETERS - RADII
        string += '#  Species parameters \n'
        string += '#\n'
        string += 'reaxff1_radii\n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f %8.4f %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno][1]), float(self.onebody[lineno][7]), float(self.onebody[lineno+2][0]))

        #SPECIES PARAMETERS - VALENCE
        string += 'reaxff1_valence\n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f %8.4f %8.4f %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno][2]), float(self.onebody[lineno+3][3]), float(self.onebody[lineno][8]), float(self.onebody[lineno+1][2]))

        #SPECIES PARAMETERS - OVER
        string += 'reaxff1_over\n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f %8.4f %8.4f %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno+2][4]), float(self.onebody[lineno+2][3]), float(self.onebody[lineno+2][5]), float(self.onebody[lineno+3][0]))

        #SPECIES PARAMETERS - UNDER KCAL
        string += 'reaxff1_under kcal\n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno+1][3]))

        #SPECIES PARAMETERS - LONEPAIR KCAL
        string += 'reaxff1_lonepair kcal\n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f %8.4f\n' % (self.onebody[lineno][0], 0.5*(float(self.onebody[lineno][8]) - float(self.onebody[lineno][2])), float(self.onebody[lineno+2][1]))

        #SPECIES PARAMETERS - ANGLE
        string += 'reaxff1_angle\n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno+3][1]), float(self.onebody[lineno+3][4]))

        #SPECIES PARAMETERS - ANGLE
        string += 'reaxff1_morse kcal\n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f %8.4f %8.4f %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno+1][0]), float(self.onebody[lineno][5]), float(self.onebody[lineno][4]), float(self.onebody[lineno+1][1]))

        #ELEMENT PARAMETERS
        string += '#\n'
        string += '#  Element parameters \n'
        string += '#\n'

        #ELEMENT PARAMETERS - CHI
        string += 'reaxff_chi  \n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno+1][5]))

        #ELEMENT PARAMETERS - MU
        string += 'reaxff_mu   \n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno+1][6]))

        #ELEMENT PARAMETERS - MU
        string += 'reaxff_gamma  \n'
        for lineno in list(range(4,len(self.onebody),4)):
            string += '%-2s core %8.4f\n' % (self.onebody[lineno][0], float(self.onebody[lineno][6]))

        #BOND PARAMETERS
        string += '#\n'
        string += '#  Bond parameters \n'
        string += '#\n'

        #BOND PARAMETERS - BO OVER BO13
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        first = True
        for lineno in list(range(2,len(self.twobody),2)):
            if (float(self.twobody[lineno][7]) > 0.001 and float(self.twobody[lineno+1][6]) > 0.001):
                if first:
                    string += 'reaxff2_bo over bo13\n'
                    first = False
                n1, n2 = int(self.twobody[lineno][0]), int(self.twobody[lineno][1])
                bo3 = 0.0 if (np.absolute(float(self.twobody[lineno+1][1])-1.0) < 1.0e-12) else float(self.twobody[lineno+1][1])
                string += '%-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], float(self.twobody[lineno+1][4]), float(self.twobody[lineno+1][5]), bo3, float(self.twobody[lineno+1][2]), float(self.twobody[lineno][6]), float(self.twobody[lineno][8]))

        #BOND PARAMETERS - BO UNDER
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        first = True
        for lineno in list(range(2,len(self.twobody),2)):
            if (float(self.twobody[lineno][7]) > 0.001 and float(self.twobody[lineno+1][6]) <= 0.001):
                if first:
                    string += 'reaxff2_bo bo13\n'
                    first = False
                n1, n2 = int(self.twobody[lineno][0]), int(self.twobody[lineno][1])
                bo3 = 0.0 if math.isclose(1.0, float(self.twobody[lineno+1][1])) else float(self.twobody[lineno+1][1])
                string += '%-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], float(self.twobody[lineno+1][4]), float(self.twobody[lineno+1][5]), bo3, float(self.twobody[lineno+1][2]), float(self.twobody[lineno][6]), float(self.twobody[lineno][8]))

        #BOND PARAMETERS - BO OVER
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        first = True
        for lineno in list(range(2,len(self.twobody),2)):
            if (float(self.twobody[lineno][7]) <= 0.001 and float(self.twobody[lineno+1][6]) > 0.001):
                if first:
                    string += 'reaxff2_bo over\n'
                    first = False
                n1, n2 = int(self.twobody[lineno][0]), int(self.twobody[lineno][1])
                bo3 = 0.0 if math.isclose(1.0, float(self.twobody[lineno+1][1])) else float(self.twobody[lineno+1][1])
                string += '%-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], float(self.twobody[lineno+1][4]), float(self.twobody[lineno+1][5]), bo3, float(self.twobody[lineno+1][2]), float(self.twobody[lineno][6]), float(self.twobody[lineno][8]))

        #BOND PARAMETERS - BO
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        first = True
        for lineno in list(range(2,len(self.twobody),2)):
            if (float(self.twobody[lineno][7]) <= 0.001 and float(self.twobody[lineno+1][6]) <= 0.001):
                if first:
                    string += 'reaxff2_bo \n'
                    first = False
                n1, n2 = int(self.twobody[lineno][0]), int(self.twobody[lineno][1])
                bo3 = 0.0 if math.isclose(1.0, float(self.twobody[lineno+1][1])) else float(self.twobody[lineno+1][1])
                string += '%-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], float(self.twobody[lineno+1][4]), float(self.twobody[lineno+1][5]), bo3, float(self.twobody[lineno+1][2]), float(self.twobody[lineno][6]), float(self.twobody[lineno][8]))

        #BOND PARAMETERS - BOND KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff2_bond kcal \n'
        for lineno in list(range(2,len(self.twobody),2)):
            n1, n2 = int(self.twobody[lineno][0]), int(self.twobody[lineno][1])
            string += '%-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], float(self.twobody[lineno][2]), float(self.twobody[lineno][3]), float(self.twobody[lineno][4]), float(self.twobody[lineno][5]), float(self.twobody[lineno+1][0]))

        #BOND PARAMETERS - BOND OVER
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff2_over \n'
        for lineno in list(range(2,len(self.twobody),2)):
            n1, n2 = int(self.twobody[lineno][0]), int(self.twobody[lineno][1])
            string += '%-2s core %-2s core %8.4f \n' % (element_number[n1], element_number[n2], float(self.twobody[lineno][9]))

        #BOND PARAMETERS - BOND PEN KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        first = True
        for lineno in list(range(2,len(self.twobody),2)):
            if float(self.twobody[lineno+1][7]) > 0.0:
                if first:
                    string += 'reaxff2_pen kcal\n'
                    first = False
                n1, n2 = int(self.twobody[lineno][0]), int(self.twobody[lineno][1])
                string += '%-2s core %-2s core %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], float(self.twobody[lineno+1][7]), float(self.general[14][0]), 1.0)

        #BOND PARAMETERS - BOND MORSE KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff2_morse kcal\n'
        for line in self.offdiagonal[1:]:
            n1, n2 = int(line[0]), int(line[1])
            string += '%-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], float(line[2]), float(line[4]), float(line[3]), float(line[5]), float(line[6]), float(line[7]))

        #ANGLE PARAMETERS
        string += '#\n'
        string += '#  Angle parameters \n'
        string += '#\n'

        #ANGLE PARAMETERS - ANGLE KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff3_angle kcal\n'
        for line in self.threebody[1:]:
            n2, n1, n3 = int(line[0]), int(line[1]), int(line[2])
            if float(line[4]) > 0.0:
                string += '%-2s core %-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], element_number[n3], float(line[3]), float(line[4]), float(line[5]), float(line[9]), float(line[7]))

        #ANGLE PARAMETERS - PENALTY KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff3_penalty kcal \n'
        for line in self.threebody[1:]:
            n2, n1, n3 = int(line[0]), int(line[1]), int(line[2])
            string += '%-2s core %-2s core %-2s core %8.4f\n' % (element_number[n1], element_number[n2], element_number[n3], float(line[8]))

        #ANGLE PARAMETERS - CONJUGATION KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff3_conjugation kcal \n'
        for line in self.threebody[1:]:
            if np.absolute(float(line[6])) > 1.0e-4:
                n2, n1, n3 = int(line[0]), int(line[1]), int(line[2])
                string += '%-2s core %-2s core %-2s core %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], element_number[n3], float(line[6]), float(self.general[3][0]), float(self.general[39][0]), float(self.general[31][0]))

        #HBOND PARAMETERS
        string += '#\n'
        string += '#  Hydrogen bond parameters \n'
        string += '#\n'

        #HBOND PARAMETERS - CONJUGATION KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff3_hbond kcal \n'
        for line in self.hbond[1:]:
            n2, n1, n3 = int(line[0]), int(line[1]), int(line[2])
            string += '%-2s core %-2s core %-2s core %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], element_number[n3], float(line[3]), float(line[4]), float(line[5]), float(line[6]))

        #TORSION PARAMETERS
        string += '#\n'
        string += '#  Torsion parameters \n'
        string += '#\n'

        #HBOND PARAMETERS - CONJUGATION KCAL
        element_number = ['X']
        for line in self.onebody[4::4]:
            element_number.append(line[0])
        string += 'reaxff4_torsion kcal \n'
        for line in self.fourbody[1:]:
            n1, n2, n3, n4 = int(line[0]), int(line[1]), int(line[2]), int(line[3])
            string += '%-2s core %-2s core %-2s core %-2s core %8.4f %8.4f %8.4f %8.4f %8.4f\n' % (element_number[n1], element_number[n2], element_number[n3], element_number[n4], float(line[4]), float(line[5]), float(line[6]), float(line[7]), float(line[8]))

        #GENERAL PARAMETERS - Thresholds and cutoffs
        string += '\n\n'
        string += 'reaxfftol {0:.12f}'.format(self.bo_threshold)
        string += '\n\n'

        if outfilename is not None:
            libfile = open(outfilename, 'w')
            libfile.write(string)
            libfile.close()
            return
        else:
            return string