#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#    Project: Azimuthal integration
#             https://github.com/silx-kit/pyFAI
#
#    Copyright (C) European Synchrotron Radiation Facility, Grenoble, France
#
#    Principal author:       Jérôme Kieffer (Jerome.Kieffer@ESRF.eu)
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.

"""Module used to perform the geometric refinement of the model
"""

from __future__ import print_function, division, absolute_import

__author__ = "Jerome Kieffer"
__contact__ = "Jerome.Kieffer@ESRF.eu"
__license__ = "MIT"
__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
__date__ = "10/01/2018"
__status__ = "development"

import os
import tempfile
import subprocess
import logging
import numpy
import types
from math import pi
from . import azimuthalIntegrator
from .calibrant import Calibrant, CALIBRANT_FACTORY
from .utils.ellipse import fit_ellipse
AzimuthalIntegrator = azimuthalIntegrator.AzimuthalIntegrator
from scipy.optimize import fmin, leastsq, fmin_slsqp
try:
    from scipy.optimize import basinhopping as anneal
except ImportError:
    from scipy.optimize import anneal
try:
    from scipy.optimize import curve_fit
except ImportError:
    curve_fit = None

if os.name != "nt":
    WindowsError = RuntimeError

logger = logging.getLogger(__name__)
# logger.setLevel(logging.DEBUG)
ROCA = "/opt/saxs/roca"

####################
# GeometryRefinement
####################

class GeometryRefinement(AzimuthalIntegrator):
    def __init__(self, data=None, dist=1, poni1=None, poni2=None,
                 rot1=0, rot2=0, rot3=0,
                 pixel1=None, pixel2=None, splineFile=None, detector=None,
                 wavelength=None, calibrant=None):
        """
        :param data: ndarray float64 shape = n, 3
            col0: pos in dim0 (in pixels)
            col1: pos in dim1 (in pixels)
            col2: ring index in calibrant object
        :param dist: guessed sample-detector distance (optional, in m)
        :param poni1: guessed PONI coordinate along the Y axis (optional, in m)
        :param poni2: guessed PONI coordinate along the X axis (optional, in m)
        :param rot1: guessed tilt of the detector around the Y axis (optional, in rad)
        :param rot2: guessed tilt of the detector around the X axis (optional, in rad)
        :param rot3: guessed tilt of the detector around the incoming beam axis (optional, in rad)
        :param pixel1: Pixel size along the vertical direction of the detector (in m), almost mandatory
        :param pixel2: Pixel size along the horizontal direction of the detector (in m), almost mandatory
        :param splineFile: file describing the detector as 2 cubic splines. Replaces pixel1 & pixel2
        :param detector: name of the detector or Detector instance. Replaces splineFile, pixel1 & pixel2
        :param wavelength: wavelength in m (1.54e-10)
        :param calibrant: instance of pyFAI.calibrant.Calibrant containing the d-Spacing

        """
        if data is None:
            self.data = None
        else:
            self.data = numpy.array(data, dtype=numpy.float64)
            assert self.data.ndim == 2
            assert self.data.shape[1] in [3, 4]  # 3 for non weighted, 4 for weighted refinement
            assert self.data.shape[0] > 0

        if (pixel1 is None) and (pixel2 is None) and (splineFile is None) and (detector is None):
            raise RuntimeError("Setting up the geometry refinement without knowing the detector makes little sense")
        AzimuthalIntegrator.__init__(self, dist, 0, 0,
                                     rot1, rot2, rot3,
                                     pixel1, pixel2, splineFile, detector, wavelength=wavelength)

        if calibrant is None:
            self.calibrant = Calibrant()
        else:
            if isinstance(calibrant, Calibrant):
                self.calibrant = calibrant
            elif type(calibrant) in types.StringTypes:
                if calibrant in CALIBRANT_FACTORY:
                    self.calibrant = CALIBRANT_FACTORY(calibrant)
                else:
                    self.calibrant = Calibrant(filename=calibrant)
            else:
                self.calibrant = Calibrant(calibrant)

        self.calibrant.wavelength = self.wavelength

        if (poni1 is None) or (poni2 is None):
            self.guess_poni()
        else:
            self.poni1 = float(poni1)
            self.poni2 = float(poni2)
        self._dist_min = 0
        self._dist_max = 10
        self._poni1_min = -10000 * self.pixel1
        self._poni1_max = 15000 * self.pixel1
        self._poni2_min = -10000 * self.pixel2
        self._poni2_max = 15000 * self.pixel2
        self._rot1_min = -pi
        self._rot1_max = pi
        self._rot2_min = -pi
        self._rot2_max = pi
        self._rot3_min = -pi
        self._rot3_max = pi
        self._wavelength_min = 1e-15
        self._wavelength_max = 100.e-10

    def guess_poni(self):
        """PONI can be guessed by the centroid of the ring with lowest 2Theta

        It may try to fit an ellipse and sometimes it works
        """

        if len(self.calibrant.dSpacing):
            # logger.warning(self.calibrant.__repr__())s
            tth = self.calc_2th(self.data[:, 2])
        else:  # assume rings are in decreasing dSpacing in the file
            tth = self.data[:, 2]
        asrt = tth.argsort()
        tth = tth[asrt]
        srtdata = self.data[asrt]
        tth_min = tth.min()
        smallRing = srtdata[tth < (tth_min + 1e-6)]
        smallRing1 = smallRing[:, 0]
        smallRing2 = smallRing[:, 1]
        smallRing_in_m = self.detector.calc_cartesian_positions(smallRing1,
                                                                smallRing2)
        nbpt = len(smallRing)
        worked = False
        if nbpt > 5:
            # If there are many control point on the inner-most ring, fit an ellipse
            try:
                ellipse = fit_ellipse(*smallRing_in_m[:2])
                direct_dist = ellipse.half_long_axis / numpy.tan(tth_min)
                tilt = numpy.arctan2(ellipse.half_long_axis - ellipse.half_short_axis, ellipse.half_short_axis)
                cos_tilt = numpy.cos(tilt)
                sin_tilt = numpy.sin(tilt)
                angle = (ellipse.angle + numpy.pi / 2.0) % numpy.pi
                cos_tpr = numpy.cos(angle)
                sin_tpr = numpy.sin(angle)
                dist = direct_dist * cos_tilt
                poni1 = ellipse.center_1 - direct_dist * sin_tilt * sin_tpr
                poni2 = ellipse.center_2 - direct_dist * sin_tilt * cos_tpr
                rot2 = numpy.arcsin(sin_tilt * sin_tpr)  # or pi-
                rot1 = numpy.arccos(min(1.0, max(-1.0, (cos_tilt / numpy.sqrt(1 - sin_tpr * sin_tpr * sin_tilt * sin_tilt)))))  # + or -
                if cos_tpr * sin_tilt > 0:
                    rot1 = -rot1
                rot3 = 0
            except:
                worked = False
            else:
                if numpy.isnan(dist + poni1 + poni2 + rot1 + rot2 + rot3):
                    worked = False
                else:
                    worked = True
                    self.dist = dist
                    self.poni1 = poni1
                    self.poni2 = poni2
                    self.rot1 = rot1
                    self.rot2 = rot2
                    self.rot3 = rot3
        if not worked:
            self.poni1 = smallRing_in_m[0].sum() / nbpt
            self.poni2 = smallRing_in_m[1].sum() / nbpt

    def set_tolerance(self, value=10):
        """
        Set the tolerance for a refinement of the geometry; in percent of the original value

        :param value: Tolerance as a percentage

        """
        low = 1.0 - value / 100.
        hi = 1.0 + value / 100.
        self.dist_min = low * self.dist
        self.dist_max = hi * self.dist
        if abs(self.poni1) > (value / 100.) ** 2:
            self.poni1_min = min(low * self.poni1, hi * self.poni1)
            self.poni1_max = max(low * self.poni1, hi * self.poni1)
        else:
            self.poni1_min = -(value / 100.) ** 2
            self.poni1_max = (value / 100.) ** 2
        if abs(self.poni2) > (value / 100.) ** 2:
            self.poni2_min = min(low * self.poni2, hi * self.poni2)
            self.poni2_max = max(low * self.poni2, hi * self.poni2)
        else:
            self.poni2_min = -(value / 100.) ** 2
            self.poni2_max = (value / 100.) ** 2
        if abs(self.rot1) > (value / 100.) ** 2:
            self.rot1_min = min(low * self.rot1, hi * self.rot1)
            self.rot1_max = max(low * self.rot1, hi * self.rot1)
        else:
            self.rot1_min = -(value / 100.) ** 2
            self.rot1_max = (value / 100.) ** 2
        if abs(self.rot2) > (value / 100.) ** 2:
            self.rot2_min = min(low * self.rot2, hi * self.rot2)
            self.rot2_max = max(low * self.rot2, hi * self.rot2)
        else:
            self.rot2_min = -(value / 100.) ** 2
            self.rot2_max = (value / 100.) ** 2
        if abs(self.rot3) > (value / 100.) ** 2:
            self.rot3_min = min(low * self.rot3, hi * self.rot3)
            self.rot3_max = max(low * self.rot3, hi * self.rot3)
        else:
            self.rot3_min = -(value / 100.) ** 2
            self.rot3_max = (value / 100.) ** 2
        self.wavelength_min = low * self.wavelength
        self.wavelength_max = hi * self.wavelength

    def calc_2th(self, rings, wavelength=None):
        """
        :param rings: indices of the rings. starts at 0 and self.dSpacing should be long enough !!!
        :param wavelength: wavelength in meter
        """
        if wavelength is None:
            wavelength = self.wavelength
        if wavelength <= 0:
            return [numpy.finfo("float32").max] * len(rings)
        rings = numpy.ascontiguousarray(rings, dtype=numpy.int32)

        if wavelength != self.calibrant.wavelength:
            self.calibrant.setWavelength_change2th(wavelength)
        ary = self.calibrant.get_2th()
        if len(ary) < rings.max():
            # complete turn ~ 2pi ~ 7: help the optimizer to find the right way
            ary += [10.0 * (rings.max() - len(ary))] * (1 + rings.max() - len(ary))
        return numpy.array(ary, dtype=numpy.float64)[rings]

    def residu1(self, param, d1, d2, rings):
        return self.tth(d1, d2, param) - self.calc_2th(rings, self.wavelength)

    def residu1_wavelength(self, param, d1, d2, rings):
        return self.tth(d1, d2, param) - self.calc_2th(rings, param[6] * 1e-10)

    def residu2(self, param, d1, d2, rings):
        # dot product is faster ...
        # return (self.residu1(param, d1, d2, rings) ** 2).sum()
        t = self.residu1(param, d1, d2, rings)
        return numpy.dot(t, t)

    def residu2_weighted(self, param, d1, d2, rings, weight):
        # return (weight * self.residu1(param, d1, d2, rings) ** 2).sum()
        t = weight * self.residu1(param, d1, d2, rings)
        return numpy.dot(t, t)

    def residu2_wavelength(self, param, d1, d2, rings):
        # return (self.residu1_wavelength(param, d1, d2, rings) ** 2).sum()
        t = self.residu1_wavelength(param, d1, d2, rings)
        return numpy.dot(t, t)

    def residu2_wavelength_weighted(self, param, d1, d2, rings, weight):
        # return (weight * self.residu1_wavelength(param, d1, d2, rings) ** 2).sum()
        t = weight * self.residu1_wavelength(param, d1, d2, rings)
        return numpy.dot(t, t)

    def refine1(self):
        self.param = numpy.array([self._dist, self._poni1, self._poni2,
                                  self._rot1, self._rot2, self._rot3],
                                 dtype=numpy.float64)
        new_param, rc = leastsq(self.residu1, self.param,
                                args=(self.data[:, 0],
                                      self.data[:, 1],
                                      self.data[:, 2]))
        oldDeltaSq = self.chi2(tuple(self.param))
        newDeltaSq = self.chi2(tuple(new_param))
        logger.info("Least square retcode=%s %s --> %s",
                    rc, oldDeltaSq, newDeltaSq)
        if newDeltaSq < oldDeltaSq:
            i = abs(self.param - new_param).argmax()
            d = ["dist", "poni1", "poni2", "rot1", "rot2", "rot3"]
            logger.info("maxdelta on %s: %s --> %s ",
                        d[i], self.param[i], new_param[i])
            self.set_param(new_param)
            return newDeltaSq
        else:
            return oldDeltaSq

    def refine2(self, maxiter=1000000, fix=None):
        if fix is None:
            fix = ["wavelength"]
        d = ["dist", "poni1", "poni2", "rot1", "rot2", "rot3"]
        param = []
        bounds = []
        for i in d:
            param.append(getattr(self, i))
            if i in fix:
                val = getattr(self, i)
                bounds.append((val, val))
            else:
                bounds.append((getattr(self, "_%s_min" % i), getattr(self, "_%s_max" % i)))
        self.param = numpy.array(param)
        if self.data.shape[-1] == 3:
            pos0 = self.data[:, 0]
            pos1 = self.data[:, 1]
            ring = self.data[:, 2].astype(numpy.int32)
            weight = None
            new_param = fmin_slsqp(self.residu2, self.param, iter=maxiter,
                                   args=(pos0, pos1, ring),
                                   bounds=bounds,
                                   acc=1.0e-12,
                                   iprint=(logger.getEffectiveLevel() <= logging.INFO))

        elif self.data.shape[-1] == 4:
            pos0 = self.data[:, 0]
            pos1 = self.data[:, 1]
            ring = self.data[:, 2].astype(numpy.int32)
            weight = self.data[:, 3]
            new_param = fmin_slsqp(self.residu2_weighted, self.param, iter=maxiter,
                                   args=(pos0, pos1, ring, weight),
                                   bounds=bounds,
                                   acc=1.0e-12,
                                   iprint=(logger.getEffectiveLevel() <= logging.INFO))
        oldDeltaSq = self.chi2() / self.data.shape[0]
        newDeltaSq = self.chi2(new_param) / self.data.shape[0]
        logger.info("Constrained Least square %s --> %s",
                    oldDeltaSq, newDeltaSq)
        if newDeltaSq < oldDeltaSq:
            i = abs(self.param - new_param).argmax()

            logger.info("maxdelta on %s: %s --> %s ",
                        d[i], self.param[i], new_param[i])
            self.set_param(new_param)
            return newDeltaSq
        else:
            return oldDeltaSq

    def refine2_wavelength(self, maxiter=1000000, fix=None):
        if fix is None:
            fix = ["wavelength"]
        d = ["dist", "poni1", "poni2", "rot1", "rot2", "rot3", "wavelength"]

        self.param = numpy.array([self.dist, self.poni1, self.poni2,
                                  self.rot1, self.rot2, self.rot3, self.wavelength],
                                 dtype=numpy.float64)
        param = []
        bounds = []
        for i in d:
            param.append(getattr(self, i))
            if i in fix:
                val = getattr(self, i)
                bounds.append((val, val))
            else:
                bounds.append((getattr(self, "_%s_min" % i), getattr(self, "_%s_max" % i)))
        # wavelength is multiplied to 10^10 to have values in the range 0.1-10: better numerical differentiation
        bounds[-1] = (bounds[-1][0] * 1e10, bounds[-1][1] * 1e10)
        param[-1] = 1e10 * param[-1]
        self.param = numpy.array(param)
        if self.data.shape[-1] == 3:
            pos0 = self.data[:, 0]
            pos1 = self.data[:, 1]
            ring = self.data[:, 2].astype(numpy.int32)
            weight = None
            new_param = fmin_slsqp(self.residu2_wavelength,
                                   self.param, iter=maxiter,
                                   args=(pos0, pos1, ring),
                                   bounds=bounds,
                                   acc=1.0e-12,
                                   iprint=(logger.getEffectiveLevel() <= logging.INFO))

        elif self.data.shape[-1] == 4:
            pos0 = self.data[:, 0]
            pos1 = self.data[:, 1]
            ring = self.data[:, 2].astype(numpy.int32)
            weight = self.data[:, 3]
            new_param = fmin_slsqp(self.residu2_wavelength_weighted,
                                   self.param, iter=maxiter,
                                   args=(pos0, pos1, ring, weight),
                                   bounds=bounds,
                                   acc=1.0e-12,
                                   iprint=(logger.getEffectiveLevel() <= logging.INFO))
        oldDeltaSq = self.chi2_wavelength() / self.data.shape[0]
        newDeltaSq = self.chi2_wavelength(new_param) / self.data.shape[0]
        logger.info("Constrained Least square %s --> %s",
                    oldDeltaSq, newDeltaSq)
        if newDeltaSq < oldDeltaSq:
            i = abs(self.param - new_param).argmax()
            logger.info("maxdelta on %s: %s --> %s ",
                        d[i], self.param[i], new_param[i])

            self.set_param(new_param[:-1])
            self.wavelength = 1e-10 * new_param[-1]
            return newDeltaSq
        else:
            return oldDeltaSq

    def simplex(self, maxiter=1000000):
        self.param = numpy.array([self.dist, self.poni1, self.poni2,
                                  self.rot1, self.rot2, self.rot3],
                                 dtype=numpy.float64)
        new_param = fmin(self.residu2, self.param,
                         args=(self.data[:, 0],
                               self.data[:, 1],
                               self.data[:, 2]),
                         maxiter=maxiter,
                         xtol=1.0e-12)
        oldDeltaSq = self.chi2(tuple(self.param)) / self.data.shape[0]
        newDeltaSq = self.chi2(tuple(new_param)) / self.data.shape[0]
        logger.info("Simplex %s --> %s", oldDeltaSq, newDeltaSq)
        if newDeltaSq < oldDeltaSq:
            i = abs(self.param - new_param).argmax()
            d = ["dist", "poni1", "poni2", "rot1", "rot2", "rot3"]
            logger.info("maxdelta on %s : %s --> %s ",
                        d[i], self.param[i], new_param[i])
            self.set_param(new_param)
            return newDeltaSq
        else:
            return oldDeltaSq

    def anneal(self, maxiter=1000000):
        self.param = [self.dist, self.poni1, self.poni2,
                      self.rot1, self.rot2, self.rot3]
        result = anneal(self.residu2, self.param,
                        args=(self.data[:, 0],
                              self.data[:, 1],
                              self.data[:, 2]),
                        lower=[self._dist_min,
                               self._poni1_min,
                               self._poni2_min,
                               self._rot1_min,
                               self._rot2_min,
                               self._rot3_min],
                        upper=[self._dist_max,
                               self._poni1_max,
                               self._poni2_max,
                               self._rot1_max,
                               self._rot2_max,
                               self._rot3_max],
                        maxiter=maxiter)
        new_param = result[0]
        oldDeltaSq = self.chi2() / self.data.shape[0]
        newDeltaSq = self.chi2(new_param) / self.data.shape[0]
        logger.info("Anneal  %s --> %s", oldDeltaSq, newDeltaSq)
        if newDeltaSq < oldDeltaSq:
            i = abs(self.param - new_param).argmax()
            d = ["dist", "poni1", "poni2", "rot1", "rot2", "rot3"]
            logger.info("maxdelta on %s : %s --> %s ",
                        d[i], self.param[i], new_param[i])
            self.set_param(new_param)
            return newDeltaSq
        else:
            return oldDeltaSq

    def chi2(self, param=None):
        if param is None:
            param = self.param[:]
        return self.residu2(param,
                            self.data[:, 0], self.data[:, 1], self.data[:, 2])

    def chi2_wavelength(self, param=None):
        if param is None:
            param = self.param
            if len(param) == 6:
                param.append(1e10 * self.wavelength)
        return self.residu2_wavelength(param,
                                       self.data[:, 0],
                                       self.data[:, 1],
                                       self.data[:, 2])

    def curve_fit(self, with_rot=True):
        """Refine the geometry and provide confidence interval
        Use curve_fit from scipy.optimize to not only refine the geometry (unconstrained fit)

        :param with_rot: include rotation intro error measurment
        :return: std_dev, confidence
        """
        if not curve_fit:
            import scipy
            logger.error("curve_fit method needs a newer scipy: at lease scipy 0.9, you are running: %s", scipy.version.version)
        d1 = self.data[:, 0]
        d2 = self.data[:, 1]
        size = d1.size
        x = d1, d2
        rings = self.data[:, 2].astype(numpy.int32)
        f_with_rot = lambda x, *param: self.tth(x[0], x[1], numpy.concatenate((param, [self.rot3])))
        f_no_rot = lambda x, *param: self.tth(x[0], x[1], numpy.concatenate((param, [self.rot1, self.rot2, self.rot3])))
        y = self.calc_2th(rings, self.wavelength)
        param0 = numpy.array([self.dist, self.poni1, self.poni2, self.rot1, self.rot2, self.rot3], dtype=numpy.float64)
        ref = self.residu2(param0, d1, d2, rings)
        print("param0: %s %s" % (param0, ref))
        if with_rot:
            popt, pcov = curve_fit(f_with_rot, x, y, param0[:-1])
            popt = numpy.concatenate((popt, [self.rot3]))
        else:
            popt, pcov = curve_fit(f_no_rot, x, y, param0[:-3])
            popt = numpy.concatenate((popt, [self.rot1, self.rot2, self.rot3]))
        obt = self.residu2(popt, d1, d2, rings)
        print("param1: %s %s" % (popt, obt))
        print(pcov)
        err = numpy.sqrt(numpy.diag(pcov))
        print("err: %s" % err)
        if obt < ref:
            self.set_param(popt)
        error = {}
        confidence = {}
        for k, v in zip(("dist", "poni1", "poni2", "rot1", "rot2", "rot3"), err):
            error[k] = v
            confidence[k] = 1.96 * v / numpy.sqrt(size)

        print("Std dev  as sqrt of the diag of covariance:\n%s" % error)
        print("Confidence as 1.95 sigma/sqrt(n):\n%s" % confidence)
        return error, confidence

    def confidence(self, with_rot=True):
        """Confidence interval obtained from the second derivative of the error function
        next to its minimum value.

        Note the confidence interval increases with the number of points which is "surprizing"

        :param with_rot: if true include rot1 & rot2 in the parameter set.
        :return: std_dev, confidence
        """
        epsilon = 1e-5
        d1 = self.data[:, 0]
        d2 = self.data[:, 1]
        r = self.data[:, 2].astype(numpy.int32)
        param0 = numpy.array([self.dist, self.poni1, self.poni2, self.rot1, self.rot2, self.rot3], dtype=numpy.float64)
        ref = self.residu2(param0, d1, d2, r)
        print(ref)
        if with_rot:
            size = 5
        else:
            size = 3
        hessian = numpy.zeros((size, size), dtype=numpy.float64)

        delta = abs(epsilon * param0)
        delta[abs(param0) < epsilon] = epsilon
        print(delta)
        for i in range(size):
            # Diagonal terms:
            deltai = delta[i]
            param = param0.copy()
            param[i] += deltai
            value_plus = self.residu2(param, d1, d2, r)
            param = param0.copy()
            param[i] -= deltai
            value_moins = self.residu2(param, d1, d2, r)
            hessian[i, i] = (value_plus + value_moins - 2.0 * ref) / (deltai ** 2)

            for j in range(i + 1, size):
                # if i == j: continue
                deltaj = delta[j]
                param = param0.copy()
                param[i] += deltai
                param[j] += deltaj
                value_plus_plus = self.residu2(param, d1, d2, r)
                param = param0.copy()
                param[i] -= deltai
                param[j] -= deltaj
                value_moins_moins = self.residu2(param, d1, d2, r)
                param = param0.copy()
                param[i] += deltai
                param[j] -= deltaj
                value_plus_moins = self.residu2(param, d1, d2, r)
                param = param0.copy()
                param[i] -= deltai
                param[j] += deltaj
                value_moins_plus = self.residu2(param, d1, d2, r)
                hessian[j, i] = hessian[i, j] = (value_plus_plus + value_moins_moins - value_plus_moins - value_moins_plus) / (4.0 * deltai * deltaj)
        print(hessian)
        w, v = numpy.linalg.eigh(hessian)
        print("eigen val: %s" % w)
        print("eigen vec: %s" % v)
        cov = numpy.linalg.inv(hessian)
        print(cov)
        err = numpy.sqrt(numpy.diag(cov))
        print("err: %s" % err)
        error = {}
        for k, v in zip(("dist", "poni1", "poni2", "rot1", "rot2", "rot3"), err):
            error[k] = v
        confidence = {}
        for i, k in enumerate(("dist", "poni1", "poni2", "rot1", "rot2", "rot3")):
            if i < size:
                confidence[k] = numpy.sqrt(ref / hessian[i, i])
        print("std_dev as sqrt of the diag of inv hessian:\n%s" % error)
        print("Convidence as sqrt of the error function /  hessian:\n%s" % confidence)
        return error, confidence

    def roca(self):
        """
        run roca to optimise the parameter set
        """
        tmpf = tempfile.NamedTemporaryFile()
        for line in self.data:
            tmpf.write("%s %s %s %s" % (line[2], line[0], line[1], os.linesep))
        tmpf.flush()
        roca = subprocess.Popen(
            [ROCA, "debug=8", "maxdev=1", "input=" + tmpf.name,
             str(self.pixel1), str(self.pixel2),
             str(self.poni1 / self.pixel1), str(self.poni2 / self.pixel2),
             str(self.dist), str(self.rot1), str(self.rot2), str(self.rot3)],
            stdout=subprocess.PIPE)
        new_param = [self.dist, self.poni1, self.poni2,
                     self.rot1, self.rot2, self.rot3]
        for line in roca.stdout:
            word = line.split()
            if len(word) == 3:
                if word[0] == "cen1":
                    new_param[1] = float(word[1]) * self.pixel1
                if word[0] == "cen2":
                    new_param[2] = float(word[1]) * self.pixel2
                if word[0] == "dis":
                    new_param[0] = float(word[1])
                if word[0] == "rot1":
                    new_param[3] = float(word[1])
                if word[0] == "rot2":
                    new_param[4] = float(word[1])
                if word[0] == "rot3":
                    new_param[5] = float(word[1])
        print("Roca %s --> %s" % (self.chi2() / self.data.shape[0], self.chi2(new_param) / self.data.shape[0]))
        if self.chi2(tuple(new_param)) < self.chi2(tuple(self.param)):
            self.param = new_param
            self.dist, self.poni1, self.poni2, \
                self.rot1, self.rot2, self.rot3 = tuple(new_param)

        tmpf.close()

    def set_dist_max(self, value):
        if isinstance(value, float):
            self._dist_max = value
        else:
            self._dist_max = float(value)

    def get_dist_max(self):
        return self._dist_max

    dist_max = property(get_dist_max, set_dist_max)

    def set_dist_min(self, value):
        if isinstance(value, float):
            self._dist_min = value
        else:
            self._dist_min = float(value)

    def get_dist_min(self):
        return self._dist_min

    dist_min = property(get_dist_min, set_dist_min)

    def set_poni1_min(self, value):
        if isinstance(value, float):
            self._poni1_min = value
        else:
            self._poni1_min = float(value)

    def get_poni1_min(self):
        return self._poni1_min

    poni1_min = property(get_poni1_min, set_poni1_min)

    def set_poni1_max(self, value):
        if isinstance(value, float):
            self._poni1_max = value
        else:
            self._poni1_max = float(value)

    def get_poni1_max(self):
        return self._poni1_max

    poni1_max = property(get_poni1_max, set_poni1_max)

    def set_poni2_min(self, value):
        if isinstance(value, float):
            self._poni2_min = value
        else:
            self._poni2_min = float(value)

    def get_poni2_min(self):
        return self._poni2_min

    poni2_min = property(get_poni2_min, set_poni2_min)

    def set_poni2_max(self, value):
        if isinstance(value, float):
            self._poni2_max = value
        else:
            self._poni2_max = float(value)

    def get_poni2_max(self):
        return self._poni2_max

    poni2_max = property(get_poni2_max, set_poni2_max)

    def set_rot1_min(self, value):
        if isinstance(value, float):
            self._rot1_min = value
        else:
            self._rot1_min = float(value)

    def get_rot1_min(self):
        return self._rot1_min

    rot1_min = property(get_rot1_min, set_rot1_min)

    def set_rot1_max(self, value):
        if isinstance(value, float):
            self._rot1_max = value
        else:
            self._rot1_max = float(value)

    def get_rot1_max(self):
        return self._rot1_max

    rot1_max = property(get_rot1_max, set_rot1_max)

    def set_rot2_min(self, value):
        if isinstance(value, float):
            self._rot2_min = value
        else:
            self._rot2_min = float(value)

    def get_rot2_min(self):
        return self._rot2_min

    rot2_min = property(get_rot2_min, set_rot2_min)

    def set_rot2_max(self, value):
        if isinstance(value, float):
            self._rot2_max = value
        else:
            self._rot2_max = float(value)

    def get_rot2_max(self):
        return self._rot2_max

    rot2_max = property(get_rot2_max, set_rot2_max)

    def set_rot3_min(self, value):
        if isinstance(value, float):
            self._rot3_min = value
        else:
            self._rot3_min = float(value)

    def get_rot3_min(self):
        return self._rot3_min

    rot3_min = property(get_rot3_min, set_rot3_min)

    def set_rot3_max(self, value):
        if isinstance(value, float):
            self._rot3_max = value
        else:
            self._rot3_max = float(value)

    def get_rot3_max(self):
        return self._rot3_max

    rot3_max = property(get_rot3_max, set_rot3_max)

    def set_wavelength_min(self, value):
        if isinstance(value, float):
            self._wavelength_min = value
        else:
            self._wavelength_min = float(value)

    def get_wavelength_min(self):
        return self._wavelength_min

    wavelength_min = property(get_wavelength_min, set_wavelength_min)

    def set_wavelength_max(self, value):
        if isinstance(value, float):
            self._wavelength_max = value
        else:
            self._wavelength_max = float(value)

    def get_wavelength_max(self):
        return self._wavelength_max

    wavelength_max = property(get_wavelength_max, set_wavelength_max)


from functions import *
import pandas as pd
import numpy as np
import os

class SMARTS:
    """
    Clase que contiene las funciones que interactuaran con el modelo SMARTS
    """

    def __init__(self, parameters={}, station=""):
        """
        Valores con los cuales se inicializa el modelo SMARTS
        ### inputs
        + station      ----> Estacion que se analizara
        + hour_i       ----> Hora inicial para correr el modelo
        + hour_f       ----> Hora final para correr el modelo
        + lon_ i       ----> Longitud de onda inicial para el modelo
        + lon_ f       ----> Longitud de onda final para el modelo
        + igas         ----> Card 6a del Modelo SMARTS
        + delta_lon    ----> Número de longitudes de onda que se saltara el resultado del modelo
        + total_minute ----> Total minutos que correra el modelo
        """
        self.parameters = parameters
        self.delta_lon = parameters["wavelength initial"]-280+1
        self.total_minute = int(
            (parameters["hour final"]-parameters["hour initial"])*60)
        self.define_location(station)

    def define_location(self, station):
        stations = {
            "centro": {
                "Lat": 25.670,
                "Lon": -100.338,
                "Height": 0.560},
            "noreste": {
                "Lat": 25.750,
                "Lon": -100.255,
                "Height": 0.476},
            "noroeste": {
                "Lat": 25.757,
                "Lon": -100.366,
                "Height": 0.571},
            "sureste2": {
                "Lat": 25.646,
                "Lon": -100.096,
                "Height": 0.387},
            "suroeste": {
                "Lat": 25.676,
                "Lon": -100.464,
                "Height": 0.694}
        }
        self.lat = stations[station]["Lat"]
        self.lon = stations[station]["Lon"]
        self.height = stations[station]["Height"]

    def atmosphere_state(self):
        pass

    def run(self, day=1, month=1, year=2000, o3=250.51, aod=0.2, name="", path=""):
        """
        Función que ejecuta el modelo SMARTS
        ### inputs:
        + day   ----> Dia del año
        + month ----> Mes del año númerico
        + year  ----> Año del dia por analizar
        + o3    ----> ozono del dia
        + aod   ----> AOD del dia
        + name  ----> nombre del archivo de resultados
        + path  ----> direccion para guardar los archivos
        """
        file_date = open("{}/{}.txt".format(path,
                                            name),
                         "w")
        for minute in range(self.total_minute):
            # Hora y minutos a hora con decimal
            minutes = self.hour_and_minute_to_hours(minute)
            # Escribir el archivo de input para el modelo SMARTS
            self.write_data_input(day,
                                  month,
                                  year,
                                  minutes,
                                  o3,
                                  aod)
            os.system("./smarts.out")
            # Resultado de la in7tegral a partir de los resultaos del modelo SMARTS
            integral = self.read_results()
            # Escritura de los resultados
            file_date.write("{} {}\n".format(minutes,
                                             integral))
        file_date.close()

    def hour_and_minute_to_hours(self, minute=5):
        return round(self.parameters["hour initial"]+minute/60, 4)

    def read_results(self, name_result="data.ext.txt"):
        """
        Funcion que realiza la lectura de los resultados del SMARTS
        y realiza la integral del especto a cada minuto
        Describción de variables
        + wavelength ----> longitudes de onda de los resultados del modelo SMARTS
        + irra       ----> Valor del especto de los resultados del modelo SMARTS
        + integral   ----> Valor que irradiancia solar
        """
        # Lectura de los resultados del modelo SMARTS
        wavelength, irradiance = np.loadtxt(
            name_result, skiprows=self.delta_lon, unpack=True)
        integral = irradiance[0]
        # Calculo de la irradiancia solar a partir de los resultados del modelo SMARTS
        size = np.size(irradiance)
        for i in range(1, size):
            integral += irradiance[i]*(wavelength[i]-wavelength[i-1])
        # Eliminación de los archivos
        os.system("rm data*")
        # Formato de la integral
        integral = str(round(integral))
        return integral

    def write_data_input(self, day=1, month=1, year=2000, hour=8, ozono=250.51, aod=0.5):
        """
        Formato del input del modelo SMARTS
        ### inputs:
        + day   -> Dia del año
        + month -> Mes del año númerico
        + year  -> Año del dia por analizar
        + hour  -> Hora del calculo de la irradiancia
        + ozono -> ozono del dia
        + aod   -> AOD del dia
        + igas  -> Card 6a
        """
        file = open("data.inp.txt", "w")
        file.write(" 'AOD={} '\n".format(aod))
        # Card 2
        file.write(" 2\n")
        # Card 2a
        # lat,altit,height
        file.write(" {:.3f} {} {}\n".format(self.lat,
                                            self.height,
                                            0))
        # Card 3
        # IATMOS
        file.write(" 1\n")
        # Card 3a
        file.write(" 'USSA'\n")
        # Card 4
        # H2O
        file.write(" 1\n")
        # Card 4a
        file.write(" 0\n")
        # Card 5
        # Ozono
        file.write(" {} {:.4f}\n".format(1,
                                         ozono/1000))
        # Card 6
        file.write(" 0\n")
        # Card 6a
        # Pristine ----> 1
        # Moderate ----> 3
        file.write(" {}\n".format(self.parameters["igas"]))
        # Card 7
        # Co2
        file.write(" 390\n")
        # Card 7a
        file.write(" 0\n")
        # Card 8
        file.write(" 'S&F_URBAN'\n")
        # Card 9
        file.write(" 5\n")
        # Card 9a
        file.write(" {} {}\n".format(aod,
                                     2))
        # Card 10
        file.write(" 18\n")
        # Card 10b
        file.write(" 1\n")
        # Card 10d
        # IALBDG, TILT,WAZIM
        file.write(" {} {} {}\n".format(51,
                                        37.,
                                        180.))
        # Card 11---
        # Wave min, Wave max, suncor, solar cons
        file.write(" {} {} {} {}\n".format(self.parameters["wavelength initial"],
                                           self.parameters["wavelength final"],
                                           1,
                                           1366.1))
        # ------Card 12---
        file.write(" 2\n")
        # Card 12a
        # Wave min, Wave max, inter wave
        file.write(" {} {} {}\n".format(self.parameters["wavelength initial"],
                                        self.parameters["wavelength final"],
                                        1))
        # Card 12b
        file.write(" 1\n")
        # Card 12c
        file.write(" 4\n")
        # Card 13
        file.write(" 1\n")
        # Card 13a
        #  slope, apert, limit
        file.write(" 0 2.9 0\n")
        # Card 14
        file.write(" 0\n")
        # Card 15
        file.write(" 0\n")
        # Card 16
        file.write(" 1\n")
        # Card 17
        file.write(" 3\n")
        # Card 17a
        # Year, month, day, hour, latit, longit, zone
        file.write(" {} {} {} {} {} {} {}\n".format(year,
                                                    month,
                                                    day,
                                                    hour,
                                                    self.lat,
                                                    self.lon,
                                                    -6))
        file.close()

class SMARTS_DR(SMARTS):
    """
    Clase heredada de SMARTS, uso especifico para la versión del modelo
    que calcula el AOD a partir de las mediciones y una RD dada
    """

    def __init__(self, parameters={}, station=""):
        """
        Valores con los cuales se inicializa el modelo SMARTS
        ### inputs
        + hour_i       ----> Hora inicial para correr el modelo
        + hour_f       ----> Hora final para correr el modelo
        + lon_ i       ----> Longitud de onda inicial para el modelo
        + lon_ f       ----> Longitud de onda final para el modelo
        + delta_lon    ----> Número de longitudes de onda que se saltara
                           el resultado del modelo
        + total_minute ----> Total minutos que correra el modelo
        + RD_lim       ----> RD al cual se quiere llegar
        + RD_delta     ----> Mas menos del RD
        """
        SMARTS.__init__(self,
                        parameters=parameters,
                        station=station)
        self.parameters = parameters
        self.station = station
        self.delta_hour = int(
            self.parameters["hour final"]-self.parameters["hour initial"])
        self.select_path_name_for_results()

    def select_path_name_for_results(self):
        names = {
            1: "pristine",
            3: "moderate"
        }
        name = names[self.parameters["igas"]]
        self.parameters["path results"] = self.parameters["path results"]+name+"/"
        self.parameters["file results"] = self.parameters["file results"]+name

    def run_search(self):
        # Direccion donde se encuentran los datos de cada estacion
        station_path = "{}{}/".format(self.parameters["path stations"],
                                      self.station)
        path_results = "{}{}".format(station_path,
                                     self.parameters["path results"])
        # Creacion de la carpeta resultados si es que no existe
        mkdir(self.parameters["path results"],
              path=station_path)
        # Archivo de resultados donde se guardara el AOD y la RD de cada dia
        AOD_file = open("{}{}.csv".format(station_path,
                                          self.parameters["file results"]),
                        "w")
        AOD_file.write("Date,year,month,day,ozone,AOD,RD\n")
        # Lectura de los parametros de entrada de cada dia
        data = pd.read_csv("{}{}".format(station_path,
                                         self.parameters["file data"]))
        for index in data.index:
            print("\n\tCalculando el dia {}-{}-{}".format(data["Year"][index],
                                                          str(data["Month"][index]).zfill(
                2),
                str(data["Day"][index]).zfill(2)))
            print("\tAOD_i\tAOD\tAOD_f\tRD")
            self.initialize_aod(self.parameters["AOD inicial"],
                                self.parameters["AOD limite"])
            # Lectura de las mediciones
            hour, measurements = np.loadtxt("{}/Mediciones/{}.txt".format(station_path,
                                                                          data["Date"][index]),
                                            skiprows=self.parameters["hour initial"],
                                            unpack=True)
            # Valor maximo de medicion, esta se usara para el calculo de la RD
            data_max = np.max(measurements[0:self.delta_hour+1])
            var = False
            # Primer valor de AOD, se puede cambiar por cualquier otro siempre y cuando este entre aod_i y aod_lim
            aod = self.obtain_aod(self.aod_i,
                                  self.aod_lim)
            # Control de iteracciones
            iter = 0
            while not(var) and iter < 10:
                # Ejecucion del modelo SMARTS con los parametros de cada dia
                self.run(day=data["Day"][index],
                         month=data["Month"][index],
                         year=data["Year"][index],
                         o3=data["Ozone"][index],
                         aod=aod,
                         name=data["Date"][index],
                         path=path_results)
                # Valor maximo de los resultados del modelo SMARTS
                data_model = self.obtain_maximum_from_results(data["Date"][index],
                                                              path=path_results)
                # Calculo del RD y verificación si se cumple la condicion
                var, RD = self.RD_decision(data_model,
                                           data_max)
                print("\t{}\t{}\t{}\t{}".format(self.aod_i,
                                                aod,
                                                self.aod_lim,
                                                RD,))
                if var:
                    # Si se cumple entonces se escribiran los parametros, el AOD y la RD en el archivo de resultados
                    self.write_results(AOD_file,
                                       data["Date"][index],
                                       data["Year"][index],
                                       data["Month"][index],
                                       data["Day"][index],
                                       data["Ozone"][index],
                                       aod,
                                       RD)
                else:
                    # Se calculara un nuevo AOD siguiendo el algoritmo de busqueda binaria
                    aod = self.aod_binary_search(aod, RD)
                    # Si se queda en un intervalo muy pequeño se verificara que cumpla la condicion si lo hace entonces escribira en el archivo el resultado, esto llega a pasar  si se pone un delta_RD menor a 1
                    if self.aod_lim == aod and abs(RD-self.RD_lim) < 2:
                        self.write_results(AOD_file,
                                           data["Date"][index],
                                           data["Year"][index],
                                           data["Month"][index],
                                           data["Day"][index],
                                           data["Ozone"][index],
                                           aod,
                                           RD)
                        var = True
                    iter += 1
        AOD_file.close()

    def initialize_aod(self, aod_i, aod_lim):
        """
        Funcion que inicializa el limite inferior y superior del AOD
        """
        self.aod_i = aod_i
        self.aod_lim = aod_lim

    def obtain_aod(self, aod_i, aod_f):
        return round((aod_i+aod_f)/2, 3)

    def obtain_maximum_from_results(self, name, path=""):
        data_model = np.loadtxt("{}{}.txt".format(path,
                                                  name),
                                usecols=1)
        pos = (np.where(np.max(data_model) == data_model)[0])[0]
        data_model = np.mean(data_model[pos-30:pos+31])
        return data_model

    def RD_decision(self, model, measurement):
        """
        Funcion que calcula la RD entre el modelo y la medicion
        """
        var = False
        RD = round(100*(model-measurement)/measurement, 3)
        if self.RD_search(RD):
            var = True
        return var, RD

    def aod_binary_search(self, aod, RD):
        """
        Función que calcula el AOD que se introducira en el modelo SMARTS
        este emplea una busqueda binaria para que sea más eficiente
        """
        if self.RD_search(RD):
            self.aod_i = aod
        elif RD > self.parameters["RD limite"]+self.parameters["RD delta"]:
            self.aod_i = aod
        else:
            self.aod_lim = aod
        aod = self.obtain_aod(self.aod_lim,
                              self.aod_i)
        return aod

    def RD_search(self, RD):
        lim_i = self.parameters["RD limite"]-self.parameters["RD delta"]
        lim_f = self.parameters["RD limite"]+self.parameters["RD delta"]
        return lim_i < RD < lim_f

    def write_results(self, file, date, year, month, day, o3, aod, RD):
        file.write("{},{},{},{},{},{:.3f},{:.2f}\n".format(date,
                                                           year,
                                                           month,
                                                           day,
                                                           o3,
                                                           aod,
                                                           RD))

class SMARTS_DR_SSAAER_CUSTOM(SMARTS_DR):
    def __init__(self, parameters={}, station=""):
        SMARTS_DR.__init__(self,
                           parameters=parameters,
                           station=station)
        self.parameters = parameters

    def write_data_input(self, day=1, month=1, year=2000, hour=8, ozono=250.51, aod=0.5):
        """
        Formato del input del modelo SMARTS
        day   ----> Dia del año
        month ----> Mes del año númerico
        year  ----> Año del dia por analizar
        hour  ----> Hora del calculo de la irradiancia
        ozono ----> ozono del dia
        aod   ----> AOD del dia
        igas  ----> Card 6a
        """
        file = open("data.inp.txt", "w")
        file.write(" 'AOD={} '\n".format(aod))
        # Card 2
        file.write(" 2\n")
        # Card 2a
        # lat,altit,height
        file.write(" {:.3f} {} {}\n".format(self.lat,
                                            self.height,
                                            0))
        # Card 3
        # IATMOS
        file.write(" 1\n")
        # Card 3a
        file.write(" 'USSA'\n")
        # Card 4
        # H2O
        file.write(" 1\n")
        # Card 4a
        file.write(" 0\n")
        # Card 5
        # Ozono
        file.write(" {} {:.4f}\n".format(1,
                                         ozono/1000))
        # Card 6
        file.write(" 0\n")
        # Card 6a
        # Pristine ----> 1
        # Moderate ----> 3
        file.write(" {}\n".format(self.parameters["igas"]))
        # Card 7
        # Co2
        file.write(" 390\n")
        # Card 7a
        file.write(" 0\n")
        # Card 8
        file.write(" 'USER'\n")
        # Card 8a
        # SSAAER Palancar
        # Asymmetry Promedio de 550 nm y humedad ente 50-70%
        file.write(" {} {} {} {}\n".format(1, 1, 0.8, 0.68))
        # Card 9
        file.write(" 5\n")
        # Card 9a
        file.write(" {} {}\n".format(aod,
                                     2))
        # Card 10
        file.write(" 18\n")
        # Card 10b
        file.write(" 1\n")
        # Card 10d
        # IALBDG, TILT,WAZIM
        file.write(" {} {} {}\n".format(51,
                                        37.,
                                        180.))
        # Card 11---
        # Wave min, Wave max, suncor, solar cons
        file.write(" {} {} {} {}\n".format(self.parameters["wavelength initial"],
                                           self.parameters["wavelength final"],
                                           1,
                                           1366.1))
        # ------Card 12---
        file.write(" 2\n")
        # Card 12a
        # Wave min, Wave max, inter wave
        file.write(" {} {} {}\n".format(self.parameters["wavelength initial"],
                                        self.parameters["wavelength final"],
                                        1))
        # Card 12b
        file.write(" 1\n")
        # Card 12c
        file.write(" 4\n")
        # Card 13
        file.write(" 1\n")
        # Card 13a
        #  slope, apert, limit
        file.write(" 0 2.9 0\n")
        # Card 14
        file.write(" 0\n")
        # Card 15
        file.write(" 0\n")
        # Card 16
        file.write(" 1\n")
        # Card 17
        file.write(" 3\n")
        # Card 17a
        # Year, month, day, hour, latit, longit, zone
        file.write(" {} {} {} {} {} {} {}\n".format(year,
                                                    month,
                                                    day,
                                                    hour,
                                                    self.lat,
                                                    self.lon,
                                                    -6))
        file.close()


import numpy as np
from parameter_loader import load_parameters
from math import pi

class FullModel:
    """
    A class that defines the full model for actomyosin contraction in soft pillar rings that accounts for both myosin
    filament binding and density changes

    Attributes:
        parameter_file (str):
            path to a file that contains all necessary parameters for the model (see provided examples).

    Methods:
        k_off_fil(total_force):
            calculates the load dependent steady state off-rate of a myosin filament.

        rhs(t, y):
            calculates the right hand side of the set of differential equations that describe the model.

        velocity(t, force, N):
            calculates the deflection velocity of the tip of the pillar.
    """

    def __init__(self, parameter_file, pillar_stiffness):
        """
        Sets all the necessary parameters for the FullModel object.

        Parameters:
            parameter_file (str):
                path to a file that contains all necessary parameters for the model (see provided examples).
            pillar_stiffness (float):
                stiffness of the pillars in the pillar ring in pN/um.
        """
        self.x_catch, self.x_slip, self.k_off0_catch, self.k_off0_slip, self.k_on, self.k_on_fil, self.a_per_kBT, \
            self.Nh, self.Nmax, self.h_eta, self.xi_rho_a2, self.rho_max_per_rho, \
            self.R0 = load_parameters('full model', parameter_file)
        self.k_p = pillar_stiffness

        self.parameter_dict = {"x_catch": self.x_catch, "x_slip": self.x_slip, "k_off0_catch": self.k_off0_catch,
                               "k_off0_slip": self.k_off0_slip, "k_on": self.k_on, "k_on_fil": self.k_on_fil,
                               "a_per_kBT": self.a_per_kBT, "Nh": self.Nh, "Nmax": self.Nmax, "h_eta": self.h_eta,
                               "xi_rho_a2": self.xi_rho_a2, "rho_max_per_rho": self.rho_max_per_rho, "R0": self.R0,
                               "k_p": self.k_p}

        self.A0 = pi * self.R0**2
        self.tau = 6. / 5. * pi * self.h_eta / self.k_p

    def __k_off(self, force):
        """Calculates the load dependent off-rate of an individual myosin head.

        Parameters:
            force (float):
                the average load that is applied to an individual myosin head.

        Returns:
            float: the average off-rate of the head.
        """
        return self.k_off0_catch * np.exp(-self.a_per_kBT * force * self.x_catch) + \
            self.k_off0_slip * np.exp(self.a_per_kBT * force * self.x_slip)

    def __calc_prob_dist(self, total_force):
        """Calculates the load dependent steady state probability distribution of the number of bound heads per
           myosin filament

        Parameters:
            total_force (float):
                the total load that is applied to the myosin filament.

        Returns:
            list(float): list of probabilities that n heads are bound per filament, where n is given by the list index.
        """
        pns = []
        for n in range(0, self.Nh + 1):
            nom = 1
            for i in range(0, n):
                nom = nom * ((self.Nh - i) * self.k_on) / ((i + 1) * self.__k_off(total_force / (i + 1)))

            denom = 1
            for k in range(1, self.Nh + 1):
                prod = 1
                for j in range(0, k):
                    prod = prod * ((self.Nh - j) * self.k_on) / ((j + 1) * self.__k_off(total_force / (j + 1)))
                denom = denom + prod

            pns.append(nom / denom)

        return pns

    def k_off_fil(self, total_force):
        """Calculates the load dependent steady state off-rate of a myosin filament.

        Parameters:
            total_force (float):
                the total load that is applied to the myosin filament.

        Returns:
            float: the off-rate of the filament.
        """
        T_off_av = 0
        pns = self.__calc_prob_dist(total_force)
        for NB_init in range(1, self.Nh + 1):
            T_off = 0
            for NB in range(1, NB_init + 1):
                s = 0
                for j in range(NB, self.Nh + 1):
                    s = s + pns[j]

                T_off = T_off + 1 / (NB * self.__k_off(total_force / NB) * pns[NB]) * s

            T_off_av = T_off_av + pns[NB_init] * T_off
        return 1 / T_off_av

    def rhs(self, t, y):
        """Calculates the right hand side of the set of differential equations that describe the model.

        Parameters:
            t (float):
                the time point.
            y (list(float)):
                a list with elements y[0] = force on the pillar at time t and y[1] = number of bound filaments at time t

        Returns:
            list(float): the temporal derivative of the input y
        """
        force = y[0]
        N = y[1]

        area = pi * (self.R0 - force / self.k_p) ** 2
        density_factor = -self.A0 / area * (self.A0 / area - self.rho_max_per_rho)

        force_prime = -force / self.tau + self.xi_rho_a2 * N * density_factor / self.tau
        N_prime = self.k_on_fil * (self.Nmax - N) - self.k_off_fil(force) * N

        return [force_prime, N_prime]

    def velocity(self, t, force, N):
        """Calculates the deflection velocity of the tip of the pillar.

        Parameters:
            t (float):
                the time point.
            force (float):
                force on the pillar at time t
            N:
                number of bound filaments at time t

        Returns:
            float: the deflection velocity of the pillar tip at time t
        """
        area = pi * (self.R0 - force / self.k_p) ** 2
        density_factor = -self.A0 / area * (self.A0 / area - self.rho_max_per_rho)

        return (-force / self.tau + self.xi_rho_a2 * N * density_factor / self.tau) / self.k_p

    def get_parameter(self, parameter_name):
        """Get all model parameters

        Parameters:
            parameter_name (str):
                parameter name.

        Returns:
            float/int: the value of the specified parameter.
        """

        return self.parameter_dict[parameter_name]

class DensityModel:
    """
    A class that defines the purley density dependent model for actomyosin contraction in soft pillar rings.
    ...

    Attributes:
        parameter_file (str):
            path to a file that contains all necessary parameters for the model (see provided examples).

    Methods:
        k_off_fil(total_force):
            calculates the load dependent steady state off-rate of a myosin filament.

        rhs(t, y):
            calculates the right hand side of the set of differential equations that describe the model.

        velocity(t, force, N):
            calculates the deflection velocity of the tip of the pillar.
    """

    def __init__(self, parameter_file, pillar_stiffness):
        """
        Sets all the necessary parameters for the DensityModel object.

        Parameters:
            parameter_file (str):
                path to a file that contains all necessary parameters for the model (see provided examples).
            pillar_stiffness (float):
                stiffness of the pillars in the pillar ring in pN/um.
        """
        self.h_eta, self.xi_N_rho_a2, self.rho_max_per_rho, self.R0 = load_parameters('density model', parameter_file)
        self.k_p = pillar_stiffness

        self.parameter_dict = {"h_eta": self.h_eta, "xi_N_rho_a2": self.xi_N_rho_a2,
                               "rho_max_per_rho": self.rho_max_per_rho, "R0": self.R0, "k_p": self.k_p}

        self.A0 = pi * self.R0 ** 2
        self.tau = 6. / 5. * pi * self.h_eta / self.k_p

    def rhs(self, t, y):
        """Calculates the right hand side of the set of differential equations that describe the model.

        Parameters:
            t (float):
                the time point.
            y (list(float)):
                a list with a single element y[0] = force on the pillar at time t

        Returns:
        list(float): the temporal derivative of the input y
        """
        force = y[0]

        area = pi * (self.R0 - force / self.k_p) ** 2
        density_factor = -self.A0 / area * (self.A0 / area - self.rho_max_per_rho)

        force_prime = -force/self.tau + self.xi_N_rho_a2 * density_factor / self.tau

        return [force_prime]

    def velocity(self, t, force):
        """Calculates the deflection velocity of the tip of the pillar.

        Parameters:
            t (float):
                the time point.
            force (float):
                force on the pillar at time t

        Returns:
        float: the deflection velocity of the pillar tip at time t
        """
        area = pi * (self.R0 - force / self.k_p) ** 2
        density_factor = -self.A0 / area * (self.A0 / area - self.rho_max_per_rho)

        return (-force/self.tau + self.xi_N_rho_a2 * density_factor / self.tau)/self.k_p

    def get_parameter(self, parameter_name):
        """Get all model parameters

        Parameters:
            parameter_name (str):
                parameter name.

        Returns:
            float/int: the value of the specified parameter.
        """

        return self.parameter_dict[parameter_name]