Basic file loading operations

The core of all the data-loading operations is the FileManager class defined in the read_input submodule. The FileManager class can load both .DTA and .mpt files generated respectively by the GAMRY and BIOLOGIC potentiostats. The class provides a simple interface to automatically parse all the loaded data and hold them in memory in the form of bytestreams. To show how this object can be operated let us consider the case of a .mpt file that, for the purposes of this example, is provided in the documentation folder. Let us look at the content of such file:

with open("./example.mpt", 'r') as file:
    for line in file:
        print(line.rstrip('\n'))
...
Acquisition started on : 25/12/2022 13:00:00
...
Number of loops : 2
Loop 0 from point number 0 to 5
Loop 1 from point number 6 to 11

mode	ox/red	error	control changes	time/s	control/V/mA	Ewe/V	I/mA	dq/mA.h	(Q-Qo)/mA.h	Q charge/discharge/mA.h	Ece/V	P/W	Q discharge/mA.h	Q charge/mA.h	Capacity/mA.h	control/V	control/mA	Ewe-Ece/V	
1	1	0	1	1,000000000000000E+002	8,0000000E+002	1,0000000E+000	8,0000000E+002	4,000000000000000E+000	1,100000000000000E+003	1,100000000000000E+003	3,0000000E-006	1,0000000E+000	0,000000000000000E+000	1,000000000000000E+003	1,000000000000000E+003	0,0000000E+000	8,0000000E+002	1,0000000E+000
1	1	0	1	1,010000000000000E+002	8,0000000E+002	1,1000000E+000	8,0000000E+002	4,000000000000000E+000	1,100000000000000E+003	1,100000000000000E+003	3,0000000E-006	1,0000000E+000	0,000000000000000E+000	1,000000000000000E+003	1,000000000000000E+003	0,0000000E+000	8,0000000E+002	1,0000000E+000
1	1	0	1	1,020000000000000E+002	8,0000000E+002	1,2000000E+000	8,0000000E+002	4,000000000000000E+000	1,100000000000000E+003	1,100000000000000E+003	3,0000000E-006	1,0000000E+000	0,000000000000000E+000	1,000000000000000E+003	1,000000000000000E+003	0,0000000E+000	8,0000000E+002	1,0000000E+000
1	0	0	0	1,030000000000000E+002	-8,0000000E+002	9,0000000E-001	-8,0000000E+002	-4,000000000000000E-005	1,100000000000000E+003	-4,000000000000000E-005	-8,0000000E-004	-4,0000000E-001	5,000000000000000E-005	0,000000000000000E+000	5,000000000000000E-005	0,0000000E+000	-8,0000000E+002	6,0000000E-001
1	0	0	0	1,040000000000000E+002	-8,0000000E+002	8,5000000E-001	-8,0000000E+002	-4,000000000000000E+000	1,100000000000000E+003	-4,000000000000000E+000	-8,0000000E-005	-4,0000000E-001	5,000000000000000E+000	0,000000000000000E+000	5,000000000000000E+000	0,0000000E+000	-8,0000000E+002	6,0000000E-001
1	0	0	0	1,050000000000000E+002	-8,0000000E+002	8,2000000E-001	-8,0000000E+002	-4,000000000000000E+000	1,100000000000000E+003	-4,000000000000000E+000	-8,0000000E-004	-4,0000000E-001	5,000000000000000E+000	0,000000000000000E+000	5,000000000000000E+000	0,0000000E+000	-8,0000000E+002	6,0000000E-001
1	1	0	1	1,060000000000000E+002	8,0000000E+002	1,0000000E+000	8,0000000E+002	4,000000000000000E+000	1,100000000000000E+003	1,100000000000000E+003	3,0000000E-006	1,0000000E+000	0,000000000000000E+000	1,000000000000000E+003	1,000000000000000E+003	0,0000000E+000	8,0000000E+002	1,0000000E+000
1	1	0	1	1,070000000000000E+002	8,0000000E+002	1,0500000E+000	8,0000000E+002	4,000000000000000E+000	1,100000000000000E+003	1,100000000000000E+003	3,0000000E-006	1,0000000E+000	0,000000000000000E+000	1,000000000000000E+003	1,000000000000000E+003	0,0000000E+000	8,0000000E+002	1,0000000E+000
1	1	0	1	1,080000000000000E+002	8,0000000E+002	1,1500000E+000	8,0000000E+002	4,000000000000000E+000	1,100000000000000E+003	1,100000000000000E+003	3,0000000E-006	1,0000000E+000	0,000000000000000E+000	1,000000000000000E+003	1,000000000000000E+003	0,0000000E+000	8,0000000E+002	1,0000000E+000
1	0	0	0	1,090000000000000E+002	-8,0000000E+002	8,9000000E-001	-8,0000000E+002	-4,000000000000000E-005	1,100000000000000E+003	-4,000000000000000E-005	-8,0000000E-004	-4,0000000E-001	5,000000000000000E-005	0,000000000000000E+000	5,000000000000000E-005	0,0000000E+000	-8,0000000E+002	6,0000000E-001
1	0	0	0	1,100000000000000E+002	-8,0000000E+002	8,4000000E-001	-8,0000000E+002	-4,000000000000000E+000	1,100000000000000E+003	-4,000000000000000E+000	-8,0000000E-005	-4,0000000E-001	5,000000000000000E+000	0,000000000000000E+000	5,000000000000000E+000	0,0000000E+000	-8,0000000E+002	6,0000000E-001
1	0	0	0	1,110000000000000E+002	-8,0000000E+002	8,0000000E-001	-8,0000000E+002	-4,000000000000000E+000	1,100000000000000E+003	-4,000000000000000E+000	-8,0000000E-004	-4,0000000E-001	5,000000000000000E+000	0,000000000000000E+000	5,000000000000000E+000	0,0000000E+000	-8,0000000E+002	6,0000000E-001

As it can be seen, the provided file describes: two full charge/discharge cycles, each of wich composed by two halfcycle, each of which composed by 3 time-steps.

To load this .mpt file using the FileManager class, the following procedure can be used:

# Loading the FileManager class from the cellcycling.read_input module
from echemsuite.cellcycling.read_input import FileManager

# Creating an instance of the FileManager class
manager = FileManager(verbose=False)

# Loading all the .mpt file in the current folder
manager.fetch_from_folder("./", extension=".mpt")   

After the call to the fetch_from_folder function, all the data has been loaded in the class and parsed in single independent half-cycles.

Loading files manually

Please notice how the user can also manually specify a list of files to be loaded by using the function fetch_files according to the syntax:

manager = FileManager()
manager.fetch_files(["./example.mpt"]) 

Please notice how in this case the file extension is automatically detected from the filenames contained in the list. As such the extension must be available and omogeneous between the various files in the list.

Considering that in the previous example we have loaded a single .mpt file, the half-cycle ordering is unambiguos and the parsed data can immediately be converted into the corresponding Cycle and CellCycling objects by using the built-in get_cycles and get_cellcycling class methods:

# Obtain the cellcycling object from the manager
cellcycling = manager.get_cellcycling()
print(cellcycling)

# Obtain the list of cycle objects from the manager
cycle_list = manager.get_cycles()
print(cycle_list[0])
<echemsuite.cellcycling.cycles.CellCycling at 0x7ff05ea42e80>
    ├─ timestamp:                 2022-12-25 13:01:40
    ├─ total number of cycles:    2
    ├─ number of visible cycles:  2
    └─ reference cycle:           0

<echemsuite.cellcycling.cycles.Cycle at 0x7ff0989842b0>
    ├─ number:     0
    ├─ halfcycles: Both charge and discharge
    └─ hidden:     False

If the cell-cycling data is instead loaded from a set of .DTA files, either complete or partial, the ordering of the half-cycles is not strictly encoded by the data available to the FileManager and, as such, user intervention may be needed. The FileManager will try to automatically generate a suggested ordering of the half-cycle files based on their timestamp and type. The suggested ordering can be obtained by calling the memeber function suggest_ordering that will return a list of lists containing the name of the loaded files. Each list-type entry represents a half-cycle level; lists with more than one element will represent partial half-cycle files to be merged. As an example consider the followiing list:

[["Charge_1.DTA"], ["Charge_2.DTA", "Charge_2b.DTA"], ["Charge_3.DTA"]]

this list represents an ordering that will generate 3 half-cycles in which the middle one will be generated from the merging of Charge_2.DTA and Charge_2b.DTA. The user can either accept the automatically generated ordering or can manually specify its own. The selected ordering can then be applyed by passing it explicitly to the get_cycles and get_cellcycling functions as the keyworded argument custom_order.

To better show how this can be done, let us consider the following example in which two partial .DTA halfcycle charge files are provided, together with a complete discharge halfcycle file, to the file-manager.

import os
import matplotlib.pyplot as plt
from echemsuite.cellcycling.read_input import FileManager
from echemsuite.cellcycling.cycles import Cycle

# Define the path to the folder holding the files
folder = "./example_partial_DTA"

# Print a list of the available files
print(f"Available files: {', '.join([f for f in os.listdir(folder)])}\n")

# Load and parse the .DTA files contained in the "my_folder" directory
manager = FileManager()
manager.fetch_from_folder(folder, ".DTA")

# Print a list of the loaded halfcycles
print("Loaded halfcycles datasets:")
for name, obj in manager.halfcycles.items():
    print(f"{obj.timestamp}: ({obj.halfcycle_type}) {name}")
print("")

# Print the suggested ordering of the loaded-halfcycles
print("Ordering suggested by the FileManager object:")
print(manager.suggest_ordering())

# Plotting the result obtained by concatenating the data according to the suggested ordering
# and according to a user-specified ordering

suggested = manager.get_cellcycling()
custom = manager.get_cellcycling(custom_order=[['charge_1b.DTA', 'charge_1.DTA'], ['discharge_1.DTA']] )

fig = plt.figure(figsize=(10, 4), dpi=400)

plt.plot(suggested[0].charge.time, suggested[0].charge.voltage, label="suggested", marker="x")
plt.plot(custom[0].charge.time, custom[0].charge.voltage, label="custom", marker="o")

plt.xlabel("Time (s)")
plt.ylabel("Voltage (V)")
plt.grid(which="major", c="#DDDDDD")
plt.legend(title="Ordering")
Available files: charge_1b.DTA, charge_1.DTA, discharge_1.DTA

Loaded halfcycles datasets:
2022-07-23 20:28:33: (charge) charge_1b.DTA
2022-07-23 20:25:33: (charge) charge_1.DTA
2022-07-24 00:23:03: (discharge) discharge_1.DTA

Ordering suggested by the FileManager object:
[['charge_1.DTA', 'charge_1b.DTA'], ['discharge_1.DTA']]
<matplotlib.legend.Legend at 0x7ff05cc34f40>
../../_images/e8e0de89cb1caf472e9fc6f29e495c1a44a0364de346c3b6c5a205e8f319dc34.png

Loading data from a BytesIO bytestream

The read_input sub-module has been developed to also operate in a web-application environment minimizing the I/O calls done on local copies of the loaded files. For this reason, all the loaded files are immediately converted into binary bytestreams and saved in a bytestrem dictionary saved in memory. By taking advantage of this behavior, a parametrized FileManager object can also be generated by directly setting the bytestream buffer via the provided setter.

To show how this can be done let us, first of all, define a BytesIO version of the .mpt file considered before. This can easily be done with few lines of code:

# Load the BytesIO stream object from the io module
from io import BytesIO

# Open the file and load it as a bytes stream
my_bytestream = None
with open("./example.mpt", 'rb') as binary_file:
    my_bytestream = BytesIO(binary_file.read())

The loaded stream can now be used to directly initialize the FileManager class according to:

from echemsuite.cellcycling.read_input import Instrument

# Create a FileManager instance
manager = FileManager()

# Set the bytestream dictionary using the loaded data
manager.bytestreams = {"example.mpt" : my_bytestream}
manager._instrument = Instrument.BIOLOGIC

# Call the parse function
manager.parse()

Please observe how the FileManager class has no access to the file extension (the label provided here as a dictionary key was used only for clarity) and, as a direct consequence, does not know the instrument type. This requires the user to manually set the _instrument variable. Furthermore, unlike the previous example in which the call to the parse function was avoided thanks to the autoparse mode of the fetch_from_folder function, this time the call to the parse function must be made explicitly.