Source code for afmformats.formats.fmt_workshop.ws_single

import io
import pathlib
import warnings

import numpy as np

from ... import errors


class AFMWorkshopFormatWarning(UserWarning):
    pass


__all__ = ["load_csv"]


months = {
    "January": 1,
    "February": 2,
    "March": 3,
    "April": 4,
    "May": 5,
    "June": 6,
    "July": 7,
    "August": 8,
    "September": 9,
    "October": 10,
    "November": 11,
    "December": 12
}


[docs]def load_csv(path, callback=None, meta_override=None, mode="single"): """Load csv data from AFM workshop The files are structured like this:: Force-Distance Curve File Format: 3 Date: Wednesday, August 1, 2018 Time: 1:07:47 PM Mode: Single Point: 1 X, um: 27.250000 Y, um: 27.250000 Extend Z-Sense(nm),Extend T-B(V),Retract Z-Sense(nm),Retract T-B(V) 13777.9288,0.6875,14167.9288,1.0917 13778.9288,0.6874,14166.9288,1.0722 13779.9288,0.6876,14165.9288,1.0693 13780.9288,0.6877,14164.9288,1.0824 13781.9288,0.6875,14163.9288,1.0989 ... The data for testing was kindly provided by Peter Eaton (afmhelp.com). Parameters ---------- path: str or pathlib.Path or io.TextIOBase data file or an open file in text (not bytes) mode callback: callable function for progress tracking; must accept a float in [0, 1] as an argument. meta_override: dict if specified, contains key-value pairs of metadata that are used when loading the files (see :data:`afmformats.meta.META_FIELDS`) mode: str curve mode to expect (either "single" or "mapping"); if an unexpected mode is found, AFMWorkshopFormatWarning is issued """ if meta_override is None: meta_override = {} metadata = {} # TODO: # - only read really the header instead of the entire file to save time if isinstance(path, io.IOBase): csvdata = path.readlines() path.seek(0) # required for np.loadtxt below else: path = pathlib.Path(path) csvdata = path.read_text(encoding="utf-8").split("\n") metadata["path"] = path # get the metadata for ii, line in enumerate(csvdata): if line.count("Force-Distance Curve"): metadata["imaging mode"] = "force-distance" elif line.startswith("Software Version"): metadata["software version"] = line.split(":")[1].strip() elif line.startswith("Date:"): cdate = parse_date(line.split(":")[1]) metadata["date"] = cdate elif line.startswith("Time:"): metadata["time"] = line.split(":", 1)[1] elif line.startswith("Mode:"): cur_mode = line.split(":")[1].strip().lower() if cur_mode != mode: warnings.warn(f"Expected '{mode}' curve; got '{cur_mode}'!", AFMWorkshopFormatWarning) elif line.startswith("Point:"): metadata["enum"] = int(line.split(":")[1]) elif line.startswith("X, um:"): metadata["position x"] = float(line.split(":")[1]) * 1e-6 elif line.startswith("Y, um:"): metadata["position y"] = float(line.split(":")[1]) * 1e-6 elif line.startswith("Light Lever Gain, mV/nm:"): value = float(line.split(":")[1]) if value != 1: # ignore default values metadata["sensitivity"] = 1 / (value * 1e-3 / 1e-9) elif line.startswith("Force Constant, nN/nm:"): value = float(line.split(":")[1]) if value != 1: # ignore default values metadata["spring constant"] = value elif line.count(",") >= 3: header_index = ii header_line = line break else: raise errors.FileFormatNotSupportedError( "Could not parse metadata correctly: {}".format(path)) metadata.update(meta_override) req_metadata = ["sensitivity", "spring constant"] mis_metadata = [key for key in req_metadata if key not in metadata] if mis_metadata: fmult = None else: fmult = metadata["sensitivity"] * metadata["spring constant"] if "imaging mode" not in metadata: raise errors.FileFormatNotSupportedError( "Unknown file format: {}".format(path)) # load data rawdata = np.loadtxt(path, skiprows=header_index+1, delimiter=",") segsize = rawdata.shape[0] data = {"height (measured)": np.zeros(2*segsize, dtype=float)*np.nan, "force": np.zeros(2*segsize, dtype=float)*np.nan, "segment": np.concatenate((np.zeros(segsize, dtype=np.uint8), np.ones(segsize, dtype=np.uint8))) } columns = header_line.strip().split(",") for jj, cc in enumerate(columns): if cc == "Extend Z-Sense(nm)": data["height (measured)"][:segsize] = -rawdata[:, jj] * 1e-9 elif cc == "Retract Z-Sense(nm)": data["height (measured)"][segsize:] = -rawdata[:, jj] * 1e-9 elif cc == "Extend T-B(V)": if mis_metadata: raise errors.MissingMetaDataError( mis_metadata, f"Please specify {' and '.join(mis_metadata)}!") else: data["force"][:segsize] = rawdata[:, jj] * fmult elif cc == "Extend Force(nN)": data["force"][:segsize] = rawdata[:, jj] * 1e-9 elif cc == "Retract T-B(V)": if mis_metadata: raise errors.MissingMetaDataError( mis_metadata, f"Please specify {' and '.join(mis_metadata)}!") else: data["force"][segsize:] = rawdata[:, jj] * fmult elif cc == "Retract Force(nN)": data["force"][segsize:] = rawdata[:, jj] * 1e-9 else: warnings.warn(f"Unknown column encountered: {cc}", AFMWorkshopFormatWarning) # remove non-existent or bad data for key in list(data.keys()): if np.sum(np.isnan(data[key])): warnings.warn(f"Removed incomplete column: {key} in {path}!", AFMWorkshopFormatWarning) data.pop(key) dd = {"data": data, "metadata": metadata} if callback is not None: callback(1) return [dd]
def parse_date(datestr): """Heuristic function for parsin AFM workshop dates""" datestr = datestr.strip() if datestr.count(",") == 2: # Friday, February 14, 2020 _, m, d, y = [a.strip(", ") for a in datestr.split()] elif datestr.count(",") == 0: # 15 January 2021 d, m, y = [a.strip("") for a in datestr.split()] else: raise errors.FileFormatMetaDataError(f"Cannot parse date '{datestr}'!") return f"{int(y):04d}-{months[m]:02d}-{int(d):02d}"