Ruby API: Transformation Class's "identity?" issue


#1

This issue describes a potential bug with one of the API methods, as there does not appear to be a category/topic specifically for them (there is for API documentation).
The developer API documentation includes:
http://www.sketchup.com/intl/en/developer/docs/ourdoc/transformation#identity?

The method is returning true only for the simplest identity initialization. The “new” initializer without any arguments will return true for “identity?”, but other initializers will not. The issue can be observed with Ruby console statements:

t1 = Geom::Transformation.new
#<Geom::Transformation:0xd9dd4b0>
t1.identity?
true

t2 = Geom::Transformation.new( t1.to_a )
#<Geom::Transformation:0xda07108>
t2.identity?
false

t3 = Geom::Transformation.scaling( 1, 1, 1 )
#<Geom::Transformation:0xdadad88>
t3.identity?
false

Transformation#identity? not working
#2

That is interesting. I can reproduce it.
You know, to get an identity transformation without re-instantiation, you can also use the IDENTITY constant (Geom::Transformation::IDENTITY). But as it seems the use case checking whether a given transformation is identity is not provided by the API. I wonder whether it could really be a bug or rather by design (a flag in the transformation object). After all, numeric discrepancies in transformations can have much more impact than in lengths. Comparison of the floats of the transformation matrix could maybe not guarantee that the transformation really behaves like an identity.


#3

This was discussed with thomthom and others not so long ago…
In some ways .identity? is a pretty useless method.
A brand .new transformation has .identity? == true, but an exactly equivalent transformation that has been too-and-fro, and now has with exactly the same elements [when expressed as an array] will not return ‘true’ when tested using ‘.identity?’ - which seems to apply only to a new ‘empty’ [virgin] transformation…
I can’t see any use for it.
Whereas:

transformation.to_a==Geom::Transformation.new().to_a

returns ‘true’ IF the current transformation is equal to a ‘virgin’ one ?


#4

When working with transformations I noticed that the built in Transformation#identity? doesn’t function (probably due to floating point precision). I need to check if a transformation is the identity transformation and only move instances that actually need to be moved. However when checking (t*(t.inverse)).identity? the result is in most cases false even though it by definition should be true.

t is given by selecting an instance and run t=Sketchup.active_model.selection.first.transformation. (t*(t.inverse)).to_a typically looks like [1.0, 0.0, -2.77555756156289e-017, 0.0, 0.0, 1.0, 0.0, 0.0, -2.77555756156289e-017, -5.55111512312578e-017, 1.0, 0.0, 0.0, 0.0, 1.4210854715202e-014, 1.0]

I wrote this snippet that relies on the Length#== method which returns true if the values are close enough. It seems to work but I’m not a mathematician or a computer scientist (or even a trained programmer) and don’t know if the same precision can/should be used in practical applications when checking lengths and transformation matrices, so for now I consider this being a hack.

def identity_matrix?(t)
  
  a = t.to_a
  a_ref = Geom::Transformation.new.to_a
  a.each_with_index do |n, i|
    return false unless n.to_l == a_ref[i].to_l
  end
  
  true
  
end

If anyone knows a better way to do this please tell me.

edit: more tests shows that selecting an instance lined up with the model coordinates and running Sketchup.active_model.selection.first.transformation.identity? returns false even tough the same transformation translates to the array [1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0]. it’s not a precision error in the built in method. The method doesn’t seem to check the matrix at all :S .


#5

Microsoft uses the IEEE floating point specifications. This limits 64 bit floating point numbers to 53 bit mantissas. A simple example is to convert the decimal number 0.2 to 64-bit floating point and then back again to decimal.

(d) 0.2 = (b) 0.00110011001100110011001100110011001100110011001100110

Reversing this:

(b) 0.00110011001100110011001100110011001100110011001100110 = (d) 0.1999999999999999555910790149937383830547332763671875

If a transformation is scaled or rotated, etc., the inverse will likely not be equal to the original. Ruby docs indicate 15 or 16 digits of precision … 14 seems to be a safe bet. Maybe you could round + truncate the numbers before comparison?

While binary integers play nicely with decimals, binary fractions are not so friendly. There are some exceptions of course … (d) 0.5 = (b) 0.1 and (d) 0.25 = (b) 0.01 and (d) 0.75 = (b) 0.11 etc.


#6

As @jimhami42 has described, computer arithmetic has finite precision. The sort of small glitches you observe are to be expected in general. Comparisons of calculated values always have to include some tolerance (which is the source of the notorious vertex merging in small size models). However, it is odd that Transformation#identity? doesn’t account for this well-known fact.

This observation (which I can duplicate via an identity Array made into a Transformation by Geom::Transformation.new(array) ) is truly strange and worries me a lot. It seems to say that the identity? test relies on some undocumented flag that is only set by internal code not accessible via the API. There does not appear to be a method by which one can create a new Transformation that will pass this test!


#7

I mentioned this recently in:

It has also been discussed at Sketchucation, Even with floating point tolerance issues, the following should work:

t1 = Geom::Transformation.new
t1.identity?
true

t2 = Geom::Transformation.new( t1.to_a )
t2.identity?
false

So it appears to be an internal flag that is set to true for just the “new” initializer with no arguments, and therefore isn’t very useful.


#8

Yes - it’s an odd behaviour where the method reflects some internal state of the transformation object indicating if it was created by “the” identity transformation. Why this was exposed and why it acts in such a way that it doesn’t reflect the current state of the transformation is lost in history. I asked around the office but it pre-dates any of the developers who worked on the API.

Normally one should not break existing behaviour in an API, but I wonder in this case if any one ever rely on the existing behaviour…