diff --git a/ohmpi/config.py b/ohmpi/config.py index 1c889c6385dbd4ba2d84c32300df5c5e2a70564f..411cad0432beea238964192e620b155581d159bf 100644 --- a/ohmpi/config.py +++ b/ohmpi/config.py @@ -17,58 +17,49 @@ OHMPI_CONFIG = { } r_shunt = 2. - -# default properties of system components that will be -# overwritten by properties defined in each the board dict below. -# if bounds are defined in board specs, values out of specs will be bounded to remain in specs -# omitted properties in config will be set to board specs default values if they exist - HARDWARE_CONFIG = { 'ctl': {'model': 'raspberry_pi'}, 'pwr': {'model': 'pwr_batt', 'voltage': 12., 'interface_name': 'none'}, 'tx': {'model': 'mb_2024_0_2', - 'voltage_max': 50., # Maximum voltage supported by the TX board [V] - 'current_max': 4.80/(50*r_shunt), # Maximum voltage read by the current ADC on the TX board [A] - 'r_shunt': r_shunt, # Shunt resistance in Ohms - 'interface_name': 'i2c', - 'vmn_hardware_offset': 2500. - }, + 'voltage_max': 50., # Maximum voltage supported by the TX board [V] + 'current_max': 4.80/(50*r_shunt), # Maximum voltage read by the current ADC on the TX board [A] + 'r_shunt': r_shunt, # Shunt resistance in Ohms + 'interface_name': 'i2c' + }, 'rx': {'model': 'mb_2024_0_2', - 'latency': 0.010, # latency in seconds in continuous mode - 'sampling_rate': 50, # number of samples per second - 'interface_name': 'i2c', - }, + 'latency': 0.010, # latency in seconds in continuous mode + 'sampling_rate': 200, # number of samples per second + 'interface_name': 'i2c' + }, 'mux': {'boards': - {'mux_A': - {'model': 'mux_2023_0_X', - 'mux_tca_address': 0x70, - 'roles': 'A', - 'electrodes': range(1, 65), - }, + {'mux_A': + {'model': 'mux_2023_0_X', + 'mux_tca_address': 0x70, + 'roles': 'A', + 'electrodes': range(1, 65)}, 'mux_B': - {'model': 'mux_2023_0_X', - 'mux_tca_address': 0x71, - 'roles': 'B', - 'electrodes': range(1, 65), - }, + {'model': 'mux_2023_0_X', + 'mux_tca_address': 0x71, + 'roles': 'B', + 'electrodes': range(1, 65)}, 'mux_M': - {'model': 'mux_2023_0_X', - 'mux_tca_address': 0x72, - 'roles': 'M', - 'electrodes': range(1, 65), - }, + {'model': 'mux_2023_0_X', + 'mux_tca_address': 0x72, + 'roles': 'M', + 'electrodes': range(1, 65)}, 'mux_N': - {'model': 'mux_2023_0_X', - 'mux_tca_address': 0x73, - 'roles': 'N', - 'electrodes': range(1, 65), - } + {'model': 'mux_2023_0_X', + 'mux_tca_address': 0x73, + 'roles': 'N', + 'electrodes': range(1, 65), + } }, - 'default': {'interface_name': 'i2c', - 'voltage_max': 50., - 'current_max': 3.} - } -} + 'default': {'interface_name': 'i2c_ext', + 'voltage_max': 50., + 'current_max': 3.} + } + } + # SET THE LOGGING LEVELS, MQTT BROKERS AND MQTT OPTIONS ACCORDING TO YOUR NEEDS # Execution logging configuration EXEC_LOGGING_CONFIG = { @@ -97,7 +88,7 @@ DATA_LOGGING_CONFIG = { SOH_LOGGING_CONFIG = { 'logging_level': logging.INFO, 'logging_to_console': True, - 'log_file_logging_level': logging.INFO, + 'log_file_logging_level': logging.DEBUG, 'file_name': f'soh{logging_suffix}.log', 'max_bytes': 16777216, 'backup_count': 1024, diff --git a/ohmpi/hardware_components/abstract_hardware_components.py b/ohmpi/hardware_components/abstract_hardware_components.py index d79142540f3d32375e796b1e3dfbd5e00c237389..439ce114a4d1463ba4e2041fe2dfd517473cba4a 100644 --- a/ohmpi/hardware_components/abstract_hardware_components.py +++ b/ohmpi/hardware_components/abstract_hardware_components.py @@ -463,6 +463,17 @@ class TxAbstract(ABC): def measuring(self, mode="off"): self._measuring = mode + def discharge_pwr(self, latency=None): + if self.pwr.voltage_adjustable: + if latency is None: + latency = self.pwr._pwr_discharge_latency + self.exec_logger.debug(f'Pwr discharge initiated for {latency} s') + + time.sleep(latency) + + else: + self.exec_logger.debug(f'Pwr discharge not supported by {self.pwr.model}') + @property def polarity(self): return self._polarity diff --git a/ohmpi/hardware_components/mb_2024_0_2.py b/ohmpi/hardware_components/mb_2024_0_2.py index 104a47503ecc6fc893063ed62199cf5d7800d0f2..036c74c89fb8534ad41624ba7049d810a27cebe1 100644 --- a/ohmpi/hardware_components/mb_2024_0_2.py +++ b/ohmpi/hardware_components/mb_2024_0_2.py @@ -36,7 +36,6 @@ SPECS = {'rx': {'model': {'default': os.path.basename(__file__).rstrip('.py')}, 'r_shunt': {'min': 0.001, 'default': 2.}, 'activation_delay': {'default': 0.010}, # Max turn on time of OMRON G5LE-1 5VDC relays 'release_delay': {'default': 0.005}, # Max turn off time of OMRON G5LE-1 5VDC relays = 1ms - 'pwr_latency': {'default': 4.} }} # TODO: move low_battery spec in pwr @@ -80,8 +79,7 @@ class Tx(Tx_mb_2023): super().__init__(**kwargs) if not subclass_init: self.exec_logger.event(f'{self.model}\ttx_init\tbegin\t{datetime.datetime.utcnow()}') - self._pwr_latency = kwargs['pwr_latency'] - self._current = 0 + # Initialize LEDs if self.connect: self.pin4 = self.mcp_board.get_pin(4) # OhmPi_run @@ -117,6 +115,13 @@ class Tx(Tx_mb_2023): elif mode == "off": self.pin5.value = False + def discharge_pwr(self, latency=None): + if latency is None: + latency = self.pwr._pwr_discharge_latency + + time.sleep(latency) + + def inject(self, polarity=1, injection_duration=None): # add leds? self.pin6.value = True @@ -141,7 +146,8 @@ class Tx(Tx_mb_2023): self.pin3.value = True self.exec_logger.debug(f'Switching DPS on') self._pwr_state = 'on' - time.sleep(self._pwr_latency) # from pwr specs + time.sleep(self.pwr._pwr_latency) # from pwr specs + elif state == 'off': self.pin2.value = False self.pin3.value = False diff --git a/ohmpi/hardware_components/mb_2024_1_X.py b/ohmpi/hardware_components/mb_2024_1_X.py new file mode 100644 index 0000000000000000000000000000000000000000..93fd1fd1a29ea9bfb950eeec31b4b4b4bd49e20b --- /dev/null +++ b/ohmpi/hardware_components/mb_2024_1_X.py @@ -0,0 +1,127 @@ +import datetime +import adafruit_ads1x15.ads1115 as ads # noqa +from adafruit_ads1x15.analog_in import AnalogIn # noqa +from adafruit_ads1x15.ads1x15 import Mode # noqa +from adafruit_mcp230xx.mcp23008 import MCP23008 # noqa +from digitalio import Direction # noqa +from busio import I2C # noqa +import os +import time +from ohmpi.utils import enforce_specs +from ohmpi.hardware_components.mb_2024_0_2 import Tx as Tx_mb_2024_0_2 +from ohmpi.hardware_components.mb_2024_0_2 import Rx as Rx_mb_2024_0_2 + +# hardware characteristics and limitations +# voltages are given in mV, currents in mA, sampling rates in Hz and data_rate in S/s +SPECS = {'rx': {'model': {'default': os.path.basename(__file__).rstrip('.py')}, + 'sampling_rate': {'min': 0., 'default': 100., 'max': 500.}, + 'data_rate': {'default': 860.}, + 'bias': {'min': -5000., 'default': 0., 'max': 5000.}, + 'coef_p2': {'default': 1.00}, + 'mcp_address': {'default': 0x27}, + 'ads_address': {'default': 0x49}, + 'voltage_min': {'default': 10.0}, + 'voltage_max': {'default': 5000.0}, # [mV] + 'dg411_gain_ratio': {'default': 1/2}, # lowest resistor value over sum of resistor values + 'vmn_hardware_offset': {'default': 2500.}, + }, + 'tx': {'model': {'default': os.path.basename(__file__).rstrip('.py')}, + 'adc_voltage_min': {'default': 10.}, # Minimum voltage value used in vmin strategy + 'adc_voltage_max': {'default': 4500.}, # Maximum voltage on ads1115 used to measure current + 'voltage_max': {'min': 0., 'default': 12., 'max': 50.}, # Maximum input voltage + 'data_rate': {'default': 860.}, + 'mcp_address': {'default': 0x21}, + 'ads_address': {'default': 0x48}, + 'compatible_power_sources': {'default': ['pwr_batt', 'dps5005']}, + 'r_shunt': {'min': 0.001, 'default': 2.}, + 'activation_delay': {'default': 0.010}, # Max turn on time of OMRON G5LE-1 5VDC relays + 'release_delay': {'default': 0.005}, # Max turn off time of OMRON G5LE-1 5VDC relays = 1ms + }} + +# TODO: move low_battery spec in pwr + + +def _ads_1115_gain_auto(channel): # Make it a class method ? + """Automatically sets the gain on a channel + + Parameters + ---------- + channel : ads.ADS1x15 + Instance of ADS where voltage is measured. + + Returns + ------- + gain : float + Gain to be applied on ADS1115. + """ + + gain = 2 / 3 + if (abs(channel.voltage) < 2.048) and (abs(channel.voltage) >= 1.024): + gain = 2 + elif (abs(channel.voltage) < 1.024) and (abs(channel.voltage) >= 0.512): + gain = 4 + elif (abs(channel.voltage) < 0.512) and (abs(channel.voltage) >= 0.256): + gain = 8 + elif abs(channel.voltage) < 0.256: + gain = 16 + return gain + + +class Tx(Tx_mb_2024_0_2): + """TX Class""" + def __init__(self, **kwargs): + if 'model' not in kwargs.keys(): + for key in SPECS['tx'].keys(): + kwargs = enforce_specs(kwargs, SPECS['tx'], key) + subclass_init = False + else: + subclass_init = True + super().__init__(**kwargs) + if not subclass_init: + self.exec_logger.event(f'{self.model}\ttx_init\tbegin\t{datetime.datetime.utcnow()}') + + self.pin5 = self.mcp_board.get_pin(5) # power_discharge_relay + self.pin5.direction = Direction.OUTPUT + self.pin5.value = False + + if not subclass_init: + self.exec_logger.event(f'{self.model}\ttx_init\tend\t{datetime.datetime.utcnow()}') + + @property + def measuring(self): + return self._measuring + + @measuring.setter + def measuring(self, mode="off"): + self._measuring = mode + + + def discharge_pwr(self, latency=None): + if self.pwr.voltage_adjustable: + if latency is None: + latency = self.pwr._pwr_discharge_latency + self.exec_logger.debug(f'Pwr discharge initiated for {latency} s') + + self.exec_logger.event(f'{self.model}\tpwr_discharge\tend\t{datetime.datetime.utcnow()}') + self.pin5.value = True + time.sleep(self._activation_delay) + + time.sleep(latency) + + if self.pwr.voltage_adjustable: + self.pin5.value = False + time.sleep(self._release_delay) + self.exec_logger.event(f'{self.model}\tpwr_discharge\tend\t{datetime.datetime.utcnow()}') + else: + self.exec_logger.debug(f'Pwr discharge not supported by {self.pwr.model}') + +class Rx(Rx_mb_2024_0_2): + """RX Class""" + def __init__(self, **kwargs): + if 'model' not in kwargs.keys(): + for key in SPECS['rx'].keys(): + kwargs = enforce_specs(kwargs, SPECS['rx'], key) + subclass_init = False + else: + subclass_init = True + super().__init__(**kwargs) diff --git a/ohmpi/hardware_components/pwr_dps5005.py b/ohmpi/hardware_components/pwr_dps5005.py index fb975984d66d00b9ff22984a87eed5855e0e46b1..edbef03a5b46e0c2e2f0f9aa16ad2894e9823f95 100644 --- a/ohmpi/hardware_components/pwr_dps5005.py +++ b/ohmpi/hardware_components/pwr_dps5005.py @@ -16,7 +16,8 @@ SPECS = {'model': {'default': os.path.basename(__file__).rstrip('.py')}, 'current_max_tolerance': {'default': 20}, # in % 'current_adjustable': {'default': False}, 'voltage_adjustable': {'default': True}, - 'pwr_latency': {'default': 0.} + 'pwr_latency': {'default': 4.}, + 'pwr_discharge_latency': {'default': 1.} } @@ -50,8 +51,9 @@ class Pwr(PwrAbstract): self.voltage_adjustable = True self.current_adjustable = False self._current = np.nan - + self._pwr_state = 'off' self._pwr_latency = kwargs['pwr_latency'] + self._pwr_discharge_latency = kwargs['pwr_discharge_latency'] if not subclass_init: self.exec_logger.event(f'{self.model}\tpwr_init\tend\t{datetime.datetime.utcnow()}') @@ -85,6 +87,7 @@ class Pwr(PwrAbstract): self.exec_logger.event(f'{self.model}\tset_voltage\tbegin\t{datetime.datetime.utcnow()}') if value != self._voltage: self.connection.write_register(0x0000, np.round(value, 2), 2) + time.sleep(max([0,1 - (self._voltage/value)])) # wait to enable DPS to reach new voltage as a function of difference between new and previous voltage self.exec_logger.event(f'{self.model}\tset_voltage\tend\t{datetime.datetime.utcnow()}') self._voltage = value @@ -149,9 +152,9 @@ class Pwr(PwrAbstract): self.exec_logger.event(f'{self.model}\tpwr_state_on\tend\t{datetime.datetime.utcnow()}') # self.current_max(self._current_max) self._pwr_state = 'on' - self.exec_logger.event(f'{self.model}\tpwr_latency\tbegin\t{datetime.datetime.utcnow()}') - time.sleep(self._pwr_latency) - self.exec_logger.event(f'{self.model}\tpwr_latency\tend\t{datetime.datetime.utcnow()}') + # self.exec_logger.event(f'{self.model}\tpwr_latency\tbegin\t{datetime.datetime.utcnow()}') + # time.sleep(self._pwr_latency) + # self.exec_logger.event(f'{self.model}\tpwr_latency\tend\t{datetime.datetime.utcnow()}') self.exec_logger.debug(f'{self.model} is on') elif state == 'off': diff --git a/ohmpi/hardware_system.py b/ohmpi/hardware_system.py index 000611345f1f2a1b7e123c218a8fc1064a8dda14..be50fedb821195aaf59d84561df66c01168593a1 100644 --- a/ohmpi/hardware_system.py +++ b/ohmpi/hardware_system.py @@ -152,7 +152,7 @@ class OhmPiHardware: self.tx.pwr = self.pwr if not self.tx.pwr.voltage_adjustable: - self.tx._pwr_latency = 0 + self.tx.pwr._pwr_latency = 0 if self.tx.specs['connect']: self.tx.polarity = 0 self.tx.pwr._current_max = self.current_max @@ -606,10 +606,16 @@ class OhmPiHardware: ### if discharge relay manually add on mb_2024_0_2, then should not activate AB relays but simply wait for automatic discharge ### if mb_20240_1_X then TX should handle the pwr discharge - time.sleep(1.0) + # time.sleep(1.0) + self.tx.discharge_pwr() def _plot_readings(self, save_fig=False, filename=None): # Plot graphs + flag = False + if self.sp is None: + flag = True + print('self.sp is None, setting it 0') + self.sp = 0 warnings.filterwarnings("ignore", category=DeprecationWarning) fig, ax = plt.subplots(nrows=5, sharex=True) ax[0].plot(self.readings[:, 0], self.readings[:, 3], '-r', marker='.', label='iab') @@ -624,6 +630,8 @@ class OhmPiHardware: ax[3].set_ylabel('R [ohm]') ax[4].plot(self.readings[v, 0], np.ones_like(self.readings[v, 0]) * self.sp, '-k', marker='.', label='SP [mV]') ax[4].set_ylabel('SP [mV]') + if flag: # if it was None, we put it back to None to not interfere with the rest + self.sp = None # fig.legend() if save_fig: if filename is None: diff --git a/ohmpi/ohmpi.py b/ohmpi/ohmpi.py index c5dda7e1977ac3de4196e4146db6b10259b258de..0282c66793f20f6fc46ebdd1ba458ec2cd22da06 100644 --- a/ohmpi/ohmpi.py +++ b/ohmpi/ohmpi.py @@ -441,9 +441,9 @@ class OhmPi(object): def download_data(self, cmd_id=None): """Create a zip of the data folder to then download it easily. """ - datadir = os.path.split(self.settings['export_path']) - # datadir = os.path.join(os.path.dirname(__file__), '../data/') - make_archive(datadir, 'zip', 'data') + datadir, _ = os.path.split(self.settings['export_path']) + zippath = os.path.abspath(os.path.join(os.path.dirname(__file__), '../data')) + make_archive(zippath, 'zip', datadir) self.data_logger.info(json.dumps({'download': 'ready'})) def shutdown(self, cmd_id=None): @@ -1065,6 +1065,75 @@ class OhmPi(object): export_dir = os.path.split(os.path.dirname(__file__))[0] self.settings['export_path'] = os.path.join(export_dir, self.settings['export_path']) + def export(self, fnames=None, outputdir=None, ftype='bert', elec_spacing=1): + """Export surveys stored in the 'data/' folder into an output + folder. + + Parameters + ---------- + fnames : list of str, optional + List of path (not filename) to survey in ohmpi format to be converted. + outputdir : str, optional + Path of the output directory where the new files are stored. If None, + a directory called 'output' is created in OhmPi. + ftype : str, optional + Type of export. To be chosen between: + - bert (same as pygimli) + - pygimli (same as bert) + - protocol (for resipy, R2 codes) + elec_spacing : float, optional + Electrode spacing in meters. Same electrode spacing is assumed. + """ + # handle parameters default values + if fnames is None: + datadir = os.path.join(os.path.dirname(__file__), '../data/') + fnames = [os.path.join(datadir, f) for f in os.listdir(datadir) if f[-4:] == '.csv'] + if outputdir is None: + outputdir = os.path.join(os.path.dirname(__file__), '../output/') + if os.path.exists(outputdir) is False: + os.mkdir(outputdir) + + # define parser + def ohmpi_parser(fname): + df = pd.read_csv(fname) + df = df.rename(columns={'A': 'a', 'B': 'b', 'M': 'm', 'N': 'n'}) + df['vp'] = df['Vmn [mV]'] + df['i'] = df['I [mA]'] + df['resist'] = df['vp']/df['i'] + df['ip'] = np.nan + emax = np.max(df[['a', 'b', 'm', 'n']].values) + elec = np.zeros((emax, 3)) + elec[:, 0] = np.arange(emax) * elec_spacing + return elec, df[['a', 'b', 'm', 'n', 'vp', 'i', 'resist', 'ip']] + + # read all files and save them in the desired format + for fname in tqdm(fnames): + try: + elec, df = ohmpi_parser(fname) + fout = os.path.join(outputdir, os.path.basename(fname).replace('.csv', '')) + if ftype == 'protocol': + fout = fout + '.dat' + with open(fout, 'w') as f: + f.write('{:d}\n'.format(df.shape[0])) + with open(fout, 'a') as f: + df['index'] = np.arange(1, df.shape[0]+1) + df[['index', 'a', 'b', 'm', 'n', 'resist']].to_csv( + f, index=False, sep=' ', header=False) + elif ftype == 'bert' or ftype == 'pygimli': + fout = fout + '.dat' + with open(fout, 'w') as f: + f.write('{:d} # positions electrodes\n'.format(elec.shape[0])) + f.write('#\tx\ty\tz\n') + for j in range(elec.shape[0]): + f.write('{:.2f}\t{:.2f}\t{:.2f}\n'.format(*elec[j, :])) + f.write('{:d} # number of data\n'.format(df.shape[0])) + f.write('#\ta\tb\tm\tn\tR\n') + with open(fout, 'a') as f: + df[['a', 'b', 'm', 'n', 'resist']].to_csv( + f, index=False, sep='\t', header=False) + except Exception as e: + print('export(): could not save file', fname) + def run_inversion(self, survey_names=None, elec_spacing=1, **kwargs): """Run a simple 2D inversion using ResIPy (https://gitlab.com/hkex/resipy). @@ -1108,7 +1177,7 @@ class OhmPi(object): from scipy.interpolate import griddata # noqa import pandas as pd # noqa import sys - sys.path.append(os.path.join(pdir, '/home/pi/resipy/src')) + sys.path.append(os.path.join(pdir, '../../resipy/src')) from resipy import Project # noqa except Exception as e: self.exec_logger.error('Cannot import ResIPy, scipy or Pandas, error: ' + str(e))