Skip to content

Make inverse(::VectorTransfrom, x) return a vector of floats #133

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 2 commits into from
Apr 7, 2025

Conversation

devmotion
Copy link
Collaborator

Fixes #132.

I was debating with myself whether this should be addressed in inverse or inverse_eltype. I came to the conclusion that inverse is the right place because modifications in inverse_eltype could lead to undesired results (too early change to floats or Float64) when composing vector transforms (and the current inverse_eltype definitions are not actually incorrect).

function inverse(t::VectorTransform, y)
inverse!(Vector{inverse_eltype(t, y)}(undef, dimension(t)), t, y)
inverse!(Vector{_float_or_Float64(inverse_eltype(t, y))}(undef, dimension(t)), t, y)
Copy link
Owner

Choose a reason for hiding this comment

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

I think that this catches too much. I would simply condition on dimension(t) == 0, I don't think that Union{} can happen outside that. Something like

T = inverse_eltype(t, y)
d = dimension(t)    
if T === Union{} || d == 0
    T = Float64
end
inverse!(Vector{T)}(undef, d), t, y)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In your suggestion you seem to condition on Union{} as well?

I was actually considering checking only dimsnion(t) == 0 as for my use case it shouldn't matter. I went with checking only whether inverse_eltype(t, y) === Union{} since I was worried that branching on the value of dimension(t) could introduce a type instability.

Copy link
Owner

Choose a reason for hiding this comment

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

Yes, that makes sense.

But now that I think about it, it is kind of funny to make eltype(inverse(t, y)) != inverse_eltype(t, y). I think it would be best to fix it in inverse_eltype, the following way:

  1. Introduce a function, eg narrow_inverse_eltype, basically rename inverse_eltype to that, in the state before this PR. Document that types should extent it.

  2. In inverse_eltype, just check for Union{} and replace with Float64.

Now that I look at the code, the eltype determination is a bit flaky in a lot of places:

julia> t = as(Array, 3)
[1:3] 3×
  asℝ

julia> inverse(t, Any[1, 2.1, 3])
ERROR: InexactError: Int64(2.1)

We should probably rethink it as a whole for all kinds of corner cases. Suggestions welcome.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Initially I wanted to change inverse_eltype. But making it concrete seemed wrong - similar to the scalar transforms (such as inverse_eltype(::Constant) = Union{}), per se an inverse_eltype seems completely correct: It avoids prematurely introducing e.g. Float64 in a "non-leaf" transform (ie before applying all compositions or combinations). IMO the element type is only relevant when the output array is actually instantiated, and changes (such as changing to floating point number types or turning Union{} to Float64) should only be performed at this final stage - as done in this PR.

Now that I look at the code, the eltype determination is a bit flaky in a lot of places:

I think this example is quite special as it is caused by the same bug as #73: The element type of the output (inverse_eltype) is in this case computed based on the first element of input x. Whereas actually for non-concrete element types, I think inverse_eltype take into account the full input x, e.g. by promoting the inverse_eltypes for each element (can't be inferred in this case) or returning an abstract type such as Any.

julia> TransformVariables.inverse_eltype(t, Any[1, 2.1, 3])
Int64

julia> TransformVariables.inverse_eltype(t, Any[2.1, 1, 3])
Float64

By construction of this failing example, it is actually fixed by this PR since Int would be changed to Float64 when constructing the Vector. With this PR,

julia> inverse(t, Any[1, 2.1, 3])
3-element Vector{Float64}:
 1.0
 2.1
 3.0

Copy link
Owner

@tpapp tpapp left a comment

Choose a reason for hiding this comment

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

LGTM, thanks! I will clarify the API in another PR.

@tpapp tpapp merged commit 5263ffe into tpapp:master Apr 7, 2025
5 checks passed
@devmotion devmotion deleted the dw/inverse_vectortransform branch April 7, 2025 12:49
@devmotion
Copy link
Collaborator Author

Could you tag a new release? I updated the package version in this PR but you might want to also include the latest commit on the master branches with the additional explanations regarding inverse_eltype.

@tpapp
Copy link
Owner

tpapp commented Apr 7, 2025

I am away from my compurer for today, please tag one (I added you to the repo).

@devmotion
Copy link
Collaborator Author

OK, will do!

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.

Avoid that inverse(::VectorTransform, x) returns a Vector{Union{}}?
2 participants