aqequil.create_data0 API documentation
Description: Python implementation of create_data0.r Converts thermodynamic data into EQ3/6 data0 format

# Module `aqequil.create_data0`

Python implementation of create_data0.r Converts thermodynamic data into EQ3/6 data0 format

## Functions

`def create_data0(thermo_df,
element_df=None,
solid_solution_df=None,
db='',
water_model='SUPCRT92',
template='',
dissrxns=None,
basis_pref=None,
exceed_Ttr=False,
fixed_species=None,
redox_elem_states=None,
verbose=1)`
  Expand source code

      def create_data0(thermo_df,
                       element_df=None,
                       solid_solution_df=None,
                       db="",
                       water_model="SUPCRT92",
                       template="",
                       dissrxns=None,
                       basis_pref=None,
                       exceed_Ttr=False,
                       fixed_species=None,
                       redox_elem_states=None,
                       verbose=1):
          """
          Main function to create a data0 file from thermodynamic data.

          Parameters
          ----------
          thermo_df : DataFrame
              Thermodynamic database
          element_df : DataFrame, optional
              Element database (not currently used, kept for compatibility)
          solid_solution_df : DataFrame, optional
              Solid solution database
          db : str
              Three-letter database code
          water_model : str
              Water model to use (SUPCRT92, IAPWS95, or DEW)
          template : str
              data0.min template content
          dissrxns : dict
              Dissociation reactions dictionary with 'basis_list' key
          basis_pref : list, optional
              Basis preference list (not currently used)
          exceed_Ttr : bool
              Whether to exceed transition temperatures
          fixed_species : list
              List of fixed species (H2O, H+, O2(g), water, Cl-, e-)
          verbose : int
              Verbosity level (0, 1, or 2)

          Returns
          -------
          str
              Formatted data0 file content
          """

          if fixed_species is None:
              fixed_species = ["H2O", "H+", "O2(g)", "water", "e-"]

          if dissrxns is None:
              dissrxns = {"basis_list": {}}

          # Set water model
          water(water_model, messages=False)

          # Initialize data structures
          azero_vec = {}
          neutral_ion_type_vec = {}
          dissociation_list = {}
          tag_vec = {}

          # Process each row in thermodynamic dataframe
          for i in range(len(thermo_df)):
              row = thermo_df.iloc[i]
              species_name = row["name"]

              # Look up azero bdot param
              azero_vec[species_name] = row["azero"]

              # Look up neutral ion type
              neutral_ion_type_vec[species_name] = row["neutral_ion_type"]

              # Look up dissociation reaction
              if pd.notna(row["dissrxn"]) and row["dissrxn"] != "" and species_name not in dissrxns:
                  dissrxn = row["dissrxn"]
              elif species_name in dissrxns:
                  # Use auto-balanced dissociation reaction
                  dissrxn = dissrxns[species_name]
              elif row["tag"] == "basis":
                  # Basis species don't need dissociation reactions
                  dissrxn = None
              else:
                  vmessage(f"Error: dissociation reaction could not be generated for:", 1, verbose)
                  vmessage(species_name, 1, verbose)
                  dissrxn = None

              # Parse dissociation reaction if not a basis species
              if row["tag"] != "basis" and dissrxn is not None:
                  dissrxn_parts = dissrxn.split()
                  dissrxn_names = dissrxn_parts[1::2]  # Get names (odd indices)
                  dissrxn_coefs = [float(c) for c in dissrxn_parts[0::2]]  # Get coeffs (even indices)
                  dissociation_list[species_name] = dict(zip(dissrxn_names, dissrxn_coefs))

              # Look up tag
              tag_vec[species_name] = row["tag"]

              # When redox is suppressed, auxiliary basis species representing pseudoelements
              # should be promoted to strict basis species
              if redox_elem_states and row["tag"] == "aux":
                  # Check if this auxiliary species represents a redox-suppressed oxidation state
                  # redox_elem_states format: {'Fe': {'Fe+2': 'Fejiip', 'Fe+3': 'Fejiiip'}}
                  for element, ox_states_dict in redox_elem_states.items():
                      # Check if species_name matches any of the oxidation state keys (e.g., 'Fe+2', 'Fe+3')
                      if species_name in ox_states_dict.keys():
                          # This aux species represents a pseudoelement, promote to basis
                          tag_vec[species_name] = "basis"
                          vmessage(f"Promoting '{species_name}' from auxiliary to strict basis for redox suppression", 1, verbose)
                          # Remove dissociation reaction since this is now a strict basis species
                          if species_name in dissociation_list:
                              del dissociation_list[species_name]
                              vmessage(f"Removed dissociation reaction for promoted species '{species_name}'", 1, verbose)
                          break

          # Initialize vector of name differences between CHNOSZ and data0
          CHNOSZ_data0_name_diff = {}

          add_obigt_df = thermo_df.copy()

          # Remove raw line indicators '\r' in data0.min template
          data0_template = template.replace("\r", "")

          # Initialize list to store names of skipped species
          skipped_species = []

          # Loop through species in thermo_df
          for idx in range(len(thermo_df)):
              entry = thermo_df.iloc[idx]
              name = entry["name"]

              # Skip certain default basis species
              if name in ["O2(g)", "H2O", "H+"]:
                  vmessage(f"{name} is included as a basis species by default. Moving to the next species...", 2, verbose)
                  continue

              # Skip species that have been promoted to strict basis (for redox suppression)
              if name in tag_vec and tag_vec[name] == "basis":
                  vmessage(f"'{name}' was promoted to strict basis species. Skipping auxiliary/non-basis processing...", 2, verbose)
                  continue

              # Skip polymorphs
              if entry["state"] in [f"cr{i}" for i in range(2, 21)]:
                  continue

              date = str(entry["date"])
              date = date[:9]  # Truncate date if greater than 9 characters

              elem = makeup(thermo_df.iloc[idx]["formula_modded"])  # Get elemental composition

              aux_basis = False

              # Check if this is a basis species
              # basis_list can be either a dict (from Python) or an R object
              if hasattr(dissrxns, 'rx2'):  # R object
                  try:
                      basis_list = dissrxns.rx2("basis_list")
                      if hasattr(basis_list, 'names'):  # R StrVector with names
                          basis_list_names = [str(basis_list.rx2(k)[0]) for k in basis_list.names]
                      else:
                          basis_list_names = []
                  except:
                      basis_list_names = []
              elif "basis_list" in dissrxns:  # Python dict
                  basis_list = dissrxns["basis_list"]
                  if hasattr(basis_list, 'items'):  # It's a dict-like object
                      basis_list_names = [v[0] if isinstance(v, (list, tuple)) else v for v in basis_list.values()]
                  elif hasattr(basis_list, '__iter__'):  # It's an iterable
                      basis_list_names = [v[0] if isinstance(v, (list, tuple)) else v for v in basis_list]
                  else:
                      basis_list_names = []
              else:
                  basis_list_names = []

              if name in basis_list_names:
                  vmessage(f"'{name}' (basis species) processed successfully.", 2, verbose)
                  continue
              elif entry["tag"] == "basis":
                  vmessage(f"'{name}' (basis species) processed successfully.", 2, verbose)
                  continue
              elif entry["tag"] == "aux":
                  # Auxiliary basis species
                  aux_basis = True

              # Format charge for this species' data0 entry
              if "Z" in elem:
                  charge = elem["Z"]
                  elem = {k: v for k, v in elem.items() if k != "Z"}
                  formatted_charge = f"{charge:.1f}"
              else:
                  formatted_charge = "0.0"

              # Format the element block of this species' data0 entry
              # Layout: 3 elements per line, each line starts with 7 spaces
              elem_list = []
              elem_names = list(elem.keys())

              for i, elem_name in enumerate(elem_names):
                  elem_val = s_d(elem[elem_name], 4)

                  # Conditional formatting based on position (3 elements per line)
                  if i % 3 == 0:  # First entry of a line (indices 0, 3, 6, 9, ...)
                      max_length = 8
                      end_char = ""
                      if len(elem_name) != 2:
                          elem_name = elem_name + " "
                  elif (i + 1) % 3 == 0 and i != len(elem_names) - 1:  # Last entry of a line (not final element)
                      max_length = 15
                      end_char = "\n"
                  else:  # Middle entry of a line
                      max_length = 15
                      end_char = ""
                      if len(elem_name) != 2:
                          elem_name = elem_name + " "

                  # Paste together value and element name
                  pasted_entry = f"{elem_val} {elem_name}"

                  # Get decimal position and format spaces accordingly
                  decimal_position = pasted_entry.index(".")
                  pasted_entry = " " * (max_length - decimal_position) + pasted_entry + end_char

                  # Add entry to element list
                  elem_list.append(pasted_entry)

              n_elements = str(len(elem_list))
              element_list = "".join(elem_list)

              # Format the dissociation reaction block of this species' data0 entry
              if name in dissociation_list:
                  species_name_list = list(dissociation_list[name].keys())
                  species_val_list = list(dissociation_list[name].values())
              else:
                  species_name_list = []
                  species_val_list = []

              n_species = len(species_name_list)
              nchar = 25  # spaces taken by entries in dissociation reaction (25 char)

              # Convert species names to their data0 counterparts
              for j, species in enumerate(species_name_list):
                  if species in CHNOSZ_data0_name_diff:
                      species_name_list[j] = CHNOSZ_data0_name_diff[species]

              logK_list = f"logK_grid_{name}"

              # Loop through species in dissociation reaction and format for data0
              spec_list = []

              for i in range(n_species):
                  # Get species value and name
                  species_val = f"{species_val_list[i]:.4f}"
                  species_name = species_name_list[i]

                  # Conditional formatting based on position
                  # Always put 2 species per line (matching R behavior)
                  # i=0, 2, 4, ... are first entries of lines
                  # i=1, 3, 5, ... are second entries of lines (add newline after)
                  if i % 2 == 0:  # First entry of a line (i=0, 2, 4, ...)
                      max_length = 7
                      end_char = ""
                      species_name = fillspace(species_name, nchar)
                  elif i % 2 == 1 and i != n_species - 1:  # Second entry of a line, not last (i=1, 3, 5, ...)
                      max_length = 8
                      end_char = "\n"
                  else:  # Last entry (odd index, last species)
                      max_length = 8
                      end_char = ""
                      species_name = fillspace(species_name, nchar)

                  # Paste together coeff and element name
                  pasted_entry = f"{species_val}  {species_name}"

                  # Get decimal position and format spaces accordingly
                  decimal_position = pasted_entry.index(".")
                  pasted_entry = " " * (max_length - decimal_position) + pasted_entry + end_char

                  # Add entry to element list
                  spec_list.append(pasted_entry)

              # Instantiate template and begin formatting aq, cr, gas, liq entries
              species_list = "".join(spec_list)
              n_species = str(n_species)

              template_fmt = ("+--------------------------------------------------------------------\n%s\n"
                             "    date last revised = %s\n%s\n     charge  =   %s\n%s\n"
                             "     %s element(s):\n%s\n     %s species in aqueous dissociation reaction:      \n%s\n"
                             "**** logK grid [T, P @ Miscellaneous parameters]=%s\n%s")

              if entry["state"] == "aq":
                  formatted_name = name
                  volume = "*"
                  if aux_basis:
                      keys = " keys   = aux              active"
                      insertline_regex = r"\+-+\naqueous species"
                      insertline = "+--------------------------------------------------------------------\naqueous species"
                  else:
                      keys = " keys   = aqueous          active"
                      insertline_regex = r"\+-+\nsolids"
                      insertline = "+--------------------------------------------------------------------\nsolids"
              elif entry["state"] == "cr":
                  formatted_name = fillspace(name, 24) + entry["formula"]
                  tag = tag_vec[entry["name"]]
                  formatted_tag = fillspace(tag, 17)
                  keys = f" keys   = solid            {formatted_tag}active"
                  formatted_V0PrTr = fillspace(str(entry["V"]), 9, spaces_after=False)
                  volume = f"     V0PrTr = {formatted_V0PrTr} cm**3/mol"
                  insertline_regex = r"\+-+\nliquids"
                  insertline = "+--------------------------------------------------------------------\nliquids"
              elif entry["state"] == "gas":
                  formatted_name = name
                  tag = tag_vec[entry["name"]]
                  formatted_tag = fillspace(tag, 17)
                  keys = f" keys   = gas              {formatted_tag}active"
                  volume = "     V0PrTr = 24465.000 cm**3/mol  (source = ideal gas law)"
                  insertline_regex = r"\+-+\nsolid solutions"
                  insertline = "+--------------------------------------------------------------------\nsolid solutions"
              elif entry["state"] == "liq":
                  formatted_name = fillspace(name, 24) + entry["formula"]
                  tag = tag_vec[entry["name"]]
                  formatted_tag = fillspace(tag, 17)
                  keys = f" keys   = liquid           {formatted_tag}active"
                  formatted_V0PrTr = fillspace(str(entry["V"]), 9, spaces_after=False)
                  volume = f"     V0PrTr = {formatted_V0PrTr} cm**3/mol"
                  insertline_regex = r"\+-+\ngases"
                  insertline = "+--------------------------------------------------------------------\ngases"
              else:
                  raise ValueError(f"Error: in {entry['name']} ... {entry['state']} "
                                 "is not a recognized state. Must be aq, gas, cr, liq, or ss.")

              # Append to aq, solid, gas, or liq portion of data0.min template
              output = template_fmt % (formatted_name, date, keys, formatted_charge, volume,
                                      n_elements, element_list, n_species, species_list,
                                      name, logK_list)

              if name == "O2":
                  # Temporary placeholder values that are overwritten with calculated values later
                  # Need DOTALL flag so . matches newlines
                  O2_entry = r"\+-+\nO2\n.*?=O2\n         2.6560    3.0310    3.1080    3.0350\n         2.8740    2.6490    2.3540    1.8830"
                  data0_template = re.sub(O2_entry, output, data0_template, flags=re.DOTALL)
              elif name == "H2O(g)":
                  # Temporary placeholder values that are overwritten with calculated values later
                  # Need DOTALL flag so . matches newlines
                  steam_entry = r"\+-+\nH2O\(g\)\n.*?=H2O\(g\)\n         2.2990    0.9950   -0.0060   -0.6630\n        -1.1560   -1.5340   -1.8290   -2.0630"
                  data0_template = re.sub(steam_entry, output, data0_template, flags=re.DOTALL)
              else:
                  data0_template = re.sub(insertline_regex, output + "\n" + insertline, data0_template)

              vmessage(f"'{name}' processed successfully.", 2, verbose)

          # Handle basis species
          basis_entry_template = ("+--------------------------------------------------------------------\n%s\n"
                                 "    date last revised =  %s\n keys   = basis            active\n"
                                 "     charge  =   %s\n     %s element(s):\n%s")

          # Extract basis list
          if hasattr(dissrxns, 'rx2'):  # R object
              try:
                  basis_list = dissrxns.rx2("basis_list")
                  if hasattr(basis_list, 'names'):  # R StrVector with names
                      basis_items = [str(basis_list.rx2(k)[0]) for k in basis_list.names]
                  else:
                      basis_items = []
              except:
                  basis_items = []
          elif "basis_list" in dissrxns:  # Python dict
              basis_list = dissrxns["basis_list"]
              if hasattr(basis_list, 'items'):  # Dict-like
                  basis_items = list(basis_list.values())
              elif hasattr(basis_list, '__iter__'):  # Iterable
                  basis_items = list(basis_list)
              else:
                  basis_items = []
          else:
              basis_items = []

          # Add promoted auxiliary basis species (for redox suppression)
          if redox_elem_states:
              for species_name, tag in tag_vec.items():
                  if tag == "basis" and species_name not in basis_items:
                      # Check if this was promoted from aux
                      species_row = thermo_df[thermo_df['name'] == species_name]
                      if len(species_row) > 0 and species_row.iloc[0]['tag'] == 'aux':
                          basis_items.append(species_name)
                          vmessage(f"Adding promoted species '{species_name}' to basis section", 1, verbose)

          for basis_item in basis_items:
              # Handle both single values and tuples/lists
              if isinstance(basis_item, (list, tuple)):
                  basis = basis_item[0]
              else:
                  basis = basis_item

              # Skip hard-coded EQ3 basis species
              if basis in fixed_species:
                  continue

              # Get the date on the basis species entry
              try:
                  date_basis = info(info(basis, messages=False), messages=False)["date"]
                  # Handle pandas Series or scalar
                  if hasattr(date_basis, 'iloc'):
                      date_basis = str(date_basis.iloc[0])
                  else:
                      date_basis = str(date_basis)
              except:
                  date_basis = "01-jan-1970"

              # Get the basis species formula
              basis_formula_matches = add_obigt_df[add_obigt_df["name"] == basis]["formula_modded"]
              if len(basis_formula_matches) > 0:
                  basis_formula = basis_formula_matches.iloc[0]
              else:
                  continue

              # Get the elemental makeup of the basis species
              elem_list_basis = makeup(basis_formula)

              # Extract charge from elemental composition
              if "Z" in elem_list_basis:
                  charge_basis = elem_list_basis["Z"]
                  elem_list_basis = {k: v for k, v in elem_list_basis.items() if k != "Z"}
                  formatted_charge_basis = f"{charge_basis:.1f}"
              else:
                  formatted_charge_basis = "0.0"

              # Begin formatting for data0 entry
              n_elem_basis = len(elem_list_basis)
              elem_list_basis_names = list(elem_list_basis.keys())
              elem_list_basis_coefs = [f"{elem_list_basis[k]:.4f}" for k in elem_list_basis_names]
              elem_list_basis_formatted = ""

              for i in range(len(elem_list_basis_names)):
                  if i == 0:
                      sp = "      "
                  else:
                      sp = "              "
                  elem_list_basis_formatted += f"{sp}{elem_list_basis_coefs[i]} {elem_list_basis_names[i]}"

              basis_entry = basis_entry_template % (basis, date_basis, formatted_charge_basis,
                                                    n_elem_basis, elem_list_basis_formatted)

              # Add basis entry to data0.min template
              # Use count=1 to only replace the first occurrence (like R's sub() function)
              basis_insertline_regex = r"\+--------------------------------------------------------------------\nO2\(g\)"
              basis_insertline = "+--------------------------------------------------------------------\nO2(g)"

              data0_template = re.sub(basis_insertline_regex, basis_entry + "\n" + basis_insertline,
                                     data0_template, count=1)

          # Handle elements
          # CRITICAL: Elements must be ordered to match basis species order for EQPT
          # The n-th basis species must contain the n-th element

          # Build ordered element list from basis species
          ordered_elements = []

          # Hardcoded basis species in template: H2O, H+, plhold, O2(g)
          # H2O contains O and H
          ordered_elements.append('O')
          ordered_elements.append('H')
          # plhold contains pl
          ordered_elements.append('pl')

          # Now add elements from basis_items (the basis species we're adding)
          for basis_item in basis_items:
              # Handle both single values and tuples/lists
              if isinstance(basis_item, (list, tuple)):
                  basis = basis_item[0]
              else:
                  basis = basis_item

              # Skip if already in fixed_species (will be handled by template)
              if basis in fixed_species:
                  continue

              # Get the formula for this basis species
              basis_formula_matches = add_obigt_df[add_obigt_df["name"] == basis]["formula_modded"]
              if len(basis_formula_matches) == 0:
                  vmessage(f"Warning: Could not find formula for basis species {basis}", 1, verbose)
                  continue

              basis_formula = basis_formula_matches.iloc[0]

              try:
                  # Get elemental composition
                  elem_makeup = makeup(basis_formula)

                  # Find the non-H-O element (this is the element this basis species represents)
                  elem_for_basis = None
                  for elem in elem_makeup.keys():
                      if elem not in ['H', 'O', 'Z']:
                          elem_for_basis = elem
                          break

                  if elem_for_basis and elem_for_basis not in ordered_elements:
                      ordered_elements.append(elem_for_basis)
              except Exception as e:
                  vmessage(f"Warning: Could not parse formula for {basis}: {e}", 1, verbose)

          # O2(g) is last in template - it uses O which is already in the list
          # So we don't need to add anything for O2(g)

          # Now format the element block
          elem_temp_lines = []
          for elem in ordered_elements:
              try:
                  weight = f"{mass(elem):.5f}".strip()
              except:
                  # Special handling for placeholder element and pseudoelements
                  if elem == 'pl':
                      weight = "1.00000"
                  elif elem.endswith('p') or elem.endswith('n') or elem.endswith('z'):
                      # Pseudoelement - extract the base element name
                      # e.g., Fejiip -> Fe, Fejiiip -> Fe
                      base_elem_match = re.match(r'([A-Z][a-z]?)j', elem)
                      if base_elem_match:
                          base_elem = base_elem_match.group(1)
                          try:
                              weight = f"{mass(base_elem):.5f}".strip()
                          except:
                              weight = "1.00000"
                      else:
                          weight = "1.00000"
                  else:
                      weight = "1.00000"

              # Format: element name, spaces, atomic weight (total 18 chars)
              elem_temp_lines.append(elem + " " * (18 - (len(elem) + len(weight))) + weight)

          # Join the elements back together
          elem_block = "\n".join(elem_temp_lines)

          # Insert the full element block into the data0 template
          data0_template = re.sub(r"elements\n\+-+\n.*\n\+-+\nbasis species",
                                 "elements\n+--------------------------------------------------------------------\n" + elem_block +
                                 "\n+--------------------------------------------------------------------\nbasis species",
                                 data0_template, flags=re.DOTALL)

          vmessage("Handling solid solutions...", 2, verbose)

          # Handle solid solutions
          if solid_solution_df is not None and len(solid_solution_df) > 0:
              for i in range(len(solid_solution_df)):
                  entry = solid_solution_df.iloc[i]
                  name = entry["name"]

                  vmessage(f"Processing {name}", 2, verbose)

                  date = entry["date"]
                  species_name_list = entry["components"].split()
                  nextflag = False

                  # Check if all components are in the database
                  for species in species_name_list:
                      if species not in add_obigt_df["name"].values:
                          vmessage(f"Error encountered when processing the solid solution '{name}': "
                                 f"'{species}' was not found in the data file as a pure mineral. Skipping it...",
                                 1, verbose)
                          nextflag = True
                          break
                      elif species in skipped_species:
                          vmessage(f"Error encountered when processing the solid solution '{name}': "
                                 f"the dissociation reaction for '{species}' contained one or more NAs "
                                 "in its logK grid. Skipping it...", 1, verbose)
                          nextflag = True
                          break

                  if nextflag:
                      continue

                  n_species = len(species_name_list)
                  species_val_list = [1] * n_species
                  nchar = 23  # spaces taken by entries in ss endmember list

                  # Loop through species and format for data0
                  spec_list = []
                  for j in range(n_species):
                      # Get species value and name
                      species_val = f"{species_val_list[j]:.4f}"
                      species_name = species_name_list[j]

                      # Conditional formatting based on position (0-indexed, so even=line start, odd=line end)
                      if j % 2 == 0:  # Even index: first entry of a line (left column)
                          max_length = 7
                          end_char = ""
                          species_name = fillspace(species_name, nchar)
                      elif j % 2 == 1 and j != n_species - 1:  # Odd index: second entry of a line (right column), not last
                          max_length = 8
                          end_char = "\n"
                      else:  # Odd index but last entry
                          max_length = 8
                          end_char = ""
                          species_name = fillspace(species_name, nchar)

                      # Paste together value and element name
                      pasted_entry = f"{species_val}  {species_name}"

                      # Get decimal position and format spaces accordingly
                      # Note: index() returns 0-based position, but R's gregexpr returns 1-based
                      # To match R behavior, add 1 to convert to 1-based indexing
                      decimal_position = pasted_entry.index(".") + 1
                      pasted_entry = " " * (max_length - decimal_position) + pasted_entry + end_char

                      # Add entry to element list
                      spec_list.append(pasted_entry)

                  species_list = "".join(spec_list)
                  n_species = str(n_species)

                  ss_template = ("+--------------------------------------------------------------------\n%s\n"
                                "    date last revised = %s\n%s\n  %s components\n%s\n type   = %s\n%s\n%s")

                  model_param_list = [entry[f"mp{i}"] for i in range(1, 7)]
                  site_param_list = [entry[f"sp{i}"] for i in range(1, 7)]

                  model_param_list = " ".join([s_d(p, 3) for p in model_param_list])
                  site_param_list = " ".join([s_d(p, 3) for p in site_param_list])

                  if float(entry["n_model_params"]) > 0:
                      model_param_template = f"  {entry['n_model_params']} model parameters\n{model_param_list}"
                  else:
                      model_param_template = "  0 model parameters"

                  if float(entry["n_site_params"]) > 0:
                      site_param_template = f"  {entry['n_site_params']} site parameters\n{site_param_list}"
                  else:
                      site_param_template = "  0 site parameters"

                  # Format solid solution entry
                  formatted_name = fillspace(name, 24) + entry["formula"]
                  tag = entry["tag"]
                  formatted_tag = fillspace(tag, 17)
                  keys = f" keys   = ss               {formatted_tag}active"

                  insertline_regex = r"\+-+\nreferences"
                  insertline = "+--------------------------------------------------------------------\nreferences"

                  # Create output for solid solution entries
                  output = ss_template % (formatted_name, date, keys, n_species, species_list,
                                         entry["type"], model_param_template, site_param_template)

                  data0_template = re.sub(insertline_regex, output + "\n" + insertline, data0_template)
          else:
              vmessage("No solid solutions supplied. Moving on...", 2, verbose)

          def add_bdot_line(spec_name, spec_azero, neutral_ion_type):
              """Add a bdot parameter line to the data0 template"""
              nonlocal data0_template

              # Handle NaN values in neutral_ion_type
              if pd.isna(neutral_ion_type):
                  neutral_ion_type_str = ""
              else:
                  neutral_ion_type_str = str(int(neutral_ion_type))

              # Format: species_name + variable spaces + azero (total width 38) + 4 spaces + neutral_ion_type (right-aligned in 2 chars)
              # This accommodates azero values from 1-99 (e.g., "9.0000" is 6 chars, "11.0000" is 7 chars)
              # Calculate spacing: len(name) + spaces + len(azero) = 38
              spaces_needed = 38 - len(spec_name) - len(spec_azero)
              name_and_azero = spec_name + " " * spaces_needed + spec_azero
              neutral_ion_type_part = neutral_ion_type_str.rjust(2)

              bdot_entry = name_and_azero + "    " + neutral_ion_type_part

              # Add bdot entry to data0.min template
              bdot_insertline_regex = r"\+--------------------------------------------------------------------\nelements"
              bdot_insertline = "+--------------------------------------------------------------------\nelements"
              data0_template = re.sub(bdot_insertline_regex, bdot_entry + "\n" + bdot_insertline,
                                     data0_template)

          # Format basis and non-basis species for bdot parameter section
          if water_model in ["SUPCRT92", "IAPWS95"]:
              # azero and neutral ion type parameter defaults for fixed species
              Cl_azero = 3
              O2_azero = 3
              O2g_azero = 3
              OH_azero = 3
              H2O_azero = 3
              H_azero = 9
              Cl_neutral_ion_type = 0
              O2_neutral_ion_type = -1
              O2g_neutral_ion_type = 0
              H2O_neutral_ion_type = 0
              H_neutral_ion_type = 0
          elif water_model == "DEW":
              Cl_azero = 3.7
              O2_azero = -0.5
              O2g_azero = 0
              OH_azero = 3.7
              H2O_azero = 0
              H_azero = 3.7
              Cl_neutral_ion_type = 0
              O2_neutral_ion_type = 0
              O2g_neutral_ion_type = 0
              H2O_neutral_ion_type = 0
              H_neutral_ion_type = 0

          # Add fixed species to bdot section
          for sp in ["Cl-", "O2", "O2(g)", "H2O", "H+"]:
              if sp == "Cl-":
                  spec_name = "Cl-"
                  spec_azero = f"{Cl_azero:.4f}"
                  neutral_ion_type = Cl_neutral_ion_type
              elif sp == "O2":
                  spec_name = "O2"
                  spec_azero = f"{O2_azero:.4f}"
                  neutral_ion_type = O2_neutral_ion_type
              elif sp == "O2(g)":
                  spec_name = "O2(g)"
                  spec_azero = f"{O2g_azero:.4f}"
                  neutral_ion_type = O2g_neutral_ion_type
              elif sp == "H2O":
                  spec_name = "H2O"
                  spec_azero = f"{H2O_azero:.4f}"
                  neutral_ion_type = H2O_neutral_ion_type
              elif sp == "H+":
                  spec_name = "H+"
                  spec_azero = f"{H_azero:.4f}"
                  neutral_ion_type = H_neutral_ion_type

              add_bdot_line(spec_name, spec_azero, neutral_ion_type)

          # Add all aqueous species to bdot section
          for i in range(len(add_obigt_df)):
              row = add_obigt_df.iloc[i]
              if row["name"] in ["Cl-", "O2", "H2O", "H+"]:
                  continue
              elif row["state"] == "aq":
                  spec_name = row["name"]
                  spec_azero_val = azero_vec[row["name"]]

                  if pd.isna(spec_azero_val) or np.isnan(spec_azero_val):
                      if row["z.T"] != 0:
                          if water_model in ["SUPCRT92", "IAPWS95"]:
                              spec_azero = "4.0000"
                          elif water_model == "DEW":
                              spec_azero = "3.7000"
                      else:
                          if water_model in ["SUPCRT92", "IAPWS95"]:
                              spec_azero = "0.0000"
                          elif water_model == "DEW":
                              spec_azero = "-0.5000"
                  else:
                      spec_azero = f"{spec_azero_val:.4f}"

                  neutral_ion_type = neutral_ion_type_vec[row["name"]]
                  add_bdot_line(spec_name, spec_azero, neutral_ion_type)

          return data0_template

  Main function to create a data0 file from thermodynamic data.

  ## Parameters

  `thermo_df` :`DataFrame`
    Thermodynamic database
  `element_df` :`DataFrame`, optional
    Element database (not currently used, kept for compatibility)
  `solid_solution_df` :`DataFrame`, optional
    Solid solution database
  `db` :`str`
    Three-letter database code
  `water_model` :`str`
    Water model to use (SUPCRT92, IAPWS95, or DEW)
  `template` :`str`
    data0.min template content
  `dissrxns` :`dict`
    Dissociation reactions dictionary with 'basis_list' key
  `basis_pref` :`list`, optional
    Basis preference list (not currently used)
  `exceed_Ttr` :`bool`
    Whether to exceed transition temperatures
  `fixed_species` :`list`
    List of fixed species (H2O, H+, O2(g), water, Cl-, e-)
  `verbose` :`int`
    Verbosity level (0, 1, or 2)

  ## Returns

  `str`
    Formatted data0 file content
`def fillspace(string, nspaces, spaces_after=True)`
  Expand source code

      def fillspace(string, nspaces, spaces_after=True):
          """
          Create a string of a certain length by adding additional spaces.
          e.g., "H2O" becomes "H2O    " if nspaces=7.
          Spaces can be added before the string by specifying spaces_after=False
          """
          try:
              if spaces_after:
                  out = string + " " * (nspaces - len(string))
              else:
                  out = " " * (nspaces - len(string)) + string
          except:
              out = ""
          return out

  Create a string of a certain length by adding additional spaces. e.g., "H2O" becomes "H2O " if nspaces=7. Spaces can be added before the string by specifying spaces_after=False

`def s_d(x, k)`
  Expand source code

      def s_d(x, k):
          """
          Specify how many decimals are printed.
          e.g. 12.433 becomes "12.4330" if k=4
          """
          return f"{float(x):.{k}f}".strip()

  Specify how many decimals are printed. e.g. 12.433 becomes "12.4330" if k=4

`def vmessage(m, vlevel, verbose)`
  Expand source code

      def vmessage(m, vlevel, verbose):
          """Print messages if 'verbose' setting >= vlevel of message."""
          if verbose >= vlevel:
              print(m)

  Print messages if 'verbose' setting >= vlevel of message.