Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Can't get SpO2 to work #13

Closed
robcazzaro opened this issue Apr 16, 2020 · 29 comments
Closed

Can't get SpO2 to work #13

robcazzaro opened this issue Apr 16, 2020 · 29 comments

Comments

@robcazzaro
Copy link

First of all, thanks so much for the work you did and for making it available.

I'm having a problem making the code work properly and I'm hoping you can point me in the right direction.

I'm using an ESP32, which is many times faster than the M0 processor you are using. And I have quite a lot of ESP32 experience, so I'm sure I2C works properly. I built a separate ESP32/MAX30102 project and that worked perfectly, so I checked the code for any potential problem and I think that the data reading from the sensor is working properly. I also have 2 different MAX30102 boards, and there is no real difference in the results between them

I'm including 2 files, one a data capture in txt format, the other I imported the same data as CSV into Excel and plotted the graph for the first 100 samples. To my eyes, that looks like an almost perfect graph, so I was expecting the code to work. The only potential issue is the the amplitude of the IR signal is much lower than the RED one

MAX30102.xlsx
MAX30102.txt

You will notice from the CSV that I'm printing both the results of your algorithm and the Maxim results and, if anything, the Maxim ones are worse, so I'm struggling to understand where the problem is. Your algorithm does a great job of reporting the heart rate (double checked with a commercial pulse oximeter), but does not report any valid SpO2 result (flag is 0, result is -999.00, expected when the flag is 0)

I tried changing the LED intensity (registers 0x0C and 0x0D), but it doesn't make a real difference, outside of "shifting the curves up" when I do it

Thanks in advance for any idea/pointer to troubleshoot my problem

@aromring
Copy link
Owner

Hi Rob,
Thank you so much. Yours is the perfect model of a proper bug report submitted with enough information and data. Others should learn from you! Usually, I am getting no more than 1-3 sentences, like "It doesn't work. Help me." Some people expect me to have psychic powers. ;)
Since I am dedicating part of this weekend to looking at MAX30102_by_RF issues your submission will get the first priority. Stay tuned.

@aromring
Copy link
Owner

Hmm, at the first glance the raw signals produced by your sensor are very good. I can't imagine why there would be no SpO2 output. Hence, debugging time... Stay tuned; I should have something by Sunday.

@robcazzaro
Copy link
Author

:) thanks for the nice words. I used to work in support for a long time, and I know perfectly well what you mean.

You might notice from the file capture that I slightly changed the print() format for the algorithm results, but that should not make any difference. As you saw, the data capture seems good, but I didn't dig too deep into the algorithm yet.

I wonder if it's possible that some of the ESP32 math libraries produce different results due to rounding errors or such, but I'm not aware of any problem in that area currently and by now the ESP32 toolchain is really mature, so it would be a surprise if that's the case.

The only other thing I can think about is that I had some issue reading the MAX30102 in my other project (in that case I was reading 2 MAX30102 at the maximum speed of 400Hz and sending data via BLE, so any small glitch caused problems with the FIFO reading and my data could get out of sync. I'm wondering if it's possible that the RED and IR data are shifted by 1 frame due to I2C problems with the ESP32. I have a STM32F104 board, and I can try on that (M3 @72MHz, so closer to the board you used), if the ESP32 is a dead end

Anyway: I truly do appreciate your time, and I'd be happy to try anything to make this work and see if your code can work on the ESP32, which is a pretty common and cheap board. The value of the ESP32, is that it's then easy to use the built-in BLE to create a truly wireless solution and act as a heart rate sensor for other apps

Needless to say, there's no rush.

@aromring
Copy link
Owner

aromring commented Apr 19, 2020

Well, no line debugging was necessary. I've figured what happened using an Excel spreadsheet.
The answer to your problem is the applicability domain of the Maxim's calibration equation:

SpO2 = (-45.06*Z + 30.354)*Z + 94.845

where Z is defined in my Instructable (roughly, it's a ratio of normalized variabilities of RED to IR signals). The above equation is an inverted parabola with one negative root and one positive root at ~1.826. Z values above the latter would result in nonsensical negative SpO2 values.
Here is an example of expected raw signals in the first graph and centered/leveled ones in the second:
image
As you can see, normalized AC/DC ratio for RED should be lower than IR. The Z value in the case is 0.4589 and SpO2 = 99.28% - reasonable.
And here are analogous graphs plotted from your signal:
image
In contrast to previous example, RED's AC/DC far exceeds IR's. The Z value in your case is 1.958 which would produce SpO2 = -18.52%. The following line of code:

if(xy_ratio>0.02 && xy_ratio<1.84) { // Check boundaries of applicability

captures this case and sets *pch_spo2_valid flag to 0.
BUT, if you switch your RED and IR signals, then Z = 0.51 and SpO2 = 98.59%. Therefore, I am suspecting that in your setup you've mixed RED with IR and vice versa.

@robcazzaro
Copy link
Author

That's interesting... I don't think I switched anything like that in the code, but I wonder if the I2C FIFO reading loop is doing something weird.

I'll do some more tests and get back to you with the results. For now, thanks for the great analysis!

@arzaman
Copy link

arzaman commented Apr 19, 2020

I'm using an ESP32, which is many times faster than the M0 processor you are using. And I have quite a lot of ESP32 experience, so I'm sure I2C works properly. I built a separate ESP32/MAX30102 project and that worked perfectly, so I checked the code for any potential problem and I think that the data reading from the sensor is working properly. I also have 2 different MAX30102 boards, and there is no real difference in the results between them

I'm doing same attempt to run the amazing @aromring code running on top of the ESP32
did you fork the project and have a repository for that ?
any direction / suggestions to adapt to ESP32 ? I had some compilation error

_

In file included from C:\Users\arzaman\Documents\Arduino\RD117_ARDUINO\RD117_ARDUINO.ino:35:0:

algorithm_by_RF.h:45:12: error: expected identifier before numeric constant

#define FS 25 // Sampling frequency in Hz. WARNING: if you change FS, then you MUST recalcuate the sum_X2 parameter below!

C:\Users\arzaman\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.4\libraries\FS\src/FS.h:86:7: note: in expansion of macro 'FS'

class FS
_

If I comment the define USE_ADALOGGER the compilation is OK

will follow also the thread for calibration issue
thank
Davide

@robcazzaro
Copy link
Author

robcazzaro commented Apr 20, 2020

I did more tests, and the only possible conclusion is that indeed the IR and RED LEDs are swapped. But the weird thing is that they are not swapped in my code, but on the chip itself (!). For what is worth, I'm using a Chinese MH-ET LIVE MAX30102 module. But I really doubt that Chinese cloners reverse engineered the Maxim part. More likely it's a different silicon revision (I have a contact in Maxim, will try to see if they know anything about it)

According to the MAX30102 datasheet, LED1 is RED, LED2 is IR. And when reading the FIFO, the first 3 bytes are the RED channel values, the following 3 are IR, then repeats for all the samples

But not on my 2 devices. The actual LEDs on the chip are swapped. It's pretty easy to see if that's the case:

In file max30102.cpp, change as follows

if(!maxim_max30102_write_reg(REG_LED1_PA,0x00))   //Choose value for ~ 7mA for LED1
    return false;
if(!maxim_max30102_write_reg(REG_LED2_PA,0x24))   // Choose value for ~ 7mA for LED2
    return false;

According to the datasheet, only the IR LED should be active. But in my case, only the RED one is.

Then I tried

if(!maxim_max30102_write_reg(REG_LED1_PA,0x24))   //Choose value for ~ 7mA for LED1
    return false;
if(!maxim_max30102_write_reg(REG_LED2_PA,0x00))   // Choose value for ~ 7mA for LED2
    return false;

And in this case only the IR LED is active: use a phone camera to look at the LED in a dark room, you will see a faint purple dot, since phone cameras are sensitive to IR (if you use 0x80 instead of 0x24, the IR is even more visible)

So, unsurprisingly, when reading the FIFO, the values are also swapped.

It's trivial to fix, by simply changing one line in the main loop() from"

maxim_max30102_read_fifo((aun_red_buffer + i), (aun_ir_buffer + i)); //read from MAX30102 FIFO

to

maxim_max30102_read_fifo((aun_ir_buffer + i), (aun_red_buffer + i)); //read from MAX30102 FIFO

Just in case, I checked the device RevID and PartID , and I got 0x03 and 0x15

  uint8_t revID;
  uint8_t partID;

  maxim_max30102_read_reg(0xFE, &revID);
  maxim_max30102_read_reg(0xFF, &partID);
  Serial.print("Rev ");
  Serial.print(revID), HEX;
  Serial.print(" Part ");
  Serial.println(partID, HEX);

Would be interesting to know what your MAX30102 reports

As for the ESP32 port, there is really nothing to do. Simply use the right SDA (D21) and SCL (D22) pins and an interrupt pin (I usedD19, changing the oxiInt value to 19), commented out #define USE_ADALOGGER, compiled and everything worked just fine, happily printing values (when valid) in the serial monitor output. The ESP32 has definitely more than enough computing power and memory to handle something like this

Thanks again for the code and the troubleshooting help. Hopefully knowing that some modules might have RED and IR swapped could help other folks. By zeroing out one of the two REG_LEDx_PA
registers it's pretty easy to see if the module follows the datasheet or if LED1 and LED2 are reversed

I would suggest adding part of the above to your troubleshooting steps, to avoid more people pinging you about this if they also use Chinese modules

@arzaman
Copy link

arzaman commented Apr 20, 2020

As for the ESP32 port, there is really nothing to do. Simply use the right SDA (D21) and SCL (D22) pins and an interrupt pin (I usedD19, changing the oxiInt value to 19), commented out #define USE_ADALOGGER, compiled and everything worked just fine, happily printing values (when valid) in the serial monitor output. The ESP32 has definitely more than enough computing power and memory to handle something like this

I'm still a step back compared to you :-) stil struggling to get some output
I use EP32 pico m5stick board and connected SDA 32 and SCL 33 (if I run ESP32 port scanner I can detect the MAX3012 on the right addres) and oxInt pin to 26
commented out #define USE_ADALOGGER (this still gives me compliation error)

but when I run the code no coversion happens
on serial monitor I just get
Press any key to start conversion
Time[s] SpO2 HR Clock Ratio Corr

but no data

if I run the DEBUG mode I can see the row data of R amd IR led and seems OK

MAX30102.txt
MAX30102.xlsx

r-ir

so seems conversion won't start
any idea why the conversion is no perfomed ?

thnaks
Davide

@robcazzaro
Copy link
Author

robcazzaro commented Apr 20, 2020

Your module, like mine, has the IR and RED channels swapped. So you also need to change the same line I changed into

maxim_max30102_read_fifo((aun_ir_buffer + i), (aun_red_buffer + i)); //read from MAX30102 FIFO

Keep in mind that if the SpO2 reading is not valid, the code doesn't print anything. It only prints when both the heart rate and SpO2 are valid. So when IR and RED are swapped, there is no output unless you use DEBUG, then you will see that it prints -999.0 when there is no valid reading

As I said, I think that the majority of the Chinese modules on sale have swapped IR and RED. It would be possible to check in the main algorithm if the values are swapped and automatically fix it, but IMHO would be a waste of time: after all, it only takes a minute to figure it out and once figured it out, that will never change for the lifetime of the device

Re-read my previous entry, and you will see how to check if your LEDs are reversed

@arzaman
Copy link

arzaman commented Apr 20, 2020

update
I swap RED and IR reading of the fifo as suggested by @robcazzaro and conversion now works fine !!
I have same Chinese clone module
thanks so much I will never realize such a problem without your help

Davide

@arzaman
Copy link

arzaman commented Apr 21, 2020

One more question/doubt
I see that in @aromring implementation the INT signal of MAX30102 is is controlled by the SW
From data sheet there are several status when the interrupt is triggered (FIFO data ready etc..), what is the case in this sketch ?
I see other examples that don't use the INT pin so I expect application does continuos polling of the FIFO register, is there any way to avoid INT usage ?...reason is simple save on more wire to the breakout sensor and just rely on I2C bus and simply the connection and wiring
What happens if I force the INT with pull down resistor on the breakout board ?

thanks again for any suggestions
Davide

@robcazzaro
Copy link
Author

Davide, it's considered bad Github etiquette to hijack an existing issue to ask unrelated questions. ESP32 port questions might have been on topic, but changes to the code are not

The MAX30102 datasheet is easily available, as are multiple open source MAX30102 libraries that can be used with and without interrupt. The original code is easy to read and well structured, so you can simply use any method to read an array of 100 IR and RED values and invoke the rf_heart_rate_and_oxygen_saturation(() function

Please refrain from adding additional comments on this issue

@aromring
Copy link
Owner

Rob,
Thank you very much for keeping this forum clean.
Since it was determined the SpO2 problems were related to cloned hardware, I am closing this issue.

@janusf
Copy link

janusf commented Jul 11, 2020

You guys are awesome. I had the same problem. Did not realize I got a clone sensor until I read this thread. Thanks!!

@theophyk
Copy link

theophyk commented Sep 25, 2020

I did more tests, and the only possible conclusion is that indeed the IR and RED LEDs are swapped. But the weird thing is that they are not swapped in my code, but on the chip itself (!). For what is worth, I'm using a Chinese MH-ET LIVE MAX30102 module. But I really doubt that Chinese cloners reverse engineered the Maxim part. More likely it's a different silicon revision (I have a contact in Maxim, will try to see if they know anything about it)

@robcazzaro Have you followed up on this issue to see whether this behavior is specific to a certain silicon revision of MAX30102 or the chips used on MH-ET LIVE MAX30102 are indeed clones expressing this kind of behavior? I tried to contact the guys at Maxim about this but didn't have any luck...

P.S. I also noticed that in addition to IR and red led channels being swapped, both PPG_ RDY and ALC_ OVF interrupts don't work either on this particular board.

@robcazzaro
Copy link
Author

robcazzaro commented Sep 27, 2020

@Alireza9900 I tried to get answers from Maxim, but never got a reply. If you get one, please share. The MAX30102 is a pretty old and simple chip and mostly used by hobbyists. So it wouldn't be that hard to clone and it wouldn't surprise me if many of the cheap eBay modules use cloned chips. After all, the STM32F103 series chips have been cloned by many Chinese makers, and that's a much harder chip to clone

@aromring
Copy link
Owner

Alireza and Rob,
Both of you may save yourself some energy and stop "trying to get answers from Maxim". Just calm down for a moment and think: how could Maxim be responsible for what cloners do? Especially the ones who stole their design?
Imagine you are a farmer that participates, e.g., in a pumpkin growing contest. Your competing neighbor steals your recipe and manages to grow an equally big pumpkin and thus get some share of the local food market. Now imagine customers who buy the thieving neighbor's pumpkins come to you with complaints that these don't taste good. What would be your response, hmm?
Maxim is trying to be very polite by not responding at all. Otherwise, they have every right to give you an earful of you-know-what.

@robcazzaro
Copy link
Author

robcazzaro commented Sep 27, 2020

@aromring I think you completely missed the point of the discussion. There are 2 possibilities here:

  1. Maxim created a different revision of the silicon, which swapped the LEDs without documenting it. Unlikely, but not unheard of
  2. Someone managed to clone the MAX30102 and made a mistake cloning it. That is also unlikely, since there aren't a lot of complex chips that get cloned, and the MAX30102 is cheap enough and not used in huge quantities, that cloning won't easily recover the cost. Also, there are no other reports on the internet of clones existing. So we have 2 unlikely scenarios

We are trying to get an answer from Maxim on "did you spin a revision with swapped LEDs?". If the answer is "no", then we will know for sure it's a clone and very likely much more than just the swapped LEDs are a problem (very likely in that case the readings are also going to be poor quality). And we can then share the knowledge with sites like Hackaday and others to warn other hobbyists. As things are now, there is only speculation. Something like this, to be clear https://hackaday.com/2020/09/15/deep-sleep-problems-lead-to-forensic-investigation-of-troublesome-chip/

If on the other hand Maxim created a different revision, then we know it's a simple fix and once again can share the knowledge. And know that once you swap the LEDs in the code, everything else works as expected

Nobody is trying to hold Maxim responsible in any way for problems created by a potential clone. Just get more info on what happened, and know more. And I strongly believe it's in Maxim's interest to let people know if a counterfeit chip exists, since it might otherwise reflect poorly on them

@aromring
Copy link
Owner

OK, fair enough, I stand corrected. However, if that's the case, then Maxim's silence is deafening in spite of numerous people asking this question. IMHO, it means that Maxim at least think of the scenario 2 rather than 1.

@robcazzaro
Copy link
Author

Well, in my case it actually means that my contact left Maxim, so I never could get an answer :) not that Maxim refused to answer. I pinged another contact today and I will see if I get a reply. But you have to realize that, since this is not a work request, Maxim employees might simply prioritize it too low to answer (and I said as much in my request, I cannot burn my goodwill with a Maxim employee I interact thru one of my consulting projects. I'm sure Maxim people are plenty busy answering questions from actual customers who buy hundreds of chips, not hobbyists with a curiosity. If my module came from an approved distributor, I'm also sure that they would have jumped on it. But since it was a one off from eBay, well...

So, please, don't read too much into the lack of an answer. It really means nothing in my specific case

@Anyeos
Copy link

Anyeos commented Dec 22, 2020

I don't understand really how it can be a clone because the quality of the glass and the terminals are good enought to guess it is a really working thing.

Here a picture of the leds:
MAX30102-inverted-leds

Here one for the chip part:
MAX30102-chip-part

@Anyeos
Copy link

Anyeos commented Dec 23, 2020

Alireza and Rob,
Both of you may save yourself some energy and stop "trying to get answers from Maxim". Just calm down for a moment and think: how could Maxim be responsible for what cloners do? Especially the ones who stole their design?
Imagine you are a farmer that participates, e.g., in a pumpkin growing contest. Your competing neighbor steals your recipe and manages to grow an equally big pumpkin and thus get some share of the local food market. Now imagine customers who buy the thieving neighbor's pumpkins come to you with complaints that these don't taste good. What would be your response, hmm?
Maxim is trying to be very polite by not responding at all. Otherwise, they have every right to give you an earful of you-know-what.

You are right but if I buy from other farmer with a bad taste and I go to the original farmer then he/she must tell me exactly what happened if he/she want to sell me in a future. I will buy him/her if that is the case.

But as I can guess there apperas to be a permitted cloned market. Or maybe they don't sell directly assembled chips but only the specifications. So, maybe the ones that we have are "original" but from one of the assemblers that there are lying around.

And I need an answer because this chip have the name of MAXIM. So MAXIM is permitting to have they name on something that they don't made? Really? That is something suspicious. Meanwhile the solution is just invert the channels but the doubt is still alive because I don't have the certainty over the specs of this chip. So, if this chip does not meet the specs I want to know.

Maxim must write a public letter to clarify this.

@moononournation
Copy link

Anyone can post some photo about original and cloned board? I want to confirm if my board is clone one and if channels are reversed.

@aromring
Copy link
Owner

Sorry guys, @Anyeos and @moononournation, although I sympathize with your woes this is not the right forum to ask these questions. The right forum is here: https://maximsupport.microsoftcrmportals.com/en-US/support-center/
I have no affiliation with Maxim Integrated, Inc., and simply don't know any helpful answers to your questions.
Other than that, have Happy Holidays!

@Anyeos
Copy link

Anyeos commented Dec 24, 2020

Sorry guys, @Anyeos and @moononournation, although I sympathize with your woes this is not the right forum to ask these questions. The right forum is here: https://maximsupport.microsoftcrmportals.com/en-US/support-center/
I have no affiliation with Maxim Integrated, Inc., and simply don't know any helpful answers to your questions.
Other than that, have Happy Holidays!

I agree and I will not put future messages about it. But please dont remove the actual ones because it is not out of scope. We are using a library designed for Maxim chips on the first place and there are a relation with how the chip is working. Except the portion about the company decisions but the rest is related.

@TheBluePhoenix10
Copy link

TheBluePhoenix10 commented Apr 18, 2021

I am facing the same problem. Reversed LED Channels that caused erroneous spo2 and Hr measurements in mode 2. To check the sensor I just tried to run it in mode 1 (only RED LED on) and found out that the IR LED was ON instead. Never realised it was this big a problem since there are no other mentions of this issue anywhere else. I wanted to ask a few things -

  • Can someone share a few links where good quality MAX30102 sensors are available? Tried and tested if possible...
  • Does the above change work on the SparkFun Arduino max library as well? (if someone has tested it, otherwise I'll test it myself) I have already implemented that, and except for the values, everything else is working fine so I would prefer if I could edit the Sparkfun library directly. I will surely try the algorithm posted on this repo sometime.

It's amazing that someone has converted the instructable post into actual working code.

Thanks a lot!

@hishd
Copy link

hishd commented Aug 22, 2021

@robcazzaro kudos for your proper research and your conclusion on the LED swap on MAX30102 clones. I was facing days with figuring this issue and you just helped me with solving the issue with just changing 2 lines of code.

Really appreciate your effort on identifying the issue and suggesting the solution.....!!!

@viafx24
Copy link

viafx24 commented Aug 19, 2022

Thanks to Robcazzaro and Aromring for precious advises. The procedure from robcazzaro perfectly works for me on a esp32 with the MH-ET live max30102. I add some details for newbies like me.

  • First i solder header pins one the side of the int pin to deal with this difference with the sparkfun code that doesn't use the int pin.
  • I created a new project under platformio (could have been done under arduino ide as well)
  • I download the zip folder of MAX30102_by_RF and extract it.
  • I pasted the resulting unzip folder in the lib directory of my platformio project. I deleted the .ino, .png, .csv and readme to let only .h and .cpp
  • in the main.cpp of the src folder of my platformio project, i pasted the code from RD117_ARDUINO.ino.
  • Then i modified a bit the include part to look like this (but it's probably not important):

#include <Arduino.h>
#include <algorithm_by_RF.h>
#include <max30102.h>
#include <SPI.h>`

  • then I changed the interrupt pin from 10 to 19 as robcazzaro did. It's a convenient pin because just closed to gnd, and gpio 21 and 22 (sda and scl)

  • then, in the main.cpp, I changed the line concerning the hardware switch of the led as Robcazzaro found it from
    maxim_max30102_read_fifo((aun_red_buffer + i), (aun_ir_buffer + i)); //read from MAX30102 FIFO
    to
    maxim_max30102_read_fifo((aun_ir_buffer + i), (aun_red_buffer + i)); //read from MAX30102 FIFO

  • then, the compilation failed because the only function of the script (millis_to_hours) needed to befined at the top of the script (probably a small difference between .cpp and .ino and if one uses a .ino file, this may probably be ignored) . I thus add this line juste before the setup() function:
    void millis_to_hours(uint32_t ms, char* hr_str)

  • Then I compiled and transfered the sketch and it works. I got 99% of spo2 and around 60 of heart rate. I performed somes tests and if i do some exercices, the heart rate increase accordinlgy. If I stopped breathing until I just can't maintain it, the spo2 decreases but with a delay and not a lot (from 99% to 97%). I would have expected a quicker response and a stronger decrease (for instance to 90%) but i'm not an expert on that stuff and it could be the physiological reality.

Thus, for me, it looks to work pretty well and i'm quite satisfied by the library and the trick of the hardware switch of red and IR led. My only concern is that I can't verify the quality of spo2 in pathologic situation (i.e when spO2 is smaller than 95%) and since i don't have a commercial apparatus, I can't compare data as well but i would bet that comparison may be quite good. Any suggestion to simulate smaller Spo2 to be confident about the data in pathologic situations ?

Thanks a lot.

Cheers.

Via_Fx_24

@viafx24
Copy link

viafx24 commented Aug 19, 2022

Concerning my previous comment, I made some quick tests with a commercial apparatus that i borrow. Results were very similar. 99% of spo2 for the max30102 (right hand), 97% of spo2 for the commercial one (left hand). A small difference of 2%. Stopping breathing fall to 97% for max30102 et to 95% for the commercial one. Thus the difference (2%) between the two apparatus remains the same giving good confidence in max30102 data. Concerning heart rate, both apparatus found 68 bpm +/- 2. I' m thus quite confident with the data provided by this code and the hardware max30102. This comment may be considered "out of range" concerning the initial question. However I added it to encourage folks that get the MH-ET live max30102 to try the code" MAX30102_by_RF" with the modification proposed by Robcazzaro because it works well, it's an easy modification and results are quite consistent with professional apparatus. Hope it's help.
Cheers.
Via_Fx_24

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants