Skip to main content
`Ctrl`+`K`

#

# Source code for ada.io.geneactiv

    # -*- coding: utf-8 -*-
    """Module for reading and cutting GeneActive data."""

    import numpy as np
    from ciso8601 import parse_datetime
    import struct
    from tqdm import tqdm
    import pprint
    import csv
    from datetime import datetime, timezone, timedelta
    from dataclasses import dataclass
    from typing import Tuple, ClassVar

    from ada.data_containers._base import _Raw, _Epoched

    @dataclass(slots=True, eq=False)
    class RawGeneActiv(_Raw):
        """A class for storing and handling raw data generated by GeneActiv."""

        _stationary_variance: ClassVar[float] = 0.0004563
        _dynamic_range: ClassVar[float] = 8.0

        def __repr__(self):
            return "Raw metadata:\n{}".format(pprint.pformat(self._metadata, indent=4))

        @property
        def stationary_variance(self) -> float:
            return self._stationary_variance

        @property
        def dynamic_range(self) -> float:
            return self._dynamic_range

        @property
        def x(self) -> np.ndarray:
            return self._data[0]

        @property
        def y(self) -> np.ndarray:
            return self._data[1]

        @property
        def z(self) -> np.ndarray:
            return self._data[2]

        @property
        def lux(self) -> np.ndarray:
            """Lightmeter values in lux."""
            return self._data[3]

        @property
        def button(self) -> np.ndarray:
            """Button values (1=pressed)."""
            return self._data[4]

        @property
        def temperature(self) -> np.ndarray:
            """Recorded temperature in Celsius degrees."""
            return self._data[5]

        @property
        def timestamp(self) -> np.ndarray:
            return self._data[6]

        @property
        def vlen(self) -> np.ndarray:
            return np.abs(np.sqrt(self._data[0] ** 2 + self._data[1] ** 2 + self._data[2] ** 2) - 1)

        @property
        def to_score(self) -> np.ndarray:
            """Data to be scored by scoring algorithms. Here equal to vlen."""
            return self.vlen

        @property
        def id(self) -> str:
            return self.metadata['Subject Code']

        @staticmethod
        def load_file(path: str) -> "RawGeneActiv":
            """Loading file generated by the GeneActiv device.

            Args:
                path (str): Path to .bin or .csv file.

            Raises:
                ValueError: Unsupported file format otherwise.

            Returns:
                RawGeneActiv: Object containing data.
            """

            if path[-3:] == 'csv':
                data, metadata, fs, channel_names = RawGeneActiv._read_csv(path)
                return RawGeneActiv(data, metadata, fs, channel_names)
            else:
                try:
                    data, metadata, fs, channel_names = RawGeneActiv._read_bin(path)
                    return RawGeneActiv(data, metadata, fs, channel_names)
                except Exception as e:
                    print(e)
                    raise ValueError('Unsupported file format')

        def export(self, path: str):
            """Export object to .csv file fully resembling the manufacturer .csv format.

            Args:
                path (str): Path to .csv file to which data will be exported.
            """

            if 'Number of Pages' in self._metadata.keys():
                self._export_bin(path)
            else:
                self._export_csv(path)

        def cut_by_samples(self, start_sample: int, end_sample: int) -> "RawGeneActiv":
            """Create new object with the data cut by given indexes.

            Args:
                start_sample (int): First sample of output data.
                end_sample (int): Sample after the last sample of output data.

            Returns:
                RawGeneActiv: Object containing the cutted data.
            """

            return RawGeneActiv(self._data[:, start_sample:end_sample], self._metadata, self._fs, self._channel_names)

        def cut_by_timestamp(self, start_ts: float, end_ts: float | None) -> "RawGeneActiv":
            """Create new object with the data cut by given timestamps.

            Args:
                start_ts (float): Unix timestamp of output data beginning.
                end_ts (float | None): Unix timestamp of output data end. If None, last sample of output data will be last sample of input data.

            Returns:
                RawGeneActiv: Object containing the cutted data.
            """

            ts = self._metadata['Start Time']
            tz = self._metadata['Time Zone'][3:].strip()
            ts = parse_datetime(ts[:-4] + '.' + ts[-3:] + '000' + tz).timestamp()
            start_sample = np.where(self._data[6, :] >= start_ts - ts)[0][0]
            if end_ts is None:
                end_ts = ts + float(self._data[6, -1])

            try:
                end_sample = np.where(self._data[6, :] >= end_ts - ts)[0][0]
            except IndexError:
                end_sample = len(self.timestamp)
            return self.cut_by_samples(start_sample, end_sample)

        def cut_by_dates(self, start_date: str, end_date: str | None) -> "RawGeneActiv":
            """Create new object with the data cut by given dates.

            Args:
                start_date (str): ISO-formated date of outputa data beginning.
                end_date (str | None): ISO-formated date of output data end. If None, last sample of output data will be last sample of input data.

            Returns:
                RawGeneActiv: Object containing the cutted data.
            """

            start_ts = parse_datetime(start_date).timestamp()
            if end_date is not None:
                end_ts = parse_datetime(end_date).timestamp()
            else:
                end_ts = None

            return self.cut_by_timestamp(start_ts, end_ts)

        @staticmethod
        def _convert_timestamp(timestamp: float, metadata: dict, to_unix: bool = False) -> float:
            ts = metadata['Start Time']
            tz = metadata['Time Zone'][3:].strip()
            ts = parse_datetime(ts[:-4] + '.' + ts[-3:] + '000' + tz).timestamp()
            if to_unix:
                return timestamp + ts
            else:
                return timestamp - ts

        @staticmethod
        def _export_csv_metadata(metadata, writer):
            def write_sensor_data(axis):
                writer.writerow(['Sensor type'] + [axis])
                writer.writerow(['Range'] + [metadata[axis + ' Range']])
                try:
                    writer.writerow(['Resolution'] + [metadata[axis + ' Resolution']])
                    writer.writerow(['Units'] + [metadata[axis + ' Units']])
                except KeyError:
                    writer.writerow(['Resolution'])
                    writer.writerow(['Units'])

                if axis + ' Additional information' in metadata.keys():
                    writer.writerow(['Additional information'] + [metadata[axis + ' Additional information']])
                else:
                    writer.writerow(['Additional information'])

            # writer = csv.writer(f, quoting=csv.QUOTE_NONE, quotechar='', escapechar='*')  # Some fields have , (which is also a delimiter) in them, so this line adds * before , and then it works
            for e in ['Device Type', 'Device Model', 'Device Unique Serial Code', 'Device Firmware Version', 'Calibration Date']:
                writer.writerow([e, metadata[e]])

            writer.writerow(['Application name & version ', 'ADA v1.0.0'])
            for i in range(4):
                writer.writerow('')

            for e in ['Measurement Frequency', 'Start Time', 'Last measurement', 'Device Location Code', 'Time Zone']:
                writer.writerow([e, metadata[e]])

            for i in range(5):
                writer.writerow('')

            for e in ['Subject Code', 'Date of Birth', 'Sex', 'Height', 'Weight', 'Handedness Code', 'Subject Notes']:
                writer.writerow([e, metadata[e]])

            for i in range(3):
                writer.writerow('')

            for e in ['Study Centre', 'Study Code', 'Investigator ID', 'Exercise Type', 'Config Operator ID', 'Config Time', 'Config Notes', 'Extract Operator ID', 'Extract Time', 'Extract Notes']:
                writer.writerow([e, metadata[e]])

            for i in range(10):
                writer.writerow('')

            for sensor in ['MEMS accelerometer x-axis', 'MEMS accelerometer y-axis', 'MEMS accelerometer z-axis', 'Lux Photodiode 400nm - 1100nm', 'User button event marker', 'Linear active thermistor']:
                write_sensor_data(sensor)

            for i in range(20):
                writer.writerow('')

        @staticmethod
        def _export_bin_metadata(metadata, writer):
            writer.writerow(['Device Type', metadata['Device Type']])
            writer.writerow(['Device Model', metadata['Device Model']])
            writer.writerow(['Device Unique Serial Code', metadata['Device Unique Serial Code']])
            writer.writerow(['Device Firmware Version', metadata['Device Firmware Version']])
            writer.writerow(['Calibration Date', metadata['Calibration Date']])
            writer.writerow(['Application name & version', 'ADA v1.0.0'])
            for i in range(4):
                writer.writerow('')
            writer.writerow(['Measurement Frequency', metadata['Measurement Frequency']])
            writer.writerow(['Start Time', metadata['Start Time']])
            writer.writerow(['Last measurement', ''])
            writer.writerow(['Device Location Code', ''])
            writer.writerow(['Time Zone', metadata['Time Zone']])
            for i in range(5):
                writer.writerow('')
            writer.writerow(['Subject Code', metadata['Subject Code']])
            writer.writerow(['Date of Birth', metadata['Date of Birth']])
            writer.writerow(['Sex', metadata['Sex']])
            writer.writerow(['Height', metadata['Height']])
            writer.writerow(['Weight', metadata['Weight']])
            writer.writerow(['Handedness Code', metadata['Handedness Code']])
            writer.writerow(['Subject Notes', metadata['Subject Notes']])
            for i in range(3):
                writer.writerow('')
            writer.writerow(['Study Centre', metadata['Study Centre']])
            writer.writerow(['Study Code', metadata['Study Code']])
            writer.writerow(['Investigator ID', metadata['Investigator ID']])
            writer.writerow(['Exercise Type', metadata['Exercise Type']])
            writer.writerow(['Config Operator ID', metadata['Config Operator ID']])
            writer.writerow(['Config Time', metadata['Config Time']])
            writer.writerow(['Config Notes', metadata['Config Notes']])
            writer.writerow(['Extract Operator ID', metadata['Extract Operator ID']])
            writer.writerow(['Extract Time', metadata['Extract Time']])
            writer.writerow(['Extract Notes', metadata['Extract Notes']])
            for i in range(10):
                writer.writerow('')
            for e in ['x', 'y', 'z']:
                writer.writerow(['Sensor type', 'MEMS accelerometer ' + e + '-axis'])
                writer.writerow(['Range', metadata['Accelerometer Range']])
                writer.writerow(['Resolution', metadata['Accelerometer Resolution']])
                writer.writerow(['Units', metadata['Accelerometer Units']])
                writer.writerow(['Additional information'])
            writer.writerow(['Sensor type', 'Lux Photodiode 400nm - 1100nm'])
            writer.writerow(['Range', metadata['Light Meter Range']])
            writer.writerow(['Resolution', metadata['Light Meter Resolution']])
            writer.writerow(['Units', metadata['Light Meter Units']])
            writer.writerow(['Additional information'])

            writer.writerow(['Sensor type', 'User button event marker'])
            writer.writerow(['Range', '1 or 0'])
            writer.writerow(['Resolution'])
            writer.writerow(['Units'])
            writer.writerow(['Additional information', '1=pressed'])

            writer.writerow(['Sensor type', 'Linear active thermistor'])
            writer.writerow(['Range', metadata['Temperature Sensor Range']])
            writer.writerow(['Resolution', metadata['Temperature Sensor Resolution']])
            writer.writerow(['Units', metadata['Temperature Sensor Units']])
            writer.writerow(['Additional information'])
            for i in range(20):
                writer.writerow('')

        def _export_csv(self, path: str):

            with open(path, 'w', newline='') as f:
                writer = csv.writer(f)
                RawGeneActiv._export_csv_metadata(self.metadata, writer)

                ts = self._metadata['Start Time']
                tz = self._metadata['Time Zone'][3:].strip()
                sign = -1 if tz[0] == '-' else 1
                if len(tz.split(':')) == 1:
                    tzinfo = timezone(sign * timedelta(hours=int(tz[1:])))
                else:
                    temp = tz[1:].split(':')
                    tzinfo = timezone(sign * timedelta(hours=int(temp[0]), minutes=int(temp[1])))

                ts = parse_datetime(ts[:-4] + '.' + ts[-3:] + '000' + tz).timestamp()
                pbar = tqdm(total=self._data.shape[1], position=0, leave=True, desc='Saving to file: {}'.format(path), dynamic_ncols=True)
                for i in range(self._data.shape[1]):
                    row = self._data[:, i]
                    date = datetime.fromtimestamp(row[6] + ts, tz=tzinfo).strftime('%Y-%m-%d %H:%M:%S:%f')[:-3]
                    writer.writerow([date] + list(row[:6]))
                    pbar.update(1)

                pbar.close()

        def _export_bin(self, file: str):
            with open(file, 'w', newline='') as f:
                # writer = csv.writer(f, quoting=csv.QUOTE_NONE, quotechar='', escapechar='*')  # Some fields have , (which is also a delimiter) in them, so this line adds * before , and then it works
                writer = csv.writer(f)
                RawGeneActiv._export_bin_metadata(self._metadata, writer)
                ts = self._metadata['Start Time']
                tz = self._metadata['Time Zone'][3:].strip()
                sign = -1 if tz[0] == '-' else 1
                hours, minutes = tz[1:].split(':')
                tzinfo = timezone(sign * timedelta(hours=int(hours), minutes=int(minutes)))
                ts = parse_datetime(ts[:-4] + '.' + ts[-3:] + '000' + tz).timestamp()
                pbar = tqdm(total=self._data.shape[1], position=0, leave=True, desc='Saving to file: {}'.format(file), dynamic_ncols=True)
                for i in range(self._data.shape[1]):
                    row = self._data[:, i]
                    date = datetime.fromtimestamp(row[6] + ts, tz=tzinfo).strftime('%Y-%m-%d %H:%M:%S:%f')[:-3]
                    writer.writerow([date] + list(row[:6]))
                    pbar.update(1)
            pbar.close()
            f.close()

        @staticmethod
        def _read_bin(path: str) -> Tuple[np.ndarray, dict, float, list]:
            metadata = {}
            with open(path, 'rb') as f:
                for i in range(59):  # lines contain metadata
                    line = (f.readline()).decode('utf-8').strip()
                    if ':' in line:
                        key, value = line.split(':', 1)
                        metadata[key] = value.strip().replace('\x00', '')

                fs = float(metadata['Measurement Frequency'][:-3])
                delta = np.linspace(0, 1 / fs * 299, 300)  # delta for time stamp interpolation
                NP = int(metadata['Number of Pages'])  # number of data blocks
                data = np.zeros((7, NP * 300))
                channel_names = ['x', 'y', 'z', 'lux', 'button', 'temperature', 'timestamp']
                tz = metadata['Time Zone'][3:].strip()
                TS = parse_datetime(metadata['Start Time'][:-4] + '.' + metadata['Start Time'][-3:] + '000' + tz).timestamp()

                c = 0  # counter for data
                for i in tqdm(range(NP), position=0, leave=True, desc='Reading from file: {}'.format(path), dynamic_ncols=True):
                    # useless lines
                    for k in range(3):
                        _ = f.readline()

                    # timestamp of data block
                    line = f.readline().strip().decode('utf-8')
                    temp_line_to_parse = line[10:].rsplit(':', 1)[-2] + '.' + line[10:].rsplit(':', 1)[-1]
                    d = parse_datetime(temp_line_to_parse + tz).timestamp() - TS  # timestamp of first sample != timestamp of Start time??? Same problem is in file, its not bug in the code, but some problem with .bin file???
                    # useless line
                    _ = f.readline()
                    # temperature
                    line = f.readline()[:-2].decode('utf-8')
                    temperature = float(line.split(':', 1)[1])
                    # temperature resampling
                    data[5, c:c + 300] = temperature
                    # time stamp interpolation
                    data[6, c:c + 300] = d + delta
                    # useless lines
                    for k in range(3):
                        _ = f.readline()

                    # data block
                    line_hex = f.readline().strip()
                    hexes = struct.unpack("12s " * 300, line_hex)
                    for e in hexes:
                        dataChunk = struct.unpack("12s 12s 12s 10s 1s 1s", bytes(bin(int(e, 16))[2:].zfill(48), 'utf-8'))

                        data[0, c] = int(dataChunk[0], 2)  # x
                        data[1, c] = int(dataChunk[1], 2)  # y
                        data[2, c] = int(dataChunk[2], 2)  # z
                        data[3, c] = int(dataChunk[3], 2)  # lux
                        data[4, c] = int(dataChunk[4], 2)  # button
                        c += 1

            # Converting unsigned 12-bit data into 12-bit signed data
            data[0, data[0, :] >= 2048] = data[0, data[0, :] >= 2048] - 4096
            data[1, data[1, :] >= 2048] = data[1, data[1, :] >= 2048] - 4096
            data[2, data[2, :] >= 2048] = data[2, data[2, :] >= 2048] - 4096
            # And calculating values in g (or lux) using calibration parameters
            data[0, :] = (data[0, :] * 100 - int(metadata['x offset'])) / int(metadata['x gain'])
            data[1, :] = (data[1, :] * 100 - int(metadata['y offset'])) / int(metadata['y gain'])
            data[2, :] = (data[2, :] * 100 - int(metadata['z offset'])) / int(metadata['z gain'])
            data[3, :] = data[3, :] * int(metadata['Lux']) / int(metadata['Volts'])

            return data, metadata, fs, channel_names

        @staticmethod
        def _read_csv(path: str) -> Tuple[np.ndarray, dict, float, list]:
            metadata = {}
            with open(path) as f:

                for i in range(76):  # First 100 lines contain metadata, but there are six blocks of 5 lines containing sensor data
                    row = f.readline()
                    if row != '\n' and ',' in row:
                        key, value = row.split(',', 1)
                        if 'Sensor type' in key:
                            sensor = value[:-1]
                            for i in range(4):
                                row = f.readline()
                                if ',' in row:
                                    key, value = row.split(',', 1)
                                    key = sensor.strip() + ' ' + key.strip()
                                    metadata[key] = value[:-1].replace('\0', '').strip()
                        else:
                            metadata[key] = value[:-1].replace('\0', '').replace('*', '').strip()  # if file is written with export method, sometimes there will be unneeded * in some values

            channel_names = ['x', 'y', 'z', 'lux', 'button', 'temperature', 'timestamp']
            tz = metadata['Time Zone'][3:].strip()
            TS = parse_datetime(metadata['Start Time'][:-4] + '.' + metadata['Start Time'][-3:] + '000' + tz).timestamp()
            data = np.loadtxt(path, skiprows=100, usecols=(1, 2, 3, 4, 5, 6), delimiter=',').T
            if data.shape[0] != 6:
                raise ValueError("Trying to read epoched data? Use GeneActivMVM instead.")

            ts = np.loadtxt(path, skiprows=100, usecols=0, dtype=str, delimiter=',')
            ts = np.array([parse_datetime(x[:-4] + '.' + x[-3:] + '000' + tz).timestamp() - TS for x in tqdm(ts, position=0, leave=True, desc='Reading from file: {}'.format(path), dynamic_ncols=True)])
            data = np.concatenate((data, ts.reshape(1, -1)), axis=0)
            fs = float(metadata['Measurement Frequency'][:-3])

            return data, metadata, fs, channel_names

        @staticmethod
        def preview_metadata(path: str) -> tuple[dict, list[str]]:
            """Preview of metadata without loading the file.

            Args:
                path (str): Path to the file

            Raises:
                ValueError: Wrong format

            Returns:
                tuple[dict, list[str]]: Metadata and channel names.
            """
            if path[-3:] == 'csv':
                return RawGeneActiv._preview_csv_metadata(path)
            else:
                try:
                    return RawGeneActiv._preview_bin_metadata(path)
                except Exception as e:
                    print(e)
                    raise ValueError('Unsupported file format')

        @staticmethod
        def _preview_bin_metadata(path: str) -> tuple[dict, list[str]]:
            metadata = {}
            with open(path, 'rb') as f:
                for i in range(59):  # lines contain metadata
                    line = (f.readline()).decode('utf-8').strip()
                    if ':' in line:
                        key, value = line.split(':', 1)
                        metadata[key] = value.strip().replace('\x00', '')
            channel_names = ['x', 'y', 'z', 'lux', 'button', 'temperature', 'timestamp']
            return metadata, channel_names

        @staticmethod
        def _preview_csv_metadata(path: str) -> tuple[dict, list[str]]:
            metadata = {}
            with open(path) as f:

                for i in range(76):  # First 100 lines contain metadata, but there are six blocks of 5 lines containing sensor data
                    row = f.readline()
                    if row != '\n' and ',' in row:
                        key, value = row.split(',', 1)
                        if 'Sensor type' in key:
                            sensor = value[:-1]
                            for i in range(4):
                                row = f.readline()
                                if ',' in row:
                                    key, value = row.split(',', 1)
                                    key = sensor.strip() + ' ' + key.strip()
                                    metadata[key] = value[:-1].replace('\0', '').strip()
                        else:
                            metadata[key] = value[:-1].replace('\0', '').replace('*', '').strip()  # if file is written with export method, sometimes there will be unneeded * in some values

            channel_names = ['x', 'y', 'z', 'lux', 'button', 'temperature', 'timestamp']
            return metadata, channel_names

    @dataclass(slots=True, eq=False)
    class GeneActivMVM(_Epoched):
        """A class for storing and handling epoched data generated by GeneActiv-style MVM method."""

        def __repr__(self):
            str1 = pprint.pformat(self._metadata, indent=4)
            str2 = pprint.pformat(self._epoching_method_metadata, indent=4)
            return 'Raw metadata:\n{}\n\nEpoching metadata:\n{}'.format(str1, str2)

        @property
        def x(self) -> np.ndarray:
            """Mean acceleration (in g) along x axis."""
            return self._data[0]

        @property
        def y(self) -> np.ndarray:
            """Epoch-wise mean acceleration (in g) along y axis."""
            return self._data[1]

        @property
        def z(self) -> np.ndarray:
            """Epoch-wise mean acceleration (in g) along z axis."""
            return self._data[2]

        @property
        def lux(self) -> np.ndarray:
            """Epoch-wise mean lightmeter values in lux."""
            return self._data[3]

        @property
        def button(self) -> np.ndarray:
            """Sum of button values (1=pressed) over epochs."""
            return self._data[4]

        @property
        def temperature(self) -> np.ndarray:
            """Epoch-wise mean recorded temperature in Celsius degrees."""
            return self._data[5]

        @property
        def timestamp(self) -> np.ndarray:
            """Timestamp of the epoch's end."""
            return self._data[6]

        @property
        def mvm(self) -> np.ndarray:
            """Mean vector magnitude calculated as sum of vlens in the epoch."""
            return self._data[7]

        @property
        def to_score(self) -> np.ndarray:
            return self.mvm

        @property
        def x_std(self) -> np.ndarray:
            """Epoch-wise standard deviation along x axis."""
            return self._data[8]

        @property
        def y_std(self) -> np.ndarray:
            """Epoch-wise standard deviation along y axis."""
            return self._data[9]

        @property
        def z_std(self) -> np.ndarray:
            """Epoch-wise standard
              deviation along z axis."""
            return self._data[10]

        @property
        def peak_lux(self) -> np.ndarray:
            """Maximum lightmeter value for each epoch (in lux)."""
            return self._data[11]

        @property
        def first_sample_timestamp(self) -> float:
            """Unix timestamp of first sample."""
            return RawGeneActiv._convert_timestamp(self.timestamp[0], self._metadata, True)

        @property
        def id(self) -> str:
            return self.metadata['Subject Code']

        @staticmethod
        def load_file(path: str) -> "GeneActivMVM":
            """Loading .csv file with epoched data generated by the GeneActiv software or the MVM method.

            Args:
                path (str): Path .csv file.

            Raises:
                ValueError: Unsupported file format otherwise.

            Returns:
                GeneActivMVM: Object containing data.
            """
            metadata = {}
            with open(path) as f:

                for i in range(76):  # First 100 lines contain metadata, but there are six blocks of 5 lines containing sensor data
                    row = f.readline()
                    if row != '\n' and ',' in row:
                        key, value = row.split(',', 1)
                        if 'Sensor type' in key:
                            sensor = value[:-1]
                            for i in range(4):
                                row = f.readline()
                                if ',' in row:
                                    key, value = row.split(',', 1)
                                    key = sensor.strip() + ' ' + key.strip()
                                    metadata[key] = value[:-1].replace('\0', '').strip()
                        else:
                            metadata[key] = value[:-1].replace('\0', '').replace('*', '').strip()  # if file is written with export method, sometimes there will be unneeded * in some values

            channel_names = ['x', 'y', 'z', 'lux', 'button', 'temperature', 'timestamp', 'mvm', 'x_std', 'y_std', 'z_std', 'peak_lux']
            tz = metadata['Time Zone'][3:].strip()
            TS = parse_datetime(metadata['Start Time'][:-4] + '.' + metadata['Start Time'][-3:] + '000' + tz).timestamp()
            data = np.loadtxt(path, skiprows=100, usecols=(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11), delimiter=',').T
            if data.shape[0] != 11:
                raise ValueError("Trying to read non-epoched, raw data? Use RawGeneactiv instead.")

            ts = np.loadtxt(path, skiprows=100, usecols=0, dtype=str, delimiter=',')
            ts = np.array([parse_datetime(x[:-4] + '.' + x[-3:] + '000' + tz).timestamp() - TS for x in tqdm(ts, position=0, leave=True, desc='Reading from file: {}'.format(path), dynamic_ncols=True)])
            data = np.concatenate((data[:6], ts.reshape(1, -1), data[6:]), axis=0)
            fs = 1 / (ts[1] - ts[0])
            epoching_method_metadata = {'epoching method': 'Geneactiv-like epoching', 'epoch length': ts[1] - ts[0], 'to_score channel': 7}

            return GeneActivMVM(data, metadata, fs, channel_names, epoching_method_metadata)

        @staticmethod
        def preview_metadata(path: str) -> tuple[dict, list[str]]:
            """Preview metadata without loading the whole file.

            Args:
                path (str): Path to a file

            Returns:
                tuple[dict, list[str]]: Metadata and channel names.
            """
            metadata = {}
            with open(path) as f:

                for i in range(76):  # First 100 lines contain metadata, but there are six blocks of 5 lines containing sensor data
                    row = f.readline()
                    if row != '\n' and ',' in row:
                        key, value = row.split(',', 1)
                        if 'Sensor type' in key:
                            sensor = value[:-1]
                            for i in range(4):
                                row = f.readline()
                                if ',' in row:
                                    key, value = row.split(',', 1)
                                    key = sensor.strip() + ' ' + key.strip()
                                    metadata[key] = value[:-1].replace('\0', '').strip()
                        else:
                            metadata[key] = value[:-1].replace('\0', '').replace('*', '').strip()  # if file is written with export method, sometimes there will be unneeded * in some values

            channel_names = ['x', 'y', 'z', 'lux', 'button', 'temperature', 'timestamp', 'mvm', 'x_std', 'y_std', 'z_std', 'peak_lux']
            return metadata, channel_names

        def export(self, path: str):
            """Export object to .csv file fully resembling the manufacturer .csv format.

            Args:
                path (str): Path to .csv file to which data will be exported.
            """

            with open(path, 'w', newline='') as f:
                writer = csv.writer(f)
                if 'Number of Pages' in self._metadata.keys():
                    RawGeneActiv._export_bin_metadata(self._metadata, writer)
                else:
                    RawGeneActiv._export_csv_metadata(self._metadata, writer)

                ts = self._metadata['Start Time']
                tz = self._metadata['Time Zone'][3:].strip()
                sign = -1 if tz[0] == '-' else 1
                if len(tz.split(':')) == 1:
                    tzinfo = timezone(sign * timedelta(hours=int(tz[1:])))
                else:
                    temp = tz[1:].split(':')
                    tzinfo = timezone(sign * timedelta(hours=int(temp[0]), minutes=int(temp[1])))

                ts = parse_datetime(ts[:-4] + '.' + ts[-3:] + '000' + tz).timestamp()
                for i in tqdm(range(self._data.shape[1]), position=0, leave=True, desc='Saving to file: {}'.format(path), dynamic_ncols=True):
                    row = self._data[:, i]
                    date = datetime.fromtimestamp(row[6] + ts, tz=tzinfo).strftime('%Y-%m-%d %H:%M:%S:%f')[:-3]
                    writer.writerow([date] + list(row[:6]) + list(row[7:]))

        def cut_by_samples(self, start_sample: int, end_sample: int) -> "GeneActivMVM":
            """Create new object with the data cut by given indexes.

            Args:
                start_sample (int): First sample of output data.
                end_sample (int): Sample after the last sample of output data.

            Returns:
                GeneActivMVM: Object containing the cutted data.
            """
            return GeneActivMVM(self._data[:, start_sample:end_sample], self._metadata, self._fs, self._channel_names, self._epoching_method_metadata)

        def cut_by_timestamp(self, start_ts: float, end_ts: float | None = None) -> "GeneActivMVM":
            """Create new object with the data cut by given timestamps.

            Args:
                start_ts (float): Unix timestamp of output data beginning.
                end_ts (float | None): Unix timestamp of output data end. If None, last sample of output data will be last sample of input data.

            Returns:
                GeneActivMVM: Object containing the cutted data.
            """
            ts = self._metadata['Start Time']
            tz = self._metadata['Time Zone'][3:].strip()
            ts = parse_datetime(ts[:-4] + '.' + ts[-3:] + '000' + tz).timestamp()
            start_sample = np.where(self._data[6, :] >= start_ts - ts)[0][0]
            if end_ts is None:
                end_ts = ts + float(self._data[6, -1])

            try:
                end_sample = np.where(self._data[6, :] >= end_ts - ts)[0][0]
            except IndexError:
                end_sample = len(self.timestamp)
            return self.cut_by_samples(start_sample, end_sample)

        def cut_by_dates(self, start_date: str, end_date: str | None = None) -> "GeneActivMVM":
            """Create new object with the data cut by given dates.

            Args:
                start_date (str): ISO-formated date of outputa data beginning.
                end_date (str | None): ISO-formated date of output data end. If None, last sample of output data will be last sample of input data.

            Returns:
                GeneActivMVM: Object containing the cutted data.
            """
            start_ts = parse_datetime(start_date).timestamp()
            if end_date is not None:
                end_ts = parse_datetime(end_date).timestamp()
            else:
                end_ts = None

            return self.cut_by_timestamp(start_ts, end_ts)