brainflow icon indicating copy to clipboard operation
brainflow copied to clipboard

WIP initial commit for issue 327: Add Swift binding

Open ScottThomasMiller opened this issue 4 years ago • 123 comments

This is my first-ever contribution to an open source project! Thanks to Andrey for his guidance and for inviting me to contribute these Swift bindings for BrainFlow. And thanks to William @ OpenBCI for his timely chime-ins on the BrainFlow Slack channel.

BoardShim.swift is fully coded and unit-tested using the synthetic board. DataFilter.swift is only about 25% complete, but I figured it's OK to include it in this WIP PR. Both modules are modeled after their Java counterparts.

Other to-do's include:

  • bridging header
  • libBoardController.dylib and libDataHandler.dylib, built for iOS on arm64, with build instructions
  • example Xcode project with unit tests

ScottThomasMiller avatar Sep 09 '21 15:09 ScottThomasMiller

Thanks, @ScottThomasMiller, I will review it soon and will do some minor changes

Andrey1994 avatar Sep 09 '21 15:09 Andrey1994

Per Andrey, I will move all global functions in BoardShim.swift to static functions within the type definition.

ScottThomasMiller avatar Sep 11 '21 11:09 ScottThomasMiller

I am working on the binding for perform_wavelet_transform in DataFilter.swift. The Java version returns a Pair type. The internet says a Java Pair is sort of like a Swift dictionary, with key/value pairs. But in your example code, you seem to use the return value like a normal tuple. Should my binding return a plain vanilla tuple, or a dictionary?

ScottThomasMiller avatar Sep 11 '21 15:09 ScottThomasMiller

Its a tuple of two arrays

Andrey1994 avatar Sep 11 '21 15:09 Andrey1994

For perform_fft, which returns Complex[], I can think of three options off the top of my head:

  1. Write my own Complex data type in Swift, modeling it after https://introcs.cs.princeton.edu/java/97data/Complex.java.html or similar.
  2. Use the Complex data type already defined by Apple in the Swift Numerics package.
  3. Simply return a tuple of [Double] arrays.

Option 1 will take some time. Option 2 will introduce a dependency on a package that is not included in the standard Swift library. Option 3 forces the caller to do their own numeric processing of complex numbers.

ScottThomasMiller avatar Sep 11 '21 22:09 ScottThomasMiller

How does the process of installing new packages(dependencies) look like in Swift? If it's smth like in python or in java(declare them in files like setup.py or pom.xml and they are downloaded automatically) its ok to introduce Swift Numerics as a dependency.

Andrey1994 avatar Sep 12 '21 08:09 Andrey1994

I think we need to add smth like this https://developer.apple.com/documentation/xcode/creating_a_standalone_swift_package_with_xcode and declare it as a dependency in project file

Andrey1994 avatar Sep 12 '21 08:09 Andrey1994

It's pretty easy. In my Xcode project for my iOS app, I selected File-> Swift Packages->Add package dependency. And then I entered the Github URL for the Swift Numerics package: https://github.com/apple/swift-numerics.git. And then I followed the prompts to assign various dependencies. Xcode downloaded the package and set the build dependencies per the prompts.

And then, at the top of DataFilter.swift I added:

Import ComplexModule

Now I can declare the FFT function as returning an array of Complex Doubles, i.e.:

static func performFFT (data: inout [Double], startPos: Int32, endPos: Int32, window: Int32) throws -> [Complex<Double>]

I will look into the package bundling, per the link you sent.

ScottThomasMiller avatar Sep 12 '21 16:09 ScottThomasMiller

Correction: I have to import the Numerics package itself, not ComplexModule. Otherwise it seems to be working fine.

ScottThomasMiller avatar Sep 13 '21 15:09 ScottThomasMiller

Its fine. We need to create brainflow package like its described at the link I've shared and add it as a dependency

Andrey1994 avatar Sep 13 '21 16:09 Andrey1994

DataFilter.swift is 100% coded but only about 10% tested. I need help with its unit tests, specifically with designing a small handful of hardcoded input data arrays for which we know the expected output.

I am working on the package. It's not easy. It won't build without the BrainFlow libraries. I think I need to create a framework for the C++ libs, and then include that framework as a dependency in the package.

ScottThomasMiller avatar Sep 15 '21 14:09 ScottThomasMiller

<< DataFilter.swift is 100% coded but only about 10% tested. I need help with its unit tests, specifically with designing a small handful of hardcoded input data arrays for which we know the expected output.

You should take a look at tests for other languages, you can find them here https://github.com/brainflow-dev/brainflow/tree/master/tests/python for example. Also, need to add it to CI pipelines https://github.com/brainflow-dev/brainflow/blob/master/.github/workflows/run_unix.yml

<< I think I need to create a framework for the C++ libs, and then include that framework as a dependency in the package.

No, you need to reuse CMake pipelines as for other languages and copy libs to a correct folder for swift. These libs should be packages together with swift binding

Andrey1994 avatar Sep 15 '21 14:09 Andrey1994

About copying libs, there are multiple files like build.cmake in different folders and there are lines like this one https://github.com/brainflow-dev/brainflow/blob/master/src/board_controller/build.cmake#L168 it should be added for swift. Easiest option to find them all is find . - name build.cmake

Andrey1994 avatar Sep 15 '21 14:09 Andrey1994

In band_power_all.py, what are the expected values for band[0] and band[1]?

ScottThomasMiller avatar Sep 17 '21 17:09 ScottThomasMiller

If you use all eeg channels from synthetic board these values should be low

Andrey1994 avatar Sep 17 '21 18:09 Andrey1994

There's a bug in DataFilter.getAvgBandPowers(). It's returning empty arrays. I'm looking into it.

ScottThomasMiller avatar Sep 19 '21 23:09 ScottThomasMiller

The bug was in my unit test, not in DataFilter. I updated my unit test, so that now, at the end of it, it does the following:

            let bands = try DataFilter.getAvgBandPowers(data: data, channels: EEGchannels,
                                                        samplingRate: samplingRate, applyFilters: true)
            
            let avgSum = bands.0.reduce(0, +)
            let stdSum = bands.1.reduce(0, +)
            XCTAssert((bands.0.count == 5) && (bands.1.count == 5) &&
                      (avgSum > 0) && (avgSum <= 1) && (stdSum > 0) && (stdSum < 10))

I ran the test successfully, 10 times in a row.

ScottThomasMiller avatar Sep 20 '21 13:09 ScottThomasMiller

Good, thanks! Let's also add 3rd main component(MLModel) and I will help to update the packaging

Andrey1994 avatar Sep 20 '21 13:09 Andrey1994

Will do, as soon as I finish testing DataFilter.

ScottThomasMiller avatar Sep 20 '21 13:09 ScottThomasMiller

In https://github.com/brainflow-dev/brainflow/blob/master/tests/python/transforms.py, the following call to perform_fft() seems to be missing start/stop or length parameters:

        # demo for fft, len of data must be a power of 2
        fft_data = DataFilter.perform_fft(data[channel], WindowFunctions.NO_WINDOW.value)

ScottThomasMiller avatar Sep 21 '21 13:09 ScottThomasMiller

No, for python it gets them from the shape of ndarray. It's done because of some language-specific implementations of arrays in java and numpy

Andrey1994 avatar Sep 21 '21 13:09 Andrey1994

https://github.com/brainflow-dev/brainflow/blob/master/java-package/brainflow/src/main/java/brainflow/DataFilter.java#L440

here is a comment about it. In c\c++ or in python you can use pointer offsets or slices, in java I didn't find anything like that and decided to add offsets as parameters

Andrey1994 avatar Sep 21 '21 13:09 Andrey1994

I can rewrite DataFilter.performFFT() so that it is modeled after the Python, instead of the Java. Or I can overload it with both signatures.

ScottThomasMiller avatar Sep 21 '21 14:09 ScottThomasMiller

Its up to you

Andrey1994 avatar Sep 21 '21 14:09 Andrey1994

I completed the initial phase of unit testing for DataFilter, using the Python tests as models. So far my unit tests cover only the following DataFilter methods:

deTrend getAvgBandPowers getBandPower getCSP getNearestPowerOfTwo getPSDwelch getWindow performBandpass performBandstop performDownsampling performFFT performHighpass performIFFT performInverseWaveletTransform performLowpass performRollingFilter performWaveletDenoising performWaveletTransform removeEnvironmentalNoise

I will start including my unit test modules in the next commit.

ScottThomasMiller avatar Sep 22 '21 13:09 ScottThomasMiller

Is it unit tests or integration tests? We currently use tests as code samples at BrainFlow docs

What you see here https://brainflow.readthedocs.io/en/stable/Examples.html#python is the same as what we run in CI pipelines to test code. And code sample on the docs page should be more like an integration test

Andrey1994 avatar Sep 22 '21 13:09 Andrey1994

These are unit tests for Xcode. They test only that my bindings return data, and that the output data is different from the input data, except testBandPowerAll() which also compares averages. I am starting to include negative tests for specific exceptions.

Per your suggestion I followed https://github.com/brainflow-dev/brainflow/tree/master/tests/python. From that page I implemented the following:

testBandPower testBandPowerAll testCSP testDenoising testDownsampling testSignalFiltering testTransforms testWindowing

For the CI pipeline should I follow the Examples.html#python page instead? Is it possible to use one set of tests for both Xcode unit and Github CI?

ScottThomasMiller avatar Sep 22 '21 15:09 ScottThomasMiller

<<For the CI pipeline should I follow the Examples.html#python page instead? Is it possible to use one set of tests for both Xcode unit and Github CI?

Its great that you've added unit tests, I still want to add some unit tests for C++ code and have no time for it. In CI pipelines we should add both(your unit tests and integration tests like at docs)

Andrey1994 avatar Sep 22 '21 15:09 Andrey1994

In the CI tests I removed the Python-specific stuff about args parsing and pandas plotting. Is that stuff absolutely necessary for CI?

ScottThomasMiller avatar Sep 24 '21 17:09 ScottThomasMiller

"In the CI tests I removed the Python-specific stuff about args parsing and pandas plotting. Is that stuff absolutely necessary for CI?"

Yes, because its the only way to test different devices using a single script, also its convenient to have similar tests across languages. And finally, its only in one test, for others we use synthetic board

Andrey1994 avatar Sep 26 '21 21:09 Andrey1994