├── .github └── workflows │ └── test.yml ├── .gitignore ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG ├── LICENSE ├── README.md ├── UliEngineering ├── Economics │ ├── Interest.py │ └── __init__.py ├── Electronics │ ├── Capacitors.py │ ├── Crystal.py │ ├── Filter.py │ ├── Hysteresis.py │ ├── Inductors.py │ ├── LED.py │ ├── LogarithmicAmplifier.py │ ├── MOSFET.py │ ├── Microstrip.py │ ├── OpAmp.py │ ├── Power.py │ ├── PowerFactor.py │ ├── Reactance.py │ ├── Resistors.py │ ├── SwitchingRegulator.py │ ├── TemperatureCoefficient.py │ ├── Thermistor.py │ ├── Tolerance.py │ ├── VoltageDivider.py │ ├── ZenerDiode.py │ └── __init__.py ├── EngineerIO.py ├── Exceptions.py ├── Filesystem │ ├── Hash.py │ └── __init__.py ├── Length.py ├── Math │ ├── Coordinates.py │ ├── Decibel.py │ ├── Geometry │ │ ├── Circle.py │ │ ├── Cylinder.py │ │ ├── Polygon.py │ │ ├── Sphere.py │ │ └── __init__.py │ └── __init__.py ├── Mechanics │ ├── Threads.py │ └── __init__.py ├── Optoelectronics │ ├── MPPC.py │ └── __init__.py ├── Physics │ ├── Acceleration.py │ ├── Density.py │ ├── Frequency.py │ ├── HalfLife.py │ ├── JohnsonNyquistNoise.py │ ├── Light.py │ ├── MagneticResonance.py │ ├── NTC.py │ ├── NoiseDensity.py │ ├── Pressure.py │ ├── RF.py │ ├── RTD.py │ ├── Rotation.py │ ├── Temperature.py │ └── __init__.py ├── SignalProcessing │ ├── Chunks.py │ ├── Correlation.py │ ├── DateTime.py │ ├── FFT.py │ ├── Filter.py │ ├── Normalize.py │ ├── Resampling.py │ ├── Selection.py │ ├── Simulation.py │ ├── Utils.py │ ├── Weight.py │ ├── Window.py │ └── __init__.py ├── Units.py ├── Utils │ ├── Compression.py │ ├── Concurrency.py │ ├── Date.py │ ├── Files.py │ ├── Iterable.py │ ├── JSON.py │ ├── NumPy.py │ ├── Parser.py │ ├── Range.py │ ├── Slice.py │ ├── String.py │ ├── Temporary.py │ ├── ZIP.py │ └── __init__.py └── __init__.py ├── coverage.ini ├── coverage.rc ├── pyproject.toml ├── pytest.ini ├── requirements.txt ├── setup.cfg ├── tests ├── Economics │ ├── TestInterest.py │ └── __init__.py ├── Electronics │ ├── TestCrystal.py │ ├── TestFilter.py │ ├── TestHysteresis.py │ ├── TestLED.py │ ├── TestLogarithmicAmplifier.py │ ├── TestMOSFET.py │ ├── TestOpAmp.py │ ├── TestPower.py │ ├── TestPowerFactor.py │ ├── TestReactance.py │ ├── TestResistors.py │ ├── TestTemperatureCoefficient.py │ ├── TestThermistor.py │ ├── TestTolerance.py │ ├── TestVoltageDivider.py │ └── __init__.py ├── Filesystem │ ├── TestHash.py │ └── __init__.py ├── Math │ ├── Geometry │ │ ├── TestCircle.py │ │ ├── TestCylinder.py │ │ ├── TestPolygon.py │ │ ├── TestSphere.py │ │ └── __init__.py │ ├── TestCoordinates.py │ ├── TestDecibel.py │ └── __init__.py ├── Mechanics │ ├── TestThreads.py │ └── __init__.py ├── Physics │ ├── TestAcceleration.py │ ├── TestCapacitors.py │ ├── TestFrequency.py │ ├── TestHalfLife.py │ ├── TestJohnsonNyquistNoise.py │ ├── TestLight.py │ ├── TestMagneticResonance.py │ ├── TestNTC.py │ ├── TestNoiseDensity.py │ ├── TestPressure.py │ ├── TestRF.py │ ├── TestRTD.py │ ├── TestRotation.py │ ├── TestTemperature.py │ └── __init__.py ├── SignalProcessing │ ├── TestChunks.py │ ├── TestCorrelation.py │ ├── TestDateTime.py │ ├── TestFFT.py │ ├── TestFilter.py │ ├── TestNormalize.py │ ├── TestResampling.py │ ├── TestSelection.py │ ├── TestSimulation.py │ ├── TestUtils.py │ ├── TestWeight.py │ ├── TestWindow.py │ └── __init__.py ├── TestEngineerIO.py ├── TestLength.py ├── Utils │ ├── TestCompression.py │ ├── TestDate.py │ ├── TestFiles.py │ ├── TestIterable.py │ ├── TestJSON.py │ ├── TestNumPy.py │ ├── TestParser.py │ ├── TestRange.py │ ├── TestSlice.py │ ├── TestString.py │ ├── TestTemporary.py │ ├── TestZIP.py │ └── __init__.py └── __init__.py └── tox.ini /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | matrix: 12 | python-version: ['3.10', '3.11', '3.12', '3.13'] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v5 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | python -m pip install tox tox-gh-actions 24 | - name: Test with tox 25 | run: python -m tox 26 | - name: List files 27 | run: ls -v 28 | - name: Upload coverage to Codecov.io 29 | if: matrix.python-version == '3.12' 30 | uses: codecov/codecov-action@v3 31 | with: 32 | token: ${{ secrets.CODECOV_TOKEN }} 33 | directory: ./coverage/reports/ 34 | env_vars: OS,PYTHON 35 | fail_ci_if_error: true 36 | files: ./coverage.xml 37 | flags: unittests 38 | name: codecov-umbrella 39 | verbose: true 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | *.egg-info/ 23 | .installed.cfg 24 | *.egg 25 | 26 | # PyInstaller 27 | # Usually these files are written by a python script from a template 28 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 29 | *.manifest 30 | *.spec 31 | 32 | # Installer logs 33 | pip-log.txt 34 | pip-delete-this-directory.txt 35 | 36 | # Unit test / coverage reports 37 | htmlcov/ 38 | .tox/ 39 | .coverage 40 | .coverage.* 41 | .cache 42 | nosetests.xml 43 | coverage.xml 44 | .noseids 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | - "3.2" 3 | - "3.3" 4 | - "3.4" 5 | - "3.5" 6 | - "3.6" 7 | before_install: 8 | - sudo apt-get -y install python3-numpy python3-scipy python3-pip liblapack-dev libblas-dev gfortran python3-setuptools 9 | - sudo pip3 install coverage codecov toolz parameterized codeclimate-test-reporter 10 | - sudo pip3 install setuptools numpy --upgrade 11 | cache: pip 12 | sudo: false 13 | script: python3 setup.py test 14 | dist: xenial 15 | after_success: 16 | - codeclimate-test-reporter --token 71eb68ef3ba1f85d8e92407d03e43df51deb6f14a870ada23e8530a16d438eae 17 | - codecov --token=e3a3e622-b5b4-40a0-b0be-f427bbb23449 18 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestEnabled": false, 3 | "python.testing.unittestEnabled": true, 4 | "python.testing.nosetestsEnabled": false, 5 | "python.testing.unittestArgs": [ 6 | "-v", 7 | "-s", 8 | "./tests", 9 | "-p", 10 | "Test*.py" 11 | ], 12 | "files.exclude": { 13 | "**/*.pyc": true, 14 | "**/__pycache__": true, 15 | ".eggs": true 16 | }, 17 | "idf.pythonInstallPath": "/usr/bin/python3" 18 | } -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | ## 0.3 2 | Somewhat breaking changes to EngineerIO internal APIs: 3 | normalize_numeric() works as before, but other (lower-level) functions returned namedtuples 4 | instead of tuples, and additionally parse separate prefix and unit prefix values. 5 | This allows for fine-grained control and validation of values like "±5%" -------------------------------------------------------------------------------- /UliEngineering/Economics/Interest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import numpy as np 3 | 4 | __all__ = [ 5 | "yearly_interest_to_equivalent_monthly_interest", 6 | "yearly_interest_to_equivalent_daily_interest", 7 | "yearly_interest_to_equivalent_arbitrary_interest", 8 | "interest_apply_multiple_times", 9 | "extrapolate_interest_to_timestamps" 10 | ] 11 | 12 | def yearly_interest_to_equivalent_monthly_interest(interest): 13 | """ 14 | Given a yearly interest such as 0.022 (= 2.2%), 15 | or a numpy ndarray of interests, 16 | computes the equivalent monthly interest so that 17 | the following holds True: 18 | 19 | (1+monthly_interest)**12-1 == yearly_interest 20 | """ 21 | # 12 months per year 22 | # monthly interest is 12th root of yearly interest 23 | # https://techoverflow.net/2022/02/02/numpy-nth-root-how-to/ 24 | return np.power(1.+interest, (1/12.))-1. 25 | 26 | def yearly_interest_to_equivalent_daily_interest(interest, days_per_year=365.25): 27 | """ 28 | Given a yearly interest such as 0.022 (= 2.2%), 29 | or a numpy ndarray of interests, 30 | computes the equivalent dayly interest so that 31 | the following holds True: 32 | 33 | (1+daily_interest)**365.25-1 == yearly_interest 34 | 35 | It is highly recommended to use the Julian year (exactly 365.25 days/year), 36 | as using any other value like 365 will mean that over time spans containing leap 37 | years, the daily interest will not be equivalent to the yearly interest 38 | """ 39 | # https://techoverflow.net/2022/02/02/numpy-nth-root-how-to/ 40 | return np.power(1.+interest, (1/days_per_year))-1. 41 | 42 | def yearly_interest_to_equivalent_arbitrary_interest(interest, seconds=1): 43 | """ 44 | Given a yearly interest such as 0.022 (= 2.2%), 45 | or a numpy ndarray of interests, 46 | computes the equivalent interest rate for a timespan of 47 | [seconds] so that the total interest of applying 48 | 49 | This function is using the Julian year (exactly 365.25 days/year) as a reference 50 | for how many seconds a years, 51 | as using any other value like 365 will mean that over time spans containing leap 52 | years, the daily interest will not be equivalent to the yearly interest 53 | """ 54 | # https://techoverflow.net/2022/02/02/numpy-nth-root-how-to/ 55 | # scipy.constants.Julian_year == 31557600.0 56 | return np.power(1.+interest, (seconds/31557600.0))-1. 57 | 58 | def interest_apply_multiple_times(interest, times): 59 | """ 60 | Given a yearly interest such as 0.022 (= 2.2%), 61 | apply it to the given values. 62 | 63 | For example, 64 | interest_apply_multiple_times(0.022, 5) 65 | will return the equivalent interest of having 66 | an interest of 2.2% 5 years in a row (including compound interest) 67 | 68 | times may be a floating point number. 69 | 70 | Computes 71 | (1+interest)**times - 1 72 | """ 73 | return np.power(1.+interest, times)-1. 74 | 75 | def extrapolate_interest_to_timestamps(interest, timestamps): 76 | """ 77 | Given a yearly interest such as 0.022 (= 2.2%), 78 | extrapolate the total interest factor up to each time point X 79 | in the timestamps array. The first timestamp in the array is assumed 80 | to be 1.0. 81 | 82 | This works by computing the time difference for each timestamps t_i 83 | to the first timestamp t_0 and then applying the correct exponent 84 | to the interest to obtain a yearly equivalent interest factor. 85 | 86 | This function returns a numpy array of interest factors (typically >1), 87 | not interests(typically <1) for easy multiplication onto any values. 88 | """ 89 | # We compute in microseconds, not nanoseconds! 90 | timestamps = timestamps.astype('datetime64[us]') 91 | tdelta_us = (timestamps - timestamps[0]).astype(np.int64) 92 | # scipy.constants.Julian_year == 31557600.0, 93 | # multiplied by 1e6 due to microseconds timestamps 94 | return np.power(1.+interest, (tdelta_us/31557600e6)) -------------------------------------------------------------------------------- /UliEngineering/Economics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/UliEngineering/Economics/__init__.py -------------------------------------------------------------------------------- /UliEngineering/Electronics/Inductors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utility to calculate inductors 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | 9 | __all__ = ["ideal_inductor_current_change_rate"] 10 | 11 | @normalize_numeric_args 12 | def ideal_inductor_current_change_rate(inductance, voltage) -> Unit("A/s"): 13 | """ 14 | Compute the rise or fall rate of current in an ideal inductor, 15 | if there's [voltage] across it. 16 | 17 | Parameters 18 | ---------- 19 | inductance: number or Engineer string 20 | The inductance in Henrys 21 | vsupply: number or Engineer string 22 | The voltage across the inductor 23 | """ 24 | return voltage / inductance -------------------------------------------------------------------------------- /UliEngineering/Electronics/LED.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for LED calculations 5 | 6 | Usage example: 7 | >>> from UliEngineering.Electronics.OpAmp import summing_amplifier_noninv 8 | >>> # Example: sum 2.5V and 0.5V with a total sum-referred gain of 1.0 9 | >>> formatValue(summing_amplifier_noninv( 10 | "2.5V", "500mV", "1kΩ", "1kΩ", "1kΩ", "1kΩ"), "V")) 11 | 12 | """ 13 | from UliEngineering.EngineerIO import normalize_numeric, normalize_numeric_args 14 | from UliEngineering.Exceptions import OperationImpossibleException 15 | from UliEngineering.Units import Unit 16 | from UliEngineering.Electronics.Resistors import resistor_current_by_power 17 | 18 | __all__ = [ 19 | "LEDForwardVoltages", 20 | "led_series_resistor", 21 | "led_series_resistor_power", 22 | "led_series_resistor_maximum_current", 23 | "led_series_resistor_current", 24 | ] 25 | 26 | 27 | class LEDForwardVoltages(): 28 | """ 29 | Common LED forward voltage values. 30 | Source: http://www.elektronik-kompendium.de/sites/bau/1109111.htm 31 | NOTE: These do NOT neccessarily represent the actual forward voltages 32 | of any LED you choose but rather the typical forward voltage at nominal 33 | current. 34 | 35 | Note that diode testers test the forward voltage with rather low currents 36 | and the forward voltage might vary slightly at operating current. 37 | Take that into account when operating a LED near its maximum allowed current. 38 | """ 39 | Infrared = 1.5 40 | Red = 1.6 41 | Yellow = 2.2 42 | Green = 2.1 43 | Blue = 2.9 44 | White = 4.0 45 | 46 | @normalize_numeric_args 47 | def led_series_resistor(vsupply, ioperating, vforward) -> Unit("Ω"): 48 | """ 49 | Computes the required series resistor for operating a LED with 50 | forward voltage [vforward] at current [ioperating] on a 51 | supply voltage of [vsupply]. 52 | 53 | Tolerances are not taken into account. 54 | """ 55 | if vforward > vsupply: 56 | raise OperationImpossibleException( 57 | f"Can't operate LED with forward voltage {vforward} on {vsupply} supply" 58 | ) 59 | return (vsupply - vforward) / ioperating 60 | 61 | @normalize_numeric_args 62 | def led_series_resistor_power(vsupply, ioperating, vforward) -> Unit("W"): 63 | """ 64 | Computes the required series resistor power for operating a LED with 65 | forward voltage [vforward] at current [ioperating] on a 66 | supply voltage of [vsupply]. 67 | 68 | The resulting power value is the minimum rated value for the resistor 69 | for continous operation 70 | 71 | Tolerances are not taken into account. 72 | """ 73 | if vforward > vsupply: 74 | raise OperationImpossibleException( 75 | f"Can't operate LED with forward voltage {vforward} on {vsupply} supply" 76 | ) 77 | # Will raise OperationImpossibleException if vforward > vsupply 78 | resistor_value = led_series_resistor(vsupply, ioperating, vforward) 79 | return resistor_value * ioperating * ioperating 80 | 81 | @normalize_numeric_args 82 | def led_series_resistor_maximum_current(resistance, power_rating) -> Unit("A"): 83 | """ 84 | Compute the maximum current through a LED + series resistor combination, 85 | so that the power rating of the resistor is not exceeded 86 | (i.e. the current where the dissipated power is exactly the power rating). 87 | 88 | Tolerances are not taken into account. 89 | """ 90 | power_rating = normalize_numeric(power_rating) 91 | resistance = normalize_numeric(resistance) 92 | # Compute the current that would flow through the resistor 93 | current = resistor_current_by_power(resistance, power_rating) 94 | return current 95 | 96 | @normalize_numeric_args 97 | def led_series_resistor_current(vsupply, resistance, vforward) -> Unit("A"): 98 | """ 99 | Compute the current that flows through a LED + series resistor combination 100 | when connected to a supply voltage [vsupply] and a series resistor of [resistance]. 101 | 102 | Tolerances are not taken into account. 103 | """ 104 | if vforward > vsupply: 105 | raise OperationImpossibleException( 106 | f"Can't operate LED with forward voltage {vforward} on {vsupply} supply" 107 | ) 108 | return (vsupply - vforward) / resistance 109 | -------------------------------------------------------------------------------- /UliEngineering/Electronics/LogarithmicAmplifier.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from UliEngineering.EngineerIO import normalize_numeric_args 3 | import numpy as np 4 | 5 | __all__ = [ 6 | "logarithmic_amplifier_output_voltage", 7 | "logarithmic_amplifier_input_current" 8 | ] 9 | 10 | @normalize_numeric_args 11 | def logarithmic_amplifier_output_voltage(ipd, gain, intercept): 12 | """ 13 | Compute the logarithmic output voltage of a ADL5303 (probably its more general than that) 14 | 15 | According to Formula (1) in the [ADL5303 datasheet](https://www.analog.com/media/en/technical-documentation/data-sheets/adl5303.pdf) 16 | 17 | Parameters 18 | ---------- 19 | ipd : float 20 | The input current (in Amperes) 21 | gain : float 22 | The gain (volts per decade) 23 | intercept : float 24 | The intercept point (in Amperes) 25 | 26 | Returns: The output voltage in Volts 27 | """ 28 | return gain * np.log10(ipd / intercept) 29 | 30 | @normalize_numeric_args 31 | def logarithmic_amplifier_input_current(vout, gain, intercept): 32 | """ 33 | Compute the input current based on the output voltage of a logarithmic amplifier 34 | 35 | The formula for this is Ipd = intercept * 10^(vout / Gain) 36 | https://techoverflow.net/2024/09/23/how-to-compute-the-input-current-of-a-logarithmic-amplifier/ 37 | 38 | Parameters 39 | ---------- 40 | vout : float 41 | The output voltage of the logarithmic amplifier 42 | gain : float 43 | The gain (volts per decade) 44 | intercept : float 45 | The intercept point (in Amperes) 46 | """ 47 | return intercept * np.power(10, vout / gain) 48 | -------------------------------------------------------------------------------- /UliEngineering/Electronics/MOSFET.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utility to calculate MOSFETs 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric, normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | 9 | __all__ = [ 10 | "mosfet_gate_charge_losses", "mosfet_gate_charge_loss_per_cycle", 11 | "mosfet_gate_capacitance_from_gate_charge"] 12 | 13 | @normalize_numeric_args 14 | def mosfet_gate_charge_losses(total_gate_charge, vsupply, frequency="100 kHz") -> Unit("W"): 15 | """ 16 | Compute the gate charge loss of a MOSFET in a switch-mode 17 | power-supply application as a total power (integrated per second). 18 | 19 | Ref: 20 | http://rohmfs.rohm.com/en/products/databook/applinote/ic/power/switching_regulator/power_loss_appli-e.pdf 21 | 22 | Parameters 23 | ---------- 24 | total_gate_charge: number or Engineer string 25 | The total gate charge in Coulomb. 26 | For multiple MOSFETs such as in synchronous applications, 27 | add their gate charges together. 28 | vsupply: number or Engineer string 29 | The gate driver supply voltage in Volts 30 | frequency: number or Engineer string 31 | The switching frequency in Hz 32 | """ 33 | return mosfet_gate_charge_loss_per_cycle(total_gate_charge, vsupply) * frequency 34 | 35 | 36 | @normalize_numeric_args 37 | def mosfet_gate_charge_loss_per_cycle(total_gate_charge, vsupply) -> Unit("J"): 38 | """ 39 | Compute the gate charge loss of a MOSFET in a switch-mode 40 | power-supply application per switching cycle. 41 | 42 | Ref: 43 | http://rohmfs.rohm.com/en/products/databook/applinote/ic/power/switching_regulator/power_loss_appli-e.pdf 44 | 45 | Parameters 46 | ---------- 47 | total_gate_charge: number or Engineer string 48 | The total gate charge in Coulomb. 49 | For multiple MOSFETs such as in synchronous applications, 50 | add their gate charges together. 51 | vsupply: number or Engineer string 52 | The gate driver supply voltage in Volts 53 | """ 54 | return total_gate_charge * vsupply 55 | 56 | @normalize_numeric_args 57 | def mosfet_gate_capacitance_from_gate_charge(total_gate_charge, vsupply) -> Unit("F"): 58 | """ 59 | Compute the gate capacitance of a MOSFET in a switch-mode 60 | power-supply application. 61 | 62 | Parameters 63 | ---------- 64 | total_gate_charge: number or Engineer string 65 | The total gate charge in Coulomb. 66 | For multiple MOSFETs such as in synchronous applications, 67 | add their gate charges together. 68 | vsupply: number or Engineer string 69 | The gate driver supply voltage in Volts 70 | """ 71 | return total_gate_charge / vsupply -------------------------------------------------------------------------------- /UliEngineering/Electronics/OpAmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for operational amplifier calculations. 5 | 6 | Usage example: 7 | >>> from UliEngineering.Electronics.OpAmp import summing_amplifier_noninv 8 | >>> # Example: sum 2.5V and 0.5V with a total sum-referred gain of 1.0 9 | >>> formatValue(summing_amplifier_noninv( 10 | "2.5V", "500mV", "1kΩ", "1kΩ", "1kΩ", "1kΩ"), "V")) 11 | 12 | """ 13 | from UliEngineering.EngineerIO import normalize_numeric 14 | from UliEngineering.Units import Unit 15 | 16 | __all__ = [ 17 | "summing_amplifier_noninv", 18 | "noninverting_amplifier_gain" 19 | ] 20 | 21 | def summing_amplifier_noninv(v1, v2, r1, r2, rfb1, rfb2) -> Unit("V"): 22 | """ 23 | Computes the output voltage of a non-inverting summing amplifier: 24 | V1 connected via R1 to IN+ 25 | V2 connected via R2 to IN+ 26 | IN- connected via RFB1 to GND 27 | IN- connected via RFB2 to VOut 28 | """ 29 | v1 = normalize_numeric(v1) 30 | v2 = normalize_numeric(v2) 31 | r1 = normalize_numeric(r1) 32 | r2 = normalize_numeric(r2) 33 | rfb1 = normalize_numeric(rfb1) 34 | rfb2 = normalize_numeric(rfb2) 35 | return (1.0 + rfb2 / rfb1) * (v1 * (r2 / (r1 + r2)) + v2 * (r1 / (r1 + r2))) 36 | 37 | def noninverting_amplifier_gain(r1, r2) -> Unit("V/V"): 38 | """ 39 | Computes the gain of a non-inverting amplifier with feedback resistors R1 and R2. 40 | 41 | # 2D ASCII graphic with rectangular opamp 42 | 43 | R1 is the resistor connected between the OpAmp output and the OpAmp IN(-). 44 | R2 is the resistor connected between the OpAmp IN(-) and GND. 45 | 46 | R2 can also be infinity (np.inf), in which case the gain is 1.0. 47 | """ 48 | r1 = normalize_numeric(r1) 49 | r2 = normalize_numeric(r2) 50 | return 1.0 + r1 / r2 51 | -------------------------------------------------------------------------------- /UliEngineering/Electronics/Power.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Utilities to compute the power of a device 4 | """ 5 | from inspect import signature 6 | from UliEngineering.EngineerIO import normalize_numeric, normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | 9 | __all__ = ["current_by_power", "power_by_current_and_voltage"] 10 | 11 | @normalize_numeric_args 12 | def current_by_power(power="25 W", voltage="230 V") -> Unit("A"): 13 | """ 14 | Given a device's power (or RMS power) and the voltage (or RMS voltage) 15 | it runs on, compute how much current it will draw. 16 | """ 17 | return power / voltage 18 | 19 | @normalize_numeric_args 20 | def power_by_current_and_voltage(current="1.0 A", voltage="230 V") -> Unit("W"): 21 | """ 22 | Given a device's current (or RMS current) and the voltage (or RMS current) 23 | it runs on, compute its power 24 | """ 25 | return current * voltage 26 | -------------------------------------------------------------------------------- /UliEngineering/Electronics/PowerFactor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Utilities to compute the power factor 4 | """ 5 | from UliEngineering.EngineerIO import normalize_numeric_args 6 | from UliEngineering.Units import Unit 7 | import numpy as np 8 | 9 | __all__ = ["power_factor_by_phase_angle"] 10 | 11 | @normalize_numeric_args 12 | def power_factor_by_phase_angle(angle="10°", unit="degrees") -> Unit(""): 13 | """ 14 | Compute the power factor given the phase angle between current and voltage 15 | This approach only returns the correct power factor if current and voltage 16 | are both sinusoidal. 17 | 18 | Keyword arguments: 19 | ------------------ 20 | angle : number or Engineer strings 21 | The phase angle between current and voltage. 22 | unit : "degrees", "deg" or "radians", "rad", "radiant" 23 | The unit to interpret angle as 24 | """ 25 | if unit in ["degrees", "deg"]: 26 | angle = np.deg2rad(angle) 27 | elif unit in ["radians", "rad", "radiant"]: 28 | pass # No need to convert 29 | else: 30 | raise ValueError(f"Angle unit '{unit}' is unknown, use 'degrees' or 'radians'!") 31 | return np.cos(angle) -------------------------------------------------------------------------------- /UliEngineering/Electronics/Reactance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utility to calculate idealized reactances. 5 | 6 | Originally published at techoverflow.net 7 | """ 8 | import numpy as np 9 | from UliEngineering.EngineerIO import normalize_numeric_args 10 | from UliEngineering.Units import Unit 11 | 12 | __all__ = ["capacitive_reactance", "inductive_reactance"] 13 | 14 | @normalize_numeric_args 15 | def capacitive_reactance(c, f=1000.0) -> Unit("Ω"): 16 | """ 17 | Compute the capacitive reactance for a given capacitance and frequency. 18 | """ 19 | return 1.0 / (2 * np.pi * f * c) 20 | 21 | 22 | @normalize_numeric_args 23 | def inductive_reactance(l, f=1000.0) -> Unit("Ω"): 24 | """ 25 | Compute the inductive reactance for a given inductance and frequency. 26 | """ 27 | return 2 * np.pi * f * l 28 | -------------------------------------------------------------------------------- /UliEngineering/Electronics/Thermistor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Thermistor computations 5 | 6 | For reference see e.g. 7 | https://www.electronics-tutorials.ws/io/thermistors.html 8 | """ 9 | from UliEngineering.EngineerIO import normalize_numeric, normalize_numeric_args 10 | from UliEngineering.Units import Unit 11 | from UliEngineering.Physics.Temperature import normalize_temperature_kelvin 12 | import numpy as np 13 | from UliEngineering.Physics.Temperature import kelvin_to_celsius 14 | 15 | 16 | __all__ = [ 17 | "thermistor_b_value", 18 | "thermistor_temperature", 19 | "thermistor_resistance", 20 | ] 21 | 22 | @normalize_numeric_args 23 | def thermistor_b_value(r1, r2, t1=25.0, t2=100.0): 24 | """ 25 | Compute the B value of a thermistor given its resistance at two temperatures 26 | 27 | The formula is B = (T1*T2) / (T2-T1) * ln(R1/R2) 28 | with T1 and T2 being the temperatures in Kelvin and R1 and R2 being the resistances 29 | 30 | t1/t2 can be given either as strings e.g. "0°F", "100°C", "300K" or as numbers 31 | r1/r2 can be given either as strings e.g. "1kΩ", "1MΩ" or as numbers 32 | 33 | Returns the B value (unitless) 34 | """ 35 | # Normalize to Kelvin (temperature needs special handling) 36 | t1 = normalize_temperature_kelvin(t1) 37 | t2 = normalize_temperature_kelvin(t2) 38 | print(t1, t2, r1, r2) 39 | 40 | return (t1*t2) / (t2-t1) * np.log(r1/r2) 41 | 42 | @normalize_numeric_args 43 | def thermistor_temperature(resistance, beta=3950.0, R0=100e3, T0=25.0) -> Unit("°C"): 44 | """ 45 | Calculate the temperature of a NTC thermistor using the Beta parameter model. 46 | 47 | Parameters: 48 | - resistance: The measured resistance of the thermistor in Ohms, for which to calculate the temperature. 49 | - beta: The Beta constant of the thermistor. 50 | - c: An additional constant, currently unused. 51 | - R0: The resistance of the thermistor at reference temperature T0 (default is 10kOhms). 52 | - T0: The reference temperature in Celsius (default is 25°C). 53 | 54 | Returns: 55 | - Temperature in degrees. 56 | """ 57 | R0 = normalize_numeric(R0) 58 | T0 = normalize_temperature_kelvin(T0) 59 | temperature_kelvin = 1 / (1/T0 + (1/beta) * np.log(resistance/R0)) 60 | return kelvin_to_celsius(temperature_kelvin) 61 | 62 | @normalize_numeric_args 63 | def thermistor_resistance(temperature, beta=3950.0, R0=100e3, T0=25.0) -> Unit("Ω"): 64 | """ 65 | Calculate the resistance of a thermistor given its temperature. 66 | 67 | Parameters: 68 | temperature (float): The temperature in Kelvin 69 | A, B, C (float): The Steinhart-Hart coefficients for the thermistor 70 | 71 | Returns: 72 | float: The resistance of the thermistor 73 | """ 74 | temperature_kelvin = normalize_temperature_kelvin(temperature) 75 | t0_kelvin = normalize_temperature_kelvin(T0) 76 | # Calculate the resistance using the inverse Steinhart-Hart equation 77 | # Wolfram Alpha: solve K = 1 / (1/T + (1/b) * log(R/R0)) for R 78 | resistance = R0 * np.exp(beta * (1/temperature_kelvin - 1/t0_kelvin)) 79 | return resistance -------------------------------------------------------------------------------- /UliEngineering/Electronics/Tolerance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for computing tolerances 5 | """ 6 | from UliEngineering.EngineerIO import normalize 7 | from UliEngineering.Utils.Range import normalize_minmax_tuple, ValueRange 8 | 9 | __all__ = ["value_range_over_tolerance"] 10 | 11 | def value_range_over_tolerance(nominal, tolerance="1 %"): 12 | """ 13 | Compute the minimum and maximum value of a given component, 14 | given its nominal value and its tolerance. 15 | """ 16 | normalized = normalize(nominal) 17 | nominal, unit = normalized.value, normalized.unit 18 | # Parse static tolerance 19 | min_tol_coeff, max_tol_coeff, nix = normalize_minmax_tuple(tolerance, name="tolerance") 20 | tol_neg_factor = 1. + min_tol_coeff 21 | tol_pos_factor = 1. + max_tol_coeff 22 | return ValueRange(tol_neg_factor * nominal, tol_pos_factor * nominal, unit) 23 | -------------------------------------------------------------------------------- /UliEngineering/Electronics/ZenerDiode.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for computing different aspects and complexities of voltage dividers 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from .Resistors import * 8 | from UliEngineering.Units import Unit 9 | 10 | __all__ = ["zener_diode_power_dissipation"] 11 | 12 | @normalize_numeric_args 13 | def zener_diode_power_dissipation(zener_voltage, current) -> Unit("W"): 14 | """ 15 | Compute the power dissipated in a zener diode 16 | given the zener voltage and the current through it. 17 | 18 | This is based on an ideal zener diode model and does not take into account 19 | the zener resistance or the zener knee voltage. 20 | """ 21 | return zener_voltage * current -------------------------------------------------------------------------------- /UliEngineering/Electronics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/UliEngineering/Electronics/__init__.py -------------------------------------------------------------------------------- /UliEngineering/Exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Specialized exceptions for UliEngineering 4 | """ 5 | 6 | __all__ = ["ConversionException", "InvalidUnitException", 7 | "OperationImpossibleException"] 8 | 9 | class ConversionException(Exception): 10 | pass 11 | 12 | class InvalidUnitException(ConversionException): 13 | pass 14 | 15 | class OperationImpossibleException(Exception): 16 | """ 17 | Raised if operation with the given parameters is impossible, 18 | i.e. they have the correct forward but the given application 19 | can't work with this specific set of values. 20 | """ 21 | pass 22 | -------------------------------------------------------------------------------- /UliEngineering/Filesystem/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 -------------------------------------------------------------------------------- /UliEngineering/Length.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for length 5 | """ 6 | import scipy.constants 7 | from .EngineerIO import EngineerIO 8 | from .Units import UnknownUnitInContextException, Unit 9 | 10 | __all__ = ["normalize_length", "convert_length_to_meters"] 11 | 12 | _length_factors = { 13 | "": 1., # Assumed. It's SI! 14 | "m": 1., 15 | "meter": 1., 16 | "meters": 1., 17 | 'mil': 1e-3 * scipy.constants.inch, 18 | 'in': scipy.constants.inch, 19 | 'inch': scipy.constants.inch, 20 | 'inches': scipy.constants.inch, 21 | '\"': scipy.constants.inch, 22 | 'foot': scipy.constants.foot, 23 | 'feet': scipy.constants.foot, 24 | 'ft': scipy.constants.foot, 25 | 'yd': scipy.constants.yard, 26 | 'yard': scipy.constants.yard, 27 | 'mile': scipy.constants.mile, 28 | 'miles': scipy.constants.mile, 29 | 'nautical mile': scipy.constants.nautical_mile, 30 | 'nautical miles': scipy.constants.nautical_mile, 31 | 'pt': scipy.constants.point, 32 | 'point': scipy.constants.point, 33 | 'points': scipy.constants.point, 34 | 'AU': scipy.constants.astronomical_unit, 35 | 'au': scipy.constants.astronomical_unit, 36 | 'AUs': scipy.constants.astronomical_unit, 37 | 'ly': scipy.constants.light_year, 38 | 'lightyear': scipy.constants.light_year, 39 | 'lightyears': scipy.constants.light_year, 40 | 'light year': scipy.constants.light_year, 41 | 'light years': scipy.constants.light_year, 42 | 'pc': scipy.constants.parsec, 43 | 'parsec': scipy.constants.parsec, 44 | 'parsecs': scipy.constants.parsec, 45 | 'Å': scipy.constants.angstrom, 46 | 'Angstrom': scipy.constants.angstrom, 47 | 'angstrom': scipy.constants.angstrom, 48 | } 49 | 50 | def convert_length_to_meters(value, unit, instance=EngineerIO.length_instance) -> Unit("m"): 51 | """ 52 | Given a number or Engineer string (unit ignored) 53 | in , convert it to meters. 54 | """ 55 | # Currently a hack, but doing it directly will not parse SI units 56 | return normalize_length("{} {}".format(value, unit), instance=instance) 57 | 58 | def normalize_length(s, instance=EngineerIO.length_instance) -> Unit("m"): 59 | """ 60 | Normalize a length to meters. 61 | Returns the numeric value in m or None. 62 | 63 | NOTE: 1 nm is one nanometer, not one nautical mile! Use "1 nautical mile" instead! 64 | 65 | Valid inputs include: 66 | - "1.0" => 1.0 67 | - "1.0 mm" => 0.001 68 | - "1 inch" => 0.0254 69 | - "1 mil" => 0.000254 70 | - "1.2 M light years" => 1.135287656709696e+22 71 | - "9.15 kpc" => 2.8233949868947424e+17 72 | """ 73 | result = instance.normalize(s) 74 | if result.unit in _length_factors: 75 | return result.value * _length_factors[result.unit] 76 | else: 77 | raise UnknownUnitInContextException("Unknown length unit: {}".format(result.unit)) 78 | -------------------------------------------------------------------------------- /UliEngineering/Math/Coordinates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Coordinate functions, mainly for 2D coordinates 5 | """ 6 | import numpy as np 7 | 8 | __all__ = ["BoundingBox"] 9 | 10 | class BoundingBox(object): 11 | """ 12 | A 2D bounding box 13 | """ 14 | def __init__(self, points): 15 | """ 16 | Compute the upright 2D bounding box for a set of 17 | 2D coordinates in a (n,2) numpy array. 18 | 19 | You can access the bbox using the 20 | (minx, maxx, miny, maxy) members. 21 | """ 22 | if len(points.shape) != 2 or points.shape[1] != 2: 23 | raise ValueError("Points must be a (n,2), array but it has shape {}".format( 24 | points.shape)) 25 | if points.shape[0] < 1: 26 | raise ValueError("Can't compute bounding box for empty coordinates") 27 | self.minx, self.miny = np.min(points, axis=0) 28 | self.maxx, self.maxy = np.max(points, axis=0) 29 | 30 | @property 31 | def width(self): 32 | """X-axis extent of the bounding box""" 33 | return self.maxx - self.minx 34 | 35 | @property 36 | def height(self): 37 | """Y-axis extent of the bounding box""" 38 | return self.maxy - self.miny 39 | 40 | @property 41 | def area(self): 42 | """width * height""" 43 | return self.width * self.height 44 | 45 | @property 46 | def aspect_ratio(self): 47 | """width / height""" 48 | return self.width / self.height 49 | 50 | @property 51 | def center(self): 52 | """(x,y) center point of the bounding box""" 53 | return (self.minx + self.width / 2, self.miny + self.height / 2) 54 | 55 | @property 56 | def max_dim(self): 57 | """The larger dimension: max(width, height)""" 58 | return max(self.width, self.height) 59 | 60 | @property 61 | def min_dim(self): 62 | """The larger dimension: max(width, height)""" 63 | return min(self.width, self.height) 64 | 65 | def __repr__(self): 66 | return "BoundingBox({}, {}, {}, {})".format( 67 | self.minx, self.maxx, self.miny, self.maxy) 68 | -------------------------------------------------------------------------------- /UliEngineering/Math/Decibel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Utilities for FFT computation and visualization 4 | """ 5 | import numpy as np 6 | from UliEngineering.Units import Unit 7 | from UliEngineering.EngineerIO import normalize_numeric_args 8 | 9 | __all__ = [ 10 | "dBFactor", 11 | "ratio_to_dB", 12 | "dB_to_ratio", 13 | "value_to_dB", 14 | "dB_to_value", 15 | "voltage_to_dBuV", 16 | "dBuV_to_voltage", 17 | "power_to_dBm" 18 | ] 19 | 20 | def _safe_log10(v): 21 | """ 22 | Log10 with negative input => -np.inf 23 | """ 24 | if isinstance(v, np.ndarray): 25 | v[v < 0] = 0 26 | else: 27 | if v < 0: 28 | return -np.inf 29 | return np.log10(v) 30 | 31 | class dBFactor: 32 | """Pre-set values for factors""" 33 | Power = 10. 34 | Field = 20. 35 | 36 | @normalize_numeric_args 37 | def ratio_to_dB(ratio, factor=dBFactor.Field) -> Unit("dB"): 38 | """ 39 | Convert a given ratio to a decibel value. 40 | For power quantities, set factor=dBFactor.Power 41 | For field quantities, set factor=dBFactor.Field 42 | 43 | dB = [factor] * log10(ratio) 44 | 45 | Returns -np.inf for negative values 46 | """ 47 | return factor * _safe_log10(ratio) 48 | 49 | @normalize_numeric_args 50 | def dB_to_ratio(dB, factor=dBFactor.Field): 51 | """ 52 | Convert a given ratio from a decibel value to the underlying quantity. 53 | The result is returned as a ratio to the 0 dB value. 54 | 55 | For power quantities, set factor=dBFactor.Power 56 | For field quantities, set factor=dBFactor.Field 57 | """ 58 | return 10**(dB/factor) 59 | 60 | @normalize_numeric_args 61 | def value_to_dB(v, v0=1.0, factor=dBFactor.Field) -> Unit("dB"): 62 | """ 63 | Convert a given quantity [v] to dB, with 0dB being [v0]. 64 | 65 | Returns -np.inf for negative values 66 | """ 67 | return ratio_to_dB(v / v0, factor=factor) 68 | 69 | @normalize_numeric_args 70 | def dB_to_value(dB, v0=1.0, factor=dBFactor.Field): 71 | """ 72 | Convert a given decibel value [dB] to dB, with 0 dB being [v0]. 73 | 74 | Returns -np.inf for negative values 75 | """ 76 | return dB_to_ratio(dB, factor=factor) * v0 77 | 78 | # Utility functions 79 | @normalize_numeric_args 80 | def voltage_to_dBuV(v) -> Unit("dBµV"): 81 | """ 82 | Represent a voltage as dB microvolts. 83 | 84 | Also see the online calculator at 85 | https://techoverflow.net/2019/07/29/volts-to-db%c2%b5v-online-calculator-ampamp-python-code/ 86 | """ 87 | return value_to_dB(v, 1e-6, factor=dBFactor.Field) 88 | 89 | @normalize_numeric_args 90 | def dBuV_to_voltage(v) -> Unit("V"): 91 | """ 92 | Represent a dB microvolt voltage in volt. 93 | 94 | Also see the online calculator at 95 | https://techoverflow.net/2019/07/28/db%c2%b5v-to-volts-online-calculator-python-code/ 96 | """ 97 | return dB_to_ratio(v, factor=dBFactor.Field) * 1e-6 98 | 99 | @normalize_numeric_args 100 | def power_to_dBm(v) -> Unit("dBm"): 101 | """ 102 | Represent a power in Watts as dB milliwatts. 103 | """ 104 | return value_to_dB(v, 1e-3, factor=dBFactor.Power) -------------------------------------------------------------------------------- /UliEngineering/Math/Geometry/Circle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Circle geometry functions 5 | """ 6 | import math 7 | from UliEngineering.EngineerIO import normalize_numeric_args, Unit 8 | 9 | __all__ = [ 10 | "circle_area", "circle_circumference" 11 | ] 12 | 13 | @normalize_numeric_args 14 | def circle_area(radius) -> Unit("m²"): 15 | """ 16 | Compute the enclosed area of a circle from its radius 17 | """ 18 | return math.pi * radius**2 19 | 20 | @normalize_numeric_args 21 | def circle_circumference(radius) -> Unit("m"): 22 | """ 23 | Compute the circumference of a circle from its radius 24 | """ 25 | return 2. * math.pi * radius 26 | -------------------------------------------------------------------------------- /UliEngineering/Math/Geometry/Cylinder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Geometry functions for cylinders and hollow cylinders 5 | """ 6 | import math 7 | from UliEngineering.EngineerIO import normalize_numeric_args, Unit 8 | from .Circle import circle_area 9 | import numpy as np 10 | 11 | __all__ = [ 12 | "cylinder_volume", "cylinder_side_surface_area", "cylinder_surface_area", 13 | "hollow_cylinder_volume", "hollow_cylinder_inner_radius_by_volume", 14 | "cylinder_weight_by_diameter", "cylinder_weight_by_radius", 15 | "cylinder_weight_by_cross_sectional_area" 16 | ] 17 | 18 | @normalize_numeric_args 19 | def cylinder_volume(radius, height) -> Unit("m³"): 20 | """ 21 | Compute the volume of a cylinder by its radius and height 22 | """ 23 | return math.pi * (radius**2) * height 24 | 25 | @normalize_numeric_args 26 | def cylinder_side_surface_area(radius, height) -> Unit("m²"): 27 | """ 28 | Compute the surface area of the side (also called ") 29 | """ 30 | return 2 * math.pi * radius * height 31 | 32 | @normalize_numeric_args 33 | def cylinder_surface_area(radius, height) -> Unit("m²"): 34 | """ 35 | Compute the surface area (side + top + bottom) 36 | """ 37 | return cylinder_side_surface_area(radius, height) + 2 * circle_area(radius) 38 | 39 | @normalize_numeric_args 40 | def hollow_cylinder_volume(outer_radius, inner_radius, height) -> Unit("m³"): 41 | """ 42 | Compute the volume of a hollow cylinder by its height and the inner and outer radii 43 | """ 44 | return cylinder_volume(outer_radius, height) - cylinder_volume(inner_radius, height) 45 | 46 | @normalize_numeric_args 47 | def cylinder_weight_by_diameter(diameter, length, density=8000): 48 | """ 49 | Compute the weight of a cylinder by its diameter, length and density. 50 | The density is in kg/m³, the diameter and length must be given in mm. 51 | 52 | The default density is an approximation for steel. 53 | """ 54 | return cylinder_volume(diameter/2., length) * density 55 | 56 | @normalize_numeric_args 57 | def cylinder_weight_by_radius(radius, length, density=8000): 58 | """ 59 | Compute the weight of a cylinder by its radius, length and density. 60 | The density is in kg/m³, the radius and length must be given in mm. 61 | 62 | The default density is an approximation for steel. 63 | """ 64 | return cylinder_volume(radius, length) * density 65 | 66 | @normalize_numeric_args 67 | def cylinder_weight_by_cross_sectional_area(area, length, density=8000): 68 | """ 69 | Compute the weight of a cylinder by its cross-sectional area, length and density. 70 | The density is in kg/m³, the area and length must be given in mm² and mm. 71 | 72 | The default density is an approximation for steel. 73 | """ 74 | return area * length * density 75 | 76 | @normalize_numeric_args 77 | def hollow_cylinder_inner_radius_by_volume(outer_radius, volume, height) -> Unit("m"): 78 | """ 79 | Given the outer radius, the height and the inner radius of a hollow cylinder, 80 | compute the inner radius 81 | """ 82 | # Wolfram Alpha: solve V=(pi*o²*h)-(pi*i²*h) for i 83 | term1 = np.pi*height*(outer_radius**2)-volume 84 | # Due to rounding errors etc, term1 might become negative. 85 | # This will lead to sqrt(-x) => NaN but we actually treat it as a zero result 86 | if term1 < 0.: 87 | return 0 88 | # Default case 89 | return np.sqrt(term1)/(np.sqrt(np.pi) * np.sqrt(height)) 90 | -------------------------------------------------------------------------------- /UliEngineering/Math/Geometry/Polygon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Geometry functions, mainly for 2D coordinates. 5 | """ 6 | import numpy as np 7 | 8 | __all__ = ["polygon_lines", "polygon_area"] 9 | 10 | def polygon_lines(coords, closed=True): 11 | """ 12 | Given a (n,2) set of XY coordinates as numpy array, 13 | creates an (n,2,2) array of coordinate pairs. 14 | If the given coordinate array represents the points of a polygon, 15 | the return value represents pairs of coordinates to draw lines between 16 | to obtain the full polygon image. 17 | 18 | If closed==True, a line is included between the last and the first point. 19 | Note that the last->first pair appears first in the list. 20 | 21 | Algorithm: http://stackoverflow.com/a/42407359/2597135 22 | """ 23 | if len(coords.shape) != 2 or coords.shape[1] != 2: 24 | raise ValueError("Wrong shape for polygon lines input (expect (n,2)): {}".format( 25 | coords.shape)) 26 | shifted = np.roll(coords, 1, axis=0) 27 | ret = np.transpose(np.dstack([shifted, coords]), axes=(0, 2, 1)) 28 | return ret if closed else ret[1:] 29 | 30 | def polygon_area(coords): 31 | """ 32 | Compute the area of a polygon using the Shoelace formula 33 | 34 | Numpy hints from http://stackoverflow.com/a/30408825/2597135 35 | 36 | Parameters 37 | ---------- 38 | coords : numpy array-like 39 | The coords in a (n,2) array 40 | """ 41 | if len(coords.shape) != 2 or coords.shape[1] != 2: 42 | raise ValueError("Wrong shape for polygon area input (expect (n,2)): {}".format( 43 | coords.shape)) 44 | x = coords[:,0] 45 | y = coords[:,1] 46 | return 0.5 * np.abs(np.dot(x, np.roll(y, 1)) - np.dot(y, np.roll(x, 1))) 47 | -------------------------------------------------------------------------------- /UliEngineering/Math/Geometry/Sphere.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import math 3 | from UliEngineering.EngineerIO import normalize_numeric_args, Unit 4 | 5 | __all__ = ["sphere_volume_by_radius", 6 | "sphere_volume_by_diameter", 7 | "sphere_surface_area_by_radius", 8 | "sphere_surface_area_by_diameter"] 9 | 10 | @normalize_numeric_args 11 | def sphere_volume_by_radius(radius) -> Unit("m³"): 12 | """ 13 | Compute the volume of a sphere of a given radius 14 | """ 15 | return 4./3. * math.pi * radius**3 16 | 17 | @normalize_numeric_args 18 | def sphere_volume_by_diameter(diameter) -> Unit("m³"): 19 | """ 20 | Compute the volume of a sphere of a given diameter 21 | """ 22 | return sphere_volume_by_radius(diameter / 2.0) 23 | 24 | @normalize_numeric_args 25 | def sphere_surface_area_by_radius(radius) -> Unit("m²"): 26 | """ 27 | Compute the surface area of a sphere of a given radius 28 | """ 29 | return 4. * math.pi * radius**2 30 | 31 | @normalize_numeric_args 32 | def sphere_surface_area_by_diameter(diameter) -> Unit("m²"): 33 | """ 34 | Compute the surface area of a sphere of a given radius 35 | """ 36 | return sphere_surface_area_by_radius(diameter / 2.0) -------------------------------------------------------------------------------- /UliEngineering/Math/Geometry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/UliEngineering/Math/Geometry/__init__.py -------------------------------------------------------------------------------- /UliEngineering/Math/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/UliEngineering/Math/__init__.py -------------------------------------------------------------------------------- /UliEngineering/Mechanics/Threads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Thread information 4 | """ 5 | from collections import namedtuple 6 | 7 | __all__ = ["ThreadParameters", "threads"] 8 | 9 | class ThreadParameters(namedtuple("ThreadParameters", ["pitch", "outer_diameter", "core_diameter"])): 10 | """ 11 | Parameters 12 | ========== 13 | pitch: 14 | Thread pitch in mm 15 | outer_diameter: 16 | Outside thread diameter in mm (for exterior thread) 17 | inner_diameter: 18 | Inside thread diameter in mm (for exterior thread) 19 | """ 20 | pass 21 | 22 | threads = { 23 | # DIN 13 24 | # Source: http://www.gewinde-norm.de/metrisches-iso-gewinde-din-13.htm 25 | "M1": ThreadParameters(.25, 1., .693), 26 | "M1.2": ThreadParameters(.25, 1.2, .893), 27 | "M1.6": ThreadParameters(.35, 1.6, 1.171), 28 | "M2": ThreadParameters(.40, 2., 1.509), 29 | "M2.5": ThreadParameters(.45, 2.5, 1.948), 30 | "M3": ThreadParameters(.5, 3., 2.387), 31 | "M4": ThreadParameters(.7, 4., 3.141), 32 | "M5": ThreadParameters(.8, 5., 4.019), 33 | "M6": ThreadParameters(1., 6., 4.773), 34 | "M8": ThreadParameters(1.25, 8., 6.466), 35 | "M10": ThreadParameters(1.5, 10., 8.160), 36 | "M12": ThreadParameters(1.75, 12., 9.853), 37 | "M16": ThreadParameters(2., 16., 13.546), 38 | "M20": ThreadParameters(2.5, 20., 16.933), 39 | "M24": ThreadParameters(3., 24., 20.319), 40 | "M36": ThreadParameters(4., 36., 31.093), 41 | "M42": ThreadParameters(4.5, 42., 36.479), 42 | "M48": ThreadParameters(5., 48., 41.866), 43 | "M56": ThreadParameters(5.5, 56., 49.252), 44 | "M64": ThreadParameters(6., 64., 56.639) 45 | } 46 | -------------------------------------------------------------------------------- /UliEngineering/Mechanics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/UliEngineering/Mechanics/__init__.py -------------------------------------------------------------------------------- /UliEngineering/Optoelectronics/MPPC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Functions for Multi-Pixel photon counters (MPPCs) 5 | """ 6 | from UliEngineering.Units import Unit 7 | from UliEngineering.EngineerIO import normalize_numeric_args 8 | 9 | __all__ = [ 10 | "pixel_capacitance_from_terminal_capacitance" 11 | ] 12 | 13 | @normalize_numeric_args 14 | def pixel_capacitance_from_terminal_capacitance(terminal_capacitance="900pF", npixels=14331) -> Unit("F"): 15 | """ 16 | Estimate a MPPC's individual pixel's capacitance from the terminal capacitance. 17 | 18 | Typically, this overestimates the capacitance because the case & trace capacitance is included 19 | in the terminal capacitance. The overestimation effect is particularly large for MPPCs with very small 20 | pixels such as 15μm. 21 | 22 | This method is outlined as alternate method [by Hamamatsu](https://hub.hamamatsu.com/us/en/technical-notes/mppc-sipms/a-technical-guide-to-silicon-photomutlipliers-MPPC-Section-3.html) 23 | """ 24 | return terminal_capacitance / npixels 25 | -------------------------------------------------------------------------------- /UliEngineering/Optoelectronics/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 -------------------------------------------------------------------------------- /UliEngineering/Physics/Acceleration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for acceleration 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | import numpy as np 9 | import scipy.constants 10 | 11 | g0 = scipy.constants.physical_constants['standard acceleration of gravity'][0] 12 | 13 | __all__ = ["g_to_ms2", "ms2_to_g", "centrifugal_acceleration", "centrifuge_radius"] 14 | 15 | @normalize_numeric_args 16 | def g_to_ms2(g) -> Unit("m/s²"): 17 | """ 18 | Compute the acceleration in m/s² given the acceleration in g 19 | """ 20 | return g * g0 21 | 22 | @normalize_numeric_args 23 | def ms2_to_g(ms2) -> Unit("g"): 24 | """ 25 | Compute the acceleration in g given the acceleration in m/s² 26 | """ 27 | return ms2 / g0 28 | 29 | @normalize_numeric_args 30 | def centrifugal_acceleration(radius, speed) -> Unit("m/s²"): 31 | """ 32 | Compute the centrifugal acceleration given 33 | 34 | Online calculator available here: 35 | https://techoverflow.net/2020/04/20/centrifuge-acceleration-calculator-from-rpm-and-diameter/ 36 | (NOTE: Different units !) 37 | 38 | Parameters 39 | ---------- 40 | radius : 41 | The radius of the centrifuge in m 42 | speed : 43 | The speed of the centrifuge in Hz 44 | 45 | Returns 46 | ------- 47 | The acceleration in m/s² 48 | """ 49 | return 4 * np.pi**2 * radius * speed**2 50 | 51 | 52 | @normalize_numeric_args 53 | def centrifuge_radius(acceleration, speed) -> Unit("m"): 54 | """ 55 | Compute the centrifugal acceleration given 56 | 57 | Online calculator available here: 58 | https://techoverflow.net/2020/04/20/centrifuge-diameter-calculator-from-acceleration-rpm/ 59 | (NOTE: Different units !) 60 | 61 | Parameters 62 | ---------- 63 | speed : 64 | The speed of the centrifuge in Hz 65 | acceleration: 66 | The acceleration of the centrifuge in m/s² 67 | 68 | Returns 69 | ------- 70 | The radius of the centrifuge in m 71 | """ 72 | return acceleration / (4 * np.pi**2 * speed**2) 73 | -------------------------------------------------------------------------------- /UliEngineering/Physics/Density.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from UliEngineering.EngineerIO import normalize_numeric_args, Unit 4 | 5 | __all__ = ["Densities", "density_by_volume_and_weight"] 6 | 7 | """ 8 | Pre-defined densities for various materials in kg/m³ 9 | """ 10 | Densities: dict[str, float] = { 11 | # Various (pure) metals 12 | "Aluminium": 2700., # Source: https://en.wikipedia.org/wiki/Aluminium 13 | "Titanium": 4506., # Source: https://en.wikipedia.org/wiki/Titanium 14 | # Various alloys 15 | "Brass-CuZn10": 8800., # Source: https://www.metal-rolling-services.com/cuzn10-en 16 | "Brass-CuZn15": 8750., # Source: https://www.metal-rolling-services.com/cuzn15-en 17 | "Brass-CuZn30": 8550., # Source: https://www.metal-rolling-services.com/cuzn30-en 18 | "Brass-CuZn33": 8500., # Source: https://www.metal-rolling-services.com/cuzn33-en 19 | "Brass-CuZn36": 8450., # Source: https://www.metal-rolling-services.com/cuzn36-en 20 | "Brass-CuZn37": 8440., # Source: https://www.metal-rolling-services.com/cuzn37-en 21 | # Steel 22 | "S235JR": 7850., # Source: https://de.materials4me.com/media/pdf/e7/7d/30/Werkstoffdatenblatt_zum_Werkstoff_S235JR.pdf 23 | # Stainless steels 24 | "1.4003": 7700., # Source: https://www.edelstahl-rostfrei.de/fileadmin/user_upload/ISER/downloads/MB_822.pdf 25 | "1.4016": 7700., # Source: (same as above) 26 | "1.4511": 7700., # Source: (same as above) 27 | "1.4521": 7700., # Source: (same as above) 28 | "1.4509": 7700., # Source: (same as above) 29 | "1.4310": 7900., # Source: (same as above) 30 | "1.4318": 7900., # Source: (same as above) 31 | "1.4307": 7900., # Source: (same as above) 32 | "1.4305": 7900., # Source: (same as above) 33 | "1.4541": 7900., # Source: (same as above) 34 | "1.4401": 8000., # Source: (same as above) 35 | "1.4571": 8000., # Source: (same as above) 36 | "1.4437": 8000., # Source: (same as above) 37 | "1.4435": 8000., # Source: (same as above) 38 | "1.4439": 8000., # Source: (same as above) 39 | "1.4567": 7900., # Source: (same as above) 40 | "1.4539": 8000., # Source: (same as above) 41 | "1.4578": 8000., # Source: (same as above) 42 | "1.4547": 8000., # Source: (same as above) 43 | "1.4529": 8100., # Source: (same as above) 44 | "1.4565": 8000., # Source: (same as above) 45 | "1.4362": 7800., # Source: (same as above) 46 | "1.4462": 7800., # Source: (same as above) 47 | "1.4301": 7900., # Source: https://www.dew-stahl.com/fileadmin/files/dew-stahl.com/documents/Publikationen/Werkstoffdatenblaetter/RSH/1.4301_de.pdf 48 | "1.4404": 8000., # Source: https://www.dew-stahl.com/fileadmin/files/dew-stahl.com/documents/Publikationen/Werkstoffdatenblaetter/RSH/1.4404_en.pdf 49 | # Various types of polymers 50 | "POM": 1410., # Source: https://www.polyplastics.com/Gidb/GradeInfoDownloadAction.do?gradeId=1771&fileNo=1&langId=1&_LOCALE=ENGLISH 51 | "PTFE": 2200., # Source: https://en.wikipedia.org/wiki/Polytetrafluoroethylene 52 | "PEEK": 1320., # Source: https://en.wikipedia.org/wiki/Polyether_ether_ketone 53 | } 54 | 55 | @normalize_numeric_args 56 | def density_by_volume_and_weight(volume, weight) -> Unit("kg/m³"): 57 | """ 58 | Calculates the density of a material by its volume and weight. 59 | 60 | Parameters 61 | ---------- 62 | volume : float 63 | Volume of the material in m³. 64 | weight : float 65 | Weight of the material in kg. 66 | 67 | Returns 68 | ------- 69 | float 70 | Density of the material in kg/m³. 71 | """ 72 | return weight / volume -------------------------------------------------------------------------------- /UliEngineering/Physics/Frequency.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for frequencies 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | 9 | __all__ = ["frequency_to_period", "period_to_frequency"] 10 | 11 | @normalize_numeric_args 12 | def frequency_to_period(frequency) -> Unit("s"): 13 | """ 14 | Compute the period associated with a frequency. 15 | 16 | Parameters 17 | ---------- 18 | frequency : number or Engineer string or NumPy array-like 19 | The frequency in Hz 20 | """ 21 | return 1./frequency 22 | 23 | @normalize_numeric_args 24 | def period_to_frequency(period) -> Unit("Hz"): 25 | """ 26 | Compute the frequency associated with a period. 27 | 28 | Parameters 29 | ---------- 30 | period : number or Engineer string or NumPy array-like 31 | The period in seconds 32 | """ 33 | return 1./period 34 | -------------------------------------------------------------------------------- /UliEngineering/Physics/JohnsonNyquistNoise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Johnson Nyquist noise utilities for both voltage and current noise 5 | 6 | # Usage example 7 | >>> from UliEngineering.Physics.JohnsonNyquistNoise import * 8 | >>> from UliEngineering.EngineerIO import autoFormat 9 | >>> print(autoFormat(johnson_nyquist_noise_current, "20 MΩ", 1000, "20 °C")) 10 | >>> print(autoFormat(johnson_nyquist_noise_voltage, "10 MΩ", 1000, 25)) 11 | """ 12 | from .Temperature import normalize_temperature 13 | from UliEngineering.EngineerIO import normalize_numeric_args 14 | from UliEngineering.Units import Unit 15 | import math 16 | 17 | __all__ = ["johnson_nyquist_noise_current", "johnson_nyquist_noise_voltage"] 18 | 19 | try: 20 | from scipy.constants import k as boltzmann_k 21 | except ModuleNotFoundError: 22 | # Exact defined value: https://physics.nist.gov/cgi-bin/cuu/Value?k 23 | boltzmann_k = 1.380649e-23 24 | 25 | @normalize_numeric_args 26 | def johnson_nyquist_noise_current(r, delta_f, T) -> Unit("A"): 27 | """ 28 | Compute the Johnson Nyquist noise current in amperes 29 | T must be given in °C whereas r must be given in Ohms. 30 | The result is given in volts 31 | """ 32 | t_kelvin = normalize_temperature(T) 33 | # Support celsius and kelvin inputs 34 | return math.sqrt((4 * boltzmann_k * t_kelvin * delta_f)/r) 35 | 36 | @normalize_numeric_args 37 | def johnson_nyquist_noise_voltage(r, delta_f, T) -> Unit("V"): 38 | """ 39 | Compute the Johnson Nyquist noise voltage in volts 40 | T must be given in °C whereas r must be given in Ohms. 41 | The result is given in volts 42 | """ 43 | t_kelvin = normalize_temperature(T) 44 | return math.sqrt(4 * boltzmann_k * t_kelvin * delta_f * r) 45 | -------------------------------------------------------------------------------- /UliEngineering/Physics/Light.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for computations related to noise density 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | import numpy as np 9 | 10 | __all__ = ["lumen_to_candela_by_apex_angle"] 11 | 12 | @normalize_numeric_args 13 | def lumen_to_candela_by_apex_angle(flux, angle) -> Unit("cd"): 14 | """ 15 | Compute the luminous intensity from the luminous flux, 16 | assuming that the flux of is distributed equally around 17 | a cone with apex angle . 18 | 19 | Keyword parameters 20 | ------------------ 21 | flux : value, engineer string or NumPy array 22 | The luminous flux in Lux. 23 | angle : value, engineer string or NumPy array 24 | The apex angle of the emission cone, in degrees 25 | For many LEDs, this is 26 | 27 | >>> autoFormat(lumen_to_candela_by_apex_angle, "25 lm", "120°") 28 | '7.96 cd' 29 | """ 30 | solid_angle = 2*np.pi*(1.-np.cos(np.deg2rad(angle)/2.0)) 31 | return flux / solid_angle 32 | -------------------------------------------------------------------------------- /UliEngineering/Physics/MagneticResonance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from UliEngineering.EngineerIO import normalize_numeric_args, Unit 3 | import scipy.constants 4 | 5 | __all__ = [ 6 | "NucleusLarmorFrequency", 7 | "larmor_frequency" 8 | ] 9 | 10 | # Nucleus Larmor frequencies in Hz 11 | 12 | class NucleusLarmorFrequency: 13 | """Standard frequencies for common nuclei""" 14 | H1 = scipy.constants.physical_constants['shielded proton gyromag. ratio in MHz/T'][0] 15 | He3 = scipy.constants.physical_constants['shielded helion gyromag. ratio in MHz/T'][0] 16 | 17 | @normalize_numeric_args 18 | def larmor_frequency(b0, nucleus_larmor_frequency=NucleusLarmorFrequency.H1) -> Unit("Hz"): 19 | """ 20 | Get the magnetic resonance frequency (larmor frequency) 21 | for a given nucleus in a given magnetic field strength B0 22 | 23 | Note that the frequency is given in Hz, not in MHz! 24 | 25 | :param b0: Magnetic field strength in Tesla 26 | :param nucleus_larmor_frequency: Larmor frequency of the nucleus in MHz/T 27 | """ 28 | return b0 * (nucleus_larmor_frequency * 1e6) # MHz/T -> Hz/T -------------------------------------------------------------------------------- /UliEngineering/Physics/NTC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities regarding NTC thermistors 5 | 6 | See http://www.vishay.com/docs/29053/ntcintro.pdf for details 7 | """ 8 | from UliEngineering.Physics.Temperature import normalize_temperature 9 | from UliEngineering.EngineerIO import normalize_numeric_args 10 | from UliEngineering.Units import Unit 11 | import numpy as np 12 | from .Temperature import normalize_temperature_celsius, zero_Celsius 13 | 14 | __all__ = ["ntc_resistance", "ntc_resistances"] 15 | 16 | @normalize_numeric_args 17 | def ntc_resistance(r25, b25, t) -> Unit("Ω"): 18 | """ 19 | Compute the NTC resistance by temperature and NTC parameters 20 | 21 | Parameters 22 | ---------- 23 | r25 : float or EngineerIO string 24 | The NTC resistance at 25°C, sometimes also called "nominal resistance" 25 | b25: float or EngineerIO string 26 | The NTC b-constant (e.g. b25/50, b25/85 or b25/100) 27 | t : temperature 28 | The temperature. Will be interpreted using normalize_temperature() 29 | """ 30 | t = normalize_temperature(t) # t is now in Kelvins 31 | # Compute resistance 32 | return r25 * np.exp(b25 * (1./t - 1./(25. + zero_Celsius))) 33 | 34 | def ntc_resistances(r25, b25, t0=-40, t1=85, resolution=0.1): 35 | """ 36 | Compute the resistances over a temperature range with a given resolution. 37 | 38 | Returns 39 | ======= 40 | A (temperatures, values) tuple 41 | """ 42 | # Convert all temperatures to Celsius 43 | t0 = normalize_temperature_celsius(t0) 44 | t1 = normalize_temperature_celsius(t1) 45 | resolution = normalize_temperature_celsius(resolution) 46 | # Create a temperature range 47 | ts = np.linspace(t0, t1, int((t1 - t0) // resolution + 1)) 48 | return ts, ntc_resistance(r25, b25, ts) 49 | -------------------------------------------------------------------------------- /UliEngineering/Physics/NoiseDensity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for computations related to noise density 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | import numpy as np 9 | 10 | __all__ = ["actual_noise", "noise_density"] 11 | 12 | @normalize_numeric_args 13 | def actual_noise(density, bandwith) -> Unit("V"): 14 | """ 15 | Compute the actual noise given: 16 | - A noise density in x/√Hz where x is any unit 17 | - A bandwith in ΔHz 18 | 19 | >>> autoFormat(actualNoise, "100 µV", "100 Hz") 20 | '1.00 mV' 21 | """ 22 | return np.sqrt(bandwith) * density 23 | 24 | @normalize_numeric_args 25 | def noise_density(actual_noise, bandwith) -> Unit("V/√Hz"): 26 | """ 27 | Compute the noise density given: 28 | - A noise density in x/√Hz where x is any unit 29 | - A bandwith in ΔHz 30 | 31 | >>> formatValue(noiseDensity("1.0 mV", "100 Hz"), "V/√Hz") 32 | '100 μV/√Hz' 33 | """ 34 | return actual_noise / np.sqrt(bandwith) 35 | -------------------------------------------------------------------------------- /UliEngineering/Physics/Pressure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Pressure utilities 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | import numpy as np 9 | 10 | __all__ = ["pascal_to_bar", "bar_to_pascal", "barlow_tangential"] 11 | 12 | @normalize_numeric_args 13 | def pascal_to_bar(pressure: Unit("Pa")) -> Unit("bar"): 14 | """ 15 | Convert the pressure in pascal to the pressure in bar 16 | """ 17 | return pressure*1e-5 18 | 19 | @normalize_numeric_args 20 | def bar_to_pascal(pressure: Unit("bar")) -> Unit("Pa"): 21 | """ 22 | Convert the pressure in bar to the pressure in Pascal 23 | """ 24 | return pressure*1e5 25 | 26 | @normalize_numeric_args 27 | def barlow_tangential(outer_diameter: Unit("m"), inner_diameter: Unit("m"), pressure: Unit("Pa")) -> Unit("Pa"): 28 | """ 29 | Compute the tangential stress of a pressure vessel at [pressure] using Barlow's formula for thin-walled tubes. 30 | 31 | Note that this formula only applies for (outer_diameter/inner_diameter) < 1.2 ! 32 | (this assumption is not checked). Otherwise, the stress distribution will be too uneven. 33 | """ 34 | dm = (outer_diameter + inner_diameter) / 2 35 | s = (outer_diameter - inner_diameter) / 2 36 | return pressure * dm / (2 * s) 37 | -------------------------------------------------------------------------------- /UliEngineering/Physics/RF.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for computations related to noise density 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | import numpy as np 9 | 10 | __all__ = [ 11 | 'quality_factor', 'resonant_impedance', 'resonant_frequency', 12 | 'resonant_inductance'] 13 | 14 | @normalize_numeric_args 15 | def quality_factor(frequency, bandwidth) -> Unit(""): 16 | """ 17 | Compute the quality factor of a resonant circuit 18 | from the frequency and the bandwidth: 19 | 20 | Q = frequency / bandwidth 21 | 22 | Source: http://www.c-max-time.com/tech/antenna.php 23 | 24 | >>> quality_factor("8.000 MHz", "1 kHz") 25 | 8000.0 26 | """ 27 | return frequency / bandwidth 28 | 29 | @normalize_numeric_args 30 | def resonant_impedance(L, C, Q=100.) -> Unit("Ω"): 31 | """ 32 | Compute the resonant impedance of a resonant circuit 33 | 34 | R_res = sqrt(L / C) / Q 35 | 36 | Source: http://www.c-max-time.com/tech/antenna.php 37 | 38 | >>> resonant_impedance("100 uH", "10 nF", Q=30.0) 39 | 3.333333333333333 40 | >>> auto_format(resonant_impedance, "100 uH", "10 nF", Q=30.0) 41 | '3.33 Ω' 42 | """ 43 | return np.sqrt(L / C) / Q 44 | 45 | @normalize_numeric_args 46 | def resonant_frequency(L, C) -> Unit("Hz"): 47 | """ 48 | Compute the resonant frequency of a resonant circuit 49 | given the inductance and capacitance. 50 | 51 | f = 1 / (2 * pi * sqrt(L * C)) 52 | 53 | Source: http://www.c-max-time.com/tech/antenna.php 54 | 55 | >>> resonant_frequency("100 uH", "10 nF") 56 | 159154.94309189534 57 | >>> auto_format(resonant_frequency, "100 uH", "10 nF") 58 | '159 kHz' 59 | """ 60 | return 1 / (2 * np.pi * np.sqrt(L * C)) 61 | 62 | @normalize_numeric_args 63 | def resonant_inductance(fres, C) -> Unit("H"): 64 | """ 65 | Compute the inductance of a resonant circuit 66 | given the resonant frequency and its capacitance. 67 | 68 | L = 1 / (4 * pi² * fres² * C) 69 | 70 | Source: http://www.c-max-time.com/tech/antenna.php 71 | 72 | >>> resonant_inductance("250 kHz", "10 nF") 73 | 4.052847345693511e-05 74 | >>> auto_format(resonant_inductance, "250 kHz", "10 nF") 75 | '40.5 µH' 76 | """ 77 | return 1 / (4 * np.pi**2 * fres**2 * C) 78 | -------------------------------------------------------------------------------- /UliEngineering/Physics/Rotation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for acceleration 5 | """ 6 | from UliEngineering.EngineerIO import normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | import numpy as np 9 | 10 | __all__ = ["rpm_to_Hz", "rpm_to_rps", "hz_to_rpm", "angular_speed", 11 | "rotation_linear_speed", "centrifugal_force"] 12 | 13 | @normalize_numeric_args 14 | def rpm_to_Hz(rpm: Unit("rpm")) -> Unit("Hz"): 15 | """ 16 | Compute the rotational speed in Hz given the rotational speed in rpm 17 | """ 18 | return rpm / 60. 19 | 20 | @normalize_numeric_args 21 | def hz_to_rpm(speed: Unit("Hz")) -> Unit("rpm"): 22 | """ 23 | Compute the rotational speed in rpm given the rotational speed in Hz 24 | """ 25 | return speed * 60. 26 | 27 | rpm_to_rps = rpm_to_Hz 28 | 29 | @normalize_numeric_args 30 | def angular_speed(speed: Unit("Hz")) -> Unit("1/s"): 31 | """ 32 | Compute Ω, the angular speed of a centrifugal system 33 | """ 34 | return 2*np.pi*speed 35 | 36 | @normalize_numeric_args 37 | def rotation_linear_speed(radius: Unit("m"), speed: Unit("Hz")) -> Unit("m/s"): 38 | """ 39 | Compute the linear speed at a given [radius] for a centrifugal system rotating at [speed]. 40 | """ 41 | return radius * angular_speed(speed) 42 | 43 | @normalize_numeric_args 44 | def centrifugal_force(radius: Unit("m"), speed: Unit("Hz"), mass: Unit("g")) -> Unit("N"): 45 | """ 46 | Compute the centrifugal force of a [mass] rotation at [speed] at radius [radius] 47 | """ 48 | mass = mass / 1000.0 # mass needs to be Kilograms TODO Improve 49 | return mass * angular_speed(speed)**2 * radius 50 | 51 | @normalize_numeric_args 52 | def rotating_liquid_pressure(density: Unit("kg/m³"), speed: Unit("Hz"), radius: Unit("m")) -> Unit("Pa"): 53 | """ 54 | Compute the pressure in a body of liquid (relative to the steady-state pressure) 55 | The calculation does not include gravity. 56 | 57 | Also see https://www.youtube.com/watch?v=kIH7wEq3H-M 58 | Also see https://www.physicsforums.com/threads/pressure-of-a-rotating-bucket-of-liquid.38112/ 59 | """ 60 | return density * angular_speed(speed)**2 * radius**2 61 | -------------------------------------------------------------------------------- /UliEngineering/Physics/Temperature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities regarding temperatures 5 | """ 6 | from UliEngineering.EngineerIO import normalize_engineer_notation, normalize_numeric_args 7 | from UliEngineering.Units import Unit 8 | from UliEngineering.Exceptions import InvalidUnitException 9 | 10 | try: 11 | from scipy.constants import zero_Celsius 12 | except: 13 | zero_Celsius = 273.15 # Defined constant for 0 °C in Kelvin 14 | 15 | __all__ = ["celsius_to_kelvin", "kelvin_to_celsius", 16 | "fahrenheit_to_kelvin", "normalize_temperature", 17 | "normalize_temperature_celsius", 18 | "normalize_temperature_kelvin", 19 | "temperature_with_dissipation", 20 | "fahrenheit_to_celsius", "zero_Celsius"] 21 | 22 | @normalize_numeric_args 23 | def celsius_to_kelvin(c) -> Unit("K"): 24 | return c + zero_Celsius 25 | 26 | @normalize_numeric_args 27 | def kelvin_to_celsius(c) -> Unit("°C"): 28 | return c - zero_Celsius 29 | 30 | @normalize_numeric_args 31 | def fahrenheit_to_kelvin(f) -> Unit("K"): 32 | return (f + 459.67) * 5.0 / 9.0 33 | 34 | def fahrenheit_to_celsius(f) -> Unit("°C"): 35 | return kelvin_to_celsius(fahrenheit_to_kelvin(f)) 36 | 37 | def normalize_temperature(t, default_unit="°C") -> Unit("K"): 38 | """ 39 | Normalize a temperature to kelvin. 40 | If it is a number or it has no unit, assume it is a default unit 41 | Else, evaluate the unit(K, °C, °F, C, F) 42 | """ 43 | unit = "" 44 | if isinstance(t, str): 45 | res = normalize_engineer_notation(t) 46 | if res is None: 47 | raise ValueError("Invalid temperature string: {}".format(t)) 48 | t, unit = res.value, res.unit 49 | if not unit: 50 | unit = default_unit 51 | # Evaluate unit 52 | if unit in ["°C", "C"]: 53 | return celsius_to_kelvin(t) 54 | elif unit in ["°K", "K"]: 55 | return t 56 | elif unit in ["°F", "F"]: 57 | return fahrenheit_to_kelvin(t) 58 | else: 59 | raise InvalidUnitException("Unknown temperature unit: '{}'".format(unit)) 60 | 61 | normalize_temperature_kelvin = normalize_temperature 62 | 63 | def normalize_temperature_celsius(t, default_unit="°C") -> Unit("°C"): 64 | """Like normalize_temperature(), but returns a value in celsius instead of Kelvin""" 65 | return kelvin_to_celsius(normalize_temperature(t, default_unit)) 66 | 67 | @normalize_numeric_args 68 | def temperature_with_dissipation(power_dissipated="1 W", theta="50 °C/W", t_ambient="25 °C") -> Unit("°C"): 69 | """ 70 | Compute the temperature of a component, given its thermal resistance (theta), 71 | its dissipated power and 72 | """ 73 | t_ambient = normalize_temperature_celsius(t_ambient) 74 | return t_ambient + power_dissipated * theta 75 | -------------------------------------------------------------------------------- /UliEngineering/Physics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/UliEngineering/Physics/__init__.py -------------------------------------------------------------------------------- /UliEngineering/SignalProcessing/Correlation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Functions for correlating a dataset with itself and other datasets 5 | Mainly built for 1D signal analysis. 6 | Might or might not work for higher-dimensional data. 7 | """ 8 | import scipy.signal 9 | 10 | __all__ = ["autocorrelate"] 11 | 12 | def autocorrelate(signal): 13 | """ 14 | Auto-correlate a signal with itself. 15 | 16 | Based on the fast FFT convolution using 17 | scipy.signal.fftconvolve. 18 | """ 19 | return scipy.signal.fftconvolve(signal, signal[::-1], mode='full') 20 | -------------------------------------------------------------------------------- /UliEngineering/SignalProcessing/DateTime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for processing and modifying datetime objects 5 | """ 6 | import datetime 7 | 8 | __all__ = ["splice_date", "auto_strptime"] 9 | 10 | 11 | def splice_date(datesrc, timesrc, tzinfo=None): 12 | """ 13 | Create a new datetime that takes the date from datesrc and 14 | the time from timesrc. The tzinfo is taken from the tzinfo 15 | parameter. If it is None, it is taken from 16 | timesrc.tzinfo. No timezone conversion is performed. 17 | """ 18 | tzinfo = timesrc.tzinfo if tzinfo is None else tzinfo 19 | return datetime.datetime(datesrc.year, datesrc.month, datesrc.day, 20 | timesrc.hour, timesrc.minute, timesrc.second, 21 | timesrc.microsecond, tzinfo=tzinfo) 22 | 23 | 24 | def auto_strptime(s): 25 | """ 26 | Parses a datetime in a number of formats, 27 | automatically recognizing which format is correct. 28 | 29 | Supported formats: 30 | %Y-%m-%d 31 | %Y-%m-%d %H 32 | %Y-%m-%d %H:%M 33 | %Y-%m-%d %H:%M:%S 34 | %Y-%m-%d %H:%M:%S.%f 35 | %H:%M:%S 36 | %H:%M:%S.%f 37 | """ 38 | s = s.strip() 39 | ncolon = s.count(":") 40 | have_date = "-" in s 41 | if "." in s: # Have fractional seconds 42 | dateformat = "%Y-%m-%d %H:%M:%S.%f" if have_date else "%H:%M:%S.%f" 43 | elif " " not in s: # Have only date or have only time 44 | dateformat = "%Y-%m-%d" if have_date else "%H:%M:%S" 45 | elif ncolon == 0: # Have date and time and no fractional but only hours 46 | dateformat = "%Y-%m-%d %H" 47 | elif ncolon == 1: # Have date and time and no fractional but only h & m 48 | dateformat = "%Y-%m-%d %H:%M" 49 | else: # Have date and time and no fractional but full time 50 | dateformat = "%Y-%m-%d %H:%M:%S" 51 | return datetime.datetime.strptime(s, dateformat) 52 | -------------------------------------------------------------------------------- /UliEngineering/SignalProcessing/Normalize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Functions for normalizing signals 5 | """ 6 | import numpy as np 7 | from collections import namedtuple 8 | import scipy.signal 9 | from .Utils import peak_to_peak 10 | 11 | __all__ = ["normalize_max", "center_to_zero", "normalize_minmax", "normalize_plusminus_peak"] 12 | 13 | NormalizationResult = namedtuple("NormalizationResult", ["data", "factor", "offset"]) 14 | 15 | def normalize_max(signal): 16 | """ 17 | Normalize signal by dividing by its max value. 18 | Does not perform any offset adjustment. 19 | 20 | This approach works well for data that is guaranteed to be 21 | positive and if no offset adjustment is desired. 22 | 23 | In case signal has a max of <= 0.0, signal is returned. 24 | For a similar function that also limits the minimum value, 25 | see normalize_plusminus_peak. 26 | 27 | Returns 28 | ------- 29 | A NormalizationResult() object. 30 | Use .data to access the data 31 | Use .factor to access the factor that signal was divided by 32 | Use .offset to access the offset that was subtracted from signal 33 | """ 34 | if len(signal) == 0: 35 | return NormalizationResult([], 1., 0.) 36 | mx = np.max(signal) 37 | # Avoid divide by zero 38 | if mx <= 0.: 39 | return NormalizationResult(signal, 1., 0.) 40 | return NormalizationResult(signal / mx, mx, 0.) 41 | 42 | def normalize_minmax(signal): 43 | """ 44 | Normalize signal by setting its lowest value 45 | to 0.0 and its highest value to 1.0, 46 | keeping all other values. 47 | 48 | If signal consists of only zeros, no factor 49 | normalization is applied. 50 | 51 | Returns 52 | ------- 53 | A NormalizationResult() object. 54 | Use .data to access the data 55 | Use .factor to access the factor that signal was divided by 56 | Use .offset to access the offset that was subtracted from signal 57 | """ 58 | if len(signal) == 0: 59 | return NormalizationResult([], 1., 0.) 60 | mi = np.min(signal) 61 | mx = np.max(signal) 62 | factor = mx - mi 63 | if factor == 0.0: 64 | factor = 1.0 65 | return NormalizationResult((signal - mi) / factor, factor, mi) 66 | 67 | def center_to_zero(signal): 68 | """ 69 | Normalize signal by subtracting its mean 70 | Does not perform any factor normalization 71 | 72 | Returns 73 | ------- 74 | A NormalizationResult() object. 75 | Use .data to access the data 76 | Use .factor to access the factor that signal was divided by 77 | Use .offset to access the offset that was subtracted from signal 78 | """ 79 | mn = np.mean(signal) 80 | return NormalizationResult(signal - mn, 1., mn) 81 | 82 | 83 | def normalize_plusminus_peak(signal): 84 | """ 85 | Center a signal to zero and normalize so that 86 | - np.max(result) is <= 1.0 87 | - np.min(result) is <= 1.0 88 | 89 | Returns 90 | ------- 91 | A NormalizationResult() object. 92 | Use .data to access the data 93 | Use .factor to access the factor that signal was divided by 94 | Use .offset to access the offset that was subtracted from signal 95 | """ 96 | norm_res = center_to_zero(signal) 97 | mi = np.min(norm_res.data) 98 | mx = np.max(norm_res.data) 99 | factor = max(mi, mx) 100 | return NormalizationResult(norm_res / factor, factor, norm_res.offset) 101 | 102 | 103 | -------------------------------------------------------------------------------- /UliEngineering/SignalProcessing/Simulation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities for FFT computation and visualization 5 | """ 6 | import numpy as np 7 | import functools 8 | from UliEngineering.EngineerIO import normalize_numeric 9 | 10 | __all__ = [ 11 | "sine_wave", 12 | "cosine_wave", 13 | "square_wave", 14 | "triangle_wave", 15 | "sawtooth", 16 | "inverse_sawtooth", 17 | ] 18 | 19 | 20 | def _generate_wave(genfn, frequency, samplerate, amplitude=1., length=1., phaseshift=0., timedelay=0., offset=0.): 21 | """ 22 | Generate a wave using a given function of a specific frequency of a specific length. 23 | 24 | :param frequency A np.sin-like generator function (period shall be 2*pi) 25 | :param frequency The frequency in Hz 26 | :param samplerate The samplerate of the resulting array 27 | :param amplitude The peak amplitude of the sinewave 28 | :param length The length of the result in seconds 29 | :param timedelay The phaseshift, in seconds (in addition to phaseshift) 30 | :param phaseshift The phaseshift in degrees (in addition to timedelay) 31 | """ 32 | # Normalize text values, e.g. "100 kHz" => 100000.0 33 | frequency = normalize_numeric(frequency) 34 | samplerate = normalize_numeric(samplerate) 35 | amplitude = normalize_numeric(amplitude) 36 | length = normalize_numeric(length) 37 | phaseshift = normalize_numeric(phaseshift) 38 | offset = normalize_numeric(offset) 39 | timedelay = normalize_numeric(timedelay) 40 | # Perform calculations 41 | x = np.arange(length * samplerate) 42 | phaseshift_add = phaseshift * samplerate / (360. * frequency) 43 | phaseshift_add += timedelay * samplerate 44 | return offset + amplitude * genfn(frequency * (2. * np.pi) * (x + phaseshift_add) / samplerate) 45 | 46 | sine_wave = functools.partial(_generate_wave, np.sin) 47 | cosine_wave = functools.partial(_generate_wave, np.cos) 48 | 49 | try: 50 | import scipy.signal 51 | square_wave = functools.partial(_generate_wave, scipy.signal.square) 52 | triangle_wave = functools.partial(_generate_wave, 53 | functools.partial(scipy.signal.sawtooth, width=0.5)) 54 | sawtooth = functools.partial(_generate_wave, 55 | functools.partial(scipy.signal.sawtooth, width=1)) 56 | inverse_sawtooth = functools.partial(_generate_wave, 57 | functools.partial(scipy.signal.sawtooth, width=0)) 58 | except ModuleNotFoundError: 59 | def _error_fn(*args, **kwargs): 60 | raise NotImplementedError("You need to install scipy to use this function!") 61 | square_wave = _error_fn 62 | triangle_wave = _error_fn 63 | sawtooth = _error_fn 64 | inverse_sawtooth = _error_fn 65 | -------------------------------------------------------------------------------- /UliEngineering/SignalProcessing/Weight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Functions related to weight of arrays 5 | """ 6 | import numpy as np 7 | 8 | __all__ = ["weigh_halves", "weight_symmetry"] 9 | 10 | def weigh_halves(arr, operator=np.sum): 11 | """ 12 | Split a 1D array into two halves (right in the middle) 13 | and compute the weight of each half (i.e. sum of all values in that half). 14 | 15 | Odd-sized arrays are handled by adding 1/2 of the middle element to each value. 16 | 17 | The purpose of this function is to allow to center the "center of weight" 18 | in sliding window algorithms 19 | 20 | Returns (weightLeft, weightReight) 21 | 22 | Parameters: 23 | ----------- 24 | arr : 1D NumPy array 25 | The array to process. 26 | operator : unary function 27 | Alternative operator to summarize the array halves. 28 | Common choices include np.mean, np.sum or rms from UliEngineering.SignalProcessing.Utils. 29 | """ 30 | if len(arr) % 2 == 0: # Even array size 31 | # => We can just split in the middle 32 | pivot = len(arr) // 2 33 | return operator(arr[:pivot]), operator(arr[pivot:]) 34 | else: 35 | # => We can just split in the middle 36 | pivot = len(arr) // 2 37 | middle = arr[pivot] / 2 38 | return operator(arr[:pivot]) + middle, operator(arr[pivot + 1:]) + middle 39 | 40 | def weight_symmetry(a, b): 41 | """ 42 | Given two weights a, b computes a coefficient 43 | about how equal they are: 44 | 45 | 1.0 : Totally equal 46 | 0.0 : Totally different 47 | 48 | The coefficient does not depend on (a + b) 49 | and is computed using the following formula 50 | 1 - (np.abs(a - b) / (a + b)) 51 | 52 | This function is often used like this: 53 | 54 | >>> weight_symmetry(*weigh_halves(arr)) 55 | 1.0 56 | """ 57 | return 1 - (np.abs(a - b) / (a + b)) 58 | -------------------------------------------------------------------------------- /UliEngineering/SignalProcessing/Window.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Window functions used e.g. for FFTs 5 | """ 6 | import numpy as np 7 | 8 | __all__ = ["WindowFunctor", "create_window", 9 | "create_and_apply_window"] 10 | 11 | # Predefined windows 12 | 13 | def create_window(size, window_id="blackman", param=None): 14 | """ 15 | Create a new window numpy array 16 | param is only used for some windows. 17 | 18 | window_id can also be a function/functor which 19 | is used to create the window. 20 | 21 | >>> create_window("blackman", 500) 22 | ... # NumPy array of size 500 23 | >>> create_window(myfunc, 500, param=3.5) 24 | ... # result of calling myfunc(500, 3.5) 25 | """ 26 | if window_id == "blackman": 27 | return np.blackman(size) 28 | elif window_id == "bartlett": 29 | return np.bartlett(size) 30 | elif window_id == "hamming": 31 | return np.hamming(size) 32 | elif window_id == "hanning": 33 | return np.hanning(size) 34 | elif window_id == "kaiser": 35 | return np.kaiser(size, 2.0 if param is None else param) 36 | elif window_id in ["ones", "none"]: 37 | return np.ones(size) 38 | elif callable(window_id): 39 | return window_id(size, param) 40 | else: 41 | raise ValueError(f"Unknown window {window_id}") 42 | 43 | def create_and_apply_window(data, window_id="blackman", param=None, inplace=False): 44 | """ 45 | Create a window suitable for data, multiply it with 46 | data and return the result 47 | 48 | Parameters 49 | ---------- 50 | data : numpy array-like 51 | The data to use. Must be 1D. 52 | window_id : string or functor 53 | The name of the window to use. 54 | See create_window() documentation 55 | param : number or None 56 | The parameter used for certain windows. 57 | See create_window() documentation 58 | inplace : bool 59 | If True, data is modified in-place 60 | If False, data is not modified. 61 | """ 62 | window = create_window(len(data), window_id, param) 63 | if inplace: 64 | data *= window 65 | return data 66 | else: 67 | return data * window 68 | 69 | class WindowFunctor(object): 70 | """ 71 | Initialize a window functor that initializes 72 | 73 | """ 74 | def __init__(self, size, window_id="blackman", param=None): 75 | """ 76 | Create a new WindowFunctor. 77 | __init__ initialized the window array 78 | 79 | window_id : string or functor 80 | The name of the window to use. 81 | See create_window() documentation 82 | param : number or None 83 | The parameter used for certain windows. 84 | See create_window() documentation 85 | """ 86 | self.size = size 87 | self.window = create_window(size, window_id, param=param) 88 | 89 | def __len__(self): 90 | return self.size 91 | 92 | def __call__(self, data, inplace=False): 93 | """ 94 | Apply this window to a data array. 95 | 96 | Parameters 97 | ---------- 98 | data : numpy array-like 99 | The data to apply the window to. 100 | The length of data must match self.size. 101 | This is verified. 102 | inplace : bool 103 | If True, data is modified in-place 104 | If False, data is not modified. 105 | """ 106 | if len(data) != self.size: 107 | raise ValueError(f"Data size {len(data)} does not match WindowFunctor size {self.size}") 108 | # Apply 109 | if inplace: 110 | data *= self.window 111 | return data 112 | else: 113 | return data * self.window 114 | 115 | 116 | -------------------------------------------------------------------------------- /UliEngineering/SignalProcessing/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 -------------------------------------------------------------------------------- /UliEngineering/Units.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Units, quantities and related 5 | """ 6 | from collections import namedtuple 7 | import functools 8 | 9 | __all__ = ["Unit", "UnannotatedReturnValueError", 10 | "InvalidUnitInContextException", "InvalidUnitCombinationException", 11 | "find_returned_unit", "UnknownUnitInContextException"] 12 | 13 | Unit = namedtuple("Unit", ["unit"]) 14 | 15 | class UnannotatedReturnValueError(Exception): 16 | """ 17 | Raised if the automatic unit finder cannot find 18 | the appropriate function annotation that 19 | tells an auto-formatting function which unit is being used. 20 | 21 | Returns the unit string 22 | """ 23 | pass 24 | 25 | 26 | class InvalidUnitInContextException(ValueError): 27 | """ 28 | Raised if the unit might not be a globally 29 | unknown or invalid unit, but in the given context 30 | it can't be used 31 | """ 32 | pass 33 | 34 | 35 | class UnknownUnitInContextException(ValueError): 36 | """ 37 | Raised if the unit is not known in this context, 38 | e.g. if "A" is used as a unit of length. 39 | 40 | The message should contain information on what type of 41 | quantity (e.g. length) is accepted. 42 | """ 43 | pass 44 | 45 | class InvalidUnitCombinationException(ValueError): 46 | """ 47 | Raised if the units involved in an operation can't be 48 | combined in the way requested, for example if the 49 | """ 50 | pass 51 | 52 | def find_returned_unit(fn): 53 | """ 54 | Given a function that is assumed to return a quantity 55 | and annotated with the corresponding unit, determines 56 | which is the unit returned by the function 57 | """ 58 | if not callable(fn): 59 | raise ValueError("fn must be callable") 60 | # Access innermost function inside possibly nested partials 61 | annotatedFN = fn 62 | while isinstance(annotatedFN, functools.partial): 63 | annotatedFN = annotatedFN.func 64 | # We have the innermost function 65 | try: 66 | unit = annotatedFN.__annotations__["return"] 67 | # Assume it's a Unit namedtuple 68 | return unit.unit 69 | except KeyError: # No return annotation 70 | raise UnannotatedReturnValueError( 71 | "Function {} does not have an annotated return value".format(fn)) 72 | -------------------------------------------------------------------------------- /UliEngineering/Utils/Compression.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Compression utilities 4 | """ 5 | import gzip 6 | import bz2 7 | import lzma 8 | import os.path 9 | 10 | __open_map = { 11 | "": open, 12 | ".gz": gzip.open, 13 | ".bz2": bz2.open, 14 | ".lzma": lzma.open, 15 | ".xz": lzma.open 16 | } 17 | 18 | """ 19 | Mode map for opening binary files that maps 20 | normal open() single-char modes to text modes 21 | and everything else to binary modes 22 | """ 23 | __mode_map = { 24 | "r": "rt", "rb": "rb", "w": "wb", "wb": "wb", 25 | "x": "xt", "xb": "xb", "a": "at", "ab": "ab", 26 | "rt": "rt", "wt": "wt", "xt": "xt", "at": "at" 27 | } 28 | 29 | def auto_open(filename, mode="r", **kwargs): 30 | """ 31 | Automatically open a potentially compressed file using the right 32 | library variant of open(). 33 | The correct decompression algorithm is selected by filename extension. 34 | This function can be used instead of open() and automatically selects 35 | the right mode (text or binary). 36 | """ 37 | extension = os.path.splitext(filename)[1] 38 | if extension not in __open_map: 39 | raise ValueError( 40 | f"Unable to find correct decompression for extension '{extension}' in filename {filename}") 41 | open_fn = __open_map[extension] 42 | mode = __mode_map[mode] if extension else mode 43 | return open_fn(filename, mode, **kwargs) 44 | 45 | -------------------------------------------------------------------------------- /UliEngineering/Utils/Concurrency.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Concurrency utilities 4 | """ 5 | import concurrent.futures 6 | import os 7 | import queue 8 | 9 | __all__ = ["QueuedThreadExecutor"] 10 | 11 | class QueuedThreadExecutor(concurrent.futures.ThreadPoolExecutor): 12 | """ 13 | In contrast to the normal ThreadPoolExecutor, this executor has 14 | the advantage of having a configurable queue size, 15 | enabling more efficient processing especially with irregular, 16 | slow or asynchronous queue feeders 17 | """ 18 | def __init__(self, nthreads=None, queue_size=100): 19 | if nthreads is None: 20 | nthreads = os.cpu_count() or 4 21 | super().__init__(nthreads) 22 | self._work_queue = queue.Queue(queue_size) -------------------------------------------------------------------------------- /UliEngineering/Utils/Iterable.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Utilities for iterables 4 | """ 5 | import collections 6 | import collections.abc 7 | 8 | __all__ = ["PeekableIteratorWrapper", "ListIterator", "skip_first"] 9 | 10 | class ListIterator(object): 11 | """ 12 | Takes an iterable (like a list) 13 | and exposes a generator-like interface. 14 | 15 | The given iterable must support len() 16 | and index-based access 17 | for this algorithm to work. 18 | 19 | Equivalent to (v for v in lst) 20 | except calling len() reveals 21 | how many values are left to iterate. 22 | 23 | Use .index to access the current index. 24 | """ 25 | def __init__(self, lst): 26 | self.index = 0 27 | self._lst = lst 28 | self._remaining = len(self._lst) 29 | 30 | def __iter__(self): 31 | return self 32 | 33 | def __next__(self): 34 | if self._remaining <= 0: 35 | raise StopIteration 36 | v = self._lst[self.index] 37 | self.index += 1 38 | self._remaining -= 1 39 | return v 40 | 41 | def __len__(self): 42 | """Remaining values""" 43 | return self._remaining 44 | 45 | 46 | 47 | 48 | def iterable_to_iterator(it): 49 | """ 50 | Given an iterable (like a list), generates 51 | an iterable out 52 | """ 53 | 54 | class PeekableIteratorWrapper(object): 55 | """ 56 | Wraps an iterator and provides the additional 57 | capability of 'peeking' and un-getting values. 58 | 59 | Works by storing un-got values in a buffer 60 | that is emptied on call to next() before 61 | touching the child iterator. 62 | 63 | The buffer is managed in a stack-like manner 64 | (LIFO) so you can un-get multiple values. 65 | """ 66 | def __init__(self, child): 67 | """ 68 | Initialize a PeekableIteratorWrapper 69 | with a given child iterator 70 | """ 71 | self.buffer = [] 72 | self.child = child 73 | 74 | def __iter__(self): 75 | return self 76 | 77 | def __next__(self): 78 | if len(self.buffer) > 0: 79 | return self.buffer.pop() 80 | return next(self.child) 81 | 82 | def __len__(self): 83 | """ 84 | Returns len(child). Only supported 85 | if child support len(). 86 | """ 87 | return len(self.child) 88 | 89 | def has_next(self): 90 | """ 91 | Returns False only if the next call to next() 92 | will raise StopIteration. 93 | 94 | This causes the next value to be generated from 95 | the child generator (and un-got) if there are no 96 | values in the buffer 97 | """ 98 | if len(self.buffer) > 0: 99 | return True 100 | else: 101 | try: 102 | v = next(self) 103 | self.unget(v) 104 | return True 105 | except StopIteration: 106 | return False 107 | 108 | 109 | def unget(self, v): 110 | """ 111 | Un-gets v so that v will be returned 112 | on the next call to __next__ (unless 113 | another value is un-got after this). 114 | """ 115 | self.buffer.append(v) 116 | 117 | def peek(self): 118 | """ 119 | Get the next value without removing it from 120 | the iterator. 121 | 122 | Note: Multiple subsequent calls to peek() 123 | without any calls to __next__() in between 124 | will return the same value. 125 | """ 126 | val = next(self) 127 | self.unget(val) 128 | return val 129 | 130 | def skip_first(it): 131 | """ 132 | Skip the first element of an Iterator or Iterable, 133 | like a Generator or a list. 134 | 135 | This will always return a generator or raise TypeError() 136 | in case the argument's type is not compatible 137 | """ 138 | if isinstance(it, collections.abc.Iterator): 139 | try: 140 | next(it) 141 | yield from it 142 | except StopIteration: 143 | return 144 | elif isinstance(it, collections.abc.Iterable): 145 | yield from skip_first(it.__iter__()) 146 | else: 147 | raise TypeError(f"You must pass an Iterator or an Iterable to skip_first(), but you passed {it}") 148 | 149 | -------------------------------------------------------------------------------- /UliEngineering/Utils/JSON.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf8 -*- 3 | """ 4 | Utilities for JSON encoding and decoding 5 | """ 6 | import json 7 | import numpy as np 8 | 9 | 10 | class NumPyEncoder(json.JSONEncoder): 11 | """ 12 | A JSON encoder that is capable of encoding NumPy ndarray objects. 13 | """ 14 | def default(self, obj): 15 | if isinstance(obj, np.ndarray): 16 | return obj.tolist() 17 | elif isinstance(obj, np.generic): # Generic scalars 18 | return obj.item() 19 | # Let the base class default method raise the TypeError 20 | raise TypeError("Unserializable object {} of type {}".format( 21 | obj, type(obj))) 22 | -------------------------------------------------------------------------------- /UliEngineering/Utils/Parser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | __all__ = ["parse_int_or_float", "try_parse_int_or_float"] 4 | 5 | def parse_int_or_float(s): 6 | """ 7 | Try to parse the given string 8 | as int, and if that fail, as float. 9 | 10 | If the parsing as float fails, raises ValueError. 11 | """ 12 | try: 13 | return int(s) 14 | except ValueError: 15 | return float(s) 16 | 17 | 18 | def try_parse_int_or_float(s): 19 | """ 20 | Try to parse the given string 21 | as int, and if that fail, as float. 22 | 23 | If the parsing as float fails, returns the string. 24 | """ 25 | try: 26 | return int(s) 27 | except ValueError: 28 | try: 29 | return float(s) 30 | except ValueError: 31 | return s 32 | -------------------------------------------------------------------------------- /UliEngineering/Utils/Range.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from collections import namedtuple 3 | from UliEngineering.EngineerIO import normalize_numeric, format_value 4 | 5 | __all__ = ["ValueRange", "normalize_minmax_tuple"] 6 | 7 | _ValueRange = namedtuple("ValueRange", ["min", "max", "unit"]) 8 | 9 | class ValueRange(_ValueRange): 10 | def __new__(cls, min, max, unit=None, significant_digits=4): 11 | self = super(ValueRange, cls).__new__(cls, min, max, unit) 12 | self.significant_digits = significant_digits 13 | return self 14 | 15 | def __repr__(self): 16 | return "ValueRange('{}', '{}')".format( 17 | format_value(self.min, self.unit, significant_digits=self.significant_digits), 18 | format_value(self.max, self.unit, significant_digits=self.significant_digits) 19 | ) 20 | 21 | @property 22 | def minmax(self): 23 | """Return (min, max). Utility e.g. for unpacking a ValueRange ignoring Unit""" 24 | return (self.min, self.max) 25 | 26 | def normalize_minmax_tuple(arg, name="field"): 27 | """ 28 | Interprets arg either a single +- value or as 29 | a 2-tuple of + and - values. 30 | All vaues 31 | 32 | If arg is a tuple: 33 | Return ValueRange(arg[0], arg[1]) (strings are normalized) 34 | Else: 35 | Return ValueRange(-arg, +arg) (strings are normalized) 36 | 37 | name is for debugging purposes and shown in the exception string 38 | """ 39 | # Parse coefficient and compute min & max factors 40 | if isinstance(arg, tuple): 41 | # Check length 2 42 | if len(arg) != 2: 43 | raise ValueError("If {} is given as a tuple, it must have length 2. {} is {}".format(name, name, arg)) 44 | # Parse tuple 45 | min_value = normalize_numeric(arg[0]) 46 | max_value = normalize_numeric(arg[1]) 47 | else: 48 | arg = normalize_numeric(arg) 49 | min_value = -arg 50 | max_value = arg 51 | return ValueRange(min_value, max_value) 52 | -------------------------------------------------------------------------------- /UliEngineering/Utils/Slice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Slice utilities 4 | """ 5 | 6 | __all__ = ["shift_slice"] 7 | 8 | def shift_slice(slc, by=0): 9 | """ 10 | Shift the given slice by index positions. 11 | Does not take into account the step size of the slice. 12 | """ 13 | return slice(slc.start + by, slc.stop + by, slc.step) -------------------------------------------------------------------------------- /UliEngineering/Utils/String.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | String utilities and algorithms 4 | """ 5 | 6 | __all__ = ["split_nth", "suffix_list", "partition_at_numeric_to_nonnumeric_boundary"] 7 | 8 | from typing import List, Tuple 9 | import re 10 | 11 | def split_nth(s, delimiter=",", nth=1): 12 | """ 13 | Like s.split(delimiter), but only returns the nth string of split's return array. 14 | Other strings or the split list itself are not generated. 15 | 16 | Using this function is ONLY recommended (because it's ONLY faster) 17 | if the string contains MANY delimiters (multiple hundreds). 18 | Else, use s.split(delimiter)[n - 1] 19 | 20 | Throws ValueError if the nth delimiter has not been found. 21 | """ 22 | if nth <= 0: 23 | raise ValueError("Invalid nth parameter: Must be >= 0 but value is {0}".format(nth)) 24 | startidx = 0 25 | # Startidx is 0 if we want the first field 26 | if nth > 1: 27 | for _ in range(nth - 1): 28 | startidx = s.index(delimiter, startidx + 1) 29 | startidx += 1 # Do not include the delimiter 30 | # Determine end index 31 | endidx = s.find(delimiter, startidx) 32 | if endidx == -1: # Not found -> the last part of the string 33 | endidx = None # Take rest of string 34 | return s[startidx:endidx] 35 | 36 | def suffix_list(s: str) -> List[str]: 37 | """ 38 | Return all suffixes for a string, including the string itself, 39 | in order of ascending length. 40 | 41 | Example: "foobar" => ['r', 'ar', 'bar', 'obar', 'oobar', 'foobar'] 42 | """ 43 | return [s[-i:] for i in range(1, len(s) + 1)] 44 | 45 | _numeric_to_nonnumeric_boundary_regex = re.compile(r"([-\.\d]+)([^\d\.]+)") 46 | 47 | def partition_at_numeric_to_nonnumeric_boundary(s: str) -> Tuple[str, str]: 48 | """ 49 | Partition a string at the first numeric->non-numeric boundary. 50 | Returns a tuple of two strings. 51 | 52 | Examples: 53 | * "foo.123bar" => ("foo.123", "bar") 54 | * "123s" => ("123", "s") 55 | * "123" => ("123", "") 56 | * "123.456km" => ("123.456", "km") 57 | * "foo" => ("foo", "") 58 | * "foo1bar" => ("foo1", "bar") 59 | """ 60 | m = _numeric_to_nonnumeric_boundary_regex.search(s) 61 | if m is None: 62 | # No such boundary found 63 | return s, "" 64 | return s[:m.span(1)[1]], s[m.span(1)[1]:] 65 | -------------------------------------------------------------------------------- /UliEngineering/Utils/Temporary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Utilities regarding temporary storage 5 | """ 6 | import tempfile 7 | import shutil 8 | import os 9 | 10 | class AutoDeleteTempfileGenerator(object): 11 | """ 12 | A wrapper for temporary files and directories that are automatically automatically 13 | deleted once this class is deleted or deleteAll() is called. 14 | 15 | This is not comparable to tempfile.TemporaryFile as the TemporaryFile instance 16 | will not guaranteed to be visible in the filesystem and is immediately removed 17 | once closed. This class will only delete files or directories upon request or upon 18 | garbage collection. 19 | 20 | Ensure this class does not go out of scope without other references to it unless 21 | you don't want to use the files any more. 22 | """ 23 | def __init__(self): 24 | self.tempdirs = [] 25 | self.tempfiles = [] 26 | 27 | def __del__(self): 28 | self.delete_all() 29 | 30 | def mkstemp(self, suffix='', prefix='tmp', dir=None): 31 | """Same as tempfile.mktemp(), but creates a file managed by this class instance""" 32 | handle, fname = tempfile.mkstemp(suffix, prefix, dir) 33 | self.tempfiles.append(fname) 34 | return (handle, fname) 35 | 36 | def mkftemp(self, suffix='', prefix='tmp', dir=None, mode='w'): 37 | """ 38 | Wrapper for self.mkstemp() that opens the OS-level file handle 39 | as a normal Python handle with the given mode 40 | """ 41 | handle, fname = self.mkstemp(suffix, prefix, dir) 42 | handle = os.fdopen(handle, mode) 43 | return (handle, fname) 44 | 45 | def mkdtemp(self, suffix='', prefix='tmp', dir=None): 46 | """Same as tempfile.mkdtemp(), but creates a file managed by this class instance""" 47 | fname = tempfile.mkdtemp(suffix, prefix, dir) 48 | self.tempdirs.append(fname) 49 | return fname 50 | 51 | def delete_all(self): 52 | """ 53 | Force-delete all files and directories created by this instance. 54 | The class instance may be used without restriction after this call 55 | """ 56 | # 57 | for filename in self.tempfiles: 58 | if os.path.isfile(filename): 59 | os.remove(filename) 60 | # Remove directories via shutil 61 | for tempdir in self.tempdirs: 62 | if os.path.isdir(tempdir) or os.path.isfile(tempdir): 63 | shutil.rmtree(tempdir) 64 | # Remove files from list 65 | self.tempfiles = [] 66 | self.tempdirs = [] 67 | -------------------------------------------------------------------------------- /UliEngineering/Utils/ZIP.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ 3 | Utilities for ZIP files 4 | """ 5 | import io 6 | import os.path 7 | import zipfile 8 | from .Files import list_recursive 9 | 10 | __all__ = ["create_zip_from_directory", "list_zip", "read_from_zip"] 11 | 12 | def create_zip_from_directory(zippath, directory, include_rootdir=True): 13 | """ 14 | Create a ZIP file from a directory that exist 15 | on the filesystem. Adds all files recursively, 16 | naming them correctly. 17 | 18 | Parameters 19 | ---------- 20 | zippath : path-like 21 | The path of the ZIP file to write 22 | directory : path-like 23 | The directory to compress 24 | include_rootdir : bool 25 | if True, the basename of the directory is prepended 26 | to each filename in the ZIP (i.e. when running unzip 27 | on the ZIP, one directory is extracted) 28 | """ 29 | basename = os.path.basename(directory) 30 | with zipfile.ZipFile(zippath, mode="w") as zipout: 31 | # Find files in directory 32 | for filename in list_recursive(directory, relative=True): 33 | filepath = os.path.join(directory, filename) 34 | # Write with custom name 35 | zipout.write(filepath, 36 | os.path.join(basename, filename) 37 | if include_rootdir else filename) 38 | 39 | def list_zip(zippath): 40 | """ 41 | Get a list of entries in the ZIP. 42 | Equivalent to calling .namelist() on the 43 | opened ZIP file. 44 | """ 45 | with zipfile.ZipFile(zippath) as zipin: 46 | return zipin.namelist() 47 | 48 | def read_from_zip(zippath, filepaths, binary=True): 49 | """ 50 | Read one or multiple files from a ZIP, copying their contents to memory. 51 | 52 | Parameters 53 | ---------- 54 | zippath : path-like 55 | The path of the ZIP file 56 | filepath : str or iterable of strings 57 | The path of the file inside the ZIP 58 | Multiple paths allowed (=> list is returned) 59 | binary : bool 60 | If True, returns a io.BytesIO(). 61 | If False, returns a io.StringIO() 62 | 63 | Returns 64 | ------- 65 | If filepath is a string, a single file-like object (in-memory). 66 | If filepath is any other iterable, a list of file-like in-memory objs. 67 | """ 68 | iof = io.BytesIO if binary else io.StringIO 69 | # Handle single file using the same code as multiple files 70 | single_file = isinstance(filepaths, str) 71 | filepaths = [filepaths] if single_file else filepaths 72 | # Actually 73 | with zipfile.ZipFile(zippath) as thezip: 74 | # Read multiple files 75 | iobufs = [] 76 | for file in filepaths: 77 | with thezip.open(file) as inf: 78 | iobufs.append(iof(inf.read())) 79 | # Return result 80 | return iobufs[0] if single_file else iobufs 81 | -------------------------------------------------------------------------------- /UliEngineering/Utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from .Temporary import AutoDeleteTempfileGenerator 3 | from .JSON import NumPyEncoder -------------------------------------------------------------------------------- /UliEngineering/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | -------------------------------------------------------------------------------- /coverage.ini: -------------------------------------------------------------------------------- 1 | [run] 2 | omit = tests/* 3 | -------------------------------------------------------------------------------- /coverage.rc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = UliEngineering 3 | branch = True 4 | 5 | [report] 6 | # Regexes for lines to exclude from consideration 7 | exclude_lines = 8 | # Have to re-enable the standard pragma 9 | pragma: no cover 10 | 11 | # Don't complain if tests don't hit defensive assertion code: 12 | raise AssertionError 13 | raise NotImplementedError 14 | 15 | # Don't complain if non-runnable code isn't run: 16 | if 0: 17 | if __name__ == .__main__.: 18 | 19 | ignore_errors = True -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "UliEngineering" 3 | version = "0.4.32" 4 | description = "Computational tools for electronics engineering" 5 | authors = ["Uli Köhler "] 6 | license = "Apache-2.0" 7 | homepage = "https://techoverflow.net/" 8 | readme = "README.md" 9 | keywords = ["electronics", "engineering", "computational tools"] 10 | packages = [ 11 | { include = "UliEngineering" }, 12 | { include = "UliEngineering/*" }, 13 | ] 14 | classifiers = [ 15 | "Development Status :: 4 - Beta", 16 | "Intended Audience :: Developers", 17 | "Intended Audience :: Science/Research", 18 | "Intended Audience :: Education", 19 | "Intended Audience :: Information Technology", 20 | "License :: DFSG approved", 21 | "License :: OSI Approved :: Apache Software License", 22 | "Programming Language :: Python", 23 | "Programming Language :: Python :: 3", 24 | "Topic :: Scientific/Engineering", 25 | "Topic :: Scientific/Engineering :: Bio-Informatics", 26 | "Topic :: Scientific/Engineering :: Information Analysis", 27 | "Topic :: Scientific/Engineering :: Physics", 28 | "Topic :: Scientific/Engineering :: Information Analysis" 29 | ] 30 | 31 | [tool.poetry.dependencies] 32 | python = ">=3.0" 33 | numpy = ">=1.5" 34 | toolz = ">=0.5" 35 | 36 | [tool.poetry.dev-dependencies] 37 | coverage = "*" 38 | mock = "*" 39 | parameterized = "*" 40 | 41 | [build-system] 42 | requires = ["poetry-core>=1.0.0"] 43 | build-backend = "poetry.core.masonry.api" 44 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = Test*.py -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy >= 1.5 2 | scipy >= 0.5 3 | toolz 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | with-coverage = 1 3 | cover-package = UliEngineering 4 | -------------------------------------------------------------------------------- /tests/Economics/TestInterest.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from UliEngineering.Economics.Interest import * 4 | import unittest 5 | 6 | class TestEquivalentInterest(unittest.TestCase): 7 | def test_yearly_interest_to_equivalent_monthly_interest(self): 8 | self.assertAlmostEqual(interest_apply_multiple_times( 9 | yearly_interest_to_equivalent_monthly_interest(0), 1), 0.0) 10 | self.assertAlmostEqual(interest_apply_multiple_times( 11 | yearly_interest_to_equivalent_monthly_interest(0.22), 12), 0.22) 12 | self.assertAlmostEqual(interest_apply_multiple_times( 13 | yearly_interest_to_equivalent_monthly_interest(0.33), 12), 0.33) 14 | 15 | def test_yearly_interest_to_equivalent_daily_interest(self): 16 | self.assertAlmostEqual(interest_apply_multiple_times( 17 | yearly_interest_to_equivalent_daily_interest(0), 1), 0.0) 18 | self.assertAlmostEqual(interest_apply_multiple_times( 19 | yearly_interest_to_equivalent_daily_interest(0.22), 365.25), 0.22) 20 | self.assertAlmostEqual(interest_apply_multiple_times( 21 | yearly_interest_to_equivalent_daily_interest(0.33), 365.25), 0.33) 22 | 23 | class TestInterestApplyMultipleTimes(unittest.TestCase): 24 | def test_interest_apply_multiple_times(self): 25 | self.assertAlmostEqual(interest_apply_multiple_times(0.22, 0), 0) 26 | self.assertAlmostEqual(interest_apply_multiple_times(0.22, 1), 0.22) 27 | self.assertAlmostEqual(interest_apply_multiple_times(0.22, 2), 1.22**2-1) 28 | 29 | self.assertAlmostEqual(interest_apply_multiple_times(0.33, 0), 0) 30 | self.assertAlmostEqual(interest_apply_multiple_times(0.33, 1), 0.33) 31 | self.assertAlmostEqual(interest_apply_multiple_times(0.33, 2), 1.33**2-1) 32 | 33 | -------------------------------------------------------------------------------- /tests/Economics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Economics/__init__.py -------------------------------------------------------------------------------- /tests/Electronics/TestCrystal.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from UliEngineering.Electronics.Crystal import * 4 | from UliEngineering.EngineerIO import auto_format 5 | import unittest 6 | 7 | class TestCrystal(unittest.TestCase): 8 | def test_load_capacitor(self): 9 | # Example from https://blog.adafruit.com/2012/01/24/choosing-the-right-crystal-and-caps-for-your-design/ 10 | self.assertEqual(auto_format(load_capacitors, "6 pF", cpin="3 pF", cstray="2pF"), '5.00 pF') 11 | 12 | def test_actual_load_capacitance(self): 13 | self.assertEqual(auto_format(actual_load_capacitance, "5 pF", cpin="3 pF", cstray="2pF"), '6.00 pF') 14 | 15 | def test_deviation(self): 16 | self.assertEqual(auto_format(crystal_deviation_seconds_per_minute, "20 ppm"), '1.20 ms') 17 | self.assertEqual(auto_format(crystal_deviation_seconds_per_hour, "20 ppm"), '72.0 ms') 18 | self.assertEqual(auto_format(crystal_deviation_seconds_per_day, "20 ppm"), '1.73 s') 19 | self.assertEqual(auto_format(crystal_deviation_seconds_per_month, "20 ppm"), '53.6 s') 20 | self.assertEqual(auto_format(crystal_deviation_seconds_per_year, "20 ppm"), '631 s') 21 | -------------------------------------------------------------------------------- /tests/Electronics/TestHysteresis.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Electronics.Hysteresis import * 5 | import unittest 6 | 7 | class TestHysteresis(unittest.TestCase): 8 | def test_hysteresis_thresholds(self): 9 | # 1e300: Near-infinite resistor should not affect ratio 10 | assert_allclose(hysteresis_threshold_ratios(1e3, 1e3, 1e300), (0.5, 0.5)) 11 | assert_allclose(hysteresis_threshold_voltages(1e3, 1e3, 1e300, 5.0), (2.5, 2.5)) 12 | assert_allclose(hysteresis_threshold_factors(1e3, 1e3, 1e300), (1.0, 1.0)) 13 | # More realistic values 14 | assert_allclose(hysteresis_threshold_ratios(1e3, 1e3, 1e3), (0.3333333333, 0.6666666666)) 15 | assert_allclose(hysteresis_threshold_voltages(1e3, 1e3, 1e3, 5.0), (0.3333333333*5., 0.6666666666*5.)) 16 | assert_allclose(hysteresis_threshold_factors(1e3, 1e3, 1e3), (0.3333333333/.5, 0.6666666666/.5)) 17 | 18 | def test_hysteresis_opendrain(self): 19 | # 1e300: Near-infinite resistor should not affect ratio 20 | assert_allclose(hysteresis_threshold_factors_opendrain(1e3, 1e3, 1e300), (1.0, 1.0)) 21 | assert_allclose(hysteresis_threshold_voltages_opendrain(1e3, 1e3, 1e300, 5.0), (2.5, 2.5)) 22 | assert_allclose(hysteresis_threshold_ratios_opendrain(1e3, 1e3, 1e300), (0.5, 0.5)) 23 | # More realistic values 24 | assert_allclose(hysteresis_threshold_factors_opendrain(1e3, 1e3, 1e3), (0.3333333333/.5, 1.)) 25 | assert_allclose(hysteresis_threshold_ratios_opendrain(1e3, 1e3, 1e3), (0.3333333333, 0.5)) 26 | assert_allclose(hysteresis_threshold_voltages_opendrain(1e3, 1e3, 1e3, 5.0), (0.3333333333*5., 0.5*5.)) 27 | 28 | def test_hysteresis_resistor(self): 29 | assert_allclose(hysteresis_resistor(1e3, 1e3, 0.1), 4500) 30 | -------------------------------------------------------------------------------- /tests/Electronics/TestLED.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Electronics.LED import * 5 | from UliEngineering.Exceptions import OperationImpossibleException 6 | from UliEngineering.EngineerIO import auto_format 7 | import unittest 8 | import pytest 9 | 10 | class TestLEDSeriesResistors(unittest.TestCase): 11 | def test_led_series_resistor(self): 12 | # Example verified at http://www.elektronik-kompendium.de/sites/bau/1109111.htm 13 | # Also verified at https://www.digikey.com/en/resources/conversion-calculators/conversion-calculator-led-series-resistor 14 | assert_approx_equal(led_series_resistor(12.0, 20e-3, 1.6), 520.) 15 | assert_approx_equal(led_series_resistor("12V", "20 mA", "1.6V"), 520.) 16 | assert_approx_equal(led_series_resistor(12.0, 20e-3, LEDForwardVoltages.Red), 520.) 17 | 18 | def test_led_series_resistor_invalid(self): 19 | # Forward voltage too high for supply voltage 20 | with self.assertRaises(OperationImpossibleException): 21 | assert_approx_equal(led_series_resistor("1V", "20 mA", "1.6V"), 520.) 22 | 23 | def test_led_series_resistor_power(self): 24 | # Values checked using https://www.pollin.de/led-vorwiderstands-rechner 25 | self.assertEqual(auto_format(led_series_resistor_power, "5V", "20mA", "2V"), "60.0 mW") 26 | self.assertEqual(auto_format(led_series_resistor_power, "5V", "20mA", "3V"), "40.0 mW") 27 | self.assertEqual(auto_format(led_series_resistor_power, "5V", "10mA", "2V"), "30.0 mW") 28 | self.assertEqual(auto_format(led_series_resistor_power, "5V", "10mA", "3V"), "20.0 mW") 29 | self.assertEqual(auto_format(led_series_resistor_power, "12V", "10mA", "2V"), "100 mW") 30 | 31 | def test_led_series_resistor_power_invalid(self): 32 | with pytest.raises(OperationImpossibleException): 33 | led_series_resistor_power("2V", "20mA", "3V") 34 | 35 | def test_led_series_resistor_maximum_current(self): 36 | # Test with valid inputs 37 | # Verified using https://www.omnicalculator.com/physics/ohms-law 38 | assert_approx_equal(led_series_resistor_maximum_current(10, 0.25), 0.1581139) 39 | assert_approx_equal(led_series_resistor_maximum_current(1, 2.56), 1.6) 40 | -------------------------------------------------------------------------------- /tests/Electronics/TestLogarithmicAmplifier.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import numpy as np 3 | 4 | from UliEngineering.Electronics.LogarithmicAmplifier import ( 5 | logarithmic_amplifier_output_voltage, 6 | logarithmic_amplifier_input_current 7 | ) 8 | 9 | class TestLogarithmicAmplifier(unittest.TestCase): 10 | def test_logarithmic_amplifier_output_voltage(self): 11 | # Test with known values 12 | ipd = 1e-6 # 1 µA 13 | gain = 0.2 # 0.2 V/decade 14 | intercept = 1e-9 # 1 nA 15 | expected_output_voltage = gain * np.log10(ipd / intercept) 16 | self.assertAlmostEqual( 17 | logarithmic_amplifier_output_voltage(ipd, gain, intercept), 18 | expected_output_voltage, 19 | places=6 20 | ) 21 | 22 | 23 | def test_logarithmic_amplifier_output_voltage_ad5303(self): 24 | """Example from AD5303 datasheet, with amperes rather than watts""" 25 | # Test with known values 26 | ipd = "3mA" 27 | gain = "200mV" # /decade 28 | intercept = "110 pA" 29 | expected_output_voltage = 1.487 # V, from datasheet example 30 | self.assertAlmostEqual( 31 | logarithmic_amplifier_output_voltage(ipd, gain, intercept), 32 | expected_output_voltage, 33 | places=3 # Datasheet gives 3 digits only 34 | ) 35 | 36 | def test_logarithmic_amplifier_input_current(self): 37 | # Test with known values 38 | vout = 0.6 # 0.6 V 39 | gain = 0.2 # 0.2 V/decade 40 | intercept = 1e-9 # 1 nA 41 | expected_input_current = intercept * np.power(10, vout / gain) 42 | self.assertAlmostEqual( 43 | logarithmic_amplifier_input_current(vout, gain, intercept), 44 | expected_input_current, 45 | places=6 46 | ) 47 | 48 | 49 | def test_logarithmic_amplifier_input_current_ad5303(self): 50 | """Example from AD5303 datasheet, with amperes rather than watts""" 51 | # Test with known values 52 | vout = "1.487 V" 53 | gain = "200mV" # /decade 54 | intercept = "110 pA" 55 | expected_input_current = 3e-3 # A, from datasheet example 56 | self.assertAlmostEqual( 57 | logarithmic_amplifier_input_current(vout, gain, intercept), 58 | expected_input_current, 59 | places=3 60 | ) 61 | 62 | if __name__ == '__main__': 63 | unittest.main() -------------------------------------------------------------------------------- /tests/Electronics/TestMOSFET.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Electronics.MOSFET import * 5 | from UliEngineering.Exceptions import OperationImpossibleException 6 | from UliEngineering.EngineerIO import auto_format 7 | import unittest 8 | 9 | class TestLEDSeriesResistors(unittest.TestCase): 10 | def test_mosfet_gate_charge_losses(self): 11 | # Example verified at http://www.elektronik-kompendium.de/sites/bau/1109111.htm 12 | # Also verified at https://www.digikey.com/en/resources/conversion-calculators/conversion-calculator-led-series-resistor 13 | assert_approx_equal(mosfet_gate_charge_losses(39.0e-9, 10, 300e3), 0.117) 14 | assert_approx_equal(mosfet_gate_charge_losses("39nC", "10V", "300 kHz"), 0.117) 15 | 16 | def test_mosfet_gate_capacitance_from_gate_charge(self): 17 | assert_approx_equal(mosfet_gate_capacitance_from_gate_charge(39.0e-9, 10), 3.9e-9) 18 | assert_approx_equal(mosfet_gate_capacitance_from_gate_charge("39nC", "10V"), 3.9e-9) 19 | -------------------------------------------------------------------------------- /tests/Electronics/TestOpAmp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Electronics.OpAmp import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | import numpy as np 8 | 9 | class TestOpAmp(unittest.TestCase): 10 | def test_summing_amplifier_noninv(self): 11 | v = summing_amplifier_noninv("2.5V", "500mV", "1kΩ", "1kΩ", "1kΩ", "1kΩ") 12 | assert_approx_equal(v, 3.0) 13 | v = summing_amplifier_noninv(2.5, 0.5, 1e3, 1e3, 300, 1e3) 14 | assert_approx_equal(v, 6.5) 15 | self.assertEqual(auto_format(summing_amplifier_noninv, 2.5, 0.5, 1e3, 1e3, 300, 1e3), "6.50 V") 16 | 17 | def test_noninverting_amplifier_gain(self): 18 | # Test cases checked using https://circuitdigest.com/calculators/op-amp-gain-calculator 19 | assert_approx_equal(noninverting_amplifier_gain("1kΩ", "1kΩ"), 2.0) 20 | assert_approx_equal(noninverting_amplifier_gain(1e3, 1e3), 2.0) 21 | assert_approx_equal(noninverting_amplifier_gain("2kΩ", "1kΩ"), 3.0) 22 | assert_approx_equal(noninverting_amplifier_gain(2e3, 1e3), 3.0) 23 | # Check auto_format 24 | self.assertEqual(auto_format(noninverting_amplifier_gain, 2e3, 1e3), "3.00 V/V") 25 | self.assertEqual(auto_format(noninverting_amplifier_gain, 1e3, 1e3), "2.00 V/V") 26 | self.assertEqual(auto_format(noninverting_amplifier_gain, "1kΩ", "1kΩ"), "2.00 V/V") 27 | # Test case with not-as-round numbers 28 | assert_approx_equal(noninverting_amplifier_gain("390kΩ", "15kΩ"), 27.0) 29 | assert_approx_equal(noninverting_amplifier_gain(390e3, 15e3), 27.0) 30 | # Test case with infinity resistor to GND (i.e. unity gain) 31 | assert_approx_equal(noninverting_amplifier_gain("1kΩ", np.inf), 1.0) 32 | assert_approx_equal(noninverting_amplifier_gain(1e3, np.inf), 1.0) -------------------------------------------------------------------------------- /tests/Electronics/TestPower.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | 5 | import numpy as np 6 | from numpy.testing import assert_allclose 7 | 8 | from UliEngineering.Electronics.Power import * 9 | 10 | 11 | class TestPower(unittest.TestCase): 12 | def test_current_by_power(self): 13 | assert_allclose(current_by_power(25), 25 / 230, atol=1e-15) 14 | assert_allclose(current_by_power(25, 230), 25 / 230, atol=1e-15) 15 | assert_allclose(current_by_power(25, 100), 25 / 100, atol=1e-15) 16 | assert_allclose(current_by_power("25 W", "100 V"), 25 / 100, atol=1e-15) 17 | 18 | def test_power_by_current_and_voltage(self): 19 | assert_allclose(power_by_current_and_voltage(1, 10), 1*10, atol=1e-15) 20 | assert_allclose(power_by_current_and_voltage(1, 230), 1 * 230, atol=1e-15) 21 | assert_allclose(power_by_current_and_voltage(0.2, 230), 0.2 * 230, atol=1e-15) 22 | assert_allclose(power_by_current_and_voltage(0.2), 0.2 * 230, atol=1e-15) 23 | assert_allclose(power_by_current_and_voltage("0.2 A", "230 V"), 0.2 * 230, atol=1e-15) 24 | assert_allclose(power_by_current_and_voltage(0.2, "230 V"), 0.2 * 230, atol=1e-15) 25 | 26 | -------------------------------------------------------------------------------- /tests/Electronics/TestPowerFactor.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_allclose 4 | from UliEngineering.Electronics.PowerFactor import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import numpy as np 7 | import unittest 8 | 9 | class TestPowerFactor(unittest.TestCase): 10 | def test_power_factor_by_phase_angle(self): 11 | assert_allclose(power_factor_by_phase_angle(0.0), 1.0, atol=1e-15) 12 | assert_allclose(power_factor_by_phase_angle("0"), 1.0, atol=1e-15) 13 | assert_allclose(power_factor_by_phase_angle("0°"), 1.0, atol=1e-15) 14 | assert_allclose(power_factor_by_phase_angle("0°", unit="deg"), 1.0, atol=1e-15) 15 | assert_allclose(power_factor_by_phase_angle("0°", unit="degrees"), 1.0, atol=1e-15) 16 | assert_allclose(power_factor_by_phase_angle("0°", unit="rad"), 1.0, atol=1e-15) 17 | assert_allclose(power_factor_by_phase_angle("0°", unit="radiant"), 1.0, atol=1e-15) 18 | assert_allclose(power_factor_by_phase_angle("90°"), 0.0, atol=1e-15) 19 | assert_allclose(power_factor_by_phase_angle("90°", unit="deg"), 0.0, atol=1e-15) 20 | assert_allclose(power_factor_by_phase_angle(90), 0.0, atol=1e-15) 21 | assert_allclose(power_factor_by_phase_angle(90+360), 0.0, atol=1e-15) 22 | assert_allclose(power_factor_by_phase_angle(90-360), 0.0, atol=1e-15) 23 | assert_allclose(power_factor_by_phase_angle(np.pi/2., unit="rad"), 0.0, atol=1e-15) 24 | 25 | def test_power_factor_by_phase_angle_bad_unit(self): 26 | with self.assertRaises(ValueError): 27 | power_factor_by_phase_angle(unit="nosuchunit") 28 | 29 | -------------------------------------------------------------------------------- /tests/Electronics/TestReactance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Electronics.Reactance import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import numpy as np 7 | import unittest 8 | 9 | class TestNoiseDensity(unittest.TestCase): 10 | def test_capacitive_reactance(self): 11 | assert_approx_equal(capacitive_reactance("100 pF", "3.2 MHz"), 497.3592) 12 | assert_approx_equal(capacitive_reactance(100e-12, 3.2e6), 497.3592) 13 | assert_approx_equal(capacitive_reactance(100e-12, 3.2e6), 497.3592) 14 | self.assertEqual(auto_format(capacitive_reactance, "100 pF", "3.2 MHz"), "497 Ω") 15 | 16 | def test_inductive_reactance(self): 17 | assert_approx_equal(inductive_reactance("100 µH", "3.2 MHz"), 2010.619) 18 | assert_approx_equal(inductive_reactance(100e-6, 3.2e6), 2010.619) 19 | self.assertEqual(auto_format(inductive_reactance, "100 µH", "3.2 MHz"), "2.01 kΩ") 20 | 21 | def test_numpy_arrays(self): 22 | l = np.asarray([100e-6, 200e-6]) 23 | assert_allclose(inductive_reactance(l, 3.2e6), [2010.6193, 4021.23859659]) 24 | -------------------------------------------------------------------------------- /tests/Electronics/TestResistors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Electronics.Resistors import * 5 | from UliEngineering.EngineerIO import * 6 | import unittest 7 | import numpy as np 8 | 9 | class TestResistors(unittest.TestCase): 10 | def test_parallel_resistors(self): 11 | assert_approx_equal(parallel_resistors(1000.0, 1000.0), 500.0) 12 | assert_approx_equal(parallel_resistors(1000.0, 1000.0, 500.0), 250.0) 13 | assert_approx_equal(parallel_resistors("1kΩ", "1kΩ"), 500.0) 14 | 15 | def test_parallel_resistors_special_cases(self): 16 | assert_approx_equal(parallel_resistors(), np.inf) 17 | assert_approx_equal(parallel_resistors(0.0, 150.0), 0.) 18 | assert_approx_equal(parallel_resistors("0", 150.0), 0.) 19 | assert_approx_equal(parallel_resistors(150.0, "0k", 150.0), 0.) 20 | 21 | def test_series_resistors(self): 22 | assert_approx_equal(series_resistors(1000.0, 1000.0), 2000.0) 23 | assert_approx_equal(series_resistors(1000.0, 1000.0, 500.0), 2500.0) 24 | assert_approx_equal(series_resistors("1kΩ", "1kΩ"), 2000.0) 25 | 26 | def test_standard_resistors(self): 27 | self.assertTrue(len(list(standard_resistors())) > 500) 28 | 29 | def test_standard_resistors_in_range(self): 30 | resistors = standard_resistors_in_range(min_resistor="1Ω", max_resistor="10MΩ", sequence=e96) 31 | resistors2 = standard_resistors_in_range(min_resistor="1Ω", max_resistor="10MΩ", sequence=e96) 32 | self.assertEqual(resistors, resistors2) 33 | 34 | self.assertTrue(len(list(resistors)) > 500) 35 | self.assertEqual(len([resistor for resistor in resistors if resistor < 1 or resistor > 10e6]), 0) 36 | 37 | def test_find_nearest_resistor(self): 38 | self.assertEqual(nearest_resistor(5000, sequence=e48), 5110.0) 39 | self.assertEqual(nearest_resistor(4998), 4990.0) 40 | 41 | def test_current_through_resistor(self): 42 | assert_approx_equal(current_through_resistor("1k", "1V"), 1e-3) 43 | assert_approx_equal(current_through_resistor(1e3, 2), 2e-3) 44 | assert_approx_equal(current_through_resistor("1Ω", "2V"), 2) 45 | 46 | def test_resistor_by_voltage_and_current(self): 47 | assert_approx_equal(resistor_by_voltage_and_current("2.5 V", "1 uA"), 2.5e6) 48 | self.assertEqual(auto_format(resistor_by_voltage_and_current, "2.5 V", "1 uA"), "2.50 MΩ") 49 | 50 | -------------------------------------------------------------------------------- /tests/Electronics/TestTemperatureCoefficient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Electronics.TemperatureCoefficient import * 5 | from UliEngineering.EngineerIO import auto_format 6 | from UliEngineering.Utils.Range import ValueRange 7 | import numpy as np 8 | import unittest 9 | 10 | class TestTemperatureCoefficient(unittest.TestCase): 11 | def test_value_range_over_temperature_zero(self): 12 | # Test with simple ppm input 13 | self.assertEqual(str(value_range_over_temperature("1 kΩ", "0 ppm")), 14 | str(ValueRange(1000, 1000, "Ω")) 15 | ) 16 | 17 | def test_value_range_over_temperature1(self): 18 | # Test with simple ppm input 19 | self.assertEqual(str(value_range_over_temperature("1 kΩ", "100 ppm")), 20 | str(ValueRange(994, 1006, "Ω")) 21 | ) 22 | 23 | def test_value_range_over_temperature2(self): 24 | # Test with +- the same ppm input 25 | self.assertEqual(str(value_range_over_temperature("1 kΩ", ("-100 ppm", "100 ppm"))), 26 | str(ValueRange(994, 1006, "Ω")) 27 | ) 28 | # Test with ++ the same ppm input 29 | self.assertEqual(str(value_range_over_temperature("1 kΩ", ("+100 ppm", "+100 ppm"))), 30 | str(ValueRange(994, 1006, "Ω")) 31 | ) 32 | 33 | def test_value_range_over_temperature3(self): 34 | # Test with +- the same ppm input 35 | self.assertEqual(str(value_range_over_temperature("1 kΩ", ("0 ppm", "100 ppm"))), 36 | str(ValueRange(994, 1006, "Ω")) 37 | ) 38 | 39 | def test_value_range_over_temperature_percent(self): 40 | # Test with +- the same ppm input 41 | self.assertEqual(str(value_range_over_temperature("1 kΩ", "1 %")), 42 | str(ValueRange(350, 1650, "Ω")) 43 | ) 44 | # Test with +- the same ppm input 45 | self.assertEqual(str(value_range_over_temperature("1 kΩ", "1.006 %")), 46 | str(ValueRange(346, 1654, "Ω")) 47 | ) 48 | 49 | def test_value_range_over_temperature_tolerance(self): 50 | # Test with +- the same ppm input 51 | self.assertEqual(str(value_range_over_temperature("1 kΩ", "100 ppm", tolerance="1%")), 52 | str(ValueRange(984, 1017, "Ω")) 53 | ) 54 | # Test with ++ the same ppm input 55 | self.assertEqual(str(value_range_over_temperature("1 kΩ", ("-100 ppm", "+100 ppm"), tolerance=("-0%", "+1%"))), 56 | str(ValueRange(994, 1017, "Ω")) 57 | ) 58 | 59 | class TestValueAtTemperature(unittest.TestCase): 60 | def test_value_at_temperature(self): 61 | # Ref temp => zero difference 62 | assert_approx_equal(value_at_temperature("1 kΩ", "25 °C", "100 ppm"), 1000.0) 63 | # delta T = 10° => 10 * 100 ppm 64 | assert_approx_equal(value_at_temperature("1 kΩ", "35 °C", "100 ppm"), 1001.) 65 | # delta T = -10° => -10 * 100 ppm 66 | assert_approx_equal(value_at_temperature("1 kΩ", "15 °C", "100 ppm"), 999.) 67 | -------------------------------------------------------------------------------- /tests/Electronics/TestTolerance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Electronics.Tolerance import * 5 | from UliEngineering.EngineerIO import auto_format 6 | from UliEngineering.Utils.Range import ValueRange 7 | import numpy as np 8 | import unittest 9 | 10 | class TestValueRangeOverTolerance(unittest.TestCase): 11 | def test_value_range_over_tolerance(self): 12 | # Test with simple ppm input 13 | self.assertEqual(str(value_range_over_tolerance("1 kΩ", "1 %")), 14 | str(ValueRange(990, 1010, "Ω")) 15 | ) 16 | self.assertEqual(str(value_range_over_tolerance("1 kΩ", "1000 ppm")), 17 | str(ValueRange(999., 1001.0, "Ω")) 18 | ) -------------------------------------------------------------------------------- /tests/Electronics/TestVoltageDivider.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Electronics.VoltageDivider import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | 8 | class TestNoiseDensity(unittest.TestCase): 9 | def test_unloaded_ratio(self): 10 | assert_approx_equal(voltage_divider_ratio(1000.0, 1000.0), 0.5) 11 | # Top resistor has lower value => ratio < 0.5 12 | assert_approx_equal(voltage_divider_ratio(600, 1000.0), 0.625) 13 | 14 | def test_loaded_ratio(self): 15 | assert_approx_equal(voltage_divider_ratio(1000.0, 1000.0, 1e60), 0.5) 16 | assert_approx_equal(voltage_divider_ratio(1000.0, 1000.0, 1000.0), 0.6666666666666666) 17 | assert_approx_equal(voltage_divider_ratio("1kΩ", "1kΩ", "10 MΩ"), 0.500024998) 18 | 19 | def test_bottom_resistor_by_ratio(self): 20 | assert_approx_equal(bottom_resistor_by_ratio(1000.0, 0.5), 1000.0) 21 | assert_approx_equal(bottom_resistor_by_ratio(200.0, 5/6.0), 1000.0) 22 | self.assertEqual(auto_format(bottom_resistor_by_ratio, 1000.0, 0.5), "1000 Ω") 23 | self.assertEqual(auto_format(bottom_resistor_by_ratio, 400.0, 5/6.0), "2.00 kΩ") 24 | 25 | def test_top_resistor_by_ratio(self): 26 | assert_approx_equal(top_resistor_by_ratio(1000.0, 0.5), 1000.0) 27 | assert_approx_equal(top_resistor_by_ratio(1000.0, 5/6.0), 200.0) 28 | self.assertEqual(auto_format(top_resistor_by_ratio, 1000.0, 0.5), "1000 Ω") 29 | self.assertEqual(auto_format(top_resistor_by_ratio, 1000.0, 5/6.0), "200 Ω") 30 | 31 | def test_feedback_resistors(self): 32 | assert_approx_equal(feedback_top_resistor(1.8, 816e3, 0.8), 1020e3) 33 | assert_approx_equal(feedback_bottom_resistor(1.8, 1020e3, 0.8), 816e3) 34 | # Test string input 35 | assert_approx_equal(feedback_top_resistor("1.8 V", "816 kΩ", "0.8 V"), 1020e3) 36 | assert_approx_equal(feedback_bottom_resistor("1.8 V", "1020 kΩ", "0.8 V"), 816e3) 37 | 38 | def test_feedback_voltage(self): 39 | assert_approx_equal(feedback_actual_voltage(1020e3, 816e3, 0.8), 1.8) 40 | # String input 41 | assert_approx_equal(feedback_actual_voltage("1.02 MΩ", "816 kΩ", "0.8 V"), 1.8) 42 | 43 | def test_feedback_voltage_str(self): 44 | self.assertEqual(voltage_divider_power("250k", "1k", "230V").__repr__(), 45 | "VoltageDividerPower(top=210 mW, bottom=840 µW, total=211 mW)") 46 | assert_approx_equal(voltage_divider_power("250k", "1k", "230V").total, 47 | 0.2107569721115538) 48 | -------------------------------------------------------------------------------- /tests/Electronics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Electronics/__init__.py -------------------------------------------------------------------------------- /tests/Filesystem/TestHash.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import unittest 4 | from UliEngineering.Filesystem.Hash import * 5 | import tempfile 6 | import hashlib 7 | import os 8 | import shutil 9 | 10 | class TestHashFile(unittest.TestCase): 11 | def test_hash_file_native(self): 12 | with tempfile.NamedTemporaryFile(delete=False) as f: 13 | f.write(b"Hello, world!") 14 | file_path = f.name 15 | assert hash_file_native(file_path) == hashlib.sha256(b"Hello, world!").hexdigest() 16 | os.unlink(file_path) 17 | 18 | def test_hash_file_sha256(self): 19 | with tempfile.NamedTemporaryFile(delete=False) as f: 20 | f.write(b"Hello, world!") 21 | file_path = f.name 22 | self.assertEqual(hash_file_sha256(file_path), "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3") 23 | os.unlink(file_path) 24 | 25 | def test_hash_file_md5(self): 26 | with tempfile.NamedTemporaryFile(delete=False) as f: 27 | f.write(b"Hello, world!") 28 | file_path = f.name 29 | self.assertEqual(hash_file_md5(file_path), "6cd3556deb0da54bca060b4c39479839") 30 | os.unlink(file_path) 31 | 32 | def test_hash_file_sha1(self): 33 | with tempfile.NamedTemporaryFile(delete=False) as f: 34 | f.write(b"Hello, world!") 35 | file_path = f.name 36 | self.assertEqual(hash_file_sha1(file_path), "943a702d06f34599aee1f8da8ef9f7296031d699") 37 | os.unlink(file_path) 38 | 39 | class TestHashDirectory(unittest.TestCase): 40 | def setUp(self): 41 | self.tmpdir = tempfile.TemporaryDirectory() 42 | # Create two files in main directory 43 | with open(os.path.join(self.tmpdir.name, "file1.txt"), "wb") as f: 44 | f.write(b"Hello, world!") 45 | with open(os.path.join(self.tmpdir.name, "file2.txt"), "wb") as f: 46 | f.write(b"Goodbye, world!") 47 | # Create a single file in a subdirectory 48 | subdir = os.path.join(self.tmpdir.name, "subdir") 49 | os.mkdir(subdir) 50 | with open(os.path.join(subdir, "file3.txt"), "wb") as f: 51 | f.write(b"Hello again, world!") 52 | 53 | def tearDown(self): 54 | shutil.rmtree(self.tmpdir.name) 55 | 56 | def test_hash_directory_non_recursive(self): 57 | results = hash_directory(self.tmpdir.name, recursive=False, relative_paths=True) 58 | self.assertEqual(len(results), 2) 59 | self.assertIn(("file1.txt", "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"), results) 60 | self.assertIn(("file2.txt", "a6ab91893bbd50903679eb6f0d5364dba7ec12cd3ccc6b06dfb04c044e43d300"), results) 61 | 62 | def test_hash_directory_recursive(self): 63 | results = hash_directory(self.tmpdir.name, recursive=True, relative_paths=True) 64 | print(results) 65 | self.assertEqual(len(results), 3) 66 | self.assertIn(("file1.txt", "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"), results) 67 | self.assertIn(("file2.txt", "a6ab91893bbd50903679eb6f0d5364dba7ec12cd3ccc6b06dfb04c044e43d300"), results) 68 | self.assertIn((os.path.join("subdir", "file3.txt"), "ef42b2ddfd8608161d0943b6c5cc349bf7b8f63c1261e393a348ffd24877b5ef"), results) 69 | 70 | def test_hash_directory_absolute_paths(self): 71 | results = hash_directory(self.tmpdir.name, recursive=True, relative_paths=False) 72 | self.assertEqual(len(results), 3) 73 | self.assertIn((os.path.join(self.tmpdir.name, "file1.txt"), "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3"), results) 74 | self.assertIn((os.path.join(self.tmpdir.name, "file2.txt"), "a6ab91893bbd50903679eb6f0d5364dba7ec12cd3ccc6b06dfb04c044e43d300"), results) 75 | self.assertIn((os.path.join(self.tmpdir.name, "subdir", "file3.txt"), "ef42b2ddfd8608161d0943b6c5cc349bf7b8f63c1261e393a348ffd24877b5ef"), results) 76 | 77 | -------------------------------------------------------------------------------- /tests/Filesystem/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Filesystem/__init__.py -------------------------------------------------------------------------------- /tests/Math/Geometry/TestCircle.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Math.Geometry.Circle import * 5 | from parameterized import parameterized 6 | import numpy as np 7 | import unittest 8 | 9 | 10 | class TestCircle(unittest.TestCase): 11 | @parameterized.expand([ 12 | (0.0, ), 13 | (1.0, ), 14 | (4.0, ), 15 | (3.125, ), 16 | ]) 17 | def test_circle_area(self, radius): 18 | assert_approx_equal(circle_area(radius), np.pi*(radius**2)) 19 | assert_approx_equal(circle_area(f"{radius}"), np.pi*(radius**2)) 20 | 21 | @parameterized.expand([ 22 | (0.0, ), 23 | (1.0, ), 24 | (4.0, ), 25 | (3.125, ), 26 | ]) 27 | def test_circle_circumference(self, radius): 28 | assert_approx_equal(circle_circumference(radius), 2*np.pi*radius) 29 | assert_approx_equal(circle_circumference(f"{radius}"), 2*np.pi*radius) 30 | 31 | -------------------------------------------------------------------------------- /tests/Math/Geometry/TestCylinder.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from UliEngineering.Math.Geometry.Cylinder import * 4 | from parameterized import parameterized 5 | import concurrent.futures 6 | import numpy as np 7 | import datetime 8 | import unittest 9 | import math 10 | 11 | class TestCylinder(unittest.TestCase): 12 | @parameterized.expand([ 13 | # Wolfram Alpha as a reference 14 | (0.0, 3.0, 0.0), 15 | (3.0, 0.0, 0.0), 16 | (1.0, 1.0, math.pi), # "volume of cylinder radius 1 height 1" 17 | (4.0, 2.5, 125.664), 18 | (3.125, 44.15, 1354.51), # "volume of cylinder radius 3.125 height 44.15" 19 | ]) 20 | def test_cylinder_volume(self, radius, height, expected): 21 | self.assertAlmostEqual(cylinder_volume(radius, height), expected, delta=.025) 22 | self.assertAlmostEqual(cylinder_volume(f"{radius}", f"{height}"), expected, delta=.025) 23 | 24 | @parameterized.expand([ 25 | # Wolfram Alpha as a reference 26 | (0.0, 3.0, 0.0), 27 | (3.0, 0.0, 0.0), 28 | (1.0, 1.0, 2*math.pi), # "lateral surface area of cylinder radius 1 height 1" 29 | (4.0, 2.5, 62.8139), 30 | (3.125, 44.15, 866.883), # "lateral surface area of cylinder radius 3.125 height 44.15" 31 | ]) 32 | def test_cylinder_side_surface_area(self, radius, height, expected): 33 | self.assertAlmostEqual(cylinder_side_surface_area(radius, height), expected, delta=.025) 34 | self.assertAlmostEqual(cylinder_side_surface_area(f"{radius}", f"{height}"), expected, delta=.025) 35 | 36 | @parameterized.expand([ 37 | # Wolfram Alpha as a reference 38 | (0.0, 3.0, 0.0), 39 | (3.0, 0.0, 56.549), 40 | (1.0, 1.0, 4*math.pi), # "lateral surface area of cylinder radius 1 height 1" 41 | (4.0, 2.5, 163.363), 42 | (3.125, 44.15, 928.242), # "lateral surface area of cylinder radius 3.125 height 44.15" 43 | ]) 44 | def test_cylinder_surface_area(self, radius, height, expected): 45 | self.assertAlmostEqual(cylinder_surface_area(radius, height), expected, delta=.025) 46 | self.assertAlmostEqual(cylinder_surface_area(f"{radius}", f"{height}"), expected, delta=.025) 47 | 48 | class TestHollowCylinder(unittest.TestCase): 49 | @parameterized.expand([ 50 | # Wolfram Alpha as a reference 51 | # The following are not actually hollow (inner_radius=0.0) 52 | (0.0, 0.0, 3.0, 0.0), 53 | (3.0, 0.0, 0.0, 0.0), 54 | (1.0, 0.0, 1.0, math.pi), # "volume of cylinder radius 1 height 1" 55 | (4.0, 0.0, 2.5, 125.664), 56 | (3.125, 0.0, 44.15, 1354.51), # "volume of cylinder radius 3.125 height 44.15" 57 | # The following are actually hollow 58 | (0.0, 0.0, 3.0, 0.0), 59 | (3.0, 1.0, 0.0, 0.0), 60 | (1.0, 1.0, 1.0, 0.), 61 | (1.0, 0.5, 1.0, math.pi - 0.785398), 62 | (4.0, 1.0, 2.5, 125.664-7.85398), 63 | (3.125, 2.25, 44.15, 1354.51-702.175), 64 | ]) 65 | def test_hollow_cylinder_volume(self, outer_radius, inner_radius, height, expected): 66 | self.assertAlmostEqual(hollow_cylinder_volume(outer_radius, inner_radius, height), expected, delta=.025) 67 | self.assertAlmostEqual(hollow_cylinder_volume(f"{outer_radius}", f"{inner_radius}", f"{height}"), expected, delta=.025) 68 | 69 | 70 | @parameterized.expand([ 71 | # Wolfram Alpha as a reference 72 | # The following are not actually hollow (inner_radius=0.0) 73 | (3.125, 0.0, 44.15, 1354.51), 74 | # The following are actually hollow 75 | (1.0, 0.5, 1.0, math.pi - 0.785398), 76 | (4.0, 1.0, 2.5, 125.664-7.85398), 77 | (3.125, 2.25, 44.15, 1354.51-702.175), 78 | ]) 79 | def test_hollow_cylinder_inner_radius_by_volume(self, outer_radius, inner_radius, height, volume): 80 | # NOTE: This uses the hollow_cylinder_volume() test cases except the ones that yield 0 volume 81 | self.assertAlmostEqual(hollow_cylinder_inner_radius_by_volume(outer_radius, volume, height), inner_radius, delta=.025) 82 | self.assertAlmostEqual(hollow_cylinder_inner_radius_by_volume(f"{outer_radius}", f"{volume}", f"{height}"), inner_radius, delta=.025) -------------------------------------------------------------------------------- /tests/Math/Geometry/TestPolygon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_allclose 4 | from UliEngineering.Math.Geometry.Polygon import * 5 | from parameterized import parameterized 6 | import functools 7 | import numpy as np 8 | import unittest 9 | 10 | class TestPolygon(unittest.TestCase): 11 | def test_polygon_lines(self): 12 | coords = np.asarray([[0, 1], 13 | [1, 2], 14 | [2, 3]]) 15 | closed = np.asarray([[[2, 3], [0, 1]], 16 | [[0, 1], [1, 2]], 17 | [[1, 2], [2, 3]]]) 18 | opened = np.asarray([[[0, 1], [1, 2]], 19 | [[1, 2], [2, 3]]]) 20 | assert_allclose(closed, polygon_lines(coords, closed=True)) 21 | assert_allclose(opened, polygon_lines(coords, closed=False)) 22 | 23 | def test_polygon_area_triangle(self): 24 | # Triangle test 25 | coords = np.asarray([[0, 0], 26 | [1, 1], 27 | [1, 0]]) 28 | assert_allclose(0.5*1*1, polygon_area(coords)) 29 | 30 | def test_polygon_area_square(self): 31 | coords = np.asarray([[0, 0], 32 | [1, 0], 33 | [1, 1], 34 | [0, 1]]) 35 | assert_allclose(1, polygon_area(coords)) 36 | 37 | def test_polygon_area_rectangle(self): 38 | # Triangle test 39 | coords = np.asarray([[0, 0], 40 | [2, 0], 41 | [2, 1], 42 | [0, 1]]) 43 | assert_allclose(2, polygon_area(coords)) 44 | 45 | @parameterized.expand([ 46 | (np.zeros(5),), 47 | (np.zeros((5,5)),), 48 | (np.zeros((5,5)),) 49 | ]) 50 | def test_polygon_area_rectangle_zeros(self, arr): 51 | with self.assertRaises(ValueError): 52 | polygon_area(arr) 53 | 54 | @parameterized.expand([ 55 | (np.zeros(5),), 56 | (np.zeros((5,5)),), 57 | (np.zeros((5,5)),) 58 | ]) 59 | def test_polygon_lines_rectangle(self, arr): 60 | with self.assertRaises(ValueError): 61 | polygon_lines(arr) 62 | -------------------------------------------------------------------------------- /tests/Math/Geometry/TestSphere.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Math.Geometry.Sphere import * 5 | from parameterized import parameterized 6 | import numpy as np 7 | import unittest 8 | from UliEngineering.EngineerIO import normalize_numeric, Unit 9 | 10 | 11 | class TestSphere(unittest.TestCase): 12 | @parameterized.expand([ 13 | (0.0, ), 14 | (1.0, ), 15 | (4.0, ), 16 | (3.125, ), 17 | (55.55, ), 18 | ]) 19 | def test_sphere_volume(self, radius): 20 | volume = 4/3*np.pi*(radius**3) 21 | assert_approx_equal(sphere_volume_by_radius(radius), volume) 22 | assert_approx_equal(sphere_volume_by_diameter(radius*2), volume) 23 | assert_approx_equal(sphere_volume_by_radius(f"{radius}"), volume) 24 | assert_approx_equal(sphere_volume_by_diameter(radius*2), volume) 25 | 26 | @parameterized.expand([ 27 | (0.0, ), 28 | (1.0, ), 29 | (4.0, ), 30 | (3.125, ), 31 | (55.55, ), 32 | ]) 33 | def test_sphere_surface_area(self, radius): 34 | area = 4*np.pi*(radius**2) 35 | assert_approx_equal(sphere_surface_area_by_radius(radius), area) 36 | assert_approx_equal(sphere_surface_area_by_diameter(radius*2), area) 37 | assert_approx_equal(sphere_surface_area_by_radius(f"{radius}"), area) 38 | assert_approx_equal(sphere_surface_area_by_diameter(radius*2), area) 39 | 40 | 41 | -------------------------------------------------------------------------------- /tests/Math/Geometry/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Math/Geometry/__init__.py -------------------------------------------------------------------------------- /tests/Math/TestCoordinates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_allclose 4 | from UliEngineering.Math.Coordinates import * 5 | from parameterized import parameterized 6 | import functools 7 | import numpy as np 8 | import unittest 9 | 10 | class TestBoundingBox(unittest.TestCase): 11 | def test_bbox_real(self): 12 | """Test bounding box with real data""" 13 | coords = [(6.74219, -53.57835), 14 | (6.74952, -53.57241), 15 | (6.75652, -53.56289), 16 | (6.74756, -53.56598), 17 | (6.73462, -53.57518)] 18 | coords = np.asarray(coords) 19 | bbox = BoundingBox(coords) 20 | assert_allclose(bbox.minx, 6.73462) 21 | assert_allclose(bbox.maxx, 6.75652) 22 | assert_allclose(bbox.miny, -53.57835) 23 | assert_allclose(bbox.maxy, -53.56289) 24 | assert_allclose(bbox.width, 6.75652 - 6.73462) 25 | assert_allclose(bbox.height, -53.56289 - -53.57835) 26 | self.assertIn("BoundingBox(", bbox.__repr__()) 27 | 28 | def test_bbox(self): 29 | """Test bounding box with simulated data""" 30 | coords = [(0., 0.), 31 | (20., 10)] 32 | 33 | coords = np.asarray(coords) 34 | bbox = BoundingBox(coords) 35 | assert_allclose(bbox.minx, 0) 36 | assert_allclose(bbox.maxx, 20) 37 | assert_allclose(bbox.miny, 0) 38 | assert_allclose(bbox.maxy, 10) 39 | assert_allclose(bbox.width, 20) 40 | assert_allclose(bbox.height, 10) 41 | assert_allclose(bbox.area, 20*10) 42 | assert_allclose(bbox.aspect_ratio, 2) 43 | assert_allclose(bbox.center, (10., 5.)) 44 | assert_allclose(bbox.max_dim, 20) 45 | assert_allclose(bbox.min_dim, 10) 46 | 47 | @parameterized.expand([(np.zeros((0,2)),), 48 | (np.zeros((2,3)),), 49 | (np.zeros((2,2,2)),) 50 | ]) 51 | def test_invalid_bbox_input(self, arr): 52 | with self.assertRaises(ValueError): 53 | BoundingBox(arr) 54 | -------------------------------------------------------------------------------- /tests/Math/TestDecibel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_allclose 4 | from UliEngineering.Math.Decibel import * 5 | from parameterized import parameterized 6 | import numpy as np 7 | import unittest 8 | import pytest 9 | 10 | class TestDecibel(unittest.TestCase): 11 | def test_ratio_to_db(self): 12 | assert_allclose(12, ratio_to_dB(4, factor=dBFactor.Field), 0.05) 13 | assert_allclose(6, ratio_to_dB(2, factor=dBFactor.Field), 0.05) 14 | assert_allclose(-6, ratio_to_dB(0.5, factor=dBFactor.Field), 0.05) 15 | assert_allclose(-12, ratio_to_dB(0.25, factor=dBFactor.Field), 0.05) 16 | 17 | assert_allclose(6, ratio_to_dB(4, factor=dBFactor.Power), 0.05) 18 | assert_allclose(3, ratio_to_dB(2, factor=dBFactor.Power), 0.05) 19 | assert_allclose(-3, ratio_to_dB(0.5, factor=dBFactor.Power), 0.05) 20 | assert_allclose(-6, ratio_to_dB(0.25, factor=dBFactor.Power), 0.05) 21 | 22 | @pytest.mark.filterwarnings("ignore:divide by zero encountered in log10") 23 | def test_ratio_to_dB_infinite(self): 24 | self.assertEqual(-np.inf, ratio_to_dB(0)) 25 | self.assertEqual(-np.inf, ratio_to_dB(0, factor=dBFactor.Power)) 26 | self.assertEqual(-np.inf, ratio_to_dB(-5)) 27 | self.assertEqual(-np.inf, ratio_to_dB(-5, factor=dBFactor.Power)) 28 | 29 | def test_value_to_db(self): 30 | # Test v0 = 1 31 | assert_allclose(12, value_to_dB(4, 1, factor=dBFactor.Field), 0.05) 32 | assert_allclose(6, value_to_dB(2, 1, factor=dBFactor.Field), 0.05) 33 | assert_allclose(-6, value_to_dB(0.5, 1, factor=dBFactor.Field), 0.05) 34 | assert_allclose(-12, value_to_dB(0.25, 1, factor=dBFactor.Field), 0.05) 35 | 36 | assert_allclose(6, value_to_dB(4, 1, factor=dBFactor.Power), 0.05) 37 | assert_allclose(3, value_to_dB(2, 1, factor=dBFactor.Power), 0.05) 38 | assert_allclose(-3, value_to_dB(0.5, 1, factor=dBFactor.Power), 0.05) 39 | assert_allclose(-6, value_to_dB(0.25, 1, factor=dBFactor.Power), 0.05) 40 | # Test v0 != 1 41 | assert_allclose(6, value_to_dB(4, 2, factor=dBFactor.Field), 0.05) 42 | assert_allclose(0, value_to_dB(2, 2, factor=dBFactor.Field), 0.05) 43 | assert_allclose(-6, value_to_dB(1, 2, factor=dBFactor.Field), 0.05) 44 | assert_allclose(-12, value_to_dB(0.5, 2, factor=dBFactor.Field), 0.05) 45 | assert_allclose(-18, value_to_dB(0.25, 2, factor=dBFactor.Field), 0.05) 46 | 47 | assert_allclose(3, value_to_dB(4, 2, factor=dBFactor.Power), 0.05) 48 | assert_allclose(0, value_to_dB(2, 2, factor=dBFactor.Power), 0.05) 49 | assert_allclose(-3, value_to_dB(1, 2, factor=dBFactor.Power), 0.05) 50 | assert_allclose(-6, value_to_dB(0.5, 2, factor=dBFactor.Power), 0.05) 51 | assert_allclose(-9, value_to_dB(0.25, 2, factor=dBFactor.Power), 0.05) 52 | # Test string 53 | assert_allclose(6, value_to_dB("4 V", "2 V", factor=dBFactor.Field), 0.05) 54 | 55 | def test_dB_to_ratio(self): 56 | self.assertAlmostEqual(dB_to_ratio(0), 1.0) 57 | self.assertAlmostEqual(dB_to_ratio(20), 10.0) 58 | self.assertAlmostEqual(dB_to_ratio(-20), 0.1) 59 | self.assertAlmostEqual(dB_to_ratio(20, factor=dBFactor.Power), 100.0) 60 | self.assertAlmostEqual(dB_to_ratio(-20, factor=dBFactor.Power), 0.01) 61 | 62 | def test_dB_to_value(self): 63 | self.assertAlmostEqual(dB_to_value(0), 1.0) 64 | self.assertAlmostEqual(dB_to_value(20), 10.0) 65 | self.assertAlmostEqual(dB_to_value(-20), 0.1) 66 | self.assertAlmostEqual(dB_to_value(20, v0=100), 1000.0) 67 | self.assertAlmostEqual(dB_to_value(-20, v0=100), 10.0) 68 | 69 | def test_voltage_to_dBuV(self): 70 | self.assertAlmostEqual(voltage_to_dBuV(1e-6), 0) 71 | self.assertAlmostEqual(voltage_to_dBuV(10e-6), 20) 72 | self.assertAlmostEqual(voltage_to_dBuV(1e-7), -20) 73 | self.assertAlmostEqual(voltage_to_dBuV(-1), -np.inf) 74 | 75 | def test_power_to_dBm(self): 76 | self.assertAlmostEqual(power_to_dBm(1e-3), 0) 77 | self.assertAlmostEqual(power_to_dBm(100e-3), 20) 78 | self.assertAlmostEqual(power_to_dBm(0.01e-3), -20) 79 | self.assertAlmostEqual(power_to_dBm(-1), -np.inf) -------------------------------------------------------------------------------- /tests/Math/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Math/__init__.py -------------------------------------------------------------------------------- /tests/Mechanics/TestThreads.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Mechanics.Threads import * 5 | import unittest 6 | 7 | class TestThreads(unittest.TestCase): 8 | def test_thread_params(self): 9 | self.assertEqual(threads["M3"].outer_diameter, 3.0) 10 | -------------------------------------------------------------------------------- /tests/Mechanics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Mechanics/__init__.py -------------------------------------------------------------------------------- /tests/Physics/TestAcceleration.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.Acceleration import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | import scipy.constants 8 | 9 | g0 = scipy.constants.physical_constants['standard acceleration of gravity'][0] 10 | 11 | class TestAccelerationConversion(unittest.TestCase): 12 | def test_g_to_ms2(self): 13 | self.assertAlmostEqual(g_to_ms2(0.), 0) 14 | self.assertAlmostEqual(g_to_ms2(60.), 60 * g0) 15 | self.assertAlmostEqual(g_to_ms2(120.), 120 * g0) 16 | self.assertAlmostEqual(g_to_ms2(150.), 150 * g0) 17 | 18 | def test_ms2_to_g(self): 19 | self.assertAlmostEqual(ms2_to_g(0.), 0) 20 | self.assertAlmostEqual(ms2_to_g(60.), 60 / g0) 21 | self.assertAlmostEqual(ms2_to_g(120.), 120 / g0) 22 | self.assertAlmostEqual(ms2_to_g(150.), 150 / g0) 23 | 24 | class TestCentrifugalAcceleration(unittest.TestCase): 25 | def test_centrifugal_acceleration(self): 26 | # Reference: https://techoverflow.net/2020/04/20/centrifuge-acceleration-calculator-from-rpm-and-diameter/ 27 | # No acceleration if not turning 28 | self.assertAlmostEqual(centrifugal_acceleration(0.1, 0), 0.0, places=2) 29 | # These have speed > 0 30 | self.assertAlmostEqual(centrifugal_acceleration(0.1, 100), 39478.417, places=2) 31 | self.assertAlmostEqual(centrifugal_acceleration(0.2, 100), 78956.835, places=2) 32 | self.assertAlmostEqual(centrifugal_acceleration(0.2, 10), 789.568, places=2) 33 | 34 | def test_centrifuge_radius(self): 35 | # Reference: Inverse of test_centrifugal_acceleration() 36 | # Note: t 37 | # These have speed > 0 38 | self.assertAlmostEqual(centrifuge_radius(39478.417, 100), 0.1, places=2) 39 | self.assertAlmostEqual(centrifuge_radius(78956.835, 100), 0.2, places=2) 40 | self.assertAlmostEqual(centrifuge_radius(789.568, 10), 0.2, places=2) -------------------------------------------------------------------------------- /tests/Physics/TestCapacitors.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Electronics.Capacitors import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import numpy as np 7 | import unittest 8 | 9 | class TestCapacitors(unittest.TestCase): 10 | def test_capacitor_energy(self): 11 | assert_approx_equal(capacitor_energy("1.5 F", "5.0 V"), 18.75) 12 | assert_approx_equal(capacitor_energy("1.5 F", "0.0 V"), 0.0) 13 | self.assertEqual(auto_format(capacitor_energy, "100 mF", "1.2 V"), "72.0 mJ") 14 | 15 | def test_capacitor_charge(self): 16 | assert_approx_equal(capacitor_charge("1.5 F", "5.0 V"), 7.5) 17 | assert_approx_equal(capacitor_charge("1.5 F", "0.0 V"), 0.0) 18 | self.assertEqual(auto_format(capacitor_charge, "1.5 F", "5.0 V"), "7.50 C") 19 | 20 | def test_numpy_arrays(self): 21 | l = np.asarray([1.5, 0.1]) 22 | assert_allclose(capacitor_energy(l, 5.0), [18.75, 1.25]) 23 | 24 | def test_capacitor_lifetime(self): 25 | self.assertAlmostEqual(capacitor_lifetime(105, "2000 h", "105 °C", A=10), 2000) 26 | self.assertAlmostEqual(capacitor_lifetime(115, "2000 h", "105 °C", A=10), 1000) 27 | self.assertAlmostEqual(capacitor_lifetime(95, "2000 h", "105 °C", A=10), 4000) 28 | 29 | def test_capacitor_constant_current_discharge_time(self): 30 | # verified using https://www.circuits.dk/calculator_capacitor_discharge.htm 31 | # with Vcapmax=10, Vcapmin=1e-9, size=100e-6, ESR=1e-9, Imax=1000uA 32 | capacitance = 100e-6 # 100 uF 33 | voltage = 10 # 10 V 34 | current = 1e-3 # 1 mA 35 | expected_time = 1 # seconds 36 | 37 | # Charge should be the same 38 | discharge_time = capacitor_constant_current_discharge_time(capacitance, voltage, current) 39 | self.assertAlmostEqual(discharge_time, expected_time, places=2) 40 | 41 | charge_time = capacitor_constant_current_discharge_time(capacitance, voltage, current) 42 | self.assertAlmostEqual(charge_time, expected_time, places=2) 43 | 44 | # Check with keyword arguments 45 | discharge_time = capacitor_constant_current_discharge_time(capacitance=capacitance, initial_voltage=voltage, current=current) 46 | self.assertAlmostEqual(discharge_time, expected_time, places=2) 47 | 48 | # Check with nonzero target voltage (half voltage => half time) 49 | charge_time = capacitor_constant_current_discharge_time(capacitance, voltage, current, target_voltage=voltage/2) 50 | self.assertAlmostEqual(charge_time, expected_time/2, places=2) 51 | 52 | def test_capacitor_voltage_by_energy(self): 53 | # Basic test with zero starting voltage 54 | capacitance = 1.5 # F 55 | voltage = 5.0 # V 56 | energy = capacitor_energy(capacitance, voltage) 57 | calculated_voltage = capacitor_voltage_by_energy(capacitance, energy) 58 | self.assertAlmostEqual(calculated_voltage, voltage, places=10) 59 | 60 | # Test with non-zero starting voltage 61 | starting_voltage = 2.0 # V 62 | # Calculate additional energy needed to reach target voltage 63 | additional_energy = capacitor_energy(capacitance, voltage) - capacitor_energy(capacitance, starting_voltage) 64 | calculated_voltage = capacitor_voltage_by_energy(capacitance, additional_energy, starting_voltage) 65 | self.assertAlmostEqual(calculated_voltage, voltage, places=10) 66 | 67 | # Test with engineering notation 68 | energy = capacitor_energy("100 mF", "5.0 V") 69 | calculated_voltage = capacitor_voltage_by_energy("100 mF", energy) 70 | self.assertAlmostEqual(calculated_voltage, 5.0, places=10) 71 | 72 | # Test with zero energy (should return starting_voltage) 73 | calculated_voltage = capacitor_voltage_by_energy(capacitance, 0, "3V") 74 | self.assertAlmostEqual(calculated_voltage, 3.0, places=10) 75 | 76 | # Test with auto_format 77 | self.assertEqual(auto_format(capacitor_voltage_by_energy, "1.5 F", "18.75 J"), "5.00 V") -------------------------------------------------------------------------------- /tests/Physics/TestFrequency.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_less 4 | from UliEngineering.Physics.Frequency import * 5 | from UliEngineering.Exceptions import * 6 | import functools 7 | import numpy as np 8 | import unittest 9 | 10 | class TestFrequencies(unittest.TestCase): 11 | def test_frequency_to_period(self): 12 | assert_approx_equal(frequency_to_period(0.1), 10) 13 | assert_approx_equal(frequency_to_period("0.1 Hz"), 10) 14 | assert_approx_equal(frequency_to_period("10 Hz"), 0.1) 15 | assert_approx_equal(frequency_to_period("10 kHz"), 0.1e-3) 16 | 17 | def test_period_to_frequency(self): 18 | assert_approx_equal(frequency_to_period(10), 0.1) 19 | assert_approx_equal(frequency_to_period("10 s"), 0.1) 20 | assert_approx_equal(frequency_to_period("10 ks"), 0.1e-3) 21 | assert_approx_equal(frequency_to_period("1 ms"), 1e3) 22 | -------------------------------------------------------------------------------- /tests/Physics/TestHalfLife.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from UliEngineering.Physics.HalfLife import * 3 | 4 | class TestHalfLife(unittest.TestCase): 5 | def test_half_lifes_passed(self): 6 | self.assertAlmostEqual(half_lifes_passed("1h", "1min"), 60.0) 7 | self.assertAlmostEqual(half_lifes_passed("2h", "30min"), 4.0) 8 | self.assertAlmostEqual(half_lifes_passed("1h", "2h"), 0.5) 9 | 10 | def test_fraction_remaining(self): 11 | self.assertAlmostEqual(fraction_remaining("1h", "1h"), 0.5) 12 | self.assertAlmostEqual(fraction_remaining("2h", "1h"), 0.25) 13 | self.assertAlmostEqual(fraction_remaining("1h", "2h"), 0.70710678118, places=6) 14 | 15 | def test_fraction_decayed(self): 16 | self.assertAlmostEqual(fraction_decayed("1h", "1h"), 0.5) 17 | self.assertAlmostEqual(fraction_decayed("2h", "1h"), 0.75) 18 | self.assertAlmostEqual(fraction_decayed("1h", "2h"), 0.29289321881, places=6) 19 | 20 | def test_remaining_quantity(self): 21 | self.assertAlmostEqual(remaining_quantity("1h", "1h", 100), 50.0) 22 | self.assertAlmostEqual(remaining_quantity("2h", "1h", 100), 25.0) 23 | self.assertAlmostEqual(remaining_quantity("1h", "2h", 100), 70.710678118, places=6) 24 | 25 | def test_decayed_quantity(self): 26 | self.assertAlmostEqual(decayed_quantity("1h", "1h", 100), 50.0) 27 | self.assertAlmostEqual(decayed_quantity("2h", "1h", 100), 75.0) 28 | self.assertAlmostEqual(decayed_quantity("1h", "2h", 100), 29.289321881, places=6) 29 | 30 | def test_half_life_from_decay_constant(self): 31 | # Test with decay constant 0.1 32 | self.assertAlmostEqual(half_life_from_decay_constant(0.1), 6.9314718056, places=10) 33 | # Test with decay constant 1.0 34 | self.assertAlmostEqual(half_life_from_decay_constant(1.0), 0.69314718056, places=10) 35 | # Test with decay constant 0.5 36 | self.assertAlmostEqual(half_life_from_decay_constant(0.5), 1.38629436112, places=10) 37 | # Test with decay constant 2.0 38 | self.assertAlmostEqual(half_life_from_decay_constant(2.0), 0.34657359028, places=10) 39 | 40 | def test_half_life_from_remaining_quantity(self): 41 | self.assertAlmostEqual( 42 | half_life_from_remaining_quantity("1h", 50, 100), 43 | 3600, 44 | places=5 45 | ) 46 | self.assertAlmostEqual( 47 | half_life_from_remaining_quantity("2h", 25, 100), 48 | 3600.0, 49 | places=5 50 | ) 51 | self.assertAlmostEqual( 52 | half_life_from_remaining_quantity(7200, 25, 100), 53 | 3600.0, 54 | places=5 55 | ) 56 | 57 | def test_half_life_from_decayed_quantity(self): 58 | self.assertAlmostEqual( 59 | half_life_from_decayed_quantity("1h", 50, 100), 60 | 3600.0, 61 | places=5 62 | ) 63 | self.assertAlmostEqual( 64 | half_life_from_decayed_quantity("2h", 75, 100), 65 | 3600.0, 66 | places=5 67 | ) 68 | self.assertAlmostEqual( 69 | half_life_from_decayed_quantity(7200, 75, 100), 70 | 3600.0, 71 | places=5 72 | ) 73 | 74 | def test_half_life_from_fraction_remaining(self): 75 | self.assertAlmostEqual( 76 | half_life_from_fraction_remaining(3600, 0.5), 77 | 3600.0, 78 | places=5 79 | ) 80 | self.assertAlmostEqual( 81 | half_life_from_fraction_remaining("2h", 0.25), 82 | 3600.0, 83 | places=5 84 | ) 85 | self.assertAlmostEqual( 86 | half_life_from_fraction_remaining(7200, 0.25), 87 | 3600.0, 88 | places=5 89 | ) 90 | 91 | def test_half_life_from_fraction_decayed(self): 92 | self.assertAlmostEqual( 93 | half_life_from_fraction_decayed(3600, 0.5), 94 | 3600.0, 95 | places=5 96 | ) 97 | self.assertAlmostEqual( 98 | half_life_from_fraction_decayed("2h", 0.75), 99 | 3600.0, 100 | ) 101 | self.assertAlmostEqual( 102 | half_life_from_fraction_decayed(7200, 0.75), 103 | 3600.0, 104 | ) -------------------------------------------------------------------------------- /tests/Physics/TestJohnsonNyquistNoise.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.JohnsonNyquistNoise import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | 8 | class TestJohnsonNyquistNoise(unittest.TestCase): 9 | def test_johnson_nyquist_noise_current(self): 10 | v = johnson_nyquist_noise_current("20 MΩ", "Δ10000 Hz", "20 °C") 11 | assert_approx_equal(v, 2.84512e-12, significant=5) 12 | self.assertEqual(auto_format(johnson_nyquist_noise_current, "20 MΩ", "Δ10000 Hz", "20 °C"), "2.85 pA") 13 | 14 | def test_johnson_nyquist_noise_voltage(self): 15 | v = johnson_nyquist_noise_voltage("20 MΩ", "Δ10000 Hz", "20 °C") 16 | self.assertEqual(auto_format(johnson_nyquist_noise_voltage, "20 MΩ", "Δ10000 Hz", "20 °C"), "56.9 µV") 17 | assert_approx_equal(v, 56.9025e-6, significant=5) 18 | -------------------------------------------------------------------------------- /tests/Physics/TestLight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.Light import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | 8 | class TestLight(unittest.TestCase): 9 | def test_lumen_to_candela_by_apex_angle(self): 10 | v = lumen_to_candela_by_apex_angle("25 lm", "120°") 11 | assert_approx_equal(v, 7.9577471546, significant=5) 12 | self.assertEqual(auto_format(lumen_to_candela_by_apex_angle, "25 lm", "120°"), "7.96 cd") 13 | 14 | -------------------------------------------------------------------------------- /tests/Physics/TestMagneticResonance.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.MagneticResonance import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | 8 | class TestLarmorFrequency(unittest.TestCase): 9 | def test_larmor_frequency_h1(self): 10 | self.assertAlmostEqual(larmor_frequency(0., nucleus_larmor_frequency=NucleusLarmorFrequency.H1), 0) 11 | self.assertAlmostEqual(larmor_frequency(1., nucleus_larmor_frequency=NucleusLarmorFrequency.H1), 42.57638543e6, places=6) 12 | self.assertAlmostEqual(larmor_frequency(2.2, nucleus_larmor_frequency=NucleusLarmorFrequency.H1), 2.2*42.57638543e6) 13 | # H1 hould be the standard value 14 | self.assertAlmostEqual(larmor_frequency(0.), 0) 15 | -------------------------------------------------------------------------------- /tests/Physics/TestNTC.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_less 4 | from UliEngineering.Physics.NTC import * 5 | from UliEngineering.Exceptions import * 6 | import functools 7 | import numpy as np 8 | import unittest 9 | 10 | class TestNTC(unittest.TestCase): 11 | def test_ntc_resistance(self): 12 | # Values arbitrarily from Murata NCP15WB473D03RC 13 | assert_approx_equal(ntc_resistance("47k", "4050K", "25°C"), 47000) 14 | assert_approx_equal(ntc_resistance("47k", "4050K", "0°C"), 162942.79) 15 | assert_approx_equal(ntc_resistance("47k", "4050K", "-18°C"), 463773.791) 16 | assert_approx_equal(ntc_resistance("47k", "4050K", "5°C"), 124819.66) 17 | assert_approx_equal(ntc_resistance("47k", "4050K", "60°C"), 11280.407) 18 | 19 | def test_ntc_resistances(self): 20 | # Currently mostly test if it runs 21 | ts, values = ntc_resistances("47k", "4050K") 22 | -------------------------------------------------------------------------------- /tests/Physics/TestNoiseDensity.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.NoiseDensity import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | 8 | class TestNoiseDensity(unittest.TestCase): 9 | def testActualNoise(self): 10 | assert_approx_equal(actual_noise("100 µV", "100 Hz"), 1e-3) 11 | assert_approx_equal(actual_noise(1e-4, 100), 1e-3) 12 | self.assertEqual(auto_format(actual_noise, "100 µV", "100 Hz"), '1.00 mV') 13 | 14 | def testNoiseDensity(self): 15 | assert_approx_equal(noise_density("1.0 mV", "100 Hz"), 1e-4) 16 | assert_approx_equal(noise_density(1e-3, 100), 1e-4) 17 | self.assertEqual(auto_format(noise_density, "1.0 mV", "100 Hz"), '100 µV/√Hz') 18 | -------------------------------------------------------------------------------- /tests/Physics/TestPressure.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.Pressure import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | import math 8 | 9 | class TestPressureConversion(unittest.TestCase): 10 | def test_pascal_to_bar(self): 11 | self.assertAlmostEqual(pascal_to_bar(0.), 0) 12 | self.assertAlmostEqual(pascal_to_bar(1.), 1/100000) 13 | self.assertAlmostEqual(pascal_to_bar(5.), 5/100000) 14 | self.assertAlmostEqual(pascal_to_bar(100000), 1) 15 | 16 | def test_bar_to_pascal(self): 17 | self.assertAlmostEqual(bar_to_pascal(0.), 0) 18 | self.assertAlmostEqual(bar_to_pascal(1.), 100000) 19 | self.assertAlmostEqual(bar_to_pascal(5.), 500000) 20 | self.assertAlmostEqual(bar_to_pascal(0.00001), 1) -------------------------------------------------------------------------------- /tests/Physics/TestRF.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.Physics.RF import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import numpy as np 7 | import unittest 8 | 9 | class TestRF(unittest.TestCase): 10 | def test_quality_factor(self): 11 | assert_approx_equal(quality_factor("8.000 MHz", "1 kHz"), 8000.0) 12 | assert_approx_equal(quality_factor("8.000 MHz", "1 MHz"), 8.0) 13 | 14 | def test_resonant_impedance(self): 15 | assert_approx_equal(resonant_impedance("100 uH", "10 nF", Q=30.0), 10./3) 16 | self.assertEqual(auto_format(resonant_impedance, "100 uH", "10 nF", Q=30.0), '3.33 Ω') 17 | 18 | def test_resonant_frequency(self): 19 | assert_approx_equal(resonant_frequency("100 uH", "10 nF"), 159154.94309189534) 20 | self.assertEqual(auto_format(resonant_frequency, "100 uH", "10 nF"), '159 kHz') 21 | 22 | def test_resonant_inductance(self): 23 | assert_approx_equal(resonant_inductance("250 kHz", "10 nF"), 4.052847345693511e-05) 24 | self.assertEqual(auto_format(resonant_inductance, "250 kHz", "10 nF"), '40.5 µH') 25 | -------------------------------------------------------------------------------- /tests/Physics/TestRotation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.Rotation import * 5 | from UliEngineering.EngineerIO import auto_format 6 | import unittest 7 | import math 8 | 9 | class TestRotationConversion(unittest.TestCase): 10 | def test_rpm_to_hz(self): 11 | self.assertAlmostEqual(rpm_to_Hz(0.), 0) 12 | self.assertAlmostEqual(rpm_to_Hz(60.), 1.) 13 | self.assertAlmostEqual(rpm_to_Hz(120.), 2.) 14 | self.assertAlmostEqual(rpm_to_Hz(150.), 2.5) 15 | 16 | def test_rpm_to_rps(self): 17 | self.assertAlmostEqual(rpm_to_rps(0.), 0) 18 | self.assertAlmostEqual(rpm_to_rps(60.), 1.) 19 | self.assertAlmostEqual(rpm_to_rps(120.), 2.) 20 | self.assertAlmostEqual(rpm_to_rps(150.), 2.5) 21 | 22 | def test_hz_to_rpm(self): 23 | self.assertAlmostEqual(hz_to_rpm(0.), 0) 24 | self.assertAlmostEqual(hz_to_rpm(1.), 60.) 25 | self.assertAlmostEqual(hz_to_rpm(2.), 120.) 26 | self.assertAlmostEqual(hz_to_rpm(2.5), 150.) 27 | 28 | class TestRotationOther(unittest.TestCase): 29 | def test_angular_speed(self): 30 | self.assertAlmostEqual(angular_speed(0.), 0) 31 | self.assertAlmostEqual(angular_speed("0 Hz"), 0) 32 | self.assertAlmostEqual(angular_speed(1), 1*2*math.pi) 33 | self.assertAlmostEqual(angular_speed("1 Hz"), 1*2*math.pi) 34 | self.assertAlmostEqual(angular_speed(1000), 1000*2*math.pi) 35 | self.assertAlmostEqual(angular_speed("1 kHz"), 1000*2*math.pi) 36 | 37 | def test_centrifugal_force(self): 38 | # Zero cases 39 | self.assertAlmostEqual(centrifugal_force(5, 10, 0), 0) 40 | self.assertAlmostEqual(centrifugal_force(0, 10, 500), 0) 41 | self.assertAlmostEqual(centrifugal_force(5, 0, 500), 0) 42 | # Non zero cases 43 | # Reference: https://www.thecalculator.co/others/Centrifugal-Force-Calculator-660.html 44 | self.assertAlmostEqual(centrifugal_force(5, 10, 500), 9869.604401089358, places=2) 45 | self.assertAlmostEqual(centrifugal_force("5", "10", "500"), 9869.604401089358, places=2) 46 | 47 | def test_rotation_linear_speed(self): 48 | self.assertAlmostEqual(rotation_linear_speed(1, 0.), 0) 49 | self.assertAlmostEqual(rotation_linear_speed(1, "0 Hz"), 0) 50 | self.assertAlmostEqual(rotation_linear_speed(1, 1), 1*2*math.pi) 51 | self.assertAlmostEqual(rotation_linear_speed(1, "1 Hz"), 1*2*math.pi) 52 | self.assertAlmostEqual(rotation_linear_speed(1, 1000), 1000*2*math.pi) 53 | self.assertAlmostEqual(rotation_linear_speed(1, "1 kHz"), 1000*2*math.pi) 54 | self.assertAlmostEqual(rotation_linear_speed("1", "1 kHz"), 1000*2*math.pi) 55 | self.assertAlmostEqual(rotation_linear_speed("2", "1 kHz"), 2000*2*math.pi) 56 | self.assertAlmostEqual(rotation_linear_speed("1m", "1 kHz"), 1*2*math.pi) 57 | -------------------------------------------------------------------------------- /tests/Physics/TestTemperature.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Physics.Temperature import * 5 | from UliEngineering.Exceptions import * 6 | from UliEngineering.EngineerIO import auto_format 7 | import unittest 8 | 9 | class TestTemperature(unittest.TestCase): 10 | def testNormalizeTemperature(self): 11 | # Pure numbers 12 | assert_approx_equal(normalize_temperature("0"), 273.15) 13 | assert_approx_equal(normalize_temperature("1"), 274.15) 14 | assert_approx_equal(normalize_temperature(1), 274.15) 15 | assert_approx_equal(normalize_temperature(1, default_unit="K"), 1.0) 16 | # With units 17 | assert_approx_equal(normalize_temperature("1 C"), 274.15) 18 | assert_approx_equal(normalize_temperature("1 °C"), 274.15) 19 | assert_approx_equal(normalize_temperature("1°C"), 274.15) 20 | assert_approx_equal(normalize_temperature("1 K"), 1.0) 21 | assert_approx_equal(normalize_temperature("60 F"), 288.71, significant=5) 22 | # Signs 23 | assert_approx_equal(normalize_temperature("-1°C"), 272.15) 24 | assert_approx_equal(normalize_temperature("-200°C"), 73.15) 25 | 26 | def testNormalizeTemperatureCelsius(self): 27 | assert_approx_equal(normalize_temperature_celsius("-200°C"), -200.0) 28 | assert_approx_equal(normalize_temperature_celsius("273.15 K"), 0.0) 29 | 30 | def testAutoFormatTemperature(self): 31 | self.assertEqual(auto_format(normalize_temperature, "-200°C"), "73.1 K") 32 | self.assertEqual(auto_format(normalize_temperature_celsius, "200°C"), "200 °C") 33 | self.assertEqual(auto_format(normalize_temperature_celsius, "-200°C"), "-200 °C") 34 | self.assertEqual(auto_format(normalize_temperature_celsius, "-111 °C"), "-111 °C") 35 | self.assertEqual(auto_format(normalize_temperature_celsius, "0 °K"), "-273 °C") 36 | self.assertEqual(auto_format(normalize_temperature_celsius, "0 K"), "-273 °C") 37 | 38 | def test_temperature_with_dissipation(self): 39 | assert_approx_equal(temperature_with_dissipation("1 W", "1 °C/W"), 26.) 40 | assert_approx_equal(temperature_with_dissipation("1 W", "50 °C/W"), 25. + 50.) 41 | 42 | def testWrongUnit(self): 43 | with self.assertRaises(InvalidUnitException): 44 | normalize_temperature("150V") 45 | 46 | def testInvalidUnit(self): 47 | with self.assertRaises(ValueError): 48 | normalize_temperature("1G50 G") 49 | 50 | class TestTemperatureConversion(unittest.TestCase): 51 | def test_fahrenheit_to_celsius(self): 52 | assert_approx_equal(fahrenheit_to_celsius("11 °F"), -11.66666667) 53 | assert_approx_equal(fahrenheit_to_celsius(11.), -11.666666667) 54 | assert_approx_equal(fahrenheit_to_celsius("20 °F"), -6.66666667) 55 | assert_approx_equal(fahrenheit_to_celsius(20.), -6.666666667) 56 | self.assertEqual(auto_format(fahrenheit_to_celsius, "20 °F"), "-6.67 °C") 57 | 58 | def test_fahrenheit_to_kelvin(self): 59 | assert_approx_equal(fahrenheit_to_kelvin("11 °F"), 261.483333333) 60 | assert_approx_equal(fahrenheit_to_kelvin(11.), 261.483333333) 61 | assert_approx_equal(fahrenheit_to_kelvin("20 °F"), 266.483333333) 62 | assert_approx_equal(fahrenheit_to_kelvin(20.), 266.483333333) 63 | self.assertEqual(auto_format(fahrenheit_to_kelvin, "20 °F"), "266 K") 64 | 65 | def test_kelvin_to_celsius(self): 66 | assert_approx_equal(kelvin_to_celsius("11 K"), -262.15) 67 | assert_approx_equal(kelvin_to_celsius(11.), -262.15) 68 | assert_approx_equal(kelvin_to_celsius("300 K"), 26.85) 69 | assert_approx_equal(kelvin_to_celsius(300.), 26.85) 70 | self.assertEqual(auto_format(kelvin_to_celsius, "300 K"), "26.9 °C") 71 | 72 | def test_celsius_to_kelvin(self): 73 | assert_approx_equal(celsius_to_kelvin("-262.15 °C"), 11.) 74 | assert_approx_equal(celsius_to_kelvin(-262.15), 11.) 75 | assert_approx_equal(celsius_to_kelvin("26.85 °C"), 300.) 76 | assert_approx_equal(celsius_to_kelvin(26.85), 300.) 77 | self.assertEqual(auto_format(celsius_to_kelvin, "26.85 °C"), "300 K") 78 | 79 | 80 | -------------------------------------------------------------------------------- /tests/Physics/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Physics/__init__.py -------------------------------------------------------------------------------- /tests/SignalProcessing/TestCorrelation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_equal 4 | from UliEngineering.SignalProcessing.Correlation import * 5 | from parameterized import parameterized 6 | import concurrent.futures 7 | import numpy as np 8 | import datetime 9 | import unittest 10 | 11 | class TestCorrelation(unittest.TestCase): 12 | def testAutocorrelation(self): 13 | # Just test no exceptions 14 | autocorrelate(np.random.random_sample(1000)) -------------------------------------------------------------------------------- /tests/SignalProcessing/TestDateTime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_equal 4 | from UliEngineering.SignalProcessing.DateTime import * 5 | from parameterized import parameterized 6 | import concurrent.futures 7 | import numpy as np 8 | import datetime 9 | import unittest 10 | 11 | class TestSpliceDate(unittest.TestCase): 12 | def test_simple(self): 13 | d1 = datetime.datetime(2016, 1, 1, 12, 32, 15, microsecond=123456) 14 | d2 = datetime.datetime(1905, 1, 1, 14, 11, 25, microsecond=52) 15 | dres = datetime.datetime(2016, 1, 1, 14, 11, 25, microsecond=52) 16 | self.assertEqual(dres, splice_date(d1, d2)) 17 | 18 | 19 | class TestAutoStrptime(unittest.TestCase): 20 | def test_formats(self): 21 | #%Y-%m-%d %H:%M:%S.%f 22 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 11, 50), auto_strptime("2016-02-01 15:02:11.000050")) 23 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 11, 50), auto_strptime(" 2016-02-01 15:02:11.000050")) 24 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 11, 50), auto_strptime("2016-02-01 15:02:11.000050 ")) 25 | #%H:%M:%S.%f 26 | self.assertEqual(datetime.datetime(1900, 1, 1, 15, 2, 11, 50), auto_strptime("15:02:11.000050")) 27 | #%Y-%m-%d 28 | self.assertEqual(datetime.datetime(2016, 2, 1), auto_strptime("2016-02-01")) 29 | self.assertEqual(datetime.datetime(2016, 2, 1), auto_strptime("2016-02-01 ")) 30 | #%H:%M:%S 31 | self.assertEqual(datetime.datetime(1900, 1, 1, 15, 2, 11), auto_strptime("15:02:11")) 32 | self.assertEqual(datetime.datetime(1900, 1, 1, 15, 2, 11), auto_strptime("15:02:11 ")) 33 | self.assertEqual(datetime.datetime(1900, 1, 1, 15, 2, 11), auto_strptime(" 15:02:11")) 34 | #%Y-%m-%d %H 35 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 0, 0), auto_strptime("2016-02-01 15")) 36 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 0, 0), auto_strptime(" 2016-02-01 15")) 37 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 0, 0), auto_strptime("2016-02-01 15")) 38 | #%Y-%m-%d %H 39 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 0), auto_strptime("2016-02-01 15:02")) 40 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 0), auto_strptime("2016-02-01 15:02 ")) 41 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 0), auto_strptime(" 2016-02-01 15:02")) 42 | #%Y-%m-%d %H:%M:%S 43 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 11), auto_strptime("2016-02-01 15:02:11")) 44 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 11), auto_strptime("2016-02-01 15:02:11 ")) 45 | self.assertEqual(datetime.datetime(2016, 2, 1, 15, 2, 11), auto_strptime(" 2016-02-01 15:02:11")) 46 | 47 | def test_examples(self): 48 | self.assertEqual(datetime.datetime(2016, 7, 21, 00, 00, 00), auto_strptime("2016-07-21 00:00:00")) 49 | self.assertEqual(datetime.datetime(2016, 7, 21, 3, 00, 00), auto_strptime("2016-07-21 03:00:00")) 50 | self.assertEqual(datetime.datetime(2016, 9, 1, 10, 00, 00), auto_strptime("2016-09-01 10")) 51 | -------------------------------------------------------------------------------- /tests/SignalProcessing/TestNormalize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_allclose 4 | from UliEngineering.SignalProcessing.Normalize import * 5 | import numpy as np 6 | import unittest 7 | import pytest 8 | 9 | class TestNormalize(unittest.TestCase): 10 | 11 | @pytest.mark.filterwarnings("ignore: invalid value encountered in scalar divide") 12 | @pytest.mark.filterwarnings("ignore: Mean of empty slice") 13 | def test_center_to_zero(self): 14 | assert_allclose(center_to_zero([]).data, []) 15 | assert_allclose(center_to_zero(np.asarray([0.])).data, [0.]) 16 | assert_allclose(center_to_zero(np.asarray([1.])).data, [0.]) 17 | assert_allclose(center_to_zero(np.asarray([-1., 1.])).data, [-1., 1.]) 18 | assert_allclose(center_to_zero(np.asarray([-2., 2.])).data, [-2., 2.]) 19 | assert_allclose(center_to_zero(np.asarray([0., 1.])).data, [-0.5, 0.5]) 20 | assert_allclose(center_to_zero(np.asarray([0., 2.])).data, [-1., 1]) 21 | 22 | def test_normalize_max(self): 23 | assert_allclose(normalize_max([]).data, []) 24 | assert_allclose(normalize_max(np.asarray([0.])).data, [0.]) 25 | assert_allclose(normalize_max(np.asarray([1.])).data, [1.]) 26 | assert_allclose(normalize_max(np.asarray([-1., 1.])).data, [-1., 1.]) 27 | assert_allclose(normalize_max(np.asarray([-2., 2.])).data, [-1., 1.]) 28 | assert_allclose(normalize_max(np.asarray([0., 1.])).data, [0., 1.]) 29 | assert_allclose(normalize_max(np.asarray([-1., 0.])).data, [-1., 0.]) 30 | assert_allclose(normalize_max(np.asarray([-1., -2.])).data, [-1., -2.]) 31 | assert_allclose(normalize_max(np.asarray([0., 2.])).data, [0., 1]) 32 | assert_allclose(normalize_max(np.asarray([-10., 1.])).data, [-10., 1.]) 33 | assert_allclose(normalize_max(np.asarray([-10., 2.])).data, [-5., 1]) 34 | 35 | 36 | def test_normalize_minmax(self): 37 | assert_allclose(normalize_minmax([]).data, []) 38 | assert_allclose(normalize_minmax(np.asarray([0.])).data, [0.]) 39 | assert_allclose(normalize_minmax(np.asarray([1.])).data, [0.]) 40 | assert_allclose(normalize_minmax(np.asarray([-1., 1.])).data, [0., 1.]) 41 | assert_allclose(normalize_minmax(np.asarray([-2., 2.])).data, [0., 1.]) 42 | assert_allclose(normalize_minmax(np.asarray([0., 1.])).data, [0., 1.]) 43 | assert_allclose(normalize_minmax(np.asarray([-1., 0.])).data, [0., 1.]) 44 | assert_allclose(normalize_minmax(np.asarray([-1., -2.])).data, [1., 0.]) 45 | assert_allclose(normalize_minmax(np.asarray([0., 2.])).data, [0., 1.]) 46 | assert_allclose(normalize_minmax(np.asarray([0., 0.])).data, [0., 0.]) 47 | assert_allclose(normalize_minmax(np.asarray([1., 1.])).data, [0., 0.]) 48 | assert_allclose(normalize_minmax(np.asarray([-10., 1.])).data, [0., 1.]) 49 | assert_allclose(normalize_minmax(np.asarray([-10., 2.])).data, [0., 1]) 50 | assert_allclose(normalize_minmax(np.asarray([1., 2., 3.])).data, [0., 0.5, 1.]) 51 | assert_allclose(normalize_minmax(np.asarray([3., 2., 1.])).data, [1., 0.5, 0.]) 52 | 53 | -------------------------------------------------------------------------------- /tests/SignalProcessing/TestResampling.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import numpy as np 3 | from numpy.testing import assert_allclose, assert_approx_equal 4 | from UliEngineering.SignalProcessing.Resampling import * 5 | import unittest 6 | 7 | 8 | class TestBSplineResampling(unittest.TestCase): 9 | def setUp(self): 10 | self.x = np.arange(100) 11 | self.y = np.square(self.x) 12 | 13 | def testDiscard(self): 14 | x = np.arange(10) 15 | assert_allclose(resample_discard(x, 2), [0, 2, 4, 6, 8]) 16 | assert_allclose(resample_discard(x, 3), [0, 3, 6, 9]) 17 | 18 | class TestSignalSamplerate(unittest.TestCase): 19 | def setUp(self): 20 | # 100% equal sample rate 21 | self.tequal = np.asarray([ 22 | '2019-02-01T12:00:00.100000000', 23 | '2019-02-01T12:00:00.200000000', 24 | '2019-02-01T12:00:00.300000000', 25 | '2019-02-01T12:00:00.400000000', 26 | '2019-02-01T12:00:00.500000000', 27 | '2019-02-01T12:00:00.600000000', 28 | '2019-02-01T12:00:00.700000000', 29 | '2019-02-01T12:00:00.800000000', 30 | '2019-02-01T12:00:00.900000000', 31 | ], dtype='datetime64[ns]') 32 | # Almost 100% equal sample rate 33 | self.talmostequal = np.asarray([ 34 | '2019-02-01T12:00:00.100000000', 35 | '2019-02-01T12:00:00.200000000', 36 | '2019-02-01T12:00:00.300000000', 37 | '2019-02-01T12:00:00.400000000', 38 | '2019-02-01T12:00:00.500000000', 39 | '2019-02-01T12:00:00.600000000', 40 | '2019-02-01T12:00:00.700000000', 41 | '2019-02-01T12:00:00.800000000', 42 | '2019-02-01T12:00:00.900000100', 43 | ], dtype='datetime64[ns]') 44 | # Jittery sample rate 45 | self.tunequal = np.asarray([ 46 | '2019-02-01T12:00:00.103000000', 47 | '2019-02-01T12:00:00.205000000', 48 | '2019-02-01T12:00:00.301000000', 49 | '2019-02-01T12:00:00.403000000', 50 | '2019-02-01T12:00:00.502000000', 51 | '2019-02-01T12:00:00.606000000', 52 | '2019-02-01T12:00:00.701000000', 53 | '2019-02-01T12:00:00.802000000', 54 | '2019-02-01T12:00:00.900000000', 55 | ], dtype='datetime64[ns]') 56 | 57 | def testSignalSamplerate(self): 58 | assert_approx_equal(signal_samplerate(self.tunequal, ignore_percentile=3), 10.03344) 59 | assert_approx_equal(signal_samplerate(self.talmostequal, ignore_percentile=3), 10.0) 60 | assert_approx_equal(signal_samplerate(self.tequal, ignore_percentile=3), 10.0) 61 | 62 | class TestParallelResampling(unittest.TestCase): 63 | def setUp(self): 64 | self.x = np.arange(100) 65 | self.y = np.square(self.x) 66 | 67 | def testSimpleCall(self): 68 | # Check if a simple call does not raise any exceptions 69 | print("foo") 70 | parallel_resample(self.x, self.y, 10.0, time_factor=1.0) 71 | -------------------------------------------------------------------------------- /tests/SignalProcessing/TestSimulation.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose 4 | from UliEngineering.SignalProcessing.Simulation import * 5 | from UliEngineering.SignalProcessing.FFT import * 6 | from UliEngineering.SignalProcessing.Chunks import * 7 | from parameterized import parameterized 8 | import concurrent.futures 9 | import numpy as np 10 | import unittest 11 | 12 | class TestGenerateSinewave(unittest.TestCase): 13 | @parameterized.expand([ 14 | (1.,), 15 | (2.,), 16 | (3.,), 17 | (4.,), 18 | (5.,), 19 | (6.,), 20 | (7.,), 21 | (8.,), 22 | (9.,), 23 | (10.,), 24 | (10.234,), 25 | (11.,), 26 | ]) 27 | def testPhaseShift(self, frequency): 28 | """Test if 0/360/720° phase shift matches, and 180/540° matches as wel""" 29 | sw0 = sine_wave(frequency, 1000.0, amplitude=1., length=5.0, phaseshift=0.0) 30 | sw180 = sine_wave(frequency, 1000.0, amplitude=1., length=5.0, phaseshift=180.0) 31 | sw360 = sine_wave(frequency, 1000.0, amplitude=1., length=5.0, phaseshift=360.0) 32 | sw540 = sine_wave(frequency, 1000.0, amplitude=1., length=5.0, phaseshift=540.0) 33 | sw720 = sine_wave(frequency, 1000.0, amplitude=1., length=5.0, phaseshift=720.0) 34 | # Test in-phase 35 | assert_allclose(sw0, sw360, atol=1e-7) 36 | assert_allclose(sw0, sw720, atol=1e-7) 37 | assert_allclose(sw180, sw540, atol=1e-7) 38 | # Test out-of-phase 39 | assert_allclose(sw0, -sw180, atol=1e-7) 40 | 41 | def testOffset(self): 42 | sw1 = sine_wave(25., 400.0, 1.0, 10.) 43 | sw2 = sine_wave(25., 400.0, 1.0, 10., offset=2.5) 44 | assert_allclose(sw1, sw2 - 2.5, atol=1e-7) 45 | 46 | 47 | 48 | class TestGenerateWaves(unittest.TestCase): 49 | @parameterized.expand([ 50 | (sine_wave,), 51 | (cosine_wave,), 52 | (square_wave,), 53 | (triangle_wave,), 54 | (sawtooth,), 55 | (inverse_sawtooth,), 56 | ]) 57 | def testByFFT(self, fn): 58 | """Test sine_wave by computing FFT dominant frequency""" 59 | sw = fn(25., 400.0, 1.0, 10.) 60 | fft = compute_fft(sw, 400.) 61 | df = fft.dominant_frequency() 62 | self.assertTrue(abs(df - 25.0) < 0.1) 63 | -------------------------------------------------------------------------------- /tests/SignalProcessing/TestWeight.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_equal 4 | from UliEngineering.SignalProcessing.Weight import * 5 | from parameterized import parameterized 6 | import concurrent.futures 7 | import numpy as np 8 | import datetime 9 | import unittest 10 | 11 | class TestWeighHalves(unittest.TestCase): 12 | def testWeighHalves(self): 13 | # Empty array 14 | assert_allclose(weigh_halves(np.zeros(0)), (0, 0)) 15 | # Even array size 16 | assert_allclose(weigh_halves(np.arange(4)), (1., 5.)) 17 | # Odd array size 18 | assert_allclose(weigh_halves(np.arange(5)), (2., 8.)) 19 | 20 | class TestWeightSymmetry(unittest.TestCase): 21 | def testWeightSymmetry(self): 22 | assert_approx_equal(weight_symmetry(0.5, 0.5), 1.0) 23 | assert_approx_equal(weight_symmetry(0.02, 0.98), 0.04) 24 | assert_approx_equal(weight_symmetry(0.0, 1.0), 0.0) 25 | # Independence of scale 26 | assert_approx_equal(weight_symmetry(0.02*13, 0.98*13), 0.04) 27 | # Example usecase 28 | assert_approx_equal(weight_symmetry(*weigh_halves(np.arange(4))), 1/3.) 29 | assert_approx_equal(weight_symmetry(*weigh_halves(np.arange(5))), 0.4) 30 | -------------------------------------------------------------------------------- /tests/SignalProcessing/TestWindow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_equal 4 | from UliEngineering.SignalProcessing.Window import * 5 | from parameterized import parameterized 6 | import concurrent.futures 7 | import numpy as np 8 | import datetime 9 | import unittest 10 | 11 | class TestWindow(unittest.TestCase): 12 | def testWindowFunctor(self): 13 | data = np.random.random_sample(1000) 14 | ftor = WindowFunctor(len(data), "blackman") 15 | # normal 16 | result = ftor(data) 17 | assert_allclose(result, data * np.blackman(1000)) 18 | # inplace 19 | result = ftor(data, inplace=True) 20 | assert_allclose(result, data) 21 | 22 | def testWindow(self): 23 | data = np.random.random_sample(1000) 24 | # normal 25 | result = create_and_apply_window(data, "blackman") 26 | assert_allclose(result, data * np.blackman(1000)) 27 | # inplace 28 | result = create_and_apply_window(data, inplace=True) 29 | assert_allclose(result, data) 30 | -------------------------------------------------------------------------------- /tests/SignalProcessing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/SignalProcessing/__init__.py -------------------------------------------------------------------------------- /tests/Utils/TestCompression.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os 4 | from subprocess import check_output 5 | from UliEngineering.Utils.Compression import * 6 | from UliEngineering.Utils.Temporary import * 7 | import unittest 8 | 9 | class TestAutoOpen(unittest.TestCase): 10 | def setUp(self): 11 | # Use auto-managed temporary files 12 | self.tempfiles = AutoDeleteTempfileGenerator() 13 | self.tempdir = self.tempfiles.mkdtemp() 14 | 15 | def testAutoOpen(self): 16 | gzfile = os.path.join(self.tempdir, "test.gz") 17 | bzfile = os.path.join(self.tempdir, "test.bz2") 18 | xzfile = os.path.join(self.tempdir, "test.xz") 19 | # Create files 20 | check_output("echo abc | gzip -c > {0}".format(gzfile), shell=True) 21 | check_output("echo def | bzip2 -c > {0}".format(bzfile), shell=True) 22 | check_output("echo ghi | xz -c > {0}".format(xzfile), shell=True) 23 | # Check output 24 | with auto_open(gzfile) as infile: 25 | self.assertEqual("abc\n", infile.read()) 26 | with auto_open(bzfile) as infile: 27 | self.assertEqual("def\n", infile.read()) 28 | with auto_open(xzfile) as infile: 29 | self.assertEqual("ghi\n", infile.read()) 30 | 31 | def testInvalidExtension(self): 32 | "Test auto_open with a .foo file" 33 | # No need to actually write the file! 34 | with self.assertRaises(ValueError): 35 | filename = os.path.join(self.tempdir, "test.foo") 36 | auto_open(filename) 37 | -------------------------------------------------------------------------------- /tests/Utils/TestFiles.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import io 4 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_equal 5 | from UliEngineering.Utils.Files import * 6 | from UliEngineering.Utils.Temporary import * 7 | import unittest 8 | import os.path 9 | 10 | class TestFileUtils(unittest.TestCase): 11 | def setUp(self): 12 | self.tmp = AutoDeleteTempfileGenerator() 13 | 14 | def testCountLinesFilelike(self): 15 | self.assertEqual(1, count_lines(io.StringIO("foo"))) 16 | self.assertEqual(1, count_lines(io.StringIO("foo\n"))) 17 | self.assertEqual(1, count_lines(io.StringIO("foo\n\n"))) 18 | self.assertEqual(1, count_lines(io.StringIO("foo\n\n\n"))) 19 | self.assertEqual(2, count_lines(io.StringIO("foo\n\n f\n"))) 20 | self.assertEqual(2, count_lines(io.StringIO("foo\na\n\n"))) 21 | self.assertEqual(2, count_lines(io.StringIO("foo\n\n\na"))) 22 | 23 | self.assertEqual(3, count_lines(io.StringIO("foo\r\n\r\n\r\n\na\r\na\n\n\n\r\n\r\r\n"))) 24 | 25 | def testCountLinesTempfile(self): 26 | # Test 1 27 | handle, fname = self.tmp.mkftemp() 28 | handle.write("foo\n\n\na") 29 | handle.close() 30 | self.assertEqual(2, count_lines(fname)) 31 | # Test 2 32 | handle, fname = self.tmp.mkftemp() 33 | handle.write("foo\r\n\r\n\r\n\na\r\na\n\n\n\r\n\r\r\n") 34 | handle.close() 35 | self.assertEqual(3, count_lines(fname)) 36 | 37 | def testExtractColumn(self): 38 | handle, fname = self.tmp.mkftemp() 39 | handle.write("foo\nbar\n\na") 40 | handle.close() 41 | # Read back 42 | self.assertEqual(extract_column(fname), ["foo", "bar", "a"]) 43 | 44 | def testExtractNumericColumn(self): 45 | handle, fname = self.tmp.mkftemp() 46 | handle.write("3.2\n2.4\n\n1.5") 47 | handle.close() 48 | # Read back 49 | assert_allclose(extract_numeric_column(fname), [3.2, 2.4, 1.5]) 50 | 51 | def testTextIO(self): 52 | tmpdir = self.tmp.mkdtemp() 53 | fname = os.path.join(tmpdir, "test.txt") 54 | 55 | write_textfile(fname, "foobar") 56 | # Check file 57 | self.assertTrue(os.path.isfile(fname)) 58 | with open(fname) as infile: 59 | self.assertEqual(infile.read(), "foobar") 60 | # Read back 61 | txt = read_textfile(os.path.join(tmpdir, "test.txt")) 62 | self.assertEqual(txt, "foobar") 63 | 64 | def test_list_recursive(self): 65 | tmpdir = self.tmp.mkdtemp() 66 | # Make platform-dependent filename 67 | filename_test2 = os.path.join("dir", "test2.txt") 68 | # Create test files 69 | write_textfile(os.path.join(tmpdir, "test.txt"), "") 70 | write_textfile(os.path.join(tmpdir, filename_test2), "") 71 | 72 | self.assertEqual(["test.txt", filename_test2], 73 | list(list_recursive(tmpdir, relative=True, files_only=True))) 74 | self.assertEqual(["test.txt", "dir/", filename_test2], 75 | list(list_recursive(tmpdir, relative=True, files_only=False))) 76 | self.assertEqual([os.path.join(tmpdir, "test.txt"), 77 | os.path.join(tmpdir, filename_test2)], 78 | list(list_recursive(tmpdir, relative=False, files_only=True))) 79 | 80 | def test_(self): 81 | inp = ['ne_10m_admin_0_countries.README.html', 82 | 'ne_10m_admin_0_countries.VERSION.txt', 83 | 'ne_10m_admin_0_countries.dbf', 84 | 'ne_10m_admin_0_countries.prj', 85 | 'ne_10m_admin_0_countries.shp', 86 | 'ne_10m_admin_0_countries.shx', 87 | 'ne_10m_admin_0_countries.cpg'] 88 | exp = [['ne_10m_admin_0_countries.dbf', 89 | 'ne_10m_admin_0_countries.prj', 90 | 'ne_10m_admin_0_countries.shp']] 91 | self.assertEqual(exp, list(find_datasets_by_extension( 92 | inp, (".dbf", ".prj", ".shp")))) -------------------------------------------------------------------------------- /tests/Utils/TestJSON.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Utils.JSON import * 5 | import unittest 6 | 7 | class TestNumpyEncoder(unittest.TestCase): 8 | def testNDArrayEncoding(self): 9 | arr = np.asarray([1, 2, 3, 5]) 10 | s = json.dumps(arr, cls=NumPyEncoder) 11 | self.assertEqual(s, "[1, 2, 3, 5]") 12 | 13 | def testNDMultidimensionalArrayEncoding(self): 14 | arr = np.asarray([[1, 1], [2, 2], [3, 3], [5, 5]]) 15 | s = json.dumps(arr, cls=NumPyEncoder) 16 | self.assertEqual(s, "[[1, 1], [2, 2], [3, 3], [5, 5]]") 17 | 18 | def testNPFloatEncoding(self): 19 | arr = [np.float64(75), 20 | np.float64(31)] 21 | print(arr) 22 | self.assertTrue(isinstance(arr[0], np.generic)) 23 | s = json.dumps(arr, cls=NumPyEncoder) 24 | self.assertEqual(s, "[75.0, 31.0]") 25 | 26 | def testNPFloatEncoding(self): 27 | arr = [np.int64(75), 28 | np.int64(31)] 29 | print(arr) 30 | self.assertTrue(isinstance(arr[0], np.generic)) 31 | s = json.dumps(arr, cls=NumPyEncoder) 32 | self.assertEqual(s, "[75, 31]") 33 | 34 | def testOtherEncoding(self): 35 | arr = {"a": "b"} 36 | self.assertEqual(json.dumps(arr, cls=NumPyEncoder), '{"a": "b"}') 37 | self.assertEqual(json.dumps("gaa", cls=NumPyEncoder), '"gaa"') 38 | self.assertEqual(json.dumps(None, cls=NumPyEncoder), 'null') 39 | 40 | def test_invalid_encoding(self): 41 | with self.assertRaises(TypeError): 42 | json.dumps(set([1,2,3]), cls=NumPyEncoder) 43 | -------------------------------------------------------------------------------- /tests/Utils/TestNumPy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_allclose, assert_array_equal 4 | from UliEngineering.Utils.NumPy import * 5 | from parameterized import parameterized 6 | import numpy as np 7 | import unittest 8 | 9 | class TestDynamicArrayResize(unittest.TestCase): 10 | def test_numpy_resize_insert(self): 11 | arr = np.zeros(3) 12 | # Insert within bounds 13 | arr = numpy_resize_insert(arr, 1, 0) 14 | arr = numpy_resize_insert(arr, 2, 1) 15 | arr = numpy_resize_insert(arr, 3, 2) 16 | self.assertEqual(arr.size, 3) 17 | assert_array_equal(arr, [1, 2, 3]) 18 | # Resize 19 | arr = numpy_resize_insert(arr, 4, 3) 20 | self.assertEqual(arr.size, 1003) # 3 orig size + min growth = 1000 21 | # Remove extra size 22 | arr = np.resize(arr, 4) 23 | assert_array_equal(arr, [1, 2, 3, 4]) 24 | 25 | 26 | class TestInvertBijection(unittest.TestCase): 27 | def testSimple(self): 28 | assert_allclose([0, 1, 2, 3], invert_bijection(np.arange(4))) 29 | assert_allclose([2, 0, 1, 3], invert_bijection([1, 2, 0, 3])) 30 | assert_allclose([1, 0, 2, 3], invert_bijection([1, 0, 2, 3])) 31 | 32 | def testEmpty(self): 33 | self.assertEqual((0,), invert_bijection([]).shape) 34 | 35 | 36 | class TestApplyPairwise1D(unittest.TestCase): 37 | def testSimple(self): 38 | assert_allclose([[0,0,0], [1,2,3], [2,4,6]], 39 | apply_pairwise_1d(np.arange(3), np.arange(1,4), lambda a, b: a * b)) 40 | 41 | 42 | class TestNgrams(unittest.TestCase): 43 | 44 | def test_ngrams1(self): 45 | inp = np.arange(5) # 0..4 46 | closed = np.asarray([[0,1],[1,2],[2,3],[3,4],[4,0]]) 47 | opened = np.asarray([[0,1],[1,2],[2,3],[3,4]]) 48 | #print(np.asarray(list(ngrams(inp, 2, closed=True)))) 49 | #print(closed) 50 | assert_allclose(closed, np.asarray(list(ngrams(inp, 2, closed=True)))) 51 | assert_allclose(opened, np.asarray(list(ngrams(inp, 2, closed=False)))) 52 | 53 | def test_ngrams2(self): 54 | inp = np.asarray([[0, 1], [1, 2], [2, 3]]) 55 | closed = np.asarray([[[0, 1], [1, 2]], 56 | [[1, 2], [2, 3]], 57 | [[2, 3], [0, 1]]]) 58 | opened = np.asarray([[[0, 1], [1, 2]], 59 | [[1, 2], [2, 3]]]) 60 | #print(np.asarray(list(ngrams(inp, 2, closed=True)))) 61 | #print(closed) 62 | assert_allclose(closed, np.asarray(list(ngrams(inp, 2, closed=True)))) 63 | assert_allclose(opened, np.asarray(list(ngrams(inp, 2, closed=False)))) 64 | 65 | class TestPivotSplit(unittest.TestCase): 66 | def test_pivot_split(self): 67 | self.assertEqual([[0,1],[2,3,4,5]], list(split_by_pivot([0,1,2,3,4,5], [2]))) 68 | self.assertEqual([[0,1],[2,3],[4,5]], list(split_by_pivot([0,1,2,3,4,5], [2,4]))) 69 | self.assertEqual([[],[0,1],[2,3],[4,5]], list(split_by_pivot([0,1,2,3,4,5], [0,2,4]))) 70 | 71 | class TestDatetime64Now(unittest.TestCase): 72 | def test_datetime64_now(self): 73 | self.assertEqual(type(datetime64_now()), np.datetime64) 74 | 75 | class TestDatatypeResolution(unittest.TestCase): 76 | 77 | @parameterized.expand([ 78 | ('us',), 79 | ('ms',), 80 | ('s',), 81 | ('D',), 82 | ('h',), 83 | ('ns',), 84 | ]) 85 | def test_timedelta64_resolution(self, res): 86 | self.assertEqual(timedelta64_resolution(np.timedelta64(100, res)), res) -------------------------------------------------------------------------------- /tests/Utils/TestParser.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Utils.Parser import * 5 | import unittest 6 | 7 | class TestParseIntFloat(unittest.TestCase): 8 | def test_parse_int_or_float(self): 9 | self.assertEqual(parse_int_or_float("1"), 1) 10 | self.assertEqual(parse_int_or_float("1.0"), 1.0) 11 | self.assertEqual(parse_int_or_float("-2.225"), -2.225) 12 | 13 | def test_parse_int_or_float_err(self): 14 | with self.assertRaises(ValueError): 15 | parse_int_or_float("3ahtj4") 16 | 17 | def test_try_parse_int_or_float(self): 18 | self.assertEqual(try_parse_int_or_float("1"), 1) 19 | self.assertEqual(try_parse_int_or_float("1.0"), 1.0) 20 | self.assertEqual(try_parse_int_or_float("-2.225"), -2.225) 21 | self.assertEqual(try_parse_int_or_float("bx3613"), "bx3613") 22 | -------------------------------------------------------------------------------- /tests/Utils/TestRange.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os.path 4 | from numpy.testing import assert_approx_equal 5 | from UliEngineering.Utils.Range import * 6 | import unittest 7 | 8 | class TestValueRange(unittest.TestCase): 9 | def testConstructorUnit(self): 10 | vr = ValueRange(-1.0, 1.0, "A") 11 | self.assertEqual(str(vr), "ValueRange('-1.000 A', '1.000 A')") 12 | assert_approx_equal(vr.min, -1.0) 13 | assert_approx_equal(vr.max, 1.0) 14 | self.assertEqual(vr.unit, "A") 15 | 16 | def testConstructorNoUnit(self): 17 | vr = ValueRange(-1.0, 1.0) 18 | self.assertEqual(str(vr), "ValueRange('-1.000', '1.000')") 19 | assert_approx_equal(vr.min, -1.0) 20 | assert_approx_equal(vr.max, 1.0) 21 | self.assertEqual(vr.unit, None) 22 | 23 | -------------------------------------------------------------------------------- /tests/Utils/TestSlice.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from numpy.testing import assert_approx_equal 4 | from UliEngineering.Utils.Slice import * 5 | import unittest 6 | 7 | class TestShiftSlice(unittest.TestCase): 8 | def test_shift_slice(self): 9 | self.assertEqual(shift_slice(slice(9123, 10000), by=0), slice(9123, 10000)) 10 | self.assertEqual(shift_slice(slice(9123, 10000), by=10), slice(9133, 10010)) -------------------------------------------------------------------------------- /tests/Utils/TestString.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | from UliEngineering.Utils.String import * 4 | from parameterized import parameterized 5 | import unittest 6 | 7 | class TestSplitNth(unittest.TestCase): 8 | def testSimple(self): 9 | self.assertEqual("a", split_nth("a,b,c,d,e,f")) 10 | self.assertEqual("a", split_nth("a,b,c,d,e,f", nth=1)) 11 | self.assertEqual("b", split_nth("a,b,c,d,e,f", nth=2)) 12 | self.assertEqual("c", split_nth("a,b,c,d,e,f", nth=3)) 13 | self.assertEqual("d", split_nth("a,b,c,d,e,f", nth=4)) 14 | self.assertEqual("e", split_nth("a,b,c,d,e,f", nth=5)) 15 | self.assertEqual("f", split_nth("a,b,c,d,e,f", nth=6)) 16 | 17 | def testMultiChar(self): 18 | self.assertEqual("ab", split_nth("ab,cd,ef,gh,ij,kl")) 19 | self.assertEqual("ab", split_nth("ab,cd,ef,gh,ij,kl", nth=1)) 20 | self.assertEqual("cd", split_nth("ab,cd,ef,gh,ij,kl", nth=2)) 21 | self.assertEqual("ef", split_nth("ab,cd,ef,gh,ij,kl", nth=3)) 22 | self.assertEqual("gh", split_nth("ab,cd,ef,gh,ij,kl", nth=4)) 23 | self.assertEqual("ij", split_nth("ab,cd,ef,gh,ij,kl", nth=5)) 24 | self.assertEqual("kl", split_nth("ab,cd,ef,gh,ij,kl", nth=6)) 25 | 26 | def testOtherDelimiter(self): 27 | self.assertEqual("a", split_nth("a;b;c;d;e;f", delimiter=';')) 28 | self.assertEqual("a", split_nth("a;b;c;d;e;f", delimiter=';', nth=1)) 29 | self.assertEqual("b", split_nth("a;b;c;d;e;f", delimiter=';', nth=2)) 30 | self.assertEqual("c", split_nth("a;b;c;d;e;f", delimiter=';', nth=3)) 31 | self.assertEqual("d", split_nth("a;b;c;d;e;f", delimiter=';', nth=4)) 32 | self.assertEqual("e", split_nth("a;b;c;d;e;f", delimiter=';', nth=5)) 33 | self.assertEqual("f", split_nth("a;b;c;d;e;f", delimiter=';', nth=6)) 34 | self.assertEqual("", split_nth("aa;bb;;dd;ee;ff", delimiter=';', nth=3)) 35 | self.assertEqual("", split_nth("aa;bb;;", delimiter=';', nth=3)) 36 | self.assertEqual("", split_nth("aa;bb;;", delimiter=';', nth=4)) 37 | 38 | def testEmpty(self): 39 | self.assertEqual("", split_nth("aa,bb,,dd,ee,ff", nth=3)) 40 | self.assertEqual("", split_nth("aa,bb,,", nth=3)) 41 | self.assertEqual("", split_nth("aa,bb,,", nth=4)) 42 | self.assertEqual("", split_nth("", nth=1)) 43 | 44 | @parameterized.expand(["a", "abc", "abcdef", "abc,def"]) 45 | def testInvalidFirst(self, param): 46 | with self.assertRaises(ValueError): 47 | split_nth(param, nth=3) 48 | 49 | def testInvalidOther(self): 50 | with self.assertRaises(ValueError): 51 | split_nth("abc,def", nth=3) 52 | 53 | class TestStringUtils(unittest.TestCase): 54 | def test_partition_at_numeric_to_nonnumeric_boundary(self): 55 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("foo.123bar"), ("foo.123", "bar")) 56 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("123s"), ("123", "s")) 57 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("123"), ("123", "")) 58 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("foo"), ("foo", "")) 59 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("foo1bar"), ("foo1", "bar")) 60 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("foo.123"), ("foo.123", "")) 61 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("foo123bar"), ("foo123", "bar")) 62 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary("123foo456"), ("123", "foo456")) 63 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary(s=""), ("", "")) 64 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary(s="123.456km"), ("123.456", "km")) 65 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary(s="1."), ("1.", "")) 66 | 67 | def test_negative_partition_at_numeric_to_nonnumeric_boundary(self): 68 | self.assertEqual(partition_at_numeric_to_nonnumeric_boundary(s="-123.456km"), ("-123.456", "km")) 69 | 70 | -------------------------------------------------------------------------------- /tests/Utils/TestTemporary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import os.path 4 | from UliEngineering.Utils.Temporary import * 5 | import unittest 6 | 7 | class TestTemporary(unittest.TestCase): 8 | def testMkstemp(self): 9 | tgen = AutoDeleteTempfileGenerator() 10 | # Create file and check if it exists 11 | (_, fname) = tgen.mkstemp() 12 | self.assertTrue(os.path.isfile(fname)) 13 | # Delete and check if file has vanished 14 | tgen.delete_all() 15 | self.assertFalse(os.path.isfile(fname)) 16 | 17 | 18 | def testMkftemp(self): 19 | tgen = AutoDeleteTempfileGenerator() 20 | # Create file and check if it exists 21 | (handle, fname) = tgen.mkftemp() 22 | # Test if we can do stuff with the file as with any open()ed file 23 | handle.write("foo") 24 | handle.close() 25 | # Should not be deleted on close 26 | self.assertTrue(os.path.isfile(fname)) 27 | # Delete and check if file has vanished 28 | tgen.delete_all() 29 | self.assertFalse(os.path.isfile(fname)) 30 | 31 | def testMkdtemp(self): 32 | tgen = AutoDeleteTempfileGenerator() 33 | # Create file and check if it exists 34 | dirname = tgen.mkdtemp() 35 | self.assertTrue(os.path.isdir(dirname)) 36 | # Delete and check if file has vanished 37 | tgen.delete_all() 38 | self.assertFalse(os.path.isdir(dirname)) 39 | 40 | -------------------------------------------------------------------------------- /tests/Utils/TestZIP.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | import io 4 | from numpy.testing import assert_approx_equal, assert_allclose, assert_array_equal 5 | from UliEngineering.Utils.ZIP import * 6 | from UliEngineering.Utils.Temporary import * 7 | import unittest 8 | 9 | class TestFileUtils(unittest.TestCase): 10 | def setUp(self): 11 | self.tmp = AutoDeleteTempfileGenerator() 12 | 13 | def create_zip_from_directory(self): 14 | pass #TODO -------------------------------------------------------------------------------- /tests/Utils/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/Utils/__init__.py -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ulikoehler/UliEngineering/63bc4a36854430afd7ce1e1aa0c68f476c29db31/tests/__init__.py -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37,py38,py39,py3.10,py3.11 3 | 4 | [gh-actions] 5 | python = 6 | 3.7: python3.7 7 | 3.8: python3.8 8 | 3.9: python3.9 9 | 3.10: python3.10, 10 | 3.11: python3.11 11 | 12 | [testenv] 13 | deps = 14 | pytest 15 | pytest-cov 16 | parameterized 17 | numpy 18 | scipy 19 | commands = 20 | python -m coverage run -p -m pytest 21 | python -m coverage combine 22 | python -m coverage report -m --skip-covered 23 | python -m coverage xml 24 | --------------------------------------------------------------------------------