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

Smoothing of WheelSpeed and CurrentResistance #140

Closed
mattipee opened this issue Nov 11, 2020 · 39 comments
Closed

Smoothing of WheelSpeed and CurrentResistance #140

mattipee opened this issue Nov 11, 2020 · 39 comments
Labels
enhancement New feature or request future development This is not on the backlog for the coming releases

Comments

@mattipee
Copy link
Contributor

As smoothing (averaging) is now used to good effect in calibration (#132), my thoughts have now come back to something I've meant to look at for a while.

I've noticed that, for example, in ERG mode at 100W, the reported power moves around, oscillating above/below. At higher wattage, the oscillation seems greater.

There are two factors... WheelSpeed and CurrentResistance

  • CurrentPower is a function of both.
  • TargetResistance is a function of TargetPower and WheelSpeed (well, actually VirtualSpeedKmh, but that has a linear relationship with WheelSpeed).

Zwift is capable of 3-second averaging of received power, which I think I have enabled, but I see the oscillation on-screen as well as in Strava uploads.

My thoughts are that by smoothing WheelSpeed a little, TargetResistance becomes smoother. And by smoothing CurrentResistance a little, coupled with the smoothing of WheelSpeed, CurrentPower becomes smoother.

Only "a little"... the issue I think is that because we sample 4Hz, the figures are going to vary through the pedal stroke. At a higher sampling frequency for "pedal stroke analysis", one might want to keep the raw CurrentResistance values, but for the purposes of the mathematical calculations to and from power, a slight smoothing is advantageous.

If the WheelSpeed and CurrentResistance are changing/oscillating through the pedal stroke, then CurrentPower will oscillate. But if WheelSpeed oscillates... for any given sample in time, both TargetPower2Resistance and TargetGrade2Resistance are going to oscillate, too.

Too much smoothing will result in sluggish response, a loss of peak power values, etc... as I found when trying to use the AverageResistance value available in Filler36_37. I couldn't work out how long an average that was, but too long.

So just 1 seconds worth of "running average" smoothing on WheelSpeed and CurrentResistance... simply to eliminate the effects of an uneven pedal stroke from the gross power/resistance calculations.

@mattipee
Copy link
Contributor Author

mattipee commented Nov 11, 2020

Test condition: Manual power mode 150W, 75rpm (metronome), two separate test runs, each approx 1m10s, without and with 1 second (4 sample) smoothing.

image
image
image

@mattipee
Copy link
Contributor Author

Does it "feel" smoother? I want to think so. You can still see high frequency, per-pedal-stroke variation in CurrentResistance and CurrentPower, so perhaps increasing smoothing window to 2 seconds might help. But you can see the smoothing effect quite clearly, indeed the effect can be seen in TargetResistance as predicted.

So perhaps, the proposal is a command line argument to specify number of seconds of smoothing... 0-3.

@mattipee
Copy link
Contributor Author

The fundamental issues I think I'm trying to address for myself are:

  • Grade2Power() and TargetPower2Resistance() using a noise, "instantaneous" value of VirtualSpeedKmh/WheelSpeed, preferring perhaps a slightly smoothed value, as this may improve feel.
  • Broadcast CurrentPower being consequently noisy, preferring perhaps a slightly smoothed value.

@marcoveeneman
Copy link
Contributor

Nice analysis. I also observed the same behaviour. When pedalling at a stable pace with stable power (well, at least it feels stable) the power reported by FortiusANT jumps around.

I was actually thinking of decoupling the sampling frequency from the broadcasting frequency. I read somewhere that the USB interface can be polled at +/- 70ms intervals, while as you mentioned it is currently sampled at 250ms. This should give about 3,5 times more datapoints to work with, which i think will improve the final result. Thoughts?

@mattipee
Copy link
Contributor Author

mattipee commented Nov 12, 2020

Cheers @marcoveeneman .

I don't think increasing frequency will help here, but maybe.

I originally played with increased frequency to see what I could get out in terms of a polar graph for pedal stroke analysis. Wouter's implemented this as a GUI option now. I was never really convinced that the resolution and response of CurrentResistance was quite good enough for meaningful PSA.

The underlying serial protocol from head unit to motor controller ultimately determines the resolution. Totalreverse stated somewhere what the maximum frequency was in this case. Running USB higher than that, just duplicates values, and can lead to instability.

A higher frequency than 4Hz may make button presses more reliable, but as I'm trying to smooth the values obtained anyway, a higher sampling rate would probably just mean more values to average.

Or are you thinking that if 4Hz is subsampling the available data, then obtaining instead say, all "10" unique points per second and averaging them all would yield a more accurate average?

@WouterJD
Copy link
Owner

If you do NOT activate PedalStrokeAnalysis, the sampling frequency = 250ms

@mattipee
Copy link
Contributor Author

If you do NOT activate PedalStrokeAnalysis, the sampling frequency = 250ms

Yes. I run on the Raspberry Pi, without GUI. That then defaults to 4Hz. 👍

@marcoveeneman
Copy link
Contributor

@mattipee Yes, i think taking an average of 10 unique samples within 1 second will yield a better result than taking an average of 4 unique samples within 1 second. With only 4 samples a second you are missing a lot of potentially useful data points.

My proposal is to always sample at the maximum frequency the USB head unit allows and only broadcast over ANT at 4Hz.

@mattipee
Copy link
Contributor Author

@marcoveeneman it's possible within the current implementation (see QuarterSecond variable). However, generally, I think the event loop could be rearchitected, introducing multiple threads and shared state (ANT+, USB, UI being the main three). Indeed, I think current threading is the cause of the GUI not functioning correctly on Linux.

That said, leaving architecture alone and simply smoothing over "number of seconds, 0-3s), whether at PSA "max rate" or even at the default 4Hz, I think is an improvement worth considering. I do enjoy improving accuracy for accuracy's sake, but also minimising change footprint for Wouter's sanity. 👍

@WouterJD
Copy link
Owner

Hmmm...

When TTS (Tacx Training Software) connects to the trainer through USB it receives the data like we do. TTS can decide what to do with it.

FortiusANT is a bridge between USB and ANT and passes received data to the other side.
Sometimes conversion is required (Grade2Power, Power2Resistance) or calculation (ANT defines power in 1/4 watts, i-Vortex in Watts). But data in = data out.

Do you say that Zwift canot handle the data like TTS could?
Might well be, since TTS/Trainer is a close cooperation, where ANT+ devices may be more intelligent and smooth themselves.

So just 1 seconds worth of "running average" smoothing on WheelSpeed and CurrentResistance... simply to eliminate the effects of an uneven pedal stroke from the gross power/resistance calculations.

In a 250ms cadence it would mean averaging over 4 samples.
Of course the averaging should take place after PedalStrokeAnalysis, some small gadget I leave intact however accuracy and applicability is limitted, since the device-resolution is not designed for this.
....

@marcoveeneman
Copy link
Contributor

I just dug up a WireShark capture of the USB communication between Tacx and Fortius Software 2.04 i made a while ago (yes this is really old software!). I took the USB packets containing a full data frame and calculated the time delta between the messages. This results in the following graph:
image

Where sample number is on x axis, and time delta since previous sample is on y axis.

As you can see, the original Tacx software was polling the trainer every 70-80ms during a ride, before and after the ride the sampling is slower, somewhere around 200ms. So it looks like faster sampling should be possible, but i agree this would require some architectural change in order to do correctly, which Wouter needs to agree on first.

I'll try to capture some more data this evening to see what the effect is of subsampling.

@mattipee
Copy link
Contributor Author

Unfortunately, no TTS source available, but GoldenCheetah's Fortius implementation has comments suggesting it averages power over 1 second (10Hz).

@WouterJD
Copy link
Owner

I'll look into it

@WouterJD WouterJD added enhancement New feature or request under investigation Being studied for implementation in next version labels Nov 12, 2020
@mattipee
Copy link
Contributor Author

Interesting that TTS uses a higher frequency during run, and GoldenCheetah also.

The biggest gain I think for >4Hz is responsiveness of the buttons - I sometimes get missed-clicks, presumably because I press and release within a 0.25s period and it reports as 0. Reliability of button presses should increase with an increased frequency.

I think whatever the frequency, the averaging should be done in terms of "seconds" rather than "samples". And indeed the split between the value used for Calculation and the value used for display on PSA requires care.

I have a branch in my fork started... https://github.com/mattipee/FortiusANT/tree/smoothing

I'll absolutely have a play next time I'm riding. I've an ERG mode FTP-building training session to repeat, and can compare the Strava trace with the last one. I intend to increase frequency of no-GUI USB to 10Hz and see if I get as far as parameterising number of seconds of averaging via the command line. I'll probably run it at either 1 or 2s averaging.

In terms of terminology, I'm calling it "smoothing using a running average" but it's a "low pass filter" we're talking about, is that accurate?

@mattipee
Copy link
Contributor Author

image
image

It looks better in Strava, that's the main thing.

@marcoveeneman
Copy link
Contributor

marcoveeneman commented Nov 12, 2020

Ok, i've spent some time collecting data by running FortiusANT, sampling via USB at 4Hz and 10Hz, collecting raw data via WireShark. By now i realise it would have been useful to write something in python so it can be re-used by others, oh well.. next time.

Let's look at these graphs first (click for bigger picture). This is the raw data that the Tacx reported back to FortiusANT. The bold lines are a running average of 1 second on the raw data.

Sampling @ 4Hz Sampling @ 10Hz
full_image_4hz full_image_10hz
part_image_4hz part_image_10hz

The first two graphs show a full capture in manual power mode at 200W around 80-85 rpm, first at 4Hz sampling, then at 10Hz sampling. When looking at the graphs, they more or less look almost identical. That changes when zooming in further, which is when we get to the second two graphs.

  1. Current load values reported by Tacx bounce up and down a lot. There is almost no single sample that is close to the previous sample.
  2. The running average of the Current load (bold green) on 4Hz is more rough compared to the running average of 10Hz.
  3. running average of Current load follows running average of Target load (bold grey). However it is delayed, and seems to overshoot all the time.

So i think we have two problems:

  1. FortiusANT is sampling too little, increasing the sampling frequency seems to improve the running average
  2. Running average is an easy but not so optimal low-pass filter, i wonder if this is also part of the behaviour we are seeing.

EDIT:
Another question that just popped up in my head, i need to check if it makes sense, but could it be the measurements are affected by the 50Hz AC power?

@mattipee
Copy link
Contributor Author

mattipee commented Nov 12, 2020

@marcoveeneman Your 200W, 85rpm beats my 150W, 75rpm. Going to have to up my game...

Buttons are improved at 10Hz. That alone is enough reasoning for me to increase.

Any running average is going to lag, you're right.

The Fortius doesn't respond that quickly to changes in TargetResistance... I tried to implement road-feel, but it just doesn't respond that fast. But CurrentResistance and WheelSpeed do appear to reflect reasonably well the variation around the pedal stroke, which other than for PSA, we do want to "ignore", I think.

I think I'm settling towards 1s (10Hz) averaging of WheelSpeed and TargetResistance, and possibly additionally 1s (4Hz) reported Power over ANT+. Just to clean it up a little.

@WouterJD
Copy link
Owner

I see your graphs and felt the need to be able to create this in more places.
Please try -d32 to create a JSON file and let me know whether that is the required info to create your graphs. I already used it for the TCXexport (made me more gray).

@marcoveeneman
Copy link
Contributor

@mattipee Keep up the good work and you'll do 200W as well 👍
Good, so the buttons would feel better now, i found them a bit awkward as well, the button press needed to be quite long.
Yeah indeed, i also noticed the response of fortius is lagging the behind the target resistance.
Average of 1s would be fine by me.

@WouterJD Nice, now my next question would be: can we also export as CSV for easy import in excel/numbers? :-)

@WouterJD
Copy link
Owner

WouterJD commented Nov 15, 2020

Would have been easier to implement.
And then running into the CSV-issues with comma's and dots (no thanks: 12,3 in dutch and 12.3 in english localization). Therefore I chose for the JSON format, with - I hope- a stable data-representation.

You can interpret the JSON using the related excel sheet

@mattipee
Copy link
Contributor Author

Simple solutions to that issue are tab-separated, semicolon-separated or quoted values.

But in any case, I look forward to playing with JSON/tcx or whatever format of data output - useful stuff. Good work! 👍 I'm not going to mention graphing.

@WouterJD
Copy link
Owner

WouterJD commented Nov 16, 2020 via email

@omedirk
Copy link

omedirk commented Nov 18, 2020

Dear all,
I've just 'fixed' my own smoothing.
what i do:
i made a dirty hack to read the USB 10Hz (I changed the 250ms to 100ms) with a variable that tracks last USB time, nothing more
so the whole section runs at 100ms, including the calculations. -> but the trick is below..

722     #---------------------------------------------------------------------------
 723     # Our main loop!
 724     # The loop has the following phases
 725     # -- Get data from trainer
 726     # -- Local adjustments (heartrate monitor, cadence sensor)
 727     # -- Display actual values
 728     # -- Pedal stroke analysis
 729     # -- Modify data, due to Buttons or ANT
 730     #---------------------------------------------------------------------------
 731     if debug.on(debug.Function): logfile.Write('Tacx2Dongle; start main loop')
 732     try:
 733         while self.RunningSwitch == True and not AntDongle.DongleReconnected:
 734             StartTime = time.time()
 735             #-------------------------------------------------------------------
 736             # USB process is done once every 100ms
 737             #-------------------------------------------------------------------
 738             if (time.time() - LastUSBtime) > 0.1:
 739                 LastUSBtime = time.time()
 740                 QuarterSecond = True
 741             else:
 742                 QuarterSecond = False

than I added that same part of code just before the ANT+ section, but there it still does 250 with the old ANT time variable.
so no trouble with sending at 4Hz.

 826             #-------------------------------------------------------------------
 827             # Do ANT work every 1/4 second
 828             #-------------------------------------------------------------------
 829             if (time.time() - LastANTtime) > 0.1:
 830                 LastANTtime = time.time()
 831                 QuarterSecond = True
 832             else:
 833                 QuarterSecond = False
 834
 835
 836
 837             messages = []       # messages to be sent to ANT
 838             data = []           # responses received from ANT
 839             if QuarterSecond:
 840                 LastANTtime = time.time()

They both read and write from the same self. something datastruct and they are consecutive, so no problems with threading anyway.

now the trick for smoothing:
i have an old fortius USB trainer so in the bottom of usbTrainer.py file i added a small filter:
https://en.wikipedia.org/wiki/Exponential_smoothing

it's very simplistic and up for improvement, but it works. (use it a lot in machinery for ugly signals)

I use the following values:
cutoff freq: 5Hz -> we are sampling at 10Hz, so no freq above 5Hz can be measured.
this results in tau of 0.03something
and a factor a for the formula of 0.9567 -> you keep 95% of the old value and update it with 5% of the new value.

this runs super smooth. but might be a bit too smooth. I think I will try 90% old 10% new.

will run my next training with it to see how it performs, but for the test ride I really like the feel.

not sure how to share the code @WouterJD because it's such a nasty hack.

I was not sure what Axis was doing, and of course it's of no use filtering the button bits.

80             #-----------------------------------------------------------------------
1881             # Parse buffer
1882             #-----------------------------------------------------------------------
1883             filtConst = 0.9567865
1884             tuple = struct.unpack (format, data)
1885             self.Axis               = tuple[nAxis1]
1886             self.Buttons            = tuple[nButtons]
1887             self.Cadence            = self.Cadence * filtConst + (1-filtConst) * (tuple[nCadence])
1888             self.CurrentResistance  = self.CurrentResistance * filtConst + (1-filtConst) * (tuple[nCurrentResistance])
1889             self.HeartRate          = tuple[nHeartRate]
1890             self.PedalEcho          = tuple[nEvents]
1891             self.TargetResistanceFT = tuple[nTargetResistance]
1892             self.WheelSpeed         = self.WheelSpeed * filtConst + (1-filtConst) * (tuple[nSpeed])

@WouterJD
Copy link
Owner

WouterJD commented Nov 18, 2020

Hi @omedirk

axis is the rotation of the steering unit, no need to smooth
The smoothing certainly works, the factor is to be established. side-effect is that the pedal-stroke analysis will not work anymore.

Question: where does 0.9567865 come from?
Funny to smooth, using a 7-digit factor :-)

@WouterJD
Copy link
Owner

II have been checking into this
https://github.com/WouterJD/FortiusANT/blob/master/pythoncode/usbTrainer.py#L1779

This delay is quite bad; it is done ALWAYS instead only when retrying.
This causes the pedal stroke analysis not to work anymore.
I will update this in next version.

Just to inform you

@omedirk
Copy link

omedirk commented Nov 18, 2020

changed that section to:

7         while retry:
1778             data = self.USB_Read()
1779             if  not len(data) < 40:
1780                 break
1781             time.sleep(0.1) # 2020-09-29 short delay @RogerPleijers
1782             retry -= 1

now checking the cpu load, because if there's no delay anywhe it will go up high..

edit: just checked, only 2% on the rpi. so there's enough delays

@omedirk
Copy link

omedirk commented Nov 18, 2020

Hi @omedirk

axis is the rotation of the steering unit, no need to smooth
The smoothing certainly works, the factor is to be established. side-effect is that the pedal-stroke analysis will not work anymore.

Question: where does 0.9567865 come from?
Funny to smooth, using a 7-digit factor :-)

I am not sure if you even need to smoothen the cadence, that's actually quite ok. I use a RPi, so I am not entirely sure what the pedal stroke analysis does..

The factor is calculated from the formula's on the site. it's basically the time constant of the filter and the sampling frequency.
I used 5Hz as a guess and sampling is at 10Hz, so I ended up with this number. But .95 will work just as fine :)

update: i think .95 is too much really, tomorrow is training day. hopefully can give a good value then.

update 2: @WouterJD now that I just posted my code I see that I send the ant messages every 100ms.. oops. but it works fine..

@WouterJD
Copy link
Owner

changed that section to:

7         while retry:
1778             data = self.USB_Read()
1779             if  not len(data) < 40:
1780                 break
1781             time.sleep(0.1) # 2020-09-29 short delay @RogerPleijers
1782             retry -= 1

now checking the cpu load, because if there's no delay anywhe it will go up high..

edit: just checked, only 2% on the rpi. so there's enough delays

I came to similar implementation; note that it is not a regular wait-loop - the retry should be occasional.
I have added a logfile-record with the sleep, so in case of debugging it is not hidden - although USB_Read() provides logging as well.

A likely cause for the error may be that Refresh() calls Receive() before Send(), which may cause that the trainer cannot respond.

@WouterJD
Copy link
Owner

update 2: @WouterJD now that I just posted my code I see that I send the ant messages every 100ms.. oops. but it works fine..

You have redefined CycleTimeANT as 0.100, I do not know whether this will cause undesired effects.

@WouterJD
Copy link
Owner

Now that the JSON export is built-in, these measurements can be nicely conducted.

I have added PedalEcho (=1 when the pedal passes the magnet) and PedalCycle, which is alternating 0 and 250 for each cycle.
Also I'm struggling with a decent time-format ... under development.

I have done the following test, in manual mode.

  • initial part
  • cycle with 200Watt, maximum difference left / right to see whether this is registered correctly
  • cycle with 100Watt, 40rpm / 60 rpm / 80 rpm / 100rpm to see how much noise there will be

The overall picture is:
FortiusANT JSON Analysis  (Noise 20ms with PSA)

The first test, the first cycles maximum emphasis on the right leg and the next cycles on the left leg.
The Pedal Stroke Analysis nicely shows the disbalance.
FortiusANT JSON Analysis  (Noise 20ms with PSA) LeftRight

Then the part with cadence = 40rpm (three pedal rotations)
FortiusANT JSON Analysis  (Noise 20ms with PSA) 40rpm

And the part with cadence = 100rpm (three pedal rotations)
FortiusANT JSON Analysis  (Noise 20ms with PSA) 100rpm

The CurrentResistance line has markers for each data-point; the others have no markers but the same number of datapoints.
I would say that I do not see noise, but this trainer nicely produces the figures as expected.
The speed is quite smooth; with exception of the Left/Right test - but indeed that was not a smooth piece of cycling.

I am curious what you think of this.

@WouterJD
Copy link
Owner

I have done a similar test without Pedal Stroke Analysis, so with a 250ms cycle:
FortiusANT JSON Analysis  (Noise 250ms)

And zooming into some cycles at 80rpm:
FortiusANT JSON Analysis  (Noise 250ms) 80rpm

The CurrentResistance is quite stable (and I tried to cycle as stable as possible, even though it's a short measurement).
The SpeedKmh is very stable, as I would expect because the physical wheel will vary less then the applied force.

So for this configuration in my occasion, I consider this quite acceptable.
Of course other Tacxes could be different.

@WouterJD
Copy link
Owner

1887 self.Cadence = self.Cadence * filtConst + (1-filtConst) * (tuple[nCadence])
1888 self.CurrentResistance = self.CurrentResistance * filtConst + (1-filtConst) * (tuple[nCurrentResistance])
1892 self.WheelSpeed = self.WheelSpeed * filtConst + (1-filtConst) * (tuple[nSpeed])

update: i think .95 is too much really, tomorrow is training day. hopefully can give a good value then.

@omedirk @mattipee
Since smoothing may be needed for some and undesired for others I suggest to make it optional with the approach that @omedirk suggested.
Please note that Sporttracks and Strava smooth the input (algorithm unknown), I assume Zwift will smooth as well.

Command-line flag -S factor/cps

  • factor is the exponential smoothing factor; numeric floating value between 0.9 and 1 (I would suggest a minimum value: 0.9)

  • optionally three characters to indicate what parameters to smooth: cadence, power, speed (actually cadence, resistance, wheelspeed - but that is transparent for the user). If not specified all three are smoothed.

  • default = 1/cps

Suggested implementation [Exact syntax to be defined]

if *SmoothResistance*:
    self.CurrentResistance  = self.CurrentResistance * *SmoothFactor* + (1 - *SmoothFactor*) * (tuple[nCurrentResistance])
else:
    self.CurrentResistance  = self.CurrentResistance

instead of SmoothFactor I could have used SmoothFactorResistance and omit the if/else; but I think this is more understandable although that is a matter of taste.

Suggestions welcome.

@marcoveeneman
Copy link
Contributor

@WouterJD I was looking at the graphs where PedalStrokeAnalysis was enabled and was wondering what the sample frequency is, looking at the code this should be around 50hz (20ms cycle). I was under the impression that the head unit could only be read every 70ms, am i wrong in that? Your graphs seem to indicate it is possible to go faster. I'm a bit confused now.

@WouterJD
Copy link
Owner

Correct; TotalReverse thought it would not be possible but it works.
Perhaps it does not work for everybody but I did not get complaints
I did not try to go faster, also because there is no need

@omedirk
Copy link

omedirk commented Nov 25, 2020

Just to let you guys know, I settled with an update rate of 100ms for the tacx, 250 for the ant+ stick.
using 0.5 as smoothing, but I think I can just as well leave it out.
The 100ms update seems more useful than smoothing

@WouterJD
Copy link
Owner

Would you suggest to change the 250 timer into 100ms?
Might be too fast when the GUI is on

When using PedalStrokeAnalysis, the timer is faster already

@omedirk
Copy link

omedirk commented Nov 26, 2020 via email

@WouterJD
Copy link
Owner

But as I was not sure why the ant was running at 250ms I created two timers.

ANT definition is 4Hz; Why would a heartrate belt or cadence meter send more data?
The purpose of ANT is to be low-energy, the more data/cycles - more energy needed

@WouterJD WouterJD added future development This is not on the backlog for the coming releases and removed under investigation Being studied for implementation in next version labels Dec 1, 2020
@WouterJD
Copy link
Owner

Since there is no communication here, I assume can be closed.
If not happy to reopen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request future development This is not on the backlog for the coming releases
Projects
None yet
Development

No branches or pull requests

4 participants