///|
pub fn celsius_to_kelvin(temperature_c : Double) -> Double {
  temperature_c + 273.15
}

///|
pub fn kelvin_to_celsius(temperature_k : Double) -> Double {
  temperature_k - 273.15
}

///|
pub fn bar_to_kpa(pressure_bar : Double) -> Double {
  pressure_bar * 100.0
}

///|
pub fn kpa_to_bar(pressure_kpa : Double) -> Double {
  pressure_kpa / 100.0
}

///|
pub fn bar_to_pascal(pressure_bar : Double) -> Double {
  pressure_bar * 100000.0
}

///|
pub fn pascal_to_bar(pressure_pa : Double) -> Double {
  pressure_pa / 100000.0
}

///|
pub fn mmhg_to_bar(pressure_mmhg : Double) -> Double {
  pressure_mmhg * 0.001333223684
}

///|
pub fn bar_to_mmhg(pressure_bar : Double) -> Double {
  pressure_bar / 0.001333223684
}

///|
pub fn atm_to_bar(pressure_atm : Double) -> Double {
  pressure_atm * 1.01325
}

///|
pub fn bar_to_atm(pressure_bar : Double) -> Double {
  pressure_bar / 1.01325
}

///|
pub fn joule_to_kilojoule(energy_j : Double) -> Double {
  energy_j / 1000.0
}

///|
pub fn kilojoule_to_joule(energy_kj : Double) -> Double {
  energy_kj * 1000.0
}

///|
pub fn watt_to_joule_per_second(power_w : Double) -> Double {
  power_w
}

///|
pub fn validate_molecular_weights(
  weights : Array[Double],
) -> Unit raise VleError {
  if weights.length() == 0 {
    raise VleError::EmptyMixture
  }
  for weight in weights {
    if weight <= 0.0 {
      raise VleError::InvalidParameter("molecular weights must be positive")
    }
  }
}

///|
pub fn mixture_molecular_weight(
  mole_fractions : Array[Double],
  molecular_weights : Array[Double],
) -> Double raise VleError {
  assert_same_length(mole_fractions.length(), molecular_weights.length())
  validate_molecular_weights(molecular_weights)
  let x = normalize(mole_fractions)
  for i = 0, value = 0.0; i < x.length(); {
    continue i + 1, value + x[i] * molecular_weights[i]
  } nobreak {
    value
  }
}

///|
pub fn molar_to_mass_fractions(
  mole_fractions : Array[Double],
  molecular_weights : Array[Double],
) -> Array[Double] raise VleError {
  assert_same_length(mole_fractions.length(), molecular_weights.length())
  validate_molecular_weights(molecular_weights)
  let x = normalize(mole_fractions)
  normalize([ for i in 0.. x[i] * molecular_weights[i] ])
}

///|
pub fn mass_to_molar_fractions(
  mass_fractions : Array[Double],
  molecular_weights : Array[Double],
) -> Array[Double] raise VleError {
  assert_same_length(mass_fractions.length(), molecular_weights.length())
  validate_molecular_weights(molecular_weights)
  let w = normalize(mass_fractions)
  normalize([ for i in 0.. w[i] / molecular_weights[i] ])
}

///|
pub fn molar_flow_to_mass_flow(
  molar_flow_mol_per_s : Double,
  molecular_weight_g_per_mol : Double,
) -> Double raise VleError {
  if molar_flow_mol_per_s < 0.0 || molecular_weight_g_per_mol <= 0.0 {
    raise VleError::InvalidParameter("flow and molecular weight are invalid")
  }
  molar_flow_mol_per_s * molecular_weight_g_per_mol / 1000.0
}

///|
pub fn mass_flow_to_molar_flow(
  mass_flow_kg_per_s : Double,
  molecular_weight_g_per_mol : Double,
) -> Double raise VleError {
  if mass_flow_kg_per_s < 0.0 || molecular_weight_g_per_mol <= 0.0 {
    raise VleError::InvalidParameter("flow and molecular weight are invalid")
  }
  mass_flow_kg_per_s * 1000.0 / molecular_weight_g_per_mol
}

///|
pub fn mole_fractions_to_partial_pressures(
  mole_fractions : Array[Double],
  pressure_bar : Double,
) -> Array[Double] raise VleError {
  assert_pressure(pressure_bar)
  let x = normalize(mole_fractions)
  [
    for value in x => value * pressure_bar
  ]
}

///|
pub fn partial_pressures_to_mole_fractions(
  partial_pressures_bar : Array[Double],
) -> Array[Double] raise VleError {
  for pressure in partial_pressures_bar {
    if pressure < 0.0 {
      raise VleError::InvalidParameter("partial pressure cannot be negative")
    }
  }
  normalize(partial_pressures_bar)
}

///|
pub fn composition_entropy(
  composition : Array[Double],
) -> Double raise VleError {
  let x = normalize(composition)
  for i = 0, accumulator = 0.0; i < x.length(); {
    let safe = clamp_positive(x[i])
    continue i + 1, accumulator - safe * @math.ln(safe)
  } nobreak {
    accumulator
  }
}

///|
pub fn composition_l1_distance(
  first : Array[Double],
  second : Array[Double],
) -> Double raise VleError {
  assert_same_length(first.length(), second.length())
  let a = normalize(first)
  let b = normalize(second)
  for i = 0, distance = 0.0; i < a.length(); {
    continue i + 1, distance + abs_double(a[i] - b[i])
  } nobreak {
    distance
  }
}

///|
pub fn composition_l2_distance(
  first : Array[Double],
  second : Array[Double],
) -> Double raise VleError {
  vector_distance(first, second)
}

///|
pub fn argmax_component(values : Array[Double]) -> Int raise VleError {
  if values.length() == 0 {
    raise VleError::EmptyMixture
  }
  for i = 1, index = 0; i < values.length(); {
    continue i + 1, if values[i] > values[index] { i } else { index }
  } nobreak {
    index
  }
}

///|
pub fn argmin_component(values : Array[Double]) -> Int raise VleError {
  if values.length() == 0 {
    raise VleError::EmptyMixture
  }
  for i = 1, index = 0; i < values.length(); {
    continue i + 1, if values[i] < values[index] { i } else { index }
  } nobreak {
    index
  }
}

///|
pub fn composition_maximum(
  composition : Array[Double],
) -> Double raise VleError {
  let x = normalize(composition)
  x[argmax_component(x)]
}

///|
pub fn composition_minimum(
  composition : Array[Double],
) -> Double raise VleError {
  let x = normalize(composition)
  x[argmin_component(x)]
}

///|
pub fn blend_compositions(
  first : Array[Double],
  first_amount : Double,
  second : Array[Double],
  second_amount : Double,
) -> Array[Double] raise VleError {
  if first_amount < 0.0 ||
    second_amount < 0.0 ||
    first_amount + second_amount <= 0.0 {
    raise VleError::InvalidParameter(
      "blend amounts must be nonnegative and nonzero",
    )
  }
  assert_same_length(first.length(), second.length())
  let x = normalize(first)
  let y = normalize(second)
  normalize(
    [
      for i in 0.. first_amount * x[i] + second_amount * y[i]
    ],
  )
}

///|
pub fn scale_composition(
  composition : Array[Double],
  scale : Double,
) -> Array[Double] raise VleError {
  if scale < 0.0 {
    raise VleError::InvalidParameter("composition scale cannot be negative")
  }
  let x = normalize(composition)
  [
    for value in x => value * scale
  ]
}

///|
pub fn composition_is_valid(
  composition : Array[Double],
  tolerance : Double,
) -> Bool {
  if composition.length() == 0 || tolerance <= 0.0 {
    return false
  }
  for value in composition {
    if value < 0.0 {
      return false
    }
  }
  let total = for i = 0, accumulator = 0.0; i < composition.length(); {
    continue i + 1, accumulator + composition[i]
  } nobreak {
    accumulator
  }
  abs_double(total - 1.0) <= tolerance
}

///|
pub fn composition_normalization_error(composition : Array[Double]) -> Double {
  abs_double(sum(composition) - 1.0)
}

///|
pub fn componentwise_product(
  first : Array[Double],
  second : Array[Double],
) -> Array[Double] raise VleError {
  assert_same_length(first.length(), second.length())
  [
    for i in 0.. first[i] * second[i]
  ]
}

///|
pub fn componentwise_quotient(
  numerator : Array[Double],
  denominator : Array[Double],
) -> Array[Double] raise VleError {
  assert_same_length(numerator.length(), denominator.length())
  [
    for i in 0.. {
      if denominator[i] == 0.0 {
        raise VleError::InvalidParameter("component denominator cannot be zero")
      }
      numerator[i] / denominator[i]
    }
  ]
}

///|
pub fn weighted_average(
  values : Array[Double],
  weights : Array[Double],
) -> Double raise VleError {
  assert_same_length(values.length(), weights.length())
  let w = normalize(weights)
  for i = 0, value = 0.0; i < values.length(); {
    continue i + 1, value + values[i] * w[i]
  } nobreak {
    value
  }
}