Source code for prdtools.designer

import os
import typing as tp
from numbers import Number
from dataclasses import dataclass

try:
    import click
except ImportError:
    click = None

from .math import *
from .table import TableParameters

__all__ = ('Designer', 'DesignResult')

[docs]class Designer: """A utility to assist in choosing correct values for :class:`~.table.TableParameters` """ ncols: int #: Current number of columns nrows: int #: Current number of rows prime_num: int #: Current prime number aspect_ratio_min: Number #: Minimum valid value for :attr:`aspect_ratio` aspect_ratio_max: Number #: Maximum valid value for :attr:`aspect_ratio` def __init__( self, aspect_ratio_min: Number = 0.4, aspect_ratio_max: Number = 2.5, ): self.ncols, self.nrows, self.prime_num = 1, 1, 1 self.aspect_ratio_min = aspect_ratio_min self.aspect_ratio_max = aspect_ratio_max @property def aspect_ratio(self) -> float: """The aspect ratio of :attr:`ncols` and :attr:`nrows` """ return self.ncols / self.nrows def next_prime(self): p = self.prime_num x = p + 1 while True: if is_prime(x): self.prime_num = x return x x += 1 def is_aspect_ratio_valid(self): rmin, rmax = self.aspect_ratio_min, self.aspect_ratio_max return rmin <= self.aspect_ratio <= rmax
[docs] def is_valid(self) -> bool: """Check whether the current values are valid * :attr:`prime_num` must equal :attr:`ncols` + :attr:`nrows` + 1 * :attr:`aspect_ratio` must be within the range (:attr:`aspect_ratio_min`, :attr:`aspect_ratio_max`) * :attr:`ncols` and :attr:`nrows` must be :term:`coprime` to each other * :attr:`prime_num` must be a prime number """ if self.ncols * self.nrows + 1 != self.prime_num: return False if not self.is_aspect_ratio_valid(): return False if not is_coprime(self.nrows, self.ncols): return False if not is_prime(self.prime_num): return False if not has_prim_roots(self.prime_num): return False return True
[docs] def from_ncols(self, ncols: int) -> tp.Iterable['DesignResult']: """Find possible choices for :attr:`nrows` (and thus :attr:`prime_num`) with the given value for :attr:`ncols` Iterates through all possible values for :attr:`nrows` that are valid using the constraints listed in :meth:`is_valid`. For each valid result, a :class:`DesignResult` is yielded. """ self.ncols = ncols min_rows = 2 max_rows = ncols * 3 results = set() self.nrows = min_rows while self.nrows <= max_rows: self.prime_num = self.ncols * self.nrows + 1 while not is_prime(self.prime_num): self.nrows += 1 self.prime_num = self.ncols * self.nrows + 1 if self.is_valid() and self.prime_num not in results: yield self._build_result() results.add(self.prime_num) self.nrows += 1
[docs] def from_prime_num(self, prime_num: int) -> tp.Iterable['DesignResult']: """Find possible choices for :attr:`ncols` and :attr:`nrows` for the given prime number Iterates through all :term:`coprime` pairs of ``prime_num - 1`` that match the constraints listed in :meth:`is_valid`. For each valid pair, a :class:`DesignResult` is yielded. """ self.prime_num = prime_num prime_changed = False while not is_prime(self.prime_num) and not has_prim_roots(self.prime_num): self.next_prime() prime_changed = True print(f'using {self.prime_num} for prime') coprime_pairs = [] for ncols, nrows in iter_coprimes(self.prime_num - 1): self.ncols, self.nrows = ncols, nrows if self.is_valid(): yield self._build_result() coprime_pairs.append((nrows, ncols)) for ncols, nrows in coprime_pairs: self.ncols, self.nrows = ncols, nrows if self.is_valid(): yield self._build_result()
def _build_result(self) -> 'DesignResult': return DesignResult( ncols=self.ncols, nrows=self.nrows, prime_num=self.prime_num, )
[docs]@dataclass class DesignResult: """Result from :class:`Designer` """ ncols: int #: Number of columns nrows: int #: Number of rows prime_num: int #: Prime number @property def aspect_ratio(self) -> float: """The aspect ratio of :attr:`ncols` and :attr:`nrows` """ return self.ncols / self.nrows
[docs] def get_primitive_roots(self) -> tp.List[int]: """Get all :term:`primitive roots <primitive root>` of :attr:`prime_num` """ return list(self.iter_primitive_roots())
def iter_primitive_roots(self) -> tp.Iterable[int]: yield from prim_roots(self.prime_num)
[docs] def choose_primitive_root(self) -> int: """Find the smallest :term:`primitive root` of :attr:`prime_num` Raises: ValueError: if no primitive roots exist """ roots = self.get_primitive_roots() if not len(roots): raise ValueError(f'No primitive roots found for {self.prime_num}') try: root = min([r for r in roots if r > 2]) except ValueError: root = min(roots) return root
[docs] def to_parameters( self, design_freq: int, prime_root: tp.Optional[int] = None, well_width: tp.Optional[float] = 3.81, speed_of_sound: tp.Optional[int] = SPEED_OF_SOUND, ) -> TableParameters: """Create a :class:`~.table.TableParameters` instance using this result All arguments of this method will be passed to the constructor (documented :class:`here <.table.TableParameters>`). If *prime_root* is not given, a value will be chosen by :meth:`choose_primitive_root` """ if prime_root is None: prime_root = self.choose_primitive_root() return TableParameters( nrows=self.nrows, ncols=self.ncols, prime_num=self.prime_num, prime_root=prime_root, design_freq=design_freq, well_width=well_width, speed_of_sound=speed_of_sound, )
if click is not None: @click.group() @click.option('-f', '--design-freq', type=int) @click.option('-w', '--well-width', default=3.81) @click.option('-s', '--speed-of-sound', default=SPEED_OF_SOUND) @click.option('-r', '--prime-root', type=int) @click.option('--offset', default=0) @click.option('--format', type=click.Choice(['csv', 'rst'], case_sensitive=False), default='rst', ) @click.pass_context def design(ctx, **kwargs): ctx.ensure_object(dict) ctx.obj.update({k:v for k,v in kwargs.items()}) def _build_result_and_output(ctx: click.Context, result: DesignResult): param_kw = ctx.obj.copy() offset, out_fmt = param_kw.pop('offset'), param_kw.pop('format') if not param_kw['prime_root']: roots = list(prim_roots(result.prime_num)) if len(roots) > 10: roots = roots[:11] roots = [str(v) for v in roots] pr = click.prompt( 'Choose a primitive root', type=click.Choice(roots), ) param_kw['prime_root'] = int(pr) if not param_kw['design_freq']: value = click.prompt('Please enter design frequency', type=int) param_kw['design_freq'] = value p = result.to_parameters(**param_kw) tbl_result = p.calculate() if out_fmt == 'csv': click.echo(tbl_result.to_csv(offset=offset)) else: click.echo(tbl_result.to_rst(offset=offset)) click.echo(tbl_result.get_info_str(offset=offset)) @design.command(name='cols') @click.option('--ncols', type=int, prompt=True) @click.pass_context def from_cols(ctx, ncols): d = Designer() found = False for result in d.from_ncols(ncols): found = True info_txt = ( f'({result.ncols}x{result.nrows}), aspect={result.aspect_ratio:.3f},' f'prime_num={result.prime_num}' ) click.echo(info_txt) if click.confirm('Use this design?'): _build_result_and_output(ctx, result) break if not found: click.echo('No results found') @design.command(name='prime') @click.option('--prime-num', type=int, prompt=True) @click.pass_context def from_prime_num(ctx, prime_num): d = Designer() found = False for result in d.from_prime_num(prime_num): found = True info_txt = ( f'({result.ncols}x{result.nrows}), aspect={result.aspect_ratio:.3f},' f'prime_num={result.prime_num}' ) click.echo(info_txt) if click.confirm('Use this design?'): _build_result_and_output(ctx, result) break if not found: click.echo('No results found') if __name__ == '__main__': design()