Hysteresis feature doesn't seem to work or just really confusing
Hysteresis sounds like a simple and useful feature. But it isn't clear what seems to be the use of hysteresis as it is implemented.
Is it there to create a stairstep effect for people with gradual fan curves?
With the option like "Hysteresis only applies on the way down" and hysteresis set to 10 degrees I'd expect following behavior:
- i have a graph with 20% below 50 degree temp and 40% above that mark
- as soon as temp goes over 50 the fan ramps to 40%
- when the load is lowered some and the temp is 45 degree the fan is still at 40% as the temp isn't 10 degrees below 50
- when the temp hits 39 degree the fan goes down to 20% until the 50 is reached again
But for now, with a simple stairstep graph it just either shift immediately between fan speed levels no mater if temps go up or down, or if you set response time to something high it lags both on the way up and down.
It would be great to see the implementation like this or another feature that enables this. I find it irritating when fans change speed be it gradually or with distinct steps and for the loads that are shifting right around the point you set on the graph it would be great to force higher RPM until it actually significantly dropped.
Hysteresis is simply the degree of change required from the last change. If the last point of change was 48 degree, it will be centered around that point. Be aware that hysteresis is ignored at the limits of your graph ( first and last point ). That option can be disabled.
Hysteresis doesn't care about the shape of your graph, so if you expect the hysteresis to always applies exactly at a specific point, like around a staircase point, this may be why you got confused. It won't be "centered" around your 50 degree point.
The type of behavior you describe looks a lot more like a trigger fan curve. Look it up.
Does trigger curve help to make coming down the speed harder that getting higher? I mean i'd really want to see a feature that allow the fan to stay at higher speeds even when temps go a bit lower. It is just too easy to encounter a load with which temps are flactuating up and down enough to change fan speeds.
I see this feature as a "Negative\Positive Bias Offset" so whenever the temp goes up it checks on the graph what speed it should be and sets fan speed higher accordingly. But when temp goes down a step it check on the graph offset by set temperature and only lowers fan speeds if they are lower on that graph with the offset.
For me, as i basically use the graph as a trigger. It would suffice to implement a Schmitt trigger behavior. But with that it would be more convenient to set the temp for two fronts rather then the middle point usually used to describe Schmitt trigger
Does trigger curve help to make coming down the speed harder that getting higher?
Yes. It needs to hit either the idle or load temp for a change to occur.
it check on the graph offset by set temperature and only lowers fan speeds if they are lower on that graph with the offset
That's kind of the idea of "hysteresis only applies on the way down". If you set a 10 deg hysteresis, it will require a 10 degree drop from the last highest point hit.
Oh, sorry, didn't gather that trigger already works like this.
That's kind of the idea of "hysteresis only applies on the way down". If you set a 10 deg hysteresis, it will require a 10 degree drop from the last highest point hit.
That what i though but it seems like it is time dependent or something and even with it set to 10 degree it just switches down even with 2-3 degree drop (I've tested it with some stable 0 degree sensor and custom offset sensor)
@catsuperberg I don't know how you did your test, but try with a file sensor and change the temp manually.
Here's a small video I made showing the behavior.
https://github.com/user-attachments/assets/5d29d47f-b282-4cca-bee8-a579d97e7ae8
Ok, thanks. I get it now. It wasn't working for me because of "ignore hysteresis at minimum and maximum temps". Idk why it should affect anything until you get to the min\max temp which are set at the bottom right of the graph. Thanks still for the trigger curve clarification. It seem much more useful as hysteresis only applies on the way down could still result in frequent speed changes if the load is such that it goes down n degrees for couple of seconds but barely passes the threshold temp (node).
that it goes down n degrees for couple of seconds but barely passes the threshold temp
That's what the response time parameter is for. Increase it.
I'm glad that catsuperberg's problem was solved (ignore hysteresis at minimum and maximum temps).
But I can definitely see why this feature is confusing (in the Graph curve at least, I haven't tried in others). First of all I don't understand why "ignore hysteresis at minimum and maximum temps" is the default setting. Could you Rem0o enlighten us about the reason or use case? In my logic it should be off by default and maybe an optional feature for whatever the use case is.
Secondly the hysteresis feature is not really a real hysteresis even though it can achieve roughly similar results. I tested it using the file sensor method and it seems to work quite nice when the curve is just a vertical step from speed A to speed B at temp T. But when there is a gradual graph, it creates the staircase effect just like OP asked "Is it there to create a stairstep effect".
Creating a staircase effect is not what a real hysteresis should be doing. It should just create a "lag" (in this case the lag is in temp, not in time) for change of direction in the input. It should not require a minimum change in the direction where the input is already going, it should be smooth.
If you want, I can code the hysteresis feature for you Rem0o.
Thanks for this great app. It's already very good and capable, but there is room for improvements too.
"Is it there to create a stairstep effect"
"Yes", but the "steps" are not at fixed %. They are dynamically moving depending on the input.
First of all I don't understand why "ignore hysteresis at minimum and maximum temps"
Goal is, if you have a 10 deg hysteresis and your idle temp in your graph is at 35, your previous change is at 41, then you need to have 31 degree for your idle speed to trigger, which you might never hit. Goal it to hit those idle/load plateau at the beginning/end of your graphs with high hysteresis values. That was FanControl's original behavior before the checkbox existed, hence when it's on by default.
Secondly the hysteresis feature is not really a real hysteresis
It is:
The B graph is a Schmitt and the red line in the top input graph is the reference point (last threshold). FanControl operates exactly like this, with symmetric + and - thresholds around the operating point.
There are various possible implementation for hysteresis. This one chosen here is also on purpose. You mention the "for change of direction in the input" aspect, but this is the problematic bit with temperatures. If your temp oscillate back and forth between let say 74 and 76 at load, it's "direction" is constantly changing. You don't want the speed to fluctuate slightly from the 74 to 76 point all the time., you want it to "stick" until a significant change (either up or down) occurs. If you want a sensitive ramp up but a slow "lagged" ramp down, the second option "only applies on the way down" is exactly for this. You'll get immediate change in the positive direction, but a moving threshold/staircase effect on the way down.
It should not require a minimum change in the direction where the input is already going, it should be smooth.
Is there a name for this? You mention a Schmiit trigger, but this isn't how a Schmiit works.
"In the non-inverting configuration, when the input is higher than a chosen threshold, the output is high. When the input is below a different (lower) chosen threshold the output is low, and when the input is between the two levels the output retains its value."
If I understand what you ask correctly, you basically ask for the hysteresis memory to not only remember the last temperature change, but the direction of that change? So that the threshold applies only on the opposite direction of that last change that crossed the threshold?
"Yes", but the "steps" are not at fixed %. They are dynamically moving depending on the input.
Yeah, I think this is true.
Goal is, if you have a 10 deg hysteresis and your idle temp in your graph is at 35, your previous change is at 41, then you need to have 31 degree for your idle speed to trigger, which you might never hit. Goal it to hit those idle/load plateau at the beginning/end of your graphs with high hysteresis values. That was FanControl's original behavior before the checkbox existed, hence when it's on by default.
Okay, thanks for explaining, I think it makes sense. I think there might be a more sophisticated solution available by creating a time based filter for this, so that the output value would slowly "steer" towards the graph value over time. But that is a little bit off topic for this discussion already.
The B graph is a Schmitt and the red line in the top input graph is the reference point (last threshold). FanControl operates exactly like this, with symmetric + and - thresholds around the operating point.
I think you are right, your implementation is a Schmitt trigger type hysteresis. I think this works perfectly with a Trigger type curve or a Graph where you have big discrete jumps. Also in Wikipedia Schmitt is mentioned with switches (discrete on/off) and analog to digital conversion. Fan Control Trigger curve effectively has this same type of hysteresis - just the settings are different - you can set the upwards and downwards trigger points in absolute temp values instead of giving the middle temp and a hysteresis value.
However in Fan Control when you have a Graph curve with gradually rising slopes, this hysteresis implementation causes the staircase effect. If you set a large hysteresis, then the staircase becomes coarser too and the resulting changes in the % output are bigger jumps that are proportional to the hysteresis setting. In my opinion a more optimal hysteresis implementation in the Graph curve would be something that preserves the gradual shape of the graph without a staircase effect, let's call it "continuous hysteresis".
There are various possible implementation for hysteresis. This one chosen here is also on purpose. You mention the "for change of direction in the input" aspect, but this is the problematic bit with temperatures. If your temp oscillate back and forth between let say 74 and 76 at load, it's "direction" is constantly changing. You don't want the speed to fluctuate slightly from the 74 to 76 point all the time., you want it to "stick" until a significant change (either up or down) occurs.
Yes, I agree. It's quite hard to describe with words really. But in the "continuous hysteresis" implementation this situation is no different than the current implementation - the output would stay constant while the temp oscillates between 74-76 - because it stays inside the hysteresis window - and would only change when it goes lower than our "hysteresis point" (for lack of a better or correct term) minus hysteresis (hysteresis window low limit) or higher than our "hysteresis point" plus hysteresis (hysteresis window high limit). With the difference to the current implementation that the change in the output would not be stepped, but proportional to the latest change (a bit hard to describe clearly again).
If you want a sensitive ramp up but a slow "lagged" ramp down, the second option "only applies on the way down" is exactly for this. You'll get immediate change in the positive direction, but a moving threshold/staircase effect on the way down.
True. But there still is the staircase on the way down. Which might be good or bad depending on the case I think.
I think you might have the quotes switched around. But I assume this one was directed to me:
If I understand what you ask correctly, you basically ask for the hysteresis memory to not only remember the last temperature change, but the direction of that change? So that the threshold applies only on the opposite direction of that last change that crossed the threshold?
Yes and no. The algorithm already knows the direction of the change, it only needs the new sample and the output from the previous step (plus obviously the hysteresis setting). The implementation is really simple and as mentioned only needs memory for one old sample. In fact I already implemented it as I don't have better things to do on this Saturday night besides solving engineering problems while drinking beer lol. I can give it to you if you are interested.
Sure shoot the algo you have in mind.
I made a repo for it and invited you.
The HysteresisStepped class imitates your current implementation. The Hysteresis class is the new implementation. You can test both using gui-main.py or cmd-main.py
Looks like with the new implementation the hysteresis setting should be divided by 2 to get the same average hysteresis as the current one provides. I didn't add this division to the code.
I tested your implementation, but the output is just cropped of the hysteresis value all the time?
Like if the temperature is creeping up to 80 and you have a 10 deg hysteresis, you'll always get 70 out and never get to the 80 point. Same thing on the way down, you will always be +[hystereis_value] off the current temperature. Not sure how that's a less confusing or better implementation for fancontrol? People expect the points on the curve to eventually get hit, and that's kinda the whole point. In this implementation, you'll never hit your min point or max point.
Yes you are right. But the same is true in the old implementation too. You will only hit the max and min temps randomly, if the previous changed happened exactly in the correct place, but all other cases you will not hit them. And for this reason you have implemented the "ignore at min and max temps" feature.
If you set hysteresis of 10, the current implementation has 1/10 chance of hitting the max temp, and the end value can be anything from 0 to 9 degrees off. In the continuous implementation the 10 hysteresis should be set 5 (not in the code now) to correspond to the current implementation. Then the temp will always be 5 deg off from the max and min. So average "error" is same on both. I think this is simply the nature of hysteresis. The ignore min and max feature can work on both implementations too. But to make it like a "real" hysteresis, then I think a time based solution is needed.
I think this would be the next improvement to make the temp drift to the correct temp over time (at least at min and max, or maybe always), then the ignore is not needed. Say the measured temp rises to 80 and stops there, the hystered temp is 70 then. After a few seconds the hystered catches up with the real temp. Should be easy to do, I might try it.
I did a little analysis and plotted both from pseudo-random temperature in time:
+- 5 degree hysteresis for both.
Here is the more interesting data though, cumulated error over time ( += abs(output-input) ) and the cumulated number of changes in time:
We can see that the number of changes in output is relatively similar, but the cumulated error is drastically worse on the non-stepped implementation.
It's easy to see why in the following example with 20 deg of hysteresis:
The yellow line always track the input, but the blue is always lagged and is thus always "off" equally all the time.
That's some nice graphs! You should set 0.5 * hysteresis setting for the continuous implementation for them to be comparable. In those graphs it's effective hysteresis is 2x compared to the stepped, and your test correctly shows the result. It always lags much more behind. Also I would expect the number of changes in the output to be much bigger for the continuous one. Can you send that same test data to me, or the code that generates it? I should do some better/proper testing too, and it would be good for the time based solution too.
I don't get the *0.5 ? For the two implementation to be comparable, both need to do +- the same value. Right now both do +- the hysteresis setting. Why cut it in half for your implementation but not the other?
I will commit the py script later.
I will draw you a graph that explains it. But basically the reason is that the stepped one always catches right on top of the input value when a change occurs. That cuts the effective hysteresis to half. If you set hysteresis of 10, you get +/- 5 effective (10 total from low to high). For the continuous one it is +/-10
But basically the reason is that the stepped one always catches right on top of the input value when a change occurs
But that step IS "hystereis value", up or down. So effectively, the hysteresis window size is hysteresis value * 2, hence the +-.
Here's the comparison, I think it should be quite clear. The stepped hysteresis is set to 6 in this plot and it's trend lags 3 behind the direct out == hysteresis +/-3. If you use the same setting of 6 on the continuous one, it will always lag 6 behind, thus +/-6. Setting 3 for it, it almost matches the trend of the stepped one. It is still a little bit late of that due to using integers.
But that step IS "hystereis value", up or down. So effectively, the hysteresis window size is hysteresis value * 2, hence the +-.
Yes, kinda. That setting is the minimum required change for a change to happen in the output. Then it jumps directly to the input value, halving the "lag" how much the output is behind the input on average.
But that's not a fair comparison, since the effective hysteresis window is different in that case, and the responsiveness of both will not be the same. In your last example, from a starting point, if the signal oscillates +-3, the non-stepped will react and the stepped one will not, which makes the comparison meaningless imo. The window size must be consistent between the two if you want to discuss performance and results.
I'm not sure if I'm following anymore. Those two implementations behave and respond differently, because that is the whole point. You will never get the same behavior out of them, unless you set the hysteresis to 0. By using the same setting for both, you can make some aspect to work more similarly between the two (e.g. the first value that they will react on, that you are focusing on), or by optimizing the setting for each individual behavior you can make some other aspect more similar (e.g. the average "lag" or "error" over a period of time, that I am focusing on).
In your last example, from a starting point, if the signal oscillates +-3, the non-stepped will react and the stepped one will not
This is true. And the next big difference between the two comes when the signal hits 6, when the non stepped will jump 6 forward at once and the non-stepped will increase at the same rate as the input signal. When the signal continues to increase, the continuous one will lag 3 behind the input on every update and the stepped one will lag variedly 0...5 behind the input.
which makes the comparison meaningless imo
I completely agree. I think I didn't make any comparison though, and I'm not suggesting some setting is better than another, I simply explained why your "error" graphs look so different between the two and what settings you should use if you want to match that measurement. The cumulative error measure in other words is the cumulative hysteresis of the functions over time. Your observation that the continuous function generates more hysteresis (when the setting is same for both) is completely correct, but the conclusion "drastically worse" is wrong. Hysteresis is the whole point thus watching it as a measure of betterness or worseness is pointless. You can adjust the "error" of both implementations by adjusting the hysteresis setting. I think comparing small details like this is quite meaningless overall because the behaviors of these two is quite a lot different (unless the hysteresis setting is very small). We should be looking at the overall practical result, and I think plotting them into a graph like you did and inspecting it by eye is a good way to do it, and finally test it in practice in the real world.
In any case, I started working towards the time-dependent function. It might take some time though, I guess it will need even a bit more trial and error than this. If you have time to shoot me the test data, it would surely help too.
Pushed the small test script I used to generate the plots in your repo!
I'm not sure if I'm following anymore.
but the conclusion "drastically worse" is wrong. Hysteresis is the whole point thus watching it as a measure of betterness or worseness is pointless. You can adjust the "error" of both implementations by adjusting the hysteresis setting.
In the context of fancontrol, the whole premise of the hysteresis feature is to prevent any change from occurring within a +- [hysteresis_value] window.
Thus, if you set "6", you should have a 12 deg window where nothings happen. That's a given/requirement. The existing implementation does this just fine.
The whole point for me of this discussion/testing is to see if there's a "better" different implementation for the hysteresis feature, given you described what an hysteresis feature "should" be doing, insinuating there's a better way.
I'm trying to extract some performance/appreciation metrics out of this, to qualify what's "better". From the experience I've gathered with feedback from a ton of users, the thing that we want to do with hysteresis in the context of FanControl is:
- Limit the number of changes according to the hysteresis value (+- window)
- Have these limited changes be responsive to a meaningful changes of temperature.
- No overshoot/undershoot/compensation mechanics, as these create even more confusion and are harder to predict for the user
- When a user defines clear load and idle speeds, he wants them respected immediately as the hysteresis "hits" its window. No lag.
Thanks for a clear and well written reply. I think we are getting on the right track now.
In the context of fancontrol, the whole premise of the hysteresis feature is to prevent any change from occurring within a +- [hysteresis_value] window. Thus, if you set "6", you should have a 12 deg window where nothings happen. That's a given/requirement. The existing implementation does this just fine.
I agree with this. If the user sets "6", the output should start moving at 6 (unless there's some optional smoothing added). To me it's more clear and logical if the hysteresis (error between input and output) stays at 6 all the time when the signal is going in one direction. This is the basic premise of my first implementation. At this setting this and the current implementation "error over time" do not match, and they can't be made to match.
given you described what an hysteresis feature "should" be doing, insinuating there's a better way.
To me it's "better" and more logical that there is no steps after the initial one when the temp is changing steadily. But mainly it allows to set a large hysteresis setting without getting any sudden jumps in the fan speed. I use "" with the word "better" because I think there is no perfect single solution here for every situation and user and opinion. With small hysteresis settings the current implementation works just fine, nothing wrong with it. Nobody can notice a 2 deg staircase.
Limit the number of changes according to the hysteresis value (+- window) This one is not 100% clear to me. Does this mean you want to limit the number of changes to < 1/s also when the temp is simply rising or falling? If yes, then my implementation completely contradicts with this requirement. I think we want to limit the changes caused by "noise" and focus on the trend.
Have these limited changes be responsive to a meaningful changes of temperature.
Agreed.
No overshoot/undershoot/compensation mechanics, as these create even more confusion and are harder to predict for the user
Agreed.
When a user defines clear load and idle speeds, he wants them respected immediately as the hysteresis "hits" its window. No lag.
Agreed as an optional setting something similar like it is now in FC. Could be documented a bit more clearly (see the original post of this thread, and I also had this same confusion when first setting up my fans). But as the only option, somewhat disagree. I think would be better to be able to set a time or some kind of smoothing instead of forced immediate change. Again this is only problematic when the hysteresis setting is large.
And I think it's clear already that I would add the requirement that the fans would ramp up smoothly if the temp is rising smoothly. Hysteresis should affect how far back the fans lag and the lag should be consistent.
I have some new solutions in mind, I will draw some curves.
The FC current and my first implementation again. The pink lines illustrate the area when my implementation has more hysteresis than the FC one, corresponds to your cumulated error calculation.
New idea with the current FC hysteresis for reference.
At start the hysteresis would be equal to the hysteresis setting towards both up and down. When trending, the output would gradually converge with the input. After fully converged, the hysteresis in the opposite direction of the latest changes would equal the hysteresis setting, so not 2x like in both the current implementations.
I don't know yet how to implement it, and whether it's time based or not, but it shouldn't be too difficult. I don't recall seeing this kind of graph on the Internet (only the opposite kind is common), but haven't looked for either. Let me know if you stumble on some example for reference.
Hi @Rem0o, I have read your discussion after looking up the behavior of the hysteresis (sharing @llahteinen 's understanding) and would like to add the following points to the discussion:
general observations regarding the data analysis:
- in your second plot titled "hysteresis vs. hysteresisStepped", the number of changes is almost similar because almost every change in temperature is large enough to trigger the hysteresisStepped as well as the hysteresis. If you repeat the same plot for data with more gradual changes, you will find that hysteresisStepped has fewer changes (as in, strictly counting the # of updates), it is hinted at a little bit in the plot below it at ~90s.
- if you instead try to evaulate the accumulated delta of your changes, the hysteresis has less than the hysteresisStepped. Easy to see in the plots because it always stays in a narrower band
- when @llahteinen argues to set the hysteresis value to +-3 instead, it will reduce the accumulated error but increase the # of changes and the accumulated deltas go up. Changing the value to +-3 will bring the error closer to that of the hysteresisStepped, and at least some of the additional accumulated delta is offset by the generally lower delta of the smooth implementation. Hard to say how it balances out, though.
- When you say "Limit the number of changes according to the hysteresis value (+- window)", is it really the number of changes that counts, or the fan wear and audible noise from significant changes? I.e. do long, gradual changes count as a lot of tiny changes (by that metric smooth is worse), one big change (then they are the same) or no noticable change per se (then smooth is better). Is the accumulated delta more important, possibly combined with a measure of "steep changes"?
Practical considerations why the smooth implementation might be better: 5. when using hysteresisStepped, the resulting stepwise changes are only smoothed out by the fan's step up/down setting, which you can only set to one value. Meaning that, unless the user combines a couple more mathematical functions, they will either
- have a low setting which then prevents it from responding to big temperature changes quickly, or
- a high setting which causes the fans to adopt the stepwise changes unnecessarily quickly, causing sound and wear this is only relevant when the hysteresis value corresponds to a large % fan speed change, though, a reasonable step up value should cover a reasonable hysteresis value unless the graph is steep.
- when looking at hysteresis on the way down, consider that this is used as a control system with a feedback loop. The fan settings are contributing to the temperature measuments, which is why fitting the algorithm outputs in correspondence to prerecorded temperature data is insufficient for a real evaluation. As an example, when you look at your third plot called "hysteresis vs hysteresisStepped", between ~120s and ~160s, the data seems to be favoring hysteresisStepped. But in reality, and I imagine that this is an example of a high load task returning to idle (idle temps reached at ~120s), then the fan setting determined by the smooth implementation might be exactly necessary to keep it at those temps, while a big jump as caused by hysteresisStepped might cause the temperatures to go up again, possibly triggering another change and entering fan cycling.
- Follow-up for 6., in my case I was mostly trying to use hysteresis in order to prevent fan cycling around the shut-off point of my fans. With a smooth implementation, the fans would keep running until the temps are really significantly below the threshold, ensuring that the temperature increase caused by shutting down a fan doesn't tip it back over again. With the stepped implementation, it is a bit up to chance: Say my threshold is at 40°C and the hysteresis set to 5°C, it might cool down to anything between 35°C and 39.9°C before shutting down the fan, depending on what the last reference value was. Giving a buffer of 5°C to 0.1°C, therefore sometimes causing a cycling.
Sorry for the wall of text, I hope my points were understandable, though.
When you say "Limit the number of changes according to the hysteresis value (+- window)", is it really the number of changes that counts, or the fan wear and audible noise from significant changes
This is an opinion, but the # of changes are what's annoying. A consistently and slowly changing pitch is really annoying imo when it comes to computer fans.
then the fan setting determined by the smooth implementation might be exactly necessary to keep it at those temps, while a big jump as caused by hysteresisStepped might cause the temperatures to go up again, possibly triggering another change and entering fan cycling.
I get your idea, but in practice, I've found this isn't really the case. Temperature reacts fast to load (wattage) change, but slowly to cooler/rpm speed change. In the context of a "normal" fan curve, you won't find a step from the hysteresis changing the speed so drastically that under a given load, the temperature will fluctuate enough to start a ping-pong effect with the fan curve.
If the temp changed enough for your high-value stepped hysteresis to kick in, it's probably because the load changed anyway, not because of the additional or reduced cooling. And even in the rare theoretical case the case where it is caused by the cooling effect, your fan curve probably has a steep section in a temperature area where your processor is under a normal load, which is a bad. If your processor under 100% gaming load for eg. oscillates between 60 and 65, you wouldn't put a really steep section in your graph at 63 right in the middle of it.