Advertisement
Kaelygon

Better divisibility rule finder

Nov 23rd, 2024
155
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 6.58 KB | None | 0 0
  1. import math
  2. import sys
  3. from tabulate import tabulate
  4.  
  5. # This terrible python code is provided by Kaelygon
  6. #
  7. # Find divisibility rule by prime P: P * N = a * 10^b + c
  8. # such that an integer can be split in two parts A and B
  9. # and reduced to a smaller number by simulating 10^b division and modulus
  10. # using string manipulation and addition: B*a-A*c
  11. # Matt Parker explanation https://youtu.be/6pLz8wEQYkA
  12.  
  13.  
  14. def countDigits(n): #floor(log10(n))+1
  15.     return int( n.bit_length() * math.log10(2) ) + 1
  16.  
  17. def getNthReciprocal(n,pos):
  18.     digits=countDigits(n)
  19.     powTen=(10**(digits+pos)) #shift decimal
  20.     digit=(powTen//n)%10 #get last digit
  21.     return digit
  22.  
  23. def getReciprocalDigits(n,count): #get non-zero digits of 1/n
  24.     array=[]
  25.     for i in range(count):
  26.         digit=getNthReciprocal(n,i)
  27.         array.append(digit)
  28.        
  29.     return array
  30.  
  31. def getNthRound(array,n): #round element if next elem is >=5
  32.     if n>=len(array)-1: return array[n] #last element
  33.     isRoundUp = int(array[n+1]>=5)
  34.     return array[n]+isRoundUp
  35.  
  36. def getRoundReciprocal(array,n0,n1):
  37.     n1-=1 #compensate for the last number
  38.     num=0
  39.     for i in range(n0,n1):
  40.        num=num*10+array[i]
  41.     num=num*10+getNthRound(array,n1)
  42.     return num
  43.  
  44. def intDiv(a,b): #Rounded integer division
  45.     return (a+b//2)//b
  46.  
  47. #Find divisibility rules P * N = a * 10^b + c
  48. def findDivRulesByInverse(P, aRange, bRange, cRange):
  49.     rules = []
  50.    
  51.     reciList=getReciprocalDigits(P,bRange[1]+1) #non-zero reciprocal digit list
  52.     magP=countDigits(P)-1
  53.     for b in range(bRange[0], bRange[1]):
  54.         bPowTen=10**b # 10^b
  55.         magReci=b-magP+1 #magnitude such that P*reci magnitude will be same as 10^(b+1) # P+r=b+1
  56.         reci=getRoundReciprocal(reciList,0,magReci)
  57.        
  58.         scalerMax=reci #next b will cover numbers when scaler>scalerMax
  59.         scaler=1
  60.         for i in range(aRange[0],aRange[1]):
  61.             scaler=intDiv(i*bPowTen,P) #Find nearest multiples that results in a*P=a*10^n+c where c is a small and n is near b
  62.             if(scaler>scalerMax):break
  63.             PN = P*scaler
  64.            
  65.             #Calculate rules
  66.             a=intDiv(PN,bPowTen)
  67.             c=PN-a*bPowTen
  68.             N=scaler
  69.             if(c>cRange[0] and c<cRange[1]):
  70.                 rules.append((P, N, a, b, c))
  71.    
  72.     return rules
  73.  
  74. #Output formatting
  75. def printRuleTables(rules):
  76.     headers = ["P", "N", "a", "b", "c", "P*N"]
  77.     table = [[P, N, a, b, c, P * N] for P, N, a, b, c in rules]
  78.     print(tabulate(table, headers=headers, tablefmt="plain"))  # Use "plain", "grid", or others
  79.  
  80. def printDivIters(iterTable):
  81.     headers = ["A.B", "B*a-A*c", "Intermed"]
  82.     table = [[f"{A}.{B}", f"{B}*{a}-{A}*{c}", iterNum] for A, B, iterNum, a, c in iterTable]
  83.     print(tabulate(table, headers=headers, tablefmt="plain"))
  84.    
  85.  
  86. # Test function that reduces a number by using the given rules
  87. def divByRule(rules,num):
  88.     P,N,a,b,c=rules
  89.     print(f"Using rules P={P} a={a} b={b} c={c}")
  90.     print(f"Testing {num} divisibility by {P}\n")
  91.    
  92.     iterNum=num
  93.     iterHistory=[num]
  94.     iterTable=[]
  95.     maxIters=100 #Very unlikely to reach +100 loop
  96.     iterCount=0
  97.     iterSmallest=num #best valid iteration
  98.    
  99.     while(iterCount<maxIters):
  100.         iterCount+=1
  101.         if iterNum<iterSmallest: iterSmallest=iterNum
  102.         #Simulate splitting number at b:th digit
  103.         powTen=10**b
  104.         A=iterNum//powTen
  105.         B=iterNum%powTen
  106.         if A==0 or B==0: break #Break if invalid A or B
  107.         iterNum=B*a-A*c
  108.         iterTable.append([A,B,iterNum,a,c])
  109.        
  110.         #Keep history to check whether the iteration is in a loop
  111.         if iterNum in iterHistory: break
  112.         iterHistory.append(iterNum)
  113.    
  114.     printDivIters(iterTable)
  115.     print("\n")
  116.    
  117.     factor=iterSmallest/P
  118.     print(f"Smallest iteration {iterSmallest} = {P}*{factor:g}")
  119.     wholeNum=math.floor(factor)
  120.     if factor != wholeNum:
  121.         print(f"Not divisible by {P}")
  122.         print(f"Remainder of {num}/{P} is {num%P}")
  123.     else:
  124.         print(f"{num} is divisible by {P}")
  125.        
  126.     return iterNum
  127.    
  128. #Simple rotate right LCG for deterministic results
  129. def rorLCG(seed):
  130.     modulus=0xFFFFFFFFFFFFFFFF #2^64-1
  131.     bits=modulus.bit_length()
  132.     shift=int((bits+1)/3)
  133.     randNum=(seed>>shift)|((seed<<(bits-shift))&modulus) #RORR
  134.     randNum=(randNum*3343+11770513)&modulus #LCG
  135.     return randNum
  136.  
  137.  
  138. def randDivRuleTest(rules,testProductsP):
  139.     print("\nExample:")
  140.    
  141.     P=rules[0]
  142.     randNum=rorLCG(P) #use the prime as seed
  143.    
  144.     minSize=P+17
  145.     maxSize=P+P//2+minSize
  146.    
  147.     randPN=1
  148.     if testProductsP:
  149.         mult=randNum%maxSize+minSize
  150.         randPN=P*mult # random product of P
  151.     else:
  152.         randPN=randNum%(maxSize**2)+minSize**2
  153.         if randPN%P==0: randPN+=1 # ensure the test number is not product of P
  154.    
  155.     num=divByRule(rules,randPN)
  156.  
  157. def main():
  158.     #Configuration
  159.     defaultP = 313 # number which divisibility rule is searched
  160.     P = int(sys.argv[1]) if len(sys.argv) > 1 else defaultP
  161.     if P==0: P=defaultP
  162.    
  163.     testBestRule  = True # Verify rule by iterating B*a-A*c
  164.     testProductsP = True # Whether we should test divisibility rules with known products of P
  165.     maxDisplay=10 # Limit shown rule count
  166.    
  167.     # 'a' and 'c' are multipliers in B*a-A*c, the smaller they are
  168.     # the easier they will be to calculate by hand
  169.     #
  170.     # 'b' determines where the tested number split right to left,
  171.     # which sets the viable magnitude of the tested numbers
  172.     # e.g. b=3 works well with numbers 10^4 to 10^7
  173.     #
  174.     # Chosen a,b,c reasoning:
  175.     # checking a>9 is redundant these values will be checked by next b (a*10^b)
  176.     # b 2-4x the magnitude of P are doable by hand. 'b' sets the split 'B' magnitude
  177.     # increase c when numbers grows as it becomes harder to find divisibility rules
  178.     #
  179.     magP=countDigits(P)
  180.     aRange = [ 1, 10             ]
  181.     bRange = [ magP, magP*2+1 ]
  182.     cRange = [ -abs(P//20), abs(P//20) ]
  183.     #upper bound minimums
  184.     if bRange[1]<5  : bRange[1]=5
  185.     if cRange[1]<16 : cRange[1]=16
  186.  
  187.     #Computation starts
  188.     rules = findDivRulesByInverse(P, aRange, bRange, cRange)
  189.     rules.sort(key=lambda x: (x[3], abs(x[4])))
  190.  
  191.     #Print
  192.     if rules:
  193.         print(f"Found {len(rules)} rules for {P}, showing first {maxDisplay}:\n")
  194.         printRuleTables(rules[:maxDisplay])  # Display first 10 rules
  195.     else:
  196.         print(f"No rules found for {P}")
  197.         return 0
  198.  
  199.     if testBestRule:
  200.         randDivRuleTest(rules[0],testProductsP)
  201.    
  202.  
  203. if __name__=="__main__":
  204.     main()
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement