-
-
Notifications
You must be signed in to change notification settings - Fork 429
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
Normalise GroupItem member calculations to use the GroupItem's Unit #4563
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: Andrew Fiddian-Green <[email protected]>
@mherwege for info.. |
This comment was marked as outdated.
This comment was marked as outdated.
...b.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java
Show resolved
Hide resolved
@mherwege while testing this, I discovered that the JUnit assertion test values are physically WRONG. e.g. the sum of 23.54 °C plus 89 °C plus 122.41 °C is absolutely NOT 234.95 °C .. rather it is 781.24 °C .. this assumes that the three absolute Kelvin values must be summed. => So this really provokes the question about what we are really trying to achieve here? Do we want a) just the sum of three numbers, or b) the sum of three physical temperatures?
|
Look at the PR I already linked several times. This is on purpose. The second and third arguments are interpreted ad differential values, not absolute values. And that’s what makes sense, as most would expect that adding 20 °C to 20 °C gives 40°C. And with the way it is done, it does. It also works for Fahrenheit, which is not the same scale as Kelvin or Celsius. This problem is only there if the 0 point of the respective units is not the same. |
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Ok. Got it. I just committed support for the following..
|
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Unit<? extends Quantity<?>> baseUnit = baseNI.getUnit(); | ||
Unit<? extends Quantity<?>> memberUnit = memberNI.getUnit(); | ||
if (baseUnit != null && memberUnit != null) { | ||
return baseUnit.isCompatible(memberUnit) || baseUnit.isCompatible(memberUnit.inverse()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return baseUnit.isCompatible(memberUnit) || baseUnit.isCompatible(memberUnit.inverse()); | |
return baseUnit.isCompatible(memberUnit) || baseUnit.isCompatible(memberUnit.getBaseUnit().inverse()); |
We discussed this previously. I think we should always use the base unit for inversion, which works in all cases we have identified so far.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you meant getSystemUnit()
here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PS Insofar as all the JUnit tests succeeded I am wondering if the change is necessary? Or should we add another test for that case?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you meant
getSystemUnit()
here?
Yes, that's what I meant.
PS Insofar as all the JUnit tests succeeded I am wondering if the change is necessary? Or should we add another test for that case?
Maybe try checking if mK
(milliKelvin) toInvertibleUnit
and then toUnit(Mired)
works. I suspect that may give strange results as the inverse of milliKelvin is not in the same scale as Mired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
milliKelvin is wrong for two reasons .. a) the factor would be micro rather than milli, and b) the conversions are mirek = 1e6 / kelvin
resp. kelvin = 1e6 / mired
-- i.e. not mirek = microKelvin
..
EDIT i.e. it is mirek = micro(inverse(kelvin))
.. that is why it is called mirek micro reverse kelvin
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe also keep in mind that isCompatible()
works both ways even without falling back to systemUnit
. The issue was that toInvertibleUnit
itself had a bug that requires an intermediate conversion via systemUnit
.
So there is nothing to fix at this point in the code.
And #4561 provides the intermediate conversion via systemUnit
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My intention was to not start from the system unit (K), but from a derived unit (like mK or whatever multiplier symbol). But you are right with the fix in toInvertibleUnit, it should be fine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My prior JUnit tests were testing
toInvertibleUnit
with target Kelvin and they all pass. However I added a test that teststoInvertibleUnit
with the inverse target Mirek. This test currently fails. However it should work fine once #4561 is merged.
I don’t get why that fix would make a difference though. Did you try running the test with the patch applied as well?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok I will patch and test.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just tried the tests with the #4561 code patch applied, and I can confirm that it works fine.
...b.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java
Outdated
Show resolved
Hide resolved
bundles/org.openhab.core/src/test/java/org/openhab/core/internal/i18n/TestKelvinProvider.java
Outdated
Show resolved
Hide resolved
...re/src/test/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunctionTest.java
Outdated
Show resolved
Hide resolved
Set<Item> items = new LinkedHashSet<>(); | ||
items.add(createNumberItem("TestItem1", Temperature.class, QuantityType.valueOf(2000, Units.KELVIN))); | ||
items.add(createNumberItem("TestItem2", Temperature.class, UnDefType.NULL)); | ||
items.add(createNumberItem("TestItem3", Temperature.class, QuantityType.valueOf(1726.85, SIUnits.CELSIUS))); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is color temperature really ever expressed in °C or °F? I know it now works, but I would just limit it to Kelvin and Mired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would rather keep the test. I know it should not be commonly used. But it addresses the common case where a user creates a plain vanilla Number:Temperature
Item without specifying a unit. By default Number:Temperature items have unit=Fahrenheit in USA and unit=Celsius everywhere else. So we need to test that this won't fall over..
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I understand. I am still worried that fixing that issue actually makes things worse. Right now the conversion from °C to mirek fails in the conversion as it correctly says it is not the same scale. With the changes, it will succeed. But looking at all of the discussion in the community post, that same user also assumes the state description min and max are in Kelvin when he doesn’t set the unit. Without the error, he would not have known.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you are referring to #4432
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Signed-off-by: Andrew Fiddian-Green <[email protected]>
What about the following approach: Create a new method in openhab-core/bundles/org.openhab.core/src/main/java/org/openhab/core/library/types/QuantityType.java Line 585 in ce37425
It should be something like:
You then call this method instead of This will then be different from when you simply do the calculation in a rule, because there only the first argument is treated as ABSOLUTE. But I don't think this kind of logic applies in groups anyhow. |
@mherwege please calm down. As you say add is a relative function. It is based on the zero point offset of the initial unit. So in the case of Celsius it is a relative add. And in the case of Kelvin (and most other zero based units) the add function is at the same time both relative and absolute. So..
|
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Absolutely true, but my worry is the different results depending on the unit used. And that is not logical. QuantityType calculations are meant to give the same result, whatever the unit. And of course that cannot be true for invertible units, which is a special case. And that's what we try to solve that by converting to the group item base unit to make the result at least predictable. But if it breaks the rule of having the same result within the same dimension, whatever the unit, that is not acceptable. And the rule is broken because of the conversion to the base unit. Add now does not do a relative add anymore, whatever the unit. |
Honestly, I am pretty calm on this. I am just trying to achieve the best possible result. That's why I keep on questioning things. If we don't do that now, I know we hit issues at some point in time and we will have to find other workarounds again. |
We just need to add the following text to the readme and JavaDoc; then everything clear :) The
See. Perfectly logical. :)
|
This is not what the current add method in QuantityType does. Within the group arithmetic functions, because you first convert all arguments to the base unit, you get (assuming base unit is K): |
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Are you sure? I would expect a result of 40 °C ??? |
@mherwege I have a doubt.. This PR has a JUnit test suite that runs 75 tests. It was about 57 tests in the prior version but I added 18 more. The initial 57 tests all pass with the assertion results as the prior version did. (So this PR did not break anything). And the 18 new tests are to check the new functionality. I checked the 18 new assertion values by hand calculator. Therefore my doubt is whether your fears are justified. If you have concerns about this PR can you therefore please submit your own proposed JUnit test cases so that I can write code that would pass your tests. Otherwise we are talking around in theoretical circles. |
Indeed, 40 °C I will correct the post. |
See the following unit tests. They all produce IMHO the correct results. Or do you expect them to produce others??. |
Signed-off-by: Andrew Fiddian-Green <[email protected]>
@andrewfg So, I ran the simulations and here is a little table. The original unit of the inputs are on top, but the values in the cells are already recalculated to the target unit. These correspond to the tests with some extensions.
My first question is the difference in outcome between 6a and 6b (is the same as 1). In the current propsal, the base unit determines the way the sum is calculated. Before, the first element in the group did determine it. I would argue that even made the whole summing quantity types in groups unpredictable, but that is another story. At least now it is predictable. To that end, I also created a scenario 3b where the base unit is °C and there is a mirek input. So the sum would be off when you try to visualize this with e.g. a Kelvin state description pattern. This is another case you would have when the °C default is not changed. I am just not so sure anymore it is the right thing to do to use group base item unit as the reference unit. It does make the result dependent on that unit setting. And so far the unit never really had an impact on any calculation we did. If someone changes the unit of an item, it has a broader impact than just this calculation. It will also impact persistence. That's why it got introduced in the first place. So if you change this to make the group sum calculation work, it impacts that as well. That's why I was somewhat advocating to always use the value against an absolute reference (hence the addAbsolute proposal). In that case base unit will not have an impact anymore. The solution here is also loosly related to how to treat it for the state filter profiles. There is no natural base unit there. Summing in absolute units would avoid that problem there as well. I don't want to be the final decision maker on this. I am not a maintainer, so won't be the final judge. I can live with your proposed solution as long as it is properly documented. |
Thanks for the analysis. AFAICT this issue only affects the SUM calculation. The MIN MAX AVG and MEDIAN functions anyway produce the same results. @mherwege can you please confirm re MEDIAN? For SUM I think we have three possible ways forward:
*) on the case of SUMREL the relative base would be the zero point of the GroupItem's Unit, rather than the Unit of the "first" item in the Group members set. |
Indeed, the challenge is only with SUM. Following wiki is an interesting read: https://github.com/unitsofmeasurement/unit-api/wiki/Arithmetic-rules-for-Difference-versus-Absolute-quantities This is actually the reference for all Quantity calculations and how they should be treated. I quote:
The QuantityType.add method treats the second argument in the add as a INCREMENTAL, and the first argument as ABSOLUTE. So it results in: If we step back for a moment, what would summing temperatures in a group really be used for? Honestly, I can't think of cases where it makes sense. Average or median, min or max I believe make a lot of sense. I am not sure summing temperatures in a group does to be honest. And unless someone finds an example where it does make sense, I would stick to the golden rule, which is convert everything to system unit before calculating. This avoids the extra complexity of adding (documenting, explaining, maintaining) another version of SUM, just for the temperature case, where I see little relevance. So my vote goes to doing the calculations in system unit and not creating an extra flavor of SUM. |
@mherwege I am good with your proposal above. But I thought it was you who was insisting that the calculations must be relative (20 C + 30 C = 50 C) so I am a little surprised that you seem to have changed your mind. ?? |
Yes, and I was turning in circles on this. I have advocated the opposite in some of my messages as well. My appologies for that. My itch came from the fact that I went the extra mile to make rules work with first argument absolute and second argument differential (relative). And that's reflected in the add method. The moment the conversion is done beforehand, you actually make them relative to a new base, not the first argument anymore, as the conversion is always converting absolule values, not differential values. And then you treat the first argument as absolute and the second (converted) argument as relative again in the add method. I actually think an extra javadoc on the add method explaining this, stating that the first argument is treated as absolute and the second argument as differential would be appropriate. Do you mind putting that in? You are touching the same classes. Also, this would anyhow be a breaking change. Currently the outcome depends on the unit of the first item in the group that is considered. I don't think that is fixed. With the current change, it becomes deterministic, whatever solution is chosen. It really doesn't matter if the current JUnit test still pass in that sense, as they are deterministic in that the first element in the group is fixed, and you fixed the tests by making the base unit the same as the unit of the first element in the list. |
I will do that. |
Signed-off-by: Andrew Fiddian-Green <[email protected]>
@mherwege I made a commit just now that should hopefully reflect our final agreement. |
Signed-off-by: Andrew Fiddian-Green <[email protected]>
@SuppressWarnings({ "rawtypes" }) | ||
protected List<QuantityType> referenceUnitQuantityTypes(Set<Item> items) { | ||
return items.stream().map(i -> i.getStateAs(QuantityType.class)).map(s -> referenceUnitQuantityType(s)) | ||
.filter(Objects::nonNull).map(s -> (QuantityType) s).toList(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.filter(Objects::nonNull).map(s -> (QuantityType) s).toList(); | |
protected List<QuantityType<?>> referenceUnitQuantityTypes(Set<Item> items) { | |
return items.stream().map(i -> i.getState()).map(s -> referenceUnitQuantityType(s)) | |
.filter(Objects::nonNull).collect(Collectors.toList()); | |
} |
I think this solves the rawtypes problem. I still had it when using a straight toList()
.
It may lead to issues further down. I am not sure it is easy to resolve. I still think you don't need the first casting to QuantityType
(as referenceUnitQuantityType
takes a State
as an argument), and the final cast back, as referenceUnitQuantityType
returns a QuantityType
.
With the following, the rawtypes problem still remains, but I didn't see issues downstream:
.filter(Objects::nonNull).map(s -> (QuantityType) s).toList(); | |
protected List<QuantityType> referenceUnitQuantityTypes(Set<Item> items) { | |
return items.stream().map(i -> i.getState()).map(s -> referenceUnitQuantityType(s)) | |
.filter(Objects::nonNull).collect(Collectors.toList()); | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this solves the rawtypes problem
Umm. Yes it solves the problem on toList()
. But the method still needs the annotation for another reason. Aargh!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, figured it out as well. That's where I find streams combined with generics are a huge challenge. See my updated comment.
Signed-off-by: Andrew Fiddian-Green <[email protected]>
...b.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java
Outdated
Show resolved
Hide resolved
...b.core/src/main/java/org/openhab/core/library/types/QuantityTypeArithmeticGroupFunction.java
Outdated
Show resolved
Hide resolved
Signed-off-by: Andrew Fiddian-Green <[email protected]>
Fixes #4562
Requires #4561
Previously the calculations over a GroupItem's set of member Items were based on the Unit of the first member in the set. There are two problems: a) the order of the set is not fixed, and b) the Unit of the first member Item is also not definitive to the Unit of the expected result. Therefore these calculations could produce variable and unexpected results.
This PR normalises the calculations over the group's set of member Item so that all calculations are based on the Unit of the GroupItem's base Item.
Furthermore this PR allows conversion between non- inverted and inverted Units so invertible conversions (e.g. Kelvin <=> Mirek) are supported.
Signed-off-by: Andrew Fiddian-Green [email protected]