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)