diff --git a/README.md b/README.md index 4775e16..0f1ad3e 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,20 @@ -# fgscenery-tools -Tools for creating, managing and editing FlightGear scenery +# fgtools +Tools for creating, managing and editing FlightGear scenery, aircraft, … + +## Installation +To run these scripts you need Python 3, Python 2 won't work. Recommended version is 3.8 as I only have that installed so couldn't test with any other versions - earlier 3.x versions should work, but no guarantee ! + +### Linux +Download / `git clone` this repo and put it in a place of your choice, say `/home/user/fgtools`. With `git clone`, you would use this command: +```sh +/home/user$ git clone https://github.com/TheFGFSEagle/fgtools +``` +Before you run the scripts you have to make sure that the folder containing this repository on your local disk (here `/home/user`) is inside your `PYTHONPATH` environment variable, or you must run the scripts from inside the `fgtools` folder. To add the folder to your `PYTHONPATH`, use this command: +```sh +export PYTHONPATH="${PYTHONPATH}:/home/user" +``` +Note: this is lost when you close the terminal / console, so you have to run this command every time you open a new console and run the scripts from it. To make the change persistent, add the command to the end of the `.profile` file in your home folder. + +### Windows +_I don't have Windows so cannot provide any instructions - contributions by Windows users welcome !_ + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/aircraft/javaprop2jsbcpct.py b/aircraft/javaprop2jsbcpct.py index 65371f5..3cef063 100755 --- a/aircraft/javaprop2jsbcpct.py +++ b/aircraft/javaprop2jsbcpct.py @@ -3,24 +3,51 @@ import argparse import os +from fgtools.utils.interpolator import Interpolator + def parse_data_files(input_files, blade_angles): blade_angles = list(map(float, blade_angles)) data = {} for file, angle in zip(input_files, blade_angles): + data[angle] = { + "Cp": Interpolator(), + "Ct": Interpolator() + } with open(file) as f: content = f.readlines() content = list(map(lambda s: s.strip().split("\t"), content))[2:] - data[angle] = {} for line in content: - data[angle][float(line[0])] = {"Ct": float(line[2]), "Cp": float(line[3])} + av, cp, ct = float(line[0]), float(line[2]), float(line[3]) + data[angle]["Cp"].add_value(av, cp) + data[angle]["Ct"].add_value(av, ct) return data +def make_tables(data, maximum, indentation="\t", resolution=0.05): + Cp = Ct = indentation * 4 + (indentation * 2).join(map(str, data)) + "\n" + av = 0 + while av <= maximum: + av = round(av, 6) + Cp += indentation * 2 + indentation + str(av) + indentation + Ct += indentation * 2 + indentation + str(av) + indentation + cps = [] + cts = [] + for angle in data: + cps.append("%.6f" % round(data[angle]["Cp"].interpolate(av, sort=False), 6)) + cts.append("%.6f" % round(data[angle]["Ct"].interpolate(av, sort=False), 6)) + + Cp += indentation.join(cps) + "\n" + Ct += indentation.join(cts) + "\n" + av += resolution + return {"Cp":Cp, "Ct": Ct} + + + if __name__ == "__main__": - argp = argparse.ArgumentParser(description="javaprop2jsbcpct.py - converts JavaProp propeller data into Cp and Ct tables for a JSBsim propelller") + argp = argparse.ArgumentParser(description="javaprop2jsbcpct.py - converts JavaProp propeller data into Cp and Ct tables for a JSBsim propeller") argp.add_argument( "-i", "--input-file", @@ -38,6 +65,13 @@ if __name__ == "__main__": dest="blade_angles" ) + argp.add_argument( + "-m", "--max", + help="Maximum advance ratio to output data for", + required=True, + type=float + ) + argp.add_argument( "--interactive", action="store_true", @@ -50,6 +84,13 @@ if __name__ == "__main__": default="\t" ) + argp.add_argument( + "-r", "--resolution", + help="Advance ratio resolution to generate (default: 0.05)", + type=float, + default=0.05 + ) + args = argp.parse_args() for path in args.input_files: @@ -60,8 +101,22 @@ if __name__ == "__main__": if len(args.blade_angles) < len(args.input_files): print("Error: less blade angles than input files") elif len(args.blade_angles) > len(args.input_files): - args.blade_angles, rest = args.blade_angles[:len(args.input_files)] + args.blade_angles, rest = args.blade_angles[:len(args.input_files) + 1] print(f"Warning: skipping {len(rest)} blade angles because no corresponding data file was specified") data = parse_data_files(args.input_files, args.blade_angles) + output = make_tables(data, args.max, args.indentation, args.resolution) + + + print(args.indentation + "") + print(args.indentation * 2 + "") + print(output["Ct"]) + print(args.indentation * 2 + "") + print(args.indentation + "
") + print(args.indentation) + print(args.indentation + "") + print(args.indentation * 2 + "") + print(output["Cp"]) + print(args.indentation * 2 + "") + print(args.indentation + "
") diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/interpolator.py b/utils/interpolator.py new file mode 100755 index 0000000..e9304cb --- /dev/null +++ b/utils/interpolator.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python +#-*- coding:utf-8 -*- + +class Interpolator: + def __init__(self): + self._indexes = [] + self._values = [] + self._sorted = False + + self.methods = {"linear": self._interpolate_linear} + + def add_value(self, index, value): + if type(index) not in (int, float) or type(value) not in (int, float): + try: + index = float(index) + value = float(value) + except ValueError: + raise TypeError(f"Interpolator.add_value: index '{index}' or value '{value}' not a number") + + self._indexes.append(index) + self._values.append(value) + self._sorted = False + + def add_values(self, indexes, values): + for i, v in zip(indexes, values): + self.add_value(i, v) + + def interpolate(self, index, extrapolate=True, method="linear", sort=True): + if not method in self.methods: + raise NotImplementedError(f"Interpolator.interpolate: interpolation method '{method}' not yet supported") + + if len(self._indexes) < 2: + raise ValueError(f"Interpolator.interpolate: cannot interpolate on a table with less than two data points") + + # only sort if not already sorted to increase performance for large tables + if not self._sorted and sort: + self._indexes.sort() + self._values.sort() + self._sorted = True + + return self.methods[method](index, extrapolate) + + def _find_neighbours(self, index): + lower = upper = 0 + last = self._indexes[0] + for it, _index in enumerate(self._indexes): + lower = last + last = it + if _index > index: + upper = it + break + + return lower, upper + + def _interpolate_linear(self, index, extrapolate=True): + if index in self._indexes: + return self._values[self._indexes.index(index)] + + if self._indexes[0] < index < self._indexes[-1]: + lower, upper = self._find_neighbours(index) + return self._values[lower] + (self._values[upper] - self._values[lower]) * (index - self._indexes[lower]) / (self._indexes[upper] - self._indexes[lower]) + else: + if not extrapolate: + if index < self._indexes[0]: + return self._values[0] + else: + return self._values[-1] + else: + if index < self._indexes[0]: + return self._values[1] + (index - self._indexes[1]) / (self._indexes[0] - self._indexes[1]) * (self._values[0] - self._values[1]) + else: + return self._values[-2] + (index - self._indexes[-2]) / (self._indexes[-1] - self._indexes[-2]) * (self._values[-1] - self._values[-2]) + +# run test if run directly +if __name__ == "__main__": + print("Test results") + i = Interpolator() + i.add_values((0, 10, 20), (0, 20, 30)) + for test_val in (-5, 0, 1, 2, 3.5, 5.55555, 9, 10, 15, 100): + print(test_val, i.interpolate(test_val)) + +