Project is now named Celeste

This commit is contained in:
Emile Clark-Boman 2025-07-06 19:20:20 +10:00
parent 87917f9526
commit 269092fb53
45 changed files with 1507 additions and 12 deletions

View file

@ -0,0 +1,2 @@
from .pftrialdivision import trial_division
from .factordb import DBResult, FType, FCertainty

View file

@ -0,0 +1,161 @@
'''
Simple interface for https://factordb.com inspired by
https://github.com/ihebski/factordb
TODO:
1. Implement primality certificate generation, read this:
https://reference.wolfram.com/language/PrimalityProving/ref/ProvablePrimeQ.html
'''
import requests
from enum import Enum, StrEnum
_FDB_URI = 'https://factordb.com'
# Generated by https://www.asciiart.eu/text-to-ascii-art
# using "ANSI Shadow" font and "Box drawings double" border with 1 H. Padding
_BANNER = '''
cli by imbored
'''.strip()
# Enumeration of number types based on their factorisation
class FType(Enum):
Unit = 1
Composite = 2
Prime = 3
Unknown = 4
class FCertainty(Enum):
Certain = 1
Partial = 2
Unknown = 3
# FactorDB result status codes
class DBStatus(StrEnum):
C = 'C'
CF = 'CF'
FF = 'FF'
P = 'P'
PRP = 'PRP'
U = 'U'
Unit = 'Unit' # just for 1
N = 'N'
Add = '*'
def is_unknown(self) -> bool:
return (self in [DBStatus.U, DBStatus.N, DBStatus.Add])
def classify(self) -> tuple[FType, FCertainty]:
return _STATUS_MAP[self]
def msg_verbose(self) -> str:
return _STATUS_MSG_VERBOSE[self]
# Map of DB Status codes to their factorisation type and certainty
_STATUS_MAP = {
DBStatus.Unit: (FType.Unit, FCertainty.Certain),
DBStatus.C: (FType.Composite, FCertainty.Unknown),
DBStatus.CF: (FType.Composite, FCertainty.Partial),
DBStatus.FF: (FType.Composite, FCertainty.Certain),
DBStatus.P: (FType.Prime, FCertainty.Certain),
DBStatus.PRP: (FType.Prime, FCertainty.Partial),
DBStatus.U: (FType.Unknown, FCertainty.Unknown),
DBStatus.N: (FType.Unknown, FCertainty.Unknown),
DBStatus.Add: (FType.Unknown, FCertainty.Unknown),
}
# Reference: https://factordb.com/status.html
# NOTE: my factor messages differ slightly from the reference
_STATUS_MSG_VERBOSE = {
DBStatus.Unit: 'Unit, trivial factorisation',
DBStatus.C: 'Composite, no factors known',
DBStatus.CF: 'Composite, *partially* factors',
DBStatus.FF: 'Composite, fully factored',
DBStatus.P: 'Prime',
DBStatus.PRP: 'Probable prime',
DBStatus.U: 'Unknown (*but in database)',
DBStatus.N: 'Not in database (-not added due to your settings)',
DBStatus.Add: 'Not in database (+added during request)',
}
# Struct for storing database results with named properties
class DBResult:
def __init__(self,
status: DBStatus,
factors: tuple[tuple[int, int]]) -> None:
self.status = status
self.factors = factors
self.ftype, self.certainty = self.status.classify()
def _make_cookie(fdbuser: str | None) -> dict[str, str]:
return {} if fdbuser is None else {'fdbuser': fdbuser}
def _get_key(by_id: bool):
return 'id' if by_id else 'query'
def _api_query(n: int,
fdbuser: str | None,
by_id: bool = False) -> requests.models.Response:
key = _get_key(by_id)
uri = f'{_FDB_URI}/api?{key}={n}'
return requests.get(uri, cookies=_make_cookie(fdbuser))
def _report_factor(n: int,
factor: int,
fdbuser: str | None,
by_id: bool = False) -> requests.models.Response:
key = _get_key(by_id)
uri = f'{_FDB_URI}/reportfactor.php?{key}={n}&factor={factor}'
return requests.get(uri, cookies=_make_cookie(fdbuser))
'''
Attempts a query to FactorDB, returns a DBResult object
on success, or None if the request failed due to the
get request raising a RequestException.
'''
def query(n: int,
token: str | None = None,
by_id: bool = False,
cli: bool = False) -> DBResult | None:
if cli:
print(_BANNER)
try:
resp = _api_query(n, token, by_id=by_id)
except requests.exceptions.RequestException:
return None
content = resp.json()
result = DBResult(
DBStatus(content['status']),
tuple((int(F[0]), F[1]) for F in content['factors'])
)
if cli:
print(f'Status: {result.status.msg_verbose()}')
print(result.factors)
# ensure the unit has the trivial factorisation (for consistency)
if result.status == DBStatus.Unit:
result.factors = ((1, 1),)
return result
'''
Reports a known factor to FactorDB, also tests it is
actually a factor to avoid wasting FactorDBs resources.
'''
def report(n: int,
factor: int,
by_id: int,
token: str | None = None) -> None:
try:
resp = _report_factor(n, factor, token)
except requests.exceptions.RequestException:
return None
content = resp.json()
print(content)

View file

@ -0,0 +1,46 @@
'''
The trial division algorithm is essentially the idea that
all factors of an integer n are less than or equal to isqrt(n),
where isqrt is floor(sqrt(n)).
Moreover, if p divides n, then all other factors of n must
be factors of n//p. Hence they must be <= isqrt(n//p).
'''
from math import isqrt # integer square root
# Returns the "multiplicity" of a prime factor
def pf_multiplicity(n: int, p: int) -> int:
mult = 0
while n % p == 0:
n //= p
mult += 1
return n, mult
'''
Trial division prime factorisation algorithm.
Returns a list of tuples (p, m) where p is
a prime factor and m is its multiplicity.
'''
def trial_division(n: int) -> list[tuple[int, int]]:
factors = []
# determine multiplicity of the only even prime (2)
n, mult_2 = pf_multiplicity(n, 2)
if mult_2: factors.append((2, mult_2))
# determine odd factors and their multiplicities
p = 3
mult = 0
limit = isqrt(n)
while p <= limit:
n, mult = pf_multiplicity(n, p)
if mult:
factors.append((p, mult))
limit = isqrt(n) # recalculate limit
mult = 0 # reset
else:
p += 2
# if n is still greater than 1, then n is a prime factor
if n > 1:
factors.append((n, 1))
return factors