mersenne

Mersenne's law based string calculator
git clone git://git.vgx.fr/mersenne
Log | Files | Refs

mersenne.py (7710B)


      1 #!/usr/bin/env python3
      2 
      3 from math import sqrt, pi
      4 from argparse import ArgumentParser, FileType
      5 import json
      6 
      7 from pint import UnitRegistry
      8 
      9 # tools
     10 
     11 notes = {
     12     "c"  :  0,
     13     "c#" :  1, "db" :  1,
     14     "d"  :  2,
     15     "d#" :  3, "eb" :  3,
     16     "e"  :  4,
     17     "f"  :  5,
     18     "f#" :  6, "gb" :  6,
     19     "g"  :  7,
     20     "g#" :  8, "ab" :  8,
     21     "a"  :  9,
     22     "a#" : 10, "bb" : 10,
     23     "b"  : 11
     24 }
     25 
     26 def note_to_freq(note="a", octave=4, basefreq=440):
     27     return basefreq * 2 ** (octave-4 + (notes[note]-9)/12)
     28 
     29 def radius_to_linear_mass(radius, volumic_mass):
     30     return volumic_mass * pi * radius ** 2
     31 
     32 def linear_mass_to_radius(linear_mass, volumic_mass):
     33     return sqrt(linear_mass / (volumic_mass * pi))
     34 
     35 # Mersenne's law formulas
     36 # see https://en.wikipedia.org/wiki/Mersenne%27s_laws
     37 
     38 def get_freq(length, tension, linear_mass):
     39     return sqrt(tension / linear_mass) / (2 * length)
     40 
     41 def get_tension(linear_mass, length, freq):
     42     return (2 * freq * length) ** 2 * linear_mass
     43 
     44 def get_linear_mass(tension, length, freq):
     45     return tension / (2 * freq * length) ** 2
     46 
     47 def get_length(linear_mass, tension, freq):
     48     return sqrt(tension / linear_mass) / (2 * freq)
     49 
     50 # data processing
     51 
     52 param_names = {
     53     # Main params
     54     'freq',
     55     'tension',
     56     'linear_mass',
     57     'length',
     58     # Auxilary params
     59     'note',
     60     'octave',
     61     'basefreq',
     62     'diameter',
     63     'radius',
     64     'volumic_mass'
     65 }
     66 
     67 param_units = {
     68     # auxiliary params
     69     "basefreq": "Hz",
     70     "diameter": "m",
     71     "radius": "m",
     72     "volumic_mass": "kg/m³",
     73     # main params
     74     "freq": "Hz",
     75     "tension": "N",
     76     "linear_mass": "kg/m",
     77     "length": "m"
     78 }
     79 
     80 param_output_units = {
     81     # auxiliary params
     82     "basefreq": "Hz",
     83     "diameter": "mm",
     84     "radius": "mm",
     85     "volumic_mass": "kg/m³",
     86     # main params
     87     "freq": "Hz",
     88     "tension": "kgf",
     89     "linear_mass": "g/m",
     90     "length": "mm"
     91 }
     92 
     93 def to_SI(data, ureg):
     94     for param in param_units:
     95         if param in data:
     96             if type(data[param]) == int or type(data[param]) == float:
     97                 continue
     98 
     99             quantity = ureg(data[param])
    100 
    101             if type(quantity) == int or type(quantity) == float:
    102                 # No unit : we assume it's already in SI unit
    103                 data[param] = quantity
    104                 continue
    105 
    106             data[param] = quantity.to(param_units[param]).magnitude
    107 
    108 def print_data(data, ureg):
    109     print("Frequency: {:n~P}".format(ureg.Quantity(data["freq"], param_units["freq"]).to(param_output_units["freq"])), end='')
    110     if "note" in data:
    111         print(' (Note: {}'.format(data["note"]), end='')
    112         if "octave" in data:
    113             print(', octave: {}'.format(data["octave"]), end='')
    114         if "basefreq" in data:
    115             print(', base frequency: {:n~P}'.format(ureg.Quantity(data["basefreq"], param_units["basefreq"]).to(param_output_units["basefreq"])), end='')
    116         print(')', end='')
    117     print()
    118 
    119     print("Tension: {:n~P}".format(ureg.Quantity(data["tension"], param_units["tension"]).to(param_output_units["tension"])))
    120 
    121     print("Length: {:n~P}".format(ureg.Quantity(data["length"], param_units["length"]).to(param_output_units["length"])))
    122 
    123     print("Linear mass: {:n~P}".format(ureg.Quantity(data["linear_mass"], param_units["linear_mass"]).to(param_output_units["linear_mass"])), end='')
    124     if "radius" in data:
    125         print(' (radius: {:n~P}'.format(ureg.Quantity(data["radius"], param_units["radius"]).to(param_output_units["radius"])), end='')
    126         print(', diameter: {:n~P}'.format(ureg.Quantity(data["diameter"], param_units["diameter"]).to(param_output_units["diameter"])), end='')
    127         print(', volumic_mass: {:n~P}'.format(ureg.Quantity(data["volumic_mass"], param_units["volumic_mass"]).to(param_output_units["volumic_mass"])), end='')
    128         print(')', end='')
    129     print()
    130 
    131 
    132 def complete_data(data):
    133 
    134     # Handle note to frequency conversion
    135     if not "freq" in data and "note" in data:
    136         note_data = {"note" : data["note"]}
    137         if "octave" in data:
    138             note_data["octave"] = data["octave"]
    139         if "basefreq" in data:
    140             note_data["basefreq"] = data["basefreq"]
    141 
    142         data["freq"] = note_to_freq(**note_data)
    143 
    144     # Handle radius to linear mass conversion
    145 
    146     if "diameter" in data:
    147         data["radius"] = data["diameter"]/2
    148 
    149     if not "linear_mass" in data and "radius" in data and "volumic_mass" in data:
    150         data["linear_mass"] = radius_to_linear_mass(data["radius"], data["volumic_mass"])
    151 
    152     # Find missing parameters
    153 
    154     missing = []
    155     available = {}
    156 
    157     for param in ["freq", "tension", "linear_mass", "length"]:
    158         if not param in data:
    159             missing.append(param)
    160         else:
    161             available[param] = data[param]
    162 
    163     if len(missing) == 0:
    164         raise RuntimeWarning("Nothing to compute")
    165         return
    166 
    167     if len(missing) > 1:
    168         raise RuntimeError("Not enough data : please leave only one parameter missing among {}".format(missing))
    169 
    170     # Compute the missing parameter
    171 
    172     missing = missing[0]
    173 
    174     if missing == "freq":
    175         data["freq"] = get_freq(**available)
    176     elif missing == "tension":
    177         data["tension"] = get_tension(**available)
    178     elif missing == "linear_mass":
    179         data["linear_mass"] = get_linear_mass(**available)
    180     elif missing == "length":
    181         data["length"] = get_length(**available)
    182 
    183     # Compute radius if needed
    184 
    185     if not "radius"in data and "volumic_mass" in data:
    186         data["radius"] = linear_mass_to_radius(data["linear_mass"], data["volumic_mass"])
    187         data["diameter"] = data["radius"]*2
    188 
    189 if __name__ == "__main__":
    190     parser = ArgumentParser(description="Apply Mersenne's law formulas.")
    191 
    192     # Main params
    193     parser.add_argument('-f','--freq', nargs=1, help='Frequency (in Hz by default)')
    194     parser.add_argument('-t','--tension', nargs=1, help='Tension of the string (in N by default)')
    195     parser.add_argument('-m','--linear_mass', nargs=1, help='Linear mass of the string (in kg/m by default)')
    196     parser.add_argument('-l','--length', nargs=1, help='Length of the string (in m by default)')
    197 
    198     # Auxilary params
    199     parser.add_argument('-n','--note', nargs=1, help='Note of the string')
    200     parser.add_argument('-o','--octave', nargs=1, type=int, help='Octave of the note')
    201     parser.add_argument('-b','--basefreq', nargs=1, help='Base frequency for 4th octave A (in Hz by default)')
    202     parser.add_argument('-d','--diameter', nargs=1, help='Diameter of the string (in m by default)')
    203     parser.add_argument('-r','--radius', nargs=1, help='Radius of the string (in m by default)')
    204     parser.add_argument('-v','--volumic_mass', nargs=1, help='Volumic mass of the string material (in kg/m³ by default)')
    205 
    206     # Options
    207     parser.add_argument('-u', '--unit', nargs=1, action='append', metavar='param:unit', help='Select an unit to display a param in default output')
    208     parser.add_argument('-j', '--json_output', action='store_true', help='Format output data as JSON')
    209     parser.add_argument('-J', '--json_input', type=FileType('r'), help="Use JSON file as input (use '-' for stdin)")
    210 
    211     args = parser.parse_args()
    212 
    213     if args.json_input:
    214         data = json.load(args.json_input)
    215     else: # Get data parameters from program arguments
    216         dargs = vars(args)
    217         data = {k:dargs[k][0] for k in dargs if dargs[k] != None and k in param_names}
    218 
    219     ureg = UnitRegistry()
    220 
    221     to_SI(data, ureg)
    222 
    223     complete_data(data)
    224 
    225     if args.json_output:
    226         print(json.dumps(data, indent=4))
    227     else:
    228         if args.unit:
    229             for param_unit in args.unit:
    230                 param, unit = param_unit[0].split(':')
    231                 param_output_units[param] = unit
    232 
    233         print_data(data, ureg)