text: # -*- coding: utf-8 -*-
  """
  .. module:: pytfa
     :platform: Unix, Windows
     :synopsis: Thermodynamics-based Flux Analysis
  
  .. moduleauthor:: pyTFA team
  
  Variable declarations
  
  """
  from abc import ABCMeta
  
  from distutils.version import LooseVersion
  from optlang import __version__ as OPTLANG_VER
  
  from ..utils.str import camel2underscores
  from .meta import ABCRequirePrefixMeta
  
  from warnings import warn
  
  
  op_replace_dict = {
      ' + ': '_ADD_',
      ' - ': '_SUB_',
      ' * ': '_MUL_',
      ' / ': '_DIV_',
  }
  
  ###################################################
  ###                  VARIABLES                  ###
  ###################################################
  
  class GenericVariable(metaclass=ABCRequirePrefixMeta):
      """
      Class to represent a generic variable. The purpose is that the interface
      is instantiated on initialization, to follow the type of interface used
      by the problem, and avoid incompatibilities in optlang
  
      Attributes:
  
          :id: Used for DictList comprehension. Usually points back at a
          enzyme or reaction id for ease of linking. Should be unique given
          a variable type.
          :name: Should be a concatenation of the id and a prefix that is
          specific to the variable type. will be used to address the variable at
          the solver level, and hence should be unique in the whole cobra_model
          :cobra_model: the cobra_model hook.
          :variable: links directly to the cobra_model representation of tbe variable
      """
      prefix = NotImplemented
  
      @property
      def __attrname__(self):
          """
          Name the attribute the instances will have
          Example: GenericVariable -> generic_variable
          :return:
          """
          return camel2underscores(self.__class__.__name__)
  
      def __init__(self, id_='', model=None, hook=None, queue=False, scaling_factor=1,
                   **kwargs):
          """
  
          :param id_: will be used to identify the variable
              (name will be a concat of this and a prefix)
          :param model: the cobra.Model object
          :param queue: whether or not to queue the variable for update object
          :param scaling_factor: scaling factor used in self.scaled, useful for
                                  adimensionalisation of constraints
          :param kwargs: stuff you want to pass to the variable constructor
          """
          self.hook = hook
          self._id = id_
          self._model = model
          self.kwargs = kwargs
          self._name = self.make_name()
          self.get_interface(queue)
          self.prefix = ''
          self._scaling_factor = scaling_factor
  
      def get_interface(self, queue):
          """
          Called upon completion of __init__, initializes the value of self.var,
          which is returned upon call, and stores the actual interfaced variable.
  
          :return: instance of Variable from the problem
          """
  
          if not self.name in self.model.variables:
              variable = self.model.problem.Variable(name = self.name, **self.kwargs)
              if not queue:
                  self.model.add_cons_vars(variable, sloppy=self.model.sloppy)
              else:
                  self.model._var_queue.append(variable)
          else:
              self.variable = self.model.variables.get(self.name)
  
      def make_name(self):
          """
          Needs to be overridden by the subclass, concats the id with a
           prefix
  
          :return: None
          """
          return self.prefix + self.id
  
      @property
      def name(self):
          return self._name
  
      @name.setter
      def name(self, value):
          self._name = value
  
      @property
      def id(self):
          """
          for cobra.thermo.DictList compatibility
          :return:
          """
          return self._id
  
      @property
      def variable(self):
          return self.model.variables[self.name]
  
      @variable.setter
      def variable(self,value):
          self.model.variables[self.name] = value
  
      @property
      def scaling_factor(self):
          return self._scaling_factor
  
      @property
      def unscaled(self):
          """
          If the scaling factor of quantity X is a, it is represented by
          the variable X_hat = X/a.
          This returns X = a.X_hat
          Useful for nondimensionalisation of variables and constraints
  
          :return: The variable divided by its scaling factor
          """
          return self.scaling_factor * self
  
      @property
      def value(self):
          try:
              return self.model.solution.raw[self.name]
          except AttributeError:
              warn('Model need to be optimized to get a value for this variable')
  
      @property
      def unscaled_value(self):
          try:
              return self.scaling_factor * self.value
          except AttributeError:
              warn('Model need to be optimized to get a value for this variable')
  
      @property
      def model(self):
          return self._model
      #
      # @variable.setter
      # def variable(self, value):
      #     self._variable = value
  
      @property
      def type(self):
          return self.variable.type
  
      def test_consistency(self, other):
          """
          Tests whether a candidate to an operation is of the right type and is
          from the same problem
  
          :param other: an object
          :return: None
          """
  
          assert (isinstance(other, GenericVariable))
          # assert (other.problem == self.problem)
  
      def get_operand(self, other):
          """
          For operations, choose if the operand is a GenericVariable, in which
          we return its optlang variable, or something else (presumably a numeric)
          and we let optlang decide what to do
  
          :param other:
          :return:
          """
          if isinstance(other, GenericVariable):
              return other.variable
          else:  # Let optlang decide what to do
              return other
  
      #########################################################################
      # We redefine all the following operations to return the equivalent     #
      # expression one would get by applying the said operation to the        #
      # self.variable attribute                                               #
      #########################################################################
  
      def __add__(self, other):
          """
          Adding either two variables together or a variable and a numeric
          results in a new variable
          :param other:
          :return: a new Generic Variable
          """
          operand = self.get_operand(other)
  
          new_variable = self.variable + operand
  
          return self.make_result(new_variable)
  
      def __radd__(self, other):
          """
          Take priority on symmetric arithmetic operation
          :param other:
          :return:
          """
  
          return self.__add__(other)
  
      def __sub__(self, other):
          """
          Substracting either two variables together or a variable and a numeric
          results in a new variable
          :param other:
          :return: a new Generic Variable
          """
          operand = self.get_operand(other)
  
          new_variable = self.variable - operand
  
          return self.make_result(new_variable)
  
      def __rsub__(self, other):
          """
          Take priority on symmetric arithmetic operation
          :param other:
          :return:
          """
  
          operand = self.get_operand(other)
  
          new_variable = operand - self.variable
  
          return self.make_result(new_variable)
  
      def __mul__(self, other):
          """
          Multiplying either two variables together or a variable and a numeric
          results in a new variable
          :param other:
          :return: a new Generic Variable
          """
          operand = self.get_operand(other)
  
          new_variable = self.variable * operand
  
          return self.make_result(new_variable)
  
      def __rmul__(self, other):
          """
          Take priority on symmetric arithmetic operation
          :param other:
          :return:
          """
          return self.__mul__(other)
  
      def __truediv__(self, other):
          """
          Dividing either two variables together or a variable and a numeric
          results in a new variable
          :param other:
          :return: a new Generic Variable
          """
          operand = self.get_operand(other)
  
          new_variable = self.variable / operand
  
          return self.make_result(new_variable)
  
      def __rtruediv__(self, other):
          """
          Take priority on symmetric arithmetic operation
          :param other:
          :return:
          """
  
          operand = self.get_operand(other)
  
          new_variable = operand / self.variable
  
          return self.make_result(new_variable)
  
  
      def make_result(self, new_variable):
          """
          Returns a Sympy expression
          :param new_variable:
          :return:
          """
  
          # if isinstance(new_variable, self.cobra_model.problem.Variable):
          #     new_name = new_variable.name
          # else: #It is an expression
          #     new_name  = new_variable.__str__()
          #     for k,v in op_replace_dict.items():
          #         new_name = new_name.replace(k,v)
  
          # result = GenericVariable(new_name, self.cobra_model)
          # result.variable = new_variable
  
          # return result
          return new_variable
  
      def __repr__(self):
          return self.variable.__repr__()
  
  def get_binary_type():
      """
      FIX : We enforce type to be integer instead of binary, else optlang does
      not allow to set the binary variable bounds to anything other than (0,1)
      You might want to set it at (0,0) to enforce directionality for example
      """
      if LooseVersion(OPTLANG_VER) < LooseVersion('1.2.3'):
          return 'integer'
      else:
          return 'binary'
  
  class ModelVariable(GenericVariable):
      """
      Class to represent a variable attached to the model
      """
  
      def __init__(self, model, id_, **kwargs):
          if not 'lb' in kwargs:
              kwargs['lb'] = 0
          GenericVariable.__init__(self,
                                   id_= id_,
                                   model=model,
                                   hook=model,
                                   **kwargs)
      prefix = 'MODV_'
  
  class GeneVariable(GenericVariable):
      """
      Class to represent a gene variable
      """
  
      prefix = 'GV_'
  
      def __init__(self, gene, **kwargs):
          model = gene.model
  
  
          if not 'lb' in kwargs:
              kwargs['lb'] = 0
          if not 'ub' in kwargs:
              pass
  
          GenericVariable.__init__(self, model=model, hook=gene,
                                   **kwargs)
  
      @property
      def gene(self):
          return self.hook
  
      @property
      def id(self):
          return self.gene.id
  
      @property
      def model(self):
          return self.gene.model
  
  class BinaryVariable(GenericVariable):
      """
      Class to represent a generic binary variable
      """
  
      def __init__(self, id_, model, **kwargs):
  
          if not 'lb' in kwargs:
              kwargs['lb'] = 0
          if not 'ub' in kwargs:
              kwargs['ub'] = 1
  
          GenericVariable.__init__(self,
                                   id_,
                                   model = model,
                                   type=get_binary_type(),
                                   **kwargs)
  
      prefix = 'B_'
  
  class ReactionVariable(GenericVariable):
      """
      Class to represent a variable attached to a reaction
      """
  
      def __init__(self, reaction, **kwargs):
          model = reaction.model
  
          GenericVariable.__init__(self,
                                   model=model,
                                   hook=reaction,
                                   **kwargs)
  
      @property
      def reaction(self):
          return self.hook
  
      @property
      def id(self):
          return self.reaction.id
  
      @property
      def model(self):
          return self.reaction.model
  
      prefix = 'RV_'
  
  class MetaboliteVariable(GenericVariable):
      """
      Class to represent a variable attached to a enzyme
      """
  
      def __init__(self, metabolite, **kwargs):
          model = metabolite.model
  
          GenericVariable.__init__(self,
                                   model=model,
                                   hook=metabolite,
                                   **kwargs)
  
      @property
      def metabolite(self):
          return self.hook
  
      @property
      def id(self):
          return self.metabolite.id
  
      @property
      def model(self):
          return self.metabolite.model
  
      prefix = 'MV_'
  
  class ForwardUseVariable(ReactionVariable, BinaryVariable):
      """
      Class to represent a forward use variable, a type of binary variable used to
      enforce forward directionality in reaction net fluxes
      """
  
      def __init__(self, reaction, **kwargs):
          ReactionVariable.__init__(self, reaction,
                                    type=get_binary_type(),
                                    **kwargs)
  
      prefix = 'FU_'
  
  class BackwardUseVariable(ReactionVariable, BinaryVariable):
      """
      Class to represent a backward use variable, a type of binary variable used
      to enforce backward directionality in reaction net fluxes
      """
  
      def __init__(self, reaction, **kwargs):
          if not 'lb' in kwargs:
              kwargs['lb'] = 0
          if not 'ub' in kwargs:
              kwargs['ub'] = 1
  
          ReactionVariable.__init__(self, reaction,
                                    type=get_binary_type(),
                                    **kwargs)
  
      prefix = 'BU_'
  
  class ForwardBackwardUseVariable(ReactionVariable, BinaryVariable):
      """
      Class to represent a type of binary variable used to tell whether the
      reaction is active or not such that:
          FU + BU + BFUSE = 1
      """
  
      def __init__(self, reaction, **kwargs):
          if not 'lb' in kwargs:
              kwargs['lb'] = 0
          if not 'ub' in kwargs:
              kwargs['ub'] = 1
  
          ReactionVariable.__init__(self, reaction,
                                    type=get_binary_type(),
                                    **kwargs)
  
      prefix = 'BFUSE_'
  
  class LogConcentration(MetaboliteVariable):
      """
      Class to represent a log concentration of a enzyme
      """
  
      prefix = 'LC_'
  
  class DeltaGErr(ReactionVariable):
      """
      Class to represent a DeltaGErr
      """
  
      prefix = 'DGE_'
  
  class DeltaG(ReactionVariable):
      """
      Class to represent a DeltaG
      """
  
      prefix = 'DG_'
  
  class DeltaGstd(ReactionVariable):
      """
      Class to represent a DeltaG^o (naught) - standard conditions
      """
  
      prefix = 'DGo_'
  
  class ThermoDisplacement(ReactionVariable):
      """
      Class to represent the thermodynamic displacement of a reaction
      \Gamma = -\DeltaG/RT
      """
  
      prefix = 'LnGamma_'
  
  class PosSlackVariable(ReactionVariable):
      """
      Class to represent a positive slack variable for relaxation problems
      """
  
      def __init__(self,reaction,**kwargs):
          ReactionVariable.__init__(self, reaction, **kwargs)
  
      prefix = 'PosSlack_'
  
  class NegSlackVariable(ReactionVariable):
      """
      Class to represent a negative slack variable for relaxation problems
      """
  
      def __init__(self,reaction,**kwargs):
          ReactionVariable.__init__(self, reaction, **kwargs)
  
      prefix = 'NegSlack_'
  
  class PosSlackLC(MetaboliteVariable):
  
      prefix = 'PosSlackLC_'
  
  class NegSlackLC(MetaboliteVariable):
  
      prefix = 'NegSlackLC_'
  
  class LinearizationVariable(ModelVariable):
      """
      Class to represent a product A*B when performing linearization of the
      model
      """
      prefix = 'LZ_'
  