# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICNSE.rst

# This module includes files automatically generated from ply (these end in
# _lextab.py and _parsetab.py). To generate these files, remove them from this
# folder, then build astropy and run the tests in-place:
#
#   python setup.py build_ext --inplace
#   pytest astropy/units
#
# You can then commit the changes to the re-generated _lextab.py and
# _parsetab.py files.

"""
Handles units in `Office of Guest Investigator Programs (OGIP)
FITS files
<https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/>`__.
"""

import copy
import keyword
import math
import os
import warnings
from fractions import Fraction

from astropy.utils import parsing

from . import core, generic, utils

class OGIP(generic.Generic):
    """
    Support the units in `Office of Guest Investigator Programs (OGIP)
    FITS files
    <https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/>`__.
    """

    _tokens = (
        'DIVISION',
        'OPEN_PAREN',
        'CLOSE_PAREN',
        'WHITESPACE',
        'STARSTAR',
        'STAR',
        'SIGN',
        'UFLOAT',
        'LIT10',
        'UINT',
        'UNKNOWN',
        'UNIT'
    )

    @staticmethod
    def _generate_unit_names():

        from astropy import units as u
        names = {}
        deprecated_names = set()

        bases = [
            'A', 'C', 'cd', 'eV', 'F', 'g', 'H', 'Hz', 'J',
            'Jy', 'K', 'lm', 'lx', 'm', 'mol', 'N', 'ohm', 'Pa',
            'pc', 'rad', 's', 'S', 'sr', 'T', 'V', 'W', 'Wb'
        ]
        deprecated_bases = []
        prefixes = [
            'y', 'z', 'a', 'f', 'p', 'n', 'u', 'm', 'c', 'd',
            '', 'da', 'h', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'
        ]

        for base in bases + deprecated_bases:
            for prefix in prefixes:
                key = prefix + base
                if keyword.iskeyword(key):
                    continue
                names[key] = getattr(u, key)
        for base in deprecated_bases:
            for prefix in prefixes:
                deprecated_names.add(prefix + base)

        simple_units = [
            'angstrom', 'arcmin', 'arcsec', 'AU', 'barn', 'bin',
            'byte', 'chan', 'count', 'day', 'deg', 'erg', 'G',
            'h', 'lyr', 'mag', 'min', 'photon', 'pixel',
            'voxel', 'yr'
        ]
        for unit in simple_units:
            names[unit] = getattr(u, unit)

        # Create a separate, disconnected unit for the special case of
        # Crab and mCrab, since OGIP doesn't define their quantities.
        Crab = u.def_unit(['Crab'], prefixes=False, doc='Crab (X-ray flux)')
        mCrab = u.Unit(10 ** -3 * Crab)
        names['Crab'] = Crab
        names['mCrab'] = mCrab

        deprecated_units = ['Crab', 'mCrab']
        for unit in deprecated_units:
            deprecated_names.add(unit)

        # Define the function names, so we can parse them, even though
        # we can't use any of them (other than sqrt) meaningfully for
        # now.
        functions = [
            'log', 'ln', 'exp', 'sqrt', 'sin', 'cos', 'tan', 'asin',
            'acos', 'atan', 'sinh', 'cosh', 'tanh'
        ]
        for name in functions:
            names[name] = name

        return names, deprecated_names, functions

    @classmethod
    def _make_lexer(cls):
        tokens = cls._tokens

        t_DIVISION = r'/'
        t_OPEN_PAREN = r'\('
        t_CLOSE_PAREN = r'\)'
        t_WHITESPACE = '[ \t]+'
        t_STARSTAR = r'\*\*'
        t_STAR = r'\*'

        # NOTE THE ORDERING OF THESE RULES IS IMPORTANT!!
        # Regular expression rules for simple tokens
        def t_UFLOAT(t):
            r'(((\d+\.?\d*)|(\.\d+))([eE][+-]?\d+))|(((\d+\.\d*)|(\.\d+))([eE][+-]?\d+)?)'
            t.value = float(t.value)
            return t

        def t_UINT(t):
            r'\d+'
            t.value = int(t.value)
            return t

        def t_SIGN(t):
            r'[+-](?=\d)'
            t.value = float(t.value + '1')
            return t

        def t_X(t):  # multiplication for factor in front of unit
            r'[x×]'
            return t

        def t_LIT10(t):
            r'10'
            return 10

        def t_UNKNOWN(t):
            r'[Uu][Nn][Kk][Nn][Oo][Ww][Nn]'
            return None

        def t_UNIT(t):
            r'[a-zA-Z][a-zA-Z_]*'
            t.value = cls._get_unit(t)
            return t

        # Don't ignore whitespace
        t_ignore = ''

        # Error handling rule
        def t_error(t):
            raise ValueError(
                f"Invalid character at col {t.lexpos}")

        return parsing.lex(lextab='ogip_lextab', package='astropy/units')

    @classmethod
    def _make_parser(cls):
        """
        The grammar here is based on the description in the
        `Specification of Physical Units within OGIP FITS files
        <https://heasarc.gsfc.nasa.gov/docs/heasarc/ofwg/docs/general/ogip_93_001/>`__,
        which is not terribly precise.  The exact grammar is here is
        based on the YACC grammar in the `unity library
        <https://bitbucket.org/nxg/unity/>`_.
        """

        tokens = cls._tokens

        def p_main(p):
            '''
            main : UNKNOWN
                 | complete_expression
                 | scale_factor complete_expression
                 | scale_factor WHITESPACE complete_expression
            '''
            if len(p) == 4:
                p[0] = p[1] * p[3]
            elif len(p) == 3:
                p[0] = p[1] * p[2]
            else:
                p[0] = p[1]

        def p_complete_expression(p):
            '''
            complete_expression : product_of_units
            '''
            p[0] = p[1]

        def p_product_of_units(p):
            '''
            product_of_units : unit_expression
                             | division unit_expression
                             | product_of_units product unit_expression
                             | product_of_units division unit_expression
            '''
            if len(p) == 4:
                if p[2] == 'DIVISION':
                    p[0] = p[1] / p[3]
                else:
                    p[0] = p[1] * p[3]
            elif len(p) == 3:
                p[0] = p[2] ** -1
            else:
                p[0] = p[1]

        def p_unit_expression(p):
            '''
            unit_expression : unit
                            | UNIT OPEN_PAREN complete_expression CLOSE_PAREN
                            | OPEN_PAREN complete_expression CLOSE_PAREN
                            | UNIT OPEN_PAREN complete_expression CLOSE_PAREN power numeric_power
                            | OPEN_PAREN complete_expression CLOSE_PAREN power numeric_power
            '''

            # If we run p[1] in cls._functions, it will try and parse each
            # item in the list into a unit, which is slow. Since we know that
            # all the items in the list are strings, we can simply convert
            # p[1] to a string instead.
            p1_str = str(p[1])

            if p1_str in cls._functions and p1_str != 'sqrt':
                raise ValueError(
                    "The function '{}' is valid in OGIP, but not understood "
                    "by astropy.units.".format(
                        p[1]))

            if len(p) == 7:
                if p1_str == 'sqrt':
                    p[0] = p[1] * p[3] ** (0.5 * p[6])
                else:
                    p[0] = p[1] * p[3] ** p[6]
            elif len(p) == 6:
                p[0] = p[2] ** p[5]
            elif len(p) == 5:
                if p1_str == 'sqrt':
                    p[0] = p[3] ** 0.5
                else:
                    p[0] = p[1] * p[3]
            elif len(p) == 4:
                p[0] = p[2]
            else:
                p[0] = p[1]

        def p_scale_factor(p):
            '''
            scale_factor : LIT10 power numeric_power
                         | LIT10
                         | signed_float
                         | signed_float power numeric_power
                         | signed_int power numeric_power
            '''
            if len(p) == 4:
                p[0] = 10 ** p[3]
            else:
                p[0] = p[1]
            # Can't use np.log10 here, because p[0] may be a Python long.
            if math.log10(p[0]) % 1.0 != 0.0:
                from astropy.units.core import UnitsWarning
                warnings.warn(
                    "'{}' scale should be a power of 10 in "
                    "OGIP format".format(p[0]), UnitsWarning)

        def p_division(p):
            '''
            division : DIVISION
                     | WHITESPACE DIVISION
                     | WHITESPACE DIVISION WHITESPACE
                     | DIVISION WHITESPACE
            '''
            p[0] = 'DIVISION'

        def p_product(p):
            '''
            product : WHITESPACE
                    | STAR
                    | WHITESPACE STAR
                    | WHITESPACE STAR WHITESPACE
                    | STAR WHITESPACE
            '''
            p[0] = 'PRODUCT'

        def p_power(p):
            '''
            power : STARSTAR
            '''
            p[0] = 'POWER'

        def p_unit(p):
            '''
            unit : UNIT
                 | UNIT power numeric_power
            '''
            if len(p) == 4:
                p[0] = p[1] ** p[3]
            else:
                p[0] = p[1]

        def p_numeric_power(p):
            '''
            numeric_power : UINT
                          | signed_float
                          | OPEN_PAREN signed_int CLOSE_PAREN
                          | OPEN_PAREN signed_float CLOSE_PAREN
                          | OPEN_PAREN signed_float division UINT CLOSE_PAREN
            '''
            if len(p) == 6:
                p[0] = Fraction(int(p[2]), int(p[4]))
            elif len(p) == 4:
                p[0] = p[2]
            else:
                p[0] = p[1]

        def p_sign(p):
            '''
            sign : SIGN
                 |
            '''
            if len(p) == 2:
                p[0] = p[1]
            else:
                p[0] = 1.0

        def p_signed_int(p):
            '''
            signed_int : SIGN UINT
            '''
            p[0] = p[1] * p[2]

        def p_signed_float(p):
            '''
            signed_float : sign UINT
                         | sign UFLOAT
            '''
            p[0] = p[1] * p[2]

        def p_error(p):
            raise ValueError()

        return parsing.yacc(tabmodule='ogip_parsetab', package='astropy/units')

    @classmethod
    def _validate_unit(cls, unit, detailed_exception=True):
        if unit not in cls._units:
            if detailed_exception:
                raise ValueError(
                    "Unit '{}' not supported by the OGIP "
                    "standard. {}".format(
                        unit, utils.did_you_mean_units(
                            unit, cls._units, cls._deprecated_units,
                            cls._to_decomposed_alternative)))
            else:
                raise ValueError()

        if unit in cls._deprecated_units:
            utils.unit_deprecation_warning(
                unit, cls._units[unit], 'OGIP',
                cls._to_decomposed_alternative)

    @classmethod
    def _parse_unit(cls, unit, detailed_exception=True):
        cls._validate_unit(unit, detailed_exception=detailed_exception)
        return cls._units[unit]

    @classmethod
    def parse(cls, s, debug=False):
        s = s.strip()
        try:
            # This is a short circuit for the case where the string is
            # just a single unit name
            return cls._parse_unit(s, detailed_exception=False)
        except ValueError:
            try:
                return core.Unit(
                    cls._parser.parse(s, lexer=cls._lexer, debug=debug))
            except ValueError as e:
                if str(e):
                    raise
                else:
                    raise ValueError(
                        f"Syntax error parsing unit '{s}'")

    @classmethod
    def _get_unit_name(cls, unit):
        name = unit.get_format_name('ogip')
        cls._validate_unit(name)
        return name

    @classmethod
    def _format_unit_list(cls, units):
        out = []
        units.sort(key=lambda x: cls._get_unit_name(x[0]).lower())

        for base, power in units:
            if power == 1:
                out.append(cls._get_unit_name(base))
            else:
                power = utils.format_power(power)
                if '/' in power:
                    out.append(f'{cls._get_unit_name(base)}**({power})')
                else:
                    out.append(f'{cls._get_unit_name(base)}**{power}')
        return ' '.join(out)

    @classmethod
    def to_string(cls, unit):
        # Remove units that aren't known to the format
        unit = utils.decompose_to_known_units(unit, cls._get_unit_name)

        if isinstance(unit, core.CompositeUnit):
            # Can't use np.log10 here, because p[0] may be a Python long.
            if math.log10(unit.scale) % 1.0 != 0.0:
                warnings.warn(
                    f"'{unit.scale}' scale should be a power of 10 in OGIP format",
                    core.UnitsWarning)

        return generic._to_string(cls, unit)

    @classmethod
    def _to_decomposed_alternative(cls, unit):
        # Remove units that aren't known to the format
        unit = utils.decompose_to_known_units(unit, cls._get_unit_name)

        if isinstance(unit, core.CompositeUnit):
            # Can't use np.log10 here, because p[0] may be a Python long.
            if math.log10(unit.scale) % 1.0 != 0.0:
                scale = unit.scale
                unit = copy.copy(unit)
                unit._scale = 1.0
                return '{} (with data multiplied by {})'.format(
                    generic._to_string(cls, unit), scale)

        return generic._to_string(unit)


"""
Example models
==============

Please note that none of the models are tuned much for performance, they are included for demonstration purposes only.
"""

from abc import ABC, abstractmethod

import numpy as np

class CompositeTrajectory(list):
  """
  Class used by the EvolutionOperator implementation to store multiple individual
  model trajectories. Derives from list and is meant to just be a collection of
  trajectory instances.
  """
  pass

class EvolutionOperator(ABC):
  """
  Abstract base class for generic evolution operators.

  This class represents evolution operators that only implement forward
  propagation in time and do not support any form of linearization.
  """

  @property
  @abstractmethod
  def ndim(self):
    """
    The number of dimensions of the model state
    """
    pass

  @abstractmethod
  def __call__(self, x, dt, trajectoryout=None):
    """
    Propagates model from the current state to a new state at `t+dt`.

    The current model state `x` is assumed to be at the time `t` and is
    propagated to the time `t+dt`. The state is to be updated in-place, replacing
    the contents of `x` with the updated state vector.
    For linear operators or nonlinear operators that do not implement linearization, the
    method will usually return ``None``. For linearized operators (see
    :class:`LinearizedEvolutionOperator`), the method must return trajectory data for
    the tangent linear and adjoint operators. The trajectory data is specific to the
    operator implementation.

    Args:
      x  (NxM array) : One or more state vectors stored in columns. Outside of ensemble
                       filtering methods, M is likely to be 1 (only a single state vector
                       to be propagated).
      dt (float)     : Propagation time step.

    Returns:
      Model trajectory at `t+dt` or ``None``.

    Subclasses must implement this method at least for M = 1.
    The default implementation handles the case of M > 1 by calling this method for each
    column of `x` individually and can therefore be used form the subclass as a convenience.
    The individual (sub)model trajectories are then joined together and returned as a
    CompositeTrajectory instance.
    """

    N, M = x.shape

    # Fallback implementation of ensemble state propagation via simple loop.
    # Subclasses may (should) do better than this
    if M > 1:

      T = CompositeTrajectory() if isinstance(self, LinearizedEvolutionOperator) else None
      for m in range(M):
        trj = self(x[:,m], dt)
        if T: T.append(trj)

      return T
    else:
      raise Exception("EvolutionOperator.__call__() not implemented for M=1")

class LinearizedEvolutionOperator(EvolutionOperator):
  """
  Abstract base class for evolution operators that implement both the tangent linear
  and adjoint. The class is an extension of the EvolutionOperator abstract base.
  """

  @abstractmethod
  def dot(self, trajectory, x, out=None):
    """
    Implements tangent-linear operator for the evolution model.

    Args:
      trajectory    : Trajectory at which the linearization occurs. This is the instance
                      returned from :func:`da.EvolutionOperator.__call__()`.
      x (NxM array) : Vector on which the tangent-linear operates.

    Returns:
      NxM array     : The product :math:`\mathbf{A}x`, where :math:`\mathbf{A}` is the tangent-linear operator.

    Subclasses must implement this method at least for M=1 (M is the number of columns in `x`).
    The default implementation handles the case of M > 1 by calling this method for each
    column of `x` individually and can therefore be used form the subclass as a convenience.

    """

    N, M = x.shape

    if M > 1:
      haveCompositeTrj = isinstance(trajectory, CompositeTrajectory)
      if haveCompositeTrj: assert M == len(trajectory)

      for m in range(M):
        x[:,m] = self.dot(trajectory[m] if haveCompositeTrj else trajectory, x[:,m])

      return x
    else:
      raise Exception("LinearizedEvolutionOperator.tl() not implemented")

  @abstractmethod
  def adjdot(self, trajectory, x, out=None):
    """
    Implements adjoint operator for the evolution model.

    Args:
      trajectory    : Trajectory at which the linearization occurs. This is the instance
                      returned from :func:`da.EvolutionOperator.__call__()`.
      x (NxM array) : Vector on which the adjoint operates.

    Returns:
      NxM array     : The product :math:`\mathbf{A}^\mathsf{T} x` where :math:`\mathbf{A}^\mathsf{T}` is the adjoint operator.

    Subclasses must implement this method at least for M=1 (M is the number of columns in `x`).
    The default implementation handles the case of M > 1 by calling this method for each
    column of `x` individually and can therefore be used form the subclass as a convenience.
    """

    N, M = x.shape

    if M > 1:
      haveCompositeTrj = isinstance(trajectory, CompositeTrajectory)
      if haveCompositeTrj: assert M == len(trajectory)

      for m in range(M):
        x[:,m] = self.adjdot(trajectory[m] if haveCompositeTrj else trajectory, x[:,m])

      return x
    else:
      raise Exception("LinearizedEvolutionOperator.ad() not implemented")


# -*- coding: utf-8 -*-
"""Chemical Engineering Design Library (ChEDL). Utilities for process modeling.
Copyright (C) 2016, 2017, 2018, 2019 Caleb Bell <Caleb.Andrew.Bell@gmail.com>

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.

This module contains various surface tension estimation routines, dataframes
of fit coefficients, fitting model equations, mixing rules, and
water-hydrocarbon interfacial tension estimation routines.

For reporting bugs, adding feature requests, or submitting pull requests,
please use the `GitHub issue tracker <https://github.com/CalebBell/chemicals/>`_.

.. contents:: :local:

Pure Component Correlations
---------------------------
.. autofunction:: chemicals.interface.Brock_Bird
.. autofunction:: chemicals.interface.Pitzer_sigma
.. autofunction:: chemicals.interface.Sastri_Rao
.. autofunction:: chemicals.interface.Zuo_Stenby
.. autofunction:: chemicals.interface.Hakim_Steinberg_Stiel
.. autofunction:: chemicals.interface.Miqueu
.. autofunction:: chemicals.interface.Aleem
.. autofunction:: chemicals.interface.Mersmann_Kind_sigma

Mixing Rules
------------
.. autofunction:: chemicals.interface.Winterfeld_Scriven_Davis
.. autofunction:: chemicals.interface.Weinaug_Katz
.. autofunction:: chemicals.interface.Diguilio_Teja

Correlations for Specific Substances
------------------------------------
.. autofunction:: chemicals.interface.sigma_IAPWS

Petroleum Correlations
----------------------
.. autofunction:: chemicals.interface.API10A32

Oil-Water Interfacial Tension Correlations
------------------------------------------
.. autofunction:: chemicals.interface.Meybodi_Daryasafar_Karimi

Fit Correlations
----------------
.. autofunction:: chemicals.interface.REFPROP_sigma
.. autofunction:: chemicals.interface.Somayajulu
.. autofunction:: chemicals.interface.Jasper
.. autofunction:: chemicals.interface.PPDS14
.. autofunction:: chemicals.interface.Watson_sigma
.. autofunction:: chemicals.interface.ISTExpansion

Fit Coefficients
----------------
All of these coefficients are lazy-loaded, so they must be accessed as an
attribute of this module.

.. data:: sigma_data_Mulero_Cachadina

    Data from [5]_ with :obj:`REFPROP_sigma` coefficients.

.. data:: sigma_data_Jasper_Lange

    Data as shown in [4]_ but originally in [3]_ with :obj:`Jasper` coefficients.

.. data:: sigma_data_Somayajulu

    Data from [1]_ with :obj:`Somayajulu` coefficients.

.. data:: sigma_data_Somayajulu2

    Data from [2]_ with :obj:`Somayajulu` coefficients. These should be
    preferred over the original coefficients.

.. data:: sigma_data_VDI_PPDS_11

    Data from [6]_ with :obj:`chemicals.dippr.EQ106` coefficients.

.. [1] Somayajulu, G. R. "A Generalized Equation for Surface Tension from
   the Triple Point to the Critical Point." International Journal of
   Thermophysics 9, no. 4 (July 1988): 559-66. doi:10.1007/BF00503154.
.. [2] Mulero, A., M. I. Parra, and I. Cachadina. "The Somayajulu
   Correlation for the Surface Tension Revisited." Fluid Phase
   Equilibria 339 (February 15, 2013): 81-88.
   doi:10.1016/j.fluid.2012.11.038.
.. [3] Jasper, Joseph J. "The Surface Tension of Pure Liquid Compounds."
   Journal of Physical and Chemical Reference Data 1, no. 4
   (October 1, 1972): 841-1010. doi:10.1063/1.3253106.
.. [4] Speight, James. Lange's Handbook of Chemistry. 16 edition.
   McGraw-Hill Professional, 2005.
.. [5] Mulero, A., I. Cachadiña, and M. I. Parra. “Recommended
   Correlations for the Surface Tension of Common Fluids.” Journal of
   Physical and Chemical Reference Data 41, no. 4 (December 1, 2012):
   043105. doi:10.1063/1.4768782.
.. [6] Gesellschaft, V. D. I., ed. VDI Heat Atlas. 2nd edition.
   Berlin; New York:: Springer, 2010.

The structure of each dataframe is shown below:

.. ipython::

    In [1]: import chemicals

    In [2]: chemicals.interface.sigma_data_Mulero_Cachadina

    In [3]: chemicals.interface.sigma_data_Jasper_Lange

    In [4]: chemicals.interface.sigma_data_Somayajulu

    In [5]: chemicals.interface.sigma_data_Somayajulu2

    In [6]: chemicals.interface.sigma_data_VDI_PPDS_11
"""

from __future__ import division

__all__ = ['REFPROP_sigma', 'Somayajulu', 'Jasper',
           'Brock_Bird', 'Pitzer_sigma', 'Sastri_Rao', 'Zuo_Stenby',
           'sigma_IAPWS', 'PPDS14', 'Watson_sigma',
           'Mersmann_Kind_sigma', 'API10A32',
           'Hakim_Steinberg_Stiel', 'Miqueu', 'Aleem',
           'Winterfeld_Scriven_Davis', 'Diguilio_Teja', 'Weinaug_Katz',
           'Meybodi_Daryasafar_Karimi', 'ISTExpansion']

import os
from fluids.numerics import numpy as np
from fluids.constants import N_A, k, root_two
from chemicals.utils import log, exp, sqrt
from chemicals.utils import mixing_simple, PY37, source_path, os_path_join, can_load_data, mark_numba_incompatible
from chemicals.data_reader import register_df_source, data_source

folder = os_path_join(source_path, 'Interface')

register_df_source(folder, 'MuleroCachadinaParameters.tsv')
register_df_source(folder, 'Jasper-Lange.tsv')
register_df_source(folder, 'Somayajulu.tsv')
register_df_source(folder, 'SomayajuluRevised.tsv')
register_df_source(folder, 'VDI PPDS surface tensions.tsv')

_interface_dfs_loaded = False
@mark_numba_incompatible
def load_interface_dfs():
    global _interface_dfs_loaded, sigma_data_Mulero_Cachadina, sigma_values_Mulero_Cachadina
    global sigma_data_Jasper_Lange, sigma_values_Jasper_Lange
    global sigma_data_Somayajulu, sigma_values_Somayajulu, sigma_data_Somayajulu2
    global sigma_values_Somayajulu2, sigma_data_VDI_PPDS_11, sigma_values_VDI_PPDS_11

    sigma_data_Mulero_Cachadina = data_source('MuleroCachadinaParameters.tsv')
    sigma_values_Mulero_Cachadina = np.array(sigma_data_Mulero_Cachadina.values[:, 1:], dtype=float)

    sigma_data_Jasper_Lange = data_source('Jasper-Lange.tsv')
    sigma_values_Jasper_Lange = np.array(sigma_data_Jasper_Lange.values[:, 1:], dtype=float)

    sigma_data_Somayajulu = data_source('Somayajulu.tsv')
    sigma_values_Somayajulu = np.array(sigma_data_Somayajulu.values[:, 1:], dtype=float)

    sigma_data_Somayajulu2 = data_source('SomayajuluRevised.tsv')
    sigma_values_Somayajulu2 = np.array(sigma_data_Somayajulu2.values[:, 1:], dtype=float)

    sigma_data_VDI_PPDS_11 = data_source('VDI PPDS surface tensions.tsv')
    sigma_values_VDI_PPDS_11 = np.array(sigma_data_VDI_PPDS_11.values[:, 1:], dtype=float)

if PY37:
    def __getattr__(name):
        if name in ('sigma_data_Mulero_Cachadina', 'sigma_values_Mulero_Cachadina',
                    'sigma_data_Jasper_Lange', 'sigma_values_Jasper_Lange',
                    'sigma_data_Somayajulu', 'sigma_values_Somayajulu', 'sigma_data_Somayajulu2',
                    'sigma_values_Somayajulu2', 'sigma_data_VDI_PPDS_11', 'sigma_values_VDI_PPDS_11'
                    ):
            load_interface_dfs()
            return globals()[name]
        raise AttributeError("module %s has no attribute %s" %(__name__, name))
else:
    if can_load_data:
        load_interface_dfs()

def sigma_IAPWS(T):
    r'''Calculate the surface tension of pure water as a function of .
    temperature. Assumes the 2011 IAPWS [1]_ formulation.

    .. math::
        \sigma = B\tau^\mu(1+b\tau)\\

    .. math::
        \tau = 1-T/T_c\\

    .. math::
        B = 0.2358 \text{N/m}\\

    .. math::
        b = -0.625\\

    .. math::
        \mu = 1.256

    Parameters
    ----------
    T : float
        Temperature of liquid [K]

    Returns
    -------
    sigma : float
        Air-water surface tension, [N/m]

    Notes
    -----
    This function is valid from the triple temperature to the critical
    temperature. No effects for pressure are included in the formulation.
    Test values are from IAPWS 2010 book.

    Examples
    --------
    >>> sigma_IAPWS(300.)
    0.0716859625271
    >>> sigma_IAPWS(450.)
    0.0428914991565
    >>> sigma_IAPWS(600.)
    0.0083756108728

    References
    ----------
    .. [1] IAPWS. 2014. Revised Release on Surface Tension of Ordinary Water
       Substance
    '''
    tau = 1. - T*(1.0/647.096)
    return 0.2358*tau**1.256*(1.0 - 0.625*tau)

### Regressed coefficient-based functions

def REFPROP_sigma(T, Tc, sigma0, n0, sigma1=0.0, n1=0.0, sigma2=0.0, n2=0.0):
    r'''Calculates air-liquid surface tension  using the REFPROP_sigma [1]_
    regression-based method. Relatively recent, and most accurate.

    .. math::
        \sigma(T)=\sigma_0\left(1-\frac{T}{T_c}\right)^{n_0}+
        \sigma_1\left(1-\frac{T}{T_c}\right)^{n_1}+
        \sigma_2\left(1-\frac{T}{T_c}\right)^{n_2}

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    sigma0 : float
        First emperical coefficient of a fluid
    n0 : float
        First emperical exponent of a fluid
    sigma1 : float, optional
        Second emperical coefficient of a fluid.
    n1 : float, optional
        Second emperical exponent of a fluid.
    sigma1 : float, optional
        Third emperical coefficient of a fluid.
    n2 : float, optional
        Third emperical exponent of a fluid.

    Returns
    -------
    sigma : float
        Liquid surface tension, [N/m]

    Notes
    -----
    Function as implemented in [1]_. No example necessary; results match
    literature values perfectly.
    Form of function returns imaginary results when T > Tc; None is returned
    if this is the case.

    Examples
    --------
    Parameters for water at 298.15 K

    >>> REFPROP_sigma(298.15, 647.096, -0.1306, 2.471, 0.2151, 1.233)
    0.07205503890847453

    References
    ----------
    .. [1] Diky, Vladimir, Robert D. Chirico, Chris D. Muzny, Andrei F.
       Kazakov, Kenneth Kroenlein, Joseph W. Magee, Ilmutdin Abdulagatov, and
       Michael Frenkel. "ThermoData Engine (TDE): Software Implementation of
       the Dynamic Data Evaluation Concept." Journal of Chemical Information
       and Modeling 53, no. 12 (2013): 3418-30. doi:10.1021/ci4005699.
    '''
    Tr = T/Tc
    one_minus_Tr = 1.0 - Tr
    sigma = sigma0*(one_minus_Tr)**n0 + sigma1*(one_minus_Tr)**n1 + sigma2*(one_minus_Tr)**n2
    return sigma

def PPDS14(T, Tc, a0, a1, a2):
    r'''Calculates air-water surface tension  using the [1]_
    emperical (parameter-regressed) method, called the PPDS 14 equation for
    surface tension.

    .. math::
        \sigma = a_0 \tau^{a_1}(1 + a_2 \tau)

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    a0 : float
        Regression parameter, [N/m]
    a1 : float
        Regression parameter, [-]
    a2 : float
        Regression parameter, [-]

    Returns
    -------
    sigma : float
        Liquid surface tension, [N/m]

    Notes
    -----

    Examples
    --------
    Benzene at 280 K from [1]_

    >>> PPDS14(T=280, Tc=562.05, a0=0.0786269, a1=1.28646, a2=-0.112304)
    0.030559764256249854

    References
    ----------
    .. [1] "ThermoData Engine (TDE103b V10.1) User’s Guide."
       https://trc.nist.gov/TDE/Help/TDE103b/Eqns-Pure-SurfaceTension/PPDS14.htm.
    .. [2] Frenkel, Michael, Robert D. Chirico, Vladimir Diky, Xinjian Yan,
       Qian Dong, and Chris Muzny. "ThermoData Engine (TDE):  Software
       Implementation of the Dynamic Data Evaluation Concept." Journal of
       Chemical Information and Modeling 45, no. 4 (July 1, 2005): 816-38.
       https://doi.org/10.1021/ci050067b.
    '''
    tau = 1.0 - T/Tc
    return a0*tau**a1*(1.0 + a2*tau)

def Watson_sigma(T, Tc, a1, a2, a3=0.0, a4=0.0, a5=0.0):
    r'''Calculates air-water surface tension using the Watson [1]_
    emperical (parameter-regressed) method developed by NIST.

    .. math::
        \sigma = \exp\left[a_{1} + \ln(1 - T_r)\left(
        a_2 + a_3T_r + a_4T_r^2 + a_5T_r^3 \right)\right]

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    a1 : float
        Regression parameter, [-]
    a2 : float
        Regression parameter, [-]
    a3 : float
        Regression parameter, [-]
    a4 : float
        Regression parameter, [-]
    a5 : float
        Regression parameter, [-]

    Returns
    -------
    sigma : float
        Liquid surface tension, [N/m]

    Notes
    -----
    This expression is also used for enthalpy of vaporization in [1]_.
    The coefficients from NIST TDE for enthalpy of vaporization are kJ/mol.

    Examples
    --------
    Isooctane at 350 K from [1]_:

    >>> Watson_sigma(T=350.0, Tc=543.836, a1=-3.02417, a2=1.21792, a3=-5.26877e-9, a4=5.62659e-9, a5=-2.27553e-9)
    0.0138340926605649

    References
    ----------
    .. [1] "ThermoData Engine (TDE103b V10.1) User’s Guide."
       https://trc.nist.gov/TDE/Help/TDE103b/Eqns-Pure-SurfaceTension/HVPExpansion-SurfaceTension.htm
    '''
    Tr = T/Tc
    l = log(1.0 - Tr)
    return exp(a1 + l*(a2 + Tr*(a3 + Tr*(a4 + a5*Tr))))

def ISTExpansion(T, Tc, a1, a2, a3=0.0, a4=0.0, a5=0.0):
    r'''Calculates air-water surface tension using the IST expansion [1]_
    emperical (parameter-regressed) method developed by NIST.

    .. math::
        \sigma = \sum_i a_i\left(1 - \frac{T}{T_c} \right)^i

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    a1 : float
        Regression parameter, [-]
    a2 : float
        Regression parameter, [-]
    a3 : float
        Regression parameter, [-]
    a4 : float
        Regression parameter, [-]
    a5 : float
        Regression parameter, [-]

    Returns
    -------
    sigma : float
        Liquid surface tension, [N/m]

    Notes
    -----

    Examples
    --------
    Diethyl phthalate at 400 K from [1]_:

    >>> ISTExpansion(T=400.0, Tc=776.0, a1=0.037545, a2=0.0363288)
    0.02672100905515996

    References
    ----------
    .. [1] "ThermoData Engine (TDE103b V10.1) User’s Guide."
       https://trc.nist.gov/TDE/Help/TDE103b/Eqns-Pure-SurfaceTension/ISTExpansion-SurfaceTension.htm
    '''
    tau = 1.0 - T/Tc
    return tau*(a1 + tau*(a2 + tau*(a3 + tau*(a4 + a5*tau))))

def Somayajulu(T, Tc, A, B, C):
    r'''Calculates air-water surface tension  using the [1]_
    emperical (parameter-regressed) method. Well regressed, no recent data.

    .. math::
        \sigma=aX^{5/4}+bX^{9/4}+cX^{13/4}

    .. math::
        X=(T_c-T)/T_c

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    A : float
        Regression parameter
    B : float
        Regression parameter
    C : float
        Regression parameter

    Returns
    -------
    sigma : float
        Liquid surface tension, N/m

    Notes
    -----
    Presently untested, but matches expected values. Internal units are mN/m.
    Form of function returns imaginary results when T > Tc; None is returned
    if this is the case. Function is claimed valid from the triple to the
    critical point. Results can be evaluated beneath the triple point.

    Examples
    --------
    Water at 300 K

    >>> Somayajulu(300, 647.126, 232.713514, -140.18645, -4.890098)
    0.07166386387996758

    References
    ----------
    .. [1] Somayajulu, G. R. "A Generalized Equation for Surface Tension from
       the Triple Point to the Critical Point." International Journal of
       Thermophysics 9, no. 4 (July 1988): 559-66. doi:10.1007/BF00503154.
    '''
    X = (Tc-T)/Tc
    return X*sqrt(sqrt(X))*(A + X*(B + C*X))*1e-3

def Jasper(T, a, b):
    r'''Calculates surface tension of a fluid given two parameters, a linear
    fit in Celcius from [1]_ with data reprinted in [2]_.

    .. math::
        \sigma = a - bT

    Parameters
    ----------
    T : float
        Temperature of fluid, [K]
    a : float
        Parameter for equation. Chemical specific.
    b : float
        Parameter for equation. Chemical specific.

    Returns
    -------
    sigma : float
        Surface tension [N/m]

    Notes
    -----
    Internal units are mN/m, and degrees Celcius.
    This function has been checked against several references.

    Examples
    --------
    >>> Jasper(298.15, 24, 0.0773)
    0.0220675

    References
    ----------
    .. [1] Jasper, Joseph J. "The Surface Tension of Pure Liquid Compounds."
       Journal of Physical and Chemical Reference Data 1, no. 4
       (October 1, 1972): 841-1010. doi:10.1063/1.3253106.
    .. [2] Speight, James. Lange's Handbook of Chemistry. 16 edition.
       McGraw-Hill Professional, 2005.
    '''
    sigma = (a - b*(T-273.15))*1e-3
    return sigma

### CSP methods

def Brock_Bird(T, Tb, Tc, Pc):
    r'''Calculates air-water surface tension  using the [1]_
    emperical method. Old and tested.

    .. math::
        \sigma = P_c^{2/3}T_c^{1/3}Q(1-T_r)^{11/9}

    .. math::
        Q = 0.1196 \left[ 1 + \frac{T_{br}\ln (P_c/1.01325)}{1-T_{br}}\right]-0.279

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tb : float
        Boiling temperature of the fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    Pc : float
        Critical pressure of fluid [Pa]

    Returns
    -------
    sigma : float
        Liquid surface tension, N/m

    Notes
    -----
    Numerous arrangements of this equation are available.
    This is DIPPR Procedure 7A: Method for the Surface Tension of Pure,
    Nonpolar, Nonhydrocarbon Liquids
    The exact equation is not in the original paper.
    If the equation yields a negative result, return None.

    Examples
    --------
    p-dichloribenzene at 412.15 K, from DIPPR; value differs due to a slight
    difference in method.

    >>> Brock_Bird(412.15, 447.3, 685, 3.952E6)
    0.02208448325192495

    Chlorobenzene from Poling, as compared with a % error value at 293 K.

    >>> Brock_Bird(293.15, 404.75, 633.0, 4530000.0)
    0.032985686413713036

    References
    ----------
    .. [1] Brock, James R., and R. Byron Bird. "Surface Tension and the
       Principle of Corresponding States." AIChE Journal 1, no. 2
       (June 1, 1955): 174-77. doi:10.1002/aic.690010208
    '''
    Tc_inv = 1.0/Tc
    Tbr = Tb*Tc_inv
    Tr = T*Tc_inv
    Pc = Pc*1e-5  # Convert to bar
    Q = 0.1196*(1.0 + Tbr*log(Pc*(1.0/1.01325))/(1.0 - Tbr)) - 0.279
    sigma = (Pc)**(2.0/3.0)*Tc**(1.0/3.0)*Q*(1.0 - Tr)**(11.0/9.0)
    sigma = sigma*1e-3  # convert to N/m
    return sigma

def Pitzer_sigma(T, Tc, Pc, omega):
    r'''Calculates air-water surface tension using the correlation derived
    by [1]_ from the works of [2]_ and [3]_. Based on critical property CSP
    methods.

    .. math::
        \sigma = P_c^{2/3}T_c^{1/3}\frac{1.86 + 1.18\omega}{19.05}
        \left[ \frac{3.75 + 0.91 \omega}{0.291 - 0.08 \omega}\right]^{2/3} (1-T_r)^{11/9}

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    Pc : float
        Critical pressure of fluid [Pa]
    omega : float
        Acentric factor for fluid, [-]

    Returns
    -------
    sigma : float
        Liquid surface tension, N/m

    Notes
    -----
    The source of this equation has not been reviewed.
    Internal units of presure are bar, surface tension of mN/m.

    Examples
    --------
    Chlorobenzene from Poling, as compared with a % error value at 293 K.

    >>> Pitzer_sigma(293., 633.0, 4530000.0, 0.249)
    0.03458453513446388

    References
    ----------
    .. [1] Poling, Bruce E. The Properties of Gases and Liquids. 5th edition.
       New York: McGraw-Hill Professional, 2000.
    .. [2] Curl, R. F., and Kenneth Pitzer. "Volumetric and Thermodynamic
       Properties of Fluids-Enthalpy, Free Energy, and Entropy." Industrial &
       Engineering Chemistry 50, no. 2 (February 1, 1958): 265-74.
       doi:10.1021/ie50578a047
    .. [3] Pitzer, K. S.: Thermodynamics, 3d ed., New York, McGraw-Hill,
       1995, p. 521.
    '''
    Tr = T/Tc
    Pc = Pc*1e-5  # Convert to bar
    sigma = Pc**(2.0/3.0)*Tc**(1.0/3.0)*(1.86 + 1.18*omega)*(1.0/19.05)*(
        (3.75 + 0.91*omega)/(0.291 - 0.08*omega))**(2.0/3.0)*(1.0 - Tr)**(11.0/9.0)
    return sigma*1e-3  # N/m, please

def Sastri_Rao(T, Tb, Tc, Pc, chemicaltype=None):
    r'''Calculates air-water surface tension using the correlation derived by
    [1]_ based on critical property CSP methods and chemical classes.

    .. math::
        \sigma = K P_c^xT_b^y T_c^z\left[\frac{1-T_r}{1-T_{br}}\right]^m

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tb : float
        Boiling temperature of the fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    Pc : float
        Critical pressure of fluid [Pa]

    Returns
    -------
    sigma : float
        Liquid surface tension, N/m

    Notes
    -----
    The source of this equation has not been reviewed.
    Internal units of presure are bar, surface tension of mN/m.

    Examples
    --------
    Chlorobenzene from Poling, as compared with a % error value at 293 K.

    >>> Sastri_Rao(293.15, 404.75, 633.0, 4530000.0)
    0.03234567739694441

    References
    ----------
    .. [1] Sastri, S. R. S., and K. K. Rao. "A Simple Method to Predict
       Surface Tension of Organic Liquids." The Chemical Engineering Journal
       and the Biochemical Engineering Journal 59, no. 2 (October 1995): 181-86.
       doi:10.1016/0923-0467(94)02946-6.
    '''
    if chemicaltype == 'alcohol':
        k, x, y, z, m = 2.28, 0.25, 0.175, 0, 0.8
    elif chemicaltype == 'acid':
        k, x, y, z, m = 0.125, 0.50, -1.5, 1.85, 11.0/9.0
    else:
        k, x, y, z, m = 0.158, 0.50, -1.5, 1.85, 11.0/9.0
    Tr = T/Tc
    Tbr = Tb/Tc
    Pc = Pc*1E-5  # Convert to bar
    sigma = k*Pc**x*Tb**y*Tc**z*((1.0 - Tr)/(1.0 - Tbr))**m
    sigma = sigma*1e-3  # N/m
    return sigma

def Zuo_Stenby(T, Tc, Pc, omega):
    r'''Calculates air-water surface tension using the reference fluids
    methods of [1]_.

    .. math::
        \sigma^{(1)} = 40.520(1-T_r)^{1.287}

    .. math::
        \sigma^{(2)} = 52.095(1-T_r)^{1.21548}

    .. math::
        \sigma_r = \sigma_r^{(1)}+ \frac{\omega - \omega^{(1)}}
        {\omega^{(2)}-\omega^{(1)}} (\sigma_r^{(2)}-\sigma_r^{(1)})

    .. math::
        \sigma = T_c^{1/3}P_c^{2/3}[\exp{(\sigma_r)} -1]

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    Pc : float
        Critical pressure of fluid [Pa]
    omega : float
        Acentric factor for fluid, [-]

    Returns
    -------
    sigma : float
        Liquid surface tension, N/m

    Notes
    -----
    Presently untested. Have not personally checked the sources.
    I strongly believe it is broken.
    The reference values for methane and n-octane are from the DIPPR database.

    Examples
    --------
    Chlorobenzene

    >>> Zuo_Stenby(293., 633.0, 4530000.0, 0.249)
    0.03345569011871088

    References
    ----------
    .. [1] Zuo, You-Xiang, and Erling H. Stenby. "Corresponding-States and
       Parachor Models for the Calculation of Interfacial Tensions." The
       Canadian Journal of Chemical Engineering 75, no. 6 (December 1, 1997):
       1130-37. doi:10.1002/cjce.5450750617
    '''
    Tc_1, Pc_1, omega_1 = 190.56, 4599000.0*1e-5, 0.012
    Tc_2, Pc_2, omega_2 = 568.7, 2490000.0*1e-5, 0.4
    Pc = Pc*1e-5
    Tr = T/Tc

    ST_1 = 40.520*(1.0 - Tr)**1.287  # Methane
    ST_2 = 52.095*(1.0 - Tr)**1.21548  # n-octane

    ST_r_1 = log(1.0 + 0.013537770442486932*ST_1) # Constant from 1/(Tc_1**(1.0/3.0)*Pc_1**(2.0/3.0))
#    ST_r_1 = log(1.0 + ST_1/(Tc_1**(1.0/3.0)*Pc_1**(2.0/3.0)))
    ST_r_2 = log(1.0 + 0.014154874587259097*ST_2) # Constant from /(Tc_2**(1.0/3.0)*Pc_2**(2.0/3.0))
    sigma_r = ST_r_1 + (omega-omega_1)*(ST_r_2-ST_r_1)*2.5773195876288657
#    sigma_r = ST_r_1 + (omega-omega_1)/(omega_2 - omega_1)*(ST_r_2-ST_r_1)
    sigma = Tc**(1.0/3.0)*Pc**(2.0/3.0)*(exp(sigma_r) - 1.0)
    sigma = sigma*1e-3  # N/m, please
    return sigma

def Hakim_Steinberg_Stiel(T, Tc, Pc, omega, StielPolar=0.0):
    r'''Calculates air-water surface tension using the reference fluids methods
    of [1]_.

    .. math::
        \sigma = 4.60104\times 10^{-7} P_c^{2/3}T_c^{1/3}Q_p \left(\frac{1-T_r}{0.4}\right)^m

    .. math::
        Q_p = 0.1574+0.359\omega-1.769\chi-13.69\chi^2-0.51\omega^2+1.298\omega\chi

    .. math::
        m = 1.21+0.5385\omega-14.61\chi-32.07\chi^2-1.65\omega^2+22.03\omega\chi

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    Pc : float
        Critical pressure of fluid [Pa]
    omega : float
        Acentric factor for fluid, [-]
    StielPolar : float, optional
        Stiel Polar Factor, [-]

    Returns
    -------
    sigma : float
        Liquid surface tension, N/m

    Notes
    -----
    Original equation for m and Q are used. Internal units are atm and mN/m.

    Examples
    --------
    1-butanol, as compared to value in CRC Handbook of 0.02493.

    >>> Hakim_Steinberg_Stiel(298.15, 563.0, 4414000.0, 0.59, StielPolar=-0.07872)
    0.02190790257519

    References
    ----------
    .. [1] Hakim, D. I., David Steinberg, and L. I. Stiel. "Generalized
       Relationship for the Surface Tension of Polar Fluids." Industrial &
       Engineering Chemistry Fundamentals 10, no. 1 (February 1, 1971): 174-75.
       doi:10.1021/i160037a032.
    '''
    omega2 = omega*omega
    StielPolar2 = StielPolar*StielPolar
    Q = (0.1574 + 0.359*omega - 1.769*StielPolar - 13.69*StielPolar2
        - 0.510*omega2 + 1.298*StielPolar*omega)
    m = (1.210 + 0.5385*omega - 14.61*StielPolar - 32.07*StielPolar2
        - 1.656*omega2 + 22.03*StielPolar*omega)
    Tr = T/Tc
    Pc = Pc*(1.0/101325.0)
    sigma = Pc**(2.0/3.)*Tc**(1.0/3.0)*Q*(2.5*(1.0 - Tr))**m
    sigma = sigma*1e-3  # convert to N/m
    return sigma

def Miqueu(T, Tc, Vc, omega):
    r'''Calculates air-water surface tension using the methods of [1]_.

    .. math::
        \sigma = k T_c \left( \frac{N_a}{V_c}\right)^{2/3}
        (4.35 + 4.14 \omega)t^{1.26}(1+0.19t^{0.5} - 0.487t)

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    Tc : float
        Critical temperature of fluid [K]
    Vc : float
        Critical volume of fluid [m^3/mol]
    omega : float
        Acentric factor for fluid, [-]

    Returns
    -------
    sigma : float
        Liquid surface tension, N/m

    Notes
    -----
    Uses Avogadro's constant and the Boltsman constant.
    Internal units of volume are mL/mol and mN/m. However, either a typo
    is in the article or author's work, or my value of k is off by 10; this is
    corrected nonetheless.
    Created with 31 normal fluids, none polar or hydrogen bonded. Has an
    AARD of 3.5%.

    Examples
    --------
    Bromotrifluoromethane, 2.45 mN/m

    >>> Miqueu(300., 340.1, 0.000199, 0.1687)
    0.003474100774091376

    References
    ----------
    .. [1] Miqueu, C, D Broseta, J Satherley, B Mendiboure, J Lachaise, and
       A Graciaa. "An Extended Scaled Equation for the Temperature Dependence
       of the Surface Tension of Pure Compounds Inferred from an Analysis of
       Experimental Data." Fluid Phase Equilibria 172, no. 2 (July 5, 2000):
       169-82. doi:10.1016/S0378-3812(00)00384-8.
    '''
    Vc = Vc*1E6
    t = 1. - T/Tc
    sigma = k*Tc*(N_A/Vc)**(2.0/3.0)*(4.35 + 4.14*omega)*t**1.26*(1.0 + 0.19*sqrt(t) - 0.25*t)*10000.0
    return sigma

def Aleem(T, MW, Tb, rhol, Hvap_Tb, Cpl):
    r'''Calculates vapor-liquid surface tension using the correlation derived by
    [1]_ based on critical property CSP methods.

    .. math::
        \sigma = \phi \frac{MW^{1/3}} {6N_A^{1/3}}\rho_l^{2/3}\left[H_{vap}
        + C_{p,l}(T_b-T)\right]

    .. math::
        \phi = 1 - 0.0047MW + 6.8\times 10^{-6} MW^2

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    MW : float
        Molecular weight [g/mol]
    Tb : float
        Boiling temperature of the fluid [K]
    rhol : float
        Liquid density at T and P [kg/m^3]
    Hvap_Tb : float
        Mass enthalpy of vaporization at the normal boiling point [kg/m^3]
    Cpl : float
        Liquid heat capacity of the chemical at T [J/kg/K]

    Returns
    -------
    sigma : float
        Liquid-vapor surface tension [N/m]

    Notes
    -----
    Internal units of molecuar weight are kg/mol. This model is dimensionally
    consistent.

    This model does not use the critical temperature. After it predicts a
    surface tension of 0 at a sufficiently high temperature, it returns
    negative results. The temperature at which this occurs (the "predicted"
    critical temperature) can be calculated as follows:

    .. math::
        \sigma = 0 \to T_{c,predicted} \text{ at } T_b + \frac{H_{vap}}{Cp_l}

    Because of its dependence on density, it has the potential to model the
    effect of pressure on surface tension.

    Claims AAD of 4.3%. Developed for normal alkanes. Total of 472 data points.
    Behaves worse for higher alkanes. Behaves very poorly overall.

    Examples
    --------
    Methane at 90 K

    >>> Aleem(T=90, MW=16.04246, Tb=111.6, rhol=458.7, Hvap_Tb=510870.,
    ... Cpl=2465.)
    0.01669970230131523

    References
    ----------
    .. [1] Aleem, W., N. Mellon, S. Sufian, M. I. A. Mutalib, and D. Subbarao.
       "A Model for the Estimation of Surface Tension of Pure Hydrocarbon
       Liquids." Petroleum Science and Technology 33, no. 23-24 (December 17,
       2015): 1908-15. doi:10.1080/10916466.2015.1110593.
    '''
    MW = MW*1e-3 # Use kg/mol for consistency with the other units
    sphericity = 1. - MW*(0.0047 - 6.8E-6*MW)
    return sphericity*MW**(1.0/3.0)/(6.*N_A**(1.0/3.0))*rhol**(2.0/3.)*(Hvap_Tb + Cpl*(Tb-T))

def Mersmann_Kind_sigma(T, Tm, Tb, Tc, Pc, n_associated=1):
    r'''Estimates the surface tension of organic liquid substances
    according to the method of [1]_.

    .. math::
        \sigma^* = \frac{\sigma n_{ass}^{1/3}} {(kT_c)^{1/3} T_{rm}P_c^{2/3}}

    .. math::
        \sigma^* = \left(\frac{T_b - T_m}{T_m}\right)^{1/3}
        \left[6.25(1-T_r) + 31.3(1-T_r)^{4/3}\right]

    Parameters
    ----------
    T : float
        Temperature of the fluid [K]
    Tm : float
        Melting temperature [K]
    Tb : float
        Boiling temperature of the fluid [K]
    Tc : float
        Critical temperature of the fluid [K]
    Pc : float
        Critical pressure of the fluid [Pa]
    n_associated : float
        Number of associated molecules in a cluster (2 for alcohols, 1
        otherwise), [-]

    Returns
    -------
    sigma : float
        Liquid-vapor surface tension [N/m]

    Notes
    -----
    In the equation, all quantities must be in SI units. `k` is the boltzman
    constant.

    Examples
    --------
    MTBE at STP (the actual value is 0.0181):

    >>> Mersmann_Kind_sigma(298.15, 164.15, 328.25, 497.1, 3430000.0)
    0.016744311449290426

    References
    ----------
    .. [1] Mersmann, Alfons, and Matthias Kind. "Prediction of Mechanical and
       Thermal Properties of Pure Liquids, of Critical Data, and of Vapor
       Pressure." Industrial & Engineering Chemistry Research, January 31,
       2017. https://doi.org/10.1021/acs.iecr.6b04323.
    '''
    Tr = T/Tc
    sigma_star = ((Tb - Tm)/Tm)**(1.0/3.)*(6.25*(1. - Tr) + 31.3*(1. - Tr)**(4.0/3.))
    sigma = sigma_star*(k*Tc)**(1.0/3.0)*(Tm/Tc)*Pc**(2.0/3.0)*n_associated**(-1.0/3.0)
    return sigma

def API10A32(T, Tc, K_W):
    r'''Calculates the interfacial tension between
    a liquid petroleum fraction and air, using the oil's pseudocritical
    temperature and Watson K Characterization factor.

    .. math::
        \sigma = \frac{673.7\left[\frac{\left(T_c - T\right)}{T_c}\right]^{1.232}}{K_W}

    Parameters
    ----------
    T : float
        Liquid temperature, [K]
    Tc : float
        Pseudocritical temperature (or critical temperature if using
        the equation with a pure component), [K]
    K_W : float
        Watson characterization factor

    Returns
    -------
    sigma : float
        Air-water surface tension, [N/m]

    Notes
    -----
    [1]_ cautions that this should not be applied to coal liquids,
    and that it will give higher errors at pressures above 500 psi.
    [1]_ claims this has an average error of 10.7%.

    This function converges to zero at `Tc`; do not use it above that
    temperature!

    Examples
    --------
    Sample problem in Comments on Procedure 10A3.2.1 of [1]_;

    >>> from fluids.core import F2K, R2K
    >>> API10A32(T=F2K(60), Tc=R2K(1334), K_W=12.4)
    29.577333312096968

    References
    ----------
    .. [1] API Technical Data Book: General Properties & Characterization.
       American Petroleum Institute, 7E, 2005.
    '''
    return 673.7*((Tc-T)/Tc)**1.232/K_W

### Surface Tension Mixtures

def Winterfeld_Scriven_Davis(xs, sigmas, rhoms):
    r'''Calculates surface tension of a liquid mixture according to
    mixing rules in [1]_ and also in [2]_.

    .. math::
        \sigma_M = \sum_i \sum_j \frac{1}{V_L^{L2}}\left(x_i V_i \right)
        \left( x_jV_j\right)\sqrt{\sigma_i\cdot \sigma_j}

    Parameters
    ----------
    xs : array-like
        Mole fractions of all components, [-]
    sigmas : array-like
        Surface tensions of all components, [N/m]
    rhoms : array-like
        Molar densities of all components, [mol/m^3]

    Returns
    -------
    sigma : float
        Air-liquid surface tension of mixture, [N/m]

    Notes
    -----
    DIPPR Procedure 7C: Method for the Surface Tension of Nonaqueous Liquid
    Mixtures

    Becomes less accurate as liquid-liquid critical solution temperature is
    approached. DIPPR Evaluation:  3-4% AARD, from 107 nonaqueous binary
    systems, 1284 points. Internally, densities are converted to kmol/m^3. The
    Amgat function is used to obtain liquid mixture density in this equation.

    Raises a ZeroDivisionError if either molar volume are zero, and a
    ValueError if a surface tensions of a pure component is negative.

    Examples
    --------
    >>> Winterfeld_Scriven_Davis([0.1606, 0.8394], [0.01547, 0.02877],
    ... [8610., 15530.])
    0.02496738845043982

    References
    ----------
    .. [1] Winterfeld, P. H., L. E. Scriven, and H. T. Davis. "An Approximate
       Theory of Interfacial Tensions of Multicomponent Systems: Applications
       to Binary Liquid-Vapor Tensions." AIChE Journal 24, no. 6
       (November 1, 1978): 1010-14. doi:10.1002/aic.690240610.
    .. [2] Danner, Ronald P, and Design Institute for Physical Property Data.
       Manual for Predicting Chemical Process Design Data. New York, N.Y, 1982.
    '''
    N = len(xs)
    Vms = [0.0]*N
    rho = 0.0
    for i in range(N):
        Vms[i] = 1e3/rhoms[i]
        rho += xs[i]*Vms[i]
#    rho = 1./rho
    rho = root_two/rho # factor out rt2
    # For speed, transform the Vms array to contain
#    xs[i]*Vms[i]*sigmas_05[i]*rho
    tot = 0.0
    for i in range(N):
        val = sqrt(sigmas[i])*xs[i]*rho*Vms[i]
        Vms[i] = val
        tot += val*val
    tot *= 0.5
    for i in range(N):
        # Symmetric - can be slightly optimized
        temp = 0.0
        for j in range(i):
            temp += Vms[j]
        tot += Vms[i]*temp

    return tot

def Diguilio_Teja(T, xs, sigmas_Tb, Tbs, Tcs):
    r'''Calculates surface tension of a liquid mixture according to
    mixing rules in [1]_.

    .. math::
        \sigma = 1.002855(T^*)^{1.118091} \frac{T}{T_b} \sigma_r

    .. math::
        T^* = \frac{(T_c/T)-1}{(T_c/T_b)-1}

    .. math::
        \sigma_r = \sum x_i \sigma_i

    .. math::
        T_b = \sum x_i T_{b,i}

    .. math::
        T_c = \sum x_i T_{c,i}

    Parameters
    ----------
    T : float
        Temperature of fluid [K]
    xs : array-like
        Mole fractions of all components
    sigmas_Tb : array-like
        Surface tensions of all components at the boiling point, [N/m]
    Tbs : array-like
        Boiling temperatures of all components, [K]
    Tcs : array-like
        Critical temperatures of all components, [K]

    Returns
    -------
    sigma : float
        Air-liquid surface tension of mixture, [N/m]

    Notes
    -----
    Simple model, however it has 0 citations. Gives similar results to the
    `Winterfeld_Scriven_Davis` model.

    Raises a ValueError if temperature is greater than the mixture's critical
    temperature or if the given temperature is negative, or if the mixture's
    boiling temperature is higher than its critical temperature.

    [1]_ claims a 4.63 percent average absolute error on 21 binary and 4
    ternary non-aqueous systems. [1]_ also considered Van der Waals mixing
    rules for `Tc`, but found it provided a higher error of 5.58%

    Examples
    --------
    >>> Diguilio_Teja(T=298.15, xs=[0.1606, 0.8394],
    ... sigmas_Tb=[0.01424, 0.02530], Tbs=[309.21, 312.95], Tcs=[469.7, 508.0])
    0.025716823875045505

    References
    ----------
    .. [1] Diguilio, Ralph, and Amyn S. Teja. "Correlation and Prediction of
       the Surface Tensions of Mixtures." The Chemical Engineering Journal 38,
       no. 3 (July 1988): 205-8. doi:10.1016/0300-9467(88)80079-0.
    '''
    Tc, Tb, sigmar = 0.0, 0.0, 0.0
    for i in range(len(xs)):
        Tc += Tcs[i]*xs[i]
        Tb += Tbs[i]*xs[i]
        sigmar += sigmas_Tb[i]*xs[i]
    if T > Tc:
        raise ValueError('T > Tc according to Kays rule - model is not valid in this range.')
    Tst = (Tc/T - 1.)/(Tc/Tb - 1.0)
    return 1.002855*Tst**1.118091*(T/Tb)*sigmar

def Weinaug_Katz(parachors, Vml, Vmg, xs, ys):
    r'''Calculates surface tension of a liquid mixture according to
    mixing rules in [1]_ and also in [2]_. This is based on the
    Parachor concept. This is called the Macleod-Sugden model in some places.

    .. math::
        \sigma_M = \left[\sum_i P_i\left( \frac{x_i}{V_{m,l}}
         - \frac{y_i}{V_{m,g}}\right) \right]^4

    Parameters
    ----------
    parachors : list[float]
        Parachors of each component, [N^0.25*m^2.75/mol]
    Vml : float
        Liquid mixture molar volume, [m^3/mol]
    Vmg : float
        Gas mixture molar volume; this can be set to zero at
        low pressures, [m^3/mol]
    xs : list[float]
        Mole fractions of all components in liquid phase, [-]
    xs : list[float]
        Mole fractions of all components in gas phase, [-]

    Returns
    -------
    sigma : float
        Air-liquid surface tension of mixture, [N/m]

    Notes
    -----
    This expression is efficient and does not require pure component
    surface tensions. Its accuracy is dubious.

    Examples
    --------
    >>> Weinaug_Katz([5.1e-5, 7.2e-5], Vml=0.000125, Vmg=0.02011, xs=[.4, .6], ys=[.6, .4])
    0.06547479150776776

    Neglect the vapor phase density by setting `Vmg` to a high value:

    >>> Weinaug_Katz([5.1e-5, 7.2e-5], Vml=0.000125, Vmg=1e100, xs=[.4, .6], ys=[.6, .4])
    0.06701752894095361

    References
    ----------
    .. [1] Weinaug, Charles F., and Donald L. Katz. "Surface Tensions of
       Methane-Propane Mixtures." Industrial & Engineering Chemistry 35,
       no. 2 (February 1, 1943): 239-246. https://doi.org/10.1021/ie50398a028.
    .. [2] Pedersen, Karen Schou, Aage Fredenslund, and Per Thomassen.
       Properties of Oils and Natural Gases. Vol. 5. Gulf Pub Co, 1989.
    '''
    tot = 0.0
    rhoml = 1.0/Vml
    rhomg = 1.0/Vmg
    for i in range(len(parachors)):
        tot += parachors[i]*(xs[i]*rhoml - ys[i]*rhomg)
    tot *= tot
    tot *= tot # fourth power it
    return tot

### Water-hydrocarbon interfacial tensions

def Meybodi_Daryasafar_Karimi(rho_water, rho_oil, T, Tc):
    r'''Calculates the interfacial tension between water and a hydrocabon
    liquid according to the correlation of [1]_.

    .. math::
        \gamma_{hw} = \left(\frac{A_1 + A_2 \Delta \rho + A_3\Delta\rho^2
        + A_4\Delta\rho^3} {A_5 + A_6\frac{T^{A_7}}{T_{c,h}} + A_8T^{A_9}}
        \right)^{A_{10}}

    Parameters
    ----------
    rho_water : float
        The density of the aqueous phase, [kg/m^3]
    rho_oil : float
        The density of the hydrocarbon phase, [kg/m^3]
    T : float
        Temperature of the fluid, [K]
    Tc : float
        Critical temperature of the hydrocarbon mixture, [K]

    Returns
    -------
    sigma : float
        Hydrocarbon-water surface tension [N/m]

    Notes
    -----
    Internal units of the equation are g/mL and mN/m.

    Examples
    --------
    >>> Meybodi_Daryasafar_Karimi(980, 760, 580, 914)
    0.02893598143089256

    References
    ----------
    .. [1] Kalantari Meybodi, Mahdi, Amin Daryasafar, and Masoud Karimi.
       "Determination of Hydrocarbon-Water Interfacial Tension Using a New
       Empirical Correlation."  Fluid Phase Equilibria 415 (May 15, 2016):
       42-50. doi:10.1016/j.fluid.2016.01.037.
    '''
    A1 = -1.3687340042E-1
    A2 = -3.0391828884E-1
    A3 = 5.6225871072E-1
    A4 = -3.3074367079E-1
    A5 = -3.0050179309E0
    A6 = 5.8914210205E-5
    A7 = -4.1388901263E0
    A8 = 3.0084299030E0
    A9 = -3.8203072876E-3
#    A10 = 3.5000000000E0
    drho = abs(rho_water - rho_oil)*1e-3 # Correlation in units of g/mL
    sigma = ((A1 + drho*(A2 + drho*(A3 + A4*drho)))
             /(A5 + A6*T**A7/Tc + A8*T**A9))
    return sigma*sigma*sigma*sqrt(sigma)*1e-3 # mN/m to N/m


"""
Calculations of overlap (similarity) between annotation sets.
"""
import abc
import math
import time
from collections import defaultdict
from pathlib import Path
from typing import Collection, Mapping, Optional, Sequence, Type, Union

import decorateme
import numpy as np
from pocketutils.core.chars import Chars
from pocketutils.core.enums import CleverEnum
from pocketutils.core.exceptions import XValueError
from pocketutils.tools.unit_tools import UnitTools
from typeddfs.df_errors import HashFileMissingError

from mandos.analysis import AnalysisUtils as Au
from mandos.analysis.io_defns import SimilarityDfLongForm, SimilarityDfShortForm
from mandos.model.hit_dfs import HitDf
from mandos.model.hits import AbstractHit
from mandos.model.utils import unlink

# note that most of these math functions are much faster than their numpy counterparts
# if we're not broadcasting, it's almost always better to use them
# some are more accurate, too
# e.g. we're using fsum rather than sum
from mandos.model.utils.setup import logger

class _Inf:
    def __init__(self, n: int):
        self.n = n
        self.used, self.t0, self.nonzeros = set(), time.monotonic(), 0

    def is_used(self, c1: str, c2: str) -> bool:
        return (c1, c2) in self.used or (c2, c1) in self.used

    def got(self, c1: str, c2: str, z: float) -> None:
        self.used.add((c1, c2))
        self.nonzeros += int(c1 != c2 and not np.isnan(z) and 0 < z < 1)
        if self.i % 1000 == 0:
            self.log("info")

    @property
    def i(self) -> int:
        return len(self.used)

    def log(self, level: str) -> None:
        delta = UnitTools.delta_time_to_str(time.monotonic() - self.t0, space=Chars.narrownbsp)
        logger.log(
            level.upper(),
            f"Processed {self.i:,}/{self.n:,} pairs in {delta};"
            + f" {self.nonzeros:,} ({self.nonzeros / self.i * 100:.1f}%) are nonzero",
        )

    def __repr__(self):
        return f"{self.__class__.__name__}({self.i}/{self.n})"

    def __str__(self):
        return repr(self)

@decorateme.auto_repr_str()
class MatrixCalculator(metaclass=abc.ABCMeta):
    def __init__(
        self,
        *,
        min_compounds: int,
        min_nonzero: int,
        min_hits: int,
        exclude: Optional[Collection[str]] = None,
    ):
        self.min_compounds = min_compounds
        self.min_nonzero = min_nonzero
        self.min_hits = min_hits
        self.exclude = set() if exclude is None else exclude

    def calc_all(self, hits: Path, to: Path, *, keep_temp: bool = False) -> SimilarityDfLongForm:
        raise NotImplemented()

class JPrimeMatrixCalculator(MatrixCalculator):
    def calc_all(self, path: Path, to: Path, *, keep_temp: bool = False) -> SimilarityDfLongForm:
        hits = self._read_hits(path)
        key_to_hit = Au.hit_multidict(hits, "search_key")
        logger.notice(f"Calculating J on {len(key_to_hit):,} keys from {len(hits):,} hits")
        good_keys = {}
        for key, key_hits in key_to_hit.items():
            key_hits: Sequence[AbstractHit] = key_hits
            n_compounds_0 = len({k.origin_inchikey for k in key_hits})
            part_path = self._part_path(to, key)
            df = None
            if part_path.exists():
                df = self._read_part(key, part_path)
            if df is None and n_compounds_0 >= self.min_compounds:
                df = self._calc_partial(key, key_hits)
                df.write_file(part_path, attrs=True, file_hash=True, mkdirs=True)
                logger.debug(f"Wrote results for {key} to {part_path}")
            if df is not None and self._should_include(df):
                good_keys[key] = part_path
            if df is not None:
                del df
        big_df = self._concat_parts(good_keys)
        big_df.write_file(to, attrs=True, file_hash=True, mkdirs=True)
        logger.notice(f"Wrote {len(big_df):,} rows to {to}")
        attrs_path = to.parent / (to.name + ".attrs.json")
        logger.sucess(f"Finished -- see {attrs_path} for statistics")
        if not keep_temp:
            for k in good_keys:
                unlink(self._part_path(to, k))

    def _read_hits(self, path: Path) -> Sequence[AbstractHit]:
        hits = HitDf.read_file(path)
        keys = hits["search_key"].unique()
        bad_excludes = [e for e in self.exclude if e not in keys]
        if len(bad_excludes) > 0:
            logger.error(f"Keys to exclude are not in the input file: {', '.join(bad_excludes)}")
        for key in keys:
            if key not in self.exclude:
                dfx = hits[hits["search_key"] == key]
                negatives = dfx[dfx["weight"] <= 0]
                if len(negatives) > 0:
                    logger.error(f"{len(negatives)} / {len(dfx):,} hits for {key} are nonpositive")
        return [h for h in hits.to_hits() if h.search_key not in self.exclude and h.weight > 0]

    def _calc_partial(self, key: str, key_hits: HitDf) -> SimilarityDfLongForm:
        df = self.calc_one(key, key_hits).to_long_form(kind="psi", key=key)
        return df.set_attrs(
            key=key,
            quartiles=[float(df["value"].quantile(x)) for x in [0, 0.25, 0.5, 0.75, 1]],
            n_hits=len(key_hits),
            n_values=len(df["value"].unique()),
            n_compounds=len(df["inchikey_1"].unique()),
            n_real=len(df[(df["value"].notna()) & (df["value"] > 0) & (df["value"] < 1)]),
        )

    def _should_include(self, df: SimilarityDfLongForm) -> bool:
        key = df.attrs["key"]
        reqs = dict(n_compounds=self.min_compounds, n_hits=self.min_hits, n_real=self.min_nonzero)
        for a, mn in reqs.items():
            v = df.attrs[a]
            if v < mn:
                logger.warning(f"Key {key}: {a} = {v:,} < {mn:,}")
                return False
        return True

    def _read_part(self, key: str, part_path: Path) -> Optional[SimilarityDfLongForm]:
        try:
            df = SimilarityDfLongForm.read_file(part_path, file_hash=True, attrs=True)
            logger.warning(f"Results for key {key} already exist ({len(df):,} rows)")
            return df
        except HashFileMissingError:
            logger.error(f"Extant results for key {key} appear incomplete; restarting")
            logger.opt(exception=True).debug(f"Hash error for {key}")
            unlink(part_path)
        return None  #  calculate from scratch

    def _concat_parts(self, keys: Mapping[str, Path]):
        logger.notice(f"Included {len(keys):,} keys: {', '.join(keys)}")
        dfs = []
        for key, pp in keys.items():
            df = SimilarityDfLongForm.read_file(pp, attrs=True)
            dfs.append(df)
        return SimilarityDfLongForm.of(dfs, keys=keys)

    def calc_one(self, key: str, hits: Sequence[AbstractHit]) -> SimilarityDfShortForm:
        ik2hits = Au.hit_multidict(hits, "origin_inchikey")
        logger.info(f"Calculating J on {key} for {len(ik2hits):,} compounds and {len(hits):,} hits")
        data = defaultdict(dict)
        inf = _Inf(n=int(len(ik2hits) * (len(ik2hits) - 1) / 2))
        for (c1, hits1) in ik2hits.items():
            for (c2, hits2) in ik2hits.items():
                if inf.is_used(c1, c2):
                    continue
                z = 1 if c1 == c2 else self._j_prime(key, hits1, hits2)
                data[c1][c2] = z
                inf.got(c1, c2, z)
        inf.log("success")
        return SimilarityDfShortForm.from_dict(data)

    def _part_path(self, path: Path, key: str):
        return path.parent / f".{path.name}-{key}.tmp.feather"

    def _j_prime(
        self, key: str, hits1: Collection[AbstractHit], hits2: Collection[AbstractHit]
    ) -> float:
        if len(hits1) == len(hits2) == 0:
            return float("NaN")  # TODO: Can this even happen?
        if len(hits1) == 0 or len(hits2) == 0:
            return 0
        sources = {h.data_source for h in hits1}.intersection({h.data_source for h in hits2})
        if len(sources) == 0:
            return float("NaN")
        values = [
            self._jx(
                key,
                [h for h in hits1 if h.data_source == source],
                [h for h in hits2 if h.data_source == source],
            )
            for source in sources
        ]
        return float(math.fsum(values) / len(values))

    def _jx(
        self, key: str, hits1: Collection[AbstractHit], hits2: Collection[AbstractHit]
    ) -> float:
        if len(hits1) == len(hits2) == 0:
            return float("NaN")  # TODO: impossible, right?
        if len(hits1) == 0 or len(hits2) == 0:
            return 0
        pair_to_weights = Au.weights_of_pairs(hits1, hits2)
        values = []
        for ca, cb in pair_to_weights.values():
            wedge = self._wedge(ca, cb)
            vee = self._vee(ca, cb)
            if vee > 0:
                values.append(wedge / vee)
        return float(math.fsum(values) / len(values))

    def _wedge(self, ca: float, cb: float) -> float:
        return math.sqrt(Au.elle(ca) * Au.elle(cb))

    def _vee(self, ca: float, cb: float) -> float:
        return Au.elle(ca) + Au.elle(cb) - math.sqrt(Au.elle(ca) * Au.elle(cb))

class MatrixAlg(CleverEnum):
    j = ()

    @property
    def clazz(self) -> Type[MatrixCalculator]:
        return {MatrixAlg.j: JPrimeMatrixCalculator}[self]

@decorateme.auto_utils()
class MatrixCalculation:
    @classmethod
    def create(
        cls,
        algorithm: Union[str, MatrixAlg],
        *,
        min_compounds: int,
        min_nonzero: int,
        min_hits: int,
        exclude: Optional[Collection[str]] = None,
    ) -> MatrixCalculator:
        return MatrixAlg.of(algorithm).clazz(
            min_compounds=min_compounds,
            min_nonzero=min_nonzero,
            min_hits=min_hits,
            exclude=exclude,
        )

__all__ = ["JPrimeMatrixCalculator", "MatrixAlg", "MatrixCalculation", "MatrixCalculator"]


# Copyright 2019 Huawei Technologies Co., Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import numpy as np
from akg.utils import kernel_exec as utils
from test_op import resize_bilinear
from tensorio import compare_tensor
from gen_random import random_gaussian

def resize_bilinear_run(in_shape, out_shape, dtype, kernel_name, attrs):
    kernel_name = utils.gen_name_kernel(kernel_name, dtype, in_shape)

    if 'tuning' in attrs.keys():
        t = attrs.get("tuning", False)
        kernel_name = attrs.get("kernel_name", False)
        mod = utils.op_build_test(resize_bilinear.resize_bilinear,
                                  input_shapes=[in_shape], input_types=[dtype],
                                  op_attrs=[out_shape], kernel_name=kernel_name, attrs=attrs, tuning=t)
        if t:
            expect, input, output = gen_data(dtype, in_shape, out_shape)
            return mod, expect, (input, output)
        else:
            return mod
    else:
        # Create op
        mod = utils.op_build_test(resize_bilinear.resize_bilinear,
                                  input_shapes=[in_shape], input_types=[dtype],
                                  op_attrs=[out_shape], kernel_name=kernel_name, attrs=attrs)

        expect, input, output = gen_data(dtype, in_shape, out_shape)
        output = utils.mod_launch(mod, (input, output), expect=expect)
        return input, output, expect, compare_tensor(output, expect, atol=5e-01, rtol=5e-03, equal_nan=True)

def gen_data(dtype, in_shape, out_shape):
    # Generate data for testing the op
    input = random_gaussian(in_shape, miu=1, sigma=4).astype(dtype)
    # Generate expected output using numpy implementation of resize bilinear
    expect = bilinear_expect(input, out_shape)
    # Predict output
    output = np.full(expect.shape, np.nan, dtype)
    return expect, input, output

def bilinear_expect(input_data, out_shape):
    # Get N,W,H,C from input data
    batch_size, in_height, in_width, channels = input_data.shape
    out_height, out_width = out_shape[0], out_shape[1]
    out_shape = [batch_size, out_height, out_width, channels]

    # scale value is required to map from input space to output space
    # align_corner version:
    height_scale = (in_height - 1.0) / (out_height - 1.0)
    width_scale = (in_width - 1.0) / (out_width - 1.0)

    # compute_interpolation_weights calculates lower, upper and lerp for each index of ys and xs
    def compute_interpolation_weights(index, scale):
        temp = CachedInterpolation(0, 0, 0)
        temp.lower = np.floor(index * scale).astype("int32")
        temp.upper = np.ceil(index * scale).astype("int32")
        temp.lerp = index * scale - temp.lower
        return temp

    # ys and xs will ensure which row(top,bottom) and column(left,right) index from input matrix will be responsible for the wighted calculation for each position of output matrix
    # ys will provide row information and xs will provide column information for output matrix
    # so ys size will be same as output height and xs size will be same as output width
    # they will also contain interpolation weight(lerp)
    ys = [compute_interpolation_weights(i, height_scale) for i in range(out_height)]
    xs = [compute_interpolation_weights(i, width_scale) for i in range(out_width)]

    return resize_image(input_data, out_shape, xs, ys)

# each position of row and column index of output matrix will contain lower, upper and lerp

class CachedInterpolation:
    def __init__(self, lower, upper, lerp):
        self.lower = lower
        self.upper = upper
        self.lerp = lerp

def resize_image(input_data, out_shape, xs, ys):
    def compute_lerp(top_left, top_right, bottom_left, bottom_right, x_lerp, y_lerp):
        top = top_left + (top_right - top_left) * x_lerp
        bottom = bottom_left + (bottom_right - bottom_left) * x_lerp
        return top + (bottom - top) * y_lerp

    output = np.zeros(out_shape).astype(input_data.dtype)
    batch_size, out_height, out_width, channels = out_shape
    for b in range(batch_size):
        for y in range(out_height):
            for x in range(out_width):
                for c in range(channels):
                    left_x_index = xs[x].lower
                    right_x_index = xs[x].upper
                    xs_lerp = xs[x].lerp

                    top_y_index = ys[y].lower
                    bottom_y_index = ys[y].upper
                    ys_lerp = ys[y].lerp

                    top_left = input_data[b][top_y_index][left_x_index][c]
                    top_right = input_data[b][top_y_index][right_x_index][c]
                    bottom_left = input_data[b][bottom_y_index][left_x_index][c]
                    bottom_right = input_data[b][bottom_y_index][right_x_index][c]

                    output[b][y][x][c] = compute_lerp(top_left, top_right, bottom_left, bottom_right, xs_lerp, ys_lerp)
    return output


import os
import csv
from scipy import integrate
import matplotlib.pyplot as plt

DEPTHS = [-1.0 * i for i in [0, 6, 19.7, 28.9, 36.4, 43.88, 51.34, 58.81, 66.36, 73.94, 81.5, 88.97, 96.45, 103.93, 111.41,
          118.92, 126.47, 134.01, 141.55, 149.09, 156.64, 164.18, 171.72, 179.27, 186.79, 194.27, 201.75,
          209.23, 216.71, 224.09, 231.4, 238.7, 246.01, 253.31, 260.62, 267.9, 275.16, 282.42, 289.68,
          296.94, 304.19, 311.41, 318.44, 325.47, 332.5, 339.53, 346.56, 353.59, 360.62, 367.66, 374.69,
          381.72, 388.75, 395.78, 402.78, 409.72, 416.67, 423.61, 430.56, 437.5, 444.44, 451.32, 457.89,
          464.47, 471.05, 477.63, 484.21, 490.79, 497.37, 503.75, 510, 516.25, 522.5, 528.75, 535, 541.25,
          547.5, 553.95, 560.53, 567.11, 573.68]]

class Inspect:

    def __init__(self, reg_path, depleted_path):
        super().__init__()
        self.reg_path = reg_path
        self.depleted_path = depleted_path
        self.adibekyan_bsp_1200 = None
        self.adibekyan_bsp_1400 = None
        self.adibekyan_bsp_1600 = None
        self.adibekyan_depleted_bsp_f1200_1200 = None
        self.adibekyan_depleted_bsp_f1400_1200 = None
        self.adibekyan_depleted_bsp_f1400_1400 = None
        self.adibekyan_depleted_bsp_f1600_1200 = None
        self.adibekyan_depleted_bsp_f1600_1400 = None
        self.adibekyan_depleted_bsp_f1600_1600 = None
        self.adibekyan_morb_f1200_1200 = None
        self.adibekyan_morb_f1400_1200 = None
        self.adibekyan_morb_f1400_1400 = None
        self.adibekyan_morb_f1600_1200 = None
        self.adibekyan_morb_f1600_1400 = None
        self.adibekyan_morb_f1600_1600 = None
        self.kepler_bsp_1200 = None
        self.kepler_bsp_1400 = None
        self.kepler_bsp_1600 = None
        self.kepler_depleted_bsp_f1200_1200 = None
        self.kepler_depleted_bsp_f1400_1200 = None
        self.kepler_depleted_bsp_f1400_1400 = None
        self.kepler_depleted_bsp_f1600_1200 = None
        self.kepler_depleted_bsp_f1600_1400 = None
        self.kepler_depleted_bsp_f1600_1600 = None
        self.kepler_morb_f1200_1200 = None
        self.kepler_morb_f1400_1200 = None
        self.kepler_morb_f1400_1400 = None
        self.kepler_morb_f1600_1200 = None
        self.kepler_morb_f1600_1400 = None
        self.kepler_morb_f1600_1600 = None

        self.__getfiles()

    def __return_df(self, f):
        return list(csv.reader(open(f, 'r'), delimiter=","))

    def __getfiles(self):
        files = []
        fnames = []
        for dirname, dirnames, f in os.walk(self.reg_path):
            for i in f:
                files.append(dirname + "/" + i)
                fnames.append(i)
        for index, i in enumerate(fnames):
            f = files[index]
            if "Adibekyan" in i and "BSP" in i and "1200" in i:
                self.adibekyan_bsp_1200 = self.__return_df(f=f)
            elif "Adibekyan" in i and "BSP" in i and "1400" in i:
                self.adibekyan_bsp_1400 = self.__return_df(f=f)
            elif "Adibekyan" in i and "BSP" in i and "1600" in i:
                self.adibekyan_bsp_1600 = self.__return_df(f=f)
            elif "Adibekyan" in i and "MORB" in i and "_F1200" in i and "_1200" in i:
                self.adibekyan_morb_f1200_1200 = self.__return_df(f=f)
            elif "Adibekyan" in i and "MORB" in i and "_F1400" in i and "_1200" in i:
                self.adibekyan_morb_f1400_1200 = self.__return_df(f=f)
            elif "Adibekyan" in i and "MORB" in i and "_F1400" in i and "_1400" in i:
                self.adibekyan_morb_f1400_1400 = self.__return_df(f=f)
            elif "Adibekyan" in i and "MORB" in i and "_F1600" in i and "_1200" in i:
                self.adibekyan_morb_f1600_1200 = self.__return_df(f=f)
            elif "Adibekyan" in i and "MORB" in i and "_F1600" in i and "_1400" in i:
                self.adibekyan_morb_f1600_1400 = self.__return_df(f=f)
            elif "Adibekyan" in i and "MORB" in i and "_F1600" in i and "_1600" in i:
                self.adibekyan_morb_f1600_1600 = self.__return_df(f=f)
            elif "Kepler" in i and "BSP" in i and "1200" in i:
                self.kepler_bsp_1200 = self.__return_df(f=f)
            elif "Kepler" in i and "BSP" in i and "1400" in i:
                self.kepler_bsp_1400 = self.__return_df(f=f)
            elif "Kepler" in i and "BSP" in i and "1600" in i:
                self.kepler_bsp_1600 = self.__return_df(f=f)
            elif "Kepler" in i and "MORB" in i and "_F1200" in i and "_1200" in i:
                self.kepler_morb_f1200_1200 = self.__return_df(f=f)
            elif "Kepler" in i and "MORB" in i and "_F1400" in i and "_1200" in i:
                self.kepler_morb_f1400_1200 = self.__return_df(f=f)
            elif "Kepler" in i and "MORB" in i and "_F1400" in i and "_1400" in i:
                self.kepler_morb_f1400_1400 = self.__return_df(f=f)
            elif "Kepler" in i and "MORB" in i and "_F1600" in i and "_1200" in i:
                self.kepler_morb_f1600_1200 = self.__return_df(f=f)
            elif "Kepler" in i and "MORB" in i and "_F1600" in i and "_1400" in i:
                self.kepler_morb_f1600_1400 = self.__return_df(f=f)
            elif "Kepler" in i and "MORB" in i and "_F1600" in i and "_1600" in i:
                self.kepler_morb_f1600_1600 = self.__return_df(f=f)

        files = []
        fnames = []
        for dirname, dirnames, f in os.walk(self.depleted_path):
            for i in f:
                files.append(dirname + "/" + i)
                fnames.append(i)
        for index, i in enumerate(fnames):
            f = files[index]
            if "Adibekyan" in i and "F1200" in i and "_1200" in i:
                self.adibekyan_depleted_bsp_f1200_1200 = self.__return_df(f=f)
            elif "Adibekyan" in i and "F1400" in i and "_1200" in i:
                self.adibekyan_depleted_bsp_f1400_1200 = self.__return_df(f=f)
            elif "Adibekyan" in i and "F1400" in i and "_1400" in i:
                self.adibekyan_depleted_bsp_f1400_1400 = self.__return_df(f=f)
            elif "Adibekyan" in i and "F1600" in i and "_1200" in i:
                self.adibekyan_depleted_bsp_f1600_1200 = self.__return_df(f=f)
            elif "Adibekyan" in i and "F1600" in i and "_1400" in i:
                self.adibekyan_depleted_bsp_f1600_1400 = self.__return_df(f=f)
            elif "Adibekyan" in i and "F1600" in i and "_1600" in i:
                self.adibekyan_depleted_bsp_f1600_1600 = self.__return_df(f=f)
            elif "Kepler" in i and "F1200" in i and "_1200" in i:
                self.kepler_depleted_bsp_f1200_1200 = self.__return_df(f=f)
            elif "Kepler" in i and "F1400" in i and "_1200" in i:
                self.kepler_depleted_bsp_f1400_1200 = self.__return_df(f=f)
            elif "Kepler" in i and "F1400" in i and "_1400" in i:
                self.kepler_depleted_bsp_f1400_1400 = self.__return_df(f=f)
            elif "Kepler" in i and "F1600" in i and "_1200" in i:
                self.kepler_depleted_bsp_f1600_1200 = self.__return_df(f=f)
            elif "Kepler" in i and "F1600" in i and "_1400" in i:
                self.kepler_depleted_bsp_f1600_1400 = self.__return_df(f=f)
            elif "Kepler" in i and "F1600" in i and "_1600" in i:
                self.kepler_depleted_bsp_f1600_1600 = self.__return_df(f=f)

    def __get_morb_row(self, star, morb_file):
        for row in morb_file:
            if row[0] == star:
                return row
        return []

    def __get_earth_row(self, file):
        for row in file:
            if row[0].lower() == "sun":
                return row

    def __compute_buoyancy_at_depth(self, density_differentials, plate_thickness=10 * 1000, gravity=9.8):
        buoyancies = []
        for index, i in enumerate(density_differentials):
            if index < len(density_differentials) - 1:
                sublist_density_diffs = density_differentials[0:index + 1]
                d = DEPTHS[0:index + 1]
                buoyancy_force = integrate.simps(sublist_density_diffs, d) * 1000 * 1000 * plate_thickness * gravity
                buoyancies.append(buoyancy_force)
            else:
                buoyancy_force = integrate.simps(density_differentials,
                                                 DEPTHS) * 1000 * 1000 * plate_thickness * gravity
                buoyancies.append(buoyancy_force)
        return buoyancies

    def get_buoyancy(self, bsp_file, morb_file):
        buoyancies = {}

        for bsp_row in bsp_file:
            star = bsp_row[0]
            morb_row = self.__get_morb_row(star=star, morb_file=morb_file)
            if len(bsp_row[1:]) == len(morb_row[1:]) == len(DEPTHS):
                densitity_differnces = [float(x) - float(y) for x, y in zip(morb_row[1:], bsp_row[1:])]
                buoyancy_force = self.__compute_buoyancy_at_depth(density_differentials=densitity_differnces)
                buoyancies.update({star: buoyancy_force})

        return buoyancies


from random import randint, randrange, choice, shuffle, uniform
import time
import keyboard
import numpy as np

#======CONSTANTS======#

WHOLESALE_PRICE = 70    #Price corps pay for replenishing their product inventory
MAX_INVSPACE = 50       #Max inventory storage a corp has
MARKET_FEE = 50         #Daily cost corps need to pay to say in the market

#=====================#

class Market():
    def __init__(self, consumers = [], corps = [], vacantConsumerIDs = [], vacantCorpIDs = []):
        self.consumers = consumers;
        self.corps = corps;
        self.vacantConsumerIDs = vacantConsumerIDs; #When a consumer exits the market, its ID gets stored here for a new consumer to come and take
        self.vacantCorpIDs = vacantCorpIDs;         #Same as above but for corps

    #Populates the market at the begining of the simulation
    def populate(self):
        #Populate consumers
        for i in range(1, 1001):
            self.consumers.append(Consumer(ID = str(i)))

        #Populate corps
        for i in range(1, 101):
            self.corps.append(Corp(ID = 'Cx' + str(i), price = randint(190, 210)))

    #Repopulates the market
    def repopulate(self):
        #Try to repopulate consumers (60% chance of replenishing per vacant)
        for i in self.vacantConsumerIDs:
            if randint(1, 100) <= 60: self.consumers.append(Consumer(ID = i))

        #Try to repopulate corps (30% chance of replenishing per vacant)
        for i in self.vacantCorpsIDs:
            if randint(1, 100) <= 30: self.corps.append(Corp(ID = i))

    #Checks for broke corps and unsatisfied consumers. Called in self.newDay()
    def update(self):
        #Remove leaving consumers from the market and store their IDs for future costumers
        for consumer in self.consumers:
            consumer.update();
            if not consumer.inMarket:
                self.vacantConsumerIDs.append(consumer.ID);
                self.consumers.remove(consumer);

        #Remove broke corps from the market and store their IDs for future corps
        for corp in self.corps:
            corp.update();
            if not corp.inBusiness:
                self.vacantCorpIDs.append(corp.ID);
                self.corps.remove(corp);

    #Simulates a full day in the market
    def newDay(self):
        #The new day's parameters get set
        shuffle(self.consumers);    #Shuffle the customer list for changing buying order each day

        #Today's simulation begins
        for consumer in self.consumers:
            shuffle(self.corps);            #Shuffle the corps list for changing access for each customer
            for corp in self.corps:
                consumer.searchProduct(corp.price, corp.inventory, corp.ID) #[consumer] searches for a product in [corp]
                if consumer.hasBought:
                    corp.inventory -= 1;    #Consumer has bought, the product gets removed from [corp]'s inventory
                    corp.demand += 1;
                    corp.money += corp.price;
                    break;                  #Consumer ends the day
        self.update()

class Consumer():
    def __init__(self, lastBoughtID = None, ID = 0, hasBought = False, inMarket = True):
        self.ID = ID;                              #Consumer's IDs are numeric
        self.lastBoughtID = lastBoughtID;          #Saves the last corp's ID from which the customer has bought
        self.buyingPrice = randint(5000, 10000);   #Maximum price the consumer will pay for a product
        self.hasBought = hasBought;                #Bool, indicates if the consumer was able to buy something
        self.inMarket = inMarket;                  #Bool, flag var for deleting consumers when they leave the market

    #Iterates through corps in the market searching for a product to buy with a valid price
    def searchProduct(self, price, products, cID):
        if products > 0 and price <= self.buyingPrice:
            self.hasBought = True;
            self.buyingPrice = price;
            self.lastBoughtID = cID;
        else: self.hasBought = False;

    #Checks if consumer has bought, if he hasnt it sets inMarket bool to False for him to exit
    def update(self):
        if not self.hasBought: self.inMarket = False;

class Corp():
    def __init__(self, demand = 0, willInvest = False, money = randint(100000, 200000), ID = 'Cx', price = 0, inventory = MAX_INVSPACE, inBusiness = True, history = []):
        self.ID = ID;               #Corp's IDs are in the form of Cx plus numeric id
        self.money = money;
        self.demand = demand;               #Daily sold products
        self.price = price;                 #Initial price always between (190, 210)
        self.inventory = inventory;         #Products in the corp's storage waiting to be sold (MAX len == 50)
        self.willInvest = willInvest;       #Random var that decides if the corp replenishes inv each day
        self.inBusiness = inBusiness;       #Bool, indicates if the corp is alive marketwise
        self.history = [str(hex(id(self)))] #List with (price, demand) historic pairs, [0] = object hex mem adress, this is to avoid all Corp.history arrays to have the same mem adress

    #Replenishes corp's inventory with new products
    def replenishInv(self):
        if len(self.inventory) <= 5 or self.willInvest:
            self.willInvest = False;
            while self.inventory <= MAX_INVSPACE and self.money >= WHOLESALE_PRICE:
                inventory += 1;
                self.money -= WHOLESALE_PRICE;

    #Sets the random bool that determines random inventory replenishing
    def setInvestment(self):
        if randint(1, 100) <= 25: self.willInvest = True;

    #Algorythm for price adjustment simulation, only called if we have 2 data tuples to start working with
    def setPrice(self):
        '''
        try:
            #1st: Find q(p) by solving a system of linear equations, this is done by understanding q(p) function as matrixes in the form of AX=B
            A = np.array([[1, self.history[-2][0]], [1, self.history[-1][0]]]);
            B = np.array([self.history[-2][1], self.history[-1][1]]);
            X = np.linalg.solve(A, B);                   #RETURNS -> (n, m) of q(p) = m*p + n so: q(p) = X[1]*p + X[0]

            #2nd: Calculate price flexibility epsilon (e)
            try:
                q = int(X[1])*self.history[-1][0]+int(X[0])
                q_prime = int(X[1])
                e = (self.history[-1][0]/q)*q_prime
            except ZeroDivisionError:
                print(f"ID: {self.ID}")
                print(f"Last 2 historic pairs: {self.history[-1]}{self.history[-2]}")
                e = uniform(0.5, 1.5);

        except np.linalg.LinAlgError:   e = uniform(0.5, 1.5);

        #3rd: Compare abs(e) with one to determine what to do with the price
        if abs(e) < 1:      self.price += int(20 * (1-e))
        elif abs(e) > 1:    self.price -= int(20 * (e-1))
        else:               pass
        '''

    #Pays market fee, checks if corp has gone bankrupt and records day's (pi, qi) pairs
    #Called in Market.update()
    def update(self):
        #print(f"BONKED corp.update(), history memdir = {hex(id(self.history))} !!!: ID: {self.ID}")
        if type(self.history[0]) is str: self.history.pop(0);  #Delete mem.access hex
        if len(self.history) >= 2: self.setPrice();            #Adjust price when enough historic data is available
        self.history.append((self.price, self.demand))
        self.money -= MARKET_FEE;                              #Corp pays the daily market fee
        if self.money < 0: self.inBusiness = False;            #Checks if corp went bankrupt, if so its kicked out

#===============SIMULATION {MAIN} CONTROL PROCESS================#

market = Market();
market.populate();
isRunning = True;
day = 0

debugTags =['Cx1', 'Cx2', 'Cx3', '1', '2', '3']

while isRunning:
    #check escape input
    try:
        pass;
        #if keyboard.is_pressed("q"): isRunning = False;
    except: pass;
    #Simulate new day is called
    market.newDay();
    day += 1
    #newDay not simulated until space is pressed for debbuging purposes
    print(f"=========DAY {day}=========")
    print("")
    print("CORPS (only 3 first by id): ")
    print("")
    for i in range(0, len(market.corps)):
        if market.corps[i].ID in debugTags:
            print(f"ID: {market.corps[i].ID}")
            print(f"Money: {market.corps[i].money}")
            print(f"Price: {market.corps[i].price}")
            print(f"Demand: {market.corps[i].demand}")
            print(f"Stock: {market.corps[i].inventory}/{MAX_INVSPACE}")
            print(f"Historic price/demand: {market.corps[i].history}")
            print(f"Number of corps: {len(market.corps)}")
            print("")

    print("CONSUMERS (only 3 first by id): ")
    print("")
    for i in range(0, len(market.consumers)):
        if market.consumers[i].ID in debugTags:
            print(f"ID: {market.consumers[i].ID}")
            print(f"MaxPrice: {market.consumers[i].buyingPrice}")
            print(f"HasBought?: {market.consumers[i].hasBought}")
            print(f"LastBoughtAt: Corp {market.consumers[i].lastBoughtID}")
            print(f"TotConsumers: {len(market.consumers)}")
            print("")
    while True:
        debugFlag = input("Input any key to continue: ");
        if debugFlag == 'q': raise ValueError("Ending simulation...");
        if debugFlag or not debugFlag: break;
    #time.sleep(3)       #Wait 3 seconds between simulated days for reviewing and debugging purpose

#================TO - DO================#

#Algorythm for corp price adjustment in response to products sold More products == Higher prices and vicev.
#ALGORYHM FORMULA: R(p) = p * q(p)   ||| p = price | q(p) = demand function | R(p) = Revenue |||[Maximize R]
#d(p) estimated from historical sales data composed by pairs of the type (pi, qi)
#   ||| pi = i(th) day's price | qi = i(th) day's demand|||
#WE NEED AT LEAST 2 PAIRS (pi, qi) FOR THE ALGORYTHM TO WORK WITH
#
#Determine the price profitability with PRICE ELASTICITY OF DEMAND: e = (p/q)*(dq/dp) || e = p/q(p) * q'(p)
#|e| > 1, the price is ELASTIC      (%∆Q > %∆P)     => DECREASE PRICE
#|e| < 1, the price is INELASTIC    (%∆Q < %∆P)     => INCREASE PRICE
#|e| = 1, the price is UNIT ELASTIC (%∆Q = %∆P)     => MANTAIN PRICE  (price is optimal)

#

#LINKS:
#
#https://math.ucr.edu/~joselg/teachingS13/elasticity.pdf
#https://stackabuse.com/solving-systems-of-linear-equations-with-pythons-numpy/
#https://www.geeksforgeeks.org/scipy-curve-fitting/ !!! -> IMPORTANT, q(p) IS NOT A LINE, ITS A CURVE, OBTAIN IT WITH THIS AND USING ALL HISTORIC PAIR DATA AVAILABLE