Пример #1
0
def measure(node: MetrolabTHM1176Node,
            N=10,
            max_num_retrials=5,
            average=False):
    """
    Measures the magnetic field with the Metrolab sensor. Returns either raw measured values of field components (x, y, z)
    or mean and standard deviation in each direction.

    Args:
        node (MetrolabTHM1176Node): represents the Metrolab THM 1176 sensor
        N (int, optional): number of data points collected for each average. Defaults to 10.
        max_num_retrials (int, optional): How many times to reattempt measurement before raising an exception. Defaults to 5.
        average (bool, optional): Average over the N measurements. Defaults to False.

    Raises:
        MeasurementError: a problem occured during measurement.

    Returns:
        tuple of 2 np.array(3): This is returned if average is true. mean and std of the three field componenents over N measurements.
        tuple (np.ndarray((N,3)), 0): This is returned if average is false. N measured values of each component are contained. Second return is 0.
    """
    # if dataDir is not None:
    #     ensure_dir_exists(dataDir, verbose=False)

    # perform measurement and collect the raw data
    for _ in range(max_num_retrials):
        try:
            # an N by 3 array
            meas_data = np.array(node.measureFieldArraymT(N)).swapaxes(0, 1)
        except BaseException:
            pass
        else:
            break

    try:
        # due to the setup, transform sensor coordinates to magnet coordinates
        meas_data = sensor_to_magnet_coordinates(meas_data)
    # if it was not possible to obtain valid measurement results after
    # max_num_retrials, raise MeasurementError, too
    except BaseException:
        raise MeasurementException()

    if average:
        # compute the mean and std from raw data for each sensor
        mean_data = np.mean(meas_data, axis=0)
        std_data = np.std(meas_data, axis=0)
        ret1, ret2 = mean_data, std_data
    else:
        # This option is more for getting time-field measurements.
        ret1, ret2 = meas_data, 0

    return ret1, ret2
def get_mean_dataset_MetrolabSensor(node: MetrolabTHM1176Node,
                                    sampling_size,
                                    verbose=False,
                                    max_num_retrials=5):
    """
    Estimate field vectors with Metrolab sensor sampling_size-times and return the mean and std 
    as 1d-ndarrays of length 3. 

    Args: 
    - node (MetrolabTHM1176Node): represents the Metrolab THM 1176 sensor
    - sampling_size (int): sampling size to estimate mean magnetic field vector and std, 
    i.e. number of times the sensor is read out in series before averaging 
    - verbose (bool): switching on/off print-statements for displaying progress

    Return: 
    - mean_data, std_data (ndarrays of of shape (number sensors, 3)): Mean magnetic field and 
    its standard deviation as a vector. 

    Raises MeasurementError if no valid measurement data could be aquired after max_num_retrials tries. 
    """
    # perform measurement and collect the raw data
    for _ in range(max_num_retrials):
        try:
            meas_data = np.array(
                node.measureFieldArraymT(sampling_size)).swapaxes(0, 1)
        except:
            pass
        else:
            break

    try:
        # due to the setup, transform sensor coordinates to magnet coordinates
        meas_data = sensor_to_magnet_coordinates(meas_data)

    # if it was not possible to obtain valid measurement results after max_num_retrials, raise MeasurementError, too
    except UnboundLocalError:
        raise MeasurementError

    # estimate the mean and std from raw data for each sensor
    mean_data = np.mean(meas_data, axis=0)
    std_data = np.std(meas_data, axis=0)

    # # save raw data if desired
    # if save_mean_data:
    #     # if no directory is provided, just take current working directory
    #     if directory is None:
    #         directory = os.getcwd()
    #     save_in_dir(meas_data, directory, 'data', now=True)

    # return mean and std fields either for only this sensor or for all sensors
    return mean_data, std_data
def callGridSweep():
    """
    Setup function to call the utility function 'sweepCurrents', see 'utility_functions.py'. Manages necessary inputs.
    """
    # must be a .csv file!
    inpFile = input("Enter a valid configuration file path: ")
    inp1 = input(
        "current factor (if file contains B fields: 1, if currents are in file, choose a current value that brings it to ): "
    )

    # the values for each measurement run should be the same for consistent results
    try:
        start_val = int(inp1)
    except BaseException:
        print("expected numerical value, defaulting to -4500")
        start_val = -4500

    # with MetrolabTHM1176Node(
    #     block_size=30, range="0.3 T", period=0.01, average=1
    # ) as node:
    node = MetrolabTHM1176Node(block_size=30,
                               range='0.1 T',
                               period=0.01,
                               average=1)

    gotoPosition()
    # doing gridSweep
    inp = input("Use current config or B vector file as input? (i/b) ")
    if inp == "b":
        use_B_vectors_as_input = True
    else:
        use_B_vectors_as_input = False

    # inp5 = input('demagnetize after each measurement? (y/n) ')
    inp5 = input("measure temperature? (y/n): ")
    inp6 = input("append today`s date to directory name? (y/n) ")
    datadir = input(
        "Enter a valid directory name to save measurement data in: ")

    gridSweep(
        node,
        inpFile,
        datadir=datadir,
        factor=start_val,
        BField=use_B_vectors_as_input,
        demagnetize=True,
        today=(inp6 == "y"),
        temp_meas=(inp5 == "y"),
    )
Пример #4
0
def timeResolvedMeasurement(block_size=20,
                            period=0.01,
                            average=5,
                            duration=10):
    """
    Measure magnetic flux density over time.

    Args:
        period (float, optional): Trigger period in seconds. Defaults to 0.001 (1 ms).
        averaging (int, optional): The arithmetic mean of this number of measurements is taken before they are fetched.
                                   Results in smoother measurements. Defaults to 1.
        block_size (int, optional): How many measurements should be fetched at once. Defaults to 1.
        duration (int, optional): Total duration of measurement. Defaults to 10.

    Raises:
        ValueError: If for some reason the number of time values and B field values is different.

    Returns:
        dictionary containing lists of floats: Bx, By, Bz, timeline, temp
        (x, y and z components of B field, times of measurements, temperature values
        are dimensionless values between 0 and 64k)
    """
    with MetrolabTHM1176Node(period=period,
                             block_size=block_size,
                             range='auto',
                             average=average,
                             unit='MT') as node:
        # gotoPosition(node, meas_height=1.5)
        # node = MetrolabTHM1176Node(period=period, block_size=block_size, range='0.3 T', average=average, unit='MT')
        thread = threading.Thread(target=node.start_acquisition,
                                  name=__name__ + 'dataCollector')
        thread.start()
        sleep(duration)
        node.stop = True
        thread.join()
        # Sensor coordinates to preferred coordinates transformation
        xValues = np.array(node.data_stack['Bx'])
        xValues = -xValues
        # Sensor coordinates to preferred coordinates transformation, offset correction
        yValues = np.array(node.data_stack['Bz'])
        #yOffset = 2.40
        # Sensor coordinates to preferred coordinates transformation, offset correction
        zValues = node.data_stack['By']
        # zValues = np.subtract(zValues, -1.11)

        timeline = node.data_stack['Timestamp']

        t_offset = timeline[0]
        for ind in range(len(timeline)):
            timeline[ind] = round(timeline[ind] - t_offset, 3)

    try:
        if (len(node.data_stack['Bx']) != len(timeline)
                or len(node.data_stack['By']) != len(timeline)
                or len(node.data_stack['Bz']) != len(timeline)):
            raise ValueError(
                "length of Bx, By, Bz do not match that of the timeline")
        else:
            return {
                'Bx': xValues.tolist(),
                'By': yValues.tolist(),
                'Bz': zValues.tolist(),
                'temp': node.data_stack['Temperature'],
                'time': timeline
            }
    except Exception as e:
        print(e)
        return {'Bx': 0, 'By': 0, 'Bz': 0, 'time': 0, 'temp': 0}
Пример #5
0
def callableFeedback(BField, currentStep=20, maxCorrection=30,
                     threshold=0.5, calibrate=False, ECB_active=True):
    """
    Feedback control of vector magnet based on measurements used to obtain any desired magnetic field. To be called externally/with predefined parameters.

    Args:
        BField (list): List which contains the coordinate system used to represent the vector included in the list.
                       Format: [char, B_Vector]; with char in {'c','s'}, c for cartesian
                       B_Vector in {(Bx,By,Bz) in R^3} if char is 'c' or {(B,theta,phi) in [0,inf]x[0,180]°x[0,360]°} if char is 's'
                       B_Vector as np.array(3)
        currentStep (int, optional): The maximum step size for derivative (system) estimation. Defaults to 20.
        maxCorrection (int, optional): The maximum step size for current correction. Defaults to 30.
        thresholdPercentage (float, optional): Percentage of total magnitude the error needs to exceed to be considered. Defaults to 0.02.
        calibrate (bool, optional): The sensor is calibrated before measuring if True. Defaults to False.
        ECB_active (bool,optional): Tells us whether there already is a connection with the ECB.
                                    Useful if this function is being called after the ECB was activated. Defaults to True.

    Returns:
        desiredBField (np.array(3)): The "goal" magnetic field.
        derivatives (np.ndarray(3,3)): The average derivative matrix used to control the current values set.
        np.array(goodCurrentValues) (np.ndarray(n,3)): A list of the currents that generate the wanted magnetic field with a small enough error.
        printCurrents (np.array(3)): The (column-wise) average of the currents that generate the wanted magnetic field with a small enough error.
    """
    # TODO: update parameters to use with new power supplies.

    global flags  # for handling input
    # currents to set on ECB
    desCurrents = [0] * 8
    # we want to find the 'best' current configuration for a certain direction ultimately.
    # So we save the current values where the error is below the threshold, at the three
    # coil values will be averaged.
    goodCurrentValues = []
    # queue with the last measured magnetic field vector
    fieldVectors = []

    try:
        # desired magnetic (flux density) field used as reference
        if BField[0] == 'c':
            desiredBField = BField[1]
            magneticField = np.linalg.norm(desiredBField)

        elif BField[0] == 's':
            magneticField = BField[1][0]
            theta = BField[1][1]
            phi = BField[1][2]
            desiredBField = tr.computeMagneticFieldVector(
                magneticField, theta, phi)

    except BaseException:
        desiredBField = np.array([20, 0, 0])
        print('No usable magnetic field vector was given! Using the default value (20mT in x direction).')

    # connect with ECB
    if not ECB_active:
        openConnection()
    # initialize sensor to start measuring
    with MetrolabTHM1176Node(period=0.05, range='0.3 T', average=20) as node:
        # node = MetrolabTHM1176Node(period=0.1, range='0.3 T', average=5)
        configCurrents = tr.computeCoilCurrents(desiredBField)
        # compute linearization Matrix around the current operating point
        enableCurrents()
        derivatives = localLinearization(
            node, configCurrents=configCurrents, currentStep=currentStep, numSteps=10)
        print(derivatives)
        demagnetizeCoils()
        sleep(0.2)
        node.calibrate()

        desCurrents[0:3] = configCurrents.tolist()
        setCurrents(desCurrents=desCurrents, direct=b'1')
        # goodCurrentValues.append(desCurrents[0:3])

        # Active monitoring & controlling section
        while flags[0]:
            newBMeasurement = gen.sensor_to_magnet_coordinates(
                np.array(node.measureFieldmT()))

            B_magnitude = np.linalg.norm(newBMeasurement)
            theta = np.degrees(np.arccos(newBMeasurement[2] / B_magnitude))
            phi = np.degrees(np.arctan2(
                newBMeasurement[1], newBMeasurement[0]))

            if flags[0]:
                print(f'\rMeasured B field: ({newBMeasurement[0]:.2f}, {newBMeasurement[1]:.2f}, {newBMeasurement[2]:.2f})'
                      f' / In polar coordinates: ({B_magnitude:.2f}, {theta:.2f}°, {phi:.2f}°)    ', sep='', end='', flush=True)

            # currentStep = currentStep * (decayFactor ** i)
            errorsB = newBMeasurement - desiredBField
            fieldDiff = np.zeros(3)
            noError = True
            # if the error of some component is greater than 2%, it needs to be decreased.
            if abs(errorsB[0]) >= abs(threshold):
                # decrease the B_x component (proportional to the error)
                fieldDiff[0] = -errorsB[0]
                count = 0
                noError = noError & False
            else:
                goodCurrentValues.append(desCurrents[0:3])
                noError = noError & True

            if abs(errorsB[1]) >= abs(threshold):
                # decrease the B_y component (proportional to the error)
                fieldDiff[1] = -errorsB[1]
                count = 0
                noError = noError & False
            else:
                goodCurrentValues.append(desCurrents[0:3])
                noError = noError & True

            if abs(errorsB[2]) >= abs(threshold):
                # decrease the B_z component (proportional to the error)
                fieldDiff[2] = -errorsB[2]
                count = 0
                noError = noError & False
            else:
                goodCurrentValues.append(desCurrents[0:3])
                noError = noError & True

            # limit duration of loop
            if noError:
                count = count + 1
            # if at least 10 consecutive measurements are within the error margin, the
            # config is good enough.
            if count >= 20:
                flags.insert(0, '')

            deltaCurrent = np.linalg.inv(derivatives).dot(fieldDiff)

            # limit step size
            for i in range(len(deltaCurrent)):
                if abs(deltaCurrent[i]) > maxCorrection:
                    deltaCurrent = np.sign(deltaCurrent) * maxCorrection

            # The set current values are adapted to correct the magnetic field
            desCurrents[0:3] = [int(desCurrents[0] + deltaCurrent[0]), int(desCurrents[1] + deltaCurrent[1]),
                                int(desCurrents[2] + deltaCurrent[2])]
            setCurrents(desCurrents=desCurrents)
            # wait for stabilization
            sleep(0.3)

    # current configurations with little to no error -> hopefully a good estimate
    # of the required currents for a given direction.
    modeCurrents, _ = stats.mode(np.array(goodCurrentValues), 0)

    print(f'\nBest config for channels 1, 2, 3: ({int(modeCurrents[0][0])},'
          f' {int(modeCurrents[0][1])}, {int(modeCurrents[0][2])})\n')
    disableCurrents()
    if not ECB_active:
        closeConnection()

    flags.insert(0, 1)

    return desiredBField, derivatives, modeCurrents[0]
Пример #6
0
def localLinearization(psu: PowerSupplyCommands, node: MetrolabTHM1176Node, configCurrents=np.array(
        [1, 1, 1]), currentStep=20, numSteps=10):
    """
    Compute a local linearization of the relationship between current and B-field using measurements and a fixed step size.

    Args:
        node (MetrolabTHM1176Node): Instance of the Metrolab sensor.
        configCurrents (np.array(3)), optional): Current configuration around which to linearize. Defaults to np.array([1000,1000,1000]).
        currentStep (int, optional): Step size by which current is changed to compute slope. If the step
                                     size is too small, the change in magnetic field strength may be
                                     inaccurate due to noise. Defaults to 20.
        numSteps (int, optional): The number of measurements over which the result is averaged. Defaults to 10.

    Returns:
        derivatives (3x3 np.ndarray): matrix with 'derivatives' of field components wrt current in each coil.
    """
    desCurrents = [0] * 3
    fieldVectors = []

    currentRefs = np.ndarray((3, 3))
    currentRefs[0, :] = np.array([1, 0, 0])
    currentRefs[1, :] = np.array([0, 1, 0])
    currentRefs[2, :] = np.array([0, 0, 1])

    differenceQuotients = []
    derivatives = np.zeros((3, 3))

    # set currents for the first time
    desCurrents[0:3] = configCurrents.tolist()
    psu.setCurrents(des_currents=desCurrents)
    print('\nComputed currents on channels 1, 2, 3: '
          f'({configCurrents[0]}, {configCurrents[1]}, {configCurrents[2]})')

    newBMeasurement = gen.sensor_to_magnet_coordinates(
        np.array(node.measureFieldmT()))
    fieldVectors.insert(0, newBMeasurement)

    for i in range(3):
        # randomize whether the current is increased or decreased
        a = np.random.randn()
        prefac = 1
        if not np.sign(a) > 0:
            prefac = -1

        # set initial current 'point'
        initialOffset = prefac * numSteps * currentStep
        desCurrents[0:3] = configCurrents.tolist()
        desCurrents[0:3] = [int(desCurrents[0] + initialOffset * currentRefs[i, 0]),
                            int(desCurrents[1] +
                                initialOffset * currentRefs[i, 1]),
                            int(desCurrents[2] + initialOffset * currentRefs[i, 2])]
        psu.setCurrents(des_currents=desCurrents)

        currentStep = - prefac * currentStep
        # increase or decrease current by step size 2*N times
        for j in range(2 * numSteps):
            desCurrents[0:3] = [int(desCurrents[0] + currentStep * currentRefs[i, 0]),
                                int(desCurrents[1] +
                                    currentStep * currentRefs[i, 1]),
                                int(desCurrents[2] + currentStep * currentRefs[i, 2])]
            psu.setCurrents(des_currents=desCurrents)

            sleep(0.5)

            newBMeasurement = gen.sensor_to_magnet_coordinates(
                np.array(node.measureFieldmT()))
            print(
                f'\rMeasured B field: ({newBMeasurement[0]:.2f}, {newBMeasurement[1]:.2f},'
                f'{newBMeasurement[2]:.2f})', sep='', end='', flush=True)
            fieldVectors.insert(0, newBMeasurement)
            if len(fieldVectors) > 4:
                # use central difference formula with 4th order accuracy, consider current step to be j+2, in total a range of 5
                # measurements is used
                differenceQuotients.insert(
                    0, (-1.0 / 12.0 * fieldVectors[0] + 2.0 / 3.0 * fieldVectors[1] - 2.0 / 3.0 * fieldVectors[3] + 1.0 / 12.0 * fieldVectors[4]) / currentStep)

        print('')
        # save the latest row of the 'derivatives' in the matrix
        #
        derivatives[:, i] = np.mean(np.array(differenceQuotients), 0)
        differenceQuotients.clear()

    return derivatives
Пример #7
0
def manualFeedback(currentStep=20, maxCorrection=30, threshold=0.5):
    """
    Feedback control of vector magnet to obtain any desired magnetic field. Enter all parameters by hand.

    Args:
        currentStep (int, optional): The maximum step size for derivative (system) estimation. Defaults to 20.
        maxCorrection (int, optional): The maximum step size for current correction. Defaults to 30.
        thresholdPercentage (float, optional): Percentage of total magnitude the error needs to exceed to be considered. Defaults to 0.02.

    """
    global flags  # for handling input
    # currents to set on ECB
    desCurrents = [0] * 3
    # we want to find the 'best' current configuration for a certain direction ultimately.
    # So we save the current values where the error is below the threshold, at the three
    # coil values will be averaged.
    goodCurrentValues = []
    # queue with the last measured magnetic field vector
    # fieldVectors = []

    # # future option: enter the field in either coordinate system
    coordinates = input('cartesian or spherical coordinates? ')
    if coordinates == 'c':
        print('Enter the desired magnetic Field vector:')
        magneticField_x = input('B_x = ')
        magneticField_y = input('B_y = ')
        magneticField_z = input('B_z = ')
    elif coordinates == 's':
        print('Enter the desired magnetic Field magnitude and direction:')
        magneticField = input('B = ')
        theta = input('theta = ')
        phi = input('phi = ')
    else:
        print('Enter the desired magnetic Field vector (cartesian):')
        magneticField_x = input('B_x = ')
        magneticField_y = input('B_y = ')
        magneticField_z = input('B_z = ')

    try:
        # desired magnetic (flux density) field used as reference
        if coordinates == 's':
            magneticField = float(magneticField)
            desiredBField = tr.computeMagneticFieldVector(
                magneticField, float(theta), float(phi))
        else:
            desiredBField = np.array([float(magneticField_x), float(
                magneticField_y), float(magneticField_z)])
            magneticField = np.linalg.norm(desiredBField)
    except BaseException:
        desiredBField = np.array([20, 0, 0])
        print("There was an issue setting the magnetic field. Using the default value (20mT in x direction).")

    # connect with ECB
    psu = PowerSupplyCommands()
    psu.openConnection()
    # initialize sensor to start measuring
    with MetrolabTHM1176Node(period=0.05, range='0.3 T', average=20) as node:
        # node = MetrolabTHM1176Node(period=0.1, range='0.3 T', average=5)
        configCurrents = tr.computeCoilCurrents(desiredBField)
        # compute linearization Matrix around the current operating point
        derivatives = localLinearization(psu,
                                         node, configCurrents=configCurrents,
                                         currentStep=currentStep, numSteps=10)
        print(derivatives)
        psu.disableCurrents()

        desCurrents[0:3] = configCurrents.tolist()
        psu.setCurrents(des_currents=desCurrents)
        goodCurrentValues.append(desCurrents[0:3])

        # 'Press Enter to quit' prompt
        test_thread = inputThread(1)
        test_thread.start()
        # Active monitoring & controlling section
        while flags[0]:
            newBMeasurement = gen.sensor_to_magnet_coordinates(
                np.array(node.measureFieldmT()))

            B_magnitude = np.linalg.norm(newBMeasurement)
            theta = np.degrees(np.arccos(newBMeasurement[2] / B_magnitude))
            phi = np.degrees(np.arctan2(
                newBMeasurement[1], newBMeasurement[0]))

            if flags[0]:
                print(f'\rMeasured B field: ({newBMeasurement[0]:.2f}, {newBMeasurement[1]:.2f}, {newBMeasurement[2]:.2f})'
                      f' / In polar coordinates: ({B_magnitude:.2f}, {theta:.2f}°, {phi:.2f}°)    ', sep='', end='', flush=True)

            # currentStep = currentStep * (decayFactor ** i)
            errorsB = newBMeasurement - desiredBField
            fieldDiff = np.zeros(3)
            # if the error of some component is greater than 2%, it needs to be decreased.
            if abs(errorsB[0]) >= abs(threshold):
                # decrease the B_x component (proportional to the error)
                fieldDiff[0] = -errorsB[0]
            else:
                goodCurrentValues.append(desCurrents[0:3])

            if abs(errorsB[1]) >= abs(threshold):
                # decrease the B_y component (proportional to the error)
                fieldDiff[1] = -errorsB[1]
            else:
                goodCurrentValues.append(desCurrents[0:3])

            if abs(errorsB[2]) >= abs(threshold):
                # decrease the B_z component (proportional to the error)
                fieldDiff[2] = -errorsB[2]
            else:
                goodCurrentValues.append(desCurrents[0:3])

            if len(goodCurrentValues) >= 50:
                goodCurrentValues.pop(0)

            deltaCurrent = np.linalg.inv(derivatives).dot(fieldDiff)

            # limit step size
            for i in range(len(deltaCurrent)):
                if abs(deltaCurrent[i]) > maxCorrection:
                    deltaCurrent = np.sign(deltaCurrent) * maxCorrection

            # The set current values are adapted to correct the magnetic field
            desCurrents[0:3] = [int(desCurrents[0] + deltaCurrent[0]), int(desCurrents[1] + deltaCurrent[1]),
                                int(desCurrents[2] + deltaCurrent[2])]
            psu.setCurrents(des_currents=desCurrents)
            # wait for stabilization
            sleep(0.4)

    # current configurations with little to no error are averaged -> hopefully a good estimate
    # of the required currents for a given direction.
    printCurrents, _ = stats.mode(np.array(goodCurrentValues), 0)

    print(printCurrents[0])

    psu.demagnetizeCoils(printCurrents[0])
    # disableCurrents()
    psu.closeConnection()
def readoutMetrolabSensor(node: MetrolabTHM1176Node,
                          measure_runs=1,
                          fname_postfix='data_sets',
                          directory='./data_sets',
                          verbose=False,
                          save_data=True):
    """
    Read measurement outcomes of Metrolab THM1176 sensor and return the estimated B-field [mT] in magnet coordinates, 
    also save data if desired.

    Magnet coordinates: coil 2 is mounted along -y axis.

    Args:
    - node (MetrolabTHM1176Node): represents the Metrolab THM 1176 sensor
    - measure_runs (int): Number of samples per fuction call per sensor
    - save_data (bool): if True, the results are stored in a csv-file
    - directory (string): valid path of the folder where data should be stored. The default name of the data file is
    'yy_mm_dd_hh-mm-ss_{fname_postfix}.csv'
    - fname_postfix (string): postfix of data file (csv).

    Return:
    - meas_time (ndarray of length=measure_runs): Contains the time of each measurement relative 
    to start of measurements 
    - meas_data (ndarray of shape=(measure_runs, 3)): Contains the measured field components

    Exceptions: 
    Raise a MeasurementError if something went wrong during measurements. 
    Possible reasons are that a sensor was skipped or that an incomplete message was received from the sensor. 

    """
    if measure_runs == 1:
        # initialize ndarray to store the measurement data and time
        meas_data = np.zeros(3)
        meas_time = 0

        # get current time before starting
        t_start = time()

        # read the current output of sensor and save measured magnetic field
        meas_data = np.array(node.measureFieldmT())

        # save the current time
        meas_time = time() - t_start

    elif measure_runs > 1:
        # get current time before starting
        # t_start = time()

        # read the current output of sensor and save measured magnetic field
        meas_data = np.array(node.measureFieldArraymT(measure_runs)).swapaxes(
            0, 1)

        # assume that each measurement took the same time, such that the times of measurements
        # are equally spaced between initial and final time and offset such that t_start = 0
        meas_time = np.linspace(0, time() - t_start, num=len(meas_data))

    else:
        raise ValueError("measure_runs must be >= 1!")

    # due to the setup, transform sensor coordinates to magnet coordinates
    meas_data = sensor_to_magnet_coordinates(meas_data)

    # save data if desired
    if save_data:

        # Measurement Time (s), Sensor Number, X-Axis (mT), Y-Axis (mT), Z-Axis (mT)
        df = pd.DataFrame({
            'Measurement Time (s)': meas_time,
            'Bx [mT]': meas_data[:, 0],
            'By [mT]': meas_data[:, 1],
            'Bz [mT]': meas_data[:, 2]
        })

        if directory is None:
            directory = os.getcwd()
        ensure_dir_exists(directory)

        now = datetime.now().strftime("%y_%m_%d_%H-%M-%S")
        output_file_name = "{}_{}.csv".format(now, fname_postfix)
        data_filepath = os.path.join(directory, output_file_name)

        try:
            df.to_csv(data_filepath, index=False, header=True)
        except FileNotFoundError:
            data_filepath = os.path.join(os.getcwd(), output_file_name)
            df.to_csv(data_filepath, index=False, header=True)

    return meas_time, meas_data
    # # save raw data if desired
    # if save_mean_data:
    #     # if no directory is provided, just take current working directory
    #     if directory is None:
    #         directory = os.getcwd()
    #     save_in_dir(meas_data, directory, 'data', now=True)

    # return mean and std fields either for only this sensor or for all sensors
    return mean_data, std_data


#%%
if __name__ == '__main__':

    with MetrolabTHM1176Node() as sensor:
        # first try with single measurements using measureFieldmT method of sensor
        meas_times, field = readoutMetrolabSensor(sensor,
                                                  measure_runs=10,
                                                  directory='./test_data')

        durations = meas_times[1:] - meas_times[:-1]
        print('average duration (single) = {:.5f} s +- {:.5f} s'.format(
            np.mean(durations), np.std(durations)))
        print(field)

        # then try with array of measurements using measureFieldArraymT method of sensor
        # meas_times, field = readoutMetrolabSensor(sensor, measure_runs=10, directory='./test_data',
        #                                             single_measurements=False)
        # durations = meas_times[1:] - meas_times[:-1]
        # print('average duration (arrays) = {:.5f} s +- {:.5f} s'.format(np.mean(durations), np.std(durations)))