9

Suppose I have the unit vector, u.

from numpy import mat
u = mat([[0.9**0.5], [-(0.1)**0.5]])
# [ 0.9486833  -0.31622777]

The unit vector is an eigenvector of a matrix with integer entries. I also know that the eigenvalues are integers. So, the unit vector will contain irrational decimals that, when squared, are decimal approximations of rational numbers.

Is there any good way to find the smallest value k such that all entries of ku are integers? In general, k will be the square root of an integer.

A naive approach would be to square each entry in the vector and find the smallest ki that produces an integer. Then, k will be the square root the of LCM of all ki. I'm hoping there is a better approach than this.


Note that this is not a duplicate of this question.

Community
  • 1
  • 1
Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
  • For an arbitrary unit vector it is not necessarily possible to scale it so that both entries are integers. Are you only concerned with the subset of unit vectors that have this property? – Christian Mar 30 '16 at 21:44
  • @Christian I know that the unit vector is an eigenvector of a matrix with integer entries and that the eigenvalues are also integers. Thus, pre-normalization, the vector certainly had rational values. I'll add this to the question. – Jared Goguen Mar 30 '16 at 21:54
  • Do you only have access to the eigenvalues and eigenvectors, or do you have access to the original matrix that these were derived from? – Christian Mar 30 '16 at 22:33
  • @Christian I have access, but I'm not looking to re-diagonals the matrix. I'm mostly looking for solutions that don't rely on the eigenvalues or the matrix because that's not going to apply in other cases. – Jared Goguen Mar 30 '16 at 22:48
  • For the time being, here's a [relevant link](http://www.mathworks.com/help/matlab/ref/rat.html?requestedDomain=www.mathworks.com). – Jared Goguen Mar 31 '16 at 03:03

4 Answers4

4

I've improved the methodology provided by Christian in order to accept a wider range of fractions. The trick was to "pre-normalize" the unit vector by dividing it by the smallest non-zero entry. This works reliably for all fractions of a specified maximum denominator.

from fractions import Fraction, gcd

def recover_integer_vector(u, denom=50):
    '''
    For a given vector u, return the smallest vector with all integer entries.
    '''

    # make smallest non-zero entry 1
    u /= min(abs(x) for x in u if x)

    # get the denominators of the fractions
    denoms = (Fraction(x).limit_denominator(denom).denominator for x in u)

    # multiply the scaled u by LCM(denominators)
    lcm = lambda a, b: a * b / gcd(a, b)
    return u * reduce(lcm, denoms)

Testing:

The following code was used to ensure that all fractions in the given range work correctly.

import numpy as np

from numpy import array
from itertools import combinations_with_replacement


for t in combinations_with_replacement(range(1, 50), 3):
    if reduce(gcd, t) != 1: continue

    v = array(map(float, t))
    u = v / np.linalg.norm(v)

    w = recover_integer_vector(u)

    assert np.allclose(w, v) or np.allclose(w, -v)

As can be seen by the testing time, this code is not very quick. It took my computer about 6 seconds to test. I'm not sure whether the timing can be improved.

Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
  • Hi! I tried with this code. Why does it not work? TypeError: argument should be a string or a Rational instance. Could someone please edit the code so that it working. I am new to python – FredrikAa Oct 05 '22 at 20:35
  • matrix_test=[[-0.1061] , [0.0005], [0.0005] , [0.0005] , [0.3017] , [0.4834] , [0.4670], [0.2398], [-0.1467], [-0.6058] ,[0.0000] ,[0.0000] ,[0.0000] ,[0.0000] ,[0.0000] ,[0.0000] ,[0.0000] ,[0.0000] , [0.0000] , [0.0000], [0.0000], [0.0000] , [0.0000] , [0.0000],[0.0000],[0.0000] , [0.0000] , [0.0000] ,[0.0000],[0.0000], [0.0000], [0.0000] ,[0.0000],[0.0000] , [0.0000] ,[0.0000], [0.0000], [0.0000] , [0.0000]] vec= np.array(matrix_test) print(vec) print(recover_integer_vector(vec)) – FredrikAa Oct 05 '22 at 20:44
2

You could square the terms, find the sequence of continued fraction convergents of each, replace the numerator of each convergent with the nearest square number, and look for the smallest denominator that appears in all of the lists of convergents (as part of convergents with tolerable accuracy). Your definition of "tolerable accuracy" allows the algorithm to deal with a bit of rounding error without producing an insanely large result. In the case of your example, 9/10 and 1/10 would be among the first few convergents, with relative errors less than 10^-9, and sqrt(10) would be your scaling factor (multiplying out to [3, -1] which you can read directly off of the square roots of the numerators of the convergents, if you make sure to recover the signs). It's unclear what to do if this algorithm doesn't find a common denominator, though.

hobbs
  • 223,387
  • 19
  • 210
  • 288
  • I'll try implementing this when I get home. Both the length of the vector and the common denominator will be relatively bounded (i.e. no crazy cases). Presumably this will help with both the "tolerable accuracy" and in accurately identifying the LCM of the divisors? – Jared Goguen Mar 30 '16 at 23:13
  • This seems like a fun way to do it, but wouldn't this have essentially the same algorithmic complexity as the original "naive approach"? – Christian Mar 31 '16 at 03:49
  • @Christian my intuition makes me think that this approach will be O(log n) rather than O(n) where n is the denominator, but I need to reinvestigate continued fractions to validate that claim. The "nearest square number part" is clearly constant time in terms of complexity. You've been awfully curious about this question, do you have any insight? (I don't intend for that to sound combative, any thoughts are welcome.) – Jared Goguen Mar 31 '16 at 05:35
  • I'm more than willing to admit that numeric stuff isn't my strong suit, so I'd be happy to see more answers here. I'm not sure if this method is faster or better than any other, just throwing it out there as a suggestion :) – hobbs Mar 31 '16 at 05:41
  • @Jared No combativeness perceived. I am very intrigued this problem and have been thinking about it all day, but have yet to come up with anything that is superior to the original algorithm you proposed. Hopefully a vision will come to me soon :) – Christian Mar 31 '16 at 05:45
  • If you're curious, it seems that [`Fraction.limit_denominator`](https://hg.python.org/cpython/file/2.7/Lib/fractions.py#l107) takes a similar approach. – Jared Goguen Mar 31 '16 at 08:22
2

Ok, here's my best stab at it. This algorithm seems to work, though I can't guarantee it will be faster than what you originally proposed:

import numpy as np
from fractions import Fraction, gcd

# need some finesse and knowledge of problem domain to fine tune this parameter
# otherwise you'll end up with extremely large k due to precision problems
MAX_DENOMINATOR = 1000

# least common multiple
def lcm(a, b):
    return a * b / gcd(a, b)

# returns the denominator of the square of a number
def get_square_denominator(x):
    return Fraction(x ** 2).limit_denominator(MAX_DENOMINATOR).denominator

# returns the smallest multiplier, k, that can be used to scale a vector to
# have integer coefficients. Assumes vector has the property that it can be 
# expressed as [x**0.5, y**0.5, ...] where x, y, ... are rational with 
# denominators <= MAX_DENOMINATOR
def get_k(v):
    denominators = [get_square_denominator(i.item()) for i in v]
    return reduce(lcm, denominators) ** 0.5

Here it is in use:

>>> u = matrix([[0.9486833], [-0.31622777]])
>>> get_k(u)
3.1622776601683795
>>> get_k(u) * u
matrix([[ 3.00000001],
        [-1.00000001]])
Christian
  • 709
  • 3
  • 8
  • This works beautifully, very nice job. To get any bearing on complexity, we'd have to dive into the inner-workings of `Fraction`, which may be another adventure in itself. – Jared Goguen Mar 31 '16 at 07:45
  • To elaborate, `float.as_integer_ratio` starts the process, but yields a denominator on the order of 10^15. The remainder of the work is done in `limit_denominator`, part of which resembles Euclid's algorithm. From the docstring, "then it can be proved that a rational number is a best upper or lower approximation to x if, and only if, it is a convergent or semiconvergent of the (unique shortest) continued fraction." – Jared Goguen Mar 31 '16 at 08:18
  • Glad you like it! Yeah I wondered if the `fractions` module was using some of @Hobbs' convergents ideas under the hood. As for the complexity, I'd be in a bit over my head, but if anything it might run faster than your original proposal in most cases simply because `fractions` is (hopefully) highly optimized. – Christian Mar 31 '16 at 14:34
  • After further testing, this is limited to pretty small denominators. For example, [-50, 1] will not work. – Jared Goguen Apr 01 '16 at 22:30
  • Right. You have to fine tune the `MAX_DENOMINATOR` value based on what you're expecting. In my original code, `MAX_DENOMINATOR = 1000`, so in this case where the denominators is `50**2 + 1**2 = 2501`. It won't work. You can probably safely set MAX_DENOMINATOR much higher, just at some point you might start running into precision errors. – Christian Apr 01 '16 at 23:22
  • I believe I tried setting it higher without success, but I'll have to verify that later. Either way, the square root portion is a red herring. It seems that dividing the vector by the smallest non-zero entry and then LCMing those denominators is a more reliable approach with the added benefit of having the maximum denominator based off the entries rather than the square of the norm. – Jared Goguen Apr 02 '16 at 00:21
  • True. Your enhanced approach is better. – Christian Apr 02 '16 at 00:25
2

Let u=[cos(α),sin(α)] the unit vector of v=[p,q], where p and q are little integers, prime together . Then tan(α) is a rational.

So there is a straightforward way to find back v from u : fractions.Fraction(u[1]/u[0]).limit_denominator()

Example :

v=array([-1,3])
u=v/np.linalg.norm(v)
# array([-0.31622777,  0.9486833 ])
f=Fraction(u[1]/u[0]).limit_denominator()
# Fraction(-1, 3)

if you want k, norm([f.numerator,f.denominator]) give it.

p=0 is a trivial particular case.

B. M.
  • 18,243
  • 2
  • 35
  • 54