aqequil.MassTransfer API documentation

# Module `aqequil.MassTransfer`

## Functions

`def join_mixes(m1, m2)`
  Expand source code

      def join_mixes(m1, m2):
          """
          Join the results of two mixes, m1 and m2, so that results extend from
          1:0 to 0:1 m1:m2.

          Parameters
          ----------
          m1, m2 : objects of class Mass_Transfer
              The mass transfer results of two mixing calculations

          Returns
          ----------
          An object of class `Mass_Transfer` with results joined for plotting.
          """

          # tables belonging to the Mass Transfer class that are used by its plotting functions
          tabs = ["misc_params", "basis_logact", "dissolved_elements_molal", "dissolved_elements_ppm",
                  "aq_distribution_logact", "aq_distribution_molal", "aq_distribution_logmolal",
                  "moles_minerals", "moles_product_minerals"]

          for tab in tabs:
              m2_reverse_rows = getattr(m2, tab).iloc[::-1]
              m2_reverse_rows["Xi"] = [1+(1-float(xi)) for xi in m2_reverse_rows["Xi"]]
              m2_reverse_rows = m2_reverse_rows[1:]
              m1_rows = getattr(m1, tab)
              setattr(m1, tab, pd.concat([m1_rows, m2_reverse_rows]).reset_index(drop=True))

          # do the same but for all EQ6 output tables
          if m1.tab != None and m2.tab != None:
              for tab in m1.tab.keys():
                  if tab in m2.tab.keys():
                      m2_reverse_rows = m2.tab[tab].iloc[::-1]
                      m2_reverse_rows["Xi"] = [1+(1-float(xi)) for xi in m2_reverse_rows["Xi"]]
                      m2_reverse_rows = m2_reverse_rows[1:]
                      m1_rows = m1.tab[tab]
                      m1.tab[tab] = pd.concat([m1_rows, m2_reverse_rows]).reset_index(drop=True)

          # do the same but for mass contribution tables
          for tab in m1.mass_contribution_dict.keys():
              if tab in m2.mass_contribution_dict.keys():
                  m2_reverse_rows = m2.mass_contribution_dict[tab].iloc[::-1]
                  m2_reverse_rows["Xi"] = [1+(1-float(xi)) for xi in m2_reverse_rows["Xi"]]
                  m2_reverse_rows = m2_reverse_rows[1:]
                  m1_rows = m1.mass_contribution_dict[tab]
                  m1.mass_contribution_dict[tab] = pd.concat([m1_rows, m2_reverse_rows]).reset_index(drop=True)

          return m1

  Join the results of two mixes, m1 and m2, so that results extend from 1:0 to 0:1 m1:m2.

  ## Parameters

  `m1`, `m2` :`objects` of `class Mass_Transfer`
    The mass transfer results of two mixing calculations

  ## Returns

  An object of class `Mass_Transfer` with results joined for plotting.

`def react(speciation,
reaction_setup,
chain_mt=False,
delete_generated_folders=False,
hide_traceback=True,
data1_override=None,
format_element_names=True,
eq36da=None,
eq36co=None,
verbose=1)`
  Expand source code

      def react(speciation,
                reaction_setup,
                chain_mt=False,
                delete_generated_folders=False,
                hide_traceback=True,
                data1_override=None,
                format_element_names=True,
                eq36da=None,
                eq36co=None,
                verbose=1,
               ):

          """
          Calculate how speciated water reacts with minerals and/or gases.

          Parameters
          ----------
          speciation : Speciation object
              The output of a speciation calculation produced by the
              AqEquil.speciate function.

          reaction_setup : str or Prepare_Reaction object
              Defines how the reaction is to be set up. There are two ways to set up
              a reaction. The first way is to prepare the reaction with
              `Prepare_Reaction`. The second way is to prepare the first half of an
              EQ6 6i file (the part without the contents of the pickup file) and then
              pass the filename to `reaction_setup`.

          chain_mt : bool, default False
              Is the speciation the result of another mass transfer calculation?
              Choosing True will allow mass transfer calculations to be chained
              together.

          delete_generated_folders : bool, default False
              Delete the 'rxn_6i', 'rxn_6o', 'rxn_6p', and 'eq6_extra_out' folders
              containing raw EQ6 input, output, and pickup files once the
              reaction calculation is complete?

          hide_traceback : bool, default True
              Hide traceback message when encountering errors handled by this class?
              When True, error messages handled by this class will be short and to
              the point.

          data1_override : str, optional
              The three letter code of a data1 file used to override the thermodynamic
              database used to speciate the sample(s). This is useful for chaining
              the results of one mass transfer calculation into another while
              simultaneously changing the temperature and pressure regime of the
              new system. See the description for the `chain_mt` parameter for more
              about chaining.

          format_element_names : bool, default True
              Format the chemical symbols of redox-isolated elements generated when
              elements are supplied to the `suppress_redox` parameter of
              `AqEquil.speciate`? Context: when elements are redox-isolated, new
              elements are designated representing different oxidations states in
              order to prevent redox reactions from occuring during calculations. The
              names of these elements appear like "Fejiip", where the first one or two
              letters represent the element (e.g. "Fe"), "j" denotes that the
              chemical symbol is finished, followed by roman numerals representing the
              magnitude of the charge (e.g., "ii" representing 2), finally followed by
              a letter that denotes the sign of the charge (e.g., "p" for positive).
              By setting this parameter to True, the element "Fejiip" will be
              converted into a more readable form, "Fe+2", in all figures and tables
              generated from calculation results. It may be useful to set this
              parameter to False if you are supplying your own data0 file containing
              custom pseudoelements that do not follow the convention used by AqEquil.

          eq36da : str, defaults to path given by the environment variable EQ36DA
              Path to directory where data1 files are stored.

          eq36co : str, defaults to path given by the environment variable EQ36CO
              Path to directory where EQ3 executables are stored.

          Returns
          ----------
          An object of class `Speciation` modified with the results of the reaction
          (if a thermodynamic database formatted as a WORM-style CSV was used during
          speciation) or an unmodified speciation object (if a data0 or data1 file
          was used during speciation).
          """

          if not isinstance(eq36da, str):
              eq36da = os.environ.get('EQ36DA')
          if not isinstance(eq36co, str):
              eq36co = os.environ.get('EQ36CO')

          prev_wd = os.getcwd()
          ae = AqEquil(load_thermo=False, eq36co=eq36co, eq36da=eq36da, verbose=verbose)

          speciation.join_6i_p(reaction_setup, chain_mt)
          __delete_file("data1.dyn")

          paths=['rxn_6o', 'rxn_6p', 'eq6_extra_out']
          for path in paths:
              if not os.path.exists(path):
                  os.makedirs(path)
              else:
                  shutil.rmtree(path)
                  os.makedirs(path)

          for sample_name in list(speciation.sample_data.keys()):
              filename_6i = speciation.sample_data[sample_name]["filename"][:-3]+".6i"
              filename_6o = filename_6i[:-3]+".6o"
              filename_6p = filename_6i[:-3]+".6p"

              if data1_override != None:
                  with open("data1."+data1_override, mode='rb') as data1:
                      speciation.data1["all_samples"] = data1.read()
                  speciation.thermo.thermo_db_filename = "data1."+data1_override

              if "all_samples" not in speciation.data1.keys():
                  # each sample has a unique data1. e.g., with dynamic_db
                  __delete_file("eq6_extra_out/data1.dyn")
                  with open("eq6_extra_out/data1.dyn", 'wb') as f:
                      f.write(speciation.data1[speciation.sample_data[sample_name]["filename"][:-3]])

              else:
                  # all samples use the same data1.
                  with open("eq6_extra_out/data1.dyn", 'wb') as f:
                      f.write(speciation.data1["all_samples"])

              path_6i="rxn_6i/"
              path_6o="rxn_6o"
              path_6p="rxn_6p"
              path_extra_out="eq6_extra_out"

              ae.runeq6(filename_6i,
                        db="dyn",
                        path_6i=path_6i,
                        data1_path=os.getcwd()+"/eq6_extra_out", # ensuring data1 is read from a folder without spaces overcomes the problem where environment variables with spaces do not work properly when assigned to EQ36DA
                        dynamic_db_name=speciation.thermo.thermo_db_filename)

              # get current working dir
              cwd = os.getcwd()
              cwdd = cwd + "/"

              filename_6o = filename_6i[:-1] + 'o'
              filename_6p = filename_6i[:-1] + 'p'
              filename_6ba = filename_6i[:-1] + 'ba'
              filename_6bb = filename_6i[:-1] + 'bb'
              filename_6t = filename_6i[:-2] + 'csv'
              filename_6tx = filename_6i[:-1] + 'tx'

              # The new eq36 build truncates names, e.g., MLS.Source.3i creates MLS.3o
              # Correct for this here:
              files_6o = [file for file in os.listdir(cwdd+path_6i) if file[-3:] == ".6o"]
              files_6p = [file for file in os.listdir(cwdd+path_6i) if file[-3:] == ".6p"]
              files_6ba = [file for file in os.listdir(cwdd+path_6i) if file[-4:] == ".6ba"]
              files_6bb = [file for file in os.listdir(cwdd+path_6i) if file[-4:] == ".6bb"]
              files_6t = [file for file in os.listdir(cwdd+path_6i) if file[-3:] == ".6t"]
              files_6tx = [file for file in os.listdir(cwdd+path_6i) if file[-4:] == ".6tx"]

              if len(files_6o) == 0:
                  if ae.verbose > 0:
                      print('Error: EQ6 failed to produce output for ' + filename_6i)
              elif len(files_6o) == 1:
                  file_6o = files_6o[0]
                  file_6ba = files_6ba[0]
                  file_6bb = files_6bb[0]
                  try:
                      file_6t = files_6t[0]
                      file_6tx = files_6tx[0]
                  except:
                      pass

                  try:
                      # report errors in output
                      with open(cwdd+path_6i+"/"+file_6o) as file:
                          lines = [line.rstrip() for line in file]
                      EQ6_errors_found = ae._report_3o_6o_errors(lines, sample_name)
                  except:
                      msg = ("Error: could not open "+path_6i+file_6o+" or there "
                            "is something wrong with EQ3/6 error reporting.")
                      ae.err_handler.raise_exception(msg)

                  try:
                      # move output
                      shutil.move(cwdd+path_6i+"/"+file_6o, cwdd+path_6o+"/"+filename_6o)
                      shutil.move(cwdd+path_6i+"/"+file_6ba, cwdd+path_extra_out+"/"+filename_6ba)
                      shutil.move(cwdd+path_6i+"/"+file_6bb, cwdd+path_extra_out+"/"+filename_6bb)
                      try:
                          shutil.move(cwdd+path_6i+"/"+file_6t, cwdd+path_extra_out+"/"+filename_6t)
                          shutil.move(cwdd+path_6i+"/"+file_6tx, cwdd+path_extra_out+"/"+filename_6tx)
                      except:
                          pass
                  except:
                      ae.err_handler.raise_exception(("Error: could not move", path_6i+"/"+file_6o, "to", path_6o+"/"+filename_6o))

              else:
                  ae.err_handler.raise_exception("Error: multiple output files detected for one mass transfer calculation.")

              if len(files_6p) == 0:
                  if ae.verbose > 0:
                      print('Error: EQ6 failed to produce a pickup file for ' + filename_6i)
              elif len(files_6p) == 1:
                  file_6p = files_6p[0]
                  try:
                      # move output
                      shutil.move(cwdd+path_6i+"/"+file_6p, cwdd+path_6p+"/"+filename_6p)
                  except:
                      ae.err_handler.raise_exception(("Error: could not move", path_6i+"/"+file_6p, "to", path_6p+"/"+filename_6p))
              else:
                  ae.err_handler.raise_exception("Error: multiple pickup files detected for one mass transfer calculation.")

              if not EQ6_errors_found:
                  m = Mass_Transfer(thermo=speciation.thermo,
                                    six_o_file='rxn_6o/'+filename_6o,
                                    format_element_names=format_element_names,
                                    tab_name="eq6_extra_out/"+filename_6i[:-2] + 'csv',
                                    hide_traceback=hide_traceback)

                  speciation.sample_data[sample_name]["mass_transfer"] = m
              else:
                  speciation.sample_data[sample_name]["mass_transfer"] = None
                  if ae.verbose > 0:
                      print(("Mass transfer results for sample '"+sample_name+"' "
                             "could not be saved because the calculation did not "
                             "finish due to error(s).\n"))

              # store input, output, and pickup as dicts in speciation object
              try:
                  with open(path_6i + "/" + filename_6i, "r") as f:
                      lines=f.readlines()
                  speciation.raw_6_input_dict[sample_name] = lines
              except:
                  pass
              try:
                  with open(path_6o + "/" + filename_6o, "r") as f:
                      lines=f.readlines()
                  speciation.raw_6_output_dict[sample_name] = lines
              except:
                  pass
              try:
                  with open(path_6p + "/" + filename_6p, "r") as f:
                      lines=f.readlines()

                  # Unlike 3p files, 6p files include headers that need to be removed
                  bottom_half = []
                  capture = False
                  for line in lines:
                      if "Start of the bottom half of the input file" in line:
                          capture = True
                      if capture:
                          bottom_half.append(line)

                  speciation.raw_6_pickup_dict[sample_name] = bottom_half

              except:
                  pass

          if delete_generated_folders:
              __delete_dir("eq6_extra_out")
              __delete_dir("rxn_6i")
              __delete_dir("rxn_6p")
              __delete_dir("rxn_6o")

          return speciation

  Calculate how speciated water reacts with minerals and/or gases.

  ## Parameters

  `speciation` :`Speciation object`
    The output of a speciation calculation produced by the AqEquil.speciate function.
  `reaction_setup` :`str` or `Prepare_Reaction object`
    Defines how the reaction is to be set up. There are two ways to set up a reaction. The first way is to prepare the reaction with `Prepare_Reaction`. The second way is to prepare the first half of an EQ6 6i file (the part without the contents of the pickup file) and then pass the filename to `reaction_setup`.
  `chain_mt` :`bool`, default `False`
    Is the speciation the result of another mass transfer calculation? Choosing True will allow mass transfer calculations to be chained together.
  `delete_generated_folders` :`bool`, default `False`
    Delete the 'rxn_6i', 'rxn_6o', 'rxn_6p', and 'eq6_extra_out' folders containing raw EQ6 input, output, and pickup files once the reaction calculation is complete?
  `hide_traceback` :`bool`, default `True`
    Hide traceback message when encountering errors handled by this class? When True, error messages handled by this class will be short and to the point.
  `data1_override` :`str`, optional
    The three letter code of a data1 file used to override the thermodynamic database used to speciate the sample(s). This is useful for chaining the results of one mass transfer calculation into another while simultaneously changing the temperature and pressure regime of the new system. See the description for the `chain_mt` parameter for more about chaining.
  `format_element_names` :`bool`, default `True`
    Format the chemical symbols of redox-isolated elements generated when elements are supplied to the `suppress_redox` parameter of `AqEquil.speciate`? Context: when elements are redox-isolated, new elements are designated representing different oxidations states in order to prevent redox reactions from occuring during calculations. The names of these elements appear like "Fejiip", where the first one or two letters represent the element (e.g. "Fe"), "j" denotes that the chemical symbol is finished, followed by roman numerals representing the magnitude of the charge (e.g., "ii" representing 2), finally followed by a letter that denotes the sign of the charge (e.g., "p" for positive). By setting this parameter to True, the element "Fejiip" will be converted into a more readable form, "Fe+2", in all figures and tables generated from calculation results. It may be useful to set this parameter to False if you are supplying your own data0 file containing custom pseudoelements that do not follow the convention used by AqEquil.
  `eq36da` :`str, defaults to path given by the environment variable EQ36DA`
    Path to directory where data1 files are stored.
  `eq36co` :`str, defaults to path given by the environment variable EQ36CO`
    Path to directory where EQ3 executables are stored.

  ## Returns

  An object of class `Speciation` modified with the results of the reaction
  (if a thermodynamic database formatted as a WORM-style CSV was used during
  `speciation)` or `an unmodified speciation object (if a data0` or `data1 file`

  was used during speciation).

## Classes

`class Gas (gas_name='None', gas_moles=0, gas_log_fugacity=0)`
  Expand source code

      class Gas:
          def __init__(self,
                       gas_name="None",
                       gas_moles=0,
                       gas_log_fugacity=0,
                      ):

              """
              Class used to define gases for `Prepare_Reaction`.

              Parameters
              ----------
              gas_name : str, default "None"
                  Name of the gas.

              gas_moles : float, default 0
                  Moles of gas.

              gas_log_fugacity : float, default 0
                  The log10 fugacity of the gas.

              """

              self.gas_name=gas_name
              self.gas_moles=gas_moles
              self.gas_log_fugacity=gas_log_fugacity

              self.__format_line()

          def __format_line(self):

              gas_options_formatted = dict(
                  gas_name = f"{self.gas_name:<24}",
                  gas_moles = f"{'{:.5E}'.format(self.gas_moles):>13}",
                  gas_log_fugacity = f"{'{:.5E}'.format(self.gas_log_fugacity):>13}",
              )

              gas_line_template = copy.copy(gl_template)
              self.formatted_line = gas_line_template.format(**gas_options_formatted)

  Class used to define gases for `Prepare_Reaction`.

  ## Parameters

  `gas_name` :`str`, default `"None"`
    Name of the gas.
  `gas_moles` :`float`, default `0`
    Moles of gas.
  `gas_log_fugacity` :`float`, default `0`
    The log10 fugacity of the gas.
`class Mass_Transfer (six_o_file,
thermo=None,
tab_name=None,
format_element_names=True,
hide_traceback=True,
verbose=1)`
  Expand source code

      class Mass_Transfer:
          """
          Class containing functions to facilitate mass transfer and reaction path
          calculations and visualize results.

          Parameters
          ----------
          six_o_file : str
              Path name of the '6o' output file generated by EQ6.

          thermo : an object of class Thermodata
              The subclass containing thermodynamic data in a Speciation object.

          thermodata_csv : str
              Path name of the WORM-styled thermodynamic database CSV used in the EQ6
              calculation.

          tab_name : str
              Path name of the TAB file generated by EQ6.

          format_element_names : bool, default True
              Format the chemical symbols of redox-isolated elements generated when
              elements are supplied to the `suppress_redox` parameter of
              `AqEquil.speciate`? Context: when elements are redox-isolated, new
              elements are designated representing different oxidations states in
              order to prevent redox reactions from occuring during calculations. The
              names of these elements appear like "Fejiip", where the first one or two
              letters represent the element (e.g. "Fe"), "j" denotes that the
              chemical symbol is finished, followed by roman numerals representing the
              magnitude of the charge (e.g., "ii" representing 2), finally followed by
              a letter that denotes the sign of the charge (e.g., "p" for positive).
              By setting this parameter to True, the element "Fejiip" will be
              converted into a more readable form, "Fe+2", in all figures and tables
              generated from calculation results. It may be useful to set this
              parameter to False if you are supplying your own data0 file containing
              custom pseudoelements that do not follow the convention used by AqEquil.

          hide_traceback : bool, default True
              Hide traceback message when encountering errors handled by this class?
              When True, error messages handled by this class will be short and to
              the point.

          """
          def __init__(self, six_o_file, thermo=None, tab_name=None,
                       format_element_names=True, hide_traceback=True, verbose=1):

              self.err_handler = Error_Handler(clean=hide_traceback)

              self.six_o_file = six_o_file

              f=open(self.six_o_file, mode='r')
              self.six_o_file_lines=f.readlines()
              f.close()

              self.thermo=thermo
              self.tab_name = tab_name
              self.verbose = verbose

              self.inactive_species = self.__get_inactive_species()

              if isinstance(self.thermo.csv_db, pd.DataFrame):

                  # these operations require a WORM-style thermodynamic database CSV
                  obigt = pychnosz.thermo().OBIGT

                  obigt.name.isin(FIXED_SPECIES)

                  obigt = assign_worm_db_col_dtypes(obigt)

                  pychnosz.thermo(OBIGT=obigt.loc[ obigt.name.isin(FIXED_SPECIES), : ])

                  _ = pychnosz.add_OBIGT(self.thermo.csv_db, force=True, messages=False)

                  self.df = copy.deepcopy(self.thermo.csv_db)

                  try:
                      self.tab = self.process_tab(tab_name, self.thermo.csv_db)
                  except:
                      self.tab = None

                  # remove species that do not have Gibbs free energy values
                  self.df = self.df[~self.df["G"].isna()]

                  # remove inactive species from the database to prevent them from
                  # showing up as mineral fields or saturation lines
                  if len(self.inactive_species) > 0:
                      self.df = self.df[~self.df.name.isin(self.inactive_species)]

                  basis_df = self.df[self.df["tag"] == "basis"]
                  aux_df = self.df[self.df["tag"] == "aux"]

                  # remove basis or aux species with no formula ox state col
                  aux_df = aux_df[aux_df["formula_ox"] != ""]
                  aux_df = aux_df[~aux_df['formula_ox'].isnull()]

                  refstate_df = self.df[self.df["tag"] == "refstate"]
                  self.basis_df = pd.concat([basis_df])
                  self.basis_aux_df = pd.concat([basis_df, aux_df, refstate_df])
                  self.df_cr = self.df[self.df["state"] == 'cr']
              else:
                  self.df = None
                  self.tab = None
                  self.basis_df = None
                  self.basis_aux_df = None
                  self.df_cr = None

              self.misc_params = self.__get_misc_params()
              self.dissolved_elements_molal = self.__get_dissolved_elements(unit="molality")
              self.dissolved_elements_ppm = self.__get_dissolved_elements(unit="ppm")

              self.aq_distribution_logact = self.__get_aq_distribution(unit="log activity")
              self.aq_distribution_molal = self.__get_aq_distribution(unit="molality")
              self.aq_distribution_logmolal = self.__get_aq_distribution(unit="log molality")
              self.moles_minerals = self.__get_moles_minerals()

              self.saturation_states_pure_solids_log_Q_over_K = self.__get_saturation_states(unit="logQ/K")
              self.saturation_states_pure_solids_affinity = self.__get_saturation_states(unit="affinity")

              self.saturation_states_solid_solutions_log_Q_over_K = self.__get_ss_saturation_states(unit="logQ/K")
              self.saturation_states_solid_solutions_affinity = self.__get_ss_saturation_states(unit="affinity")
              self.solid_solution_names = [col for col in list(self.saturation_states_solid_solutions_affinity.columns) if col != "Xi"] # required by a few things below

              self.moles_product_minerals_and_solid_solutions = self.__get_moles_product_minerals(include_solid_solutions=True)
              self.moles_product_minerals = self.__get_moles_product_minerals(include_solid_solutions=False) # relies on self.solid_solution_names

              self.solid_solution_x_dict = self.__get_solid_solution_product_phases(unit="x") # relies on self.solid_solution_names
              self.moles_solid_solutions = self.moles_product_minerals_and_solid_solutions.loc[:, ["Xi"] + list(self.solid_solution_x_dict.keys())]
              self.solid_solution_log_x_dict = self.__get_solid_solution_product_phases(unit="log x") # relies on self.solid_solution_names
              self.solid_solution_log_lambda_dict = self.__get_solid_solution_product_phases(unit="log lambda") # relies on self.solid_solution_names
              self.solid_solution_log_lambda_dict = self.__get_solid_solution_product_phases(unit="log activity") # relies on self.solid_solution_names

              self.basis_molality = self.__get_basis_species(unit="molality")
              self.basis_ppm = self.__get_basis_species(unit="ppm")
              self.basis_logact = self.__get_basis_species(unit="logact") # this one needs to be after __get_aq_distribution()

              if format_element_names:
                  try:
                      # format element names in case there are redox-isolated elements
                      self.dissolved_elements_molal.columns = ["Xi"]+[_format_pseudoelement_name(e) for e in self.dissolved_elements_molal.columns if e not in ["Xi", "t(days)"]]
                      self.dissolved_elements_ppm.columns = ["Xi"]+[_format_pseudoelement_name(e) for e in self.dissolved_elements_ppm.columns if e not in ["Xi", "t(days)"]]
                  except:
                      self.err_handler.raise_exception("One of the following chemical symbols could not be formatted:" + str(self.dissolved_elements_molal.columns)+". Try setting the parameter format_element_names to False and try again.")

              if self.moles_minerals.shape[0] == 0 and self.moles_product_minerals.shape[0] > 0:
                  # in the case of special reactants, there is no grand summary table in the 6o file
                  # that combine reactant and product minerals (because there is no reactant mineral).
                  # In this case, just assume these tables are equal.
                  self.moles_minerals = self.moles_product_minerals

              self.mass_contribution_dict = self.__get_mass_contribution()

          def __get_misc_params(self):

              recording = False
              xi_vals = []
              t_vals = []
              p_vals = []
              pH_vals = []
              pmH_vals = []
              logfO2_vals = []
              Eh_vals = []
              pe_vals = []
              aw_vals = []
              IS_vals = []
              for line in self.six_o_file_lines:
                  if "                    Xi=" in line:
                      recording = True
                      splitstrings = line.strip().split(" ")
                      xi = [float(v) for v in splitstrings if v not in ['', 'Xi=']][0]
                      xi_vals.append(xi)
                  elif " Temperature=" in line and recording:
                      splitstrings = line.strip().split(" ")
                      t = [float(v) for v in splitstrings if v not in ['', 'Temperature=', 'C']][0]
                      t_vals.append(t)
                  elif " Pressure=" in line and recording:
                      splitstrings = line.strip().split(" ")
                      p = [float(v) for v in splitstrings if v not in ['', 'Pressure=', 'bars']][0]
                      p_vals.append(p)
                  elif "NBS pH scale " in line and recording:
                      splitstrings = line.strip().split(" ")
                      multival = [v for v in splitstrings if v not in ['', 'NBS', 'pH', 'scale']]
                      multival = [float(v) if v != "********" else float('nan') for v in multival]
                      pH_vals.append(multival[0])
                      Eh_vals.append(multival[1])
                      pe_vals.append(multival[2])
                  elif "Mesmer pH (pmH) scale " in line and recording:
                      splitstrings = line.strip().split(" ")
                      pmH = [v for v in splitstrings if v not in ['', 'Mesmer', 'pH', '(pmH)', 'scale']]
                      pmH = [float(v) if v != "********" else float('nan') for v in pmH][0]
                      pmH_vals.append(pmH)
                  elif "  Log oxygen fugacity=" in line and recording:
                      splitstrings = line.strip().split(" ")
                      logfO2 = [float(v) for v in splitstrings if v not in ['', "Log", "oxygen", "fugacity="]][0]
                      logfO2_vals.append(logfO2)
                  elif "  Activity of water=" in line and recording:
                      splitstrings = line.strip().split(" ")
                      aw = [float(v) for v in splitstrings if v not in ['', "Activity", "of", "water="]][0]
                      aw_vals.append(aw)
                  elif " Ionic strength (I)=" in line and recording:
                      splitstrings = line.strip().split(" ")
                      IS = [float(v) for v in splitstrings if v not in ['', "Ionic", "strength", "(I)=", "molal"]][0]
                      IS_vals.append(IS)
                  if "- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -" in line:
                      recording = False

              df = pd.DataFrame({
                  "Xi":xi_vals,
                  "t(days)":[0]*len(xi_vals), # TODO: make this actually reflect days, need an example output file
                  "Temp(C)":t_vals,
                  "Press(bars)":p_vals,
                  "pH":pH_vals,
                  "pmH":pmH_vals,
                  "log fO2":logfO2_vals,
                  "Eh(v)":Eh_vals,
                  "pe":pe_vals,
                  "aw":aw_vals,
                  "Ionic strength (molal)":IS_vals,
              })

              return df

          def __get_saturation_states(self, unit="logQ/K"):

              if unit == "logQ/K":
                  col_index = 1
              elif unit == "affinity":
                  col_index = 2
              else:
                  self.err_handler.raise_exception("Error in get_saturation_states()"
                          ". Unit not recognized.")

              return self.mine_6o_table(table_start="--- Saturation States of Pure Solids ---",
                      table_stop="--- Saturation States of Pure Liquids ---",
                      ignore = ["", "Phases", 'Phase', '---', '-'],
                      col_index=col_index)

          def __get_ss_saturation_states(self, unit="logQ/K"):

              if unit == "logQ/K":
                  col_index = 1
              elif unit == "affinity":
                  col_index = 2
              else:
                  self.err_handler.raise_exception("Error in get_ss_saturation_states()"
                          ". Unit not recognized.")

              return self.mine_6o_table(table_start="--- Saturation States of Solid Solutions ---",
                      table_stop="--- Summary of Saturated and Supersaturated Phases ---",
                      ignore = ["", "Phases", 'Phase', '---', '-'],
                      col_index=col_index)

          def __get_dissolved_elements(self, unit="molality"):

              if unit == "molality":
                  col_index = -1
              elif unit == "mg/kg.sol" or unit == "ppm":
                  col_index = 1
              else:
                  self.err_handler.raise_exception("Error in get_dissolved_elements()"
                          ". Unit not recognized.")

              return self.mine_6o_table(table_start="Elemental Composition of the Aqueous Solution",
                      table_stop="Numerical Composition of the Aqueous Solution",
                      ignore = ["", "Element", '---', '-'],
                      col_index=col_index)

          def __get_basis_species(self, unit="logact"):

              if unit == "molality" or unit == "logact":
                  col_index = -1
              elif unit == "mg/kg.sol" or unit == "ppm":
                  col_index = 1
              else:
                  self.err_handler.raise_exception("Error in get_basis_species()"
                          ". Unit not recognized.")

              basis_df = self.mine_6o_table(table_start="Numerical Composition of the Aqueous Solution",
                      table_stop="Sensible Composition of the Aqueous Solution",
                      ignore = ["", "Some", 'Species', '---', '-'],
                      col_index=col_index)

              if unit in ["molality", "ppm", 'mg/kg.sol']:
                  return basis_df
              elif unit == "logact":

                  cols_to_harvest = [b for b in basis_df.columns if b not in ["O2(g)", "H2O"]]

                  out = self.aq_distribution_logact[[c for c in cols_to_harvest if c in self.aq_distribution_logact.columns]]

                  return out

          def __get_mass_contribution(self):

              bases = [b for b in self.basis_logact.columns if b not in ['Xi', 't(days)', 'H2O', "O2(g)", "H+"]]

              mass_contribution_dict = {}
              for basis in bases:

                  df = self.mine_6o_table(
                                  table_start="Species Accounting for 99% or More of Aqueous "+basis,
                                  table_stop="Subtotal",
                                  ignore = ["", "Species", '---', '-'],
                                  col_index=-1)

                  df['Xi'] = df['Xi'].astype(str)
                  df['Other'] = 100 - df.sum(axis=1, numeric_only=True)
                  df['Xi'] = df['Xi'].astype(float)

                  df[df["Other"] < 0] = 0

                  df["basis"] = basis
                  df["factor"] = None
                  df["molality"] = None

                  mass_contribution_dict[basis] = df

              return mass_contribution_dict

          def mine_6o_table(self,
                            table_start="--- Distribution of Aqueous Solute Species ---",
                            table_stop="Species with molalities less than",
                            ignore = ["", "Species", '---'],
                            col_index=-1):

              """
              Mine a table in a '6o' EQ6 output file and consolidate results into a
              dataframe.

              Parameters
              ----------
              table_start : str
                  A unique string that indicates the start of the table.

              table_stop : str, optional
                  A unique string that indicates the end of the table.

              ignore : list of str
                  A list of strings representing lines to ignore when mining a table.
                  For example, it is prudent to ignore blank lines, or lines
                  containing the table column headers.
                  A line will be skipped if line.strip().split(' ')[0] matches any
                  of the strings in the given list.

              col_index : int, default -1
                  Integer representing the index of the table column to be mined.
                  The default is -1, which is the last column in the table.

              Returns
              -------
              df : Pandas dataframe
                  A dataframe with rows of the extent of reaction (Xi), and columns
                  containing the values of chemical species mined from the file.
              """

              lines = self.six_o_file_lines

              species = []
              xi_vals=[]
              collect_values = False
              for i in lines:
                  if len(i.strip().split(' ')) > 1 and i.strip().split(' ')[0] == "Xi=":
                      this_xi_val = float(i.split(' ')[-1])
                  if table_stop in i:
                      collect_values = False
                  if table_start in i:
                      xi_vals.append(this_xi_val) # appending here prevents mismatch where there can be more Xi vals than tables to mine
                      collect_values = True
                  if collect_values:
                      if i.strip().split(' ')[0] not in ignore:
                          species.append(i.strip().split(' ')[0])

              species = list(set(species))

              species_dict = {"Xi":xi_vals}

              if len(species) == 0:
                  df = pd.DataFrame(species_dict)
                  return df

              for s in species:

                  vals=[]
                  collect_values = False
                  for i in lines:
                      if collect_values and table_stop in i:
                          # stop collecting
                          collect_values = False
                          if not got_value:
                              vals.append(np.nan)
                      if table_start in i:
                          # start collecting
                          collect_values = True
                          got_value = False
                      if collect_values:
                          if len(i.strip().split(' ')) > 2 and i.strip().split(' ')[0] == s:
                              split_i = i.strip().split(' ')
                              split_i_clean = [v for v in split_i if v != '']
                              val = split_i_clean[col_index]
                              try:
                                  val = float(val)
                              except:
                                  # if a value is not a float, e.g., 3.1450-100
                                  if "-" in val:
                                      val_list = val.split("-")
                                      val = "".join([val_list[0], "E-", val_list[1]])
                                      val = float(val)
                                  elif "+" in val:
                                      val_list = val.split("+")
                                      val = "".join([val_list[0], "E", val_list[1]])
                                      val = float(val)
                                  elif "SATD" in val:
                                      pass
                                  else:
                                      self.err_handler.raise_exception(("Error: "
                                          "Encountered a non-numeric value when mining "
                                          "a .6o file: "+val))
                              vals.append(val)
                              got_value = True
                  species_dict[s] = vals

              df = pd.DataFrame(species_dict)

              if 'None' in df.columns:
                  df = df.drop(['None'], axis=1)

              return df

          def __get_inactive_species(self):

              lines=self.six_o_file_lines

              table_start = "--- Inactive Species ---"
              table_stop = "The activity coefficients of aqueous species"

              vals=[]
              collect_values=False
              for i in lines:
                  if collect_values and table_stop in i:
                      # stop collecting
                      break
                  if table_start in i:
                      # start collecting
                      collect_values = True
                      got_value = False
                  if collect_values:
                      if table_start not in i:
                          split_i = i.strip().split(' ')
                          split_i_clean = [v for v in split_i if v != '' and v != "None"]
                          if len(split_i_clean) > 0:
                              vals.append(split_i_clean[0])
                              got_value = True

              return vals

          def __get_aq_distribution(self, unit="log activity"):

              if unit == "log activity":
                  col_index = -1
              elif unit == "molality":
                  col_index = 1
              elif unit == "log molality":
                  col_index = 2

              return self.mine_6o_table(
                            table_start="--- Distribution of Aqueous Solute Species ---",
                            table_stop="Species with molalities less than",
                            ignore = ["", "Species", '---'],
                            col_index=col_index)

          def __get_moles_minerals(self):

              return self.mine_6o_table(
                            table_start="Grand Summary of Solid Phases",
                            table_stop="Mass, grams       Volume, cm3",
                            ignore = ["", "Phase/End-member", '---'],
                            col_index=2)

          def __get_moles_product_minerals(self, include_solid_solutions=True):

              if "Grand Summary of Solid Phases" in "\n".join(self.six_o_file_lines):
                  table_stop = "Grand Summary of Solid Phases"
              else:
                  table_stop = "Mass, grams       Volume, cm3"

              df = self.mine_6o_table(
                            table_start="--- Summary of Solid Phases (ES) ---",
                            table_stop=table_stop,
                            ignore = ["", "Phase/End-member", '---'],
                            col_index=2)

              if not include_solid_solutions:
                  mineral_names_to_keep = []
                  minerals_in_solid_solutions = []

                  recording = True
                  for col in df.columns:
                      if col not in self.solid_solution_names:
                          mineral_names_to_keep.append(col)
                  df = df[mineral_names_to_keep]

              return df

          def __get_solid_solution_product_phases(self, unit="x"):

              if unit == "x":
                  col_index = 1
              elif unit == "log x":
                  col_index = 2
              elif unit == "log lambda":
                  col_index = 3
              elif unit == "log activity":
                  col_index = 4

              ss_dict = {}
              for ss in self.solid_solution_names:
                  df = self.mine_6o_table(
                               table_start="--- "+ss+" ---",
                               table_stop="Mineral",
                               ignore = ["", "Component", "Ideal", '---'],
                               col_index=col_index)

                  if not df.empty:
                      ss_dict[ss] = df

              return ss_dict

          def print_tabs(self):
              """
              Print the names of tables contained in a tab file processed by the
              the Mass_Transfer class.
              """
              if self.tab != None:
                  [print(key) for key in self.tab.keys()]
              else:
                  print("A processed TAB file is not associated with this sample.")

          @staticmethod
          def __is_all_same_value(s):
              a = s.to_numpy()
              return (a[0] == a).all()

          def plot_reaction_paths(self,
                                  xyb=None,
                                  path_margin=0.25,
                                  flip_xy=False,
                                  show_annotation=True,
                                  annotation_coords=[0,0],
                                  show_nonparticipating_mineral_lines=False,
                                  minerals_to_show=[],
                                  calculate_projected_points=True,
                                  path_line_type = "markers+lines",
                                  path_line_color = "red",
                                  path_point_fill_color = "red",
                                  path_point_line_color = "red",
                                  projected_point_fill_color = "white",
                                  projected_point_line_color = "red",
                                  h_line_color="black",
                                  v_line_color="black",
                                  d_line_color="black",
                                  res=300,
                                  plot_width=4,
                                  plot_height=3,
                                  ppi=122,
                                  borders=0,
                                  save_as=None,
                                  save_format=None,
                                  save_scale=1,
                                  colormap="bw"):

              """
              Create interactive plots of reaction paths in geochemical variable
              space.

              Parameters
              ----------
              xyb : list of three str, default None
                  By default, this function will plot reaction paths in all possible
                  dimensions.

                  Optionally, if you want to produce only a specific plot,
                  you can provide a list containing the basis species to be used for
                  the x-axis and y-axis, followed by the basis species used for
                  balance. For example, ["Fe+2", "Fe+3", "Mg+2"] will have the log
                  activity of Fe+2 on the x-axis, the log activity of Fe+3 on the
                  y-axis, and will be balanced on Mg+2.

              path_margin : float, default 0.25
                  Controls the spacing between the reaction path and the plot axes.
                  Increasing this value increases the spacing.

              flip_xy : bool, default False
                  Transpose the plot so the x and y variables switch axes?

              show_annotation : bool, default True
                  Show annotation in the bottom left of the figure? The annotation
                  includes the species used for balance, the temperature, and
                  the pressure.

              annotation_coords : list, default [0,0]
                  List of two numeric values representing the X and Y coordinates of
                  the annotation, where 0,0 is the bottom left, 0.5,0 is the bottom
                  center, 1,0 is the bottom right, 1,1 is the top right, and so on.
                  The annotation includes the species used for balance, the
                  temperature and the pressure.

              show_nonparticipating_mineral_lines : bool, default False
                  Depict lines for minerals even if those minerals do not participate
                  in the reaction? This does not affect the mineral field of the
                  diagram, only the mineral planes denoted as lines.

              path_line_type : str, default "markers+lines"
                  Reaction path line type. Can be either "markers+lines", "lines", or
                  "markers".

              path_line_color, str, default "black"
                  Color of reaction path line.

              path_point_fill_color : str, default "black"
                  Fill color of non-projected points along the reaction path. The
                  fill color of projected points is handled by
                  `projected_point_fill_color`.

              path_point_line_color : str, default "black"
                  Color of the outlines of non-projected points along the reaction
                  path. The outline color of projected points is handled by
                  `projected_point_line_color`.

              projected_point_fill_color : str, default "white"
                  Fill color of projected points along the reaction path. The
                  fill color of non-projected points is handled by
                  `path_point_fill_color`.

              projected_point_line_color : str, default "black"
                  Color of the outlines of projected points along the reaction path.
                  The outline color of non-projected points is handled by
                  `path_point_line_color`.

              h_line_color : str, default "black"
                  Color of horizontal lines representing minerals.

              v_line_color : str, default "black"
                  Color of vertical lines representing minerals.

              d_line_color : str, default "black"
                  Color of diagonal lines representing minerals.

              res : int, default 300
                  Resolution, or number of calculations along each axis, for mineral
                  stability fields. A lower number will be faster but will make appear
                  boundaries blockier. A higher number takes longer to calculate, but
                  will result in smoother boundaries.

              plot_width, plot_height : numeric, default 4 by 3
                  Width and height of the plot, in inches.

              ppi : numeric, default 122
                  Pixels per inch. Along with `plot_width` and `plot_height`,
                  determines the size of interactive plots.

              borders : float, default 0
                  Thickness of black lines forming boundaries between mineral
                  stability regions. No lines appear if equal to 0.

              save_as : str, optional
                  Provide a filename to save this figure. Filetype of saved figure is
                  determined by `save_format`.
                  Note: interactive plots can be saved by clicking the 'Download plot'
                  button in the plot's toolbar.

              save_format : str, default "png"
                  Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                  'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                  an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                  and 'webp' can be downloaded with the 'download as' button in the
                  toolbar of an interactive plot.

              save_scale : numeric, default 1
                  Multiply title/legend/axis/canvas sizes by this factor when saving
                  the figure.

              colormap : str, default "bw"
                  Name of the colormap to color the scatterpoints. Accepts "bw"
                  or names of matplotlib colormaps. If set to "bw", the plot will be
                  set to black and white, except for the reaction path line itself.
                  The colors of the reaction path line and its points are controlled
                  by `path_line_color`, `path_point_fill_color`,
                  `path_point_line_color`, `projected_point_fill_color`,
                  and `projected_point_line_color`.

              Returns
              -------
              fig_list : a list of Plotly figure objects
                  A list of interactive Plotly figures. If xyb equals None
                  (the default), then the list will contain figures representing all
                  combinations of geochemical variables. Optionally, if xyb is
                  specified, fig_list will only contain the single figure of interest.
              """

              error_messages = []

              # check that there is only one temperature and pressure
              if not self.__is_all_same_value(self.misc_params["Temp(C)"]):
                  error_messages.append("Reaction paths cannot be plotted when temperature changes with reaction progress.")
              if not self.__is_all_same_value(self.misc_params["Press(bars)"]):
                  error_messages.append("Reaction paths cannot be plotted when pressure changes with reaction progress.")

              if isinstance(xyb, list):
                  if len(xyb) != 3:
                      error_messages.append(("Error in xyb={}".format(xyb)+". "
                              "The xyb parameter must either be None or a list of "
                              "three basis species to serve as x, y, and balance variables."))

              if not isinstance(self.df, pd.DataFrame):
                  error_messages.append(("The plot_reaction_paths() function requires "
                          "a thermodynamic database in a WORM-style CSV format, e.g., "
                          "'wrm_data.csv'. You may be getting this message because "
                          "a data0 or data1 file was used."))

              if len(error_messages)>0:
                  self.err_handler.raise_exception("\n".join(error_messages))

              self.T = float(self.misc_params["Temp(C)"][0])
              self.P = float(self.misc_params["Press(bars)"][0])
              self.IS = float(self.misc_params["Ionic strength (molal)"][0])
              self.path_margin = path_margin

              minerals_formed = [m for m in self.moles_minerals.columns if m != "Xi"]

              all_elements_of_interest = []
              for mineral in minerals_formed:

                  if mineral not in list(self.df["name"]):
                      if self.verbose > 0:
                          print("The mineral", mineral, "cannot be represented in a",
                                 "reaction path diagram, likely because it is missing",
                                 "a Gibbs free energy value in the thermodynamic",
                                 "database. Continuing anyway, but be aware that",
                                 "this mineral will not be represented in diagrams.")
                      continue

                  all_elements_of_interest += self.__get_elem_ox_of_interest_in_minerals(mineral)
              all_elements_of_interest_pre = list(set(all_elements_of_interest))

              # filter out elements like Fe+0, which has no aqueous species representative
              # for which to create an axis.
              bad_elem = []
              for elem in all_elements_of_interest_pre:
                  if len(list(self.thermo.csv_db.loc[self.thermo.csv_db['name'] == self.__get_basis_from_elem(elem), 'state'])) == 0:
                      bad_elem.append(elem)
                  elif list(self.thermo.csv_db.loc[self.thermo.csv_db['name'] == self.__get_basis_from_elem(elem), 'state'])[0] != 'aq':
                      bad_elem.append(elem)

              all_elements_of_interest = [elem for elem in all_elements_of_interest_pre if elem not in bad_elem]

              all_elements_of_interest = sorted(all_elements_of_interest)

              self.all_elements_of_interest = all_elements_of_interest

              fig_list = []

              # if there are only 2 elements of interest, these become the axes, and there is no
              # need to fuss with real vs projected points.
              if len(all_elements_of_interest) == 2:
                  fig, _ , _ = self.__plot_reaction_path_main(
                                                      triad = all_elements_of_interest,
                                                      T=self.T, P=self.P, IS=self.IS,
                                                      path_margin=self.path_margin,
                                                      flip_xy=flip_xy,
                                                      show_annotation=show_annotation,
                                                      annotation_coords=annotation_coords,
                                                      show_nonparticipating_mineral_lines=show_nonparticipating_mineral_lines,
                                                      minerals_to_show=minerals_to_show,
                                                      path_line_type=path_line_type,
                                                      path_line_color=path_line_color,
                                                      path_point_fill_color=path_point_fill_color,
                                                      path_point_line_color=path_point_line_color,
                                                      projected_point_fill_color=projected_point_fill_color,
                                                      projected_point_line_color=projected_point_line_color,
                                                      h_line_color=h_line_color,
                                                      v_line_color=v_line_color,
                                                      d_line_color=d_line_color,
                                                      res=res,
                                                      plot_width=plot_width,
                                                      plot_height=plot_height,
                                                      ppi=ppi,
                                                      colormap=colormap,
                                                      borders=borders,
                                                      projected_points=["real"]*self.moles_product_minerals.shape[0],
                                                      first_pass=False)

                  fig_list = [fig]

              elif len(all_elements_of_interest) >= 3:

                  # get a list of elem pairs for plotting
                  alist = self.all_elements_of_interest
                  element_plot_pairs = []
                  for result in itertools.combinations(alist, 2):
                      element_plot_pairs.append(list(result))

                  element_plot_triad = []
                  for pair in element_plot_pairs:
                      elem_not_in_pair = [e for e in self.all_elements_of_interest if e not in pair]
                      for e in elem_not_in_pair:
                          triad_to_append = pair + [e]
                          element_plot_triad.append(triad_to_append)

                  if colormap == "bw":
                      if borders == 0:
                          borders = 1
                      colormap = "none"
                      h_line_color = "black"
                      v_line_color = "black"
                      d_line_color = "black"

                  fig_list = []
                  pred_minerals_from_fields_list = []
                  pred_minerals_from_lines_list = []

                  if isinstance(xyb, list):
                      try:
                          xyb_element_plot_triad = [[self.__get_elem_ox_of_interest_in_minerals(v)[0] for v in xyb]]
                      except:
                          err = ("Plot axes cannot accomodate desired variables. "
                                 "Available variables include {}".format([self.__get_basis_from_elem(elem) for elem in alist]))
                          self.err_handler.raise_exception(err)
                      xyb_i = None
                      # get index of triad that matches xyb:
                      for i,triad in enumerate(element_plot_triad):
                          if set(xyb_element_plot_triad[0][0:2]) == set(triad[0:2]) and xyb_element_plot_triad[0][2] == triad[2]:
                              xyb_i = i
                      if xyb_i == None:
                          err = ("Plot axes cannot accomodate desired variables. "
                                 "Available variables include {}".format([self.__get_basis_from_elem(elem) for elem in alist]))
                          self.err_handler.raise_exception(err)

                  if not calculate_projected_points or path_line_type=="lines":
                      projected_points = ["real"]*self.moles_product_minerals.shape[0]
                      fig_list_projected_points = [projected_points]*len(element_plot_triad)

                  else:

                      if len(element_plot_triad) > 20:
                          if self.verbose > 0:
                              print("Warning! There are {}".format(len(element_plot_triad)),
                                    "different combinations of variables that must be considered",
                                    "in order to plot markers.")
                              print("This might take a very long time or may not finish calculating at all.")
                              print("We recommend setting calculate_projected_points=False in",
                                    "plot_reaction_paths() and then restarting the",
                                    "calculation to avoid lengthy calculation times.")

                      for triad in element_plot_triad:

                          # do a quick first pass at making figures to see which points are projections.
                          fig, pred_minerals_from_fields, pred_minerals_from_lines = self.__plot_reaction_path_main(
                                                              triad, T=self.T, P=self.P, IS=self.IS,
                                                              show_nonparticipating_mineral_lines=False, # no need for this in first pass
                                                              minerals_to_show=[], # no need for this in first pass
                                                              path_margin=self.path_margin,
                                                              flip_xy=flip_xy,
                                                              first_pass=True, # flag for skipping certain calculations/plotting
                                                              res=1) # low res first pass

                          if pred_minerals_from_fields == None:
                              pred_minerals_from_fields=[None]*self.moles_product_minerals.shape[0]

                          fig_list.append(fig)
                          pred_minerals_from_fields_list.append(pred_minerals_from_fields)
                          pred_minerals_from_lines_list.append(pred_minerals_from_lines)

                      # determine which line segments in the reaction path are projections
                      # and which are actually in the plane of the diagram.
                      # This is the "first pass"
                      fig_list_projected_points = []
                      for i,triad in enumerate(element_plot_triad):

                          projected_points = ["projection"]*self.moles_product_minerals.shape[0]
                          for irow in range(0, self.moles_product_minerals.shape[0]):

                              # get names of minerals formed at this xi
                              xirow = list(self.moles_product_minerals.iloc[irow])
                              formed_minerals = [self.moles_product_minerals.columns[1:][ii] for
                                                 ii,mineral in enumerate(xirow[1:]) if
                                                 mineral>0]

                              available_pred_minerals_from_fields = [l[irow] for l in pred_minerals_from_fields_list]

                              for mineral in formed_minerals:

                                  if mineral == pred_minerals_from_fields_list[i][irow]:
                                      # if this mineral is in pred_minerals_from_fields_list,
                                      # then it is NOT a projection.
                                      projected_points[irow] = "real"
                                  if mineral in available_pred_minerals_from_fields and mineral in pred_minerals_from_lines_list[i]:
                                      # if this mineral is in the irowth location of any of the
                                      # lists in pred_minerals_from_lines_list, it is NOT a
                                      # projection.
                                      projected_points[irow] = "real"

                          fig_list_projected_points.append(projected_points)

                  if isinstance(xyb, list):
                      # if xyb is defined, make element_plot_triad have a length of 1
                      element_plot_triad = xyb_element_plot_triad
                      # give the list of projected points lists a length of 1
                      fig_list_projected_points = [fig_list_projected_points[xyb_i]]

                  fig_list = []
                  for i,triad in enumerate(element_plot_triad):
                      # re-run figure generation, passing in a list of which points are projected.
                      fig, _ , _ = self.__plot_reaction_path_main(
                                          triad, T=self.T, P=self.P, IS=self.IS,
                                          path_margin=self.path_margin,
                                          flip_xy=flip_xy,
                                          show_annotation=show_annotation,
                                          annotation_coords=annotation_coords,
                                          show_nonparticipating_mineral_lines=show_nonparticipating_mineral_lines,
                                          minerals_to_show=minerals_to_show,
                                          path_line_type=path_line_type,
                                          path_line_color=path_line_color,
                                          path_point_fill_color=path_point_fill_color,
                                          path_point_line_color=path_point_line_color,
                                          projected_point_fill_color=projected_point_fill_color,
                                          projected_point_line_color=projected_point_line_color,
                                          h_line_color=h_line_color,
                                          v_line_color=v_line_color,
                                          d_line_color=d_line_color,
                                          res=res,
                                          plot_width=plot_width,
                                          plot_height=plot_height,
                                          ppi=ppi,
                                          colormap=colormap,
                                          borders=borders,
                                          projected_points=fig_list_projected_points[i],
                                          first_pass=False)

                      fig_list.append(fig)

              if not fig_list and self.verbose > 0:
                  print("Warning: a reaction path plot could not be generated for this system.")

              if isinstance(save_as, str):
                  dummy_sp = Speciation({})
                  for i,fig in enumerate(fig_list):

                      if isinstance(xyb, list):
                          name_append = ""
                      else:
                          name_append = "_{}".format(i+1)
                      _, _ = dummy_sp._save_figure(fig,
                              save_as+name_append, save_format, save_scale,
                              plot_width, plot_height, ppi)

              return fig_list

          @staticmethod
          def process_tab(tab_name, thermodata_csv):

              """
              Process a TAB file (from EQ6) into a dictionary of Pandas dataframes.

              Parameters
              ----------
              tab_name : str
                  Path name of the TAB file generated by EQ6.

              thermodata_csv : str or Pandas dataframe
                  Path name of the WORM-styled thermodynamic database CSV used in the
                  EQ6 calculation. Alternately, the thermodynamic database itself as a
                  Pandas dataframe itself.

              Returns
              -------
              tab : a dict of Pandas dataframes
                  A dictionary of dataframes representing tables mined from the TAB
                  file.
              """

              if isinstance(thermodata_csv, str):
                  thermo_db = pd.read_csv(thermodata_csv)
              else:
                  thermo_db = thermodata_csv

              thermo_db_names = list(thermo_db["name"])

              with open(tab_name, "r") as tabfile:
                  tab_lines = tabfile.readlines()

              tab = {}

              tables = ["B1", "B2", "C1", "C2", "C3", "C4",
                        "D1", "D2", "D3", "D4", "E1", "E2",
                        "E3", "J", "K", "P", "Q", "T", "W"]

              record_lines = False
              get_header = False
              recorded_lines = []
              for line in tab_lines:
                  split_line = line.split(",")

                  if True in [s in ["Table " + t for t in tables] for s in split_line]:
                      get_header = True
                      table_name = " ".join(split_line[1:-1])
                      continue

                  if get_header:
                      header = split_line[1:-1]

                      # handle instances where the tab file creates extra columns for things like "albite,low"
                      if table_name in ["Table P Moles of product minerals",
                                        "Table Q Saturation indices of potential product phases",
                                        "Table J Moles of reactants destroyed/created",
                                        "Table K Affinities of reactants (kcal)"]:

                          if table_name in ["Table P Moles of product minerals",
                                            "Table J Moles of reactants destroyed/created"]:
                              new_header = ["Xi", "t(days)"] # table P and J
                          elif table_name == "Table Q Saturation indices of potential product phases":
                              new_header = ["Xi", "t(days)", "H2O", "Gas"] # table Q
                          elif table_name == "Table K Affinities of reactants (kcal)":
                              new_header = ["Xi", "t(days)", "Total"]

                          for i,h in enumerate(header):

                              if h != "Xi" and h != "t(days)" and h != "H2O" and h != "Gas" and h != "Total":
                                  if h not in thermo_db_names:
                                      if header[i-1]+","+h in thermo_db_names:
                                          new_header = new_header[:-1]
                                          new_header.append(header[i-1]+","+h)
                                      else:
                                          new_header.append(h)
                                  else:
                                      new_header.append(h)

                          header = new_header

                      record_lines = True
                      get_header = False
                      continue

                  if "EndTable:" in split_line:
                      record_lines = False
                      if len(recorded_lines) > 0:
                          df = pd.DataFrame(recorded_lines)
                          recorded_lines = []
                          df.columns = header
                          tab[table_name] = df

                  if record_lines:
                      recorded_lines.append(split_line[1:-1])

              return tab

          def __get_mineral_elem_ox(self, mineral):
              split_list = list(self.df[self.df["name"]==mineral]["formula_ox"])[0].split()
              split_list_clean = [s.replace(" ", "") for s in split_list]
              try:
                  elem_ox_list = [re.findall(r"^(?:\d+|)([A-Z].*$)", s)[0] for s in split_list_clean]
              except:
                  elem_ox_list = []
              return elem_ox_list

          def __get_elem_ox_of_interest_in_minerals(self, mineral_name):
              mineral_elements = self.__get_mineral_elem_ox(mineral_name)
              mineral_elements_of_interest = [e for e in mineral_elements if e not in ["H+", "O-2"]]
              return mineral_elements_of_interest

          def __get_mineral_elem_ox_dict(self, mineral):
              split_list = list(self.df[self.df["name"]==mineral]["formula_ox"])[0].split()
              split_list_clean = [s.replace(" ", "") for s in split_list]
              elem_ox_names = self.__get_mineral_elem_ox(mineral)

              elem_ox_list = []
              for s in split_list:
                  coeff = re.findall(r"(\d+)[A-Z]", s)
                  if len(coeff) == 0:
                      coeff = 1
                  else:
                      coeff = float(coeff[0])
                  elem_ox_list.append(coeff)

              return {key:val for key,val in zip(elem_ox_names, elem_ox_list)}

          def __get_mineral_elem_ox_dict_interest(self, mineral):
              mineral_dict = self.__get_mineral_elem_ox_dict(mineral)
              return {key:value for key,value in zip(mineral_dict.keys(), mineral_dict.values()) if key not in ["H+", "O-2"]}

          def __get_basis_from_elem(self, elem):

              basis_species_x = None

              for s in list(self.basis_df["name"]):
                  if elem in self.__get_elem_ox_of_interest_in_minerals(s):
                      basis_species_x = s

              if basis_species_x == None:
                  for s in list(self.basis_aux_df["name"]):
                      if elem in self.__get_elem_ox_of_interest_in_minerals(s):
                          basis_species_x = s

              return basis_species_x

          def __get_reaction_path(self, plot_basis_x, plot_basis_y, div_var_name):

              xi_vals=self.aq_distribution_logact["Xi"]
              proton_vals=self.aq_distribution_logact["H+"]
              x_vals=self.aq_distribution_logact[plot_basis_x]
              y_vals=self.aq_distribution_logact[plot_basis_y]

              assert len(proton_vals) == len(x_vals), f"number of proton values ({proton_vals}) should equal number of x values ({x_vals})"
              assert len(proton_vals) == len(y_vals), f"number of proton values ({proton_vals}) should equal number of y values ({y_vals})"

              x_vals = [log10((10**float(x))/(10**float(d))**_get_ion_ratio_exponent(plot_basis_x, "H+")) for x,d in zip(x_vals,proton_vals)]
              y_vals = [log10((10**float(y))/(10**float(d))**_get_ion_ratio_exponent(plot_basis_y, "H+")) for y,d in zip(y_vals,proton_vals)]

              return xi_vals, x_vals, y_vals

          def __get_plot_range(self, x_vals, y_vals):
              min_x_val = min(x_vals)
              min_y_val = min(y_vals)
              max_x_val = max(x_vals)
              max_y_val = max(y_vals)

              path_x_range = max_x_val - min_x_val
              path_y_range = max_y_val - min_y_val

              if len(list(set(x_vals))) == 1:
                  # x values form a vertical line
                  plot_x_range = [min_x_val-self.path_margin*(path_x_range+1), max_x_val+self.path_margin*(path_x_range+1)]
              else:
                  plot_x_range = [min_x_val-self.path_margin*path_x_range, max_x_val+self.path_margin*path_x_range]

              if len(list(set(y_vals))) == 1:
                  # y values form a horizontal line
                  plot_y_range = [min_y_val-self.path_margin*(path_y_range+1), max_y_val+self.path_margin*(path_y_range+1)]
              else:
                  plot_y_range = [min_y_val-self.path_margin*path_y_range, max_y_val+self.path_margin*path_y_range]

              return plot_x_range, plot_y_range

          @staticmethod
          def __get_xy_labs(plot_basis_x, plot_basis_y):
              try:
                  xlab = pychnosz.ratlab_html(plot_basis_x)
              except:
                  xlab = "log a"+chemlabel(plot_basis_x)
              try:
                  ylab = pychnosz.ratlab_html(plot_basis_y)
              except:
                  ylab = "log a"+chemlabel(plot_basis_y)

              return xlab, ylab

          def __plot_reaction_path_background(self, plot_basis_x, plot_basis_y,
                                              div_var_name, x_vals, y_vals,
                                              colormap="viridis", borders=0,
                                              field_minerals_exist=True, path_margin=0.25,
                                              plot_width=4, plot_height=3, ppi=122, res=300,
                                              annotation=None, annotation_coords=[0, 0],
                                              basis_state=None, species_state=None, messages=False):

              plot_x_range, plot_y_range = self.__get_plot_range(x_vals, y_vals)

              xlab,ylab = self.__get_xy_labs(plot_basis_x, plot_basis_y)

              args = {plot_basis_x:plot_x_range+[res],
                      plot_basis_y:plot_y_range+[res],
                      "T":self.T, "P":self.P,
                      "IS":self.IS,
                      #"IS":0,
                      "basis":basis_state,
                      "species":species_state,
                      "messages":messages}

              if field_minerals_exist:

                  # check each value of Xi to see which mineral is most predominant
                  pred_minerals_from_fields = []
                  for i,val in enumerate(x_vals):

                      args_temp = {plot_basis_x:[x_vals[i], x_vals[i], 1],
                                   plot_basis_y:[y_vals[i], y_vals[i], 1],
                                   "T":self.T, "P":self.P,
                                   "IS":self.IS,
                                   "basis":basis_state,
                                   "species":species_state,
                                   #"IS":0,
                                   "messages":messages}

                      a = pychnosz.affinity(**args_temp)
                      e = pychnosz.equilibrate(a, balance=self.__get_basis_from_elem(div_var_name), messages=messages)
                      table = pychnosz.diagram(e, balance=self.__get_basis_from_elem(div_var_name), interactive=True, fig_out=False, plot_it=False, messages=messages)

                      pred_minerals_from_fields.append(table["df"]["prednames"][0])

                  a = pychnosz.affinity(**args)
                  e = pychnosz.equilibrate(a, balance=self.__get_basis_from_elem(div_var_name), messages=messages)

                  table,fig = pychnosz.diagram_interactive(e,
                                      fill=colormap, borders=borders,
                                      balance=self.__get_basis_from_elem(div_var_name),
                                      width=plot_width*ppi, height=plot_height*ppi,
                                      xlab=xlab, ylab=ylab, annotation=annotation,
                                      annotation_coords=annotation_coords,
                                      plot_it=False, messages=messages)

                  return table, fig, pred_minerals_from_fields

              else:
                  # empty plot upon
                  fig = go.Figure(go.Scatter(x=pd.Series(dtype=object),
                                             y=pd.Series(dtype=object),
                                             mode="markers",
                                             showlegend=False,
                                             ),
                                 layout_xaxis_range=plot_x_range,
                                 layout_yaxis_range=plot_y_range,
                                 )

                  fig.add_annotation(x=annotation_coords[0],
                                     y=annotation_coords[1],
                                     xref="paper",
                                     yref="paper",
                                     align='left',
                                     text=annotation,
                                     bgcolor="rgba(255, 255, 255, 0.5)",
                                     showarrow=False)

                  fig.update_layout(
                      width=plot_width*ppi, height=plot_height*ppi,
                      xaxis={"title": xlab},
                      yaxis={"title": ylab},
                      template="simple_white",
                  )

                  fig.update_yaxes(autorange=True)

                  # Store config on figure so it persists when fig.show() is called later
                  config = {'displaylogo': False,
                            'modeBarButtonsToRemove': ['zoom2d', 'pan2d', 'zoomIn2d', 'zoomOut2d',
                                                       'autoScale2d', 'resetScale2d', 'toggleSpikelines',
                                                       'hoverClosestCartesian', 'hoverCompareCartesian'],
                            'toImageButtonOptions': {
                                'format': 'png',
                                'filename': 'reaction_path',
                                'height': plot_height*ppi,
                                'width': plot_width*ppi,
                                'scale': 1,
                            }}
                  fig._config = fig._config | config

                  return None,fig,None # a table, diagram without regions, and a list of predicted minerals at each Xi

          @staticmethod
          def __calc_dissrxn_logK(mineral, T, P, IS, basis_state):

              logK = pychnosz.subcrt([mineral], coeff=[-1], property='logK', T=T, P=P, #IS=IS,
                                     basis=basis_state,
                                     show=False, messages=False).out["logK"].item()

              return logK

          def __add_reaction_path_to_plot(self, x_vals, y_vals, xi_vals, fig,
                                          basis_species_x, basis_species_y,
                                          path_margin=0.25, projected_points=[],
                                          path_line_type = "markers+lines",
                                          path_line_color = "black",
                                          path_point_fill_color = "black",
                                          path_point_line_color = "black",
                                          projected_point_fill_color = "white",
                                          projected_point_line_color = "black"):

              min_x_val = min(x_vals)
              min_y_val = min(y_vals)
              max_x_val = max(x_vals)
              max_y_val = max(y_vals)

              path_x_range = max_x_val - min_x_val
              path_y_range = max_y_val - min_y_val

              xlab,ylab = self.__get_xy_labs(basis_species_x, basis_species_y)

              with np.errstate(divide='ignore'):
                  log_xi_vals = [round(np.log10(val), 4) for val in xi_vals]
                  log_xi_vals = ["N/A" if np.isinf(val) else val for val in log_xi_vals]

              if path_line_type in ["markers+lines", "lines"]:
                  fig.add_trace(
                      go.Scatter(
                          x=x_vals,
                          y=y_vals,
                          line=go.scatter.Line(color=path_line_color),
                          mode='lines',
                          showlegend=True,
                          name='reaction path',
                          customdata = np.stack((xi_vals, log_xi_vals), axis=-1),
                          hovertemplate = 'Xi: %{customdata[0]}<br>log Xi: %{customdata[1]}<br>'+xlab+': %{x}<br>'+ylab+': %{y}<extra></extra>',
                          legendgroup='reaction path',
                      )
                  )

              if len(projected_points) > 0 and path_line_type in ["markers+lines", "markers"]:

                  x_vals_real = []
                  x_vals_projected = []
                  y_vals_real = []
                  y_vals_projected = []
                  xi_vals_real = []
                  xi_vals_projected = []
                  for i,p in enumerate(projected_points):
                      if p == "real":
                          x_vals_real.append(x_vals[i])
                          y_vals_real.append(y_vals[i])
                          xi_vals_real.append(xi_vals[i])
                      else:
                          x_vals_projected.append(x_vals[i])
                          y_vals_projected.append(y_vals[i])
                          xi_vals_projected.append(xi_vals[i])

                  np.seterr(divide='ignore') # todo: reset np warnings
                  log_xi_vals_real = [round(np.log10(val), 4) for val in xi_vals_real]
                  log_xi_vals_real = ["N/A" if np.isinf(val) else val for val in log_xi_vals_real]
                  log_xi_vals_projected = [round(np.log10(val), 4) for val in xi_vals_projected]
                  log_xi_vals_projected = ["N/A" if np.isinf(val) else val for val in log_xi_vals_projected]

                  if len(x_vals_real) > 0:
                      fig.add_trace(
                          go.Scatter(
                              x=x_vals_real,
                              y=y_vals_real,
                              marker=dict(
                                  color=path_point_fill_color,
                                  line=dict(
                                      color=path_point_line_color,
                                      width=2
                                  )
                              ),
                              mode='markers',
                              showlegend=False,
                              name='reaction path',
                              customdata = np.stack((xi_vals_real, log_xi_vals_real), axis=-1),
                              hovertemplate = 'Xi: %{customdata[0]}<br>log Xi: %{customdata[1]}<br>'+xlab+': %{x}<br>'+ylab+': %{y}<extra></extra>',
                              legendgroup='reaction path',
                          )
                      )
                  if len(x_vals_projected) > 0:

                      fig.add_trace(
                          go.Scatter(
                              x=x_vals_projected,
                              y=y_vals_projected,
                              marker=dict(
                                  color=projected_point_fill_color,
                                  line=dict(
                                      color=projected_point_line_color,
                                      width=2
                                  )
                              ),
                              mode='markers',
                              showlegend=False,
                              name='reaction path',
                              customdata = np.stack((xi_vals_projected, log_xi_vals_projected), axis=-1),
                              hovertemplate = 'Xi: %{customdata[0]}<br>log Xi: %{customdata[1]}<br>'+xlab+': %{x}<br>'+ylab+': %{y}<extra></extra>',
                              legendgroup='reaction path',
                          )
                      )

              fig.update_xaxes(range=self.__get_plot_range(x_vals, y_vals)[0], autorange=False)
              fig.update_yaxes(range=self.__get_plot_range(x_vals, y_vals)[1], autorange=False)

              return fig

          def __plot_reaction_path_main(self,
                                        triad, T=25, P=1, IS=0, path_margin=0.25,
                                        flip_xy=False,
                                        show_annotation=False,
                                        annotation_coords=[0,0],
                                        show_nonparticipating_mineral_lines = False,
                                        minerals_to_show=[],
                                        path_line_type = "markers+lines",
                                        path_line_color = "black",
                                        path_point_fill_color = "black",
                                        path_point_line_color = "black",
                                        projected_point_color = "white",
                                        projected_point_fill_color= "white",
                                        projected_point_line_color = "black",
                                        h_line_color="red",
                                        v_line_color="blue",
                                        d_line_color="orange",
                                        res=300,
                                        plot_width=4,
                                        plot_height=3,
                                        ppi=122,
                                        colormap="viridis",
                                        borders=0,
                                        projected_points=[],
                                        first_pass=False):

              e_pair = triad[0:2]

              if len(triad) == 3:
                  div_var_name = triad[2]
              elif len(triad) == 2:
                  div_var_name = "None"

              if flip_xy:
                  e_pair.reverse()

              basis_species_x = self.__get_basis_from_elem(e_pair[0])
              basis_species_y = self.__get_basis_from_elem(e_pair[1])

              basis_sp_list = list(set([self.__get_basis_from_elem(e) for e in triad] + ["H+","H2O"]))

              try:
                  b = pychnosz.basis(basis_sp_list, global_state=False, messages=False)
              except:
                  b = pychnosz.basis(basis_sp_list + ["H2"], global_state=False, messages=False)

              elems = []
              for elem in triad:
                  elems.append(elem.split("+")[0].split("-")[0])

              mineral_names = []
              for elem in triad:
                  elem = elem.split("+")[0].split("-")[0]
                  m_idx = pychnosz.retrieve((elem), list(set(["O", "H"]+elems)), state=["cr"], messages=False)
                  if len(m_idx) > 0:
                      mineral_names += list(pychnosz.info(list(m_idx), messages=False)["name"])

              # exclude inactive minerals
              mineral_names_active = [m for m in mineral_names if m in list(self.df["name"])]
              mineral_names = list(set(mineral_names_active))

              mineral_formula_ox = [self.__get_elem_ox_of_interest_in_minerals(m) for m in mineral_names]
              mineral_formula_ox_singles = [e if len(e)==1 else [] for e in mineral_formula_ox]
              mineral_formula_ox_doubles = [e if len(e)==2 else [] for e in mineral_formula_ox]
              mineral_formula_ox_triples = [e if len(e)==3 else [] for e in mineral_formula_ox]

              retrieved_minerals = []
              for i,s in enumerate(mineral_formula_ox_singles):
                  if [e_pair[0]] == s:
                      retrieved_minerals.append(mineral_names[i])
              x_minerals_to_plot = retrieved_minerals

              retrieved_minerals = []
              for i,s in enumerate(mineral_formula_ox_singles):
                  if [e_pair[1]] == s:
                      retrieved_minerals.append(mineral_names[i])
              y_minerals_to_plot = retrieved_minerals

              xy_minerals_to_plot = []
              for i,s in enumerate(mineral_formula_ox_doubles):
                  if e_pair[0] in s and e_pair[1] in s:
                      xy_minerals_to_plot.append(mineral_names[i])

              if len(x_minerals_to_plot) > 1:
                  spcs = pychnosz.species(x_minerals_to_plot, add=True, global_state=False, basis=b, messages=False)
              if len(y_minerals_to_plot) > 1:
                  spcs = pychnosz.species(y_minerals_to_plot, add=True, global_state=False, basis=b, messages=False)
              if len(xy_minerals_to_plot) > 1:
                  spcs = pychnosz.species(xy_minerals_to_plot, add=True, global_state=False, basis=b, messages=False)

      #         print("x, y, xy")
      #         print(x_minerals_to_plot)
      #         print(y_minerals_to_plot)
      #         print(xy_minerals_to_plot)

              field_minerals_to_plot = []
              for i,s in enumerate(mineral_formula_ox_triples):
                  if e_pair[0] in s and e_pair[1] in s and div_var_name in s:
                      field_minerals_to_plot.append(mineral_names[i])
              for i,s in enumerate(mineral_formula_ox_doubles):
                  if e_pair[0] in s and div_var_name in s:
                      field_minerals_to_plot.append(mineral_names[i])
                  elif e_pair[1] in s and div_var_name in s:
                      field_minerals_to_plot.append(mineral_names[i])
              for i,s in enumerate(mineral_formula_ox_singles):
                  if div_var_name in s:
                      field_minerals_to_plot.append(mineral_names[i])

              field_minerals_exist = True
              try:
                  # get minerals relevant to plotted element pair
                  spcs = pychnosz.species(field_minerals_to_plot, global_state=False, basis=b, messages=False)#, add=True)
              except:
                  field_minerals_exist = False

      #         print("field minerals")
      #         print(field_minerals_to_plot)

              xi_vals, x_vals, y_vals = self.__get_reaction_path(basis_species_x, basis_species_y, div_var_name)

              if self.P <= 1:
                  bar_bars = "bar"
              else:
                  bar_bars = "bars"

              if show_annotation and div_var_name != "None":
                  annotation = "Balanced on: "+chemlabel(self.__get_basis_from_elem(div_var_name))+"<br>"+'%g'%(self.T)+" °C, "+'%g'%(self.P)+" "+bar_bars
              elif show_annotation:
                  annotation = '%g'%(self.T)+" °C, "+'%g'%(self.P)+" "+bar_bars
              else:
                  annotation = None

              table,fig,pred_minerals_from_fields = self.__plot_reaction_path_background(
                  basis_species_x, basis_species_y, div_var_name, x_vals, y_vals,
                  plot_width=plot_width, plot_height=plot_height, ppi=ppi, res=res,
                  colormap=colormap, borders=borders,
                  annotation_coords=annotation_coords,
                  field_minerals_exist=field_minerals_exist, path_margin=self.path_margin,
                  annotation=annotation, basis_state=b, species_state=spcs, messages=False)

              # plot minerals with a single element of interest as a line
              plot_x_range, plot_y_range = self.__get_plot_range(x_vals, y_vals)

              line_styles = ['dot', 'dash', 'dashdot', 'longdash', 'longdashdot']
              line_i = 0
              line_width = 1

              if first_pass:
                  fig = None
              else:
                  for mineral in x_minerals_to_plot + y_minerals_to_plot + xy_minerals_to_plot:

                      if not show_nonparticipating_mineral_lines and mineral not in list(self.moles_product_minerals.columns) and mineral not in minerals_to_show:
                          continue

                      # deal with mineral line style (dot, dashed, etc.)
                      if line_i % 5 == 0:
                          if line_i != 0:
                              line_width += 0.5
                          line_i = 0

                      line_style = line_styles[line_i]

                      eoi = self.__get_elem_ox_of_interest_in_minerals(mineral)

                      logK = self.__calc_dissrxn_logK(mineral, T, P, IS, b)
                      mineral_formula_dict = self.__get_mineral_elem_ox_dict_interest(mineral)

                      xlab,ylab = self.__get_xy_labs(basis_species_x, basis_species_y)

                      if len(eoi) == 1:

                          # if the element of interest is not in the current element pair, move on
                          if eoi[0] not in e_pair:
                              continue

                          if self.__get_elem_ox_of_interest_in_minerals(mineral)[0] == e_pair[0]:
                              # vertical line
                              x0, x1 = (1/mineral_formula_dict[e_pair[0]])*logK, (1/mineral_formula_dict[e_pair[0]])*logK
                              y0 = min(plot_y_range)
                              y1 = max(plot_y_range)
                              color = v_line_color
                              hovertemplate=mineral+'<br>'+xlab+' = '+str(round(x0, 3))
                          elif self.__get_elem_ox_of_interest_in_minerals(mineral)[0] == e_pair[1]:
                              # horizontal line
                              y0, y1 = (1/mineral_formula_dict[e_pair[1]])*logK, (1/mineral_formula_dict[e_pair[1]])*logK
                              x0 = min(plot_x_range)
                              x1 = max(plot_x_range)
                              color=h_line_color
                              hovertemplate=mineral+'<br>'+ylab+' = '+str(round(y0))

                      if len(eoi) == 2:

                          line_slope = mineral_formula_dict[e_pair[0]]/mineral_formula_dict[e_pair[1]]

                          x0 = min(plot_x_range)
                          x1 = max(plot_x_range)
                          y0 = (1/mineral_formula_dict[e_pair[1]])*logK - line_slope*x0
                          y1 = (1/mineral_formula_dict[e_pair[1]])*logK - line_slope*x1

                          intercept = (1/mineral_formula_dict[e_pair[1]])*logK

                          color = d_line_color

                          hovertemplate = mineral+'<br>slope = '+str(round(line_slope))+'<br>intercept = '+str(round(intercept))+'<extra></extra>'

                      fig.add_trace(
                          go.Scatter(x=[x0, x1], y=[y0, y1], mode="lines",
                                     name=mineral,
                                     line=dict(color=color, width=line_width, dash=line_style),
                                     hovertemplate=hovertemplate,
                                     showlegend=True,
                                    ),
                      )

                      line_i += 1

                  fig = self.__add_reaction_path_to_plot(x_vals, y_vals, xi_vals, fig,
                                                         basis_species_x, basis_species_y,
                                                         path_margin=self.path_margin,
                                                         projected_points=projected_points,
                                                         path_line_type=path_line_type,
                                                         path_line_color=path_line_color,
                                                         path_point_fill_color=path_point_fill_color,
                                                         path_point_line_color=path_point_line_color,
                                                         projected_point_fill_color=projected_point_fill_color,
                                                         projected_point_line_color=projected_point_line_color)

                  # Enable legend display
                  fig.update_layout(showlegend=True)

              pred_minerals_from_lines = x_minerals_to_plot+y_minerals_to_plot+xy_minerals_to_plot
              return fig, pred_minerals_from_fields, pred_minerals_from_lines

          def __get_xlab_xvar(self, x_type):

              x_type_dict = {
                  "logxi" : ["log Xi", "log Xi"],
                  "xi" : ["Xi", "Xi"],
                  "temperature" : ["Temp(C)", "Temperature, °C"],
                  "pressure" : ["Press(bars)", "Pressure, bars"],
                  "pH" : ["pH", "pH"],
                  "pmH" : ["pmH", "pmH"],
                  "logfO2" : ["log fO2", "log <i>f</i>O<sub>2</sub>"],
                  "Eh" : ["Eh(v)", "Eh, volts"],
                  "pe" : ["pe", "pe"],
                  "aw" : ["aw", "aw"],
              }

              if x_type not in x_type_dict.keys():
                  self.err_handler.raise_exception(("x_type must be set to either "
                      "'logxi', 'xi', 'temperature', 'pressure', 'pH', 'pmH',"
                      "'log fO2', 'Eh(v)', 'pe', or 'aw'."))

              xvar = x_type_dict[x_type][0]
              xlab = x_type_dict[x_type][1]

              return xlab, xvar

          def plot_elements(self, plot_elements=None, units="molality", log=True,
                            x_type="logxi", plot_width=4, plot_height=3, ppi=122,
                            ylim=None, show_legend=True, charge_sign_at_end=False,
                            save_as=None, save_format=None, save_scale=1):

              """
              Generate a line plot of the log activities of aqueous species as a
              function of the log of the extent of reaction (log Xi).

              Parameters
              ----------
              plot_elements : list of str, optional
                  A list of elements to plot. If undefined, every element will be
                  plotted at once.

              units : str, default "molality"
                  Units of elemental abundance to plot. Can be "molality" or "ppm".

              log : bool, default True
                  Display elemental abundances in log scale?

              plot_width, plot_height : numeric, default 4 by 3
                  Width and height of the plot, in inches. Size of interactive plots
                  is also determined by pixels per inch, set by the parameter `ppi`.

              ppi : numeric, default 122
                  Pixels per inch. Along with `plot_width` and `plot_height`,
                  determines the size of interactive plots.

              ylim : list of two numeric values, optional
                  Minimum and maximum value of the y-axis.

              show_legend : bool, default True
                  Show the legend?

              charge_sign_at_end : bool, default False
                  Display charge with sign after the number (e.g. SO4 2-)?

              save_as : str, optional
                  Provide a filename to save this figure. Filetype of saved figure is
                  determined by `save_format`.
                  Note: interactive plots can be saved by clicking the 'Download plot'
                  button in the plot's toolbar.

              save_format : str, default "png"
                  Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                  'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                  an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                  and 'webp' can be downloaded with the 'download as' button in the
                  toolbar of an interactive plot.

              save_scale : numeric, default 1
                  Multiply title/legend/axis/canvas sizes by this factor when saving
                  the figure.

              Returns
              -------
              fig : Plotly figure object
                  A line plot.
              """

              title = "Concentrations of dissolved elements"
              if units == "molality":
                  df = pd.concat([self.dissolved_elements_molal, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                  ylab = "{}molality"
              elif units == "ppm":
                  df = pd.concat([self.dissolved_elements_ppm, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                  ylab = "{}ppm"
      #             elif units == "molarity":

              plot_columns = [col for col in df.columns]
              if isinstance(plot_elements, list):
                  plot_columns_temp = [col for col in plot_columns if col in plot_elements]
                  plot_columns = plot_columns_temp

              df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
              df.columns = list(self.misc_params.columns)+["variable", "value"]
              df["variable"] = df["variable"].apply(chemlabel, charge_sign_at_end=charge_sign_at_end)

              df["Xi"] = pd.to_numeric(df["Xi"])

              df["value"] = pd.to_numeric(df["value"])
              df["value"] = df["value"].fillna(0)
              df["value"] = df["value"].replace(0, np.nan)

              with np.errstate(divide='ignore'):
                  df['log Xi'] = np.log10(df['Xi'])
                  if log:
                      df['value'] = np.log10(df['value'])
                      ylab = ylab.format("log ")
              if not log:
                  ylab = ylab.format("")

              xlab, xvar = self.__get_xlab_xvar(x_type)

              fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                                    width=plot_width*ppi,  height=plot_height*ppi,
                                    labels=dict(value=ylab, x=xlab), render_mode='svg',
                                   )

              fig.update_layout(xaxis_title=xlab,
                                yaxis_title=ylab,
                                legend_title=None,
                                showlegend=show_legend,
                                yaxis={'showexponent': 'all',
                                       'exponentformat': 'power'},
                               )

              if isinstance(title, str):
                  fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

              if isinstance(ylim, list):
                  fig.update_layout(yaxis_range=ylim)

              if isinstance(save_as, str):
                  dummy_sp = Speciation({})
                  save_as, save_format = dummy_sp._save_figure(fig,
                          save_as, save_format, save_scale,
                          plot_width, plot_height, ppi)

              return fig

          def plot_pH(self, x_type="logxi", show_neutrality=True, title=None,
                      plot_width=4, plot_height=3, ppi=122, ylim=None, save_as=None,
                      save_format=None, save_scale=1):

              """
              Generate a line plot of pH as a function of the log of the extent of
              reaction (log Xi) or some other variable.

              Parameters
              ----------
              x_type : str, default "logxi"
                  Variable to appear on the x-axis. Can be "logxi", "xi",
                  "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                  "aw".

              show_neutrality : bool, default True,
                  Display a reference line representing neutral pH? Setting this
                  option to True requires a thermodynamic database in a WORM-style CSV
                  format, e.g., 'wrm_data.csv'.

              title : str
                  Title of the plot to display.

              plot_width, plot_height : numeric, default 4 by 3
                  Width and height of the plot, in inches. Size of interactive plots
                  is also determined by pixels per inch, set by the parameter `ppi`.

              ppi : numeric, default 122
                  Pixels per inch. Along with `plot_width` and `plot_height`,
                  determines the size of interactive plots.

              ylim : list of two numeric values, optional
                  Minimum and maximum value of the y-axis.

              save_as : str, optional
                  Provide a filename to save this figure. Filetype of saved figure is
                  determined by `save_format`.
                  Note: interactive plots can be saved by clicking the 'Download plot'
                  button in the plot's toolbar.

              save_format : str, default "png"
                  Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                  'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                  an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                  and 'webp' can be downloaded with the 'download as' button in the
                  toolbar of an interactive plot.

              save_scale : numeric, default 1
                  Multiply title/legend/axis/canvas sizes by this factor when saving
                  the figure.

              Returns
              -------
              fig : Plotly figure object
                  A line plot.
              """

              df = copy.deepcopy(self.misc_params)

              xlab, xvar = self.__get_xlab_xvar(x_type)

              if x_type == "logxi":
                  with np.errstate(divide='ignore'):
                      df['log Xi'] = np.log10(df['Xi'])

              fig = px.line(df, x=xvar, y="pH", template="simple_white",
                            width=plot_width*ppi,  height=plot_height*ppi,
                            labels=dict(value="pH", x=xlab), render_mode='svg',
                            )

              fig.update_layout(xaxis_title=xlab,
                                yaxis_title="pH",
                                showlegend=False)

              if isinstance(title, str):
                  fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

              if isinstance(ylim, list):
                  fig.update_layout(yaxis_range=ylim)

              if show_neutrality and isinstance(self.thermo.csv_db, pd.DataFrame):
                  _, df_pH = self.plot_energy(species=["H2O", "H+", "OH-"],
                                              stoich=[-1, 1, 1],
                                              divisor=-2,
                                              x_type=x_type,
                                              y_type="logK",
                                              df_out=True,
                                              )

                  fig.add_trace(go.Scatter(x=df_pH[xvar],
                                           y=df_pH["logK"],
                                           mode='lines',
                                           name='neutral pH',
                                           showlegend=True,
                                           hovertemplate = xlab+': %{x}<br>pH: %{y}<extra></extra>',
                                           line=dict(color='silver',
                                                     width=3,
                                                     dash='dot')
                                          ))

                  fig['data'][0]['showlegend']=True
                  fig['data'][0]['name']='pH'
                  fig['data'][1]['showlegend']=True
                  fig['data'][1]['name']='neutral pH'
                  fig.update_layout(showlegend=True)

              if isinstance(save_as, str):
                  dummy_sp = Speciation({})
                  save_as, save_format = dummy_sp._save_figure(fig,
                          save_as, save_format, save_scale,
                          plot_width, plot_height, ppi)

              return fig

          def plot_product_minerals(self, show_reactant_minerals=False,
                                    plot_minerals=None, x_type="logxi", y_type="mole",
                                    log_y=True, df_out=False, markers=False,
                                    plot_width=4, plot_height=3, ppi=122, ylim=None,
                                    show_legend=True, save_as=None, save_format=None,
                                    save_scale=1):

              """
              Generate a line plot of the log moles of product minerals as a
              function of the log of the extent of reaction (log Xi) or some other
              variable.

              Parameters
              ----------
              show_reactant_minerals : bool, default False
                  Include log moles of reactant minerals?

              plot_minerals : list, optional
                  List of minerals to plot. Useful for isolating one or more
                  minerals.

              x_type : str, default "logxi"
                  Variable to appear on the x-axis. Can be "logxi", "xi",
                  "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                  "aw".

              y_type : str, default 'mole'
                  The variable to plot on the y-axis. Can be either 'mole' (for moles
                  of minerals), 'mass' (for masses of minerals), or 'volume' (for
                  volumes of minerals).

              log_y : bool, default True
                  Should the y-axis be logarithmic?

              df_out : bool, default False
                  Should a dataframe of values also be returned? For example, if
                  `y_type` is set to 'volume', should a table of mineral volumes be
                  returned?

              markers : bool, default True
                  Add circular markers to lines to indicate calculation steps?

              plot_width, plot_height : numeric, default 4 by 3
                  Width and height of the plot, in inches. Size of interactive plots
                  is also determined by pixels per inch, set by the parameter `ppi`.

              ppi : numeric, default 122
                  Pixels per inch. Along with `plot_width` and `plot_height`,
                  determines the size of interactive plots.

              ylim : list of two numeric values, optional
                  Minimum and maximum value of the y-axis.

              show_legend : bool, default True
                  Show the legend?

              save_as : str, optional
                  Provide a filename to save this figure. Filetype of saved figure is
                  determined by `save_format`.
                  Note: interactive plots can be saved by clicking the 'Download plot'
                  button in the plot's toolbar.

              save_format : str, default "png"
                  Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                  'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                  an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                  and 'webp' can be downloaded with the 'download as' button in the
                  toolbar of an interactive plot.

              save_scale : numeric, default 1
                  Multiply title/legend/axis/canvas sizes by this factor when saving
                  the figure.

              Returns
              -------
              fig : Plotly figure object
                  A line plot.

              df : a Pandas dataframe
                  A dataframe is only returned if `df_out` is set to True (it is
                  set to False by default).

              """

              xlab, xvar = self.__get_xlab_xvar(x_type)

              if show_reactant_minerals:
                  df = pd.concat([self.moles_minerals, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                  title = "{}{} of reactant and product minerals"
              else:
                  df = pd.concat([self.moles_product_minerals, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                  title = "{}{} of product minerals"

              if log_y:
                  log_text = "log "
              else:
                  log_text = ""

              # sort in order of appearance along Xi
              sort_order = list(self.moles_minerals.columns)

              sort_order = [v for v in sort_order if v not in self.misc_params.columns]

              if y_type == "mole":
                  ylab = "{}moles".format(log_text)
                  title = title.format(log_text, "moles")
              elif y_type == "mass": # not yet supported
                  y_lab = "{}grams".format(log_text)
                  title = title.format(log_text, "masses")
                  self.err_handler.raise_exception("Plotting mineral masses is not yet "
                          "supported.")
              elif y_type == "volume":

                  if not isinstance(self.df, pd.DataFrame):
                      self.err_handler.raise_exception("Plotting mineral volume "
                              "requires a WORM-formatted CSV thermodynamic database. "
                              "You may be seeing this message because your speciation "
                              "used a data0-type thermodynamic database (e.g., 'wrm'), "
                              "which does not contain mineral volume data.")

                  ylab = "{}cm<sup>3</sup>".format(log_text)
                  title = title.format(log_text, "volumes")
                  temps = df["Temp(C)"]

                  minerals = [col for col in df.columns if col not in list(self.misc_params.columns)]

                  for i,T in enumerate(temps):
                      for ii,mineral in enumerate(minerals):
                          mineral_df = copy.deepcopy(self.df[self.df["name"]==mineral])
                          polymorph_idxs = []
                          for iii in range(0, mineral_df.shape[0]): # loop through mineral polymorphs

                              if float(T) < float(list(mineral_df["z.T"])[0]):
                                  polymorph_idxs.append(iii)
                          if len(polymorph_idxs)==0:
                              polymorph_idx = iii
                          else:
                              polymorph_idx = polymorph_idxs[0]

                          partial_molal_volume = list(mineral_df["V"])[polymorph_idx]

                          df.at[i, mineral] = df[mineral][i]*partial_molal_volume
              else:
                  self.err_handler.raise_exception("y_type must be either 'mole', "
                              "'mass', or 'volume'.")

              plot_columns = [col for col in df.columns if col not in list(self.misc_params.columns)]

              if isinstance(plot_minerals, list):
                  plot_columns_temp = [col for col in plot_columns if col in plot_minerals]
                  plot_columns = plot_columns_temp

              plot_columns = sorted(plot_columns, key=sort_order.index)

              df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
              df.columns = list(self.misc_params.columns)+["variable", "value"]

              df = df[df["variable"] != "None"]

              df["Xi"] = pd.to_numeric(df["Xi"])

              df["value"] = pd.to_numeric(df["value"])
              df["value"] = df["value"].fillna(0)

              with np.errstate(divide='ignore'):
                  df['log Xi'] = np.log10(df['Xi'])
                  if log_y:
                      df["value"] = df["value"].replace(0, np.nan)
                      df['value'] = np.log10(df['value'])

              fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                            width=plot_width*ppi,  height=plot_height*ppi, markers=markers,
                            labels=dict(value=ylab, x=xlab), render_mode='svg',
                            )

              df_to_return = copy.deepcopy(df)

              # add lines that go to -9999 (representing -Inf) in log y plots
              if log_y:
                  if not isinstance(ylim, list):
                      # grab the automatic y-axis range from the plot, above
                      full_fig = fig.full_figure_for_development(warn=False)
                      ylim = list(full_fig.layout.yaxis.range)

                  # add new rows
                  for irow in range(0, df.shape[0]):
                      if not isnan(df["value"].iloc[irow]) and irow != 0 and irow != df.shape[0]-1:
                          if isnan(df["value"].iloc[irow-1]):

                              df_dict = {}
                              for col in list(self.misc_params.columns)+["log Xi"]:
                                  df_dict[col] = df.loc[irow-1, col]
                              df_dict["variable"] = df.loc[irow-1, "variable"]
                              df_dict["value"] = df.loc[irow-1, "value"]

                              new_row = pd.DataFrame(df_dict, index=[irow-1])
                              df = pd.concat([df.iloc[:irow], new_row, df.iloc[irow:]]).reset_index(drop=True)
                              irow=0

                          if isnan(df["value"].iloc[irow+1]):

                              df_dict = {}
                              for col in list(self.misc_params.columns)+["log Xi"]:
                                  df_dict[col] = df.loc[irow+1, col]
                              df_dict["variable"] = df.loc[irow+1, "variable"]
                              df_dict["value"] = df.loc[irow+1, "value"]

                              new_row = pd.DataFrame(df_dict, index=[irow+1])
                              df = pd.concat([df.iloc[:irow], new_row, df.iloc[irow:]]).reset_index(drop=True)
                              irow=0

                  # fill new rows with -9999 value
                  for irow in range(0, df.shape[0]):
                      if not isnan(df["value"].iloc[irow]) and irow != 0 and irow != df.shape[0]-1:
                          if isnan(df["value"].iloc[irow-1]):
                              df.loc[irow-1, "value"] = -9999
                          if isnan(df["value"].iloc[irow+1]):
                              df.loc[irow+1, "value"] = -9999

                  # re-create the plot with log values down to -9999. This will screw up the
                  # automatic y-axis range, but we grabbed it from the first generated plot.
                  # We will reset the range in update_layout(yaxis_range) a little later.
                  fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                                width=plot_width*ppi,  height=plot_height*ppi, markers=markers,
                                labels=dict(value=ylab, x=xlab), render_mode='svg',
                                )

              fig.update_layout(xaxis_title=xlab,
                                yaxis_title=ylab,
                                legend_title=None,
                                showlegend=show_legend)

              if isinstance(title, str):
                  fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

              if isinstance(ylim, list):
                  fig.update_layout(yaxis_range=ylim)

              if isinstance(save_as, str):
                  dummy_sp = Speciation({})
                  save_as, save_format = dummy_sp._save_figure(fig,
                          save_as, save_format, save_scale,
                          plot_width, plot_height, ppi)

              if df_out:
                  df_to_return = pd.pivot_table(df_to_return, index='log Xi', columns='variable', values='value').reset_index()
                  df_to_return.columns = [col for col in df_to_return.columns[:]] # make index column name blank
                  return df_to_return, fig
              else:
                  return fig

          def plot_mineral_saturation(self, solid_solutions=False, plot_minerals=None,
                                            x_type="logxi", y_type="affinity", log_y=True,
                                            df_out=False, markers=False, title=None,
                                            plot_width=4, plot_height=3, ppi=122, ylim=None,
                                            show_legend=True, save_as=None, save_format=None,
                                            save_scale=1):

              """
              Generate a line plot of the saturation indices of minerals or solid
              solutions as a function of the log of the extent of reaction (log Xi) or
              some other variable.

              Parameters
              ----------
              plot_minerals : list, optional
                  List of minerals to plot. Useful for isolating one or more
                  minerals.

              x_type : str, default "logxi"
                  Variable to appear on the x-axis. Can be "logxi", "xi",
                  "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                  "aw".

              y_type : str, default 'affinity'
                  The variable to plot on the y-axis. Can be either 'affinity'
                  or 'logQ/K'.

              df_out : bool, default False
                  Should a dataframe of values also be returned? For example, if
                  `y_type` is set to 'volume', should a table of mineral volumes be
                  returned?

              markers : bool, default True
                  Add circular markers to lines to indicate calculation steps?

              title : str, optional
                  Used to customize the title of the plot.

              plot_width, plot_height : numeric, default 4 by 3
                  Width and height of the plot, in inches. Size of interactive plots
                  is also determined by pixels per inch, set by the parameter `ppi`.

              ppi : numeric, default 122
                  Pixels per inch. Along with `plot_width` and `plot_height`,
                  determines the size of interactive plots.

              ylim : list of two numeric values, optional
                  Minimum and maximum value of the y-axis.

              show_legend : bool, default True
                  Show the legend?

              save_as : str, optional
                  Provide a filename to save this figure. Filetype of saved figure is
                  determined by `save_format`.
                  Note: interactive plots can be saved by clicking the 'Download plot'
                  button in the plot's toolbar.

              save_format : str, default "png"
                  Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                  'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                  an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                  and 'webp' can be downloaded with the 'download as' button in the
                  toolbar of an interactive plot.

              save_scale : numeric, default 1
                  Multiply title/legend/axis/canvas sizes by this factor when saving
                  the figure.

              Returns
              -------
              fig : Plotly figure object
                  A line plot.

              df : a Pandas dataframe
                  A dataframe is only returned if `df_out` is set to True (it is
                  set to False by default).

              """

              xlab, xvar = self.__get_xlab_xvar(x_type)

              if not isinstance(title, str):
                  title = "Saturation indices of "

              if not solid_solutions:
                  if y_type == "affinity":
                      df = pd.concat([self.saturation_states_pure_solids_affinity,
                                      self.misc_params[self.misc_params.columns[1:]]], axis=1)
                      ylab = "affinity, kcal/mol"
                  else:
                      df = pd.concat([self.saturation_states_pure_solids_log_Q_over_K,
                                      self.misc_params[self.misc_params.columns[1:]]], axis=1)
                      ylab = "logQ/K"

                  title = title + "minerals"
              else:
                  if y_type == "affinity":
                      df = pd.concat([self.saturation_states_solid_solutions_affinity,
                                      self.misc_params[self.misc_params.columns[1:]]], axis=1)
                      ylab = "affinity, kcal/mol"
                  else:
                      df = pd.concat([self.saturation_states_solid_solutions_log_Q_over_K,
                                      self.misc_params[self.misc_params.columns[1:]]], axis=1)
                      ylab = "logQ/K"
                  title = title + "solid solutions"

              plot_columns = [col for col in df.columns if col not in list(self.misc_params.columns)]

              if isinstance(plot_minerals, list):
                  plot_columns_temp = [col for col in plot_columns if col in plot_minerals]
                  plot_columns = plot_columns_temp

              df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
              df.columns = list(self.misc_params.columns)+["variable", "value"]

              df = df[df["variable"] != "None"]

              df["Xi"] = pd.to_numeric(df["Xi"])

              df["value"] = pd.to_numeric(df["value"])
              df["value"] = df["value"].fillna(0)

              with np.errstate(divide='ignore'):
                  df['log Xi'] = np.log10(df['Xi'])

              fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                            width=plot_width*ppi,  height=plot_height*ppi, markers=markers,
                            labels=dict(value=ylab, x=xlab), render_mode='svg',
                            )

              fig.update_layout(xaxis_title=xlab,
                                yaxis_title=ylab,
                                legend_title=None,
                                showlegend=show_legend)

              if isinstance(title, str):
                  fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

              if isinstance(ylim, list):
                  fig.update_layout(yaxis_range=ylim)

              if isinstance(save_as, str):
                  dummy_sp = Speciation({})
                  save_as, save_format = dummy_sp._save_figure(fig,
                          save_as, save_format, save_scale,
                          plot_width, plot_height, ppi)

              return fig

          def plot_aqueous_species(self, plot_basis=False, plot_species=None,
                                   x_type="logxi", y_type="log activity",
                                   initially_visible=None, show_legend=True,
                                   charge_sign_at_end=False,
                                   plot_width=4, plot_height=3, ppi=122, xlim=None,
                                   ylim=None, save_as=None, save_format=None,
                                   save_scale=1):

              """
              Generate a line plot of the log activities of aqueous species as a
              function of the log of the extent of reaction (log Xi) or some other
              variable.

              Parameters
              ----------
              plot_basis : bool, default False
                  Plot basis species only?

              plot_species : list of str, optional
                  A list of aqueous species to plot. If undefined, every species at
                  will be plotted at once.

              x_type : str, default "logxi"
                  Variable to appear on the x-axis. Can be "logxi", "xi",
                  "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                  "aw".

              y_type : str, default 'log activity'
                  The variable to plot on the y-axis. Can be either 'log activity',
                  'molality', or 'log molality'.

              initially_visible : list of str, optional
                  A list of aqueous species that will be visible on the plot
                  initially. All other species will be hidden, but can still be
                  toggled back on in the legend.

              show_legend : bool, default True
                  Show the legend?

              charge_sign_at_end : bool, default False
                  Display charge with sign after the number (e.g. SO4 2-)?

              plot_width, plot_height : numeric, default 4 by 3
                  Width and height of the plot, in inches. Size of interactive plots
                  is also determined by pixels per inch, set by the parameter `ppi`.

              ppi : numeric, default 122
                  Pixels per inch. Along with `plot_width` and `plot_height`,
                  determines the size of interactive plots.

              xlim, ylim : list of two numeric values, optional
                  Minimum and maximum value of the x-axis and y-axis, respectively.

              save_as : str, optional
                  Provide a filename to save this figure. Filetype of saved figure is
                  determined by `save_format`.
                  Note: interactive plots can be saved by clicking the 'Download plot'
                  button in the plot's toolbar.

              save_format : str, default "png"
                  Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                  'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                  an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                  and 'webp' can be downloaded with the 'download as' button in the
                  toolbar of an interactive plot.

              save_scale : numeric, default 1
                  Multiply title/legend/axis/canvas sizes by this factor when saving
                  the figure.

              Returns
              -------
              fig : Plotly figure object
                  A line plot.
              """

              if plot_basis:

                  df = pd.concat([self.basis_logact, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                  title = "Solute basis species"
                  ylab = "log activity"

              else:
                  if y_type == "log activity":
                      df = pd.concat([self.aq_distribution_logact, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                      ylab = "log activity"
                  elif y_type == "molality":
                      df = pd.concat([self.aq_distribution_molal, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                      ylab = "molality"
                  elif y_type == "log molality":
                      df = pd.concat([self.aq_distribution_logmolal, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                      ylab = "log molality"
                  else:
                      self.err_handler.raise_exception("The chosen 'y_type' parameter "
                          "is not recognized. 'y_type' can be 'log activity', "
                          "'molality', or 'log molality'")
                  title = "Solute species"

              plot_columns = [col for col in df.columns]
              if isinstance(plot_species, list):
                  plot_columns_temp = [col for col in plot_columns if col in plot_species]
                  plot_columns = plot_columns_temp

              df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
              df.columns = list(self.misc_params.columns)+["variable", "value"]

              df["variable"] = df["variable"].apply(chemlabel, charge_sign_at_end=charge_sign_at_end)

              df["Xi"] = pd.to_numeric(df["Xi"])

              df["value"] = pd.to_numeric(df["value"])
              df["value"] = df["value"].fillna(0)
              df["value"] = df["value"].replace(0, np.nan)

              with np.errstate(divide='ignore'):
                  df['log Xi'] = np.log10(df['Xi'])

              xlab, xvar = self.__get_xlab_xvar(x_type)

              fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                                    width=plot_width*ppi,  height=plot_height*ppi,
                                    labels=dict(value=ylab, x=xlab), render_mode='svg',
                                   )

              if isinstance(initially_visible, list):
                  initially_visible_html = [chemlabel(sp, charge_sign_at_end=charge_sign_at_end) for sp in initially_visible]
                  for trace in fig['data']:
                      if (not trace['name'] in initially_visible_html):
                          trace['showlegend'] = True
                          trace['visible'] = 'legendonly'

              fig.update_layout(xaxis_title=xlab,
                                yaxis_title=ylab,
                                legend_title=None,
                                showlegend=show_legend)

              if isinstance(title, str):
                  fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

              if isinstance(ylim, list):
                  fig.update_layout(yaxis_range=ylim)

              if isinstance(xlim, list):
                  fig.update_layout(xaxis_range=xlim)

              if isinstance(save_as, str):
                  dummy_sp = Speciation({})
                  save_as, save_format = dummy_sp._save_figure(fig,
                          save_as, save_format, save_scale,
                          plot_width, plot_height, ppi)

              return fig

          def plot_energy(self, species, stoich,
                          divisor=1, x_type="logxi", y_type="A", y_units="kcal",
                          show_zero_line=False, limiting=None, xlab=None, ylab=None,
                          title=None, charge_sign_at_end=False, log_y=False,
                          plot_width=4, plot_height=3, ppi=122,
                          xlim=None, ylim=None, df_out=False,
                          save_as=None, save_format=None,
                          save_scale=1, print_logK_messages=False):

              """
              Generate a line plot of the energy profile for a reaction as a
              function of the log of the extent of reaction (log Xi) or some other
              variable.

              Parameters
              ----------
              species : list of str
                  A list of species that match the order of the stoichiometric
                  reaction coefficients in the `stoich` parameter.

              stoich : list of numeric
                  A list of stoichiometric reaction coefficients that match the order
                  of the species in the `species` parameter.

              divisor : float, default 1
                  Divide all values in the energy profile by this number. Useful for
                  calculating energy per electron transferred or similar.

              x_type : str, default "logxi"
                  Variable to appear on the x-axis. Can be "logxi", "xi",
                  "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                  "aw".

              y_type : str, default 'A'
                  The variable to plot on the y-axis. Can be either 'A' (for chemical
                  affinity), 'G' (for Gibbs free energy, ΔG), 'logK' (for the log
                  of the equilibrium constant), 'logQ' (for the log of the reaction
                  quotient), or 'E' for energy supply.

              y_units : str, default 'kcal'
                  The unit that energy will be reported in (per mol for G and A, or
                  per kg fluid for energy supply, or unitless for logK and logQ).
                  Can be 'kcal', 'cal', 'J', or 'kJ'.

              show_zero_line : bool, default False
                  If True, displays a dotted line where affinity or ΔG equals 0 (at
                  equilibrium).

              limiting : str, optional
                  Name of the species to act as the limiting reactant when calculating
                  energy supply. If this parameter is left undefined, then a
                  limiting reactant will be chosen automatically based on
                  concentration and stoichiometry. This parameter is ignored unless
                  `y_type` is set to 'E' (energy supply).

              xlab, ylab : str, optional
                  Custom x and y axis labels.

              title : str, optional
                  Title of the plot to display.

              charge_sign_at_end : bool, default False
                  Display charge with sign after the number (e.g. SO4 2- instead of
                  SO4-2) in species names when the reaction is displayed in the plot
                  title?

              plot_width, plot_height : numeric, default 4 by 3
                  Width and height of the plot, in inches. Size of interactive plots
                  is also determined by pixels per inch, set by the parameter `ppi`.

              ppi : numeric, default 122
                  Pixels per inch. Along with `plot_width` and `plot_height`,
                  determines the size of interactive plots.

              xlim, ylim : list of two numeric values, optional
                  Minimum and maximum value of the x-axis and y-axis, respectively.

              df_out = bool, default False
                  Return a pandas dataframe in addition to a figure?

              save_as : str, optional
                  Provide a filename to save this figure. Filetype of saved figure is
                  determined by `save_format`.
                  Note: interactive plots can be saved by clicking the 'Download plot'
                  button in the plot's toolbar.

              save_format : str, default "png"
                  Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                  'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                  an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                  and 'webp' can be downloaded with the 'download as' button in the
                  toolbar of an interactive plot.

              save_scale : numeric, default 1
                  Multiply title/legend/axis/canvas sizes by this factor when saving
                  the figure.

              print_logK_messages : bool, default False
                  Print chnosz messages while the logK of the reaction is
                  calculated?

              Returns
              -------
              fig : Plotly figure object, optionally a Pandas Dataframe
                  A line plot. If `df_out` is True, also returns a dataframe.
              """
              # check that a thermodynamic CSV is being used
              if not isinstance(self.thermo.csv_db, pd.DataFrame):
                  self.err_handler.raise_exception("The plot_energy() function requires "
                          "a thermodynamic database in a WORM-style CSV format, e.g., "
                          "'wrm_data.csv'. You may be getting this message because "
                          "a data0 or data1 file was used.")

              # check that the divisor is valid
              if isinstance(divisor, list) or isinstance(divisor, pd.Series):
                  if len(divisor) != len(self.misc_params["Temp(C)"]):
                      self.err_handler.raise_exception("The length of the divisor is "
                          "not equal to the number of reported xi steps.")

              # check that the reaction is balanced
              formulas = []
              for s in species:
                  if s == "H+":
                      formulas.append("H+")
                  elif s == "H2O":
                      formulas.append("H2O")
                  else:
                      if s in list(self.thermo.csv_db["name"]):
                          formulas.append(list(self.thermo.csv_db[self.thermo.csv_db["name"]==s]["formula"])[0])
                      else:
                          self.err_handler.raise_exception("Valid thermodynamic data "
                                  "was not found for species "+str(s)+"")

              missing_composition = check_balance(formulas, stoich)

              # check that there are valid limiting reactants when calculating energy
              # e.g., prevent issue when the only reactant is a mineral, etc.
              reactant_idx = [1 if i<0 else 0 for i in stoich]
              reactants = [species[i] for i,idx in enumerate(reactant_idx) if idx == 1]
              invalid_limiting_reactants = []
              for r in reactants:
                  if r not in ["H2O", "H+", "OH-"]:
                      if list(self.thermo.csv_db[self.thermo.csv_db["name"]==r]["state"])[0] != "aq":
                          invalid_limiting_reactants.append(r)
                  else:
                      invalid_limiting_reactants.append(r)
              if reactants == invalid_limiting_reactants and y_type == "E":
                  self.err_handler.raise_exception("Energy supply for this reaction "
                      "cannot be calculated because none of the reactants are "
                      "limiting. A limiting reactant must be aqueous and cannot be H+ "
                      "or OH-.")

              if limiting != None:
                  # check that the limiting reactant is in the thermodynamic database
                  if limiting not in list(self.thermo.csv_db["name"]):
                      self.err_handler.raise_exception("Valid thermodynamic data was "
                              "not found for limiting reactant "+str(limiting)+"")

                  # check that the limiting reactant is aqueous or gaseous
                  if list(self.thermo.csv_db[self.thermo.csv_db["name"]==limiting]["state"])[0] != "aq":
                      self.err_handler.raise_exception("The limiting reactant must "
                              "be an aqueous species.")

                  # check that the limiting reactant is a reactant in the `species` parameter
                  if limiting not in reactants:
                      self.err_handler.raise_exception("The species specified as a "
                              "limiting reactant, '"+str(limiting)+"', is not a "
                              "reactant in this reaction.")

              # format reaction equation
              equation_to_display = format_equation(
                                            species,
                                            stoich,
                                            charge_sign_at_end=charge_sign_at_end,
                                            )

              # create a dictionary of species logacts across xi
              s_logact_dict = {}
              s_molal_dict = {}
              for s in species:
                  if s == "H+":
                      s_logact_dict[s] = [-pH for pH in list(self.misc_params["pH"])]
                      s_molal_dict[s] = [float("NaN")]*len(self.misc_params["pH"])
                  elif s == "H2O":
                      s_logact_dict[s] = [0]*len(self.misc_params["Temp(C)"])
                      s_molal_dict[s] = [float("NaN")]*len(self.misc_params["Temp(C)"])
                  elif list(self.thermo.csv_db[self.thermo.csv_db["name"]==s]["state"])[0] not in ["cr", "liq"]:
                      if s in self.aq_distribution_logact.columns:
                          # aqueous species
                          s_logact_dict[s] = list(self.aq_distribution_logact[s])
                          s_molal_dict[s] = list(self.aq_distribution_molal[s])
                      else:
                          self.err_handler.raise_exception("The species "+str(s)+" is "
                                  "not among the distribution of aqueous species in "
                                  "this calculation.")
                  else:
                      # liq and cr species
                      s_logact_dict[s] = [0]*len(self.misc_params["Temp(C)"])
                      s_molal_dict[s] = [float("NaN")]*len(self.misc_params["Temp(C)"])

              xlab, xvar = self.__get_xlab_xvar(x_type)

              if y_type not in ["logK", "logQ"]:
                  if y_units in ["cal", "kcal"]:
                      r_div = 4.184
                  elif y_units in ["J", "kJ"]:
                      r_div = 1
                  R = 8.314/r_div  # gas constant, unit = [cal/mol/K]

              if "k" in y_units:
                  k_div = 1000
              else:
                  k_div = 1

              y_list = []
              lr_name_list = []
              for i,T in enumerate(list(self.misc_params["Temp(C)"])):

                  if isinstance(divisor, list):
                      divisor_i = divisor[i]
                  else:
                      divisor_i = divisor

                  if y_type != "logQ":
                      logK = pychnosz.subcrt(
                                    species,
                                    stoich,
                                    T=T,
                                    P=list(self.misc_params["Press(bars)"])[i],
                                    IS=0, # ionic strength of 0 ensures that the logK calculated is standard, as required
                                    messages=print_logK_messages,
                                    show=False).out["logK"]

                      logK = float(logK.iloc[0])

                  if y_type == "logK":
                      ylab_out = "log K"
                      if title == None:
                          title = "Equilibrium constant for the reaction<br>"+equation_to_display
                      y_list.append(round(logK/divisor_i, 4))
                      df_y_name = "logK"
                      continue

                  logQ = sum([st*s_logact_dict[sp][i] for st,sp in zip(stoich,species)])

                  if y_type == "logQ":
                      ylab_out = "log Q"
                      if title == None:
                          title = "Reaction quotient for the reaction<br>"+equation_to_display
                      y_list.append(round(logQ/divisor_i, 4))
                      df_y_name = "logQ"
                      continue

                  else:
                      A = 2.303 * R * (273.15+T) * (logK - logQ)  # affinity, unit = [cal/mol]
                      A = A/k_div

                      if title == None:
                          title = "Energy profile for the reaction<br>"+equation_to_display

                      if y_type=="G":
                          G = -A # gibbs free energy, unit = [cal/mol]
                          y_list.append(G/divisor_i)
                          ylab_out="ΔG, {}/mol".format(y_units)
                          y_units_out = y_units+"/mol"
                      elif y_type=="A":
                          y_list.append(round(A/divisor_i, 4))
                          ylab_out="A, {}/mol".format(y_units)
                          y_units_out = y_units+"/mol"
                      elif y_type=="E":

                          if not isinstance(limiting, str):
                              lrc_dict = {}
                              for i_s,s in enumerate(species):
                                  # identify valid limiting reactants and record concentrations
                                  # 1. negative coefficient (reactant)
                                  # 2. can't be OH-, H+, H2O
                                  # 3. can't be cr or liq
                                  if stoich[i_s] < 0 and s not in ["H2O", "H+", "OH-"] and list(self.thermo.csv_db[self.thermo.csv_db["name"]==s]["state"])[0] not in ["cr", "liq"]:
                                      lrc_dict[s] = s_molal_dict[s][i]/abs(stoich[i_s])

                          if not isinstance(limiting, str):
                              lr_name = min(lrc_dict, key=lrc_dict.get)
                              lr_val = lrc_dict[lr_name]

                              # handle situations where there might be multiple limiting reactants
                              lr_list = []
                              for k,v in zip(lrc_dict.keys(), lrc_dict.values()):
                                  if v == lr_val:
                                      lr_list.append(str(k))

                          else:
                              lr_list = [limiting]

                          lr_list_formatted = [chemlabel(lr_name, charge_sign_at_end=charge_sign_at_end) for lr_name in lr_list]
                          if len(lr_list_formatted) > 1:
                              lr_reported = ", ".join(lr_list_formatted)
                          else:
                              lr_reported = lr_list[0]

                          lr_name = lr_list[0] # doesn't matter which lr is used to calculate
                          lr_concentration = s_molal_dict[lr_name][i]
                          lr_name_list.append(lr_reported)
                          lr_stoich = -stoich[species.index(lr_name)]
                          E = A * (lr_concentration/lr_stoich)

                          y_list.append(round(E/divisor_i, 4))
                          y_units_out = y_units+"/kg fluid"
                          ylab_out="Energy Supply, {}".format(y_units+"/kg fluid")
      #                     else:
      #                         lr_name_list.append("NA")
      #                         y_list.append(float('NaN'))
      #                         y_units_out = y_units+"/kg fluid"
      #                         ylab_out="Energy Supply, {}".format(y_units+"/kg fluid")

                      df_y_name = y_type+", "+y_units_out

              if xlab != None:
                  xlab_out = xlab
              if ylab != None:
                  ylab_out = ylab

              df = copy.deepcopy(self.misc_params)
              with np.errstate(divide='ignore'):
                  df['log Xi'] = np.log10(df['Xi'])

              df[df_y_name] = y_list

              fig = px.line(df, x=xvar, y=df_y_name, log_y=log_y,
                            width=plot_width*ppi, height=plot_height*ppi,
                            template="simple_white")

              if y_type=="E":
                  fig.add_trace(
                      go.Scatter(
                          x=df[xvar],
                          y=df[df_y_name],
                          mode='lines',
                          customdata = lr_name_list,
                          hovertemplate = xlab+': %{x}<br>'+ylab_out+': %{y}<br>Limiting : %{customdata}<extra></extra>',
                      )
                  )
                  fig.data[0].visible=False

              fig.update_layout(xaxis_title=xlab_out,
                                yaxis_title=ylab_out)

              if isinstance(title, str):
                  fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

              if isinstance(ylim, list):
                  fig.update_layout(yaxis_range=ylim)

              if isinstance(xlim, list):
                  fig.update_layout(xaxis_range=xlim)

              if isinstance(save_as, str):
                  dummy_sp = Speciation({})
                  save_as, save_format = dummy_sp._save_figure(fig,
                          save_as, save_format, save_scale,
                          plot_width, plot_height, ppi)

              if show_zero_line:
                  fig.add_hline(y=0, line_width=3, line_dash="dash", line_color="black")

              if df_out:
                  return fig, df
              else:
                  return fig

          def plot_mass_contribution(self, *args, x_type="xi", x_decimals=3,
                                           track_steps=True, keep_xi_order=False,
                                           **kwargs):

              """
              Generate a bar plot of mass contributions (in mole percent) of aqueous
              species formed as a function of reaction progress Xi or some other
              variable.

              Parameters
              ----------
              *args : iterable
                  Arguments to be passed to `Speciation.plot_mass_contribution`.

              x_type : str, default "xi"
                  Variable to appear on the x-axis. Can be "logxi", "xi",
                  "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                  "aw".

              x_decimals : int
                  Number of decimals to display in the numeric values of the x-axis
                  variable defined by `x_type`.

              track_steps : bool, default True
                  Show reported xi steps on x-axis ticks? Useful for plotting as a
                  function of an x-axis variable that can be out-of-order, like
                  Eh or temperature. This parameter will become True automatically
                  if it will prevent column stacking (which happens if there are
                  duplicate x-axis values).

              **kargs : dict
                  Keyword arguments to be passed to `Speciation.plot_mass_contribution`.

              Returns
              -------
              fig : Plotly figure object
                  A mass contribution bar plot.
              """

              basis = args[0]

              xlab, kwargs["sample_label"] = self.__get_xlab_xvar(x_type)

              df_sp = pd.concat([self.mass_contribution_dict.get(basis, "error"),
                                 self.misc_params[self.misc_params.columns[1:]]], axis=1)

              with np.errstate(divide='ignore'):
                  df_sp['log Xi'] = np.log10(df_sp['Xi'])

              # handle display of the x axis variable
              # if number of decimals to display is too low, columns will stack
              # check to see if this happens, then increment x_decimals until the stacking problem is solved
              if x_decimals < 0:
                  msg = "The parameter x_decimals must be greater than or equal to 0."
                  self.err_handler.raise_exception(msg)
              original_x_decimals = copy.copy(x_decimals)

              len_unique_labels_rounded = len(set(df_sp[kwargs["sample_label"]].apply(lambda x: ('%.'+str(x_decimals)+'e') % x)))
              len_unique_labels_unrounded = len(set(df_sp[kwargs["sample_label"]]))

              if len_unique_labels_rounded < len_unique_labels_unrounded:
                  x_decimals += 1
                  solved_decimals = False
                  for i in range(x_decimals, x_decimals+10):
                      len_unique_labels_rounded = len(set(df_sp[kwargs["sample_label"]].apply(lambda x: ('%.'+str(x_decimals)+'e') % x)))
                      if len_unique_labels_rounded == len_unique_labels_unrounded:
                          if self.verbose > 0:
                              print("Number of decimals to display for x-axis variable",
                                    "increased to", x_decimals, "to prevent column stacking.")
                          solved_decimals = True
                          break
                      else:
                          x_decimals += 1
                  if not solved_decimals:
                      msg = ("X-axis value decimal formatting is resulting in column "
                             "stacking even after attempting 10 increments of x_decimals.")
                      self.err_handler.raise_exception(msg)

              if isinstance(df_sp, str):
                  msg = ("The basis species {} ".format(basis)+"could not be found "
                         "among available basis species: "
                         "{}".format(str(list(self.mass_contribution_dict.keys()))))
                  self.err_handler.raise_exception(msg)

              df_sp["position"] = list(range(0, df_sp.shape[0]))

              df_sp_melt = df_sp.melt(
                      id_vars=list(self.misc_params.columns)+["log Xi", "basis", "factor", "molality", "position"])

              df_sp_melt.rename(columns={kwargs["sample_label"] : "sample",
                                         df_sp_melt.columns[-2] : "species",
                                         df_sp_melt.columns[-1] : "percent",
                                         }, inplace=True)

              df_sp_melt = df_sp_melt[df_sp_melt['percent'].notna()]

              if keep_xi_order:
                  sample_order = ["position", "species", "percent"]
              else:
                  sample_order = ["sample", "position", "species", "percent"]

              df_sp_melt.sort_values(sample_order, axis=0, ascending=True, inplace=True)

              if df_sp_melt.dtypes["sample"] != "O": # if the column isn't formatted as a string
                  if any(["e" in v2 for v2 in [str(v1) for v1 in df_sp_melt['sample']]]): # if value is in scientific notation
                      df_sp_melt['sample'] = df_sp_melt['sample'].apply(lambda x: ('%.'+str(x_decimals)+'e') % x) # converts numeric to string
                  else:
                      df_sp_melt['sample'] = [('{0:.'+str(x_decimals)+'f}').format(v) for v in df_sp_melt['sample']]

              if len(list(set(df_sp_melt["sample"]))) != len(list(set(df_sp_melt["position"]))) or track_steps:
                  # handle duplicate x-axis values to prevent stacking
                  temp_col = []
                  for i,v in enumerate(df_sp_melt["sample"]):
                      temp_col.append(" (step "+str(list(df_sp_melt["position"])[i])+")")
                  df_sp_melt["sample"] = [str(v)+a for v,a in zip(df_sp_melt["sample"], temp_col)]
                  if not track_steps and self.verbose > 0:
                      print("Reported Xi step tracking has been added to x-axis ticks to prevent column stacking.")

              sp = Speciation(args={})
              sp.mass_contribution = df_sp_melt

              if not kwargs.get("plot_out", False):
                  plot_out = False
              else:
                  plot_out = True
              kwargs["plot_out"] = True

              fig = sp.plot_mass_contribution(*args, **kwargs)

              fig.update_layout(
                  xaxis_title=xlab, # add an x axis title
              )

              if plot_out:
                  return fig
              else:
                  fig.show()

  Class containing functions to facilitate mass transfer and reaction path calculations and visualize results.

  ## Parameters

  `six_o_file` :`str`
    Path name of the '6o' output file generated by EQ6.
  `thermo` :`an object` of `class Thermodata`
    The subclass containing thermodynamic data in a Speciation object.
  `thermodata_csv` :`str`
    Path name of the WORM-styled thermodynamic database CSV used in the EQ6 calculation.
  `tab_name` :`str`
    Path name of the TAB file generated by EQ6.
  `format_element_names` :`bool`, default `True`
    Format the chemical symbols of redox-isolated elements generated when elements are supplied to the `suppress_redox` parameter of `AqEquil.speciate`? Context: when elements are redox-isolated, new elements are designated representing different oxidations states in order to prevent redox reactions from occuring during calculations. The names of these elements appear like "Fejiip", where the first one or two letters represent the element (e.g. "Fe"), "j" denotes that the chemical symbol is finished, followed by roman numerals representing the magnitude of the charge (e.g., "ii" representing 2), finally followed by a letter that denotes the sign of the charge (e.g., "p" for positive). By setting this parameter to True, the element "Fejiip" will be converted into a more readable form, "Fe+2", in all figures and tables generated from calculation results. It may be useful to set this parameter to False if you are supplying your own data0 file containing custom pseudoelements that do not follow the convention used by AqEquil.
  `hide_traceback` :`bool`, default `True`
    Hide traceback message when encountering errors handled by this class? When True, error messages handled by this class will be short and to the point.

  ### Static methods

  `def process_tab(tab_name, thermodata_csv)`
    Expand source code

        @staticmethod
        def process_tab(tab_name, thermodata_csv):

            """
            Process a TAB file (from EQ6) into a dictionary of Pandas dataframes.

            Parameters
            ----------
            tab_name : str
                Path name of the TAB file generated by EQ6.

            thermodata_csv : str or Pandas dataframe
                Path name of the WORM-styled thermodynamic database CSV used in the
                EQ6 calculation. Alternately, the thermodynamic database itself as a
                Pandas dataframe itself.

            Returns
            -------
            tab : a dict of Pandas dataframes
                A dictionary of dataframes representing tables mined from the TAB
                file.
            """

            if isinstance(thermodata_csv, str):
                thermo_db = pd.read_csv(thermodata_csv)
            else:
                thermo_db = thermodata_csv

            thermo_db_names = list(thermo_db["name"])

            with open(tab_name, "r") as tabfile:
                tab_lines = tabfile.readlines()

            tab = {}

            tables = ["B1", "B2", "C1", "C2", "C3", "C4",
                      "D1", "D2", "D3", "D4", "E1", "E2",
                      "E3", "J", "K", "P", "Q", "T", "W"]

            record_lines = False
            get_header = False
            recorded_lines = []
            for line in tab_lines:
                split_line = line.split(",")

                if True in [s in ["Table " + t for t in tables] for s in split_line]:
                    get_header = True
                    table_name = " ".join(split_line[1:-1])
                    continue

                if get_header:
                    header = split_line[1:-1]

                    # handle instances where the tab file creates extra columns for things like "albite,low"
                    if table_name in ["Table P Moles of product minerals",
                                      "Table Q Saturation indices of potential product phases",
                                      "Table J Moles of reactants destroyed/created",
                                      "Table K Affinities of reactants (kcal)"]:

                        if table_name in ["Table P Moles of product minerals",
                                          "Table J Moles of reactants destroyed/created"]:
                            new_header = ["Xi", "t(days)"] # table P and J
                        elif table_name == "Table Q Saturation indices of potential product phases":
                            new_header = ["Xi", "t(days)", "H2O", "Gas"] # table Q
                        elif table_name == "Table K Affinities of reactants (kcal)":
                            new_header = ["Xi", "t(days)", "Total"]

                        for i,h in enumerate(header):

                            if h != "Xi" and h != "t(days)" and h != "H2O" and h != "Gas" and h != "Total":
                                if h not in thermo_db_names:
                                    if header[i-1]+","+h in thermo_db_names:
                                        new_header = new_header[:-1]
                                        new_header.append(header[i-1]+","+h)
                                    else:
                                        new_header.append(h)
                                else:
                                    new_header.append(h)

                        header = new_header

                    record_lines = True
                    get_header = False
                    continue

                if "EndTable:" in split_line:
                    record_lines = False
                    if len(recorded_lines) > 0:
                        df = pd.DataFrame(recorded_lines)
                        recorded_lines = []
                        df.columns = header
                        tab[table_name] = df

                if record_lines:
                    recorded_lines.append(split_line[1:-1])

            return tab

    Process a TAB file (from EQ6) into a dictionary of Pandas dataframes.

    ## Parameters

    `tab_name` :`str`
      Path name of the TAB file generated by EQ6.
    `thermodata_csv` :`str` or `Pandas dataframe`
      Path name of the WORM-styled thermodynamic database CSV used in the EQ6 calculation. Alternately, the thermodynamic database itself as a Pandas dataframe itself.

    ## Returns

    `tab` :`a dict` of `Pandas dataframes`
      A dictionary of dataframes representing tables mined from the TAB file.

  ### Methods

  `def mine_6o_table(self,
  table_start='--- Distribution of Aqueous Solute Species ---',
  table_stop='Species with molalities less than',
  ignore=['', 'Species', '---'],
  col_index=-1)`
    Expand source code

        def mine_6o_table(self,
                          table_start="--- Distribution of Aqueous Solute Species ---",
                          table_stop="Species with molalities less than",
                          ignore = ["", "Species", '---'],
                          col_index=-1):

            """
            Mine a table in a '6o' EQ6 output file and consolidate results into a
            dataframe.

            Parameters
            ----------
            table_start : str
                A unique string that indicates the start of the table.

            table_stop : str, optional
                A unique string that indicates the end of the table.

            ignore : list of str
                A list of strings representing lines to ignore when mining a table.
                For example, it is prudent to ignore blank lines, or lines
                containing the table column headers.
                A line will be skipped if line.strip().split(' ')[0] matches any
                of the strings in the given list.

            col_index : int, default -1
                Integer representing the index of the table column to be mined.
                The default is -1, which is the last column in the table.

            Returns
            -------
            df : Pandas dataframe
                A dataframe with rows of the extent of reaction (Xi), and columns
                containing the values of chemical species mined from the file.
            """

            lines = self.six_o_file_lines

            species = []
            xi_vals=[]
            collect_values = False
            for i in lines:
                if len(i.strip().split(' ')) > 1 and i.strip().split(' ')[0] == "Xi=":
                    this_xi_val = float(i.split(' ')[-1])
                if table_stop in i:
                    collect_values = False
                if table_start in i:
                    xi_vals.append(this_xi_val) # appending here prevents mismatch where there can be more Xi vals than tables to mine
                    collect_values = True
                if collect_values:
                    if i.strip().split(' ')[0] not in ignore:
                        species.append(i.strip().split(' ')[0])

            species = list(set(species))

            species_dict = {"Xi":xi_vals}

            if len(species) == 0:
                df = pd.DataFrame(species_dict)
                return df

            for s in species:

                vals=[]
                collect_values = False
                for i in lines:
                    if collect_values and table_stop in i:
                        # stop collecting
                        collect_values = False
                        if not got_value:
                            vals.append(np.nan)
                    if table_start in i:
                        # start collecting
                        collect_values = True
                        got_value = False
                    if collect_values:
                        if len(i.strip().split(' ')) > 2 and i.strip().split(' ')[0] == s:
                            split_i = i.strip().split(' ')
                            split_i_clean = [v for v in split_i if v != '']
                            val = split_i_clean[col_index]
                            try:
                                val = float(val)
                            except:
                                # if a value is not a float, e.g., 3.1450-100
                                if "-" in val:
                                    val_list = val.split("-")
                                    val = "".join([val_list[0], "E-", val_list[1]])
                                    val = float(val)
                                elif "+" in val:
                                    val_list = val.split("+")
                                    val = "".join([val_list[0], "E", val_list[1]])
                                    val = float(val)
                                elif "SATD" in val:
                                    pass
                                else:
                                    self.err_handler.raise_exception(("Error: "
                                        "Encountered a non-numeric value when mining "
                                        "a .6o file: "+val))
                            vals.append(val)
                            got_value = True
                species_dict[s] = vals

            df = pd.DataFrame(species_dict)

            if 'None' in df.columns:
                df = df.drop(['None'], axis=1)

            return df

    Mine a table in a '6o' EQ6 output file and consolidate results into a dataframe.

    ## Parameters

    `table_start` :`str`
      A unique string that indicates the start of the table.
    `table_stop` :`str`, optional
      A unique string that indicates the end of the table.
    `ignore` :`list` of `str`
      A list of strings representing lines to ignore when mining a table. For example, it is prudent to ignore blank lines, or lines containing the table column headers. A line will be skipped if line.strip().split(' ')[0] matches any of the strings in the given list.
    `col_index` :`int`, default `-1`
      Integer representing the index of the table column to be mined. The default is -1, which is the last column in the table.

    ## Returns

    `df` :`Pandas dataframe`
      A dataframe with rows of the extent of reaction (Xi), and columns containing the values of chemical species mined from the file.
  `def plot_aqueous_species(self,
  plot_basis=False,
  plot_species=None,
  x_type='logxi',
  y_type='log activity',
  initially_visible=None,
  show_legend=True,
  charge_sign_at_end=False,
  plot_width=4,
  plot_height=3,
  ppi=122,
  xlim=None,
  ylim=None,
  save_as=None,
  save_format=None,
  save_scale=1)`
    Expand source code

        def plot_aqueous_species(self, plot_basis=False, plot_species=None,
                                 x_type="logxi", y_type="log activity",
                                 initially_visible=None, show_legend=True,
                                 charge_sign_at_end=False,
                                 plot_width=4, plot_height=3, ppi=122, xlim=None,
                                 ylim=None, save_as=None, save_format=None,
                                 save_scale=1):

            """
            Generate a line plot of the log activities of aqueous species as a
            function of the log of the extent of reaction (log Xi) or some other
            variable.

            Parameters
            ----------
            plot_basis : bool, default False
                Plot basis species only?

            plot_species : list of str, optional
                A list of aqueous species to plot. If undefined, every species at
                will be plotted at once.

            x_type : str, default "logxi"
                Variable to appear on the x-axis. Can be "logxi", "xi",
                "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                "aw".

            y_type : str, default 'log activity'
                The variable to plot on the y-axis. Can be either 'log activity',
                'molality', or 'log molality'.

            initially_visible : list of str, optional
                A list of aqueous species that will be visible on the plot
                initially. All other species will be hidden, but can still be
                toggled back on in the legend.

            show_legend : bool, default True
                Show the legend?

            charge_sign_at_end : bool, default False
                Display charge with sign after the number (e.g. SO4 2-)?

            plot_width, plot_height : numeric, default 4 by 3
                Width and height of the plot, in inches. Size of interactive plots
                is also determined by pixels per inch, set by the parameter `ppi`.

            ppi : numeric, default 122
                Pixels per inch. Along with `plot_width` and `plot_height`,
                determines the size of interactive plots.

            xlim, ylim : list of two numeric values, optional
                Minimum and maximum value of the x-axis and y-axis, respectively.

            save_as : str, optional
                Provide a filename to save this figure. Filetype of saved figure is
                determined by `save_format`.
                Note: interactive plots can be saved by clicking the 'Download plot'
                button in the plot's toolbar.

            save_format : str, default "png"
                Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                and 'webp' can be downloaded with the 'download as' button in the
                toolbar of an interactive plot.

            save_scale : numeric, default 1
                Multiply title/legend/axis/canvas sizes by this factor when saving
                the figure.

            Returns
            -------
            fig : Plotly figure object
                A line plot.
            """

            if plot_basis:

                df = pd.concat([self.basis_logact, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                title = "Solute basis species"
                ylab = "log activity"

            else:
                if y_type == "log activity":
                    df = pd.concat([self.aq_distribution_logact, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "log activity"
                elif y_type == "molality":
                    df = pd.concat([self.aq_distribution_molal, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "molality"
                elif y_type == "log molality":
                    df = pd.concat([self.aq_distribution_logmolal, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "log molality"
                else:
                    self.err_handler.raise_exception("The chosen 'y_type' parameter "
                        "is not recognized. 'y_type' can be 'log activity', "
                        "'molality', or 'log molality'")
                title = "Solute species"

            plot_columns = [col for col in df.columns]
            if isinstance(plot_species, list):
                plot_columns_temp = [col for col in plot_columns if col in plot_species]
                plot_columns = plot_columns_temp

            df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
            df.columns = list(self.misc_params.columns)+["variable", "value"]

            df["variable"] = df["variable"].apply(chemlabel, charge_sign_at_end=charge_sign_at_end)

            df["Xi"] = pd.to_numeric(df["Xi"])

            df["value"] = pd.to_numeric(df["value"])
            df["value"] = df["value"].fillna(0)
            df["value"] = df["value"].replace(0, np.nan)

            with np.errstate(divide='ignore'):
                df['log Xi'] = np.log10(df['Xi'])

            xlab, xvar = self.__get_xlab_xvar(x_type)

            fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                                  width=plot_width*ppi,  height=plot_height*ppi,
                                  labels=dict(value=ylab, x=xlab), render_mode='svg',
                                 )

            if isinstance(initially_visible, list):
                initially_visible_html = [chemlabel(sp, charge_sign_at_end=charge_sign_at_end) for sp in initially_visible]
                for trace in fig['data']:
                    if (not trace['name'] in initially_visible_html):
                        trace['showlegend'] = True
                        trace['visible'] = 'legendonly'

            fig.update_layout(xaxis_title=xlab,
                              yaxis_title=ylab,
                              legend_title=None,
                              showlegend=show_legend)

            if isinstance(title, str):
                fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

            if isinstance(ylim, list):
                fig.update_layout(yaxis_range=ylim)

            if isinstance(xlim, list):
                fig.update_layout(xaxis_range=xlim)

            if isinstance(save_as, str):
                dummy_sp = Speciation({})
                save_as, save_format = dummy_sp._save_figure(fig,
                        save_as, save_format, save_scale,
                        plot_width, plot_height, ppi)

            return fig

    Generate a line plot of the log activities of aqueous species as a function of the log of the extent of reaction (log Xi) or some other variable.

    ## Parameters

    `plot_basis` :`bool`, default `False`
      Plot basis species only?
    `plot_species` :`list` of `str`, optional
      A list of aqueous species to plot. If undefined, every species at will be plotted at once.
    `x_type` :`str`, default `"logxi"`
      Variable to appear on the x-axis. Can be "logxi", "xi", "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or "aw".
    `y_type` :`str`, default `'log activity'`
      The variable to plot on the y-axis. Can be either 'log activity', 'molality', or 'log molality'.
    `initially_visible` :`list` of `str`, optional
      A list of aqueous species that will be visible on the plot initially. All other species will be hidden, but can still be toggled back on in the legend.
    `show_legend` :`bool`, default `True`
      Show the legend?
    `charge_sign_at_end` :`bool`, default `False`
      Display charge with sign after the number (e.g. SO4 2-)?
    `plot_width`, `plot_height` :`numeric`, default `4 by 3`
      Width and height of the plot, in inches. Size of interactive plots is also determined by pixels per inch, set by the parameter `ppi`.
    `ppi` :`numeric`, default `122`
      Pixels per inch. Along with `plot_width` and `plot_height`, determines the size of interactive plots.
    `xlim`, `ylim` :`list` of `two numeric values`, optional
      Minimum and maximum value of the x-axis and y-axis, respectively.
    `save_as` :`str`, optional
      Provide a filename to save this figure. Filetype of saved figure is determined by `save_format`. Note: interactive plots can be saved by clicking the 'Download plot' button in the plot's toolbar.
    `save_format` :`str`, default `"png"`
      Desired format of saved or downloaded figure. Can be 'png', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html', an interactive plot will be saved. Only 'png', 'svg', 'jpeg', and 'webp' can be downloaded with the 'download as' button in the toolbar of an interactive plot.
    `save_scale` :`numeric`, default `1`
      Multiply title/legend/axis/canvas sizes by this factor when saving the figure.

    ## Returns

    `fig` :`Plotly figure object`
      A line plot.
  `def plot_elements(self,
  plot_elements=None,
  units='molality',
  log=True,
  x_type='logxi',
  plot_width=4,
  plot_height=3,
  ppi=122,
  ylim=None,
  show_legend=True,
  charge_sign_at_end=False,
  save_as=None,
  save_format=None,
  save_scale=1)`
    Expand source code

            def plot_elements(self, plot_elements=None, units="molality", log=True,
                              x_type="logxi", plot_width=4, plot_height=3, ppi=122,
                              ylim=None, show_legend=True, charge_sign_at_end=False,
                              save_as=None, save_format=None, save_scale=1):

                """
                Generate a line plot of the log activities of aqueous species as a
                function of the log of the extent of reaction (log Xi).

                Parameters
                ----------
                plot_elements : list of str, optional
                    A list of elements to plot. If undefined, every element will be
                    plotted at once.

                units : str, default "molality"
                    Units of elemental abundance to plot. Can be "molality" or "ppm".

                log : bool, default True
                    Display elemental abundances in log scale?

                plot_width, plot_height : numeric, default 4 by 3
                    Width and height of the plot, in inches. Size of interactive plots
                    is also determined by pixels per inch, set by the parameter `ppi`.

                ppi : numeric, default 122
                    Pixels per inch. Along with `plot_width` and `plot_height`,
                    determines the size of interactive plots.

                ylim : list of two numeric values, optional
                    Minimum and maximum value of the y-axis.

                show_legend : bool, default True
                    Show the legend?

                charge_sign_at_end : bool, default False
                    Display charge with sign after the number (e.g. SO4 2-)?

                save_as : str, optional
                    Provide a filename to save this figure. Filetype of saved figure is
                    determined by `save_format`.
                    Note: interactive plots can be saved by clicking the 'Download plot'
                    button in the plot's toolbar.

                save_format : str, default "png"
                    Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                    'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                    an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                    and 'webp' can be downloaded with the 'download as' button in the
                    toolbar of an interactive plot.

                save_scale : numeric, default 1
                    Multiply title/legend/axis/canvas sizes by this factor when saving
                    the figure.

                Returns
                -------
                fig : Plotly figure object
                    A line plot.
                """

                title = "Concentrations of dissolved elements"
                if units == "molality":
                    df = pd.concat([self.dissolved_elements_molal, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "{}molality"
                elif units == "ppm":
                    df = pd.concat([self.dissolved_elements_ppm, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "{}ppm"
        #             elif units == "molarity":

                plot_columns = [col for col in df.columns]
                if isinstance(plot_elements, list):
                    plot_columns_temp = [col for col in plot_columns if col in plot_elements]
                    plot_columns = plot_columns_temp

                df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
                df.columns = list(self.misc_params.columns)+["variable", "value"]
                df["variable"] = df["variable"].apply(chemlabel, charge_sign_at_end=charge_sign_at_end)

                df["Xi"] = pd.to_numeric(df["Xi"])

                df["value"] = pd.to_numeric(df["value"])
                df["value"] = df["value"].fillna(0)
                df["value"] = df["value"].replace(0, np.nan)

                with np.errstate(divide='ignore'):
                    df['log Xi'] = np.log10(df['Xi'])
                    if log:
                        df['value'] = np.log10(df['value'])
                        ylab = ylab.format("log ")
                if not log:
                    ylab = ylab.format("")

                xlab, xvar = self.__get_xlab_xvar(x_type)

                fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                                      width=plot_width*ppi,  height=plot_height*ppi,
                                      labels=dict(value=ylab, x=xlab), render_mode='svg',
                                     )

                fig.update_layout(xaxis_title=xlab,
                                  yaxis_title=ylab,
                                  legend_title=None,
                                  showlegend=show_legend,
                                  yaxis={'showexponent': 'all',
                                         'exponentformat': 'power'},
                                 )

                if isinstance(title, str):
                    fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

                if isinstance(ylim, list):
                    fig.update_layout(yaxis_range=ylim)

                if isinstance(save_as, str):
                    dummy_sp = Speciation({})
                    save_as, save_format = dummy_sp._save_figure(fig,
                            save_as, save_format, save_scale,
                            plot_width, plot_height, ppi)

                return fig

    Generate a line plot of the log activities of aqueous species as a function of the log of the extent of reaction (log Xi).

    ## Parameters

    `plot_elements` :`list` of `str`, optional
      A list of elements to plot. If undefined, every element will be plotted at once.
    `units` :`str`, default `"molality"`
      Units of elemental abundance to plot. Can be "molality" or "ppm".
    `log` :`bool`, default `True`
      Display elemental abundances in log scale?
    `plot_width`, `plot_height` :`numeric`, default `4 by 3`
      Width and height of the plot, in inches. Size of interactive plots is also determined by pixels per inch, set by the parameter `ppi`.
    `ppi` :`numeric`, default `122`
      Pixels per inch. Along with `plot_width` and `plot_height`, determines the size of interactive plots.
    `ylim` :`list` of `two numeric values`, optional
      Minimum and maximum value of the y-axis.
    `show_legend` :`bool`, default `True`
      Show the legend?
    `charge_sign_at_end` :`bool`, default `False`
      Display charge with sign after the number (e.g. SO4 2-)?
    `save_as` :`str`, optional
      Provide a filename to save this figure. Filetype of saved figure is determined by `save_format`. Note: interactive plots can be saved by clicking the 'Download plot' button in the plot's toolbar.
    `save_format` :`str`, default `"png"`
      Desired format of saved or downloaded figure. Can be 'png', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html', an interactive plot will be saved. Only 'png', 'svg', 'jpeg', and 'webp' can be downloaded with the 'download as' button in the toolbar of an interactive plot.
    `save_scale` :`numeric`, default `1`
      Multiply title/legend/axis/canvas sizes by this factor when saving the figure.

    ## Returns

    `fig` :`Plotly figure object`
      A line plot.
  `def plot_energy(self,
  species,
  stoich,
  divisor=1,
  x_type='logxi',
  y_type='A',
  y_units='kcal',
  show_zero_line=False,
  limiting=None,
  xlab=None,
  ylab=None,
  title=None,
  charge_sign_at_end=False,
  log_y=False,
  plot_width=4,
  plot_height=3,
  ppi=122,
  xlim=None,
  ylim=None,
  df_out=False,
  save_as=None,
  save_format=None,
  save_scale=1,
  print_logK_messages=False)`
    Expand source code

            def plot_energy(self, species, stoich,
                            divisor=1, x_type="logxi", y_type="A", y_units="kcal",
                            show_zero_line=False, limiting=None, xlab=None, ylab=None,
                            title=None, charge_sign_at_end=False, log_y=False,
                            plot_width=4, plot_height=3, ppi=122,
                            xlim=None, ylim=None, df_out=False,
                            save_as=None, save_format=None,
                            save_scale=1, print_logK_messages=False):

                """
                Generate a line plot of the energy profile for a reaction as a
                function of the log of the extent of reaction (log Xi) or some other
                variable.

                Parameters
                ----------
                species : list of str
                    A list of species that match the order of the stoichiometric
                    reaction coefficients in the `stoich` parameter.

                stoich : list of numeric
                    A list of stoichiometric reaction coefficients that match the order
                    of the species in the `species` parameter.

                divisor : float, default 1
                    Divide all values in the energy profile by this number. Useful for
                    calculating energy per electron transferred or similar.

                x_type : str, default "logxi"
                    Variable to appear on the x-axis. Can be "logxi", "xi",
                    "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                    "aw".

                y_type : str, default 'A'
                    The variable to plot on the y-axis. Can be either 'A' (for chemical
                    affinity), 'G' (for Gibbs free energy, ΔG), 'logK' (for the log
                    of the equilibrium constant), 'logQ' (for the log of the reaction
                    quotient), or 'E' for energy supply.

                y_units : str, default 'kcal'
                    The unit that energy will be reported in (per mol for G and A, or
                    per kg fluid for energy supply, or unitless for logK and logQ).
                    Can be 'kcal', 'cal', 'J', or 'kJ'.

                show_zero_line : bool, default False
                    If True, displays a dotted line where affinity or ΔG equals 0 (at
                    equilibrium).

                limiting : str, optional
                    Name of the species to act as the limiting reactant when calculating
                    energy supply. If this parameter is left undefined, then a
                    limiting reactant will be chosen automatically based on
                    concentration and stoichiometry. This parameter is ignored unless
                    `y_type` is set to 'E' (energy supply).

                xlab, ylab : str, optional
                    Custom x and y axis labels.

                title : str, optional
                    Title of the plot to display.

                charge_sign_at_end : bool, default False
                    Display charge with sign after the number (e.g. SO4 2- instead of
                    SO4-2) in species names when the reaction is displayed in the plot
                    title?

                plot_width, plot_height : numeric, default 4 by 3
                    Width and height of the plot, in inches. Size of interactive plots
                    is also determined by pixels per inch, set by the parameter `ppi`.

                ppi : numeric, default 122
                    Pixels per inch. Along with `plot_width` and `plot_height`,
                    determines the size of interactive plots.

                xlim, ylim : list of two numeric values, optional
                    Minimum and maximum value of the x-axis and y-axis, respectively.

                df_out = bool, default False
                    Return a pandas dataframe in addition to a figure?

                save_as : str, optional
                    Provide a filename to save this figure. Filetype of saved figure is
                    determined by `save_format`.
                    Note: interactive plots can be saved by clicking the 'Download plot'
                    button in the plot's toolbar.

                save_format : str, default "png"
                    Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                    'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                    an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                    and 'webp' can be downloaded with the 'download as' button in the
                    toolbar of an interactive plot.

                save_scale : numeric, default 1
                    Multiply title/legend/axis/canvas sizes by this factor when saving
                    the figure.

                print_logK_messages : bool, default False
                    Print chnosz messages while the logK of the reaction is
                    calculated?

                Returns
                -------
                fig : Plotly figure object, optionally a Pandas Dataframe
                    A line plot. If `df_out` is True, also returns a dataframe.
                """
                # check that a thermodynamic CSV is being used
                if not isinstance(self.thermo.csv_db, pd.DataFrame):
                    self.err_handler.raise_exception("The plot_energy() function requires "
                            "a thermodynamic database in a WORM-style CSV format, e.g., "
                            "'wrm_data.csv'. You may be getting this message because "
                            "a data0 or data1 file was used.")

                # check that the divisor is valid
                if isinstance(divisor, list) or isinstance(divisor, pd.Series):
                    if len(divisor) != len(self.misc_params["Temp(C)"]):
                        self.err_handler.raise_exception("The length of the divisor is "
                            "not equal to the number of reported xi steps.")

                # check that the reaction is balanced
                formulas = []
                for s in species:
                    if s == "H+":
                        formulas.append("H+")
                    elif s == "H2O":
                        formulas.append("H2O")
                    else:
                        if s in list(self.thermo.csv_db["name"]):
                            formulas.append(list(self.thermo.csv_db[self.thermo.csv_db["name"]==s]["formula"])[0])
                        else:
                            self.err_handler.raise_exception("Valid thermodynamic data "
                                    "was not found for species "+str(s)+"")

                missing_composition = check_balance(formulas, stoich)

                # check that there are valid limiting reactants when calculating energy
                # e.g., prevent issue when the only reactant is a mineral, etc.
                reactant_idx = [1 if i<0 else 0 for i in stoich]
                reactants = [species[i] for i,idx in enumerate(reactant_idx) if idx == 1]
                invalid_limiting_reactants = []
                for r in reactants:
                    if r not in ["H2O", "H+", "OH-"]:
                        if list(self.thermo.csv_db[self.thermo.csv_db["name"]==r]["state"])[0] != "aq":
                            invalid_limiting_reactants.append(r)
                    else:
                        invalid_limiting_reactants.append(r)
                if reactants == invalid_limiting_reactants and y_type == "E":
                    self.err_handler.raise_exception("Energy supply for this reaction "
                        "cannot be calculated because none of the reactants are "
                        "limiting. A limiting reactant must be aqueous and cannot be H+ "
                        "or OH-.")

                if limiting != None:
                    # check that the limiting reactant is in the thermodynamic database
                    if limiting not in list(self.thermo.csv_db["name"]):
                        self.err_handler.raise_exception("Valid thermodynamic data was "
                                "not found for limiting reactant "+str(limiting)+"")

                    # check that the limiting reactant is aqueous or gaseous
                    if list(self.thermo.csv_db[self.thermo.csv_db["name"]==limiting]["state"])[0] != "aq":
                        self.err_handler.raise_exception("The limiting reactant must "
                                "be an aqueous species.")

                    # check that the limiting reactant is a reactant in the `species` parameter
                    if limiting not in reactants:
                        self.err_handler.raise_exception("The species specified as a "
                                "limiting reactant, '"+str(limiting)+"', is not a "
                                "reactant in this reaction.")

                # format reaction equation
                equation_to_display = format_equation(
                                              species,
                                              stoich,
                                              charge_sign_at_end=charge_sign_at_end,
                                              )

                # create a dictionary of species logacts across xi
                s_logact_dict = {}
                s_molal_dict = {}
                for s in species:
                    if s == "H+":
                        s_logact_dict[s] = [-pH for pH in list(self.misc_params["pH"])]
                        s_molal_dict[s] = [float("NaN")]*len(self.misc_params["pH"])
                    elif s == "H2O":
                        s_logact_dict[s] = [0]*len(self.misc_params["Temp(C)"])
                        s_molal_dict[s] = [float("NaN")]*len(self.misc_params["Temp(C)"])
                    elif list(self.thermo.csv_db[self.thermo.csv_db["name"]==s]["state"])[0] not in ["cr", "liq"]:
                        if s in self.aq_distribution_logact.columns:
                            # aqueous species
                            s_logact_dict[s] = list(self.aq_distribution_logact[s])
                            s_molal_dict[s] = list(self.aq_distribution_molal[s])
                        else:
                            self.err_handler.raise_exception("The species "+str(s)+" is "
                                    "not among the distribution of aqueous species in "
                                    "this calculation.")
                    else:
                        # liq and cr species
                        s_logact_dict[s] = [0]*len(self.misc_params["Temp(C)"])
                        s_molal_dict[s] = [float("NaN")]*len(self.misc_params["Temp(C)"])

                xlab, xvar = self.__get_xlab_xvar(x_type)

                if y_type not in ["logK", "logQ"]:
                    if y_units in ["cal", "kcal"]:
                        r_div = 4.184
                    elif y_units in ["J", "kJ"]:
                        r_div = 1
                    R = 8.314/r_div  # gas constant, unit = [cal/mol/K]

                if "k" in y_units:
                    k_div = 1000
                else:
                    k_div = 1

                y_list = []
                lr_name_list = []
                for i,T in enumerate(list(self.misc_params["Temp(C)"])):

                    if isinstance(divisor, list):
                        divisor_i = divisor[i]
                    else:
                        divisor_i = divisor

                    if y_type != "logQ":
                        logK = pychnosz.subcrt(
                                      species,
                                      stoich,
                                      T=T,
                                      P=list(self.misc_params["Press(bars)"])[i],
                                      IS=0, # ionic strength of 0 ensures that the logK calculated is standard, as required
                                      messages=print_logK_messages,
                                      show=False).out["logK"]

                        logK = float(logK.iloc[0])

                    if y_type == "logK":
                        ylab_out = "log K"
                        if title == None:
                            title = "Equilibrium constant for the reaction<br>"+equation_to_display
                        y_list.append(round(logK/divisor_i, 4))
                        df_y_name = "logK"
                        continue

                    logQ = sum([st*s_logact_dict[sp][i] for st,sp in zip(stoich,species)])

                    if y_type == "logQ":
                        ylab_out = "log Q"
                        if title == None:
                            title = "Reaction quotient for the reaction<br>"+equation_to_display
                        y_list.append(round(logQ/divisor_i, 4))
                        df_y_name = "logQ"
                        continue

                    else:
                        A = 2.303 * R * (273.15+T) * (logK - logQ)  # affinity, unit = [cal/mol]
                        A = A/k_div

                        if title == None:
                            title = "Energy profile for the reaction<br>"+equation_to_display

                        if y_type=="G":
                            G = -A # gibbs free energy, unit = [cal/mol]
                            y_list.append(G/divisor_i)
                            ylab_out="ΔG, {}/mol".format(y_units)
                            y_units_out = y_units+"/mol"
                        elif y_type=="A":
                            y_list.append(round(A/divisor_i, 4))
                            ylab_out="A, {}/mol".format(y_units)
                            y_units_out = y_units+"/mol"
                        elif y_type=="E":

                            if not isinstance(limiting, str):
                                lrc_dict = {}
                                for i_s,s in enumerate(species):
                                    # identify valid limiting reactants and record concentrations
                                    # 1. negative coefficient (reactant)
                                    # 2. can't be OH-, H+, H2O
                                    # 3. can't be cr or liq
                                    if stoich[i_s] < 0 and s not in ["H2O", "H+", "OH-"] and list(self.thermo.csv_db[self.thermo.csv_db["name"]==s]["state"])[0] not in ["cr", "liq"]:
                                        lrc_dict[s] = s_molal_dict[s][i]/abs(stoich[i_s])

                            if not isinstance(limiting, str):
                                lr_name = min(lrc_dict, key=lrc_dict.get)
                                lr_val = lrc_dict[lr_name]

                                # handle situations where there might be multiple limiting reactants
                                lr_list = []
                                for k,v in zip(lrc_dict.keys(), lrc_dict.values()):
                                    if v == lr_val:
                                        lr_list.append(str(k))

                            else:
                                lr_list = [limiting]

                            lr_list_formatted = [chemlabel(lr_name, charge_sign_at_end=charge_sign_at_end) for lr_name in lr_list]
                            if len(lr_list_formatted) > 1:
                                lr_reported = ", ".join(lr_list_formatted)
                            else:
                                lr_reported = lr_list[0]

                            lr_name = lr_list[0] # doesn't matter which lr is used to calculate
                            lr_concentration = s_molal_dict[lr_name][i]
                            lr_name_list.append(lr_reported)
                            lr_stoich = -stoich[species.index(lr_name)]
                            E = A * (lr_concentration/lr_stoich)

                            y_list.append(round(E/divisor_i, 4))
                            y_units_out = y_units+"/kg fluid"
                            ylab_out="Energy Supply, {}".format(y_units+"/kg fluid")
        #                     else:
        #                         lr_name_list.append("NA")
        #                         y_list.append(float('NaN'))
        #                         y_units_out = y_units+"/kg fluid"
        #                         ylab_out="Energy Supply, {}".format(y_units+"/kg fluid")

                        df_y_name = y_type+", "+y_units_out

                if xlab != None:
                    xlab_out = xlab
                if ylab != None:
                    ylab_out = ylab

                df = copy.deepcopy(self.misc_params)
                with np.errstate(divide='ignore'):
                    df['log Xi'] = np.log10(df['Xi'])

                df[df_y_name] = y_list

                fig = px.line(df, x=xvar, y=df_y_name, log_y=log_y,
                              width=plot_width*ppi, height=plot_height*ppi,
                              template="simple_white")

                if y_type=="E":
                    fig.add_trace(
                        go.Scatter(
                            x=df[xvar],
                            y=df[df_y_name],
                            mode='lines',
                            customdata = lr_name_list,
                            hovertemplate = xlab+': %{x}<br>'+ylab_out+': %{y}<br>Limiting : %{customdata}<extra></extra>',
                        )
                    )
                    fig.data[0].visible=False

                fig.update_layout(xaxis_title=xlab_out,
                                  yaxis_title=ylab_out)

                if isinstance(title, str):
                    fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

                if isinstance(ylim, list):
                    fig.update_layout(yaxis_range=ylim)

                if isinstance(xlim, list):
                    fig.update_layout(xaxis_range=xlim)

                if isinstance(save_as, str):
                    dummy_sp = Speciation({})
                    save_as, save_format = dummy_sp._save_figure(fig,
                            save_as, save_format, save_scale,
                            plot_width, plot_height, ppi)

                if show_zero_line:
                    fig.add_hline(y=0, line_width=3, line_dash="dash", line_color="black")

                if df_out:
                    return fig, df
                else:
                    return fig

    Generate a line plot of the energy profile for a reaction as a function of the log of the extent of reaction (log Xi) or some other variable.

    ## Parameters

    `species` :`list` of `str`
      A list of species that match the order of the stoichiometric reaction coefficients in the `stoich` parameter.
    `stoich` :`list` of `numeric`
      A list of stoichiometric reaction coefficients that match the order of the species in the `species` parameter.
    `divisor` :`float`, default `1`
      Divide all values in the energy profile by this number. Useful for calculating energy per electron transferred or similar.
    `x_type` :`str`, default `"logxi"`
      Variable to appear on the x-axis. Can be "logxi", "xi", "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or "aw".
    `y_type` :`str`, default `'A'`
      The variable to plot on the y-axis. Can be either 'A' (for chemical affinity), 'G' (for Gibbs free energy, ΔG), 'logK' (for the log of the equilibrium constant), 'logQ' (for the log of the reaction quotient), or 'E' for energy supply.
    `y_units` :`str`, default `'kcal'`
      The unit that energy will be reported in (per mol for G and A, or per kg fluid for energy supply, or unitless for logK and logQ). Can be 'kcal', 'cal', 'J', or 'kJ'.
    `show_zero_line` :`bool`, default `False`
      If True, displays a dotted line where affinity or ΔG equals 0 (at equilibrium).
    `limiting` :`str`, optional
      Name of the species to act as the limiting reactant when calculating energy supply. If this parameter is left undefined, then a limiting reactant will be chosen automatically based on concentration and stoichiometry. This parameter is ignored unless `y_type` is set to 'E' (energy supply).
    `xlab`, `ylab` :`str`, optional
      Custom x and y axis labels.
    `title` :`str`, optional
      Title of the plot to display.
    `charge_sign_at_end` :`bool`, default `False`
      Display charge with sign after the number (e.g. SO4 2- instead of SO4-2) in species names when the reaction is displayed in the plot title?
    `plot_width`, `plot_height` :`numeric`, default `4 by 3`
      Width and height of the plot, in inches. Size of interactive plots is also determined by pixels per inch, set by the parameter `ppi`.
    `ppi` :`numeric`, default `122`
      Pixels per inch. Along with `plot_width` and `plot_height`, determines the size of interactive plots.
    `xlim`, `ylim` :`list` of `two numeric values`, optional
      Minimum and maximum value of the x-axis and y-axis, respectively.

    df_out = bool, default False Return a pandas dataframe in addition to a figure?

    `save_as` :`str`, optional
      Provide a filename to save this figure. Filetype of saved figure is determined by `save_format`. Note: interactive plots can be saved by clicking the 'Download plot' button in the plot's toolbar.
    `save_format` :`str`, default `"png"`
      Desired format of saved or downloaded figure. Can be 'png', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html', an interactive plot will be saved. Only 'png', 'svg', 'jpeg', and 'webp' can be downloaded with the 'download as' button in the toolbar of an interactive plot.
    `save_scale` :`numeric`, default `1`
      Multiply title/legend/axis/canvas sizes by this factor when saving the figure.
    `print_logK_messages` :`bool`, default `False`
      Print chnosz messages while the logK of the reaction is calculated?

    ## Returns

    `fig` :`Plotly figure object, optionally a Pandas Dataframe`
      A line plot. If `df_out` is True, also returns a dataframe.
  `def plot_mass_contribution(self, *args, x_type='xi', x_decimals=3, track_steps=True, keep_xi_order=False, **kwargs)`
    Expand source code

        def plot_mass_contribution(self, *args, x_type="xi", x_decimals=3,
                                         track_steps=True, keep_xi_order=False,
                                         **kwargs):

            """
            Generate a bar plot of mass contributions (in mole percent) of aqueous
            species formed as a function of reaction progress Xi or some other
            variable.

            Parameters
            ----------
            *args : iterable
                Arguments to be passed to `Speciation.plot_mass_contribution`.

            x_type : str, default "xi"
                Variable to appear on the x-axis. Can be "logxi", "xi",
                "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                "aw".

            x_decimals : int
                Number of decimals to display in the numeric values of the x-axis
                variable defined by `x_type`.

            track_steps : bool, default True
                Show reported xi steps on x-axis ticks? Useful for plotting as a
                function of an x-axis variable that can be out-of-order, like
                Eh or temperature. This parameter will become True automatically
                if it will prevent column stacking (which happens if there are
                duplicate x-axis values).

            **kargs : dict
                Keyword arguments to be passed to `Speciation.plot_mass_contribution`.

            Returns
            -------
            fig : Plotly figure object
                A mass contribution bar plot.
            """

            basis = args[0]

            xlab, kwargs["sample_label"] = self.__get_xlab_xvar(x_type)

            df_sp = pd.concat([self.mass_contribution_dict.get(basis, "error"),
                               self.misc_params[self.misc_params.columns[1:]]], axis=1)

            with np.errstate(divide='ignore'):
                df_sp['log Xi'] = np.log10(df_sp['Xi'])

            # handle display of the x axis variable
            # if number of decimals to display is too low, columns will stack
            # check to see if this happens, then increment x_decimals until the stacking problem is solved
            if x_decimals < 0:
                msg = "The parameter x_decimals must be greater than or equal to 0."
                self.err_handler.raise_exception(msg)
            original_x_decimals = copy.copy(x_decimals)

            len_unique_labels_rounded = len(set(df_sp[kwargs["sample_label"]].apply(lambda x: ('%.'+str(x_decimals)+'e') % x)))
            len_unique_labels_unrounded = len(set(df_sp[kwargs["sample_label"]]))

            if len_unique_labels_rounded < len_unique_labels_unrounded:
                x_decimals += 1
                solved_decimals = False
                for i in range(x_decimals, x_decimals+10):
                    len_unique_labels_rounded = len(set(df_sp[kwargs["sample_label"]].apply(lambda x: ('%.'+str(x_decimals)+'e') % x)))
                    if len_unique_labels_rounded == len_unique_labels_unrounded:
                        if self.verbose > 0:
                            print("Number of decimals to display for x-axis variable",
                                  "increased to", x_decimals, "to prevent column stacking.")
                        solved_decimals = True
                        break
                    else:
                        x_decimals += 1
                if not solved_decimals:
                    msg = ("X-axis value decimal formatting is resulting in column "
                           "stacking even after attempting 10 increments of x_decimals.")
                    self.err_handler.raise_exception(msg)

            if isinstance(df_sp, str):
                msg = ("The basis species {} ".format(basis)+"could not be found "
                       "among available basis species: "
                       "{}".format(str(list(self.mass_contribution_dict.keys()))))
                self.err_handler.raise_exception(msg)

            df_sp["position"] = list(range(0, df_sp.shape[0]))

            df_sp_melt = df_sp.melt(
                    id_vars=list(self.misc_params.columns)+["log Xi", "basis", "factor", "molality", "position"])

            df_sp_melt.rename(columns={kwargs["sample_label"] : "sample",
                                       df_sp_melt.columns[-2] : "species",
                                       df_sp_melt.columns[-1] : "percent",
                                       }, inplace=True)

            df_sp_melt = df_sp_melt[df_sp_melt['percent'].notna()]

            if keep_xi_order:
                sample_order = ["position", "species", "percent"]
            else:
                sample_order = ["sample", "position", "species", "percent"]

            df_sp_melt.sort_values(sample_order, axis=0, ascending=True, inplace=True)

            if df_sp_melt.dtypes["sample"] != "O": # if the column isn't formatted as a string
                if any(["e" in v2 for v2 in [str(v1) for v1 in df_sp_melt['sample']]]): # if value is in scientific notation
                    df_sp_melt['sample'] = df_sp_melt['sample'].apply(lambda x: ('%.'+str(x_decimals)+'e') % x) # converts numeric to string
                else:
                    df_sp_melt['sample'] = [('{0:.'+str(x_decimals)+'f}').format(v) for v in df_sp_melt['sample']]

            if len(list(set(df_sp_melt["sample"]))) != len(list(set(df_sp_melt["position"]))) or track_steps:
                # handle duplicate x-axis values to prevent stacking
                temp_col = []
                for i,v in enumerate(df_sp_melt["sample"]):
                    temp_col.append(" (step "+str(list(df_sp_melt["position"])[i])+")")
                df_sp_melt["sample"] = [str(v)+a for v,a in zip(df_sp_melt["sample"], temp_col)]
                if not track_steps and self.verbose > 0:
                    print("Reported Xi step tracking has been added to x-axis ticks to prevent column stacking.")

            sp = Speciation(args={})
            sp.mass_contribution = df_sp_melt

            if not kwargs.get("plot_out", False):
                plot_out = False
            else:
                plot_out = True
            kwargs["plot_out"] = True

            fig = sp.plot_mass_contribution(*args, **kwargs)

            fig.update_layout(
                xaxis_title=xlab, # add an x axis title
            )

            if plot_out:
                return fig
            else:
                fig.show()

    Generate a bar plot of mass contributions (in mole percent) of aqueous species formed as a function of reaction progress Xi or some other variable.

    ## Parameters

    `*args` :`iterable`
      Arguments to be passed to `Speciation.plot_mass_contribution`.
    `x_type` :`str`, default `"xi"`
      Variable to appear on the x-axis. Can be "logxi", "xi", "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or "aw".
    `x_decimals` :`int`
      Number of decimals to display in the numeric values of the x-axis variable defined by `x_type`.
    `track_steps` :`bool`, default `True`
      Show reported xi steps on x-axis ticks? Useful for plotting as a function of an x-axis variable that can be out-of-order, like Eh or temperature. This parameter will become True automatically if it will prevent column stacking (which happens if there are duplicate x-axis values).
    `**kargs` :`dict`
      Keyword arguments to be passed to `Speciation.plot_mass_contribution`.

    ## Returns

    `fig` :`Plotly figure object`
      A mass contribution bar plot.
  `def plot_mineral_saturation(self,
  solid_solutions=False,
  plot_minerals=None,
  x_type='logxi',
  y_type='affinity',
  log_y=True,
  df_out=False,
  markers=False,
  title=None,
  plot_width=4,
  plot_height=3,
  ppi=122,
  ylim=None,
  show_legend=True,
  save_as=None,
  save_format=None,
  save_scale=1)`
    Expand source code

        def plot_mineral_saturation(self, solid_solutions=False, plot_minerals=None,
                                          x_type="logxi", y_type="affinity", log_y=True,
                                          df_out=False, markers=False, title=None,
                                          plot_width=4, plot_height=3, ppi=122, ylim=None,
                                          show_legend=True, save_as=None, save_format=None,
                                          save_scale=1):

            """
            Generate a line plot of the saturation indices of minerals or solid
            solutions as a function of the log of the extent of reaction (log Xi) or
            some other variable.

            Parameters
            ----------
            plot_minerals : list, optional
                List of minerals to plot. Useful for isolating one or more
                minerals.

            x_type : str, default "logxi"
                Variable to appear on the x-axis. Can be "logxi", "xi",
                "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                "aw".

            y_type : str, default 'affinity'
                The variable to plot on the y-axis. Can be either 'affinity'
                or 'logQ/K'.

            df_out : bool, default False
                Should a dataframe of values also be returned? For example, if
                `y_type` is set to 'volume', should a table of mineral volumes be
                returned?

            markers : bool, default True
                Add circular markers to lines to indicate calculation steps?

            title : str, optional
                Used to customize the title of the plot.

            plot_width, plot_height : numeric, default 4 by 3
                Width and height of the plot, in inches. Size of interactive plots
                is also determined by pixels per inch, set by the parameter `ppi`.

            ppi : numeric, default 122
                Pixels per inch. Along with `plot_width` and `plot_height`,
                determines the size of interactive plots.

            ylim : list of two numeric values, optional
                Minimum and maximum value of the y-axis.

            show_legend : bool, default True
                Show the legend?

            save_as : str, optional
                Provide a filename to save this figure. Filetype of saved figure is
                determined by `save_format`.
                Note: interactive plots can be saved by clicking the 'Download plot'
                button in the plot's toolbar.

            save_format : str, default "png"
                Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                and 'webp' can be downloaded with the 'download as' button in the
                toolbar of an interactive plot.

            save_scale : numeric, default 1
                Multiply title/legend/axis/canvas sizes by this factor when saving
                the figure.

            Returns
            -------
            fig : Plotly figure object
                A line plot.

            df : a Pandas dataframe
                A dataframe is only returned if `df_out` is set to True (it is
                set to False by default).

            """

            xlab, xvar = self.__get_xlab_xvar(x_type)

            if not isinstance(title, str):
                title = "Saturation indices of "

            if not solid_solutions:
                if y_type == "affinity":
                    df = pd.concat([self.saturation_states_pure_solids_affinity,
                                    self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "affinity, kcal/mol"
                else:
                    df = pd.concat([self.saturation_states_pure_solids_log_Q_over_K,
                                    self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "logQ/K"

                title = title + "minerals"
            else:
                if y_type == "affinity":
                    df = pd.concat([self.saturation_states_solid_solutions_affinity,
                                    self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "affinity, kcal/mol"
                else:
                    df = pd.concat([self.saturation_states_solid_solutions_log_Q_over_K,
                                    self.misc_params[self.misc_params.columns[1:]]], axis=1)
                    ylab = "logQ/K"
                title = title + "solid solutions"

            plot_columns = [col for col in df.columns if col not in list(self.misc_params.columns)]

            if isinstance(plot_minerals, list):
                plot_columns_temp = [col for col in plot_columns if col in plot_minerals]
                plot_columns = plot_columns_temp

            df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
            df.columns = list(self.misc_params.columns)+["variable", "value"]

            df = df[df["variable"] != "None"]

            df["Xi"] = pd.to_numeric(df["Xi"])

            df["value"] = pd.to_numeric(df["value"])
            df["value"] = df["value"].fillna(0)

            with np.errstate(divide='ignore'):
                df['log Xi'] = np.log10(df['Xi'])

            fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                          width=plot_width*ppi,  height=plot_height*ppi, markers=markers,
                          labels=dict(value=ylab, x=xlab), render_mode='svg',
                          )

            fig.update_layout(xaxis_title=xlab,
                              yaxis_title=ylab,
                              legend_title=None,
                              showlegend=show_legend)

            if isinstance(title, str):
                fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

            if isinstance(ylim, list):
                fig.update_layout(yaxis_range=ylim)

            if isinstance(save_as, str):
                dummy_sp = Speciation({})
                save_as, save_format = dummy_sp._save_figure(fig,
                        save_as, save_format, save_scale,
                        plot_width, plot_height, ppi)

            return fig

    Generate a line plot of the saturation indices of minerals or solid solutions as a function of the log of the extent of reaction (log Xi) or some other variable.

    ## Parameters

    `plot_minerals` :`list`, optional
      List of minerals to plot. Useful for isolating one or more minerals.
    `x_type` :`str`, default `"logxi"`
      Variable to appear on the x-axis. Can be "logxi", "xi", "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or "aw".
    `y_type` :`str`, default `'affinity'`
      The variable to plot on the y-axis. Can be either 'affinity' or 'logQ/K'.
    `df_out` :`bool`, default `False`
      Should a dataframe of values also be returned? For example, if `y_type` is set to 'volume', should a table of mineral volumes be returned?
    `markers` :`bool`, default `True`
      Add circular markers to lines to indicate calculation steps?
    `title` :`str`, optional
      Used to customize the title of the plot.
    `plot_width`, `plot_height` :`numeric`, default `4 by 3`
      Width and height of the plot, in inches. Size of interactive plots is also determined by pixels per inch, set by the parameter `ppi`.
    `ppi` :`numeric`, default `122`
      Pixels per inch. Along with `plot_width` and `plot_height`, determines the size of interactive plots.
    `ylim` :`list` of `two numeric values`, optional
      Minimum and maximum value of the y-axis.
    `show_legend` :`bool`, default `True`
      Show the legend?
    `save_as` :`str`, optional
      Provide a filename to save this figure. Filetype of saved figure is determined by `save_format`. Note: interactive plots can be saved by clicking the 'Download plot' button in the plot's toolbar.
    `save_format` :`str`, default `"png"`
      Desired format of saved or downloaded figure. Can be 'png', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html', an interactive plot will be saved. Only 'png', 'svg', 'jpeg', and 'webp' can be downloaded with the 'download as' button in the toolbar of an interactive plot.
    `save_scale` :`numeric`, default `1`
      Multiply title/legend/axis/canvas sizes by this factor when saving the figure.

    ## Returns

    `fig` :`Plotly figure object`
      A line plot.
    `df` :`a Pandas dataframe`
      A dataframe is only returned if `df_out` is set to True (it is set to False by default).
  `def plot_pH(self,
  x_type='logxi',
  show_neutrality=True,
  title=None,
  plot_width=4,
  plot_height=3,
  ppi=122,
  ylim=None,
  save_as=None,
  save_format=None,
  save_scale=1)`
    Expand source code

        def plot_pH(self, x_type="logxi", show_neutrality=True, title=None,
                    plot_width=4, plot_height=3, ppi=122, ylim=None, save_as=None,
                    save_format=None, save_scale=1):

            """
            Generate a line plot of pH as a function of the log of the extent of
            reaction (log Xi) or some other variable.

            Parameters
            ----------
            x_type : str, default "logxi"
                Variable to appear on the x-axis. Can be "logxi", "xi",
                "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                "aw".

            show_neutrality : bool, default True,
                Display a reference line representing neutral pH? Setting this
                option to True requires a thermodynamic database in a WORM-style CSV
                format, e.g., 'wrm_data.csv'.

            title : str
                Title of the plot to display.

            plot_width, plot_height : numeric, default 4 by 3
                Width and height of the plot, in inches. Size of interactive plots
                is also determined by pixels per inch, set by the parameter `ppi`.

            ppi : numeric, default 122
                Pixels per inch. Along with `plot_width` and `plot_height`,
                determines the size of interactive plots.

            ylim : list of two numeric values, optional
                Minimum and maximum value of the y-axis.

            save_as : str, optional
                Provide a filename to save this figure. Filetype of saved figure is
                determined by `save_format`.
                Note: interactive plots can be saved by clicking the 'Download plot'
                button in the plot's toolbar.

            save_format : str, default "png"
                Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                and 'webp' can be downloaded with the 'download as' button in the
                toolbar of an interactive plot.

            save_scale : numeric, default 1
                Multiply title/legend/axis/canvas sizes by this factor when saving
                the figure.

            Returns
            -------
            fig : Plotly figure object
                A line plot.
            """

            df = copy.deepcopy(self.misc_params)

            xlab, xvar = self.__get_xlab_xvar(x_type)

            if x_type == "logxi":
                with np.errstate(divide='ignore'):
                    df['log Xi'] = np.log10(df['Xi'])

            fig = px.line(df, x=xvar, y="pH", template="simple_white",
                          width=plot_width*ppi,  height=plot_height*ppi,
                          labels=dict(value="pH", x=xlab), render_mode='svg',
                          )

            fig.update_layout(xaxis_title=xlab,
                              yaxis_title="pH",
                              showlegend=False)

            if isinstance(title, str):
                fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

            if isinstance(ylim, list):
                fig.update_layout(yaxis_range=ylim)

            if show_neutrality and isinstance(self.thermo.csv_db, pd.DataFrame):
                _, df_pH = self.plot_energy(species=["H2O", "H+", "OH-"],
                                            stoich=[-1, 1, 1],
                                            divisor=-2,
                                            x_type=x_type,
                                            y_type="logK",
                                            df_out=True,
                                            )

                fig.add_trace(go.Scatter(x=df_pH[xvar],
                                         y=df_pH["logK"],
                                         mode='lines',
                                         name='neutral pH',
                                         showlegend=True,
                                         hovertemplate = xlab+': %{x}<br>pH: %{y}<extra></extra>',
                                         line=dict(color='silver',
                                                   width=3,
                                                   dash='dot')
                                        ))

                fig['data'][0]['showlegend']=True
                fig['data'][0]['name']='pH'
                fig['data'][1]['showlegend']=True
                fig['data'][1]['name']='neutral pH'
                fig.update_layout(showlegend=True)

            if isinstance(save_as, str):
                dummy_sp = Speciation({})
                save_as, save_format = dummy_sp._save_figure(fig,
                        save_as, save_format, save_scale,
                        plot_width, plot_height, ppi)

            return fig

    Generate a line plot of pH as a function of the log of the extent of reaction (log Xi) or some other variable.

    ## Parameters

    `x_type` :`str`, default `"logxi"`
      Variable to appear on the x-axis. Can be "logxi", "xi", "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or "aw".
    `show_neutrality` :`bool`, default `True,`
      Display a reference line representing neutral pH? Setting this option to True requires a thermodynamic database in a WORM-style CSV format, e.g., 'wrm_data.csv'.
    `title` :`str`
      Title of the plot to display.
    `plot_width`, `plot_height` :`numeric`, default `4 by 3`
      Width and height of the plot, in inches. Size of interactive plots is also determined by pixels per inch, set by the parameter `ppi`.
    `ppi` :`numeric`, default `122`
      Pixels per inch. Along with `plot_width` and `plot_height`, determines the size of interactive plots.
    `ylim` :`list` of `two numeric values`, optional
      Minimum and maximum value of the y-axis.
    `save_as` :`str`, optional
      Provide a filename to save this figure. Filetype of saved figure is determined by `save_format`. Note: interactive plots can be saved by clicking the 'Download plot' button in the plot's toolbar.
    `save_format` :`str`, default `"png"`
      Desired format of saved or downloaded figure. Can be 'png', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html', an interactive plot will be saved. Only 'png', 'svg', 'jpeg', and 'webp' can be downloaded with the 'download as' button in the toolbar of an interactive plot.
    `save_scale` :`numeric`, default `1`
      Multiply title/legend/axis/canvas sizes by this factor when saving the figure.

    ## Returns

    `fig` :`Plotly figure object`
      A line plot.
  `def plot_product_minerals(self,
  show_reactant_minerals=False,
  plot_minerals=None,
  x_type='logxi',
  y_type='mole',
  log_y=True,
  df_out=False,
  markers=False,
  plot_width=4,
  plot_height=3,
  ppi=122,
  ylim=None,
  show_legend=True,
  save_as=None,
  save_format=None,
  save_scale=1)`
    Expand source code

        def plot_product_minerals(self, show_reactant_minerals=False,
                                  plot_minerals=None, x_type="logxi", y_type="mole",
                                  log_y=True, df_out=False, markers=False,
                                  plot_width=4, plot_height=3, ppi=122, ylim=None,
                                  show_legend=True, save_as=None, save_format=None,
                                  save_scale=1):

            """
            Generate a line plot of the log moles of product minerals as a
            function of the log of the extent of reaction (log Xi) or some other
            variable.

            Parameters
            ----------
            show_reactant_minerals : bool, default False
                Include log moles of reactant minerals?

            plot_minerals : list, optional
                List of minerals to plot. Useful for isolating one or more
                minerals.

            x_type : str, default "logxi"
                Variable to appear on the x-axis. Can be "logxi", "xi",
                "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or
                "aw".

            y_type : str, default 'mole'
                The variable to plot on the y-axis. Can be either 'mole' (for moles
                of minerals), 'mass' (for masses of minerals), or 'volume' (for
                volumes of minerals).

            log_y : bool, default True
                Should the y-axis be logarithmic?

            df_out : bool, default False
                Should a dataframe of values also be returned? For example, if
                `y_type` is set to 'volume', should a table of mineral volumes be
                returned?

            markers : bool, default True
                Add circular markers to lines to indicate calculation steps?

            plot_width, plot_height : numeric, default 4 by 3
                Width and height of the plot, in inches. Size of interactive plots
                is also determined by pixels per inch, set by the parameter `ppi`.

            ppi : numeric, default 122
                Pixels per inch. Along with `plot_width` and `plot_height`,
                determines the size of interactive plots.

            ylim : list of two numeric values, optional
                Minimum and maximum value of the y-axis.

            show_legend : bool, default True
                Show the legend?

            save_as : str, optional
                Provide a filename to save this figure. Filetype of saved figure is
                determined by `save_format`.
                Note: interactive plots can be saved by clicking the 'Download plot'
                button in the plot's toolbar.

            save_format : str, default "png"
                Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                and 'webp' can be downloaded with the 'download as' button in the
                toolbar of an interactive plot.

            save_scale : numeric, default 1
                Multiply title/legend/axis/canvas sizes by this factor when saving
                the figure.

            Returns
            -------
            fig : Plotly figure object
                A line plot.

            df : a Pandas dataframe
                A dataframe is only returned if `df_out` is set to True (it is
                set to False by default).

            """

            xlab, xvar = self.__get_xlab_xvar(x_type)

            if show_reactant_minerals:
                df = pd.concat([self.moles_minerals, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                title = "{}{} of reactant and product minerals"
            else:
                df = pd.concat([self.moles_product_minerals, self.misc_params[self.misc_params.columns[1:]]], axis=1)
                title = "{}{} of product minerals"

            if log_y:
                log_text = "log "
            else:
                log_text = ""

            # sort in order of appearance along Xi
            sort_order = list(self.moles_minerals.columns)

            sort_order = [v for v in sort_order if v not in self.misc_params.columns]

            if y_type == "mole":
                ylab = "{}moles".format(log_text)
                title = title.format(log_text, "moles")
            elif y_type == "mass": # not yet supported
                y_lab = "{}grams".format(log_text)
                title = title.format(log_text, "masses")
                self.err_handler.raise_exception("Plotting mineral masses is not yet "
                        "supported.")
            elif y_type == "volume":

                if not isinstance(self.df, pd.DataFrame):
                    self.err_handler.raise_exception("Plotting mineral volume "
                            "requires a WORM-formatted CSV thermodynamic database. "
                            "You may be seeing this message because your speciation "
                            "used a data0-type thermodynamic database (e.g., 'wrm'), "
                            "which does not contain mineral volume data.")

                ylab = "{}cm<sup>3</sup>".format(log_text)
                title = title.format(log_text, "volumes")
                temps = df["Temp(C)"]

                minerals = [col for col in df.columns if col not in list(self.misc_params.columns)]

                for i,T in enumerate(temps):
                    for ii,mineral in enumerate(minerals):
                        mineral_df = copy.deepcopy(self.df[self.df["name"]==mineral])
                        polymorph_idxs = []
                        for iii in range(0, mineral_df.shape[0]): # loop through mineral polymorphs

                            if float(T) < float(list(mineral_df["z.T"])[0]):
                                polymorph_idxs.append(iii)
                        if len(polymorph_idxs)==0:
                            polymorph_idx = iii
                        else:
                            polymorph_idx = polymorph_idxs[0]

                        partial_molal_volume = list(mineral_df["V"])[polymorph_idx]

                        df.at[i, mineral] = df[mineral][i]*partial_molal_volume
            else:
                self.err_handler.raise_exception("y_type must be either 'mole', "
                            "'mass', or 'volume'.")

            plot_columns = [col for col in df.columns if col not in list(self.misc_params.columns)]

            if isinstance(plot_minerals, list):
                plot_columns_temp = [col for col in plot_columns if col in plot_minerals]
                plot_columns = plot_columns_temp

            plot_columns = sorted(plot_columns, key=sort_order.index)

            df = pd.melt(df, id_vars=list(self.misc_params.columns), value_vars=plot_columns)
            df.columns = list(self.misc_params.columns)+["variable", "value"]

            df = df[df["variable"] != "None"]

            df["Xi"] = pd.to_numeric(df["Xi"])

            df["value"] = pd.to_numeric(df["value"])
            df["value"] = df["value"].fillna(0)

            with np.errstate(divide='ignore'):
                df['log Xi'] = np.log10(df['Xi'])
                if log_y:
                    df["value"] = df["value"].replace(0, np.nan)
                    df['value'] = np.log10(df['value'])

            fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                          width=plot_width*ppi,  height=plot_height*ppi, markers=markers,
                          labels=dict(value=ylab, x=xlab), render_mode='svg',
                          )

            df_to_return = copy.deepcopy(df)

            # add lines that go to -9999 (representing -Inf) in log y plots
            if log_y:
                if not isinstance(ylim, list):
                    # grab the automatic y-axis range from the plot, above
                    full_fig = fig.full_figure_for_development(warn=False)
                    ylim = list(full_fig.layout.yaxis.range)

                # add new rows
                for irow in range(0, df.shape[0]):
                    if not isnan(df["value"].iloc[irow]) and irow != 0 and irow != df.shape[0]-1:
                        if isnan(df["value"].iloc[irow-1]):

                            df_dict = {}
                            for col in list(self.misc_params.columns)+["log Xi"]:
                                df_dict[col] = df.loc[irow-1, col]
                            df_dict["variable"] = df.loc[irow-1, "variable"]
                            df_dict["value"] = df.loc[irow-1, "value"]

                            new_row = pd.DataFrame(df_dict, index=[irow-1])
                            df = pd.concat([df.iloc[:irow], new_row, df.iloc[irow:]]).reset_index(drop=True)
                            irow=0

                        if isnan(df["value"].iloc[irow+1]):

                            df_dict = {}
                            for col in list(self.misc_params.columns)+["log Xi"]:
                                df_dict[col] = df.loc[irow+1, col]
                            df_dict["variable"] = df.loc[irow+1, "variable"]
                            df_dict["value"] = df.loc[irow+1, "value"]

                            new_row = pd.DataFrame(df_dict, index=[irow+1])
                            df = pd.concat([df.iloc[:irow], new_row, df.iloc[irow:]]).reset_index(drop=True)
                            irow=0

                # fill new rows with -9999 value
                for irow in range(0, df.shape[0]):
                    if not isnan(df["value"].iloc[irow]) and irow != 0 and irow != df.shape[0]-1:
                        if isnan(df["value"].iloc[irow-1]):
                            df.loc[irow-1, "value"] = -9999
                        if isnan(df["value"].iloc[irow+1]):
                            df.loc[irow+1, "value"] = -9999

                # re-create the plot with log values down to -9999. This will screw up the
                # automatic y-axis range, but we grabbed it from the first generated plot.
                # We will reset the range in update_layout(yaxis_range) a little later.
                fig = px.line(df, x=xvar, y="value", color='variable', template="simple_white",
                              width=plot_width*ppi,  height=plot_height*ppi, markers=markers,
                              labels=dict(value=ylab, x=xlab), render_mode='svg',
                              )

            fig.update_layout(xaxis_title=xlab,
                              yaxis_title=ylab,
                              legend_title=None,
                              showlegend=show_legend)

            if isinstance(title, str):
                fig.update_layout(title={'text':title, 'x':0.5, 'xanchor':'center'})

            if isinstance(ylim, list):
                fig.update_layout(yaxis_range=ylim)

            if isinstance(save_as, str):
                dummy_sp = Speciation({})
                save_as, save_format = dummy_sp._save_figure(fig,
                        save_as, save_format, save_scale,
                        plot_width, plot_height, ppi)

            if df_out:
                df_to_return = pd.pivot_table(df_to_return, index='log Xi', columns='variable', values='value').reset_index()
                df_to_return.columns = [col for col in df_to_return.columns[:]] # make index column name blank
                return df_to_return, fig
            else:
                return fig

    Generate a line plot of the log moles of product minerals as a function of the log of the extent of reaction (log Xi) or some other variable.

    ## Parameters

    `show_reactant_minerals` :`bool`, default `False`
      Include log moles of reactant minerals?
    `plot_minerals` :`list`, optional
      List of minerals to plot. Useful for isolating one or more minerals.
    `x_type` :`str`, default `"logxi"`
      Variable to appear on the x-axis. Can be "logxi", "xi", "temperature", "pressure", "pH", "pmH", "logfO2", "Eh", "pe", or "aw".
    `y_type` :`str`, default `'mole'`
      The variable to plot on the y-axis. Can be either 'mole' (for moles of minerals), 'mass' (for masses of minerals), or 'volume' (for volumes of minerals).
    `log_y` :`bool`, default `True`
      Should the y-axis be logarithmic?
    `df_out` :`bool`, default `False`
      Should a dataframe of values also be returned? For example, if `y_type` is set to 'volume', should a table of mineral volumes be returned?
    `markers` :`bool`, default `True`
      Add circular markers to lines to indicate calculation steps?
    `plot_width`, `plot_height` :`numeric`, default `4 by 3`
      Width and height of the plot, in inches. Size of interactive plots is also determined by pixels per inch, set by the parameter `ppi`.
    `ppi` :`numeric`, default `122`
      Pixels per inch. Along with `plot_width` and `plot_height`, determines the size of interactive plots.
    `ylim` :`list` of `two numeric values`, optional
      Minimum and maximum value of the y-axis.
    `show_legend` :`bool`, default `True`
      Show the legend?
    `save_as` :`str`, optional
      Provide a filename to save this figure. Filetype of saved figure is determined by `save_format`. Note: interactive plots can be saved by clicking the 'Download plot' button in the plot's toolbar.
    `save_format` :`str`, default `"png"`
      Desired format of saved or downloaded figure. Can be 'png', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html', an interactive plot will be saved. Only 'png', 'svg', 'jpeg', and 'webp' can be downloaded with the 'download as' button in the toolbar of an interactive plot.
    `save_scale` :`numeric`, default `1`
      Multiply title/legend/axis/canvas sizes by this factor when saving the figure.

    ## Returns

    `fig` :`Plotly figure object`
      A line plot.
    `df` :`a Pandas dataframe`
      A dataframe is only returned if `df_out` is set to True (it is set to False by default).
  `def plot_reaction_paths(self,
  xyb=None,
  path_margin=0.25,
  flip_xy=False,
  show_annotation=True,
  annotation_coords=[0, 0],
  show_nonparticipating_mineral_lines=False,
  minerals_to_show=[],
  calculate_projected_points=True,
  path_line_type='markers+lines',
  path_line_color='red',
  path_point_fill_color='red',
  path_point_line_color='red',
  projected_point_fill_color='white',
  projected_point_line_color='red',
  h_line_color='black',
  v_line_color='black',
  d_line_color='black',
  res=300,
  plot_width=4,
  plot_height=3,
  ppi=122,
  borders=0,
  save_as=None,
  save_format=None,
  save_scale=1,
  colormap='bw')`
    Expand source code

        def plot_reaction_paths(self,
                                xyb=None,
                                path_margin=0.25,
                                flip_xy=False,
                                show_annotation=True,
                                annotation_coords=[0,0],
                                show_nonparticipating_mineral_lines=False,
                                minerals_to_show=[],
                                calculate_projected_points=True,
                                path_line_type = "markers+lines",
                                path_line_color = "red",
                                path_point_fill_color = "red",
                                path_point_line_color = "red",
                                projected_point_fill_color = "white",
                                projected_point_line_color = "red",
                                h_line_color="black",
                                v_line_color="black",
                                d_line_color="black",
                                res=300,
                                plot_width=4,
                                plot_height=3,
                                ppi=122,
                                borders=0,
                                save_as=None,
                                save_format=None,
                                save_scale=1,
                                colormap="bw"):

            """
            Create interactive plots of reaction paths in geochemical variable
            space.

            Parameters
            ----------
            xyb : list of three str, default None
                By default, this function will plot reaction paths in all possible
                dimensions.

                Optionally, if you want to produce only a specific plot,
                you can provide a list containing the basis species to be used for
                the x-axis and y-axis, followed by the basis species used for
                balance. For example, ["Fe+2", "Fe+3", "Mg+2"] will have the log
                activity of Fe+2 on the x-axis, the log activity of Fe+3 on the
                y-axis, and will be balanced on Mg+2.

            path_margin : float, default 0.25
                Controls the spacing between the reaction path and the plot axes.
                Increasing this value increases the spacing.

            flip_xy : bool, default False
                Transpose the plot so the x and y variables switch axes?

            show_annotation : bool, default True
                Show annotation in the bottom left of the figure? The annotation
                includes the species used for balance, the temperature, and
                the pressure.

            annotation_coords : list, default [0,0]
                List of two numeric values representing the X and Y coordinates of
                the annotation, where 0,0 is the bottom left, 0.5,0 is the bottom
                center, 1,0 is the bottom right, 1,1 is the top right, and so on.
                The annotation includes the species used for balance, the
                temperature and the pressure.

            show_nonparticipating_mineral_lines : bool, default False
                Depict lines for minerals even if those minerals do not participate
                in the reaction? This does not affect the mineral field of the
                diagram, only the mineral planes denoted as lines.

            path_line_type : str, default "markers+lines"
                Reaction path line type. Can be either "markers+lines", "lines", or
                "markers".

            path_line_color, str, default "black"
                Color of reaction path line.

            path_point_fill_color : str, default "black"
                Fill color of non-projected points along the reaction path. The
                fill color of projected points is handled by
                `projected_point_fill_color`.

            path_point_line_color : str, default "black"
                Color of the outlines of non-projected points along the reaction
                path. The outline color of projected points is handled by
                `projected_point_line_color`.

            projected_point_fill_color : str, default "white"
                Fill color of projected points along the reaction path. The
                fill color of non-projected points is handled by
                `path_point_fill_color`.

            projected_point_line_color : str, default "black"
                Color of the outlines of projected points along the reaction path.
                The outline color of non-projected points is handled by
                `path_point_line_color`.

            h_line_color : str, default "black"
                Color of horizontal lines representing minerals.

            v_line_color : str, default "black"
                Color of vertical lines representing minerals.

            d_line_color : str, default "black"
                Color of diagonal lines representing minerals.

            res : int, default 300
                Resolution, or number of calculations along each axis, for mineral
                stability fields. A lower number will be faster but will make appear
                boundaries blockier. A higher number takes longer to calculate, but
                will result in smoother boundaries.

            plot_width, plot_height : numeric, default 4 by 3
                Width and height of the plot, in inches.

            ppi : numeric, default 122
                Pixels per inch. Along with `plot_width` and `plot_height`,
                determines the size of interactive plots.

            borders : float, default 0
                Thickness of black lines forming boundaries between mineral
                stability regions. No lines appear if equal to 0.

            save_as : str, optional
                Provide a filename to save this figure. Filetype of saved figure is
                determined by `save_format`.
                Note: interactive plots can be saved by clicking the 'Download plot'
                button in the plot's toolbar.

            save_format : str, default "png"
                Desired format of saved or downloaded figure. Can be 'png', 'jpg',
                'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html',
                an interactive plot will be saved. Only 'png', 'svg', 'jpeg',
                and 'webp' can be downloaded with the 'download as' button in the
                toolbar of an interactive plot.

            save_scale : numeric, default 1
                Multiply title/legend/axis/canvas sizes by this factor when saving
                the figure.

            colormap : str, default "bw"
                Name of the colormap to color the scatterpoints. Accepts "bw"
                or names of matplotlib colormaps. If set to "bw", the plot will be
                set to black and white, except for the reaction path line itself.
                The colors of the reaction path line and its points are controlled
                by `path_line_color`, `path_point_fill_color`,
                `path_point_line_color`, `projected_point_fill_color`,
                and `projected_point_line_color`.

            Returns
            -------
            fig_list : a list of Plotly figure objects
                A list of interactive Plotly figures. If xyb equals None
                (the default), then the list will contain figures representing all
                combinations of geochemical variables. Optionally, if xyb is
                specified, fig_list will only contain the single figure of interest.
            """

            error_messages = []

            # check that there is only one temperature and pressure
            if not self.__is_all_same_value(self.misc_params["Temp(C)"]):
                error_messages.append("Reaction paths cannot be plotted when temperature changes with reaction progress.")
            if not self.__is_all_same_value(self.misc_params["Press(bars)"]):
                error_messages.append("Reaction paths cannot be plotted when pressure changes with reaction progress.")

            if isinstance(xyb, list):
                if len(xyb) != 3:
                    error_messages.append(("Error in xyb={}".format(xyb)+". "
                            "The xyb parameter must either be None or a list of "
                            "three basis species to serve as x, y, and balance variables."))

            if not isinstance(self.df, pd.DataFrame):
                error_messages.append(("The plot_reaction_paths() function requires "
                        "a thermodynamic database in a WORM-style CSV format, e.g., "
                        "'wrm_data.csv'. You may be getting this message because "
                        "a data0 or data1 file was used."))

            if len(error_messages)>0:
                self.err_handler.raise_exception("\n".join(error_messages))

            self.T = float(self.misc_params["Temp(C)"][0])
            self.P = float(self.misc_params["Press(bars)"][0])
            self.IS = float(self.misc_params["Ionic strength (molal)"][0])
            self.path_margin = path_margin

            minerals_formed = [m for m in self.moles_minerals.columns if m != "Xi"]

            all_elements_of_interest = []
            for mineral in minerals_formed:

                if mineral not in list(self.df["name"]):
                    if self.verbose > 0:
                        print("The mineral", mineral, "cannot be represented in a",
                               "reaction path diagram, likely because it is missing",
                               "a Gibbs free energy value in the thermodynamic",
                               "database. Continuing anyway, but be aware that",
                               "this mineral will not be represented in diagrams.")
                    continue

                all_elements_of_interest += self.__get_elem_ox_of_interest_in_minerals(mineral)
            all_elements_of_interest_pre = list(set(all_elements_of_interest))

            # filter out elements like Fe+0, which has no aqueous species representative
            # for which to create an axis.
            bad_elem = []
            for elem in all_elements_of_interest_pre:
                if len(list(self.thermo.csv_db.loc[self.thermo.csv_db['name'] == self.__get_basis_from_elem(elem), 'state'])) == 0:
                    bad_elem.append(elem)
                elif list(self.thermo.csv_db.loc[self.thermo.csv_db['name'] == self.__get_basis_from_elem(elem), 'state'])[0] != 'aq':
                    bad_elem.append(elem)

            all_elements_of_interest = [elem for elem in all_elements_of_interest_pre if elem not in bad_elem]

            all_elements_of_interest = sorted(all_elements_of_interest)

            self.all_elements_of_interest = all_elements_of_interest

            fig_list = []

            # if there are only 2 elements of interest, these become the axes, and there is no
            # need to fuss with real vs projected points.
            if len(all_elements_of_interest) == 2:
                fig, _ , _ = self.__plot_reaction_path_main(
                                                    triad = all_elements_of_interest,
                                                    T=self.T, P=self.P, IS=self.IS,
                                                    path_margin=self.path_margin,
                                                    flip_xy=flip_xy,
                                                    show_annotation=show_annotation,
                                                    annotation_coords=annotation_coords,
                                                    show_nonparticipating_mineral_lines=show_nonparticipating_mineral_lines,
                                                    minerals_to_show=minerals_to_show,
                                                    path_line_type=path_line_type,
                                                    path_line_color=path_line_color,
                                                    path_point_fill_color=path_point_fill_color,
                                                    path_point_line_color=path_point_line_color,
                                                    projected_point_fill_color=projected_point_fill_color,
                                                    projected_point_line_color=projected_point_line_color,
                                                    h_line_color=h_line_color,
                                                    v_line_color=v_line_color,
                                                    d_line_color=d_line_color,
                                                    res=res,
                                                    plot_width=plot_width,
                                                    plot_height=plot_height,
                                                    ppi=ppi,
                                                    colormap=colormap,
                                                    borders=borders,
                                                    projected_points=["real"]*self.moles_product_minerals.shape[0],
                                                    first_pass=False)

                fig_list = [fig]

            elif len(all_elements_of_interest) >= 3:

                # get a list of elem pairs for plotting
                alist = self.all_elements_of_interest
                element_plot_pairs = []
                for result in itertools.combinations(alist, 2):
                    element_plot_pairs.append(list(result))

                element_plot_triad = []
                for pair in element_plot_pairs:
                    elem_not_in_pair = [e for e in self.all_elements_of_interest if e not in pair]
                    for e in elem_not_in_pair:
                        triad_to_append = pair + [e]
                        element_plot_triad.append(triad_to_append)

                if colormap == "bw":
                    if borders == 0:
                        borders = 1
                    colormap = "none"
                    h_line_color = "black"
                    v_line_color = "black"
                    d_line_color = "black"

                fig_list = []
                pred_minerals_from_fields_list = []
                pred_minerals_from_lines_list = []

                if isinstance(xyb, list):
                    try:
                        xyb_element_plot_triad = [[self.__get_elem_ox_of_interest_in_minerals(v)[0] for v in xyb]]
                    except:
                        err = ("Plot axes cannot accomodate desired variables. "
                               "Available variables include {}".format([self.__get_basis_from_elem(elem) for elem in alist]))
                        self.err_handler.raise_exception(err)
                    xyb_i = None
                    # get index of triad that matches xyb:
                    for i,triad in enumerate(element_plot_triad):
                        if set(xyb_element_plot_triad[0][0:2]) == set(triad[0:2]) and xyb_element_plot_triad[0][2] == triad[2]:
                            xyb_i = i
                    if xyb_i == None:
                        err = ("Plot axes cannot accomodate desired variables. "
                               "Available variables include {}".format([self.__get_basis_from_elem(elem) for elem in alist]))
                        self.err_handler.raise_exception(err)

                if not calculate_projected_points or path_line_type=="lines":
                    projected_points = ["real"]*self.moles_product_minerals.shape[0]
                    fig_list_projected_points = [projected_points]*len(element_plot_triad)

                else:

                    if len(element_plot_triad) > 20:
                        if self.verbose > 0:
                            print("Warning! There are {}".format(len(element_plot_triad)),
                                  "different combinations of variables that must be considered",
                                  "in order to plot markers.")
                            print("This might take a very long time or may not finish calculating at all.")
                            print("We recommend setting calculate_projected_points=False in",
                                  "plot_reaction_paths() and then restarting the",
                                  "calculation to avoid lengthy calculation times.")

                    for triad in element_plot_triad:

                        # do a quick first pass at making figures to see which points are projections.
                        fig, pred_minerals_from_fields, pred_minerals_from_lines = self.__plot_reaction_path_main(
                                                            triad, T=self.T, P=self.P, IS=self.IS,
                                                            show_nonparticipating_mineral_lines=False, # no need for this in first pass
                                                            minerals_to_show=[], # no need for this in first pass
                                                            path_margin=self.path_margin,
                                                            flip_xy=flip_xy,
                                                            first_pass=True, # flag for skipping certain calculations/plotting
                                                            res=1) # low res first pass

                        if pred_minerals_from_fields == None:
                            pred_minerals_from_fields=[None]*self.moles_product_minerals.shape[0]

                        fig_list.append(fig)
                        pred_minerals_from_fields_list.append(pred_minerals_from_fields)
                        pred_minerals_from_lines_list.append(pred_minerals_from_lines)

                    # determine which line segments in the reaction path are projections
                    # and which are actually in the plane of the diagram.
                    # This is the "first pass"
                    fig_list_projected_points = []
                    for i,triad in enumerate(element_plot_triad):

                        projected_points = ["projection"]*self.moles_product_minerals.shape[0]
                        for irow in range(0, self.moles_product_minerals.shape[0]):

                            # get names of minerals formed at this xi
                            xirow = list(self.moles_product_minerals.iloc[irow])
                            formed_minerals = [self.moles_product_minerals.columns[1:][ii] for
                                               ii,mineral in enumerate(xirow[1:]) if
                                               mineral>0]

                            available_pred_minerals_from_fields = [l[irow] for l in pred_minerals_from_fields_list]

                            for mineral in formed_minerals:

                                if mineral == pred_minerals_from_fields_list[i][irow]:
                                    # if this mineral is in pred_minerals_from_fields_list,
                                    # then it is NOT a projection.
                                    projected_points[irow] = "real"
                                if mineral in available_pred_minerals_from_fields and mineral in pred_minerals_from_lines_list[i]:
                                    # if this mineral is in the irowth location of any of the
                                    # lists in pred_minerals_from_lines_list, it is NOT a
                                    # projection.
                                    projected_points[irow] = "real"

                        fig_list_projected_points.append(projected_points)

                if isinstance(xyb, list):
                    # if xyb is defined, make element_plot_triad have a length of 1
                    element_plot_triad = xyb_element_plot_triad
                    # give the list of projected points lists a length of 1
                    fig_list_projected_points = [fig_list_projected_points[xyb_i]]

                fig_list = []
                for i,triad in enumerate(element_plot_triad):
                    # re-run figure generation, passing in a list of which points are projected.
                    fig, _ , _ = self.__plot_reaction_path_main(
                                        triad, T=self.T, P=self.P, IS=self.IS,
                                        path_margin=self.path_margin,
                                        flip_xy=flip_xy,
                                        show_annotation=show_annotation,
                                        annotation_coords=annotation_coords,
                                        show_nonparticipating_mineral_lines=show_nonparticipating_mineral_lines,
                                        minerals_to_show=minerals_to_show,
                                        path_line_type=path_line_type,
                                        path_line_color=path_line_color,
                                        path_point_fill_color=path_point_fill_color,
                                        path_point_line_color=path_point_line_color,
                                        projected_point_fill_color=projected_point_fill_color,
                                        projected_point_line_color=projected_point_line_color,
                                        h_line_color=h_line_color,
                                        v_line_color=v_line_color,
                                        d_line_color=d_line_color,
                                        res=res,
                                        plot_width=plot_width,
                                        plot_height=plot_height,
                                        ppi=ppi,
                                        colormap=colormap,
                                        borders=borders,
                                        projected_points=fig_list_projected_points[i],
                                        first_pass=False)

                    fig_list.append(fig)

            if not fig_list and self.verbose > 0:
                print("Warning: a reaction path plot could not be generated for this system.")

            if isinstance(save_as, str):
                dummy_sp = Speciation({})
                for i,fig in enumerate(fig_list):

                    if isinstance(xyb, list):
                        name_append = ""
                    else:
                        name_append = "_{}".format(i+1)
                    _, _ = dummy_sp._save_figure(fig,
                            save_as+name_append, save_format, save_scale,
                            plot_width, plot_height, ppi)

            return fig_list

    Create interactive plots of reaction paths in geochemical variable space.

    ## Parameters

    `xyb` :`list` of `three str`, default `None`

      By default, this function will plot reaction paths in all possible dimensions.

      Optionally, if you want to produce only a specific plot, you can provide a list containing the basis species to be used for the x-axis and y-axis, followed by the basis species used for balance. For example, ["Fe+2", "Fe+3", "Mg+2"] will have the log activity of Fe+2 on the x-axis, the log activity of Fe+3 on the y-axis, and will be balanced on Mg+2.

    `path_margin` :`float`, default `0.25`
      Controls the spacing between the reaction path and the plot axes. Increasing this value increases the spacing.
    `flip_xy` :`bool`, default `False`
      Transpose the plot so the x and y variables switch axes?
    `show_annotation` :`bool`, default `True`
      Show annotation in the bottom left of the figure? The annotation includes the species used for balance, the temperature, and the pressure.
    `annotation_coords` :`list`, default `[0,0]`
      List of two numeric values representing the X and Y coordinates of the annotation, where 0,0 is the bottom left, 0.5,0 is the bottom center, 1,0 is the bottom right, 1,1 is the top right, and so on. The annotation includes the species used for balance, the temperature and the pressure.
    `show_nonparticipating_mineral_lines` :`bool`, default `False`
      Depict lines for minerals even if those minerals do not participate in the reaction? This does not affect the mineral field of the diagram, only the mineral planes denoted as lines.
    `path_line_type` :`str`, default `"markers+lines"`
      Reaction path line type. Can be either "markers+lines", "lines", or "markers".

    path_line_color, str, default "black" Color of reaction path line.

    `path_point_fill_color` :`str`, default `"black"`
      Fill color of non-projected points along the reaction path. The fill color of projected points is handled by `projected_point_fill_color`.
    `path_point_line_color` :`str`, default `"black"`
      Color of the outlines of non-projected points along the reaction path. The outline color of projected points is handled by `projected_point_line_color`.
    `projected_point_fill_color` :`str`, default `"white"`
      Fill color of projected points along the reaction path. The fill color of non-projected points is handled by `path_point_fill_color`.
    `projected_point_line_color` :`str`, default `"black"`
      Color of the outlines of projected points along the reaction path. The outline color of non-projected points is handled by `path_point_line_color`.
    `h_line_color` :`str`, default `"black"`
      Color of horizontal lines representing minerals.
    `v_line_color` :`str`, default `"black"`
      Color of vertical lines representing minerals.
    `d_line_color` :`str`, default `"black"`
      Color of diagonal lines representing minerals.
    `res` :`int`, default `300`
      Resolution, or number of calculations along each axis, for mineral stability fields. A lower number will be faster but will make appear boundaries blockier. A higher number takes longer to calculate, but will result in smoother boundaries.
    `plot_width`, `plot_height` :`numeric`, default `4 by 3`
      Width and height of the plot, in inches.
    `ppi` :`numeric`, default `122`
      Pixels per inch. Along with `plot_width` and `plot_height`, determines the size of interactive plots.
    `borders` :`float`, default `0`
      Thickness of black lines forming boundaries between mineral stability regions. No lines appear if equal to 0.
    `save_as` :`str`, optional
      Provide a filename to save this figure. Filetype of saved figure is determined by `save_format`. Note: interactive plots can be saved by clicking the 'Download plot' button in the plot's toolbar.
    `save_format` :`str`, default `"png"`
      Desired format of saved or downloaded figure. Can be 'png', 'jpg', 'jpeg', 'webp', 'svg', 'pdf', 'eps', 'json', or 'html'. If 'html', an interactive plot will be saved. Only 'png', 'svg', 'jpeg', and 'webp' can be downloaded with the 'download as' button in the toolbar of an interactive plot.
    `save_scale` :`numeric`, default `1`
      Multiply title/legend/axis/canvas sizes by this factor when saving the figure.
    `colormap` :`str`, default `"bw"`
      Name of the colormap to color the scatterpoints. Accepts "bw" or names of matplotlib colormaps. If set to "bw", the plot will be set to black and white, except for the reaction path line itself. The colors of the reaction path line and its points are controlled by `path_line_color`, `path_point_fill_color`, `path_point_line_color`, `projected_point_fill_color`, and `projected_point_line_color`.

    ## Returns

    `fig_list` :`a list` of `Plotly figure objects`
      A list of interactive Plotly figures. If xyb equals None (the default), then the list will contain figures representing all combinations of geochemical variables. Optionally, if xyb is specified, fig_list will only contain the single figure of interest.
  `def print_tabs(self)`
    Expand source code

        def print_tabs(self):
            """
            Print the names of tables contained in a tab file processed by the
            the Mass_Transfer class.
            """
            if self.tab != None:
                [print(key) for key in self.tab.keys()]
            else:
                print("A processed TAB file is not associated with this sample.")

    Print the names of tables contained in a tab file processed by the the Mass_Transfer class.

`class Mixing_Fluid (speciation,
sample_name,
amount_remaining=1,
amount_destroyed=0,
molar_volume=1,
mass_ratio=1,
hide_traceback=True)`
  Expand source code

      class Mixing_Fluid:
          def __init__(self,
                       speciation,
                       sample_name,
                       amount_remaining=1,
                       amount_destroyed=0,
                       molar_volume=1,
                       mass_ratio=1,
                       hide_traceback=True,
                      ):

              """
              Class used to define the fluid to be mixed with other fluids in
              `Prepare_Reaction`.

              Parameters
              ----------
              speciation : object of class Speciation
                  The speciation object containing the fluid to be mixed.

              sample_name : str
                  The name of the fluid sample that will be mixed with all other
                  speciated fluids.

              amount_remaining : float, default 1
                  Number of moles of the fluid to be mixed with all others.

              amount_destroyed : float, default 0
                  Number of moles of the mixing fluid that has been destroyed.

              molar_volume : float, default 1
                  Molar volume of the mixing fluid, in moles/cm3.

              mass_ratio : float, default 1
                  Ratio of mass of the mixing fluid to all other fluids.

              hide_traceback : bool, default True
                  Hide traceback message when encountering errors handled by this
                  class? When True, error messages handled by this class will be short
                  and to the point.

              """

              self.err_handler = Error_Handler(clean=hide_traceback)

              # Prepare a special reactant to be used in a mixing calculation.
              if isinstance(speciation, Speciation):

                  self.sample_name = sample_name

                  if not sample_name in speciation.sample_data.keys():

                      self.err_handler.raise_exception(("The sample '"+str(sample_name)+"'"
                              " was not found amongst the samples in this speciation"
                              " calculation: "+str(list(speciation.sample_data.keys()))))

                  self.speciation_sample_data = speciation.sample_data[sample_name]
                  self.T = self.speciation_sample_data["temperature"]
                  self.mass_ratio = mass_ratio

                  elemental_composition_lines = []
                  capture = False
                  for i,line in enumerate(speciation.raw_3_pickup_dict_top[sample_name]):
                      if "|->|Composition" in line:
                          capture = True
                          i_start = i
                      if "|->|Reaction" in line:
                          capture = False
                      if capture and i > i_start + 3:
                          elemental_composition_lines.append(line)

                  # ignore last line, which is a divider "|----------..."
                  elemental_composition_lines = elemental_composition_lines[:-1]

                  fluid_2_dict = {}
                  for line in elemental_composition_lines:
                      split_line = line.split("|")
                      element = split_line[2].strip()
                      value = float(split_line[3])
                      fluid_2_dict[element] = value

                  self.reactant = Reactant(reactant_name="Fluid 2",
                                           reactant_type="Special reactant",
                                           special_reactant_dict=fluid_2_dict,
                                           amount_remaining=amount_remaining,
                                           amount_destroyed=amount_destroyed,
                                           molar_volume=molar_volume,
                                           hide_traceback=hide_traceback)

                  self.formatted_block = self.reactant.formatted_block
                  self.reactant_type = "Special reactant"

              else:
                  self.err_handler.raise_exception(("The speciation parameter was"
                          " not given a Speciation object. A Speciation object is"
                          " produced by the AqEquil.speciate() function."))

              # handle fluid mixing reaction block

              lines_to_keep = self.formatted_block.split("\n")
              raw_p_dict_top = speciation.raw_3_pickup_dict_top

              # grab this fluid's reaction block
              reaction_block_lines = []
              capture = False
              for line in raw_p_dict_top[sample_name]:
                  if "|->|Reaction" in line:
                      capture = True
                  if "|->|Surface area" in line:
                      capture = False
                  if capture:
                      reaction_block_lines.append(line)

              # insert this fluid's reaction block
              before_lines = []
              after_lines = []
              is_before = True
              for i,line in enumerate(lines_to_keep):
                  if "|->|Reaction" in line:
                      is_before=False
                  if is_before:
                      before_lines.append(line)
                  else:
                      after_lines.append(line)

              # trim redundant lines
              after_lines = after_lines[5:]

              lines_to_keep = before_lines + reaction_block_lines + after_lines

              self.formatted_block = "\n".join(lines_to_keep)

  Class used to define the fluid to be mixed with other fluids in `Prepare_Reaction`.

  ## Parameters

  `speciation` :`object` of `class Speciation`
    The speciation object containing the fluid to be mixed.
  `sample_name` :`str`
    The name of the fluid sample that will be mixed with all other speciated fluids.
  `amount_remaining` :`float`, default `1`
    Number of moles of the fluid to be mixed with all others.
  `amount_destroyed` :`float`, default `0`
    Number of moles of the mixing fluid that has been destroyed.
  `molar_volume` :`float`, default `1`
    Molar volume of the mixing fluid, in moles/cm3.
  `mass_ratio` :`float`, default `1`
    Ratio of mass of the mixing fluid to all other fluids.
  `hide_traceback` :`bool`, default `True`
    Hide traceback message when encountering errors handled by this class? When True, error messages handled by this class will be short and to the point.
`class Prepare_Reaction (reactants,
gases=[],
t_option=None,
t_value_1=None,
t_value_2=None,
t_value_3=None,
p_option=0,
p_value_1=None,
p_value_2=0,
xi_range=[0, 1],
time_range=[0, 1e+38],
pH_range=[-1e+38, 1e+38],
Eh_range=[-1e+38, 1e+38],
fO2_range=[-1e+38, 1e+38],
aw_range=[-1e+38, 1e+38],
max_n_steps=900,
xi_print_int=1,
log_xi_print_int=1,
time_print_int=1e+38,
log_time_print_int=1e+38,
pH_print_interval=1e+38,
Eh_print_interval=1e+38,
logfO2_print_interval=1e+38,
aw_print_interval=1e+38,
n_steps_print_interval=100,
physical_system_model='closed',
kinetic_mode='arbitrary',
phase_boundary_search=0,
permit_solid_solutions=False,
clear_es_solids_read=False,
clear_es_solids_initial=False,
clear_es_solids_end=False,
clear_prs_solids_read=False,
clear_prs_solids_end=False,
auto_basis_switching_pre_NR=False,
auto_basis_switching_post_NR=False,
calc_mode_selection=0,
ODE_corrector_mode=0,
mineral_suppression_option='None',
write_tab=-1,
fluid_mixing_setup=False,
max_finite_difference_order=6,
beta_convergence_tolerance=0,
del_convergence_tolerance=0,
max_n_NR_iter=500,
search_find_convergeance_tolerance=0,
saturation_tolerance=0,
max_n_phase_assemblage_tries=0,
zero_order_step_size=0,
max_interval_in_xi_between_PRS_transfers=0,
filename=None,
hide_traceback=True)`
  Expand source code

      class Prepare_Reaction:
          def __init__(self,
                       reactants,
                       gases=[],
                       t_option=None,
                       t_value_1=None, # temp
                       t_value_2=None,  # temp or deriv
                       t_value_3=None,  # mass ratio factor
                       p_option=0,
                       p_value_1=None,  # pressure
                       p_value_2=0,  # deriv
                       xi_range=[0, 1],
                       time_range=[0, 1e38],
                       pH_range=[-1e38, 1e38],
                       Eh_range=[-1e38, 1e38],
                       fO2_range=[-1e38, 1e38],
                       aw_range=[-1e38, 1e38],
                       max_n_steps=900,
                       xi_print_int=1,
                       log_xi_print_int=1,
                       time_print_int=1e38,
                       log_time_print_int=1e38,
                       pH_print_interval=1e38,
                       Eh_print_interval=1e38,
                       logfO2_print_interval=1e38,
                       aw_print_interval=1e38,
                       n_steps_print_interval=100,
                       physical_system_model="closed",
                       kinetic_mode="arbitrary",
                       phase_boundary_search=0,
                       permit_solid_solutions=False,
                       clear_es_solids_read=False,
                       clear_es_solids_initial=False,
                       clear_es_solids_end=False,
                       clear_prs_solids_read=False,
                       clear_prs_solids_end=False,
                       auto_basis_switching_pre_NR=False,
                       auto_basis_switching_post_NR=False,
                       calc_mode_selection=0,
                       ODE_corrector_mode=0,
                       mineral_suppression_option="None",
                       write_tab=-1, # do not write a TAB file by default because EQ6 can encounter an access violation when writing a TAB file
                       fluid_mixing_setup=False,
                       max_finite_difference_order=6,
                       beta_convergence_tolerance=0,
                       del_convergence_tolerance=0,
                       max_n_NR_iter=500,
                       search_find_convergeance_tolerance=0,
                       saturation_tolerance=0,
                       max_n_phase_assemblage_tries=0,
                       zero_order_step_size=0,
                       max_interval_in_xi_between_PRS_transfers=0,
                       filename=None,
                       hide_traceback=True,
                      ):

              """
              Class used to set the parameters of a reaction between the results of a
              speciation calculation and minerals, gases, etc.

              Parameters
              ----------
              reactants : list
                  List of reactants defined by the 'Reactant' class. Can be an empty
                  list if no reactants are desired.

              gases : list, default []
                  List of gases defined by the 'Gas' class. Can be an empty
                  list if no gases are desired.

              t_option : int, default 0
                  Desired option for handling temperature of the reaction. Valid
                  choices include:

                  - 0 for constant temperature
                  - 1 for linear tracking in Xi
                  - 2 for linear tracking in time

                  Fluid mixing tracking is not yet supported.

              t_value_1, t_value_2, t_value_3 : default None, 0, 0, respectively
                  By default, the temperature of samples in the speciation
                  calculation will be used, so the user does not need to specify
                  temperature values here. However, if a user wishes, temperature
                  values can be defined here that will be applied to all samples in
                  the speciation. Note that doing so may result in incongruous
                  results. That said, the values specified here depend on which
                  option is selected for `t_option`:

                  - If 't_option' is 0, then t_value_1 is the value of the constant
                  temperature (in degrees C), and t_value_2 and t_value_3 are ignored.
                  - If 't_option' is 1, then t_value_1 is the base value
                  temperature (in degrees C), t_value_2 is the derivative, and
                  t_value_3 is ignored.
                  - If 't_option' is 2, then t_value_1 is the base value
                  temperature (in degrees C), t_value_2 is the derivative, and
                  t_value_3 is ignored.

              p_option : int, default 0
                  Desired option for handling pressure of the reaction. Valid
                  choices include:

                  - 0 to follow the data file reference pressure curve
                  - 1 to follow the 1.013-bar/steam-saturation curve
                  - 2 for constant pressure
                  - 3 for linear tracking in Xi
                  - 4 for linear tracking in time

              p_value_1, p_value_2 : float default None, 0
                  Values assigned to desired `p_option`.

                  - If `p_option` is 0 or 1, p_value_1 and p_value_2 are ignored.
                  - If `p_option` is 2, p_value_1 represents a constant pressure,
                  in bars, and p_value_2 is ignored.
                  - If `p_option` is 3 or 4, p_value_1 represents the base pressure
                  value, in bars, and p_value_2 is the derivative.

              xi_range : list of two float, default [0, 1]
                  A list containing the starting and maximum value of Xi,
                  respectively.

              time_range : list of two float, default [0, 1e38]
                  A list containing the starting and maximum time, respectively.

              pH_range : list of two float, default [-1e38, 1e38]
                  A list containing the minimum and maximum values of pH.

              Eh_range : list of two float, default [-1e38, 1e38]
                  A list containing the minimum and maximum values of Eh.

              fO2_range : list of two float, default [-1e38, 1e38]
                  A list containing the minimum and maximum values of the fugacity
                  of oxygen, fO2.

              aw_range : list of two float, default [-1e38, 1e38]
                  A list containing the minimum and maximum values of water activity.

              max_n_steps : int, default 900
                  Maximum number of steps of Xi allowed.

              xi_print_int : int, default 1
                  Xi print interval.

              log_xi_print_int : int, default 1
                  Log Xi print interval.

              time_print_int : int, default 1e38
                  Time print interval.

              log_time_print_int : int, default 1e38
                  Log time print interval.

              pH_print_interval : int, default 1e38
                  pH print interval.

              Eh_print_interval : int, default 1e38
                  Eh (v) print interval.

              logfO2_print_interval : int, default 1e38
                  Log fO2 print interval.

              aw_print_interval : int, default 1e38
                  Activity of water (aw) print interval.

              n_steps_print_interval : int, default 100
                  Steps print interval.

              physical_system_model : str, default "closed"
                  Selection for the physical system model. Valid options include:

                  - "closed"
                  - "titration"
                  - "fluid-centered flow-through open"

              kinetic_mode : str, default "arbitrary"
                  Selection for kinetic mode. Valid options include:
                  - "arbitrary" for arbitrary kinetics, reaction progress mode
                  - "true" for true kinetics, reaction progress/time mode

              phase_boundary_search : int, default 0
                  Selection for phase boundary searches. Valid options include:

                  - 0 to search for phase boundaries and constrain the step size to
                  match.
                  - 1 to search for phase boundaries and print their locations.
                  - 2 to not search for phase boundaries.

              permit_solid_solutions : bool, default False
                  Permit solid solutions? If False, solid solutions are ignored.

              clear_es_solids_read : bool, default False
                  Clear the ES solids read from the input file?

              clear_es_solids_initial : bool, default False
                  Clear the ES solids at the initial value of reaction progress?

              clear_es_solids_end : bool, default False
                  Clear the ES solids at the end of the run?

              clear_prs_solids_read : bool, default False
                  Clear the PRS solids read from the input file?

              clear_prs_solids_end : bool, default False
                  Clear the PRS solids at the end of the run? If True, PRS solids will
                  be cleared unless numerical problems cause early termination.

              auto_basis_switching_pre_NR : bool, default False
                  Turn on auto basis switching in pre-Newton-Raphson optimization?

              auto_basis_switching_post_NR : bool, default False
                  Turn on auto basis switching after Newton-Raphson iteration?

              calc_mode_selection : int, default 0
                  Calculational mode selection. Valid options include:

                  - 0 for normal path tracing
                  - 1 for economy mode (if permissible)
                  - 2 for super economy mode (if permissible)

              ODE_corrector_mode : int, default 0
                  ODE integrator corrector mode selection. Valid options include:

                  - 0 to allow stiff and simple correctors
                  - 1 to allow only simple corrector
                  - 2 to allow only stiff corrector
                  - 3 to allow no correctors

              mineral_suppression_option : str, default "None"
                  Option to suppress formation of minerals. Can be either "None" (no
                  minerals are suppressed) or "All" (all minerals are suppressed).

              write_tab : int, default -1
                  Option to write a TAB file. Valid options include:

                  - -1 do not write a TAB file (default)
                  - 0 write a TAB file
                  - 1 write a TAB file prepending TABX file data from a previous run

              fluid_mixing_setup : bool, default False
                  If True, will write an EQ6 input file with Fluid 1 set up for
                  fluid mixing. If False, a normal EQ6 pickup file will be written.

              max_finite_difference_order : int, default 6
                  Maximum finite-difference order (numerical parameter).

              beta_convergence_tolerance : float, default 0
                  Beta convergence tolerance (numerical parameter).

              del_convergence_tolerance : float, default 0
                  Delta convergence tolerance (numerical parameter).

              max_n_NR_iter : int, default 500
                  Maximum number of N-R iterations (numerical parameter).

              search_find_convergeance_tolerance : float, default 0
                  Search/find convergence tolerance (numerical parameter).

              saturation_tolerance : float, default 0
                  Saturation tolerance (numerical parameter).

              max_n_phase_assemblage_tries : int, default 0
                  Maximum number of phase assemblage tries (numerical parameter).

              zero_order_step_size : int, default 0
                  Zero order step size in Xi (numerical parameter).

              max_interval_in_xi_between_PRS_transfers : int, default 0
                  Maximum interval in Xi between PRS transfers (numerical parameter).

              filename : str, default None
                  Filename where the results of `Prepare_Reaction` will be written.
                  This is equivalent to the top half of an EQ3/6 6i file. If None,
                  no file will be written.

              hide_traceback : bool, default True
                  Hide traceback message when encountering errors handled by this
                  class? When True, error messages handled by this class will be short
                  and to the point.

              """

              self.err_handler = Error_Handler(clean=hide_traceback)

              if len(reactants) == 1 and not isinstance(reactants, list):
                  reactants = list(reactants)

              if len(gases) == 0:
                  gases = [Gas()]
              elif len(gases) == 1 and not isinstance(gases, list):
                  gases = list(gases)

              self.reactants=reactants
              self.gases=gases
              self.t_option=t_option
              self.t_value_1=t_value_1
              self.t_value_2=t_value_2
              self.t_value_3=t_value_3
              self.p_option=p_option
              self.p_value_1=p_value_1
              self.p_value_2=p_value_2
              self.start_xi=xi_range[0]
              self.max_xi=xi_range[1]
              self.start_time=time_range[0]
              self.max_time=time_range[1]
              self.min_pH=pH_range[0]
              self.max_pH=pH_range[1]
              self.min_Eh=Eh_range[0]
              self.max_Eh=Eh_range[1]
              self.min_fO2=fO2_range[0]
              self.max_fO2=fO2_range[1]
              self.min_aw=aw_range[0]
              self.max_aw=aw_range[1]
              self.max_n_steps=max_n_steps
              self.xi_print_int=xi_print_int
              self.log_xi_print_int=log_xi_print_int
              self.time_print_int=time_print_int
              self.log_time_print_int=log_time_print_int
              self.pH_print_interval=pH_print_interval
              self.Eh_print_interval=Eh_print_interval
              self.logfO2_print_interval=logfO2_print_interval
              self.aw_print_interval=aw_print_interval
              self.n_steps_print_interval=n_steps_print_interval
              self.max_finite_difference_order=max_finite_difference_order
              self.beta_convergence_tolerance=beta_convergence_tolerance
              self.del_convergence_tolerance=del_convergence_tolerance
              self.max_n_NR_iter=max_n_NR_iter
              self.search_find_convergeance_tolerance=search_find_convergeance_tolerance
              self.saturation_tolerance=saturation_tolerance
              self.max_n_phase_assemblage_tries=max_n_phase_assemblage_tries
              self.zero_order_step_size=zero_order_step_size
              self.max_interval_in_xi_between_PRS_transfers=max_interval_in_xi_between_PRS_transfers
              self.mineral_suppression_option=mineral_suppression_option

              self.t_checkbox_1=" "
              self.t_checkbox_2=" "
              self.t_checkbox_3=" "
              self.t_checkbox_4=" "
              self.tval1=0
              self.tval2=0
              self.tval3=0
              self.tval4=0
              self.tval5=0
              self.tval6=0
              self.tval7=0
              self.tval8=0
              self.p_checkbox_1=" "
              self.p_checkbox_2=" "
              self.p_checkbox_3=" "
              self.p_checkbox_4=" "
              self.p_checkbox_5=" "
              self.pval1=0
              self.pval2=0
              self.pval3=0
              self.pval4=0
              self.pval5=0
              self.i1_checkbox_1 = " "
              self.i1_checkbox_2 = " "
              self.i1_checkbox_3 = " "
              self.i2_checkbox_1 = " "
              self.i2_checkbox_2 = " "
              self.i3_checkbox_1 = " "
              self.i3_checkbox_2 = " "
              self.i3_checkbox_3 = " "
              self.i4_checkbox_1 = " "
              self.i4_checkbox_2 = " "
              self.i5_checkbox_1 = " "
              self.i5_checkbox_2 = " "
              self.i6_checkbox_1 = " "
              self.i6_checkbox_2 = " "
              self.i7_checkbox_1 = " "
              self.i7_checkbox_2 = " "
              self.i9_checkbox_1 = " "
              self.i9_checkbox_2 = " "
              self.i10_checkbox_1 = " "
              self.i10_checkbox_2 = " "
              self.i11_checkbox_1 = " "
              self.i11_checkbox_2 = " "
              self.i12_checkbox_1 = " "
              self.i12_checkbox_2 = " "
              self.i13_checkbox_1 = " "
              self.i13_checkbox_2 = " "
              self.i13_checkbox_3 = " "
              self.i14_checkbox_1 = " "
              self.i14_checkbox_2 = " "
              self.i14_checkbox_3 = " "
              self.i14_checkbox_4 = " "
              self.i15_checkbox_1 = " "
              self.i15_checkbox_2 = " "
              self.i18_checkbox_1 = " "
              self.i18_checkbox_2 = " "
              self.i18_checkbox_3 = " "
              self.i20_checkbox_1 = " "
              self.i20_checkbox_2 = " "

              tval_var_to_format = None
              pval_var_to_format = None

              # set t_option and t_value defaults when there is a mixing calculation
              n_mixing_fluid_reactants=0
              for reactant in reactants:
                  if isinstance(reactant, Mixing_Fluid):
                      n_mixing_fluid_reactants += 1
                      if t_option == None:
                          t_option = 3
                          self.t_option=t_option
                      if t_value_1 == None:
                          t_value_1 = None # will be formatted with temp of fluid 1 later
                          self.t_value_1=t_value_1
                      if t_value_2 == None:
                          t_value_2 = float(reactant.T) # temp of fluid 2
                          self.t_value_2=t_value_2
                      if t_value_3 == None:
                          t_value_3 = reactant.mass_ratio # mass ratio factor
                          self.t_value_3=t_value_3

              if n_mixing_fluid_reactants == 0:
                  # set t_option and t_value defaults when there is no mixing calculation
                  if t_option == None:
                      t_option = 0
                      self.t_option = 0
      #             if t_value_1 == None:
      #                 t_value_1 = None
      #                 self.t_value_1 = 0
      #             if t_value_2 == None:
      #                 t_value_2 = 0
      #                 self.t_value_2 = 0
      #             if t_value_3 == None:
      #                 t_value_3 = 0
      #                 self.t_value_3 = 0
              elif n_mixing_fluid_reactants == 1:
                  pass
              else:
                  self.err_handler.raise_exception((""
                          "There are {} mixing fluids ".format(n_mixing_fluid_reactants)+""
                          "in the list of reactants. There may only be one."))
              if t_option == 0:
                  self.t_checkbox_1="x"
                  if isinstance(t_value_1, numbers.Number):
                      self.tval1=t_value_1
                  else:
                      tval_var_to_format = 1

              elif t_option == 1:
                  self.t_checkbox_2="x"
                  if isinstance(t_value_1, numbers.Number):
                      self.tval2=t_value_1
                      self.tval3=t_value_2
                  else:
                      tval_var_to_format = 2

              elif t_option == 2:
                  self.t_checkbox_3="x"
                  if isinstance(t_value_1, numbers.Number):
                      self.tval4=t_value_1
                      self.tval5=t_value_2
                  else:
                      tval_var_to_format = 4

              elif t_option == 3:
                  self.t_checkbox_4="x"
                  if isinstance(t_value_1, numbers.Number):
                      self.tval6=t_value_1
                      self.tval7=t_value_2
                      self.tval8=t_value_3
                  else:
                      self.tval7=t_value_2
                      self.tval8=t_value_3
                      tval_var_to_format = 6
              else:
                  raise Exception("t_option must be 0, 1, 2, or 3.")

              if p_option == 0:
                  self.p_checkbox_1="x"

              elif p_option == 1:
                  self.p_checkbox_2="x"

              elif p_option == 2:
                  self.p_checkbox_3="x"
                  if isinstance(p_value_1, numbers.Number):
                      self.pval1=p_value_1
                  else:
                      pval_var_to_format = 1

              elif p_option == 3:
                  self.p_checkbox_4="x"
                  if isinstance(p_value_1, numbers.Number):
                      self.pval2=p_value_1
                      self.pval3=p_value_2
                  else:
                      pval_var_to_format = 2

              elif p_option == 4:
                  self.p_checkbox_5="x"
                  if isinstance(p_value_1, numbers.Number):
                      self.pval4=p_value_1
                      self.pval5=p_value_2
                  else:
                      pval_var_to_format = 4

              else:
                  raise Exception("p_option must be 0, 1, 2, 3, or 4.")

              if physical_system_model == "closed":
                  self.i1_checkbox_1 = "x"
              elif physical_system_model == "titration":
                  self.i1_checkbox_2 = "x"
              elif physical_system_model == "fluid-centered flow-through open":
                  self.i1_checkbox_3 = "x"
              else:
                  msg = ("physical_system_model must either be 'closed', 'titration',"
                      " or 'fluid-centered flow-through open'.")
                  self.err_handler.raise_exception(msg)

              if kinetic_mode == "arbitrary":
                  self.i2_checkbox_1 = "x"
              elif kinetic_mode == "true":
                  self.i2_checkbox_2 = "x"
              else:
                  msg = "kinetic_mode must either be 'arbitrary' or 'true'."
                  self.err_handler.raise_exception(msg)

              if phase_boundary_search == 0:
                  self.i3_checkbox_1 = "x"
              elif phase_boundary_search == 1:
                  self.i3_checkbox_2 = "x"
              elif phase_boundary_search == 2:
                  self.i3_checkbox_3 = "x"
              else:
                  msg = "phase_boundary_search must be 0, 1, or 2."
                  self.err_handler.raise_exception(msg)

              if permit_solid_solutions == False:
                  self.i4_checkbox_1 = "x"
              elif permit_solid_solutions == True:
                  self.i4_checkbox_2 = "x"
              else:
                  msg = "permit_solid_solutions must be True or False."
                  self.err_handler.raise_exception(msg)

              if clear_es_solids_read == False:
                  self.i5_checkbox_1 = "x"
              elif clear_es_solids_read == True:
                  self.i5_checkbox_2 = "x"
              else:
                  msg = "clear_es_solids_read must be True or False."
                  self.err_handler.raise_exception(msg)

              if clear_es_solids_initial == False:
                  self.i6_checkbox_1 = "x"
              elif clear_es_solids_initial == True:
                  self.i6_checkbox_2 = "x"
              else:
                  msg = "clear_es_solids_initial must be True or False."
                  self.err_handler.raise_exception(msg)

              if clear_es_solids_end == False:
                  self.i7_checkbox_1 = "x"
              elif clear_es_solids_end == True:
                  self.i7_checkbox_2 = "x"
              else:
                  msg = "clear_es_solids_end must be True or False."
                  self.err_handler.raise_exception(msg)

              # there is no iopt8

              if clear_prs_solids_read == False:
                  self.i9_checkbox_1 = "x"
              elif clear_prs_solids_read == True:
                  self.i9_checkbox_2 = "x"
              else:
                  msg = "clear_prs_solids_read must be True or False."
                  self.err_handler.raise_exception(msg)

              if clear_prs_solids_end == False:
                  self.i10_checkbox_1 = "x"
              elif clear_prs_solids_end == True:
                  self.i10_checkbox_2 = "x"
              else:
                  msg = "clear_prs_solids_end must be True or False."
                  self.err_handler.raise_exception(msg)

              if auto_basis_switching_pre_NR == False:
                  self.i11_checkbox_1 = "x"
              elif auto_basis_switching_pre_NR == True:
                  self.i11_checkbox_2 = "x"
              else:
                  msg = "auto_basis_switching_pre_NR must be True or False."
                  self.err_handler.raise_exception(msg)

              if auto_basis_switching_post_NR == False:
                  self.i12_checkbox_1 = "x"
              elif auto_basis_switching_post_NR == True:
                  self.i12_checkbox_2 = "x"
              else:
                  msg = "auto_basis_switching_post_NR must be True or False."
                  self.err_handler.raise_exception(msg)

              if calc_mode_selection == 0:
                  self.i13_checkbox_1 = "x"
              elif calc_mode_selection == 1:
                  self.i13_checkbox_2 = "x"
              elif calc_mode_selection == 2:
                  self.i13_checkbox_3 = "x"
              else:
                  msg = "calc_mode_selection must be 0, 1, or 2."
                  self.err_handler.raise_exception(msg)

              if ODE_corrector_mode == 0:
                  self.i14_checkbox_1 = "x"
              elif ODE_corrector_mode == 1:
                  self.i14_checkbox_2 = "x"
              elif ODE_corrector_mode == 2:
                  self.i14_checkbox_3 = "x"
              elif ODE_corrector_mode == 3:
                  self.i14_checkbox_4 = "x"
              else:
                  msg = "ODE_corrector_mode must be 0, 1, or 2."
                  self.err_handler.raise_exception(msg)

              # The suppress redox option in EQ6 doesn't work with WORM data.
              # Use redox-isolated elements from the redox suppression
              # option in the AqEquil class instead.
              self.i15_checkbox_1 = "x"

              if write_tab == -1:
                  self.i18_checkbox_1 = "x"
              elif write_tab == 0:
                  self.i18_checkbox_2 = "x"
              elif write_tab == 1:
                  self.i18_checkbox_3 = "x"
              else:
                  self.err_handler.raise_exception("write_tab_option must be -1, 0, "
                          "or 1.")

              if fluid_mixing_setup == False:
                  self.i20_checkbox_1 = "x"
              elif fluid_mixing_setup == True:
                  self.i20_checkbox_2 = "x"
              else:
                  msg = "fluid_mixing_setup must be True or False."
                  self.err_handler.raise_exception(msg)

              now = datetime.now()
              self.date_created = now.strftime('%Y-%m-%d %I:%M %p')

              self.__format_reaction(tval_var_to_format, pval_var_to_format)

              if filename != None:
                  with open(filename, 'w') as f:
                      f.write(pr.formatted_reaction)

          def __format_reaction(self, tval_var_to_format=None, pval_var_to_format=None):
              reaction_options_formatted = dict(
                  date_created=f"{self.date_created:<24}",
                  start_xi=f"{'{:.5E}'.format(self.start_xi):>12}",
                  max_xi=f"{'{:.5E}'.format(self.max_xi):>12}",
                  start_time=f"{'{:.5E}'.format(self.start_time):>12}",
                  max_time=f"{'{:.5E}'.format(self.max_time):>12}",
                  min_pH=f"{'{:.5E}'.format(self.min_pH):>12}",
                  max_pH=f"{'{:.5E}'.format(self.max_pH):>12}",
                  min_Eh=f"{'{:.5E}'.format(self.min_Eh):>12}",
                  max_Eh=f"{'{:.5E}'.format(self.max_Eh):>12}",
                  min_fO2=f"{'{:.5E}'.format(self.min_fO2):>12}",
                  max_fO2=f"{'{:.5E}'.format(self.max_fO2):>12}",
                  min_aw=f"{'{:.5E}'.format(self.min_aw):>12}",
                  max_aw=f"{'{:.5E}'.format(self.max_aw):>12}",
                  max_n_steps=f"{self.max_n_steps:>12}",
                  xi_print_int=f"{'{:.5E}'.format(self.xi_print_int):>12}",
                  log_xi_print_int=f"{'{:.5E}'.format(self.log_xi_print_int):>12}",
                  time_print_int=f"{'{:.5E}'.format(self.time_print_int):>12}",
                  log_time_print_int=f"{'{:.5E}'.format(self.log_time_print_int):>12}",
                  pH_print_interval=f"{'{:.5E}'.format(self.pH_print_interval):>12}",
                  Eh_print_interval=f"{'{:.5E}'.format(self.Eh_print_interval):>12}",
                  logfO2_print_interval=f"{'{:.5E}'.format(self.logfO2_print_interval):>12}",
                  aw_print_interval=f"{'{:.5E}'.format(self.aw_print_interval):>12}",
                  n_steps_print_interval=f"{self.n_steps_print_interval:>12}",
                  max_finite_difference_order=f"{self.max_finite_difference_order:>12}",
                  beta_convergence_tolerance=f"{self.beta_convergence_tolerance:>12}",
                  del_convergence_tolerance=f"{self.del_convergence_tolerance:>12}",
                  max_n_NR_iter=f"{self.max_n_NR_iter:>12}",
                  search_find_convergeance_tolerance=f"{self.search_find_convergeance_tolerance:>12}",
                  saturation_tolerance=f"{self.saturation_tolerance:>12}",
                  max_n_phase_assemblage_tries=f"{self.max_n_phase_assemblage_tries:>12}",
                  zero_order_step_size=f"{self.zero_order_step_size:>12}",
                  max_interval_in_xi_between_PRS_transfers=f"{self.max_interval_in_xi_between_PRS_transfers:>12}",
                  t_checkbox_1=self.t_checkbox_1,
                  t_checkbox_2=self.t_checkbox_2,
                  t_checkbox_3=self.t_checkbox_3,
                  t_checkbox_4=self.t_checkbox_4,
                  p_checkbox_1=self.p_checkbox_1,
                  p_checkbox_2=self.p_checkbox_2,
                  p_checkbox_3=self.p_checkbox_3,
                  p_checkbox_4=self.p_checkbox_4,
                  p_checkbox_5=self.p_checkbox_5,
                  i1_checkbox_1=self.i1_checkbox_1,
                  i1_checkbox_2=self.i1_checkbox_2,
                  i1_checkbox_3=self.i1_checkbox_3,
                  i2_checkbox_1=self.i2_checkbox_1,
                  i2_checkbox_2=self.i2_checkbox_2,
                  i3_checkbox_1=self.i3_checkbox_1,
                  i3_checkbox_2=self.i3_checkbox_2,
                  i3_checkbox_3=self.i3_checkbox_3,
                  i4_checkbox_1=self.i4_checkbox_1,
                  i4_checkbox_2=self.i4_checkbox_2,
                  i5_checkbox_1=self.i5_checkbox_1,
                  i5_checkbox_2=self.i5_checkbox_2,
                  i6_checkbox_1=self.i6_checkbox_1,
                  i6_checkbox_2=self.i6_checkbox_2,
                  i7_checkbox_1=self.i7_checkbox_1,
                  i7_checkbox_2=self.i7_checkbox_2,
                  i9_checkbox_1=self.i9_checkbox_1,
                  i9_checkbox_2=self.i9_checkbox_2,
                  i10_checkbox_1=self.i10_checkbox_1,
                  i10_checkbox_2=self.i10_checkbox_2,
                  i11_checkbox_1=self.i11_checkbox_1,
                  i11_checkbox_2=self.i11_checkbox_2,
                  i12_checkbox_1=self.i12_checkbox_1,
                  i12_checkbox_2=self.i12_checkbox_2,
                  i13_checkbox_1=self.i13_checkbox_1,
                  i13_checkbox_2=self.i13_checkbox_2,
                  i13_checkbox_3=self.i13_checkbox_3,
                  i14_checkbox_1=self.i14_checkbox_1,
                  i14_checkbox_2=self.i14_checkbox_2,
                  i14_checkbox_3=self.i14_checkbox_3,
                  i14_checkbox_4=self.i14_checkbox_4,
                  i15_checkbox_1=self.i15_checkbox_1,
                  i15_checkbox_2=self.i15_checkbox_2,
                  i18_checkbox_1=self.i18_checkbox_1,
                  i18_checkbox_2=self.i18_checkbox_2,
                  i18_checkbox_3=self.i18_checkbox_3,
                  i20_checkbox_1=self.i20_checkbox_1,
                  i20_checkbox_2=self.i20_checkbox_2,
              )

              # leave {tval1} (or {tval2}, {tval3}...) in pre_6i file so it can be updated after joining 3p
              if tval_var_to_format != None:
                  reaction_options_formatted["tval"+str(tval_var_to_format)] = "{tval}"
                  for i in range(1, 9):
                      if i == tval_var_to_format:
                          continue
                      else:
                          reaction_options_formatted["tval"+str(i)]=f"{'{:.5E}'.format(getattr(self, 'tval'+str(i))):>12}"
              else:
                  reaction_options_formatted.update(dict(
                      tval1=f"{'{:.5E}'.format(self.tval1):>12}",
                      tval2=f"{'{:.5E}'.format(self.tval2):>12}",
                      tval3=f"{'{:.5E}'.format(self.tval3):>12}",
                      tval4=f"{'{:.5E}'.format(self.tval4):>12}",
                      tval5=f"{'{:.5E}'.format(self.tval5):>12}",
                      tval6=f"{'{:.5E}'.format(self.tval6):>12}",
                      tval7=f"{'{:.5E}'.format(self.tval7):>12}",
                      tval8=f"{'{:.5E}'.format(self.tval8):>12}",
                  ))

              # leave {pval1} (or {pval2}, {pval3}...) in pre_6i file so it can be updated after joining 3p
              if pval_var_to_format != None:
                  reaction_options_formatted["pval"+str(pval_var_to_format)] = "{pval}"
                  for i in range(1, 6):
                      if i == pval_var_to_format:
                          continue
                      else:
                          reaction_options_formatted["pval"+str(i)]=f"{'{:.5E}'.format(getattr(self, 'pval'+str(i))):>12}"
              else:
                  reaction_options_formatted.update(dict(
                      pval1=f"{'{:.5E}'.format(self.pval1):>12}",
                      pval2=f"{'{:.5E}'.format(self.pval2):>12}",
                      pval3=f"{'{:.5E}'.format(self.pval3):>12}",
                      pval4=f"{'{:.5E}'.format(self.pval4):>12}",
                      pval5=f"{'{:.5E}'.format(self.pval5):>12}",
                  ))

              if len(self.reactants) > 0:
                  reactant_blocks = "".join([r.formatted_block for r in self.reactants])
              else:
                  reactant_blocks = """\n|Reactant        |None                    | (ureac(n))                         |
      |------------------------------------------------------------------------------|
      |->|Type         |None                    | (urcjco(jcode(n)))                 |
      |------------------------------------------------------------------------------|
      |->|Status       |None                    | (urcjre(jreac(n)))                 |
      |------------------------------------------------------------------------------|
      |->|Amount remaining (moles) | 0.00000E+00| (morr(n))                          |
      |------------------------------------------------------------------------------|
      |->|Amount destroyed (moles) | 0.00000E+00| (modr(n))                          |
      |------------------------------------------------------------------------------|"""
              gas_lines = "".join([r.formatted_line for r in self.gases])
              reaction_options_formatted.update(dict(reactant_blocks=reactant_blocks))
              reaction_options_formatted.update(dict(gas_lines=gas_lines))

              if self.mineral_suppression_option == "None":
                  mineral_suppress_option_value = "None    "
              elif self.mineral_suppression_option == "All":
                  mineral_suppress_option_value = "All     "
              else:
                  msg = ("Error in Prepare_Reaction(): `mineral_suppression_option` "
                         "must be 'None' or 'All'.")
                  self.err_handler.raise_exception(msg)

              min_supp_temp = copy.copy(mineral_suppress_template)
              min_supp_dict = dict(mineral_suppress_option=mineral_suppress_option_value)
              min_supp_temp = min_supp_temp.format(**min_supp_dict)

              reaction_options_formatted.update(
                      dict(mineral_suppress_lines=min_supp_temp)
                      )

              reaction_template = copy.copy(template)
              self.formatted_reaction = reaction_template.format(**reaction_options_formatted)

  Class used to set the parameters of a reaction between the results of a speciation calculation and minerals, gases, etc.

  ## Parameters

  `reactants` :`list`
    List of reactants defined by the 'Reactant' class. Can be an empty list if no reactants are desired.
  `gases` :`list`, default `[]`
    List of gases defined by the 'Gas' class. Can be an empty list if no gases are desired.
  `t_option` :`int`, default `0`

    Desired option for handling temperature of the reaction. Valid choices include:

    - 0 for constant temperature
    - 1 for linear tracking in Xi
    - 2 for linear tracking in time

    Fluid mixing tracking is not yet supported.

  `t_value_1`, `t_value_2`, `t_value_3` :`default None, 0, 0, respectively`

    By default, the temperature of samples in the speciation calculation will be used, so the user does not need to specify temperature values here. However, if a user wishes, temperature values can be defined here that will be applied to all samples in the speciation. Note that doing so may result in incongruous results. That said, the values specified here depend on which option is selected for `t_option`:

    - If 't_option' is 0, then t_value_1 is the value of the constant temperature (in degrees C), and t_value_2 and t_value_3 are ignored.
    - If 't_option' is 1, then t_value_1 is the base value temperature (in degrees C), t_value_2 is the derivative, and t_value_3 is ignored.
    - If 't_option' is 2, then t_value_1 is the base value temperature (in degrees C), t_value_2 is the derivative, and t_value_3 is ignored.
  `p_option` :`int`, default `0`

    Desired option for handling pressure of the reaction. Valid choices include:

    - 0 to follow the data file reference pressure curve
    - 1 to follow the 1.013-bar/steam-saturation curve
    - 2 for constant pressure
    - 3 for linear tracking in Xi
    - 4 for linear tracking in time
  `p_value_1`, `p_value_2` :`float default None, 0`

    Values assigned to desired `p_option`.

    - If `p_option` is 0 or 1, p_value_1 and p_value_2 are ignored.
    - If `p_option` is 2, p_value_1 represents a constant pressure, in bars, and p_value_2 is ignored.
    - If `p_option` is 3 or 4, p_value_1 represents the base pressure value, in bars, and p_value_2 is the derivative.
  `xi_range` :`list` of `two float`, default `[0, 1]`
    A list containing the starting and maximum value of Xi, respectively.
  `time_range` :`list` of `two float`, default `[0, 1e38]`
    A list containing the starting and maximum time, respectively.
  `pH_range` :`list` of `two float`, default `[-1e38, 1e38]`
    A list containing the minimum and maximum values of pH.
  `Eh_range` :`list` of `two float`, default `[-1e38, 1e38]`
    A list containing the minimum and maximum values of Eh.
  `fO2_range` :`list` of `two float`, default `[-1e38, 1e38]`
    A list containing the minimum and maximum values of the fugacity of oxygen, fO2.
  `aw_range` :`list` of `two float`, default `[-1e38, 1e38]`
    A list containing the minimum and maximum values of water activity.
  `max_n_steps` :`int`, default `900`
    Maximum number of steps of Xi allowed.
  `xi_print_int` :`int`, default `1`
    Xi print interval.
  `log_xi_print_int` :`int`, default `1`
    Log Xi print interval.
  `time_print_int` :`int`, default `1e38`
    Time print interval.
  `log_time_print_int` :`int`, default `1e38`
    Log time print interval.
  `pH_print_interval` :`int`, default `1e38`
    pH print interval.
  `Eh_print_interval` :`int`, default `1e38`
    Eh (v) print interval.
  `logfO2_print_interval` :`int`, default `1e38`
    Log fO2 print interval.
  `aw_print_interval` :`int`, default `1e38`
    Activity of water (aw) print interval.
  `n_steps_print_interval` :`int`, default `100`
    Steps print interval.
  `physical_system_model` :`str`, default `"closed"`

    Selection for the physical system model. Valid options include:

    - "closed"
    - "titration"
    - "fluid-centered flow-through open"
  `kinetic_mode` :`str`, default `"arbitrary"`
    Selection for kinetic mode. Valid options include: - "arbitrary" for arbitrary kinetics, reaction progress mode - "true" for true kinetics, reaction progress/time mode
  `phase_boundary_search` :`int`, default `0`

    Selection for phase boundary searches. Valid options include:

    - 0 to search for phase boundaries and constrain the step size to match.
    - 1 to search for phase boundaries and print their locations.
    - 2 to not search for phase boundaries.
  `permit_solid_solutions` :`bool`, default `False`
    Permit solid solutions? If False, solid solutions are ignored.
  `clear_es_solids_read` :`bool`, default `False`
    Clear the ES solids read from the input file?
  `clear_es_solids_initial` :`bool`, default `False`
    Clear the ES solids at the initial value of reaction progress?
  `clear_es_solids_end` :`bool`, default `False`
    Clear the ES solids at the end of the run?
  `clear_prs_solids_read` :`bool`, default `False`
    Clear the PRS solids read from the input file?
  `clear_prs_solids_end` :`bool`, default `False`
    Clear the PRS solids at the end of the run? If True, PRS solids will be cleared unless numerical problems cause early termination.
  `auto_basis_switching_pre_NR` :`bool`, default `False`
    Turn on auto basis switching in pre-Newton-Raphson optimization?
  `auto_basis_switching_post_NR` :`bool`, default `False`
    Turn on auto basis switching after Newton-Raphson iteration?
  `calc_mode_selection` :`int`, default `0`

    Calculational mode selection. Valid options include:

    - 0 for normal path tracing
    - 1 for economy mode (if permissible)
    - 2 for super economy mode (if permissible)
  `ODE_corrector_mode` :`int`, default `0`

    ODE integrator corrector mode selection. Valid options include:

    - 0 to allow stiff and simple correctors
    - 1 to allow only simple corrector
    - 2 to allow only stiff corrector
    - 3 to allow no correctors
  `mineral_suppression_option` :`str`, default `"None"`
    Option to suppress formation of minerals. Can be either "None" (no minerals are suppressed) or "All" (all minerals are suppressed).
  `write_tab` :`int`, default `-1`

    Option to write a TAB file. Valid options include:

    - -1 do not write a TAB file (default)
    - 0 write a TAB file
    - 1 write a TAB file prepending TABX file data from a previous run
  `fluid_mixing_setup` :`bool`, default `False`
    If True, will write an EQ6 input file with Fluid 1 set up for fluid mixing. If False, a normal EQ6 pickup file will be written.
  `max_finite_difference_order` :`int`, default `6`
    Maximum finite-difference order (numerical parameter).
  `beta_convergence_tolerance` :`float`, default `0`
    Beta convergence tolerance (numerical parameter).
  `del_convergence_tolerance` :`float`, default `0`
    Delta convergence tolerance (numerical parameter).
  `max_n_NR_iter` :`int`, default `500`
    Maximum number of N-R iterations (numerical parameter).
  `search_find_convergeance_tolerance` :`float`, default `0`
    Search/find convergence tolerance (numerical parameter).
  `saturation_tolerance` :`float`, default `0`
    Saturation tolerance (numerical parameter).
  `max_n_phase_assemblage_tries` :`int`, default `0`
    Maximum number of phase assemblage tries (numerical parameter).
  `zero_order_step_size` :`int`, default `0`
    Zero order step size in Xi (numerical parameter).
  `max_interval_in_xi_between_PRS_transfers` :`int`, default `0`
    Maximum interval in Xi between PRS transfers (numerical parameter).
  `filename` :`str`, default `None`
    Filename where the results of `Prepare_Reaction` will be written. This is equivalent to the top half of an EQ3/6 6i file. If None, no file will be written.
  `hide_traceback` :`bool`, default `True`
    Hide traceback message when encountering errors handled by this class? When True, error messages handled by this class will be short and to the point.
`class Reactant (reactant_name,
reactant_type='Pure mineral',
reactant_status='Reacting',
amount_remaining=1,
amount_destroyed=0,
molar_volume=0,
surface_area_option=0,
surface_area_value=0,
surface_area_factor=0,
f_rate_law='Relative rate equation',
f_eq1=1,
f_eq2=0,
f_eq3=0,
b_rate_law='Partial equilibrium',
b_eq1=1,
b_eq2=0,
b_eq3=0,
special_reactant_dict={},
hide_traceback=True)`
  Expand source code

      class Reactant:
          def __init__(self,
                       reactant_name,
                       reactant_type="Pure mineral",
                       reactant_status="Reacting",
                       amount_remaining=1,
                       amount_destroyed=0,
                       molar_volume=0,
                       surface_area_option=0,
                       surface_area_value=0,
                       surface_area_factor=0,
                       f_rate_law="Relative rate equation",
                       f_eq1=1,
                       f_eq2=0,
                       f_eq3=0,
                       b_rate_law="Partial equilibrium",
                       b_eq1=1,
                       b_eq2=0,
                       b_eq3=0,
                       special_reactant_dict={},
                       hide_traceback=True,
                       ):

              """
              Class used to define reactants for `Prepare_Reaction`.

              Parameters
              ----------
              reactant_name : str
                  Name of the reactant.

              reactant_type : str, default "Pure mineral"
                  Reactant type. Valid types include:

                  - Pure mineral
                  - Solid solution
                  - Special reactant
                  - Aqueous species
                  - Gas species
                  - Generic ion exchanger

              reactant_status : str, default "Reacting"
                  Status of the reactant. Valid statuses include:

                  - Reacting
                  - Saturated, reacting
                  - Exhausted
                  - Saturated, not reacting

              amount_remaining : float, default 1
                  Moles of reactant remaining.

              amount_destroyed : float, default 0
                  Moles of reactant destroyed.

              surface_area_option : int, default 0
                  Option for reactant surface area. Valid options include:

                  - 0, for Constant surface area (in cm2)
                  - 1, for Constant specific surface area (in cm2/g). Surface area
                  changes in proportion to the reactant mass.
                  - 2, for n**2/3 growth law- current surface area (in cm2)

              surface_area_value : float, default 0
                  The value assigned to choice of surface area option according to:

                  - If `surface_area_option` is 0, value is in cm2.
                  - If `surface_area_option` is 1, value is in cm2/g.
                  - If `surface_area_option` is 2, value is in cm2.

              surface_area_factor : float, default 0
                  Value assigned to surface area factor.

              f_rate_law : str, default "Relative rate equation"
                  Type of forward rate law. Valid types include:

                  - Use backward rate law
                  - Relative rate equation
                  The TST rate equation is not yet supported.

              f_eq1, f_eq2, f_eq3 : float, default 1, 0, 0, respectively
                  Coefficients of the forward rate law defined for `f_rate_law`.
                  If `f_rate_law` is "Relative rate equation", then:

                  - f_eq1 is dXi(n)/dXi (mol/mol)
                  - f_eq2 is d2Xi(n)/dXi2 (mol/mol2)
                  - f_eq3 is d3Xi(n)/dXi3 (mol/mol3)

              b_rate_law : str, default "Partial equilibrium"
                  Type of backward rate law. Valid types include:

                  - Use forward rate law
                  - Partial equilibrium
                  - Relative rate equation
                  The TST rate equation is not yet supported.

              b_eq1, b_eq2, b_eq3 : float, default 1, 0, 0, respectively
                  Coefficients of the backward rate law defined for `b_rate_law`.
                  If `b_rate_law` is "Relative rate equation", then:

                  - the value of b_eq1 represents dXi(n)/dXi (mol/mol)
                  - the value of b_eq2 represents d2Xi(n)/dXi2 (mol/mol2)
                  - the value of b_eq3 represents d3Xi(n)/dXi3 (mol/mol3)

              hide_traceback : bool, default True
                  Hide traceback message when encountering errors handled by this class?
                  When True, error messages handled by this class will be short and to
                  the point.

              """

              self.err_handler = Error_Handler(clean=hide_traceback)

              if f_rate_law not in ["Use backward rate law", "Relative rate equation"]:
                  self.err_handler.raise_exception(("f_rate_law must be either "
                          "'Use backward rate law' or 'Relative rate equation'."))
              if b_rate_law not in ["Use forward rate law", "Partial equilibrium", "Relative rate equation"]:
                  self.err_handler.raise_exception(("b_rate_law must be either "
                          "'Use forward rate law', 'Partial equilibrium', or 'Relative rate equation'."))

              self.reactant_name=reactant_name
              self.reactant_type=reactant_type
              self.reactant_status=reactant_status
              self.amount_remaining=amount_remaining
              self.amount_destroyed=amount_destroyed
              self.molar_volume=molar_volume
              self.sa_val_1=0
              self.sa_val_2=0
              self.sa_val_3=0
              self.sa_checkbox_1= " "
              self.sa_checkbox_2= " "
              self.sa_checkbox_3= " "

              self.special_reactant_dict=special_reactant_dict

              if surface_area_option == 0:
                  self.sa_checkbox_1 = "x"
                  self.sa_val_1 = surface_area_value
              elif surface_area_option == 1:
                  self.sa_checkbox_2 = "x"
                  self.sa_val_2 = surface_area_value
              elif surface_area_option == 2:
                  self.sa_checkbox_3 = "x"
                  self.sa_val_3 = surface_area_value
              self.sa_factor=surface_area_factor

              self.f_rate_law=f_rate_law
              self.f_eq1=f_eq1
              self.f_eq2=f_eq2
              self.f_eq3=f_eq3
              self.b_rate_law=b_rate_law
              self.b_eq1=b_eq1
              self.b_eq2=b_eq2
              self.b_eq3=b_eq3

              self.__format_rate_block("forward")
              self.__format_rate_block("backward")

              self.__format_block()

          def __format_block(self):

              rb_dict_formatted = dict(
                      reactant_name = f"{self.reactant_name:<24}",
                      reactant_type = f"{self.reactant_type:<24}",
                      reactant_status = f"{self.reactant_status:<24}",
                      amount_remaining = f"{'{:.5E}'.format(self.amount_remaining):>12}",
                      amount_destroyed = f"{'{:.5E}'.format(self.amount_destroyed):>12}",
                      sa_checkbox_1 = self.sa_checkbox_1,
                      sa_checkbox_2 = self.sa_checkbox_2,
                      sa_checkbox_3 = self.sa_checkbox_3,
                      sa_val_1 = f"{'{:.5E}'.format(self.sa_val_1):>12}",
                      sa_val_2 = f"{'{:.5E}'.format(self.sa_val_2):>12}",
                      sa_val_3 = f"{'{:.5E}'.format(self.sa_val_3):>12}",
                      sa_factor = f"{'{:.5E}'.format(self.sa_factor):>12}",
                      f_rate_law = f"{self.f_rate_law:<24}",
                      b_rate_law = f"{self.b_rate_law:<24}",
                      f_rate_block = self.formatted_f_rate_block,
                      b_rate_block = self.formatted_b_rate_block,
                      )

              if self.reactant_type.lower() == "special reactant":
                  elem_lines = []
                  for key in list(self.special_reactant_dict.keys()):
                      sp_elem_line = copy.copy(srb_stoich_template)
                      elem_name = f"{str(key):<8}"
                      elem_value = f"{'{:.15E}'.format(float(self.special_reactant_dict[key])):>22}"
                      sp_elem_line = sp_elem_line.format(**{"elem":elem_name, "elem_val":elem_value})
                      elem_lines.append(sp_elem_line)
                  elem_lines = "".join(elem_lines)

                  srb_dict_formatted = dict(
                          molar_volume = f"{'{:.5E}'.format(self.molar_volume):>12}",
                          srb_stoich = elem_lines,
                          )

                  sp_reactant_block_template = copy.copy(srb_template)
                  sp_reactant_block_formatted = sp_reactant_block_template.format(**srb_dict_formatted)
                  rb_dict_formatted["sr_block"] = sp_reactant_block_formatted
              else:
                  rb_dict_formatted["sr_block"] = ""

              reactant_block_template = copy.copy(rb_template)
              self.formatted_block = reactant_block_template.format(**rb_dict_formatted)

          def __format_rate_block(self, direction):
              if direction == "forward":
                  d="f"
                  od="b"
                  odirection="backward"
              else:
                  d="b"
                  od="f"
                  odirection="forward"

              eq1 = getattr(self, d+"_eq1")
              eq2 = getattr(self, d+"_eq2")
              eq3 = getattr(self, d+"_eq3")

              if getattr(self, d+"_rate_law") == "Relative rate equation":
                  rate_block = copy.deepcopy(relative_rate_equation_template)

                  rate_options_formatted = dict(
                      eq1=f"{'{:.5E}'.format(eq1):>12}",
                      eq2=f"{'{:.5E}'.format(eq2):>12}",
                      eq3=f"{'{:.5E}'.format(eq3):>12}",
                  )

                  rate_block = rate_block.format(**rate_options_formatted)
                  setattr(self, "formatted_"+d+"_rate_block", rate_block)
              elif getattr(self, d+"_rate_law") == "Linear rate equation":
                  # Note: "Linear rate equation" is valid in EQ6, but I cannot find any
                  # EQ6 input files formatted for this option. My guess is that linear
                  # rate equations can be imposed by only specifying the first parameter
                  # of a relative rate equation.
                  self.err_handler.raise_exception("The 'Linear rate equation' option is not supported at this time.")
              elif getattr(self, d+"_rate_law") == "TST rate equation":
                  self.err_handler.raise_exception("TST rate equations are not supported at this time.")
              elif getattr(self, d+"_rate_law") == "Partial equilibrium":
                  setattr(self, "formatted_"+d+"_rate_block", "")
              elif getattr(self, d+"_rate_law") == "Use backward rate law":
                  setattr(self, "formatted_"+d+"_rate_block", "")
              elif getattr(self, d+"_rate_law") == "Use forward rate law":
                  setattr(self, "formatted_"+d+"_rate_block", "")
              else:
                  msg = ("Valid rate laws for the "+direction+" rate law includes "
                      "'Relative rate equation', 'TST rate equation', "
                      "'Linear rate equation', or 'Use "+odirection+" rate law'")
                  self.err_handler.raise_exception(msg)

  Class used to define reactants for `Prepare_Reaction`.

  ## Parameters

  `reactant_name` :`str`
    Name of the reactant.
  `reactant_type` :`str`, default `"Pure mineral"`

    Reactant type. Valid types include:

    - Pure mineral
    - Solid solution
    - Special reactant
    - Aqueous species
    - Gas species
    - Generic ion exchanger
  `reactant_status` :`str`, default `"Reacting"`

    Status of the reactant. Valid statuses include:

    - Reacting
    - Saturated, reacting
    - Exhausted
    - Saturated, not reacting
  `amount_remaining` :`float`, default `1`
    Moles of reactant remaining.
  `amount_destroyed` :`float`, default `0`
    Moles of reactant destroyed.
  `surface_area_option` :`int`, default `0`

    Option for reactant surface area. Valid options include:

    - 0, for Constant surface area (in cm2)
    - 1, for Constant specific surface area (in cm2/g). Surface area changes in proportion to the reactant mass.
    - 2, for n**2/3 growth law- current surface area (in cm2)
  `surface_area_value` :`float`, default `0`

    The value assigned to choice of surface area option according to:

    - If `surface_area_option` is 0, value is in cm2.
    - If `surface_area_option` is 1, value is in cm2/g.
    - If `surface_area_option` is 2, value is in cm2.
  `surface_area_factor` :`float`, default `0`
    Value assigned to surface area factor.
  `f_rate_law` :`str`, default `"Relative rate equation"`

    Type of forward rate law. Valid types include:

    - Use backward rate law
    - Relative rate equation The TST rate equation is not yet supported.
  `f_eq1`, `f_eq2`, `f_eq3` :`float`, default `1, 0, 0, respectively`

    Coefficients of the forward rate law defined for `f_rate_law`. If `f_rate_law` is "Relative rate equation", then:

    - f_eq1 is dXi(n)/dXi (mol/mol)
    - f_eq2 is d2Xi(n)/dXi2 (mol/mol2)
    - f_eq3 is d3Xi(n)/dXi3 (mol/mol3)
  `b_rate_law` :`str`, default `"Partial equilibrium"`

    Type of backward rate law. Valid types include:

    - Use forward rate law
    - Partial equilibrium
    - Relative rate equation The TST rate equation is not yet supported.
  `b_eq1`, `b_eq2`, `b_eq3` :`float`, default `1, 0, 0, respectively`

    Coefficients of the backward rate law defined for `b_rate_law`. If `b_rate_law` is "Relative rate equation", then:

    - the value of b_eq1 represents dXi(n)/dXi (mol/mol)
    - the value of b_eq2 represents d2Xi(n)/dXi2 (mol/mol2)
    - the value of b_eq3 represents d3Xi(n)/dXi3 (mol/mol3)
  `hide_traceback` :`bool`, default `True`
    Hide traceback message when encountering errors handled by this class? When True, error messages handled by this class will be short and to the point.