Skip to content

VOC Index #9

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

Merged
merged 11 commits into from
Sep 2, 2021
Merged

VOC Index #9

merged 11 commits into from
Sep 2, 2021

Conversation

AVanVlack
Copy link
Contributor

@AVanVlack AVanVlack commented Aug 28, 2021

Resolves #2 and adds a voc index algorithm to measure_index method.

Changes

  • Moved adafruit_sgp40.py to adafruit_sgp40/__init__.py
  • Added voc algorithm converted by Dfrobot
  • Added index example
  • Updated readme with usage example

@AVanVlack
Copy link
Contributor Author

Fixed Issues in build tests

@@ -31,7 +31,8 @@
"""
from time import sleep
from struct import unpack_from
import adafruit_bus_device.i2c_device as i2c_device
from adafruit_bus_device import i2c_device
from adafruit_sgp40.voc_algorithm import VOCAlgorithm
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may want to have this only be imported if the voc algorithm is requested, since its a large py file

VOCALGORITHM_TAU_INITIAL_MEAN = 20.0
VOCALGORITHM_INITI_DURATION_MEAN = 3600.0 * 0.75
VOCALGORITHM_INITI_TRANSITION_MEAN = 0.01
VOCALGORITHM_TAU_INITIAL_VARIANCE = 2500.0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can make these const() have the name start with _ as well - this will reduce the memory usage

@KeithTheEE
Copy link
Contributor

So the Dfrobot implementation swaps all math to fixed point math in a 1 to 1 copy of the sension c implementation--which I believe is the primary reason it's so large (to be fair, the algorithm is large in it's own right). Would the size be enough of a justification to explore a floating point implementation to avoid the overhead of defining fixed point math?

@ladyada
Copy link
Member

ladyada commented Aug 30, 2021

no, its fine to just have it be a non-immediate import

Copy link
Contributor

@KeithTheEE KeithTheEE left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome work, just a couple of small things but really great job with this PR!

I have a meeting but I should be able to test this in full on my sensor in a couple of hours.

Thank you! The VOC algorithm was giving me a massive knot of trouble.

README.rst Outdated
compensated_raw_gas = sgp.measure_raw(temperature = temperature, relative_humidity = humidity)

# For compensated raw gas readings
compensated_raw_gas = sgp.raw(temperature = temperature, relative_humidity = humidity)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sgp.raw doesn't take in any inputs, you'll need measure_raw in it's place

# For compensated raw gas readings
compensated_raw_gas = sgp.raw(temperature = temperature, relative_humidity = humidity)

# For Compensated voc index readings
Copy link
Contributor

@KeithTheEE KeithTheEE Aug 31, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might also need to be a time.sleep(1) here, or we can comment out one of the examples (in this case compensated_raw_gas would be better to comment out) because I think the sensor needs full second between readings, and both the measure_raw and measure_index make a call to the sensor over i2c.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch, datasheet says 1s typical, .5s min

compensated_raw_gas = sgp.measure_raw(
temperature=temperature, relative_humidity=humidity
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The time.sleep(1) or commenting out of the codeblock brought up in the readme applies here

@AVanVlack
Copy link
Contributor Author

Tested with example loops

@KeithTheEE
Copy link
Contributor

Thanks for those adjustments!

Now for an issue outside of your PR, which either I can handle or you can if you're up for it.

It looks like the adafruit_bme280 has changed since the measure_raw example was written, so when we reference it in the readme and example files, we need to change

import adafruit_bme280

to

from adafruit_bme280 import basic as adafruit_bme280.

Again, I can handle that or you can, either option works.

Now I've got another question for you. So I think the sgp40 voc algorithm is time dependent. The source c library has tuning parameters based off of the number of minute the device has been online. Do you know how the dfrobot implementation knows the duration the device has been running? I can't find it, and I'm wondering if the algorithm goes wonky if the sensor is sampled at a rate other than once a second.

As an aside, I've been running it with a sample once a second for about two hours and it seems to be working well. It's responsive when I put it next to a sour dough starter as it should be.

@KeithTheEE
Copy link
Contributor

KeithTheEE commented Aug 31, 2021

Found the variable!

It looks like it defined a constant,

_VOCALGORITHM_SAMPLING_INTERVAL = const(1)

as a once per second sample rate. I think this means if the sampling is at a different rate, the sampling interval needs to be adjusted accordingly for a proper use of the algorithm.

I'm not the best person to make a suggestion as to what to do going forward though. Maybe in __init__ for the sgp40 we can add an initialization variable, sampling_interval_seconds=1, and when we get to the import in measure_index and run self._voc_algorithm.vocalgorithm_init(), we could pass in the sampling interval?

What are your thoughts?

@KeithTheEE
Copy link
Contributor

Sensirion/embedded-sgp#130

Dug deeper into it--it looks like the algorithm is required to sample at once per second and not valid otherwise. So feel free to ignore my previous comments.

It might be worthwhile to add a note about the sample rate in the example and readme though. I can take care of it or you can, up to you.

Thanks for all of your work on this! This is fantastic and I think it's ready to go now that I understand the sampling rate better :D

@caternuson
Copy link
Contributor

@AVanVlack @KeithTheEE I tested this as far as basic functionality and it seems OK. It doesn't appear to break any existing use and the measure_index method returns a value. This was initially 0 but changed after running for some time. And the value also changed when breathed on, etc.

From your testing, do you consider the values returned by measure_index to be good? In general, the new behavior is working for your use case?

Here's my simple test program:

import time
import board
import adafruit_sgp40

sgp = adafruit_sgp40.SGP40(board.I2C())

while True:
    print(sgp.measure_index(20, 40))
    time.sleep(1)

@KeithTheEE
Copy link
Contributor

From my testing and from reading the code, the initial startup time will return 0's because there isn't any history to draw a known variance in sensor values from. After the warm up phase to settles in on a better value. One that seems more or less accurate for my place.

To test it's responsiveness I brought the sensor above my sour dough starter, which off-gasses things like alcohol and co2 as the yeast ferments, and it spiked above 200 as I'd expect it to before dropping off back to around 120. The AC is running in my apartment so it make sense that the gas would dissipate.

It's pretty responsive, with the numbers rising in a very short interval of time, like a sample or two. It's step increase is around 10 indices per sample give or take. It might not seem like much but the value is pretty constant when left at my desk, hovering at around 100 and jittering only one or two values above that from time to time. This responsiveness is good as the sgp40 sensor can't return an actual count of VOCs, and instead returns changes in it.

I've only been running this code for a few hours, and it has a burn in period as well before the algorithm switches to 'heating element has fully heated' mode (I think that's 2 hours in?), so it's possible after a while I'll notice something's amiss, but I don't expect that to be the case from what I've seen so far.

My code is mostly identical to your test, with the exception of including the bme280 to get a humidity and temperature response as well.

From my usage, the pull request itself is fantastic,

I need to follow up with a second pull request to adjust the humidity and temperature compensated example as it is now out of date since the bme280 requires an additional bit, from adafruit_bme280 import basic as adafruit_bme280 and to add a note that the sample rate for this algorithm must be once a second otherwise it is not trustworthy--because this sensor only can respond to changes, a slower sample rate ruins it's ability to respond.

But those aren't really about this pull request, they're separate issues.

@KeithTheEE
Copy link
Contributor

The sensor has been running for more than 24 hours and seems somewhat responsive as well today. It still responds to the sour dough starter, but if I smell a subtle odor or have a bag of fruit next to it, it doesn't spike up as I'd expect it to. That might be my fault though for subjecting it to multiple bursts of sour dough starter--it keeps on internal model of how much the odors vary, and I might have made it think my home is more wildly variable with smells than it really is.

The values still seem within reason, and responded to being near the sour dough as I'd expect.

I'd say it looks good to me after running for so long. I forgot to check how much memory the full algorithm occupies, maybe going forward I should and I can report that back, but for the time being I'm going to put my home air quality sensor back together and add this into it unless you have another aspect you want me to test.

@AVanVlack
Copy link
Contributor Author

@KeithTheEE Most of the math in the algorithm is much above my math knowledge I appreciate your understating of it.

In thinking about making the library straight forward to use, what about giving the user a loop method to call in their main loop (noted at a max of 1s intervals). The method would make sure to call the algorithm at proper 1s intravals, store the returned index in an instance variable and return when there is a new reading. Then the user can just pull the variable for the index and/or the loop method can return when there is new data. Of corse we would leave the measure_index method as is for someone that wants to manage scheduling themselves.

Ideally some kind of async loop/interrupt timer would be better but it looks like that is still in early stages on circuitpython.

@caternuson
Copy link
Contributor

Ideally some kind of async loop/interrupt timer would be better but it looks like that is still in early stages on circuitpython.

Correct. This is generally not an option with CircuitPython.

Also still wondering if this would better to keep separate from the current library. Or at least don't modify adafruit_sgp40.py. Anyone wanting to use this could import the main driver and the VOC algorithm class from voc_algorithm.py. They'd create the instance of VOCAlgorithm in their user program and also manage all the initialization and time critical updating. This would also allow user's to easily tweak and modify how they use VOCAlgorithm if they wanted.

@KeithTheEE
Copy link
Contributor

KeithTheEE commented Sep 2, 2021

@caternuson That structure wouldn't be difficult, in an example omitting comments it would look something like

import time
import board
import adafruit_bme280
import adafruit_sgp40
import adafruit_sgp40_VOC_Algorithm

i2c = board.I2C()  # uses board.SCL and board.SDA
bme280 = adafruit_bme280.Adafruit_BME280_I2C(i2c)
sgp = adafruit_sgp40.SGP40(i2c)
voc_algorithm =  adafruit_sgp40_VOC_Algorithm.VOC_Algorithim() 


while True:
    temperature = bme280.temperature
    humidity = bme280.relative_humidity
    compensated_raw_gas = sgp.measure_raw(temperature = temperature, relative_humidity = humidity)
    voc_index = voc_algorithm.update(compensated_raw_gas)
    print(compensated_raw_gas)
    print(voc_index)
    print("")
    time.sleep(1)

There is a downside and an upside to this.

The upside being memory management.

The downside is the compensated_raw_gas score is pretty meaningless without the VOC_Index translation. So the SGP40 as a sensor is somewhat meaningless without it. You can have a second controller or your computer run the algorithm as a work around. But the sensor raw score doesn't do a whole lot on its own.

@AVanVlack you've done a great job with this! I am not great on the math either, I've just been digging in the docs and code for a while trying to make sense of 'why' anything in the voc algorithm happens.

@caternuson
Copy link
Contributor

So the SGP40 as a sensor is somewhat meaningless without it.

Makes sense. Amazing how much it takes to go from raw to useful/meaningful measurement.

It sounds like this code is generally working and useful. So let's go ahead and merge it in. Thanks!

@caternuson caternuson merged commit cf90d93 into adafruit:main Sep 2, 2021
@AVanVlack
Copy link
Contributor Author

Hey @Curt-is-Pi Is this on a blinka setup? It looks like I missed the import statement for const, it seems to be automatically imported on microcontrollers. from micropython import const should fix it. I will create a new pull request with that fix.

@caternuson caternuson mentioned this pull request Sep 6, 2021
@Curt-is-Pi
Copy link

Curt-is-Pi commented Sep 6, 2021 via email

adafruit-adabot added a commit to adafruit/Adafruit_CircuitPython_Bundle that referenced this pull request Sep 8, 2021
Updating https://github.com/adafruit/Adafruit_CircuitPython_SGP40 to 1.3.0 from 1.2.1:
  > Merge pull request adafruit/Adafruit_CircuitPython_SGP40#10 from AVanVlack/main
  > Merge pull request adafruit/Adafruit_CircuitPython_SGP40#9 from AVanVlack/main

Updating https://github.com/adafruit/Adafruit_CircuitPython_ST7789 to 1.5.1 from 1.5.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_ST7789#25 from makermelissa/master
  > Merge pull request adafruit/Adafruit_CircuitPython_ST7789#24 from makermelissa/master

Updating https://github.com/adafruit/Adafruit_CircuitPython_turtle to 2.2.1 from 2.2.0:
  > Merge pull request adafruit/Adafruit_CircuitPython_turtle#26 from FoamyGuy/angle_docs
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

Successfully merging this pull request may close these issues.

VOC algorithm
5 participants