User:Hkm/Fokker block code

Revision as of 00:25, 31 July 2025 by Hkm (talk | contribs) (Created page with "This code can be used to find Fokker blocks. <syntaxhighlight=python> import numpy as np from fractions import Fraction from itertools import combinations, product from math import gcd from functools import reduce from temper.lib_temper.temper import cokernel, defactored_hnf from temper.lib_temper.optimize import lstsq from temper.lib_temper.subgroup import p_limit from temper.lib_temper.interval import factors def prime_factors(n): factors = [] for d in range(...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This code can be used to find Fokker blocks. <syntaxhighlight=python> import numpy as np from fractions import Fraction from itertools import combinations, product from math import gcd from functools import reduce

from temper.lib_temper.temper import cokernel, defactored_hnf from temper.lib_temper.optimize import lstsq from temper.lib_temper.subgroup import p_limit from temper.lib_temper.interval import factors

def prime_factors(n):

   factors = []
   for d in range(2, int(n**0.5) + 1): 
       while n % d == 0: factors.append(d); n //= d
   return factors + ([n] if n > 1 else [])

def parse_subgroup(subgroup_str: str) -> list[Fraction]:

   if not subgroup_str.strip(): return []
   return [Fraction(s) for s in subgroup_str.replace('.', ' ').split()]

def parse_commas(comma_str: str, subgroup: list[Fraction] = [Fraction(p) for p in p_limit(97)]) -> list[Fraction]:

   comma_str = comma_str.strip()
   if not comma_str:
       return []
   if comma_str.startswith('['):
       try:
           cleaned_str = comma_str.replace('[', ).replace(']', ).replace(',', ' ').replace('>', ' >')
           monzo_parts = cleaned_str.split('>')
           monzo_strs = [part.strip() for part in monzo_parts if part.strip()]
           
           commas = []
           for s in monzo_strs:
               monzo = np.fromstring(s, dtype=int, sep=' ')
               if len(monzo) > len(subgroup):
                   raise ValueError(f"Monzo length {len(monzo)} does not match subgroup length {len(subgroup)}")
               elif len(monzo) < len(subgroup):
                   monzo = monzo[:len(subgroup)]
               
               comma = reduce(lambda x, y: x * y, [subgroup[i]**monzo[i] for i in range(len(monzo))], 1)
               commas.append(comma)
           return commas
           
       except ValueError as e:
           raise ValueError(f"Failed to parse monzo string: {comma_str}. Error: {e}")
   else:
       return [Fraction(s) for s in comma_str.split(', ')]

def _setup_temperament_analysis(comma_str: str, subgroup_str: str):

   """Prepares temperament data for analysis."""
   if not subgroup_str:
       parsed_commas = parse_commas(comma_str)
       subgroup_str = ".".join([str(f) for f in sorted(set(
           f for c in parsed_commas for f in prime_factors(c.denominator) + prime_factors(c.numerator)))])
       subgroup = parse_subgroup(subgroup_str)
   else:
       subgroup = parse_subgroup(subgroup_str)
       parsed_commas = parse_commas(comma_str, subgroup)
   prime_subgroup = sorted(list(set(f for s in subgroup for f in prime_factors(s.numerator) + prime_factors(s.denominator))))
   if not len(parsed_commas):
       mapping = np.eye(len(prime_subgroup))
   else:
       mapping = defactored_hnf(cokernel(np.hstack([factors(c, prime_subgroup) for c in parsed_commas])))
   
   solution, _ = lstsq((mapping, prime_subgroup), weight="tenney")
   
   return parsed_commas, prime_subgroup, mapping, solution, subgroup_str

def fokker_block(tmonzos: list[tuple], temperament_comma_str: str, subgroup_str: str="",

                offset: tuple=(), _prime_subgroup=None, _mapping=None, _solution=None):
   """ also allows tmonzos to be a comma string """
   if _prime_subgroup is None or _mapping is None or _solution is None:
       _, prime_subgroup, mapping, solution, subgroup_str_out = _setup_temperament_analysis(temperament_comma_str, subgroup_str)
   else:
       prime_subgroup, mapping, solution = _prime_subgroup, _mapping, _solution
       subgroup_str_out = subgroup_str
   # if tmonzos was passed as a comma string, convert it here. find the commas' monzos and apply the mapping
   if isinstance(tmonzos, str):
       monzos = [factors(comma, prime_subgroup) for comma in parse_commas(tmonzos, parse_subgroup(subgroup_str_out))]
       tmonzos = [np.dot(mapping, monzo) for monzo in monzos]
       
       # forbid enfactored comma bases
       reduced_tmonzos = []
       for tmonzo in tmonzos:
           vector = tmonzo.flatten() # tmonzo is a 2D array, flatten it to get the vector
           vector_gcd = reduce(gcd, [abs(x) for x in vector if x != 0])
           
           if vector_gcd > 1: print(f"Warning: enfactored comma bases must be expressed as tmonzos.")
           reduced_vector = vector // vector_gcd
           reduced_tmonzos.append(tuple(reduced_vector))
       
       tmonzos = reduced_tmonzos
   # get list of tmonzos inside parallelotope. offset is left bottom corner. also get tunings
   basis_vectors = np.array([t[1:] for t in tmonzos])
   dim = basis_vectors.shape[1]
   tunings = []
   if not offset: offset = np.zeros(dim)
   else: offset = np.array(offset)
   if basis_vectors.shape[0] != dim: raise ValueError("The number of tmonzo vectors must be" 
                           "equal to the dimension of the space after the first coordinate.")
   try: basis_inv = np.linalg.inv(basis_vectors)
   except np.linalg.LinAlgError: raise ValueError("tmonzos must be linearly independent.")
   iter_min = np.floor(np.sum(np.minimum(0, basis_vectors), axis=0) + offset).astype(int)
   iter_max = np.ceil (np.sum(np.maximum(0, basis_vectors), axis=0) + offset).astype(int)
   lattice_points = []
   ranges = [range(min_c, max_c + 1) for min_c, max_c in zip(iter_min, iter_max)]
   for p_tuple in product(*ranges):
       p = np.array(p_tuple)
       alpha = (p - offset) @ basis_inv
       if np.all((alpha >= 0) & (alpha < 1)):
           lattice_points.append(p)
   points_inside = []
   if lattice_points:
       s = solution.flatten()
       if len(s) != dim + 1:
           raise ValueError(f"Number of generators {len(s)} does not match tmonzo dimension {dim+1}.")
       
       for p in lattice_points:
           val = np.dot(s[1:], p)
           c1 = 1 - np.ceil(val / s[0])
           points_inside.append(tuple(np.concatenate(([c1], p)).astype(int)))
           tunings.append((val + c1 * s[0]) * 1200)  # in cents
   
   # get epimorph val
   epimorph_val = cokernel(np.array(tmonzos).T)
   # sort points inside by size for checking strength
   equal_order = []
   zipped = sorted(zip(points_inside, tunings), key=lambda x: x[1])
   for point, _ in zipped:
       equal_order.append(np.dot(epimorph_val, point))
   return zipped, tuple(epimorph_val[0]), all(equal_order[i] == i+1 for i in range(len(equal_order)))

def find_strong_blocks(note_range: tuple[int, int], comma_str: str, subgroup_str: str="", offset: tuple=()):

   low_count, high_count = note_range
   parsed_commas, prime_subgroup, mapping, solution, subgroup_str_out = _setup_temperament_analysis(comma_str, subgroup_str)
   dim = len(prime_subgroup) - len(parsed_commas) - 1
   if dim <= 0: return []
   s = solution.flatten()
   candidate_tmonzos = set()
   exponent_ranges = [range(high_count+1)] + [range(-high_count, high_count + 1)] * (dim-1)
   for p_tuple in product(*exponent_ranges):
       p = np.array(p_tuple)
       if np.linalg.norm(p) > high_count:
           continue
       val = np.dot(s[1:], p)
       t_0 = int(np.round(-val / s[0]))
       tmonzo = np.concatenate(([t_0], p)).astype(int)
       if not np.any(tmonzo):
           continue
       candidate_tmonzos.add(tuple(tmonzo)) # includes tmonzos with common factors
   candidate_tmonzos = sorted(list(candidate_tmonzos), key=lambda x: (sum(abs(c) for c in x), x))
   strong_blocks = []
   for tmonzo_basis in combinations(candidate_tmonzos, dim):
       try:
           basis_vectors = np.array([t[1:] for t in tmonzo_basis])
           if basis_vectors.shape[0] == basis_vectors.shape[1]:
               num_notes = round(abs(np.linalg.det(basis_vectors)))
               if not (low_count <= num_notes <= high_count):
                   continue
           result, val, is_strong = fokker_block(
               list(tmonzo_basis),
               comma_str,
               subgroup_str_out,
               offset,
               _prime_subgroup=prime_subgroup,
               _mapping=mapping,
               _solution=solution
           )
           
           if is_strong:
               strong_blocks.append({
                   'tmonzos': tmonzo_basis,
                   'notes': result,
                   'val': val,
                   'note_count': len(result),
               })
       except ValueError:
           continue
           
   return strong_blocks

if __name__ == "__main__":

   # print(*fokker_block([(-2, 20)], "225/224, 1029/1024", offset=(0)), sep="\n")
   # print(*fokker_block("25/24", "225/224, 1029/1024", offset=(0)), sep="\n")
   # print(*fokker_block("81/80, 49/48", "441/440, 540/539"), sep="\n")
   # print(*fokker_block([(-9, 6)], "81/80"), sep="\n")
   # print(*fokker_block("25/24", "81/80"), sep="\n")
   # print(*fokker_block("1029/1024", "[-4 -1 2>", "2.3.7"), sep="\n")
   # print(*fokker_block("1029/1024", "[-4 -1 0 2>"), sep="\n")
   # print(*find_strong_blocks((15, 16), "441/440, 540/539"), sep="\n\n")
   # print(*find_strong_blocks((9, 9), "", "2.3.5"), sep="\n\n")
   print(*find_strong_blocks((4, 26), "49/48", offset=(-2,)), sep="\n\n")
   

</syntaxhighlight>