import pint
import time
import serial
from rdkit.Chem import MolFromSmiles, MolToSmiles
from robotics_api.settings import *
[docs]
def sig_figs(number: float or str, num_sig_figs=5):
"""
Round a number to the specified number of significant figures.
Args:
number (float): The number to round.
num_sig_figs (int): The number of significant figures to retain.
Returns:
float: The number rounded to the given significant figures.
"""
if isinstance(number, str):
ureg = pint.UnitRegistry()
number = ureg(number).magnitude
if number == 0:
return 0 # Zero remains zero regardless of sig figs
if num_sig_figs <= 0:
raise ValueError("Number of significant figures must be greater than zero.")
import math
# Calculate the order of magnitude
magnitude = math.floor(math.log10(abs(number)))
# Scale the number to round at the desired decimal place
scale = 10 ** (magnitude - num_sig_figs + 1)
# Scale the number, round it, and scale it back
rounded = round(number / scale) * scale
# Avoid floating-point representation issues by formatting
return float(f"{rounded:.{num_sig_figs - 1}e}")
[docs]
def is_mass_unit(unit):
# Check if a unit is a unit of mass
ureg = pint.UnitRegistry()
if isinstance(unit, pint.Unit):
ureg_unit = unit
elif isinstance(unit, str):
ureg_unit = ureg(unit)
else:
raise ValueError(f"Cannot determine if datatype {type(unit)} is a mass unit. ")
return ureg_unit.dimensionality == ureg.kilogram.dimensionality
[docs]
def unit_conversion(measurement, default_unit: str, density=None, return_dict=False):
"""
Convert a measurement into a default unit using pint.
:param measurement: Measurements can be pint object, int or float(in which case it will be assumed to already be in the default unit), string of magnitude and unit, or a measurement dictionary (EX: {"value": 0.5, "unit": "eV"}
:param default_unit: default unit / unit to be converted to
:param return_dict:
:param density: molecular density (in case needed for conversion)
:return: float magnitude for the converted measurement
"""
if measurement is None:
return None
# Set context in case conversion include mass-->volume or volume-->mass
ureg = pint.UnitRegistry()
c = pint.Context('mol_density')
if density:
c.add_transformation('[mass]', '[volume]', lambda ureg_c, x: x / ureg_c(density))
c.add_transformation('[volume]', '[mass]', lambda ureg_c, x: x * ureg_c(density))
ureg.add_context(c)
# Get measurement value and unit
if not isinstance(measurement, (str, float, int, dict)):
value, unit = getattr(measurement, "magnitude"), getattr(measurement, "units")
else:
value = measurement.get("value") if isinstance(measurement, dict) else measurement
unit = ""
if isinstance(value, float) or str(value).replace('.', '', 1).replace('-', '', 1).isdigit():
unit = measurement.get("unit", default_unit) if isinstance(measurement, dict) else default_unit
# Convert measurement to default unit
unit = default_unit if unit == "dimensionless" else unit
pint_unit = ureg("{}{}".format(value, unit))
if return_dict:
return {"value": pint_unit.to(default_unit, 'mol_density').magnitude, "unit": default_unit}
return pint_unit.to(default_unit, 'mol_density').magnitude
[docs]
def write_test(file_path, test_type=""):
"""
Writes test data to a file based on the test type.
Args:
file_path (str): Path to the output file.
test_type (str): Type of test data to write. Options are "cv", "ca", or "ircomp".
"""
test_files = {
"cv": os.path.join(TEST_DATA_DIR, "standard_data", "CV.txt"),
"cvUM": os.path.join(TEST_DATA_DIR, "standard_data", "CVUM.txt"),
"ca": os.path.join(TEST_DATA_DIR, "standard_data", "CA.txt"),
"ircomp": os.path.join(TEST_DATA_DIR, "standard_data", "iRComp.txt"),
}
test_fn = test_files.get(test_type.lower())
if os.path.isfile(test_fn):
with open(test_fn, 'r') as fn:
test_text = fn.read()
else:
test_text = "test"
with open(file_path, 'w+') as fn:
fn.write(test_text)
[docs]
def send_arduino_cmd(station: str, command: str or float, address: str = ARDUINO_PORT, return_txt: bool = False):
"""
Sends a command to the Arduino controlling a specific station.
Args:
station (str): The station identifier (e.g., "E1", "P1").
command (str or float): The command to send (e.g., "0", "1", "500").
address (str): Address of the Arduino port (default is ARDUINO_PORT).
return_txt (bool): Whether to return the Arduino response text (default is False).
Returns:
bool or str: True if the command succeeded, the response text if return_txt is True, otherwise False on failure.
Raises:
Exception: If unable to connect to the Arduino.
"""
try:
arduino = serial.Serial(address, 115200, timeout=.1)
except:
try:
time.sleep(20)
arduino = serial.Serial(address, 115200, timeout=.1)
except:
raise Exception("Warning! {} is not connected".format(address))
time.sleep(1) # give the connection a second to settle
arduino.write(bytes(f"{station}_{command}", encoding='utf-8')) # EX: E1_0 or P1_1_500
print("Command {} given to station {} at {} via Arduino.".format(command, station, address))
start_time = time.time()
try:
while True:
print("trying to read...")
data = arduino.readline()
print("waiting for {} arduino results for {:.1f} seconds...".format(station, time.time() - start_time))
if data:
result_txt = data.decode().strip() # strip out the old lines
print("ARDUINO RESULT: ", result_txt)
if "success" in result_txt:
return result_txt if return_txt else True
elif "failure" in result_txt:
return False
time.sleep(1)
except KeyboardInterrupt:
arduino.write(bytes(f"ABORT_{station}", encoding='utf-8'))
while True:
print("Aborted arduino. Trying to read abort message...")
data = arduino.readline()
if data:
print("ARDUINO ABORT MESSAGE: ", data.decode().strip()) # strip out the old lines
raise KeyboardInterrupt
time.sleep(1)
[docs]
def get_testing_mass(init_mass, added_volume, soln_density=TEST_SOLN_DENSITY, adding=True):
init_mass_g = unit_conversion(init_mass, default_unit="g")
added_vol_L = unit_conversion(added_volume, default_unit="L")
soln_density_g_L = unit_conversion(soln_density, default_unit="g/L")
mass_change = added_vol_L * soln_density_g_L
return init_mass_g + mass_change if adding else init_mass_g - mass_change
[docs]
def rdkit_smiles(smiles):
try:
return MolToSmiles(MolFromSmiles(smiles))
except Exception:
return smiles