diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21121bf..c9fb140 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: python-version: 3.7 - name: Install release tooling run: | - pip install twine wheel + pip install -r requirements/dev.txt - name: Build package run: | python setup.py sdist bdist_wheel diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 37a014b..dbb7448 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,8 +16,8 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | - pip install pytest hypothesis coverage pytest-cov pytest-xdist - pip install -r requirements.txt + pip install -r requirements/common.txt + pip install -r requirements/tests.txt - name: Fetch uses: actions/checkout@master - name: Test diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 0ba88c0..199fddd 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -11,7 +11,7 @@ build: jobs: post_create_environment: - pip install . - - pip install -r docs/source/requirements.txt + - pip install -r requirements/docs.txt sphinx: configuration: docs/source/conf.py diff --git a/README.md b/README.md index b4483e6..0110a8f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - [Source code](https://github.com/AndreaBlengino/gearpy/tree/master/gearpy) - [Issue tracker](https://github.com/AndreaBlengino/gearpy/issues) - [Documentation](https://gearpy.readthedocs.io/en/latest/?badge=latest) -- [Installation](https://gearpy.readthedocs.io/en/latest/installation.html) +- [Installation](https://gearpy.readthedocs.io/en/latest/get_started.html) - [Usage examples](https://gearpy.readthedocs.io/en/latest/examples/index.html) - [PyPI](https://pypi.org/project/gearpy/) - [GNU General Public License v3.0](https://github.com/AndreaBlengino/gearpy/blob/master/LICENSE) diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css new file mode 100644 index 0000000..1c9f8a3 --- /dev/null +++ b/docs/source/_static/custom.css @@ -0,0 +1,4 @@ +.sidebar-container, .toc-drawer { + box-sizing: border-box; + width: 20em; +} diff --git a/docs/source/conf.py b/docs/source/conf.py index bd686e0..2aa0d68 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -33,5 +33,6 @@ html_theme = 'furo' html_static_path = ['_static'] +html_css_files = ['custom.css'] add_module_names = False html_title = 'gearpy' diff --git a/docs/source/examples/example_1/images/plot_1.png b/docs/source/examples/example_1/images/plot_1.png deleted file mode 100644 index e8e3a9a..0000000 Binary files a/docs/source/examples/example_1/images/plot_1.png and /dev/null differ diff --git a/docs/source/examples/example_1/images/plot_2.png b/docs/source/examples/example_1/images/plot_2.png deleted file mode 100644 index c68dc29..0000000 Binary files a/docs/source/examples/example_1/images/plot_2.png and /dev/null differ diff --git a/docs/source/examples/example_1/images/plot_3.png b/docs/source/examples/example_1/images/plot_3.png deleted file mode 100644 index 86ace8f..0000000 Binary files a/docs/source/examples/example_1/images/plot_3.png and /dev/null differ diff --git a/docs/source/examples/index.rst b/docs/source/examples/index.rst index 58562af..45d7b0c 100644 --- a/docs/source/examples/index.rst +++ b/docs/source/examples/index.rst @@ -4,4 +4,5 @@ Usage Examples .. toctree:: - example_1/index + simple_transmission_chain/index + structural_analysis/index diff --git a/docs/source/examples/example_1/README.md b/docs/source/examples/simple_transmission_chain/README.md similarity index 87% rename from docs/source/examples/example_1/README.md rename to docs/source/examples/simple_transmission_chain/README.md index d6a2842..3b537dc 100644 --- a/docs/source/examples/example_1/README.md +++ b/docs/source/examples/simple_transmission_chain/README.md @@ -8,7 +8,9 @@ A *flywheel* and the *gear 1* are connected to the *DC motor* output shaft and rotate with it. The *gear 2* mates with *gear 1* and is connected to *gear 3* through a rigid shaft, so *gear 2* and *gear 3* rotate together. The *gear 3* mates with *gear 4*, to which is connected -to the external load. +to the external load. +The analysis is focused on transmission elements kinematics and torques, +without computing structural strength of the gear teeth. ### Model Set Up @@ -115,13 +117,13 @@ transmission.snapshot(target_time = Time(1000, 'sec')) ```text Mechanical Transmission Status at Time = 1000 sec - angular position (rad) angular speed (rad/s) angular acceleration (rad/s^2) torque (Nm) driving torque (Nm) load torque (Nm) -motor 459683.165617 570.175248 0.019532 0.001199 0.006199 0.005 -flywheel 459683.165617 570.175248 0.019532 0.001199 0.006199 0.005 -gear 1 459683.165617 570.175248 0.019532 0.001199 0.006199 0.005 -gear 2 229841.582808 285.087624 0.009766 0.001158 0.011158 0.01 -gear 3 229841.582808 285.087624 0.009766 0.001158 0.011158 0.01 -gear 4 76708.893383 95.032447 0.003239 0.000126 0.030126 0.03 + angular position (rad) angular speed (rad/s) angular acceleration (rad/s^2) torque (Nm) driving torque (Nm) load torque (Nm) tangential force (N) bending stress (MPa) contact stress (MPa) +motor 459683.165617 570.175248 0.019532 0.001199 0.006199 0.005 NaN NaN NaN +flywheel 459683.165617 570.175248 0.019532 0.001199 0.006199 0.005 NaN NaN NaN +gear 1 459683.165617 570.175248 0.019532 0.001199 0.006199 0.005 NaN NaN NaN +gear 2 229841.582808 285.087624 0.009766 0.001158 0.011158 0.01 NaN NaN NaN +gear 3 229841.582808 285.087624 0.009766 0.001158 0.011158 0.01 NaN NaN NaN +gear 4 76708.893383 95.032447 0.003239 0.000126 0.030126 0.03 NaN NaN NaN ``` The default unit used for torques are not so convenient in this case, so @@ -136,18 +138,18 @@ transmission.snapshot(target_time = Time(1000, 'sec'), ```text Mechanical Transmission Status at Time = 1000 sec - angular position (rad) angular speed (rad/s) angular acceleration (rad/s^2) torque (mNm) driving torque (mNm) load torque (mNm) -motor 459683.165617 570.175248 0.019532 1.198832 6.198832 5.0 -flywheel 459683.165617 570.175248 0.019532 1.198832 6.198832 5.0 -gear 1 459683.165617 570.175248 0.019532 1.198832 6.198832 5.0 -gear 2 229841.582808 285.087624 0.009766 1.157897 11.157897 10.0 -gear 3 229841.582808 285.087624 0.009766 1.157897 11.157897 10.0 -gear 4 76708.893383 95.032447 0.003239 0.126322 30.126322 30.0 + angular position (rad) angular speed (rad/s) angular acceleration (rad/s^2) torque (mNm) driving torque (mNm) load torque (mNm) tangential force (N) bending stress (MPa) contact stress (MPa) +motor 459683.165617 570.175248 0.019532 1.198832 6.198832 5.0 NaN NaN NaN +flywheel 459683.165617 570.175248 0.019532 1.198832 6.198832 5.0 NaN NaN NaN +gear 1 459683.165617 570.175248 0.019532 1.198832 6.198832 5.0 NaN NaN NaN +gear 2 229841.582808 285.087624 0.009766 1.157897 11.157897 10.0 NaN NaN NaN +gear 3 229841.582808 285.087624 0.009766 1.157897 11.157897 10.0 NaN NaN NaN +gear 4 76708.893383 95.032447 0.003239 0.126322 30.126322 30.0 NaN NaN NaN ``` Notice that the load torque applied on the *gear 4* is exactly the constant external torque we defined beforehand. -After 1000 seconds after the simulation start, the driving torque +About 1000 seconds after the simulation start, the driving torque applied on *gear 4* is almost equal to the load torque on it, resulting in a very tiny angular acceleration. As a result, we can conclude that the system is almost in a stationary condition after 1000 seconds from @@ -156,7 +158,7 @@ We can get a more general view of the system by plotting the time variables of each element with respect to time: ```python -transmission.plot(figsize = (12, 8)) +transmission.plot(figsize = (12, 9)) ``` ![](images/plot_1.png) @@ -182,7 +184,7 @@ sorts: - the elements: the motor on the left and following elements proceeding to the right, - the time variables: kinematic variables at the top and torques in -the last row, in the same plot. +the second-last row and force in the last row, in the same plot. We can see that at 1000 seconds the angular speeds of *motor* and *gear 4* are almost constant, confirming the insight previously diff --git a/docs/source/examples/simple_transmission_chain/images/plot_1.png b/docs/source/examples/simple_transmission_chain/images/plot_1.png new file mode 100644 index 0000000..31743ba Binary files /dev/null and b/docs/source/examples/simple_transmission_chain/images/plot_1.png differ diff --git a/docs/source/examples/simple_transmission_chain/images/plot_2.png b/docs/source/examples/simple_transmission_chain/images/plot_2.png new file mode 100644 index 0000000..71b553d Binary files /dev/null and b/docs/source/examples/simple_transmission_chain/images/plot_2.png differ diff --git a/docs/source/examples/simple_transmission_chain/images/plot_3.png b/docs/source/examples/simple_transmission_chain/images/plot_3.png new file mode 100644 index 0000000..98deeb1 Binary files /dev/null and b/docs/source/examples/simple_transmission_chain/images/plot_3.png differ diff --git a/docs/source/examples/example_1/images/scheme.png b/docs/source/examples/simple_transmission_chain/images/scheme.png similarity index 100% rename from docs/source/examples/example_1/images/scheme.png rename to docs/source/examples/simple_transmission_chain/images/scheme.png diff --git a/docs/source/examples/simple_transmission_chain/index.rst b/docs/source/examples/simple_transmission_chain/index.rst new file mode 100644 index 0000000..ebf804c --- /dev/null +++ b/docs/source/examples/simple_transmission_chain/index.rst @@ -0,0 +1,11 @@ +Simple Transmission Chain +========================= + + +.. mdinclude:: README.md + + +.. toctree:: + :hidden: + + README diff --git a/docs/source/examples/example_1/example_1.py b/docs/source/examples/simple_transmission_chain/simple_transmission_chain.py similarity index 98% rename from docs/source/examples/example_1/example_1.py rename to docs/source/examples/simple_transmission_chain/simple_transmission_chain.py index 22c82e2..a2ca16c 100644 --- a/docs/source/examples/example_1/example_1.py +++ b/docs/source/examples/simple_transmission_chain/simple_transmission_chain.py @@ -75,7 +75,7 @@ def ext_torque(time, angular_position, angular_speed): load_torque_unit = 'mNm') -transmission.plot(figsize = (12, 8)) +transmission.plot(figsize = (12, 9)) transmission.plot(elements = ['gear 4', motor], variables = ['torque', 'driving torque', 'angular speed', 'load torque'], diff --git a/docs/source/examples/structural_analysis/README.md b/docs/source/examples/structural_analysis/README.md new file mode 100644 index 0000000..2ff2d97 --- /dev/null +++ b/docs/source/examples/structural_analysis/README.md @@ -0,0 +1,260 @@ +### System in Analysis + +The mechanical transmission to be studied is reported in the image below: + +![](images/scheme.png) + +A *flywheel* and the *gear 1* are connected to the *DC motor* output +shaft and rotate with it. The *gear 2* mates with *gear 1* and is +connected to *gear 3* through a rigid shaft, so *gear 2* and *gear 3* +rotate together. The *gear 3* mates with *gear 4*, to which is connected +to the external load. +The first step is the analysis of transmission elements kinematics and +torques, then the focus is set to structural strength of the gear teeth. +The teeth of all gears must be verified for static bending and contact +stress with a safety factor 1.5. + + +### Model Set Up + +As a first step, we instantiate the components of the mechanical +transmission: + +```python +from gearpy.mechanical_object import DCMotor, SpurGear, Flywheel +from gearpy.units import AngularSpeed, InertiaMoment, Length, Torque, Stress + +motor = DCMotor(name = 'motor', + no_load_speed = AngularSpeed(2000, 'rpm'), + maximum_torque = Torque(200, 'mNm'), + inertia_moment = InertiaMoment(5, 'gm^2')) +flywheel = Flywheel(name = 'flywheel', + inertia_moment = InertiaMoment(30, 'gm^2')) +gear_1 = SpurGear(name = 'gear 1', + n_teeth = 10, + inertia_moment = InertiaMoment(10, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_2 = SpurGear(name = 'gear 2', + n_teeth = 80, + inertia_moment = InertiaMoment(80, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_3 = SpurGear(name = 'gear 3', + n_teeth = 10, + inertia_moment = InertiaMoment(10, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_4 = SpurGear(name = 'gear 4', + n_teeth = 60, + inertia_moment = InertiaMoment(60, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +``` + +All gears are assumed to be equal, with 1 mm module, 5 mm face width, +made in steel. The steel yield stress is 250 MPa. +Then it is necessary to specify the types of connection between the +components. We choose to study a non-ideal transmission so, in order to +take into account power loss in matings due to friction, we specify a +gear mating efficiency below `$100\%$`: + +```python +from gearpy.utils import add_gear_mating, add_fixed_joint + +add_fixed_joint(master = motor, slave = flywheel) +add_fixed_joint(master = flywheel, slave = gear_1) +add_gear_mating(master = gear_1, slave = gear_2, efficiency = 0.9) +add_fixed_joint(master = gear_2, slave = gear_3) +add_gear_mating(master = gear_3, slave = gear_4, efficiency = 0.9) +``` + +We have to define the external load applied to *gear 4*. The torque +taken into account has 6 Nm constant component and a variable component +dependent on the angular position of the last gear: + +```python +def ext_torque(time, angular_position, angular_speed): + return Torque(value = 6 + 0.5*np.sin(2*np.pi/10*angular_position.to('rad').value), + unit = 'Nm') + +gear_4.external_torque = ext_torque + ``` + +Finally, it is necessary to combine all components in a transmission +object: + +```python +from gearpy.transmission import Transmission + +transmission = Transmission(motor = motor) +``` + +### Simulation Set Up + +Before performing the simulation, it is necessary to specify the initial +condition of the system in terms of angular position and speed of the +last gear in the mechanical transmission. In this case we can consider +the *gear 4* stop in the reference position: + +```python +from gearpy.units import AngularPosition + +gear_4.angular_position = AngularPosition(0, 'rad') +gear_4.angular_speed = AngularSpeed(0, 'rad/s') +``` + +Finally, we have to set up the simulation parameters: the time +discretization for the time integration and the simulation time. Now we +are ready to run the simulation:: + +```python +from gearpy.units import TimeInterval +from gearpy.solver import Solver + +solver = Solver(time_discretization = TimeInterval(0.1, 'sec'), + simulation_time = TimeInterval(100, 'sec'), + transmission = transmission) +solver.run() +``` + +### Results Analysis + +We can get a snapshot of the system at a particular time of interest: + +```python +from gearpy.units import Time + +transmission.snapshot(target_time = Time(50, 'sec')) +``` + +```text +Mechanical Transmission Status at Time = 50 sec + angular position (rad) angular speed (rad/s) angular acceleration (rad/s^2) torque (Nm) driving torque (Nm) load torque (Nm) tangential force (N) bending stress (MPa) contact stress (MPa) +motor 2221.559697 48.412745 5.886017 0.036284 0.153769 0.117485 NaN NaN NaN +flywheel 2221.559697 48.412745 5.886017 0.036284 0.153769 0.117485 NaN NaN NaN +gear 1 2221.559697 48.412745 5.886017 0.036284 0.153769 0.117485 23.497094 23.380193 218.540109 +gear 2 277.694962 6.051593 0.735752 0.167255 1.107138 0.939884 27.678462 12.696542 237.189281 +gear 3 277.694962 6.051593 0.735752 0.167255 1.107138 0.939884 187.97675 187.041543 629.467458 +gear 4 46.384583 1.02089 0.122915 0.339245 5.978548 5.639303 199.284927 94.672174 648.124502 +``` + +We can get a more general view of the system by plotting the time +variables of each element with respect to time: + +```python +transmission.plot(figsize = (10, 12)) +``` + +![](images/plot_1.png) + +After few seconds from the simulation start, the driving torque +applied on *gear 4* is almost equal to the load torque on it. After +about 10 seconds the transient is finished and the system is in a +stationary regime, where the periodicity of the external load is clearly +visible. +The kinematics of the transmission is clear, but gears stresses deserve +more attention, so we can generate a plot focused on structural time +variables of the gears: + +```python +transmission.plot(elements = ['gear 1', 'gear 2', 'gear 3', 'gear 4'], + variables = ['angular position', 'torque', 'driving torque', 'load torque', + 'tangential force', 'bending stress', 'contact stress'], + angular_position_unit = 'rot', + figsize = (8, 6)) +``` + +![](images/plot_2.png) + +The computed stresses on gears teeth are to high, in particular the +contact stress, even higher than the steel yield stress, so we have to +improve the strength of the teeth by increasing the module and face +width. As a consequence of the increased module and face width, the gear +size will be higher, so the inertia moment increases too. + + +### Improved Model Set Up + +The transmission elements are updated as follows: + +```python +motor = DCMotor(name = 'motor', + no_load_speed = AngularSpeed(2000, 'rpm'), + maximum_torque = Torque(200, 'mNm'), + inertia_moment = InertiaMoment(5, 'gm^2')) +flywheel = Flywheel(name = 'flywheel', + inertia_moment = InertiaMoment(30, 'gm^2')) +gear_1 = SpurGear(name = 'gear 1', + n_teeth = 10, + inertia_moment = InertiaMoment(10, 'gm^2'), + module = Length(1.25, 'mm'), + face_width = Length(10, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_2 = SpurGear(name = 'gear 2', + n_teeth = 80, + inertia_moment = InertiaMoment(80, 'gm^2'), + module = Length(1.25, 'mm'), + face_width = Length(10, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_3 = SpurGear(name = 'gear 3', + n_teeth = 10, + inertia_moment = InertiaMoment(40, 'gm^2'), + module = Length(2.5, 'mm'), + face_width = Length(20, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_4 = SpurGear(name = 'gear 4', + n_teeth = 60, + inertia_moment = InertiaMoment(240, 'gm^2'), + module = Length(2.5, 'mm'), + face_width = Length(20, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +``` + +Note that *gear 1* and *gear 2* must have the same value of module to +make the mating possible, as well as *gear 3* and *gear 4*. +The other parameters of the simulation remain the same as before. + + +### Improved Model Result Analysis + +We can get a snapshot at the same time of interest as before: + +```python +transmission.snapshot(target_time = Time(50, 'sec')) +``` + +```text +Mechanical Transmission Status at Time = 50 sec + angular position (rad) angular speed (rad/s) angular acceleration (rad/s^2) torque (Nm) driving torque (Nm) load torque (Nm) tangential force (N) bending stress (MPa) contact stress (MPa) +motor 2217.156919 47.314348 5.4848 0.036905 0.154818 0.117913 NaN NaN NaN +flywheel 2217.156919 47.314348 5.4848 0.036905 0.154818 0.117913 NaN NaN NaN +gear 1 2217.156919 47.314348 5.4848 0.036905 0.154818 0.117913 18.866154 7.508917 123.849929 +gear 2 277.144615 5.914293 0.6856 0.171383 1.114691 0.943308 22.29381 4.090607 134.631358 +gear 3 277.144615 5.914293 0.6856 0.171383 1.114691 0.943308 75.464616 7.508917 126.122595 +gear 4 46.290493 0.997237 0.115219 0.359483 6.019329 5.659846 80.257717 3.812718 130.066249 +``` + +We can also get the same plot as before, in order to make a complete +comparison: + +```python +transmission.plot(elements = ['gear 1', 'gear 2', 'gear 3', 'gear 4'], + variables = ['angular position', 'torque', 'driving torque', 'load torque', + 'tangential force', 'bending stress', 'contact stress'], + angular_position_unit = 'rot', + figsize = (8, 6)) +``` + +![](images/plot_3.png) + +As desired, all stresses are decreased, the maximum contact stress is +about 153 MPa on the *gear 2* resulting in a safety factor about 1.63, +higher than the target 1.5. The bending stresses are even lower on all +gears. +We can consider these gears correctly designed for the considered +application. diff --git a/docs/source/examples/structural_analysis/images/plot_1.png b/docs/source/examples/structural_analysis/images/plot_1.png new file mode 100644 index 0000000..43547ed Binary files /dev/null and b/docs/source/examples/structural_analysis/images/plot_1.png differ diff --git a/docs/source/examples/structural_analysis/images/plot_2.png b/docs/source/examples/structural_analysis/images/plot_2.png new file mode 100644 index 0000000..d837b01 Binary files /dev/null and b/docs/source/examples/structural_analysis/images/plot_2.png differ diff --git a/docs/source/examples/structural_analysis/images/plot_3.png b/docs/source/examples/structural_analysis/images/plot_3.png new file mode 100644 index 0000000..c82233e Binary files /dev/null and b/docs/source/examples/structural_analysis/images/plot_3.png differ diff --git a/docs/source/examples/structural_analysis/images/scheme.png b/docs/source/examples/structural_analysis/images/scheme.png new file mode 100644 index 0000000..0acac25 Binary files /dev/null and b/docs/source/examples/structural_analysis/images/scheme.png differ diff --git a/docs/source/examples/example_1/index.rst b/docs/source/examples/structural_analysis/index.rst similarity index 61% rename from docs/source/examples/example_1/index.rst rename to docs/source/examples/structural_analysis/index.rst index 5d31989..aafaac0 100644 --- a/docs/source/examples/example_1/index.rst +++ b/docs/source/examples/structural_analysis/index.rst @@ -1,5 +1,5 @@ -Example 1 -========= +Structural Analysis +=================== .. mdinclude:: README.md diff --git a/docs/source/examples/structural_analysis/structural_analysis.py b/docs/source/examples/structural_analysis/structural_analysis.py new file mode 100644 index 0000000..6713526 --- /dev/null +++ b/docs/source/examples/structural_analysis/structural_analysis.py @@ -0,0 +1,160 @@ +# Import required packages, classes and functions + +from gearpy.mechanical_object import DCMotor, SpurGear, Flywheel +from gearpy.units import AngularSpeed, InertiaMoment, Length, Torque, AngularPosition, TimeInterval, Time, Stress +from gearpy.utils import add_gear_mating, add_fixed_joint +from gearpy.transmission import Transmission +from gearpy.solver import Solver +import numpy as np + + +# Model Set Up + +motor = DCMotor(name = 'motor', + no_load_speed = AngularSpeed(2000, 'rpm'), + maximum_torque = Torque(200, 'mNm'), + inertia_moment = InertiaMoment(5, 'gm^2')) +flywheel = Flywheel(name = 'flywheel', + inertia_moment = InertiaMoment(30, 'gm^2')) +gear_1 = SpurGear(name = 'gear 1', + n_teeth = 10, + inertia_moment = InertiaMoment(10, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_2 = SpurGear(name = 'gear 2', + n_teeth = 80, + inertia_moment = InertiaMoment(80, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_3 = SpurGear(name = 'gear 3', + n_teeth = 10, + inertia_moment = InertiaMoment(10, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_4 = SpurGear(name = 'gear 4', + n_teeth = 60, + inertia_moment = InertiaMoment(60, 'gm^2'), + module = Length(1, 'mm'), + face_width = Length(5, 'mm'), + elastic_modulus = Stress(210, 'GPa')) + + +add_fixed_joint(master = motor, slave = flywheel) +add_fixed_joint(master = flywheel, slave = gear_1) +add_gear_mating(master = gear_1, slave = gear_2, efficiency = 0.9) +add_fixed_joint(master = gear_2, slave = gear_3) +add_gear_mating(master = gear_3, slave = gear_4, efficiency = 0.9) + + +def ext_torque(time, angular_position, angular_speed): + return Torque(value = 6 + 0.5*np.sin(2*np.pi/10*angular_position.to('rad').value), + unit = 'Nm') + +gear_4.external_torque = ext_torque + + +transmission = Transmission(motor = motor) + + +# Simulation Set Up + +gear_4.angular_position = AngularPosition(0, 'rad') +gear_4.angular_speed = AngularSpeed(0, 'rad/s') + + +solver = Solver(time_discretization = TimeInterval(0.1, 'sec'), + simulation_time = TimeInterval(100, 'sec'), + transmission = transmission) +solver.run() + + +# Result Analysis + +transmission.snapshot(target_time = Time(50, 'sec')) + + +transmission.plot(figsize = (10, 12)) + +transmission.plot(elements = ['gear 1', 'gear 2', 'gear 3', 'gear 4'], + variables = ['angular position', 'torque', 'driving torque', 'load torque', + 'tangential force', 'bending stress', 'contact stress'], + angular_position_unit = 'rot', + figsize = (8, 6)) + + +# # Improved Model Set Up + +motor = DCMotor(name = 'motor', + no_load_speed = AngularSpeed(2000, 'rpm'), + maximum_torque = Torque(200, 'mNm'), + inertia_moment = InertiaMoment(5, 'gm^2')) +flywheel = Flywheel(name = 'flywheel', + inertia_moment = InertiaMoment(30, 'gm^2')) +gear_1 = SpurGear(name = 'gear 1', + n_teeth = 10, + inertia_moment = InertiaMoment(10, 'gm^2'), + module = Length(1.25, 'mm'), + face_width = Length(10, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_2 = SpurGear(name = 'gear 2', + n_teeth = 80, + inertia_moment = InertiaMoment(80, 'gm^2'), + module = Length(1.25, 'mm'), + face_width = Length(10, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_3 = SpurGear(name = 'gear 3', + n_teeth = 10, + inertia_moment = InertiaMoment(40, 'gm^2'), + module = Length(2.5, 'mm'), + face_width = Length(20, 'mm'), + elastic_modulus = Stress(210, 'GPa')) +gear_4 = SpurGear(name = 'gear 4', + n_teeth = 60, + inertia_moment = InertiaMoment(240, 'gm^2'), + module = Length(2.5, 'mm'), + face_width = Length(20, 'mm'), + elastic_modulus = Stress(210, 'GPa')) + + +add_fixed_joint(master = motor, slave = flywheel) +add_fixed_joint(master = flywheel, slave = gear_1) +add_gear_mating(master = gear_1, slave = gear_2, efficiency = 0.9) +add_fixed_joint(master = gear_2, slave = gear_3) +add_gear_mating(master = gear_3, slave = gear_4, efficiency = 0.9) + + +def ext_torque(time, angular_position, angular_speed): + return Torque(value = 6 + 0.5*np.sin(2*np.pi/10*angular_position.to('rad').value), + unit = 'Nm') + +gear_4.external_torque = ext_torque + + +transmission = Transmission(motor = motor) + + +# Simulation Set Up + +gear_4.angular_position = AngularPosition(0, 'rad') +gear_4.angular_speed = AngularSpeed(0, 'rad/s') + + +solver = Solver(time_discretization = TimeInterval(0.1, 'sec'), + simulation_time = TimeInterval(100, 'sec'), + transmission = transmission) +solver.run() + + +# Improved Model Result Analysis + +transmission.snapshot(target_time = Time(50, 'sec')) + + +transmission.plot(elements = ['gear 1', 'gear 2', 'gear 3', 'gear 4'], + variables = ['angular position', 'torque', 'driving torque', 'load torque', + 'tangential force', 'bending stress', 'contact stress'], + angular_position_unit = 'rot', + figsize = (8, 6)) diff --git a/docs/source/installation.md b/docs/source/installation.md index 9a640b2..8abf999 100644 --- a/docs/source/installation.md +++ b/docs/source/installation.md @@ -10,6 +10,16 @@ gearpy runs for python version 3.9+. ## Dependencies +- [matplotlib](https://matplotlib.org) >= 3.8.0 + Creates static, animated, and interactive visualizations in Python. - [numpy](https://numpy.org) >= 1.26.0 Adds support for large, multi-dimensional arrays, matrices and high-level mathematical functions to operate on these arrays. +- [pandas](https://pandas.pydata.org) >= 2.1.1 + Provides fast, flexible, and expressive data structures designed to + make working with "relational" or "labeled" data both easy and + intuitive. +- [scipy](https://scipy.org) >= 1.11.3 + Includes modules for statistics, optimization, integration, linear + algebra, Fourier transforms, signal and image processing, ODE solvers, + and more. diff --git a/docs/source/mechanical_object/SpurGear/bending_stress.rst b/docs/source/mechanical_object/SpurGear/bending_stress.rst new file mode 100644 index 0000000..aeb1ef8 --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/bending_stress.rst @@ -0,0 +1,7 @@ +bending_stress +============== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.bending_stress diff --git a/docs/source/mechanical_object/SpurGear/bending_stress_is_computable.rst b/docs/source/mechanical_object/SpurGear/bending_stress_is_computable.rst new file mode 100644 index 0000000..4455210 --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/bending_stress_is_computable.rst @@ -0,0 +1,7 @@ +bending_stress_is_computable +============================ + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.bending_stress_is_computable diff --git a/docs/source/mechanical_object/SpurGear/compute_bending_stress.rst b/docs/source/mechanical_object/SpurGear/compute_bending_stress.rst new file mode 100644 index 0000000..036d1dc --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/compute_bending_stress.rst @@ -0,0 +1,7 @@ +compute_bending_stress +====================== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. automethod:: SpurGear.compute_bending_stress diff --git a/docs/source/mechanical_object/SpurGear/compute_contact_stress.rst b/docs/source/mechanical_object/SpurGear/compute_contact_stress.rst new file mode 100644 index 0000000..5ec3f5b --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/compute_contact_stress.rst @@ -0,0 +1,7 @@ +compute_contact_stress +====================== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. automethod:: SpurGear.compute_contact_stress diff --git a/docs/source/mechanical_object/SpurGear/compute_tangential_force.rst b/docs/source/mechanical_object/SpurGear/compute_tangential_force.rst new file mode 100644 index 0000000..210e1bb --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/compute_tangential_force.rst @@ -0,0 +1,7 @@ +compute_tangential_force +======================== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. automethod:: SpurGear.compute_tangential_force diff --git a/docs/source/mechanical_object/SpurGear/contact_stress.rst b/docs/source/mechanical_object/SpurGear/contact_stress.rst new file mode 100644 index 0000000..550c1d6 --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/contact_stress.rst @@ -0,0 +1,7 @@ +contact_stress +============== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.contact_stress diff --git a/docs/source/mechanical_object/SpurGear/contact_stress_is_computable.rst b/docs/source/mechanical_object/SpurGear/contact_stress_is_computable.rst new file mode 100644 index 0000000..1bae9df --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/contact_stress_is_computable.rst @@ -0,0 +1,7 @@ +contact_stress_is_computable +============================ + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.contact_stress_is_computable diff --git a/docs/source/mechanical_object/SpurGear/elastic_modulus.rst b/docs/source/mechanical_object/SpurGear/elastic_modulus.rst new file mode 100644 index 0000000..d33484b --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/elastic_modulus.rst @@ -0,0 +1,7 @@ +elastic_modulus +=============== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.elastic_modulus diff --git a/docs/source/mechanical_object/SpurGear/face_width.rst b/docs/source/mechanical_object/SpurGear/face_width.rst new file mode 100644 index 0000000..dcc5081 --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/face_width.rst @@ -0,0 +1,7 @@ +face_width +========== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.face_width diff --git a/docs/source/mechanical_object/SpurGear/index.rst b/docs/source/mechanical_object/SpurGear/index.rst index 4e46094..68fed4a 100644 --- a/docs/source/mechanical_object/SpurGear/index.rst +++ b/docs/source/mechanical_object/SpurGear/index.rst @@ -16,16 +16,31 @@ SpurGear angular_acceleration angular_position angular_speed + bending_stress + bending_stress_is_computable + compute_bending_stress + compute_contact_stress + compute_tangential_force + contact_stress + contact_stress_is_computable driven_by drives driving_torque + elastic_modulus external_torque + face_width inertia_moment + lewis_factor load_torque master_gear_efficiency master_gear_ratio + mating_role + module n_teeth name + reference_diameter + tangential_force + tangential_force_is_computable time_variables torque update_time_variables diff --git a/docs/source/mechanical_object/SpurGear/lewis_factor.rst b/docs/source/mechanical_object/SpurGear/lewis_factor.rst new file mode 100644 index 0000000..e9f452e --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/lewis_factor.rst @@ -0,0 +1,7 @@ +lewis_factor +============ + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.lewis_factor diff --git a/docs/source/mechanical_object/SpurGear/mating_role.rst b/docs/source/mechanical_object/SpurGear/mating_role.rst new file mode 100644 index 0000000..46e0d45 --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/mating_role.rst @@ -0,0 +1,7 @@ +mating_role +=========== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.mating_role diff --git a/docs/source/mechanical_object/SpurGear/module.rst b/docs/source/mechanical_object/SpurGear/module.rst new file mode 100644 index 0000000..cdcf560 --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/module.rst @@ -0,0 +1,7 @@ +module +====== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.module diff --git a/docs/source/mechanical_object/SpurGear/reference_diameter.rst b/docs/source/mechanical_object/SpurGear/reference_diameter.rst new file mode 100644 index 0000000..0a0585b --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/reference_diameter.rst @@ -0,0 +1,7 @@ +reference_diameter +================== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.reference_diameter diff --git a/docs/source/mechanical_object/SpurGear/tangential_force.rst b/docs/source/mechanical_object/SpurGear/tangential_force.rst new file mode 100644 index 0000000..b57aa8e --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/tangential_force.rst @@ -0,0 +1,7 @@ +tangential_force +================ + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.tangential_force diff --git a/docs/source/mechanical_object/SpurGear/tangential_force_is_computable.rst b/docs/source/mechanical_object/SpurGear/tangential_force_is_computable.rst new file mode 100644 index 0000000..3f7889b --- /dev/null +++ b/docs/source/mechanical_object/SpurGear/tangential_force_is_computable.rst @@ -0,0 +1,7 @@ +tangential_force_is_computable +============================== + + +.. currentmodule:: gearpy.mechanical_object.mechanical_objects + +.. autoproperty:: SpurGear.tangential_force_is_computable diff --git a/docs/source/units/Force/index.rst b/docs/source/units/Force/index.rst new file mode 100644 index 0000000..0fccf7c --- /dev/null +++ b/docs/source/units/Force/index.rst @@ -0,0 +1,18 @@ +Force +===== + + +.. currentmodule:: gearpy.units +.. autoclass:: gearpy.units.units.Force + :members: + :undoc-members: + :show-inheritance: + :no-index: + + +.. toctree:: + :hidden: + + to + unit + value diff --git a/docs/source/units/Force/to.rst b/docs/source/units/Force/to.rst new file mode 100644 index 0000000..c4d20f1 --- /dev/null +++ b/docs/source/units/Force/to.rst @@ -0,0 +1,7 @@ +to +== + + +.. currentmodule:: gearpy.units.units + +.. automethod:: Force.to diff --git a/docs/source/units/Force/unit.rst b/docs/source/units/Force/unit.rst new file mode 100644 index 0000000..4304f1c --- /dev/null +++ b/docs/source/units/Force/unit.rst @@ -0,0 +1,7 @@ +unit +==== + + +.. currentmodule:: gearpy.units.units + +.. autoproperty:: Force.unit diff --git a/docs/source/units/Force/value.rst b/docs/source/units/Force/value.rst new file mode 100644 index 0000000..171ffa3 --- /dev/null +++ b/docs/source/units/Force/value.rst @@ -0,0 +1,7 @@ +value +===== + + +.. currentmodule:: gearpy.units.units + +.. autoproperty:: Force.value diff --git a/docs/source/units/Length/index.rst b/docs/source/units/Length/index.rst new file mode 100644 index 0000000..d10b542 --- /dev/null +++ b/docs/source/units/Length/index.rst @@ -0,0 +1,18 @@ +Length +====== + + +.. currentmodule:: gearpy.units +.. autoclass:: gearpy.units.units.Length + :members: + :undoc-members: + :show-inheritance: + :no-index: + + +.. toctree:: + :hidden: + + to + unit + value diff --git a/docs/source/units/Length/to.rst b/docs/source/units/Length/to.rst new file mode 100644 index 0000000..44bebe5 --- /dev/null +++ b/docs/source/units/Length/to.rst @@ -0,0 +1,7 @@ +to +== + + +.. currentmodule:: gearpy.units.units + +.. automethod:: Length.to diff --git a/docs/source/units/Length/unit.rst b/docs/source/units/Length/unit.rst new file mode 100644 index 0000000..74dc7ed --- /dev/null +++ b/docs/source/units/Length/unit.rst @@ -0,0 +1,7 @@ +unit +==== + + +.. currentmodule:: gearpy.units.units + +.. autoproperty:: Length.unit diff --git a/docs/source/units/Length/value.rst b/docs/source/units/Length/value.rst new file mode 100644 index 0000000..eb60ccc --- /dev/null +++ b/docs/source/units/Length/value.rst @@ -0,0 +1,7 @@ +value +===== + + +.. currentmodule:: gearpy.units.units + +.. autoproperty:: Length.value diff --git a/docs/source/units/index.rst b/docs/source/units/index.rst index 41cc499..242d7da 100644 --- a/docs/source/units/index.rst +++ b/docs/source/units/index.rst @@ -14,7 +14,9 @@ units AngularAcceleration/index AngularPosition/index AngularSpeed/index + Force/index InertiaMoment/index + Length/index Time/index TimeInterval/index Torque/index diff --git a/gearpy/mechanical_object/__init__.py b/gearpy/mechanical_object/__init__.py index 5c01e4f..10946bb 100644 --- a/gearpy/mechanical_object/__init__.py +++ b/gearpy/mechanical_object/__init__.py @@ -1,2 +1,2 @@ -from .mechanical_objects import DCMotor, SpurGear, Flywheel -from .mechanical_object_base import MechanicalObject, RotatingObject, MotorBase, GearBase +from .mechanical_objects import DCMotor, SpurGear, Flywheel, MatingMaster, MatingSlave +from .mechanical_object_base import MechanicalObject, RotatingObject, MotorBase, GearBase, Role diff --git a/gearpy/mechanical_object/gear_data/__init__.py b/gearpy/mechanical_object/gear_data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gearpy/mechanical_object/gear_data/lewis_factor_table.csv b/gearpy/mechanical_object/gear_data/lewis_factor_table.csv new file mode 100644 index 0000000..d83431f --- /dev/null +++ b/gearpy/mechanical_object/gear_data/lewis_factor_table.csv @@ -0,0 +1,39 @@ +number of teeth,Lewis factor +10,0.201 +11,0.226 +12,0.245 +13,0.264 +14,0.276 +15,0.289 +16,0.295 +17,0.302 +18,0.308 +19,0.314 +20,0.320 +21,0.325 +22,0.330 +24,0.337 +26,0.344 +28,0.352 +30,0.358 +32,0.364 +34,0.370 +36,0.377 +38,0.383 +40,0.389 +43,0.394 +45,0.399 +50,0.408 +55,0.415 +60,0.421 +65,0.425 +70,0.429 +75,0.433 +80,0.436 +90,0.442 +100,0.446 +150,0.458 +200,0.463 +300,0.471 +400,0.478 +500,0.484 diff --git a/gearpy/mechanical_object/mechanical_object_base.py b/gearpy/mechanical_object/mechanical_object_base.py index a909a76..295ebed 100644 --- a/gearpy/mechanical_object/mechanical_object_base.py +++ b/gearpy/mechanical_object/mechanical_object_base.py @@ -1,8 +1,23 @@ from abc import ABC, abstractmethod -from gearpy.units import AngularPosition, AngularSpeed, AngularAcceleration, InertiaMoment, Time, Torque, UnitBase +from . import gear_data +from gearpy.units import AngularPosition, AngularSpeed, AngularAcceleration, Force, InertiaMoment, Length, Stress, \ + Time, Torque, UnitBase +from importlib import resources as imp_resources +import pandas as pd +from scipy.interpolate import interp1d from typing import Callable, Dict, List, Union +LEWIS_FACTOR_DATA_FILE = (imp_resources.files(gear_data) / 'lewis_factor_table.csv') +LEWIS_FACTOR_DATA = pd.read_csv(LEWIS_FACTOR_DATA_FILE) +MINIMUM_TEETH_NUMBER = LEWIS_FACTOR_DATA.loc[LEWIS_FACTOR_DATA.index[0], 'number of teeth'] +lewis_factor_function = interp1d(x = LEWIS_FACTOR_DATA['number of teeth'], + y = LEWIS_FACTOR_DATA['Lewis factor'], + fill_value = (LEWIS_FACTOR_DATA.loc[LEWIS_FACTOR_DATA.index[0], 'Lewis factor'], + LEWIS_FACTOR_DATA.loc[LEWIS_FACTOR_DATA.index[-1], 'Lewis factor']), + bounds_error = False) + + class MechanicalObject(ABC): @abstractmethod @@ -230,27 +245,86 @@ def compute_torque(self) -> Torque: ... class GearBase(RotatingObject): @abstractmethod - def __init__(self, name: str, n_teeth: int, inertia_moment: InertiaMoment): + def __init__(self, + name: str, + n_teeth: int, + module: Length, + face_width: Length, + inertia_moment: InertiaMoment, + elastic_modulus: Stress): super().__init__(name = name, inertia_moment = inertia_moment) if not isinstance(n_teeth, int): raise TypeError("Parameter 'n_teeth' must be an integer.") - if n_teeth <= 0: - raise ValueError("Parameter 'n_teeth' must be positive.") + if n_teeth < MINIMUM_TEETH_NUMBER: + raise ValueError(f"Parameter 'n_teeth' must be greater or equal to {MINIMUM_TEETH_NUMBER}.") + + if module is not None: + if not isinstance(module, Length): + raise TypeError(f"Parameter 'module' must be an instance of {Length.__name__!r}.") + + if face_width is not None: + if not isinstance(face_width, Length): + raise TypeError(f"Parameter 'face_width' must be an instance of {Length.__name__!r}.") + + if elastic_modulus is not None: + if not isinstance(elastic_modulus, Stress): + raise TypeError(f"Parameter 'elastic_modulus' must be an instance of {Stress.__name__!r}.") + + if elastic_modulus.value <= 0: + raise ValueError(f"Parameter 'elastic_modulus' must be positive.") self.__n_teeth = n_teeth self.__driven_by = None self.__drives = None self.__master_gear_ratio = None self.__master_gear_efficiency = 1 + self.__mating_role = None self.__external_torque = None + self.__module = module + self.__face_width = face_width + self.__elastic_modulus = elastic_modulus + + if self.tangential_force_is_computable: + self.__reference_diameter = n_teeth*module + self.__tangential_force = None + + if self.bending_stress_is_computable: + self.__bending_stress = None + + if self.contact_stress_is_computable: + self.__contact_stress = None @property @abstractmethod def n_teeth(self) -> int: return self.__n_teeth + @property + @abstractmethod + def module(self) -> Length: + return self.__module + + @property + @abstractmethod + def reference_diameter(self) -> Length: + return self.__reference_diameter + + @property + @abstractmethod + def face_width(self) -> Length: + return self.__face_width + + @property + @abstractmethod + def elastic_modulus(self) -> Stress: + return self.__elastic_modulus + + @property + @abstractmethod + def lewis_factor(self): ... + @property @abstractmethod def driven_by(self) -> RotatingObject: @@ -337,6 +411,69 @@ def load_torque(self) -> Torque: def load_torque(self, load_torque: Torque): super(GearBase, type(self)).load_torque.fset(self, load_torque) + @property + @abstractmethod + def tangential_force(self) -> Force: + return self.__tangential_force + + @tangential_force.setter + @abstractmethod + def tangential_force(self, tangential_force: Force): + if not isinstance(tangential_force, Force): + raise TypeError(f"Parameter 'tangential_force' must be an instance of {Force.__name__!r}.") + + self.__tangential_force = tangential_force + + @abstractmethod + def compute_tangential_force(self): ... + + @property + @abstractmethod + def tangential_force_is_computable(self) -> bool: + return self.__module is not None + + @property + @abstractmethod + def bending_stress(self) -> Stress: + return self.__bending_stress + + @bending_stress.setter + @abstractmethod + def bending_stress(self, bending_stress: Stress): + if not isinstance(bending_stress, Stress): + raise TypeError(f"Parameter 'bending_stress' must be an instance of {Stress.__name__!r}.") + + self.__bending_stress = bending_stress + + @abstractmethod + def compute_bending_stress(self): ... + + @property + @abstractmethod + def bending_stress_is_computable(self) -> bool: + return (self.__module is not None) and (self.__face_width is not None) + + @property + @abstractmethod + def contact_stress(self) -> Stress: + return self.__contact_stress + + @contact_stress.setter + @abstractmethod + def contact_stress(self, contact_stress: Stress): + if not isinstance(contact_stress, Stress): + raise TypeError(f"Parameter 'contact_stress' must be an instance of {Stress.__name__!r}.") + + self.__contact_stress = contact_stress + + @abstractmethod + def compute_contact_stress(self): ... + + @property + @abstractmethod + def contact_stress_is_computable(self) -> bool: + return (self.__module is not None) and (self.__face_width is not None) and (self.__elastic_modulus is not None) + @property @abstractmethod def master_gear_ratio(self) -> float: @@ -369,6 +506,22 @@ def master_gear_efficiency(self, master_gear_efficiency: Union[float, int]): self.__master_gear_efficiency = master_gear_efficiency + @property + @abstractmethod + def mating_role(self) -> 'Role': + return self.__mating_role + + @mating_role.setter + @abstractmethod + def mating_role(self, mating_role): + if hasattr(mating_role, '__name__'): + if not issubclass(mating_role, Role): + raise TypeError(f"Parameter 'mating_role' must be a subclass of {Role.__name__!r}.") + else: + raise TypeError(f"Parameter 'mating_role' must be a subclass of {Role.__name__!r}.") + + self.__mating_role = mating_role + @property @abstractmethod def external_torque(self) -> Callable[[AngularPosition, AngularSpeed, Time], Torque]: @@ -381,3 +534,6 @@ def external_torque(self, external_torque: Callable[[AngularPosition, AngularSpe raise TypeError("Parameter 'external_torque' must be callable.") self.__external_torque = external_torque + + +class Role(ABC): ... diff --git a/gearpy/mechanical_object/mechanical_objects.py b/gearpy/mechanical_object/mechanical_objects.py index 9be6c8c..b79df16 100644 --- a/gearpy/mechanical_object/mechanical_objects.py +++ b/gearpy/mechanical_object/mechanical_objects.py @@ -1,6 +1,8 @@ -from gearpy.units import AngularPosition, AngularSpeed, AngularAcceleration, InertiaMoment, Time, Torque, UnitBase -from .mechanical_object_base import RotatingObject, GearBase, MotorBase -from typing import Callable, Dict, List, Union +from gearpy.units import AngularPosition, AngularSpeed, AngularAcceleration, Force, InertiaMoment, Length, Stress, \ + Time, Torque, UnitBase +from math import pi, sin, cos, sqrt +from .mechanical_object_base import RotatingObject, GearBase, MotorBase, lewis_factor_function, Role +from typing import Callable, Dict, List, Union, Optional class SpurGear(GearBase): @@ -12,6 +14,18 @@ class SpurGear(GearBase): Name of the spur gear. :py:attr:`n_teeth` : int Number of gear teeth. + :py:attr:`inertia_moment` : InertiaMoment + Moment of inertia of the gear. + :py:attr:`module` : Length + Unit of the gear teeth size. + :py:attr:`reference_diameter` : Length + Reference diameter of the gear. + :py:attr:`face_width` : Length + Face width of the gear. + :py:attr:`elastic_modulus` : Stress + Elastic modulus of the material of the gear. + :py:attr:`lewis_factor` : float + Factor used to compute stresses on the gear tooth. :py:attr:`driven_by` : RotatingObject Rotating object that drives the gear, for example a motor or another gear. :py:attr:`drives` : RotatingObject @@ -20,6 +34,8 @@ class SpurGear(GearBase): Gear ratio of the mating between the gear and its driving gear. :py:attr:`master_gear_efficiency` : float or int Efficiency of the gear mating between the gear and its driving gear. + :py:attr:`mating_role`: Role + Role of the gear in the gear mating. :py:attr:`angular_position` : AngularPosition Angular position of the gear. :py:attr:`angular_speed` : AngularSpeed @@ -32,27 +48,65 @@ class SpurGear(GearBase): Driving torque applied on the gear by its driving gear. :py:attr:`load_torque` : Torque Load torque applied on the gear by its driven gear or an external load. - :py:attr:`inertia_moment` : InertiaMoment - Moment of inertia of the gear. + :py:attr:`tangential_force` : Force + Tangential force applied on the gear teeth by the mating gear. + :py:attr:`tangential_force_is_computable` : bool + Whether or not is possible to compute the tangential force on the gear teeth. + :py:attr:`bending_stress` : Stress + Bending stress applied on the gear teeth by the mating gear. + :py:attr:`bending_stress_is_computable` : bool + Whether or not is possible to compute the bending stress on the gear teeth. + :py:attr:`contact_stress` : Stress + Stress generated by the contact with mating gear teeth. + :py:attr:`contact_stress_is_computable` : bool + Whether or not is possible to compute the contact stress on the gear teeth. :py:attr:`time_variables` : dict Time variables of the gear. Methods ------- + :py:meth:`compute_tangential_force` + Computes the tangential force applied on the gear teeth by the mating gear. + :py:meth:`compute_bending_stress` + Computes the bending stress applied on the gear teeth by the mating gear. + :py:meth:`compute_contact_stress` + Computes the contact stress generated by the contact with mating gear teeth. :py:meth:`external_torque` Custom function to compute the external torque applied on the gear. :py:meth:`update_time_variables` Updates ``time_variables`` dictionary by appending the last value of each time variable to corresponding list. """ - def __init__(self, name: str, n_teeth: int, inertia_moment: InertiaMoment): - super().__init__(name = name, n_teeth = n_teeth, inertia_moment = inertia_moment) + def __init__(self, + name: str, + n_teeth: int, + inertia_moment: InertiaMoment, + module: Optional[Length] = None, + face_width: Optional[Length] = None, + elastic_modulus: Optional[Stress] = None): + super().__init__(name = name, + n_teeth = n_teeth, + module = module, + face_width = face_width, + inertia_moment = inertia_moment, + elastic_modulus = elastic_modulus) + + if self.tangential_force_is_computable: + self.time_variables['tangential force'] = [] + + if self.bending_stress_is_computable: + self.time_variables['bending stress'] = [] + self.__lewis_factor = lewis_factor_function(self.n_teeth).take(0) + + if self.contact_stress_is_computable: + self.time_variables['contact stress'] = [] + self.__PRESSURE_ANGLE = 20/180*pi @property def name(self) -> str: """Name of the spur gear. It must be a non-empty string. \n It must be an unique name, not shared by other elements in the transmission chain. \n - Once set at the spur gear instantiation, it cannot be further changed. + Once set at the spur gear instantiation, it cannot be changed afterwards. Returns ------- @@ -71,7 +125,7 @@ def name(self) -> str: @property def n_teeth(self) -> int: """Number of gear teeth. It must be a positive integer. \n - Once set at the spur gear instantiation, it cannot be further changed. + Once set at the spur gear instantiation, it cannot be changed afterwards. Returns ------- @@ -83,10 +137,97 @@ def n_teeth(self) -> int: TypeError If ``n_teeth`` is not an integer. ValueError - If ``n_teeth`` is not positive. + If ``n_teeth`` is less than the minimum number of teeth, based on Lewis Factor table. """ return super().n_teeth + @property + def inertia_moment(self) -> InertiaMoment: + """Moment of inertia of the gear. It must be an instance of ``InertiaMoment``. \n + Once set at the spur gear instantiation, it cannot be changed afterwards. + + Returns + ------- + InertiaMoment + Moment of inertia of the gear. + + Raises + ------ + TypeError + If ``inertia_moment`` is not an instance of ``InertiaMoment``. + + See Also + -------- + :py:class:`gearpy.units.units.InertiaMoment` + """ + return super().inertia_moment + + @property + def module(self) -> Length: + """Unit of the gear teeth size. It must be an instance of ``Length``. \n + Once set at the spur gear instantiation, it cannot be changed afterwards. + + Returns + ------- + Length + Unit of the gear teeth size. + + Raises + ------ + TypeError + If ``module`` is not an instance of ``Length``. + """ + return super().module + + @property + def reference_diameter(self) -> Length: + """Reference diameter of the gear. It must be an instance of ``Length``. \n + It is computed as the product of ``n_teeth`` times ``module`` at the spur gear instantiation and it cannot be + changed afterwards. + + Returns + ------- + Length + Reference diameter of the gear. + """ + return super().reference_diameter + + @property + def face_width(self) -> Length: + """Face width of the gear. It must be an instance of ``Length``. + + Returns + ------- + Length + Face width of the gear. + """ + return super().face_width + + @property + def elastic_modulus(self) -> Stress: + """Elastic modulus of the material of the gear. It must be an instance of ``Stress``. + + Returns + ------- + Stress + Elastic modulus of the material of the gear. + """ + return super().elastic_modulus + + @property + def lewis_factor(self) -> float: + """Factor used to compute stresses on the gear tooth. + It is a tabular value that in general depends on the number of gear teeth and and the pressure angle. In this + case, the considered pressure angle is always 20 degrees, so the Lewis factor depends only on the number of gear + teeth. + + Returns + ------- + float + Factor used to compute stresses on the gear tooth. + """ + return self.__lewis_factor + @property def driven_by(self) -> RotatingObject: """Rotating object that drives the gear, for example a motor or another gear. It must be a ``RotatingObject``. @@ -178,6 +319,28 @@ def master_gear_efficiency(self) -> Union[float, int]: def master_gear_efficiency(self, master_gear_efficiency: Union[float, int]): super(SpurGear, type(self)).master_gear_efficiency.fset(self, master_gear_efficiency) + @property + def mating_role(self) -> Role: + """Role of the gear in the gear mating. To set this parameter use ``add_gear_mating``. \n + If the gear drives the mate, then it is the "master" gear and its role is ``MatingMaster``, otherwise it is the + "slave" one and its role is ``MatingSlave``. + + Returns + ------- + Role + Role of the gear in the gear mating. + + Raises + ------ + ValueError + If ``mating_role`` is not a subclass of ``Role``. + """ + return super().mating_role + + @mating_role.setter + def mating_role(self, mating_role: Role): + super(SpurGear, type(self)).mating_role.fset(self, mating_role) + @property def angular_position(self) -> AngularPosition: """Angular position of the gear. It must be an instance of ``AngularPosition``. @@ -325,25 +488,286 @@ def load_torque(self, load_torque: Torque): super(SpurGear, type(self)).load_torque.fset(self, load_torque) @property - def inertia_moment(self) -> InertiaMoment: - """Moment of inertia of the gear. It must be an instance of ``InertiaMoment``. \n - Once set at the spur gear instantiation, it cannot be further changed. + def tangential_force(self) -> Force: + """Tangential force applied on the gear teeth by the mating gear. It must be an instance of ``Force``. Returns ------- - InertiaMoment - Moment of inertia of the gear. + Force + Tangential force applied on the gear teeth by the mating gear. Raises ------ TypeError - If ``inertia_moment`` is not an instance of ``InertiaMoment``. + If ``tangential_force`` is not an instance of ``Force``. See Also -------- - :py:class:`gearpy.units.units.InertiaMoment` + :py:class:`gearpy.units.units.Force` + :py:meth:`compute_tangential_force` """ - return super().inertia_moment + return super().tangential_force + + @tangential_force.setter + def tangential_force(self, tangential_force: Force): + super(SpurGear, type(self)).tangential_force.fset(self, tangential_force) + + def compute_tangential_force(self): + """Computes the tangential force applied on the gear teeth by the mating gear. \n + Considering a gear mating: + + - if the gear is the master one, then it takes into account the ``load_torque`` for the computation + - if the gear is the slave one, then it take into account the ``driving_torque`` for the computation + + The tangential force is computed dividing the just described reference torque by the reference radius (half of + the reference diameter). + + Raises + ------ + ValueError + If a gear mating between two gears has not been set. + + See Also + -------- + :py:attr:`tangential_force` + """ + if self.mating_role == MatingMaster: + self.tangential_force = abs(self.load_torque)/(self.reference_diameter/2) + elif self.mating_role == MatingSlave: + self.tangential_force = abs(self.driving_torque)/(self.reference_diameter/2) + else: + raise ValueError("Gear mating not defined. " + "Use 'gearpy.utils.add_gear_mating' to set up a mating between two gears.") + + @property + def tangential_force_is_computable(self) -> bool: + """Whether or not is possible to compute the tangential force on the gear teeth. The tangential force + computation depends on the value of ``module``, so if this optional parameter has been set at spur gear + instantiation, then it is possible to compute the tangential force and this property is ``True``, otherwise is + ``False``. + + Returns + ------- + bool + Whether or not is possible to compute the tangential force on the gear teeth. + + See Also + -------- + :py:attr:`module` + :py:attr:`tangential_force` + :py:meth:`compute_tangential_force` + """ + return super().tangential_force_is_computable + + @property + def bending_stress(self) -> Stress: + """Bending stress applied on the gear teeth by the mating gear. It must be an instance of ``Stress``. + + Returns + ------- + Stress + Bending stress applied on the gear teeth by the mating gear. + + Raises + ------ + TypeError + If ``bending_stress`` is not an instance of ``Stress``. + + See Also + -------- + :py:class:`gearpy.units.units.Stress` + :py:meth:`compute_bending_stress` + """ + return super().bending_stress + + @bending_stress.setter + def bending_stress(self, bending_stress: Stress): + super(SpurGear, type(self)).bending_stress.fset(self, bending_stress) + + def compute_bending_stress(self): + r"""Computes the bending stress applied on the gear teeth by the mating gear. + + Notes + ----- + The bending stress computation is based on the following assumptions: + + - the tooth is stressed by the overall force acting on the tip of the tooth itself, + - the most unfavorable situation is considered in the calculation, as if there is only one pair of teeth in + contact within the contact segment, + - the component of the overall force that determines the bending on the tooth is the only one considered and, + for simplicity, is taken as having a value equal to the tangential force on the reference diameter, + - the radial component of the overall force that causes a compressive stress on the tooth is neglected. + + The bending stress is computed with the following formula: + + .. math:: + \sigma_b = \frac{F_t}{m \, b \, Y_{LW}} + + where: + + - :math:`F_t` is the tangential force applied on the tooth + - :math:`m` is the gear module + - :math:`b` is the gear face width + - :math:`Y_{LW}` is the gear Lewis factor + + See Also + -------- + :py:attr:`bending_stress` + :py:attr:`tangential_force` + :py:attr:`module` + :py:attr:`face_width` + :py:attr:`lewis_factor` + """ + self.bending_stress = self.tangential_force/(self.module*self.face_width)/self.lewis_factor + + @property + def bending_stress_is_computable(self) -> bool: + """Whether or not is possible to compute the bending stress on the gear teeth. The bending stress computation + depends on the value of ``module`` and ``face_width``, so if these optional parameters have been set at spur + gear instantiation, then it is possible to compute the bending stress and this property is ``True``, otherwise + is ``False``. + + Returns + ------- + bool + Whether or not is possible to compute the bending stress on the gear teeth. + + See Also + -------- + :py:attr:`module` + :py:attr:`face_width` + :py:attr:`bending_stress` + :py:meth:`compute_bending_stress` + """ + return super().bending_stress_is_computable + + @property + def contact_stress(self) -> Stress: + """Stress generated by the contact with mating gear teeth. It must be an instance of ``Stress``. + + Returns + ------- + Stress + Stress generated by the contact with mating gear teeth. + + Raises + ------ + TypeError + If ``contact_stress`` is not an instance of ``Stress``. + + See Also + -------- + :py:class:`gearpy.units.units.Stress` + :py:meth:`compute_contact_stress` + """ + return super().contact_stress + + @contact_stress.setter + def contact_stress(self, contact_stress: Stress): + super(SpurGear, type(self)).contact_stress.fset(self, contact_stress) + + def compute_contact_stress(self): + r"""Computes the contact stress generated by the contact with mating gear teeth. + + Raises + ------ + ValueError + - If a gear mating between two gears has not been set, + - if gear role is ``MatingMaster`` and its slave gear misses ``module`` parameter, + - if gear role is ``MatingMaster`` and its slave gear misses ``elastic_modulus`` parameter, + - if gear role is ``MatingSlave`` and its master gear misses ``module`` parameter, + - if gear role is ``MatingSlave`` and its master gear misses ``elastic_modulus`` parameter. + + Notes + ----- + The contact stress computation is based on the following assumptions: + + - perfect elasticity of the materials the the two mating gear, + - absence of friction forces in the contact point, + - small size of the contact surface compared to the size of the bodies between which contact occurs. + + The contact stress is computed with the following formula: + + .. math:: + \sigma_c = 0.262922 \, \sqrt{\frac{4 \, F_t}{b \, \cos \alpha \, \sin \alpha} + \left( \frac{1}{D_1} + \frac{1}{D_2} \right) \, \frac{E_1 \, E_2}{E_1 + E_2}} + + where: + + - :math:`F_t` is the tangential force applied on the tooth, + - :math:`b` is the gear face width, + - :math:`\alpha` is the pressure angle of the gear, always equal to 20 degrees, + - :math:`D_1` is the gear reference diameter, + - :math:`D_2` is the mating gear reference diameter, + - :math:`E_1` is the gear elastic modulus, + - :math:`E_2` is the mating gear elastic modulus. + + See Also + -------- + :py:attr:`contact_stress` + :py:attr:`tangential_force` + :py:attr:`elastic_modulus` + :py:attr:`face_width` + :py:attr:`reference_diameter` + """ + if self.mating_role == MatingMaster: + if self.drives.module is not None: + mate_reference_diameter = self.drives.reference_diameter + else: + raise ValueError(f"Impossible to compute contact stress for {self.__class__.__name__} {self.name!r} " + f"because its mating {self.__class__.__name__} {self.drives.name!r} misses " + f"'module' parameter.") + if self.drives.elastic_modulus is not None: + mate_elastic_modulus = self.drives.elastic_modulus + else: + raise ValueError(f"Impossible to compute contact stress for {self.__class__.__name__} {self.name!r} " + f"because its mating {self.__class__.__name__} {self.drives.name!r} misses " + f"'elastic_modulus' parameter.") + elif self.mating_role == MatingSlave: + if self.driven_by.module is not None: + mate_reference_diameter = self.driven_by.reference_diameter + else: + raise ValueError(f"Impossible to compute contact stress for {self.__class__.__name__} {self.name!r} " + f"because its mating {self.__class__.__name__} {self.driven_by.name!r} misses " + f"'module' parameter.") + if self.driven_by.elastic_modulus is not None: + mate_elastic_modulus = self.driven_by.elastic_modulus + else: + raise ValueError(f"Impossible to compute contact stress for {self.__class__.__name__} {self.name!r} " + f"because its mating {self.__class__.__name__} {self.driven_by.name!r} misses " + f"'elastic_modulus' parameter.") + else: + raise ValueError("Gear mating not defined. " + "Use 'gearpy.utils.add_gear_mating' to set up a mating between two gears.") + + equivalent_elastic_modulus = 2*self.elastic_modulus*(mate_elastic_modulus/(self.elastic_modulus + mate_elastic_modulus)) + inverse_curvature_sum = sin(self.__PRESSURE_ANGLE)/2*self.reference_diameter*(mate_reference_diameter/(self.reference_diameter + mate_reference_diameter)) + contact_pressure = self.tangential_force/cos(self.__PRESSURE_ANGLE)/(self.face_width*inverse_curvature_sum) + + self.contact_stress = Stress(value = 0.262922*sqrt(equivalent_elastic_modulus.to('Pa').value*contact_pressure.to('Pa').value), + unit = 'Pa') + + @property + def contact_stress_is_computable(self) -> bool: + """Whether or not is possible to compute the contact stress force on the gear teeth. The contact stress + computation depends on the value of ``module``, ``face_width`` and ``elastic_modulus``, so if these optional + parameters have been set at spur gear instantiation, then it is possible to compute the contact stress and this + property is ``True``, otherwise is ``False``. + + Returns + ------- + bool + Whether or not is possible to compute the contact stress on the gear teeth. + + See Also + -------- + :py:attr:`module` + :py:attr:`face_width` + :py:attr:`elastic_modulus` + :py:attr:`contact_stress` + :py:meth:`compute_contact_stress` + """ + return super().contact_stress_is_computable @property def external_torque(self) -> Callable[[AngularPosition, AngularSpeed, Time], Torque]: @@ -412,8 +836,13 @@ def time_variables(self) -> Dict[str, List[UnitBase]]: - ``angular_acceleration``, - ``torque``, - ``driving_torque``, - - ``load_torque``. + - ``load_torque``, + - ``tangential_force``, + - ``bending_stress``, + - ``contact_stress``. + ``tangential_force``, ``bending_stress`` and ``contact_stress`` are listed among time variables only if they are + computable indeed, depending on which gear parameters are set at gear instantiation. \n Corresponding values of the dictionary are lists of the respective time variable values. \n At each time iteration, the ``Solver`` appends every time variables' values to the relative list in the dictionary. @@ -432,14 +861,20 @@ def time_variables(self) -> Dict[str, List[UnitBase]]: def update_time_variables(self) -> None: """Updates ``time_variables`` dictionary by appending the last value of each time variable (key of the dictionary) to corresponding list (value of the dictionary). \n - Time variables are ``angular_position``, ``angular_speed``, ``angular_acceleration``, ``torque``, - ``driving_torque`` and ``load_torque`` of the gear. See Also -------- :py:attr:`time_variables` """ super().update_time_variables() + if self.tangential_force_is_computable: + self.time_variables['tangential force'].append(self.tangential_force) + + if self.bending_stress_is_computable: + self.time_variables['bending stress'].append(self.bending_stress) + + if self.contact_stress_is_computable: + self.time_variables['contact stress'].append(self.contact_stress) class DCMotor(MotorBase): @@ -480,7 +915,11 @@ class DCMotor(MotorBase): Updates ``time_variables`` dictionary by appending the last value of each time variable to corresponding list. """ - def __init__(self, name: str, inertia_moment: InertiaMoment, no_load_speed: AngularSpeed, maximum_torque: Torque): + def __init__(self, + name: str, + inertia_moment: InertiaMoment, + no_load_speed: AngularSpeed, + maximum_torque: Torque): super().__init__(name = name, inertia_moment = inertia_moment) if not isinstance(no_load_speed, AngularSpeed): @@ -502,7 +941,7 @@ def __init__(self, name: str, inertia_moment: InertiaMoment, no_load_speed: Angu def name(self) -> str: """Name of the DC motor. It must be a non-empty string. \n It must be an unique name, not shared by other elements in the transmission chain. \n - Once set at the DC motor instantiation, it cannot be further changed. + Once set at the DC motor instantiation, it cannot be changed afterwards. Returns ------- @@ -615,7 +1054,7 @@ def no_load_speed(self) -> AngularSpeed: """No load angular speed of the DC motor. It must be an instance of ``AngularSpeed``. Its value must be positive. \n It is the angular speed at which the DC motor rotates when no load is applied to its rotor. \n - Once set at the DC motor instantiation, it cannot be further changed. + Once set at the DC motor instantiation, it cannot be changed afterwards. Returns ------- @@ -639,7 +1078,7 @@ def no_load_speed(self) -> AngularSpeed: def maximum_torque(self) -> Torque: """Maximum torque developed by the DC motor. It must be an instance of ``Torque``. Its value must be positive. \n It is the maximum torque the DC motor can develop when its rotor is kept still by the load. \n - Once set at the DC motor instantiation, it cannot be further changed. + Once set at the DC motor instantiation, it cannot be changed afterwards. Returns ------- @@ -735,7 +1174,7 @@ def load_torque(self, load_torque: Torque): @property def inertia_moment(self) -> InertiaMoment: """Moment of inertia of the DC motor's rotor. It must be an instance of ``InertiaMoment``. \n - Once set at the DC motor instantiation, it cannot be further changed. + Once set at the DC motor instantiation, it cannot be changed afterwards. Returns ------- @@ -871,7 +1310,7 @@ def __init__(self, name: str, inertia_moment: InertiaMoment): def name(self) -> str: """Name of the flywheel. It must be a non-empty string. \n It must be an unique name, not shared by other elements in the transmission chain. \n - Once set at the flywheel instantiation, it cannot be further changed. + Once set at the flywheel instantiation, it cannot be changed afterwards. Returns ------- @@ -1125,7 +1564,7 @@ def master_gear_efficiency(self) -> float: @property def inertia_moment(self) -> InertiaMoment: """Moment of inertia of the flywheel. It must be an instance of ``InertiaMoment``. \n - Once set at the flywheel instantiation, it cannot be further changed. + Once set at the flywheel instantiation, it cannot be changed afterwards. Returns ------- @@ -1181,3 +1620,9 @@ def update_time_variables(self) -> None: :py:attr:`time_variables` """ super().update_time_variables() + + +class MatingMaster(Role): ... + + +class MatingSlave(Role): ... diff --git a/gearpy/solver.py b/gearpy/solver.py index 3313c7b..d5db390 100644 --- a/gearpy/solver.py +++ b/gearpy/solver.py @@ -1,4 +1,4 @@ -from gearpy.mechanical_object import RotatingObject, MotorBase +from gearpy.mechanical_object import RotatingObject, MotorBase, GearBase from gearpy.transmission import Transmission from gearpy.units import Time, TimeInterval import numpy as np @@ -84,6 +84,8 @@ def run(self): self._compute_driving_torque() self._compute_load_torque() self._compute_torque() + self._compute_force() + self._compute_stress() self._time_integration() self._update_time_variables() @@ -104,6 +106,8 @@ def _compute_transmission_initial_state(self): self._compute_driving_torque() self._compute_load_torque() self._compute_torque() + self._compute_force() + self._compute_stress() self.transmission.chain[-1].angular_acceleration = self.transmission.chain[-1].torque/\ self.transmission_inertia_moment @@ -164,6 +168,22 @@ def _compute_torque(self): for item in self.transmission.chain: item.torque = item.driving_torque - item.load_torque + def _compute_force(self): + + for item in self.transmission.chain: + if isinstance(item, GearBase): + if item.tangential_force_is_computable: + item.compute_tangential_force() + + def _compute_stress(self): + + for item in self.transmission.chain: + if isinstance(item, GearBase): + if item.bending_stress_is_computable: + item.compute_bending_stress() + if item.contact_stress_is_computable: + item.compute_contact_stress() + def _time_integration(self): self.transmission.chain[-1].angular_acceleration = self.transmission.chain[-1].torque/\ diff --git a/gearpy/transmission.py b/gearpy/transmission.py index 6e9b29b..e64dd89 100644 --- a/gearpy/transmission.py +++ b/gearpy/transmission.py @@ -1,5 +1,5 @@ from collections import Counter -from gearpy.mechanical_object import MotorBase, RotatingObject +from gearpy.mechanical_object import MotorBase, RotatingObject, GearBase from gearpy.units import Time import matplotlib.pyplot as plt import pandas as pd @@ -114,12 +114,14 @@ def snapshot(self, torque_unit: str = 'Nm', driving_torque_unit: str = 'Nm', load_torque_unit: str = 'Nm', + force_unit: str = 'N', + stress_unit: str = 'MPa', print_data: bool = True) -> pd.DataFrame: """Computes a snapshot of the time variables of the elements in the mechanical transmission at the specified ``target_time``. The computed time variables are organized in a ``pandas.DataFrame``, returned by the method. Each element in the transmission chain is a row of the DataFrame, while the columns are the time variables - angular position, angular speed, angular acceleration, torque, driving torque, load torque. Each time variable - is converted to the relative unit passed as optional parameter. \n + angular position, angular speed, angular acceleration, torque, driving torque, load torque and tangential force. + Each time variable is converted to the relative unit passed as optional parameter. \n If the ``target_time`` is not among simulated time steps in the ``time`` list, it computes a linear interpolation from the two closest simulated time steps. @@ -146,6 +148,12 @@ def snapshot(self, load_torque_unit : str, optional Symbol of the unit of measurement to which convert the load torque in the DataFrame. It must be a string. Default is ``'Nm'``. + force_unit : str, optional + Symbol of the unit of measurement to which convert the force values in the plot. It must be a string. + Default is ``'N'``. + stress_unit : str, optional + Symbol of the unit of measurement to which convert the stress values in the plot. It must be a string. + Default is ``'MPa'``. print_data : bool, optional Whether or not to print the computed time variables DataFrame. Default is ``True``. @@ -166,6 +174,8 @@ def snapshot(self, - if ``torque_unit`` is not a string, - if ``driving_torque_unit`` is not a string, - if ``load_torque_unit`` is not a string, + - if ``force_unit`` is not a string, + - if ``stress_unit`` is not a string, - if ``print_data`` is not a bool. ValueError If ``time`` is an empty list. @@ -197,6 +207,12 @@ def snapshot(self, if not isinstance(load_torque_unit, str): raise TypeError(f"Parameter 'load_torque_unit' must be a string.") + if not isinstance(force_unit, str): + raise TypeError(f"Parameter 'force_unit' must be a string.") + + if not isinstance(stress_unit, str): + raise TypeError(f"Parameter 'stress_unit' must be a string.") + if not isinstance(print_data, bool): raise TypeError(f"Parameter 'print_data' must be a bool.") @@ -205,7 +221,10 @@ def snapshot(self, f'angular acceleration ({angular_acceleration_unit})', f'torque ({torque_unit})', f'driving torque ({driving_torque_unit})', - f'load torque ({load_torque_unit})']) + f'load torque ({load_torque_unit})', + f'tangential force ({force_unit})', + f'bending stress ({stress_unit})', + f'contact stress ({stress_unit})']) for element in self.chain: for variable, unit in zip(['angular position', 'angular speed', 'angular acceleration', @@ -217,6 +236,25 @@ def snapshot(self, for value in element.time_variables[variable]]) data.loc[element.name, f'{variable} ({unit})'] = interpolation_function(target_time.to('sec').value).take(0) + if isinstance(element, GearBase): + variable_list = [] + unit_list = [] + if element.tangential_force_is_computable: + variable_list.append('tangential force') + unit_list.append(force_unit) + if element.bending_stress_is_computable: + variable_list.append('bending stress') + unit_list.append(stress_unit) + if element.contact_stress_is_computable: + variable_list.append('contact stress') + unit_list.append(stress_unit) + + for variable, unit in zip(variable_list, unit_list): + interpolation_function = interp1d(x = [instant.to('sec').value for instant in self.time], + y = [value.to(unit).value + for value in element.time_variables[variable]]) + data.loc[element.name, f'{variable} ({unit})'] = interpolation_function(target_time.to('sec').value).take(0) + if print_data: print(f'Mechanical Transmission Status at Time = {target_time}') print(data.to_string()) @@ -231,15 +269,19 @@ def plot(self, angular_speed_unit: str = 'rad/s', angular_acceleration_unit: str = 'rad/s^2', torque_unit: str = 'Nm', + force_unit: str = 'N', + stress_unit: str = 'MPa', time_unit: str = 'sec', figsize: Union[None, tuple] = None): """Plots time variables for selected elements in the mechanical transmission chain. \n Generates a grid of subplots, one column for each selected element of the transmission chain and one rows for each selected time variable. \n Available elements are the ones in the ``chain`` tuple and available variables are: ``'angular position'``, - ``'angular speed'``, ``'angular acceleration'``, ``'torque'``, ``'driving torque'`` and ``'load torque'``. + ``'angular speed'``, ``'angular acceleration'``, ``'torque'``, ``'driving torque'``, ``'load torque'`` and + ``'tangential force'``. The kinematic variables position, speed and acceleration are separately plotted in the first three rows of the - grid, while torques are grouped together in the last fourth row. \n + grid, while torques are grouped together in the last fourth row (if kinematic variables are present) and + tangential force is plotted in the fifth row (if all other variables are present). \n Plotted values' units are managed with optional parameters. \n Elements to be plotted can be passed as instances or names (strings) in a list. @@ -262,6 +304,12 @@ def plot(self, torque_unit : str, optional Symbol of the unit of measurement to which convert the torque values in the plot. It must be a string. Default is ``'Nm'``. + force_unit : str, optional + Symbol of the unit of measurement to which convert the force values in the plot. It must be a string. + Default is ``'N'``. + stress_unit : str, optional + Symbol of the unit of measurement to which convert the stress values in the plot. It must be a string. + Default is ``'MPa'``. time_unit : str, optional Symbol of the unit of measurement to which convert the time values in the plot. It must be a string. Default is ``'sec'``. @@ -279,6 +327,8 @@ def plot(self, - if ``angular_speed_unit`` is not a string, - if ``angular_acceleration_unit`` is not a string, - if ``torque_unit`` is not a string, + - if ``force_unit`` is not a string, + - if ``stress_unit`` is not a string, - if ``time_unit`` is not a string. ValueError - If ``elements`` is an empty list, @@ -315,7 +365,10 @@ def plot(self, if not variables: raise ValueError("Parameter 'variables' cannot be an empty list.") - valid_variables = list(self.chain[0].time_variables.keys()) + valid_variables = [] + for element in self.chain: + valid_variables.extend(element.time_variables.keys()) + valid_variables = list(set(valid_variables)) for variable in variables: if not isinstance(variable, str): raise TypeError("Each element of 'variables' must be a string.") @@ -335,6 +388,12 @@ def plot(self, if not isinstance(torque_unit, str): raise TypeError("Parameter 'torque_unit' must be a string.") + if not isinstance(force_unit, str): + raise TypeError("Parameter 'force_unit' must be a string.") + + if not isinstance(stress_unit, str): + raise TypeError("Parameter 'stress_unit' must be a string.") + if not isinstance(time_unit, str): raise TypeError("Parameter 'time_unit' must be a string.") @@ -352,35 +411,57 @@ def plot(self, elements = self.chain else: elements = [element for element in self.chain if element in elements or element.name in elements] + n_elements = len(elements) if variables is None: - variables = list(self.chain[0].time_variables.keys()) + variables = [] + for element in elements: + variables.extend(element.time_variables.keys()) + variables = list(set(variables)) else: variables = list(set(variables)) SORT_ORDER = {'angular position': 0, 'angular speed': 1, 'angular acceleration': 2, - 'torque': 3, 'driving torque': 4, 'load torque': 5} + 'torque': 3, 'driving torque': 4, 'load torque': 5, 'tangential force': 6, 'bending stress': 7, + 'contact stress': 8} variables.sort(key = lambda variable: SORT_ORDER[variable]) - kinematic_variables = [variable for variable in variables if 'torque' not in variable] + kinematic_variables = [variable for variable in variables + if 'torque' not in variable and 'force' not in variable and 'stress' not in variable] torques_variables = [variable for variable in variables if 'torque' in variable] + forces_variables = [variable for variable in variables if 'force' in variable] + stress_variables = [variable for variable in variables if 'stress' in variable] + + torques_variables_index = len(kinematic_variables) + forces_variables_index = len(kinematic_variables) + stress_variables_index = len(kinematic_variables) + n_variables = len(kinematic_variables) if torques_variables: n_variables += 1 + forces_variables_index += 1 + stress_variables_index += 1 + if forces_variables: + n_variables += 1 + stress_variables_index += 1 + if stress_variables: + n_variables += 1 time_values = [instant.to(time_unit).value for instant in self.time] UNITS = {'angular position': angular_position_unit, 'angular speed': angular_speed_unit, 'angular acceleration': angular_acceleration_unit, 'torque': torque_unit, - 'driving torque': torque_unit, 'load torque': torque_unit} + 'driving torque': torque_unit, 'load torque': torque_unit, 'tangential force': force_unit, + 'bending stress': stress_unit, 'contact stress': stress_unit} - fig, ax = plt.subplots(nrows = n_variables, ncols = len(elements), sharex = 'all', figsize = figsize) + fig, ax = plt.subplots(nrows = n_variables, ncols = n_elements, sharex = 'all', + layout = 'constrained', figsize = figsize) for i, item in enumerate(elements, 0): if n_variables > 1: - axes = ax[:, i] if len(elements) > 1 else ax + axes = ax[:, i] if n_elements > 1 else ax else: - axes = [ax[i]] if len(elements) > 1 else [ax] + axes = [ax[i]] if n_elements > 1 else [ax] axes[0].set_title(item.name) for j, variable in enumerate(kinematic_variables, 0): @@ -390,29 +471,98 @@ def plot(self, for variable in torques_variables: label = variable.replace('torque', '').replace(' ', '') label = 'net' if label == '' else label - axes[-1].plot(time_values, [variable_value.to(UNITS[variable]).value - for variable_value in item.time_variables[variable]], label = label) + axes[torques_variables_index].plot(time_values, + [variable_value.to(UNITS[variable]).value + for variable_value in item.time_variables[variable]], + label = label) + + if isinstance(item, GearBase): + if item.tangential_force_is_computable: + for variable in forces_variables: + axes[forces_variables_index].plot(time_values, + [variable_value.to(UNITS[variable]).value + for variable_value in item.time_variables[variable]]) + + for variable in stress_variables: + if (variable == 'bending stress' and item.bending_stress_is_computable) or \ + (variable == 'contact stress' and item.contact_stress_is_computable): + axes[stress_variables_index].plot(time_values, + [variable_value.to(UNITS[variable]).value + for variable_value in item.time_variables[variable]], + label = variable.replace('stress', '').replace(' ', '')) + + if stress_variables \ + and not item.bending_stress_is_computable and not item.contact_stress_is_computable: + axes[stress_variables_index].axis('off') - axes[-1].set_xlabel(f'time ({time_unit})') + else: + if forces_variables: + axes[forces_variables_index].axis('off') + if stress_variables: + axes[stress_variables_index].axis('off') + + else: + if forces_variables: + axes[forces_variables_index].axis('off') + if stress_variables: + axes[stress_variables_index].axis('off') + + last_row_index = n_variables - 1 + if not isinstance(item, GearBase): + if forces_variables: + last_row_index -= 1 + if stress_variables: + last_row_index -= 1 + else: + if forces_variables and not item.tangential_force_is_computable: + last_row_index -= 1 + if stress_variables and \ + (not item.bending_stress_is_computable or 'bending stress' not in stress_variables) and \ + (not item.contact_stress_is_computable or 'contact stress' not in stress_variables): + last_row_index -= 1 + + axes[last_row_index].set_xlabel(f'time ({time_unit})') + axes[last_row_index].xaxis.set_tick_params(which = 'both', labelbottom = True) if n_variables > 1: - first_column_axes = ax[:, 0] if len(elements) > 1 else ax + first_column_axes = ax[:, 0] if n_elements > 1 else ax else: - first_column_axes = [ax[0]] if len(elements) > 1 else [ax] + first_column_axes = [ax[0]] if n_elements > 1 else [ax] for j, variable in enumerate(kinematic_variables, 0): first_column_axes[j].set_ylabel(f'{variable} ({UNITS[variable]})') if torques_variables: - first_column_axes[-1].set_ylabel(f'torque ({torque_unit})') - first_column_axes[-1].legend(title = 'torque', frameon = True) - - if len(elements) > 1 or n_variables > 1: + first_column_axes[torques_variables_index].set_ylabel(f'torque ({torque_unit})') + first_column_axes[torques_variables_index].legend(title = 'torque', frameon = True) + + if forces_variables: + for i, item in enumerate(elements, 0): + if isinstance(item, GearBase): + if item.tangential_force_is_computable: + if n_variables > 1: + axes = ax[:, i] if n_elements > 1 else ax + else: + axes = [ax[i]] if n_elements > 1 else [ax] + axes[forces_variables_index].set_ylabel(f'force ({force_unit})') + break + + if stress_variables: + for i, item in enumerate(elements, 0): + if isinstance(item, GearBase): + if item.bending_stress_is_computable: + if n_variables > 1: + axes = ax[:, i] if n_elements > 1 else ax + else: + axes = [ax[i]] if n_elements > 1 else [ax] + axes[stress_variables_index].set_ylabel(f'stress ({stress_unit})') + axes[stress_variables_index].legend(title = 'stress', frameon = True) + break + + if n_elements > 1 or n_variables > 1: for axi in ax.flatten(): axi.tick_params(bottom = False, top = False, left = False, right = False) else: ax.tick_params(bottom = False, top = False, left = False, right = False) - plt.tight_layout() - plt.show() diff --git a/gearpy/units/__init__.py b/gearpy/units/__init__.py index 0135cb6..718275d 100644 --- a/gearpy/units/__init__.py +++ b/gearpy/units/__init__.py @@ -1,7 +1,11 @@ from .units import AngularPosition, \ AngularSpeed, \ AngularAcceleration, \ + Force, \ InertiaMoment, \ + Length, \ + Stress, \ + Surface, \ Torque, \ Time, \ TimeInterval diff --git a/gearpy/units/unit_base.py b/gearpy/units/unit_base.py index 813eb2e..7cc818e 100644 --- a/gearpy/units/unit_base.py +++ b/gearpy/units/unit_base.py @@ -1,7 +1,11 @@ from abc import ABC, abstractmethod +from math import fabs from typing import Union +COMPARISON_TOLERANCE = 1e-12 + + class UnitBase(ABC): __UNITS = {} @@ -14,20 +18,29 @@ def __init__(self, value: Union[float, int], unit: str): if not isinstance(unit, str): raise TypeError("Parameter 'unit' must be a string.") - @abstractmethod - def __repr__(self): ... + def __repr__(self): + return f'{self.value} {self.unit}' + + def __abs__(self): + return self.__class__(abs(self.value), self.unit) - @abstractmethod def __add__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'It is not allowed to sum a {self.__class__.__name__} and a {other.__class__.__name__}.') - @abstractmethod + return self.__class__(value = self.value + other.to(self.unit).value, unit = self.unit) + def __sub__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'It is not allowed to subtract a {other.__class__.__name__} ' f'from a {self.__class__.__name__}.') + try: + return self.__class__(value = self.value - other.to(self.unit).value, unit = self.unit) + except ValueError: + if self.value - other.to(self.unit).value <= 0: + raise ValueError('Cannot perform the subtraction because the result is not positive.') + @abstractmethod def __mul__(self, other: Union[float, int]) -> None: ... @@ -46,36 +59,60 @@ def __truediv__(self, other: Union['UnitBase', float, int]) -> None: if other == 0: raise ZeroDivisionError('It is not allowed to divide a Unit by zero.') - @abstractmethod def __eq__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'Cannot compare {self.__class__.__name__} and {other.__class__.__name__}') - @abstractmethod + if self.unit == other.unit: + return self.value == other.value + else: + return fabs(self.value - other.to(self.unit).value) < COMPARISON_TOLERANCE + def __ne__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'Cannot compare {self.__class__.__name__} and {other.__class__.__name__}') - @abstractmethod + if self.unit == other.unit: + return self.value != other.value + else: + return fabs(self.value - other.to(self.unit).value) > COMPARISON_TOLERANCE + def __gt__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'Cannot compare {self.__class__.__name__} and {other.__class__.__name__}') - @abstractmethod + if self.unit == other.unit: + return self.value > other.value + else: + return self.value - other.to(self.unit).value > COMPARISON_TOLERANCE + def __ge__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'Cannot compare {self.__class__.__name__} and {other.__class__.__name__}') - @abstractmethod + if self.unit == other.unit: + return self.value >= other.value + else: + return self.value - other.to(self.unit).value >= -COMPARISON_TOLERANCE + def __lt__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'Cannot compare {self.__class__.__name__} and {other.__class__.__name__}') - @abstractmethod + if self.unit == other.unit: + return self.value < other.value + else: + return self.value - other.to(self.unit).value < -COMPARISON_TOLERANCE + def __le__(self, other: 'UnitBase') -> None: if not isinstance(other, self.__class__) and not issubclass(self.__class__, other.__class__): raise TypeError(f'Cannot compare {self.__class__.__name__} and {other.__class__.__name__}') + if self.unit == other.unit: + return self.value <= other.value + else: + return self.value - other.to(self.unit).value <= COMPARISON_TOLERANCE + @property @abstractmethod def value(self) -> None: ... diff --git a/gearpy/units/units.py b/gearpy/units/units.py index 0d2bdb8..67b0304 100644 --- a/gearpy/units/units.py +++ b/gearpy/units/units.py @@ -1,11 +1,8 @@ -from math import pi, fabs +from math import pi from typing import Union from .unit_base import UnitBase -COMPARISON_TOLERANCE = 1e-12 - - class AngularPosition(UnitBase): r"""``gearpy.units.units.AngularPosition`` object. @@ -38,19 +35,6 @@ def __init__(self, value: Union[float, int], unit: str): self.__value = value self.__unit = unit - def __repr__(self) -> str: - return f'{self.__value} {self.__unit}' - - def __add__(self, other: 'AngularPosition') -> 'AngularPosition': - super().__add__(other = other) - - return AngularPosition(value = self.__value + other.to(self.__unit).value, unit = self.__unit) - - def __sub__(self, other: 'AngularPosition') -> 'AngularPosition': - super().__sub__(other = other) - - return AngularPosition(value = self.__value - other.to(self.__unit).value, unit = self.__unit) - def __mul__(self, other: Union[float, int]) -> 'AngularPosition': super().__mul__(other = other) @@ -81,54 +65,6 @@ def __truediv__(self, other: Union['AngularPosition', float, int]) -> Union['Ang else: return AngularPosition(value = self.__value/other, unit = self.__unit) - def __eq__(self, other: 'AngularPosition') -> bool: - super().__eq__(other = other) - - if self.__unit == other.unit: - return self.__value == other.value - else: - return fabs(self.__value - other.to(self.__unit).value) < COMPARISON_TOLERANCE - - def __ne__(self, other: 'AngularPosition') -> bool: - super().__ne__(other = other) - - if self.__unit == other.unit: - return self.__value != other.value - else: - return fabs(self.__value - other.to(self.__unit).value) > COMPARISON_TOLERANCE - - def __gt__(self, other: 'AngularPosition') -> bool: - super().__gt__(other = other) - - if self.__unit == other.unit: - return self.__value > other.value - else: - return self.__value - other.to(self.__unit).value > COMPARISON_TOLERANCE - - def __ge__(self, other: 'AngularPosition') -> bool: - super().__ge__(other = other) - - if self.__unit == other.unit: - return self.__value >= other.value - else: - return self.__value - other.to(self.__unit).value >= -COMPARISON_TOLERANCE - - def __lt__(self, other: 'AngularPosition') -> bool: - super().__lt__(other = other) - - if self.__unit == other.unit: - return self.__value < other.value - else: - return self.__value - other.to(self.__unit).value < -COMPARISON_TOLERANCE - - def __le__(self, other: 'AngularPosition') -> bool: - super().__le__(other = other) - - if self.__unit == other.unit: - return self.__value <= other.value - else: - return self.__value - other.to(self.__unit).value <= COMPARISON_TOLERANCE - @property def value(self) -> Union[float, int]: """Angular position numerical value. The relative unit is expressed by the ``unit`` property. @@ -274,19 +210,6 @@ def __init__(self, value: Union[float, int], unit: str): self.__value = value self.__unit = unit - def __repr__(self) -> str: - return f'{self.__value} {self.__unit}' - - def __add__(self, other: 'AngularSpeed') -> 'AngularSpeed': - super().__add__(other = other) - - return AngularSpeed(value = self.__value + other.to(self.__unit).value, unit = self.__unit) - - def __sub__(self, other: 'AngularSpeed') -> 'AngularSpeed': - super().__sub__(other = other) - - return AngularSpeed(value = self.__value - other.to(self.__unit).value, unit = self.__unit) - def __mul__(self, other: Union['Time', float, int]) -> Union['AngularPosition', 'AngularSpeed']: super().__mul__(other = other) @@ -322,54 +245,6 @@ def __truediv__(self, other: Union['AngularSpeed', float, int]) -> Union['Angula else: return AngularSpeed(value = self.__value/other, unit = self.__unit) - def __eq__(self, other: 'AngularSpeed') -> bool: - super().__eq__(other = other) - - if self.__unit == other.unit: - return self.__value == other.value - else: - return fabs(self.__value - other.to(self.__unit).value) < COMPARISON_TOLERANCE - - def __ne__(self, other: 'AngularSpeed') -> bool: - super().__ne__(other = other) - - if self.__unit == other.unit: - return self.__value != other.value - else: - return fabs(self.__value - other.to(self.__unit).value) > COMPARISON_TOLERANCE - - def __gt__(self, other: 'AngularSpeed') -> bool: - super().__gt__(other = other) - - if self.__unit == other.unit: - return self.__value > other.value - else: - return self.__value - other.to(self.__unit).value > COMPARISON_TOLERANCE - - def __ge__(self, other: 'AngularSpeed') -> bool: - super().__ge__(other = other) - - if self.__unit == other.unit: - return self.__value >= other.value - else: - return self.__value - other.to(self.__unit).value >= -COMPARISON_TOLERANCE - - def __lt__(self, other: 'AngularSpeed') -> bool: - super().__lt__(other = other) - - if self.__unit == other.unit: - return self.__value < other.value - else: - return self.__value - other.to(self.__unit).value < -COMPARISON_TOLERANCE - - def __le__(self, other: 'AngularSpeed') -> bool: - super().__le__(other = other) - - if self.__unit == other.unit: - return self.__value <= other.value - else: - return self.__value - other.to(self.__unit).value <= COMPARISON_TOLERANCE - @property def value(self) -> Union[float, int]: """Angular speed numerical value. The relative unit is expressed by the ``unit`` property. @@ -515,19 +390,6 @@ def __init__(self, value: Union[float, int], unit: str): self.__value = value self.__unit = unit - def __repr__(self) -> str: - return f'{self.__value} {self.__unit}' - - def __add__(self, other: 'AngularAcceleration') -> 'AngularAcceleration': - super().__add__(other = other) - - return AngularAcceleration(value = self.__value + other.to(self.__unit).value, unit = self.__unit) - - def __sub__(self, other: 'AngularAcceleration') -> 'AngularAcceleration': - super().__sub__(other = other) - - return AngularAcceleration(value = self.__value - other.to(self.__unit).value, unit = self.__unit) - def __mul__(self, other: Union['Time', float, int]) -> Union['AngularAcceleration', 'AngularSpeed']: super().__mul__(other = other) @@ -564,54 +426,6 @@ def __truediv__(self, other: Union['AngularAcceleration', float, int]) -> Union[ else: return AngularAcceleration(value = self.__value/other, unit = self.__unit) - def __eq__(self, other: 'AngularAcceleration') -> bool: - super().__eq__(other = other) - - if self.__unit == other.unit: - return self.__value == other.value - else: - return fabs(self.__value - other.to(self.__unit).value) < COMPARISON_TOLERANCE - - def __ne__(self, other: 'AngularAcceleration') -> bool: - super().__ne__(other = other) - - if self.__unit == other.unit: - return self.__value != other.value - else: - return fabs(self.__value - other.to(self.__unit).value) > COMPARISON_TOLERANCE - - def __gt__(self, other: 'AngularAcceleration') -> bool: - super().__gt__(other = other) - - if self.__unit == other.unit: - return self.__value > other.value - else: - return self.__value - other.to(self.__unit).value > COMPARISON_TOLERANCE - - def __ge__(self, other: 'AngularAcceleration') -> bool: - super().__ge__(other = other) - - if self.__unit == other.unit: - return self.__value >= other.value - else: - return self.__value - other.to(self.__unit).value >= -COMPARISON_TOLERANCE - - def __lt__(self, other: 'AngularAcceleration') -> bool: - super().__lt__(other = other) - - if self.__unit == other.unit: - return self.__value < other.value - else: - return self.__value - other.to(self.__unit).value < -COMPARISON_TOLERANCE - - def __le__(self, other: 'AngularAcceleration') -> bool: - super().__le__(other = other) - - if self.__unit == other.unit: - return self.__value <= other.value - else: - return self.__value - other.to(self.__unit).value <= COMPARISON_TOLERANCE - @property def value(self) -> Union[float, int]: """Angular acceleration numerical value. The relative unit is expressed by the ``unit`` property. @@ -759,22 +573,6 @@ def __init__(self, value: Union[float, int], unit: str): self.__value = value self.__unit = unit - def __repr__(self) -> str: - return f'{self.__value} {self.__unit}' - - def __add__(self, other: 'InertiaMoment') -> 'InertiaMoment': - super().__add__(other = other) - - return InertiaMoment(value = self.__value + other.to(self.__unit).value, unit = self.__unit) - - def __sub__(self, other: 'InertiaMoment') -> 'InertiaMoment': - super().__sub__(other = other) - - if self.__value - other.to(self.__unit).value <= 0: - raise ValueError('Cannot perform the subtraction because the result is not positive.') - - return InertiaMoment(value = self.__value - other.to(self.__unit).value, unit = self.__unit) - def __mul__(self, other: Union[float, int]) -> 'InertiaMoment': super().__mul__(other = other) @@ -811,54 +609,6 @@ def __truediv__(self, other: Union['InertiaMoment', float, int]) -> Union['Inert else: return InertiaMoment(value = self.__value/other, unit = self.__unit) - def __eq__(self, other: 'InertiaMoment') -> bool: - super().__eq__(other = other) - - if self.__unit == other.unit: - return self.__value == other.value - else: - return fabs(self.__value - other.to(self.__unit).value) < COMPARISON_TOLERANCE - - def __ne__(self, other: 'InertiaMoment') -> bool: - super().__ne__(other = other) - - if self.__unit == other.unit: - return self.__value != other.value - else: - return fabs(self.__value - other.to(self.__unit).value) > COMPARISON_TOLERANCE - - def __gt__(self, other: 'InertiaMoment') -> bool: - super().__gt__(other = other) - - if self.__unit == other.unit: - return self.__value > other.value - else: - return self.__value - other.to(self.__unit).value > COMPARISON_TOLERANCE - - def __ge__(self, other: 'InertiaMoment') -> bool: - super().__ge__(other = other) - - if self.__unit == other.unit: - return self.__value >= other.value - else: - return self.__value - other.to(self.__unit).value >= -COMPARISON_TOLERANCE - - def __lt__(self, other: 'InertiaMoment') -> bool: - super().__lt__(other = other) - - if self.__unit == other.unit: - return self.__value < other.value - else: - return self.__value - other.to(self.__unit).value < -COMPARISON_TOLERANCE - - def __le__(self, other: 'InertiaMoment') -> bool: - super().__le__(other = other) - - if self.__unit == other.unit: - return self.__value <= other.value - else: - return self.__value - other.to(self.__unit).value <= COMPARISON_TOLERANCE - @property def value(self) -> Union[float, int]: """Moment of inertia numerical value. The relative unit is expressed by the ``unit`` property. It must be @@ -1020,19 +770,6 @@ def __init__(self, value: Union[float, int], unit: str): self.__value = value self.__unit = unit - def __repr__(self) -> str: - return f'{self.__value} {self.__unit}' - - def __add__(self, other: 'Torque') -> 'Torque': - super().__add__(other = other) - - return Torque(value = self.__value + other.to(self.__unit).value, unit = self.__unit) - - def __sub__(self, other: 'Torque') -> 'Torque': - super().__sub__(other = other) - - return Torque(value = self.__value - other.to(self.__unit).value, unit = self.__unit) - def __mul__(self, other: Union[float, int]) -> 'Torque': super().__mul__(other = other) @@ -1051,68 +788,22 @@ def __rmul__(self, other: Union[float, int]) -> 'Torque': return Torque(value = self.__value*other, unit = self.__unit) - def __truediv__(self, other: Union['InertiaMoment', 'Torque', float, int]) -> Union['AngularAcceleration', 'Torque', float]: + def __truediv__(self, other: Union['InertiaMoment', 'Length', 'Torque', float, int]) -> Union['AngularAcceleration', 'Force', float, 'Torque']: super().__truediv__(other = other) - if not isinstance(other, Torque) and not isinstance(other, float) and not isinstance(other, int) \ - and not isinstance(other, InertiaMoment): + if not isinstance(other, InertiaMoment) and not isinstance(other, Length) and not isinstance(other, Torque) \ + and not isinstance(other, float) and not isinstance(other, int): raise TypeError(f'It is not allowed to divide a {self.__class__.__name__} by a {other.__class__.__name__}.') - if isinstance(other, Torque): - return self.__value/other.to(self.__unit).value - elif isinstance(other, InertiaMoment): + if isinstance(other, InertiaMoment): return AngularAcceleration(value = self.to('Nm').value/other.to('kgm^2').value, unit = 'rad/s^2') + elif isinstance(other, Length): + return Force(value = self.to('Nm').value/other.to('m').value, unit = 'N') + elif isinstance(other, Torque): + return self.__value/other.to(self.__unit).value else: return Torque(value = self.__value/other, unit = self.__unit) - def __eq__(self, other: 'Torque') -> bool: - super().__eq__(other = other) - - if self.__unit == other.unit: - return self.__value == other.value - else: - return fabs(self.__value - other.to(self.__unit).value) < COMPARISON_TOLERANCE - - def __ne__(self, other: 'Torque') -> bool: - super().__ne__(other = other) - - if self.__unit == other.unit: - return self.__value != other.value - else: - return fabs(self.__value - other.to(self.__unit).value) > COMPARISON_TOLERANCE - - def __gt__(self, other: 'Torque') -> bool: - super().__gt__(other = other) - - if self.__unit == other.unit: - return self.__value > other.value - else: - return self.__value - other.to(self.__unit).value > COMPARISON_TOLERANCE - - def __ge__(self, other: 'Torque') -> bool: - super().__ge__(other = other) - - if self.__unit == other.unit: - return self.__value >= other.value - else: - return self.__value - other.to(self.__unit).value >= -COMPARISON_TOLERANCE - - def __lt__(self, other: 'Torque') -> bool: - super().__lt__(other = other) - - if self.__unit == other.unit: - return self.__value < other.value - else: - return self.__value - other.to(self.__unit).value < -COMPARISON_TOLERANCE - - def __le__(self, other: 'Torque') -> bool: - super().__le__(other = other) - - if self.__unit == other.unit: - return self.__value <= other.value - else: - return self.__value - other.to(self.__unit).value <= COMPARISON_TOLERANCE - @property def value(self) -> Union[float, int]: """Torque numerical value. The relative unit is expressed by the ``unit`` property. @@ -1267,19 +958,6 @@ def __init__(self, value: Union[float, int], unit: str): self.__value = value self.__unit = unit - def __repr__(self) -> str: - return f'{self.__value} {self.__unit}' - - def __add__(self, other: 'Time') -> 'Time': - super().__add__(other = other) - - return Time(value = self.__value + other.to(self.__unit).value, unit = self.__unit) - - def __sub__(self, other: 'Time') -> 'Time': - super().__sub__(other = other) - - return Time(value = self.__value - other.to(self.__unit).value, unit = self.__unit) - def __mul__(self, other: Union['AngularAcceleration', 'AngularSpeed', float, int]) -> Union['AngularPosition', 'AngularSpeed', 'Time']: super().__mul__(other = other) @@ -1321,54 +999,6 @@ def __truediv__(self, other: Union['Time', float, int]) -> Union['Time', float]: else: return Time(value = self.__value/other, unit = self.__unit) - def __eq__(self, other: 'Time') -> bool: - super().__eq__(other = other) - - if self.__unit == other.unit: - return self.__value == other.value - else: - return fabs(self.__value - other.to(self.__unit).value) < COMPARISON_TOLERANCE - - def __ne__(self, other: 'Time') -> bool: - super().__ne__(other = other) - - if self.__unit == other.unit: - return self.__value != other.value - else: - return fabs(self.__value - other.to(self.__unit).value) > COMPARISON_TOLERANCE - - def __gt__(self, other: 'Time') -> bool: - super().__gt__(other = other) - - if self.__unit == other.unit: - return self.__value > other.value - else: - return self.__value - other.to(self.__unit).value > COMPARISON_TOLERANCE - - def __ge__(self, other: 'Time') -> bool: - super().__ge__(other = other) - - if self.__unit == other.unit: - return self.__value >= other.value - else: - return self.__value - other.to(self.__unit).value >= -COMPARISON_TOLERANCE - - def __lt__(self, other: 'Time') -> bool: - super().__lt__(other = other) - - if self.__unit == other.unit: - return self.__value < other.value - else: - return self.__value - other.to(self.__unit).value < -COMPARISON_TOLERANCE - - def __le__(self, other: 'Time') -> bool: - super().__le__(other = other) - - if self.__unit == other.unit: - return self.__value <= other.value - else: - return self.__value - other.to(self.__unit).value <= COMPARISON_TOLERANCE - @property def value(self) -> Union[float, int]: """Time numerical value. The relative unit is expressed by the ``unit`` property. @@ -1513,9 +1143,6 @@ def __add__(self, other: Union['Time', 'TimeInterval']) -> Union['Time', 'TimeIn def __sub__(self, other: Union['Time', 'TimeInterval']) -> Union['Time', 'TimeInterval']: super().__sub__(other = other) - if self.__value - other.to(self.__unit).value <= 0: - raise ValueError('Cannot perform the subtraction because the result is not positive.') - if isinstance(other, TimeInterval): return TimeInterval(value = self.__value - other.to(self.__unit).value, unit = self.__unit) else: @@ -1644,3 +1271,702 @@ def to(self, target_unit: str, inplace: bool = False) -> 'TimeInterval': return self else: return TimeInterval(value = converted.value, unit = converted.unit) + + +class Length(UnitBase): + r"""``gearpy.units.units.Length`` object. + + Attributes + ---------- + :py:attr:`unit` : str + Symbol of the unit of measurement for length. + :py:attr:`value` : float or int + Length numerical value. + + Methods + ------- + :py:meth:`to` + Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + """ + + __UNITS = {'m': 1, + 'dm': 1e-1, + 'cm': 1e-2, + 'mm': 1e-3} + + def __init__(self, value: Union[float, int], unit: str): + super().__init__(value = value, unit = unit) + + if unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + if value <= 0: + raise ValueError("Parameter 'value' must be positive.") + + self.__value = value + self.__unit = unit + + def __mul__(self, other: Union['Length', float, int]) -> Union['Surface', 'Length']: + super().__mul__(other = other) + + if not isinstance(other, Length) and not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + if isinstance(other, Length): + return Surface(value = self.to('m').value*other.to('m').value, unit = 'm^2') + else: + return Length(value = self.__value*other, unit = self.__unit) + + def __rmul__(self, other: Union[float, int]) -> 'Length': + super().__rmul__(other = other) + + if not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply a {other.__class__.__name__} by an ' + f'{self.__class__.__name__}.') + + return Length(value = self.__value*other, unit = self.__unit) + + def __truediv__(self, other: Union['Length', float, int]) -> Union['Length', float]: + super().__truediv__(other = other) + + if not isinstance(other, Length) and not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to divide an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + if isinstance(other, Length): + return self.__value/other.to(self.__unit).value + else: + return Length(value = self.__value/other, unit = self.__unit) + + @property + def value(self) -> Union[float, int]: + """Length numerical value. The relative unit is expressed by the ``unit`` property. It must be positive. + + Returns + ------- + float or int + Length numerical value. + + Raises + ------ + TypeError + If ``value`` is not a float or an integer. + ValueError + If ``value`` is not positive. + """ + return self.__value + + @property + def unit(self) -> str: + """Symbol of the unit of measurement for length. It must be a string. + Available units are: + + - ``'m'`` for meter, + - ``'dm'`` for decimeter, + - ``'cm'`` for centimeter, + - ``'mm'`` for millimeter. + + Returns + ------- + str + Symbol of the unit of measurement for length. + + Raises + ------ + TypeError + If ``unit`` is not a string. + KeyError + If the ``unit`` is not among available ones. + """ + return self.__unit + + def to(self, target_unit: str, inplace: bool = False) -> 'Length': + """Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + If ``inplace`` is ``True``, it overrides actual ``value`` and ``unit``, otherwise it returns a new instance with + the converted ``value`` and the ``target_unit`` as ``unit``. + + Parameters + ---------- + target_unit : str + Target unit to which convert the current value. + inplace : bool, optional + Whether or not to override the current instance value. Default is ``False``, so it does not override the + current value. + + Returns + ------- + Length + Converted length. + + Raises + ------ + TypeError + - If ``target_unit`` is not a string, + - if ``inplace`` is not a bool. + KeyError + If the ``target_unit`` is not among available ones. + + Examples + -------- + ``Length`` instantiation. + + >>> from gearpy.units import Length + >>> l = Length(1, 'm') + >>> l + ... 1 m + + Conversion from meter to centimeter with ``inplace = False`` by default, so it does not override the current + value. + + >>> l.to('cm') + ... 100.0 cm + >>> l + ... 1 m + + Conversion from meter to centimeter with ``inplace = True``, in order to override the current value. + + >>> l.to('cm', inplace = True) + ... 100.0 cm + >>> l + ... 100.0 cm + """ + super().to(target_unit = target_unit, inplace = inplace) + + if target_unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{target_unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + if target_unit != self.__unit: + target_value = self.__value*self.__UNITS[self.__unit]/self.__UNITS[target_unit] + else: + target_value = self.__value + + if inplace: + self.__value = target_value + self.__unit = target_unit + return self + else: + return Length(value = target_value, unit = target_unit) + + +class Surface(UnitBase): + r"""``gearpy.units.units.Surface`` object. + + Attributes + ---------- + :py:attr:`unit` : str + Symbol of the unit of measurement for surface. + :py:attr:`value` : float or int + Surface numerical value. + + Methods + ------- + :py:meth:`to` + Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + """ + + __UNITS = {'m^2': 1, + 'dm^2': 1e-2, + 'cm^2': 1e-4, + 'mm^2': 1e-6} + + def __init__(self, value: Union[float, int], unit: str): + super().__init__(value = value, unit = unit) + + if unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + if value <= 0: + raise ValueError("Parameter 'value' must be positive.") + + self.__value = value + self.__unit = unit + + def __mul__(self, other: Union[float, int]) -> 'Surface': + super().__mul__(other = other) + + if not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + return Surface(value = self.__value*other, unit = self.__unit) + + def __rmul__(self, other: Union[float, int]) -> 'Surface': + super().__rmul__(other = other) + + if not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply a {other.__class__.__name__} by an ' + f'{self.__class__.__name__}.') + + return Surface(value = self.__value*other, unit = self.__unit) + + def __truediv__(self, other: Union['Surface', float, int]) -> Union['Surface', float]: + super().__truediv__(other = other) + + if not isinstance(other, Surface) and not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to divide an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + if isinstance(other, Surface): + return self.__value/other.to(self.__unit).value + else: + return Surface(value = self.__value/other, unit = self.__unit) + + @property + def value(self) -> Union[float, int]: + """Surface numerical value. The relative unit is expressed by the ``unit`` property. It must be positive. + + Returns + ------- + float or int + Surface numerical value. + + Raises + ------ + TypeError + If ``value`` is not a float or an integer. + ValueError + If ``value`` is not positive. + """ + return self.__value + + @property + def unit(self) -> str: + """Symbol of the unit of measurement for surface. It must be a string. + Available units are: + + - ``'m^2'`` for square meter, + - ``'dm^2'`` for square decimeter, + - ``'cm^2'`` for square centimeter, + - ``'mm^2'`` for square millimeter. + + Returns + ------- + str + Symbol of the unit of measurement for surface. + + Raises + ------ + TypeError + If ``unit`` is not a string. + KeyError + If the ``unit`` is not among available ones. + """ + return self.__unit + + def to(self, target_unit: str, inplace: bool = False) -> 'Surface': + """Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + If ``inplace`` is ``True``, it overrides actual ``value`` and ``unit``, otherwise it returns a new instance with + the converted ``value`` and the ``target_unit`` as ``unit``. + + Parameters + ---------- + target_unit : str + Target unit to which convert the current value. + inplace : bool, optional + Whether or not to override the current instance value. Default is ``False``, so it does not override the + current value. + + Returns + ------- + Surface + Converted surface. + + Raises + ------ + TypeError + - If ``target_unit`` is not a string, + - if ``inplace`` is not a bool. + KeyError + If the ``target_unit`` is not among available ones. + + Examples + -------- + ``Surface`` instantiation. + + >>> from gearpy.units import Surface + >>> s = Surface(1, 'm^2') + >>> s + ... 1 m^2 + + Conversion from square meter to square millimeter with ``inplace = False`` by default, so it does not override + the current value. + + >>> s.to('mm^2') + ... 1000000.0 mm^2 + >>> s + ... 1 m^2 + + Conversion from square meter to square millimeter with ``inplace = True``, in order to override the current + value. + + >>> s.to('mm^2', inplace = True) + ... 1000000.0 mm^2 + >>> s + ... 1000000.0 mm^2 + """ + super().to(target_unit = target_unit, inplace = inplace) + + if target_unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{target_unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + if target_unit != self.__unit: + target_value = self.__value*self.__UNITS[self.__unit]/self.__UNITS[target_unit] + else: + target_value = self.__value + + if inplace: + self.__value = target_value + self.__unit = target_unit + return self + else: + return Surface(value = target_value, unit = target_unit) + + +class Force(UnitBase): + r"""``gearpy.units.units.Force`` object. + + Attributes + ---------- + :py:attr:`unit` : str + Symbol of the unit of measurement for force. + :py:attr:`value` : float or int + Force numerical value. + + Methods + ------- + :py:meth:`to` + Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + """ + + __UNITS = {'N': 1, + 'mN': 1e-3, + 'kN': 1e3, + 'kgf': 9.80665, + 'gf': 9.80665e-3} + + def __init__(self, value: Union[float, int], unit: str): + super().__init__(value = value, unit = unit) + + if unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + self.__value = value + self.__unit = unit + + def __mul__(self, other: Union[float, int]) -> 'Force': + super().__mul__(other = other) + + if not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + return Force(value = self.__value*other, unit = self.__unit) + + def __rmul__(self, other: Union[float, int]) -> 'Force': + super().__rmul__(other = other) + + if not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply a {other.__class__.__name__} by an ' + f'{self.__class__.__name__}.') + + return Force(value = self.__value*other, unit = self.__unit) + + def __truediv__(self, other: Union['Force', 'Surface', float, int]) -> Union['Force', 'Stress', float]: + super().__truediv__(other = other) + + if not isinstance(other, Force) and not isinstance(other, Surface) and not isinstance(other, float) \ + and not isinstance(other, int): + raise TypeError(f'It is not allowed to divide an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + if isinstance(other, Force): + return self.__value/other.to(self.__unit).value + elif isinstance(other, Surface): + return Stress(value = self.to('N').value/other.to('m^2').value, unit = 'Pa') + else: + return Force(value = self.__value/other, unit = self.__unit) + + @property + def value(self) -> Union[float, int]: + """Force numerical value. The relative unit is expressed by the ``unit`` property. It must be positive. + + Returns + ------- + float or int + Force numerical value. + + Raises + ------ + TypeError + If ``value`` is not a float or an integer. + """ + return self.__value + + @property + def unit(self) -> str: + """Symbol of the unit of measurement for force. It must be a string. + Available units are: + + - ``'N'`` for newton, + - ``'mN'`` for milli-newton, + - ``'kN'`` for kilo-newton, + - ``'kgf'`` for kilogram force, + - ``'gf'`` for gram force. + + Returns + ------- + str + Symbol of the unit of measurement for force. + + Raises + ------ + TypeError + If ``unit`` is not a string. + KeyError + If the ``unit`` is not among available ones. + """ + return self.__unit + + def to(self, target_unit: str, inplace: bool = False) -> 'Force': + """Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + If ``inplace`` is ``True``, it overrides actual ``value`` and ``unit``, otherwise it returns a new instance with + the converted ``value`` and the ``target_unit`` as ``unit``. + + Parameters + ---------- + target_unit : str + Target unit to which convert the current value. + inplace : bool, optional + Whether or not to override the current instance value. Default is ``False``, so it does not override the + current value. + + Returns + ------- + Force + Converted force. + + Raises + ------ + TypeError + - If ``target_unit`` is not a string, + - if ``inplace`` is not a bool. + KeyError + If the ``target_unit`` is not among available ones. + + Examples + -------- + ``Force`` instantiation. + + >>> from gearpy.units import Force + >>> f = Force(1, 'N') + >>> f + ... 1 N + + Conversion from newton to kilogram force with ``inplace = False`` by default, so it does not override the + current value. + + >>> f.to('kgf') + ... 0.10197162129779283 kgf + >>> f + ... 1 N + + Conversion from newton to kilogram force with ``inplace = True``, in order to override the current value. + + >>> f.to('kgf', inplace = True) + ... 0.10197162129779283 kgf + >>> f + ... 0.10197162129779283 kgf + """ + super().to(target_unit = target_unit, inplace = inplace) + + if target_unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{target_unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + if target_unit != self.__unit: + target_value = self.__value*self.__UNITS[self.__unit]/self.__UNITS[target_unit] + else: + target_value = self.__value + + if inplace: + self.__value = target_value + self.__unit = target_unit + return self + else: + return Force(value = target_value, unit = target_unit) + + +class Stress(UnitBase): + r"""``gearpy.units.units.Stress`` object. + + Attributes + ---------- + :py:attr:`unit` : str + Symbol of the unit of measurement for stress. + :py:attr:`value` : float or int + Stress numerical value. + + Methods + ------- + :py:meth:`to` + Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + """ + + __UNITS = {'Pa': 1, + 'kPa': 1e3, + 'MPa': 1e6, + 'GPa': 1e9} + + def __init__(self, value: Union[float, int], unit: str): + super().__init__(value = value, unit = unit) + + if unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + self.__value = value + self.__unit = unit + + def __mul__(self, other: Union[float, int]) -> 'Stress': + super().__mul__(other = other) + + if not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + return Stress(value = self.__value*other, unit = self.__unit) + + def __rmul__(self, other: Union[float, int]) -> 'Stress': + super().__rmul__(other = other) + + if not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to multiply a {other.__class__.__name__} by an ' + f'{self.__class__.__name__}.') + + return Stress(value = self.__value*other, unit = self.__unit) + + def __truediv__(self, other: Union['Stress', float, int]) -> Union['Stress', float]: + super().__truediv__(other = other) + + if not isinstance(other, Stress) and not isinstance(other, float) and not isinstance(other, int): + raise TypeError(f'It is not allowed to divide an {self.__class__.__name__} by a ' + f'{other.__class__.__name__}.') + + if isinstance(other, Stress): + return self.__value/other.to(self.__unit).value + else: + return Stress(value = self.__value/other, unit = self.__unit) + + @property + def value(self) -> Union[float, int]: + """Stress numerical value. The relative unit is expressed by the ``unit`` property. It must be positive. + + Returns + ------- + float or int + Stress numerical value. + + Raises + ------ + TypeError + If ``value`` is not a float or an integer. + """ + return self.__value + + @property + def unit(self) -> str: + """Symbol of the unit of measurement for stress. It must be a string. + Available units are: + + - ``'Pa'`` for pascal, + - ``'kPa'`` for kilo-pascal, + - ``'MPa'`` for mega-pascal, + - ``'GPa'`` for giga-pascal. + + Returns + ------- + str + Symbol of the unit of measurement for stress. + + Raises + ------ + TypeError + If ``unit`` is not a string. + KeyError + If the ``unit`` is not among available ones. + """ + return self.__unit + + def to(self, target_unit: str, inplace: bool = False) -> 'Stress': + """Converts actual ``value`` to a new value computed using ``target_unit`` as the reference unit of measurement. + If ``inplace`` is ``True``, it overrides actual ``value`` and ``unit``, otherwise it returns a new instance with + the converted ``value`` and the ``target_unit`` as ``unit``. + + Parameters + ---------- + target_unit : str + Target unit to which convert the current value. + inplace : bool, optional + Whether or not to override the current instance value. Default is ``False``, so it does not override the + current value. + + Returns + ------- + Stress + Converted stress. + + Raises + ------ + TypeError + - If ``target_unit`` is not a string, + - if ``inplace`` is not a bool. + KeyError + If the ``target_unit`` is not among available ones. + + Examples + -------- + ``Stress`` instantiation. + + >>> from gearpy.units import Stress + >>> s = Stress(1, 'GPa') + >>> s + ... 1 GPa + + Conversion from giga-pascal to mega-pascal with ``inplace = False`` by default, so it does not override the + current value. + + >>> s.to('MPa') + ... 1000.0 MPa + >>> s + ... 1 GPa + + Conversion from giga-pascal to mega-pascal with ``inplace = True``, in order to override the current value. + + >>> s.to('MPa', inplace = True) + ... 1000.0 MPa + >>> s + ... 1000.0 MPa + """ + super().to(target_unit = target_unit, inplace = inplace) + + if target_unit not in self.__UNITS.keys(): + raise KeyError(f"{self.__class__.__name__} unit '{target_unit}' not available. " + f"Available units are: {list(self.__UNITS.keys())}") + + if target_unit != self.__unit: + target_value = self.__value*self.__UNITS[self.__unit]/self.__UNITS[target_unit] + else: + target_value = self.__value + + if inplace: + self.__value = target_value + self.__unit = target_unit + return self + else: + return Stress(value = target_value, unit = target_unit) diff --git a/gearpy/utils/relations.py b/gearpy/utils/relations.py index 9639c08..05693e6 100644 --- a/gearpy/utils/relations.py +++ b/gearpy/utils/relations.py @@ -1,4 +1,4 @@ -from gearpy.mechanical_object import MotorBase, GearBase, Flywheel +from gearpy.mechanical_object import MotorBase, GearBase, Flywheel, MatingMaster, MatingSlave from typing import Union @@ -26,7 +26,8 @@ def add_gear_mating(master: GearBase, slave: GearBase, efficiency: Union[float, - if ``slave`` is not an instance of ``GearBase``, - if ``efficiency`` is not a float or an integer. ValueError - If ``efficiency`` is not within ``0`` and ``1``. + - If ``efficiency`` is not within ``0`` and ``1``, + - if ``master`` and ``slave`` have different values for ``module``. """ if not isinstance(master, GearBase): raise TypeError("Parameter 'master' must be an instance of GearBase.") @@ -40,8 +41,15 @@ def add_gear_mating(master: GearBase, slave: GearBase, efficiency: Union[float, if efficiency > 1 or efficiency < 0: raise ValueError("Parameter 'efficiency' must be within 0 and 1.") + if master.module is not None and slave.module is not None: + if master.module != slave.module: + raise ValueError(f"Gears {master.name!r} and {slave.name!r} have different modules, " + f"so they cannot mate together.") + master.drives = slave + master.mating_role = MatingMaster slave.driven_by = master + slave.mating_role = MatingSlave slave.master_gear_ratio = slave.n_teeth/master.n_teeth slave.master_gear_efficiency = efficiency diff --git a/requirements.txt b/requirements/common.txt similarity index 100% rename from requirements.txt rename to requirements/common.txt diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000..34b0fd7 --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,2 @@ +twine==4.0.2 +wheel==0.41.2 diff --git a/docs/source/requirements.txt b/requirements/docs.txt similarity index 100% rename from docs/source/requirements.txt rename to requirements/docs.txt index ced87c5..62797c6 100644 --- a/docs/source/requirements.txt +++ b/requirements/docs.txt @@ -1,3 +1,3 @@ -sphinx==7.2.6 -m2r2==0.3.3.post2 furo==2023.9.10 +m2r2==0.3.3.post2 +sphinx==7.2.6 diff --git a/requirements/tests.txt b/requirements/tests.txt new file mode 100644 index 0000000..2e3627b --- /dev/null +++ b/requirements/tests.txt @@ -0,0 +1,5 @@ +coverage==7.3.2 +hypothesis==6.88.1 +pytest==7.4.3 +pytest-cov==4.1.0 +pytest-xdist==3.3.1 diff --git a/setup.py b/setup.py index b62e60f..9929332 100644 --- a/setup.py +++ b/setup.py @@ -6,6 +6,18 @@ with open('README.md', 'r') as f: long_description = f.read() +def read_requirements(path: str): + with open(path) as file: + lines = file.readlines() + + return [line.replace('==', ' >= ').replace('\n', '') for line in lines] + +basic_requirements = read_requirements(r'requirements/common.txt') +extras_dev = read_requirements(r'requirements/dev.txt') +extras_docs = read_requirements(r'requirements/docs.txt') +extras_tests = read_requirements(r'requirements/tests.txt') + + setup(name = 'gearpy', version = version, description = "Python package for mechanical transmission analysis", @@ -28,14 +40,8 @@ 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', 'Topic :: Scientific/Engineering'], - install_requires = ['matplotlib >= 3.8.0', - 'numpy >= 1.26.0', - 'pandas >= 2.1.1', - 'scipy >= 1.11.3'], - extras_require = {'dev': ['sphinx >= 7.2.6', - 'm2r2 >= 0.3.3.post2', - 'furo >= 2023.9.10', - 'tox >= 4.11.3', - 'hypothesis >= 6.87.1', - 'twine >= 4.0.2']}, + install_requires = basic_requirements, + extras_require = {'dev': extras_dev, + 'docs': extras_docs, + 'tests': extras_tests}, python_requires = '>=3.9, <3.12') diff --git a/tests/conftest.py b/tests/conftest.py index a6e1321..1702cb6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,12 @@ -from gearpy.mechanical_object import SpurGear, DCMotor, Flywheel +from gearpy.mechanical_object import SpurGear, DCMotor, Flywheel, MatingMaster, MatingSlave from gearpy.solver import Solver from gearpy.transmission import Transmission -from gearpy.units import AngularAcceleration, AngularPosition, AngularSpeed, InertiaMoment, Torque, Time, TimeInterval +from gearpy.units import AngularAcceleration, AngularPosition, AngularSpeed, Force, InertiaMoment, Length, Stress, \ + Surface, Time, TimeInterval, Torque from gearpy.utils import add_fixed_joint, add_gear_mating from hypothesis.strategies import composite, text, integers, floats, lists, sampled_from, shared, builds, characters import numpy as np +from random import shuffle basic_dc_motor = DCMotor(name = 'motor', @@ -14,7 +16,10 @@ basic_flywheel = Flywheel(name = 'flywheel', inertia_moment = InertiaMoment(1, 'kgm^2')) -basic_spur_gear = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) +basic_spur_gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) + +basic_spur_gear_2 = SpurGear(name = 'gear 2', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) transmission_dc_motor = DCMotor(name = 'motor', @@ -39,9 +44,10 @@ types_to_check = ['string', 2, 2.2, True, (0, 1), [0, 1], {0, 1}, {0: 1}, None, np.array([0]), - AngularPosition(1, 'rad'), AngularSpeed(1, 'rad/s'), AngularAcceleration(1, 'rad/s^2'), - InertiaMoment(1, 'kgm^2'), Torque(1, 'Nm'), Time(1, 'sec'), TimeInterval(1, 'sec'), - basic_dc_motor, basic_spur_gear, basic_flywheel] + AngularPosition(1, 'rad'), AngularSpeed(1, 'rad/s'), AngularAcceleration(1, 'rad/s^2'), Force(1, 'N'), + InertiaMoment(1, 'kgm^2'), Length(1, 'm'), Stress(1, 'Pa'), Surface(1, 'm^2'), Time(1, 'sec'), + TimeInterval(1, 'sec'), Torque(1, 'Nm'), basic_dc_motor, basic_spur_gear_1, basic_flywheel, + MatingMaster, MatingSlave, SpurGear] @composite @@ -51,14 +57,29 @@ def names(draw, strategy): @composite -def spur_gears(draw): +def simple_spur_gears(draw): name = draw(names(text(min_size = 1, alphabet = characters(categories = ['L', 'N'])))) - n_teeth = draw(integers(min_value = 1, max_value = 100)) + n_teeth = draw(integers(min_value = 10, max_value = 100)) inertia_moment_value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 10, max_value = 1000)) return SpurGear(name = name, n_teeth = n_teeth, inertia_moment = InertiaMoment(inertia_moment_value, 'kgmm^2')) +@composite +def structural_spur_gears(draw): + name = draw(names(text(min_size = 1, alphabet = characters(categories = ['L', 'N'])))) + n_teeth = draw(integers(min_value = 10, max_value = 100)) + inertia_moment_value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 10, max_value = 1000)) + module = Length(1, 'mm') + face_width_value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 10, max_value = 1000)) + face_width = Length(face_width_value, 'mm') + elastic_modulus_value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 10, max_value = 1000)) + elastic_modulus = Stress(elastic_modulus_value, 'GPa') + + return SpurGear(name = name, n_teeth = n_teeth, inertia_moment = InertiaMoment(inertia_moment_value, 'kgmm^2'), + module = module, face_width = face_width, elastic_modulus = elastic_modulus) + + @composite def dc_motors(draw): name = draw(names(text(min_size = 1, alphabet = characters(categories = ['L', 'N'])))) @@ -83,7 +104,10 @@ def flywheels(draw): @composite def transmissions(draw): motor = draw(dc_motors()) - gears = draw(lists(elements = spur_gears(), min_size = 1)) + gears = draw(lists(elements = structural_spur_gears(), min_size = 2)) + + if len(gears)%2 == 1: + gears = gears[:-1] add_fixed_joint(master = motor, slave = gears[0]) @@ -99,7 +123,29 @@ def transmissions(draw): @composite def solved_transmissions(draw): motor = draw(dc_motors()) - gears = draw(lists(elements = spur_gears(), min_size = 1, max_size = 4)) + gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), module = Length(1, 'mm')) + gear_2 = SpurGear(name = 'gear 2', n_teeth = 20, inertia_moment = InertiaMoment(1, 'kgm^2'), module = Length(1, 'mm')) + simple_gears = draw(lists(elements = simple_spur_gears(), min_size = 2, max_size = 4)) + structural_gears = draw(lists(elements = structural_spur_gears(), min_size = 2, max_size = 4)) + + + if len(simple_gears)%2 == 1: + simple_gears = simple_gears[:-1] + + if len(structural_gears)%2 == 1: + structural_gears = structural_gears[:-1] + + simple_gears = list(zip(*(iter(simple_gears),)*2)) + structural_gears = list(zip(*(iter(structural_gears),)*2)) + + gears = [] + gears.extend(simple_gears) + gears.extend(structural_gears) + shuffle(gears) + + gears = [element for pair in gears for element in pair] + gears.append(gear_1) + gears.append(gear_2) time_discretization_value = draw(floats(min_value = 1e-3, max_value = 1, allow_nan = False, allow_infinity = False)) time_discretization = TimeInterval(value = time_discretization_value, unit = 'sec') diff --git a/tests/test_mechanical_object/test_dc_motor/test_dc_motor.py b/tests/test_mechanical_object/test_dc_motor/test_dc_motor.py index 163f3f3..b8dc5e9 100644 --- a/tests/test_mechanical_object/test_dc_motor/test_dc_motor.py +++ b/tests/test_mechanical_object/test_dc_motor/test_dc_motor.py @@ -1,5 +1,5 @@ from gearpy.mechanical_object import DCMotor, SpurGear -from gearpy.units import Torque +from gearpy.units import Length, Torque from hypothesis import given, settings from hypothesis.strategies import text from pytest import mark, raises @@ -47,7 +47,7 @@ class TestDCMotorDrives: @mark.genuine def test_property(self): - gear = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = basic_inertia_moment) + gear = SpurGear(name = 'gear', n_teeth = 10, module = Length(1, 'mm'), inertia_moment = basic_inertia_moment) basic_dc_motor.drives = gear assert basic_dc_motor.drives == gear diff --git a/tests/test_mechanical_object/test_flywheel/test_flywheel.py b/tests/test_mechanical_object/test_flywheel/test_flywheel.py index a07d228..cef707b 100644 --- a/tests/test_mechanical_object/test_flywheel/test_flywheel.py +++ b/tests/test_mechanical_object/test_flywheel/test_flywheel.py @@ -1,5 +1,5 @@ from gearpy.mechanical_object import DCMotor, SpurGear, Flywheel -from gearpy.units import AngularSpeed, InertiaMoment, Torque +from gearpy.units import AngularSpeed, InertiaMoment, Length, Torque from hypothesis import given, settings from hypothesis.strategies import text, floats from pytest import mark, raises @@ -43,7 +43,7 @@ def test_property(self): motor = DCMotor(name = 'motor', inertia_moment = InertiaMoment(1, 'kgm^2'), no_load_speed = AngularSpeed(1000, 'rpm'), maximum_torque = Torque(1, 'Nm')) flywheel = Flywheel(name = 'flywheel', inertia_moment = InertiaMoment(1, 'kgm^2')) - gear = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) + gear = SpurGear(name = 'gear', n_teeth = 10, module = Length(1, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) for master in [motor, flywheel, gear]: basic_flywheel.driven_by = master @@ -63,7 +63,7 @@ class TestFlywheelDrives: @mark.genuine def test_property(self): - gear = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) + gear = SpurGear(name = 'gear', n_teeth = 10, module = Length(1, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) flywheel = Flywheel(name = 'flywheel', inertia_moment = InertiaMoment(1, 'kgm^2')) for slave in [gear, flywheel]: diff --git a/tests/test_mechanical_object/test_rotating_object/conftest.py b/tests/test_mechanical_object/test_rotating_object/conftest.py index 5c870d5..392ab1f 100644 --- a/tests/test_mechanical_object/test_rotating_object/conftest.py +++ b/tests/test_mechanical_object/test_rotating_object/conftest.py @@ -1,9 +1,9 @@ from gearpy.units import AngularAcceleration, AngularPosition, AngularSpeed, Torque -from tests.conftest import types_to_check, basic_spur_gear, basic_dc_motor, basic_flywheel +from tests.conftest import types_to_check, basic_spur_gear_1, basic_spur_gear_2, basic_dc_motor, basic_flywheel from pytest import fixture -basic_rotating_objects = [basic_dc_motor, basic_spur_gear, basic_flywheel] +basic_rotating_objects = [basic_dc_motor, basic_spur_gear_1, basic_spur_gear_2, basic_flywheel] @fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, AngularPosition)]) diff --git a/tests/test_mechanical_object/test_rotating_object/test_rotating_object.py b/tests/test_mechanical_object/test_rotating_object/test_rotating_object.py index af76182..9d7e82a 100644 --- a/tests/test_mechanical_object/test_rotating_object/test_rotating_object.py +++ b/tests/test_mechanical_object/test_rotating_object/test_rotating_object.py @@ -1,3 +1,4 @@ +from gearpy.mechanical_object import GearBase from hypothesis import given, settings from pytest import mark, raises from tests.test_mechanical_object.test_rotating_object.conftest import basic_rotating_objects @@ -5,6 +6,8 @@ from tests.test_units.test_angular_position.conftest import angular_positions from tests.test_units.test_angular_speed.conftest import angular_speeds from tests.test_units.test_torque.conftest import torques +from tests.test_units.test_force.conftest import forces +from tests.test_units.test_stress.conftest import stresses @mark.rotating_object @@ -142,9 +145,18 @@ def test_property(self): for rotating_object in basic_rotating_objects: time_variables = rotating_object.time_variables + time_variables_list = ['angular position', 'angular speed', 'angular acceleration', 'torque', + 'driving torque', 'load torque'] + if isinstance(rotating_object, GearBase): + if rotating_object.tangential_force_is_computable: + time_variables_list.append('tangential force') + if rotating_object.bending_stress_is_computable: + time_variables_list.append('bending stress') + if rotating_object.contact_stress_is_computable: + time_variables_list.append('contact stress') + assert isinstance(time_variables, dict) - assert ['angular position', 'angular speed', 'angular acceleration', - 'torque', 'driving torque', 'load torque'] == list(time_variables.keys()) + assert time_variables_list == list(time_variables.keys()) assert all([value == [] for value in time_variables.values()]) @@ -158,8 +170,12 @@ class TestRotatingObjectUpdateTimeVariables: angular_acceleration = angular_accelerations(), torque = torques(), driving_torque = torques(), - load_torque = torques()) - def test_method(self, angular_position, angular_speed, angular_acceleration, torque, driving_torque, load_torque): + load_torque = torques(), + tangential_force = forces(), + bending_stress = stresses(), + contact_stress = stresses()) + def test_method(self, angular_position, angular_speed, angular_acceleration, torque, driving_torque, load_torque, + tangential_force, bending_stress, contact_stress): for rotating_object in basic_rotating_objects: rotating_object.angular_position = angular_position rotating_object.angular_speed = angular_speed @@ -167,16 +183,39 @@ def test_method(self, angular_position, angular_speed, angular_acceleration, tor rotating_object.torque = torque rotating_object.driving_torque = driving_torque rotating_object.load_torque = load_torque + if isinstance(rotating_object, GearBase): + if rotating_object.tangential_force_is_computable: + rotating_object.tangential_force = tangential_force + if rotating_object.bending_stress_is_computable: + rotating_object.bending_stress = bending_stress + if rotating_object.contact_stress_is_computable: + rotating_object.contact_stress = contact_stress rotating_object.update_time_variables() time_variables = rotating_object.time_variables + time_variables_list = ['angular position', 'angular speed', 'angular acceleration', 'torque', + 'driving torque', 'load torque'] + if isinstance(rotating_object, GearBase): + if rotating_object.tangential_force_is_computable: + time_variables_list.append('tangential force') + if rotating_object.bending_stress_is_computable: + time_variables_list.append('bending stress') + if rotating_object.contact_stress_is_computable: + time_variables_list.append('contact stress') + assert isinstance(time_variables, dict) - assert ['angular position', 'angular speed', 'angular acceleration', - 'torque', 'driving torque', 'load torque'] == list(time_variables.keys()) + assert time_variables_list == list(time_variables.keys()) assert time_variables['angular position'][-1] == angular_position assert time_variables['angular speed'][-1] == angular_speed assert time_variables['angular acceleration'][-1] == angular_acceleration assert time_variables['torque'][-1] == torque assert time_variables['driving torque'][-1] == driving_torque assert time_variables['load torque'][-1] == load_torque + if isinstance(rotating_object, GearBase): + if rotating_object.tangential_force_is_computable: + assert time_variables['tangential force'][-1] == tangential_force + if rotating_object.bending_stress_is_computable: + assert time_variables['bending stress'][-1] == bending_stress + if rotating_object.contact_stress_is_computable: + assert time_variables['contact stress'][-1] == contact_stress diff --git a/tests/test_mechanical_object/test_spur_gear/conftest.py b/tests/test_mechanical_object/test_spur_gear/conftest.py index b71a2c1..af5542d 100644 --- a/tests/test_mechanical_object/test_spur_gear/conftest.py +++ b/tests/test_mechanical_object/test_spur_gear/conftest.py @@ -1,29 +1,58 @@ -from gearpy.mechanical_object import RotatingObject -from gearpy.units import InertiaMoment +from gearpy.mechanical_object import RotatingObject, Role, SpurGear +from gearpy.units import Force, InertiaMoment, Length, Stress +from gearpy.utils import add_gear_mating from pytest import fixture from tests.conftest import types_to_check from typing import Callable -spur_gear_init_type_error_1 = [{'name': type_to_check, 'n_teeth': 10, 'inertia_moment': InertiaMoment(1, 'kgm^2')} +spur_gear_init_type_error_1 = [{'name': type_to_check, 'n_teeth': 10, 'inertia_moment': InertiaMoment(1, 'kgm^2'), + 'module': Length(1, 'mm'), 'face_width': Length(5, 'mm'), + 'elastic_modulus': Stress(100, 'GPa')} for type_to_check in types_to_check if not isinstance(type_to_check, str)] -spur_gear_init_type_error_2 = [{'name': 'gear', 'n_teeth': type_to_check, 'inertia_moment': InertiaMoment(1, 'kgm^2')} +spur_gear_init_type_error_2 = [{'name': 'gear', 'n_teeth': type_to_check, 'inertia_moment': InertiaMoment(1, 'kgm^2'), + 'module': Length(1, 'mm'), 'face_width': Length(5, 'mm'), + 'elastic_modulus': Stress(100, 'GPa')} for type_to_check in types_to_check if not isinstance(type_to_check, int) and not isinstance(type_to_check, bool)] -spur_gear_init_type_error_3 = [{'name': 'gear', 'n_teeth': 10, 'inertia_moment': type_to_check} +spur_gear_init_type_error_3 = [{'name': 'gear', 'n_teeth': 10, 'inertia_moment': type_to_check, + 'module': Length(1, 'mm'), 'face_width': Length(5, 'mm'), + 'elastic_modulus': Stress(100, 'GPa')} for type_to_check in types_to_check if not isinstance(type_to_check, InertiaMoment)] +spur_gear_init_type_error_4 = [{'name': 'gear', 'n_teeth': 10, 'inertia_moment': InertiaMoment(1, 'kgm^2'), + 'module': type_to_check, 'face_width': Length(5, 'mm'), + 'elastic_modulus': Stress(100, 'GPa')} + for type_to_check in types_to_check if not isinstance(type_to_check, Length) + and type_to_check is not None] + +spur_gear_init_type_error_5 = [{'name': 'gear', 'n_teeth': 10, 'inertia_moment': InertiaMoment(1, 'kgm^2'), + 'module': Length(1, 'mm'), 'face_width': type_to_check, + 'elastic_modulus': Stress(100, 'GPa')} + for type_to_check in types_to_check if not isinstance(type_to_check, Length) + and type_to_check is not None] + +spur_gear_init_type_error_6 = [{'name': 'gear', 'n_teeth': 10, 'inertia_moment': InertiaMoment(1, 'kgm^2'), + 'module': Length(1, 'mm'), 'face_width': Length(5, 'mm'), + 'elastic_modulus': type_to_check} + for type_to_check in types_to_check if not isinstance(type_to_check, Stress) + and type_to_check is not None] + @fixture(params = [*spur_gear_init_type_error_1, *spur_gear_init_type_error_2, - *spur_gear_init_type_error_3]) + *spur_gear_init_type_error_3, + *spur_gear_init_type_error_4, + *spur_gear_init_type_error_5, + *spur_gear_init_type_error_6]) def spur_gear_init_type_error(request): return request.param -@fixture(params = [{'name': '', 'n_teeth': 1, 'inertia_moment': InertiaMoment(1, 'kgm^2')}, - {'name': 'gear', 'n_teeth': -1, 'inertia_moment': InertiaMoment(1, 'kgm^2')}]) +@fixture(params = [{'name': '', 'n_teeth': 10, 'inertia_moment': InertiaMoment(1, 'kgm^2')}, + {'name': 'gear', 'n_teeth': -1, 'inertia_moment': InertiaMoment(1, 'kgm^2')}, + {'name': 'gear', 'n_teeth': 10, 'inertia_moment': InertiaMoment(1, 'kgm^2'), 'elastic_modulus': Stress(-10, 'GPa')}]) def spur_gear_init_value_error(request): return request.param @@ -37,7 +66,6 @@ def spur_gear_driven_by_type_error(request): def spur_gear_drives_type_error(request): return request.param - @fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float)]) def spur_gear_master_gear_ratio_type_error(request): return request.param @@ -54,6 +82,62 @@ def spur_gear_master_gear_efficiency_value_error(request): return request.param +@fixture(params = [type_to_check for type_to_check in types_to_check if not hasattr(type_to_check, '__name__') or not issubclass(type_to_check, Role)]) +def spur_gear_mating_role_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def spur_gear_tangential_force_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def spur_gear_bending_stress_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def spur_gear_contact_stress_type_error(request): + return request.param + + +spur_gear_compute_contact_stress_value_error_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) + +spur_gear_compute_contact_stress_value_error_2 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) +spur_gear_compute_contact_stress_value_error_2_mate = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) +add_gear_mating(master = spur_gear_compute_contact_stress_value_error_2, slave = spur_gear_compute_contact_stress_value_error_2_mate, efficiency = 1) + +spur_gear_compute_contact_stress_value_error_3 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) +spur_gear_compute_contact_stress_value_error_3_mate = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm')) +add_gear_mating(master = spur_gear_compute_contact_stress_value_error_3, slave = spur_gear_compute_contact_stress_value_error_3_mate, efficiency = 1) + +spur_gear_compute_contact_stress_value_error_4 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) +spur_gear_compute_contact_stress_value_error_4_mate = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) +add_gear_mating(master = spur_gear_compute_contact_stress_value_error_4_mate, slave = spur_gear_compute_contact_stress_value_error_4, efficiency = 1) + +spur_gear_compute_contact_stress_value_error_5 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) +spur_gear_compute_contact_stress_value_error_5_mate = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm')) +add_gear_mating(master = spur_gear_compute_contact_stress_value_error_5_mate, slave = spur_gear_compute_contact_stress_value_error_5, efficiency = 1) + +@fixture(params = [spur_gear_compute_contact_stress_value_error_1, + spur_gear_compute_contact_stress_value_error_2, + spur_gear_compute_contact_stress_value_error_3, + spur_gear_compute_contact_stress_value_error_4, + spur_gear_compute_contact_stress_value_error_5]) +def spur_gear_compute_contact_stress_value_error(request): + return request.param + + @fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Callable)]) def spur_gear_external_torque_type_error(request): return request.param diff --git a/tests/test_mechanical_object/test_spur_gear/test_spur_gear.py b/tests/test_mechanical_object/test_spur_gear/test_spur_gear.py index 4071f2d..b11c2ea 100644 --- a/tests/test_mechanical_object/test_spur_gear/test_spur_gear.py +++ b/tests/test_mechanical_object/test_spur_gear/test_spur_gear.py @@ -1,9 +1,10 @@ -from gearpy.mechanical_object import DCMotor, SpurGear -from gearpy.units import AngularSpeed, InertiaMoment, Torque +from gearpy.mechanical_object import DCMotor, SpurGear, MatingMaster, MatingSlave +from gearpy.units import AngularSpeed, Force, InertiaMoment, Length, Stress, Torque +from gearpy.utils import add_gear_mating from hypothesis import given, settings from hypothesis.strategies import text, floats, integers, functions from pytest import mark, raises -from tests.conftest import basic_spur_gear +from tests.conftest import basic_spur_gear_1, basic_spur_gear_2 from tests.test_units.test_inertia_moment.conftest import inertia_moments @@ -13,15 +14,26 @@ class TestSpurGearInit: @mark.genuine @given(name = text(min_size = 1), - n_teeth = integers(min_value = 1), - inertia_moment = inertia_moments()) + n_teeth = integers(min_value = 10, max_value = 1000), + inertia_moment = inertia_moments(), + module_value = floats(allow_nan = False, allow_infinity = False, min_value = 0.1, max_value = 10), + face_width_value = floats(allow_nan = False, allow_infinity = False, min_value = 0.1, max_value = 100), + elastic_modulus_value = floats(allow_nan = False, allow_infinity = False, min_value = 0.1, max_value = 10)) @settings(max_examples = 100) - def test_method(self, name, n_teeth, inertia_moment): - gear = SpurGear(name = name, n_teeth = n_teeth, inertia_moment = inertia_moment) + def test_method(self, name, n_teeth, inertia_moment, module_value, face_width_value, elastic_modulus_value): + module = Length(module_value, 'mm') + face_width = Length(face_width_value, 'mm') + elastic_modulus = Stress(elastic_modulus_value, 'GPa') + gear = SpurGear(name = name, n_teeth = n_teeth, inertia_moment = inertia_moment, + module = module, face_width = face_width, elastic_modulus = elastic_modulus) assert gear.name == name assert gear.n_teeth == n_teeth assert gear.inertia_moment == inertia_moment + assert gear.module == module + assert gear.reference_diameter == n_teeth*module + assert gear.face_width == face_width + assert gear.elastic_modulus == elastic_modulus @mark.error @@ -47,15 +59,15 @@ def test_property(self): gear = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) for master in [motor, gear]: - basic_spur_gear.driven_by = master + basic_spur_gear_1.driven_by = master - assert basic_spur_gear.driven_by == master + assert basic_spur_gear_1.driven_by == master @mark.error def test_raises_type_error(self, spur_gear_driven_by_type_error): with raises(TypeError): - basic_spur_gear.driven_by = spur_gear_driven_by_type_error + basic_spur_gear_1.driven_by = spur_gear_driven_by_type_error @mark.spur_gear @@ -65,15 +77,15 @@ class TestSpurGearDrives: @mark.genuine def test_property(self): gear = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) - basic_spur_gear.drives = gear + basic_spur_gear_1.drives = gear - assert basic_spur_gear.drives == gear + assert basic_spur_gear_1.drives == gear @mark.error def test_raises_type_error(self, spur_gear_drives_type_error): with raises(TypeError): - basic_spur_gear.drives = spur_gear_drives_type_error + basic_spur_gear_1.drives = spur_gear_drives_type_error @mark.spur_gear @@ -84,21 +96,21 @@ class TestSpurGearMasterGearRatio: @given(master_gear_ratio = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = True)) @settings(max_examples = 100) def test_property(self, master_gear_ratio): - basic_spur_gear.master_gear_ratio = master_gear_ratio + basic_spur_gear_1.master_gear_ratio = master_gear_ratio - assert basic_spur_gear.master_gear_ratio == master_gear_ratio + assert basic_spur_gear_1.master_gear_ratio == master_gear_ratio @mark.error def test_raises_type_error(self, spur_gear_master_gear_ratio_type_error): with raises(TypeError): - basic_spur_gear.master_gear_ratio = spur_gear_master_gear_ratio_type_error + basic_spur_gear_1.master_gear_ratio = spur_gear_master_gear_ratio_type_error @mark.error def test_raises_value_error(self): with raises(ValueError): - basic_spur_gear.master_gear_ratio = -1.0 + basic_spur_gear_1.master_gear_ratio = -1.0 @mark.spur_gear @@ -110,21 +122,204 @@ class TestSpurGearMasterGearEfficiency: min_value = 0, exclude_min = False, max_value = 1, exclude_max = False)) @settings(max_examples = 100) def test_property(self, master_gear_efficiency): - basic_spur_gear.master_gear_efficiency = master_gear_efficiency + basic_spur_gear_1.master_gear_efficiency = master_gear_efficiency - assert basic_spur_gear.master_gear_efficiency == master_gear_efficiency + assert basic_spur_gear_1.master_gear_efficiency == master_gear_efficiency @mark.error def test_raises_type_error(self, spur_gear_master_gear_efficiency_type_error): with raises(TypeError): - basic_spur_gear.master_gear_efficiency = spur_gear_master_gear_efficiency_type_error + basic_spur_gear_1.master_gear_efficiency = spur_gear_master_gear_efficiency_type_error @mark.error def test_raises_value_error(self, spur_gear_master_gear_efficiency_value_error): with raises(ValueError): - basic_spur_gear.master_gear_efficiency = spur_gear_master_gear_efficiency_value_error + basic_spur_gear_1.master_gear_efficiency = spur_gear_master_gear_efficiency_value_error + + +@mark.spur_gear +class TestSpurGearMatingRole: + + + @mark.genuine + def test_property(self): + gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'gm^2')) + gear_2 = SpurGear(name = 'gear 2', n_teeth = 10, inertia_moment = InertiaMoment(1, 'gm^2')) + add_gear_mating(master = gear_1, slave = gear_2, efficiency = 1) + + assert gear_1.mating_role == MatingMaster + assert gear_2.mating_role == MatingSlave + + + @mark.error + def test_raises_type_error(self, spur_gear_mating_role_type_error): + with raises(TypeError): + basic_spur_gear_1.mating_role = spur_gear_mating_role_type_error + + +@mark.spur_gear +class TestSpurGearTangentialForce: + + + @mark.genuine + def test_property(self): + tangential_force = Force(1, 'N') + basic_spur_gear_1.tangential_force = tangential_force + + assert basic_spur_gear_1.tangential_force == tangential_force + + + @mark.error + def test_raises_type_error(self, spur_gear_tangential_force_type_error): + with raises(TypeError): + basic_spur_gear_1.tangential_force = spur_gear_tangential_force_type_error + + +@mark.spur_gear +class TestSpurGearComputeTangentialForce: + + + @mark.genuine + def test_method(self): + gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), module = Length(1, 'mm')) + gear_2 = SpurGear(name = 'gear 2', n_teeth = 20, inertia_moment = InertiaMoment(1, 'kgm^2'), module = Length(1, 'mm')) + + add_gear_mating(master = gear_1, slave = gear_2, efficiency = 0.9) + gear_1.driving_torque = Torque(1, 'Nm') + gear_2.driving_torque = Torque(1, 'Nm') + gear_1.load_torque = Torque(1, 'Nm') + gear_2.load_torque= Torque(1, 'Nm') + gear_1.compute_tangential_force() + gear_2.compute_tangential_force() + + assert gear_1.tangential_force is not None + assert gear_2.tangential_force is not None + assert isinstance(gear_1.tangential_force, Force) + assert isinstance(gear_2.tangential_force, Force) + + + @mark.error + def test_raises_value_error(self): + gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), module = Length(1, 'mm')) + with raises(ValueError): + gear_1.compute_tangential_force() + + +@mark.spur_gear +class TestSpurGearTangentialForceIsComputable: + + + @mark.genuine + def test_property(self): + for gear in [basic_spur_gear_1, basic_spur_gear_2]: + if gear.module is None: + assert not gear.tangential_force_is_computable + else: + assert gear.tangential_force_is_computable + + +@mark.spur_gear +class TestSpurGearBendingStress: + + @mark.genuine + def test_property(self): + bending_stress = Stress(1, 'MPa') + basic_spur_gear_1.bending_stress = bending_stress + + assert basic_spur_gear_1.bending_stress == bending_stress + + @mark.error + def test_raises_type_error(self, spur_gear_bending_stress_type_error): + with raises(TypeError): + basic_spur_gear_1.bending_stress = spur_gear_bending_stress_type_error + + +@mark.spur_gear +class TestSpurGearComputeBendingStress: + + @mark.genuine + def test_method(self): + gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm')) + gear_2 = SpurGear(name = 'gear 2', n_teeth = 20, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm')) + add_gear_mating(master = gear_1, slave = gear_2, efficiency = 0.9) + gear_2.driving_torque = Torque(1, 'Nm') + gear_2.compute_tangential_force() + gear_2.compute_bending_stress() + + assert gear_2.bending_stress is not None + assert isinstance(gear_2.bending_stress, Stress) + + +@mark.spur_gear +class TestSpurGearBendingStressIsComputable: + + @mark.genuine + def test_property(self): + for gear in [basic_spur_gear_1, basic_spur_gear_2]: + if (gear.module is None) or (gear.face_width is None): + assert not gear.bending_stress_is_computable + else: + assert gear.bending_stress_is_computable + + +@mark.spur_gear +class TestSpurGearContactStress: + + @mark.genuine + def test_property(self): + contact_stress = Stress(1, 'MPa') + basic_spur_gear_1.contact_stress = contact_stress + + assert basic_spur_gear_1.contact_stress == contact_stress + + @mark.error + def test_raises_type_error(self, spur_gear_contact_stress_type_error): + with raises(TypeError): + basic_spur_gear_1.contact_stress = spur_gear_contact_stress_type_error + + +@mark.spur_gear +class TestSpurGearComputeContactStress: + + @mark.genuine + def test_method(self): + gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(100, 'GPa')) + gear_2 = SpurGear(name = 'gear 2', n_teeth = 20, inertia_moment = InertiaMoment(1, 'kgm^2'), + module = Length(1, 'mm'), face_width = Length(5, 'mm'), elastic_modulus = Stress(200, 'GPa')) + add_gear_mating(master = gear_1, slave = gear_2, efficiency = 0.9) + gear_1.master_gear_ratio = 1.0 + + for gear in [gear_1, gear_2]: + gear.driving_torque = Torque(1, 'Nm') + gear.load_torque = Torque(1, 'Nm') + gear.compute_tangential_force() + gear.compute_contact_stress() + + assert gear.contact_stress is not None + assert isinstance(gear.contact_stress, Stress) + + + @mark.error + def test_raises_value_error(self, spur_gear_compute_contact_stress_value_error): + with raises(ValueError): + spur_gear_compute_contact_stress_value_error.compute_contact_stress() + + +@mark.spur_gear +class TestSpurGearContactStressIsComputable: + + @mark.genuine + def test_property(self): + for gear in [basic_spur_gear_1, basic_spur_gear_2]: + if (gear.module is None) or (gear.face_width is None) or (gear.elastic_modulus is None): + assert not gear.contact_stress_is_computable + else: + assert gear.contact_stress_is_computable @mark.spur_gear @@ -135,12 +330,12 @@ class TestSpurGearExternalTorque: @given(external_torque = functions()) @settings(max_examples = 100) def test_property(self, external_torque): - basic_spur_gear.external_torque = external_torque + basic_spur_gear_1.external_torque = external_torque - assert basic_spur_gear.external_torque == external_torque + assert basic_spur_gear_1.external_torque == external_torque @mark.error def test_raises_type_error(self, spur_gear_external_torque_type_error): with raises(TypeError): - basic_spur_gear.external_torque = spur_gear_external_torque_type_error + basic_spur_gear_1.external_torque = spur_gear_external_torque_type_error diff --git a/tests/test_solver/conftest.py b/tests/test_solver/conftest.py index ad2d415..9db0216 100644 --- a/tests/test_solver/conftest.py +++ b/tests/test_solver/conftest.py @@ -1,14 +1,14 @@ from gearpy.mechanical_object import RotatingObject, MotorBase, SpurGear, DCMotor from gearpy.transmission import Transmission -from gearpy.units import AngularSpeed, InertiaMoment, Torque, TimeInterval +from gearpy.units import AngularSpeed, InertiaMoment, Length, Torque, TimeInterval from gearpy.utils import add_fixed_joint from pytest import fixture -from tests.conftest import types_to_check, basic_spur_gear, basic_dc_motor +from tests.conftest import types_to_check, basic_spur_gear_1, basic_dc_motor motor_transmission_solver_init_type_error = DCMotor(name = 'name', inertia_moment = InertiaMoment(1, 'kgm^2'), no_load_speed = AngularSpeed(1000, 'rpm'), maximum_torque = Torque(1, 'Nm')) -gear_transmission_solver_init_type_error = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) +gear_transmission_solver_init_type_error = SpurGear(name = 'gear', n_teeth = 10, module = Length(1, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) add_fixed_joint(master = motor_transmission_solver_init_type_error, slave = gear_transmission_solver_init_type_error) transmission_solver_init_type_error = Transmission(motor = motor_transmission_solver_init_type_error) @@ -38,7 +38,7 @@ def chain(self, chain): if not isinstance(type_to_check, Transmission)] solver_init_type_error_4 = [{'time_discretization': TimeInterval(1, 'sec'), 'simulation_time': TimeInterval(5, 'sec'), - 'transmission': TransmissionFake([type_to_check, basic_spur_gear])} + 'transmission': TransmissionFake([type_to_check, basic_spur_gear_1])} for type_to_check in types_to_check if not isinstance(type_to_check, MotorBase)] solver_init_type_error_5 = [{'time_discretization': TimeInterval(1, 'sec'), 'simulation_time': TimeInterval(5, 'sec'), @@ -55,7 +55,7 @@ def solver_init_type_error(request): motor_transmission_solver_init_value_error = DCMotor(name = 'name', inertia_moment = InertiaMoment(1, 'kgm^2'), no_load_speed = AngularSpeed(1000, 'rpm'), maximum_torque = Torque(1, 'Nm')) -gear_transmission_solver_init_value_error = SpurGear(name = 'gear', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) +gear_transmission_solver_init_value_error = SpurGear(name = 'gear', n_teeth = 10, module = Length(1, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) add_fixed_joint(master = motor_transmission_solver_init_value_error, slave = gear_transmission_solver_init_value_error) transmission_solver_init_value_error = Transmission(motor = motor_transmission_solver_init_value_error) diff --git a/tests/test_solver/test_solver.py b/tests/test_solver/test_solver.py index 4ebea7f..05bd5c4 100644 --- a/tests/test_solver/test_solver.py +++ b/tests/test_solver/test_solver.py @@ -7,6 +7,7 @@ from tests.conftest import transmissions, time_intervals from tests.test_units.test_angular_position.conftest import angular_positions from tests.test_units.test_angular_speed.conftest import angular_speeds +import warnings @mark.solver @@ -53,6 +54,8 @@ class TestSolverRun: initial_angular_speed = angular_speeds()) @settings(max_examples = 100, deadline = None) def test_method(self, time_discretization, simulation_steps, transmission, initial_angular_position, initial_angular_speed): + warnings.filterwarnings('ignore', category = RuntimeWarning) + transmission.chain[-1].angular_position = initial_angular_position transmission.chain[-1].angular_speed = initial_angular_speed transmission.chain[-1].external_torque = lambda time, angular_position, angular_speed: Torque(0.001, 'Nm') diff --git a/tests/test_transmission/conftest.py b/tests/test_transmission/conftest.py index d5adf6e..d19cd86 100644 --- a/tests/test_transmission/conftest.py +++ b/tests/test_transmission/conftest.py @@ -17,52 +17,70 @@ def transmission_update_time_type_error(request): transmission_snapshot_type_error_1 = [{'target_time': type_to_check, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', 'load_torque_unit': 'Nm', - 'print_data': False} for type_to_check in types_to_check - if not isinstance(type_to_check, Time)] + 'force_unit': 'N', 'stress_unit': 'MPa', 'print_data': False} + for type_to_check in types_to_check if not isinstance(type_to_check, Time)] transmission_snapshot_type_error_2 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': type_to_check, 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', 'load_torque_unit': 'Nm', - 'print_data': False} for type_to_check in types_to_check - if not isinstance(type_to_check, str)] + 'force_unit': 'N', 'stress_unit': 'MPa', 'print_data': False} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_snapshot_type_error_3 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', 'angular_speed_unit': type_to_check, 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', 'load_torque_unit': 'Nm', - 'print_data': False} for type_to_check in types_to_check - if not isinstance(type_to_check, str)] + 'force_unit': 'N', 'stress_unit': 'MPa', 'print_data': False} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_snapshot_type_error_4 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': type_to_check, 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', 'load_torque_unit': 'Nm', - 'print_data': False} for type_to_check in types_to_check - if not isinstance(type_to_check, str)] + 'force_unit': 'N', 'stress_unit': 'MPa', 'print_data': False} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_snapshot_type_error_5 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': type_to_check, 'driving_torque_unit': 'Nm', - 'load_torque_unit': 'Nm', 'print_data': False} + 'load_torque_unit': 'Nm', 'force_unit': 'N', 'stress_unit': 'MPa', + 'print_data': False} for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_snapshot_type_error_6 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'driving_torque_unit': type_to_check, - 'load_torque_unit': 'Nm', 'print_data': False} + 'load_torque_unit': 'Nm', 'force_unit': 'N', 'stress_unit': 'MPa', + 'print_data': False} for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_snapshot_type_error_7 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', - 'load_torque_unit': type_to_check, 'print_data': False} + 'load_torque_unit': type_to_check, 'force_unit': 'N', 'stress_unit': 'MPa', + 'print_data': False} for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_snapshot_type_error_8 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', + 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', + 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', + 'load_torque_unit': 'Nm', 'force_unit': type_to_check, 'stress_unit': 'MPa', + 'print_data': False} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + +transmission_snapshot_type_error_9 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', + 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', + 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', + 'load_torque_unit': 'Nm', 'force_unit': 'N', 'stress_unit': type_to_check, + 'print_data': False} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + +transmission_snapshot_type_error_10 = [{'target_time': Time(1, 'sec'), 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'driving_torque_unit': 'Nm', 'load_torque_unit': 'Nm', - 'print_data': type_to_check} for type_to_check in types_to_check - if not isinstance(type_to_check, int) and not isinstance(type_to_check, bool)] + 'force_unit': 'N', 'stress_unit': 'MPa', 'print_data': type_to_check} + for type_to_check in types_to_check + if not isinstance(type_to_check, int) and not isinstance(type_to_check, bool)] -transmission_snapshot_type_error_9 = [{}] +transmission_snapshot_type_error_11 = [{}] @fixture(params = [*transmission_snapshot_type_error_1, *transmission_snapshot_type_error_2, @@ -72,7 +90,9 @@ def transmission_update_time_type_error(request): *transmission_snapshot_type_error_6, *transmission_snapshot_type_error_7, *transmission_snapshot_type_error_8, - *transmission_snapshot_type_error_9]) + *transmission_snapshot_type_error_9, + *transmission_snapshot_type_error_10, + *transmission_snapshot_type_error_11]) def transmission_snapshot_type_error(request): return request.param @@ -82,72 +102,86 @@ def transmission_snapshot_type_error(request): transmission_plot_type_error_1 = [{'elements': type_to_check, 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'time_unit': 'sec', - 'figsize': None} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check if not isinstance(type_to_check, list) and type_to_check is not None] transmission_plot_type_error_2 = [{'elements': [type_to_check], 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'time_unit': 'sec', - 'figsize': None} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check if not isinstance(type_to_check, RotatingObject) and not isinstance(type_to_check, str)] transmission_plot_type_error_3 = [{'elements': elements, 'variables': type_to_check, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'time_unit': 'sec', - 'figsize': None} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check if not isinstance(type_to_check, list) and type_to_check is not None] transmission_plot_type_error_4 = [{'elements': elements, 'variables': [type_to_check], 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'time_unit': 'sec', - 'figsize': None} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_plot_type_error_5 = [{'elements': elements, 'variables': variables, 'angular_position_unit': type_to_check, 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'time_unit': 'sec', - 'figsize': None} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_plot_type_error_6 = [{'elements': elements, 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': type_to_check, - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'time_unit': 'sec', - 'figsize': None} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_plot_type_error_7 = [{'elements': elements, 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': type_to_check, 'torque_unit': 'Nm', 'time_unit': 'sec', - 'figsize': None} + 'angular_acceleration_unit': type_to_check, 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_plot_type_error_8 = [{'elements': elements, 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': type_to_check, - 'time_unit': 'sec', 'figsize': None} for type_to_check in types_to_check - if not isinstance(type_to_check, str)] + 'force_unit': 'N', 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': None} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_plot_type_error_9 = [{'elements': elements, 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', - 'time_unit': type_to_check, 'figsize': None} for type_to_check in types_to_check - if not isinstance(type_to_check, str)] + 'force_unit': type_to_check, 'stress_unit': 'MPa', 'time_unit': 'sec', + 'figsize': None} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] transmission_plot_type_error_10 = [{'elements': elements, 'variables': variables, + 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', + 'force_unit': 'N', 'stress_unit': type_to_check, 'time_unit': 'sec', 'figsize': None} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + + +transmission_plot_type_error_11 = [{'elements': elements, 'variables': variables, + 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': type_to_check, 'figsize': None} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + +transmission_plot_type_error_12 = [{'elements': elements, 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', - 'time_unit': type_to_check, 'figsize': type_to_check} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': type_to_check} for type_to_check in types_to_check if not isinstance(type_to_check, tuple) and type_to_check is not None] -transmission_plot_type_error_11 = [{'elements': elements, 'variables': variables, +transmission_plot_type_error_13 = [{'elements': elements, 'variables': variables, 'angular_position_unit': 'rad', 'angular_speed_unit': 'rad/s', - 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', - 'time_unit': type_to_check, 'figsize': (type_to_check, type_to_check)} + 'angular_acceleration_unit': 'rad/s^2', 'torque_unit': 'Nm', 'force_unit': 'N', + 'stress_unit': 'MPa', 'time_unit': 'sec', 'figsize': (type_to_check, type_to_check)} for type_to_check in types_to_check if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)] @@ -161,7 +195,9 @@ def transmission_snapshot_type_error(request): *transmission_plot_type_error_8, *transmission_plot_type_error_9, *transmission_plot_type_error_10, - *transmission_plot_type_error_11]) + *transmission_plot_type_error_11, + *transmission_plot_type_error_12, + *transmission_plot_type_error_13]) def transmission_plot_type_error(request): return request.param diff --git a/tests/test_transmission/test_transmission.py b/tests/test_transmission/test_transmission.py index 2bafcc6..7b28687 100644 --- a/tests/test_transmission/test_transmission.py +++ b/tests/test_transmission/test_transmission.py @@ -1,14 +1,14 @@ from gearpy.mechanical_object import DCMotor, SpurGear from gearpy.solver import Solver from gearpy.transmission import Transmission -from gearpy.units import AngularAcceleration, AngularPosition, AngularSpeed, InertiaMoment, Torque, Time +from gearpy.units import AngularAcceleration, AngularPosition, AngularSpeed, Force, InertiaMoment, Length, Stress, Torque, Time from gearpy.utils import add_gear_mating, add_fixed_joint from hypothesis import given, settings, HealthCheck from hypothesis.strategies import lists, floats, sampled_from, booleans, one_of, none, integers, tuples import matplotlib.pyplot as plt import pandas as pd from pytest import mark, raises -from tests.conftest import dc_motors, spur_gears, flywheels, time_intervals, transmissions, basic_transmission, solved_transmissions +from tests.conftest import dc_motors, simple_spur_gears, flywheels, time_intervals, transmissions, basic_transmission, solved_transmissions from tests.test_units.test_angular_position.conftest import angular_positions from tests.test_units.test_angular_speed.conftest import angular_speeds from tests.test_units.test_time.conftest import times @@ -22,7 +22,7 @@ class TestTransmissionInit: @mark.genuine @given(motor = dc_motors(), flywheel = flywheels(), - gears = lists(elements = spur_gears(), min_size = 1)) + gears = lists(elements = simple_spur_gears(), min_size = 1)) @settings(max_examples = 100, deadline = None, suppress_health_check = [HealthCheck.too_slow]) def test_property(self, motor, flywheel, gears): add_fixed_joint(master = motor, slave = flywheel) @@ -63,7 +63,7 @@ def test_raises_value_error(self): def test_raises_name_error(self): motor = DCMotor(name = 'not unique name', inertia_moment = InertiaMoment(1, 'kgm^2'), no_load_speed = AngularSpeed(1000, 'rpm'), maximum_torque = Torque(1, 'Nm')) - gear = SpurGear(name = 'not unique name', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) + gear = SpurGear(name = 'not unique name', n_teeth = 10, module = Length(1, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) add_fixed_joint(master = motor, slave = gear) with raises(NameError): Transmission(motor = motor) @@ -105,11 +105,16 @@ class TestTransmissionSnapshot: torque_unit = sampled_from(elements = list(Torque._Torque__UNITS.keys())), driving_torque_unit = sampled_from(elements = list(Torque._Torque__UNITS.keys())), load_torque_unit = sampled_from(elements = list(Torque._Torque__UNITS.keys())), + force_unit = sampled_from(elements = list(Force._Force__UNITS.keys())), + stress_unit = sampled_from(elements = list(Stress._Stress__UNITS.keys())), print_data = booleans()) @settings(max_examples = 100, deadline = None) def test_method(self, time_discretization, simulation_steps, transmission, initial_angular_position, initial_angular_speed, target_time_fraction, angular_position_unit, angular_speed_unit, - angular_acceleration_unit, torque_unit, driving_torque_unit, load_torque_unit, print_data): + angular_acceleration_unit, torque_unit, driving_torque_unit, load_torque_unit, force_unit, + stress_unit, print_data): + warnings.filterwarnings('ignore', category = RuntimeWarning) + transmission.chain[-1].angular_position = initial_angular_position transmission.chain[-1].angular_speed = initial_angular_speed transmission.chain[-1].external_torque = lambda time, angular_position, angular_speed: Torque(0.001, 'Nm') @@ -123,7 +128,7 @@ def test_method(self, time_discretization, simulation_steps, transmission, initi angular_position_unit = angular_position_unit, angular_speed_unit = angular_speed_unit, angular_acceleration_unit = angular_acceleration_unit, torque_unit = torque_unit, driving_torque_unit = driving_torque_unit, load_torque_unit = load_torque_unit, - print_data = print_data) + force_unit = force_unit, stress_unit = stress_unit, print_data = print_data) assert isinstance(data, pd.DataFrame) assert [element.name for element in transmission.chain] == data.index.to_list() @@ -159,19 +164,21 @@ class TestTransmissionPlot: elements_index = one_of(none(), lists(integers(min_value = 0, max_value = 4), min_size = 1)), single_element = booleans(), elements_as_names = booleans(), - variables = one_of(none(), lists(sampled_from(elements = list(basic_transmission.chain[0].time_variables.keys())), min_size = 1)), angular_position_unit = sampled_from(elements = list(AngularPosition._AngularPosition__UNITS.keys())), angular_speed_unit = sampled_from(elements = list(AngularSpeed._AngularSpeed__UNITS.keys())), angular_acceleration_unit = sampled_from(elements = list(AngularAcceleration._AngularAcceleration__UNITS.keys())), torque_unit = sampled_from(elements = list(Torque._Torque__UNITS.keys())), + force_unit = sampled_from(elements = list(Force._Force__UNITS.keys())), + stress_unit = sampled_from(elements = list(Stress._Stress__UNITS.keys())), time_unit = sampled_from(elements = list(Time._Time__UNITS.keys())), figsize = one_of(none(), tuples(floats(min_value = 1, max_value = 10, allow_nan = False, allow_infinity = False), floats(min_value = 1, max_value = 10, allow_nan = False,allow_infinity = False)))) @settings(max_examples = 100, deadline = None) - def test_method(self, solved_transmission, elements_index, single_element, elements_as_names, variables, - angular_position_unit, angular_speed_unit, angular_acceleration_unit, torque_unit, time_unit, figsize): - warnings.filterwarnings("ignore", category = UserWarning) - warnings.filterwarnings("ignore", category = RuntimeWarning) + def test_method(self, solved_transmission, elements_index, single_element, elements_as_names, angular_position_unit, + angular_speed_unit, angular_acceleration_unit, torque_unit, force_unit, stress_unit, time_unit, + figsize): + warnings.filterwarnings('ignore', category = UserWarning) + warnings.filterwarnings('ignore', category = RuntimeWarning) if elements_index is not None: valid_index = [index for index in elements_index if index < len(solved_transmission.chain)] @@ -183,10 +190,23 @@ def test_method(self, solved_transmission, elements_index, single_element, eleme else: elements = None - solved_transmission.plot(elements = elements, variables = variables, - angular_position_unit = angular_position_unit, angular_speed_unit = angular_speed_unit, + solved_transmission.plot(elements = elements, angular_position_unit = angular_position_unit, + angular_speed_unit = angular_speed_unit, angular_acceleration_unit = angular_acceleration_unit, torque_unit = torque_unit, - time_unit = time_unit, figsize = figsize) + force_unit = force_unit, stress_unit = stress_unit, time_unit = time_unit, + figsize = figsize) + plt.close() + + solved_transmission.plot(elements = elements) + plt.close() + + solved_transmission.plot(elements = [solved_transmission.chain[0]], variables = ['angular position']) + plt.close() + + solved_transmission.plot(elements = list(solved_transmission.chain[:2]), variables = ['tangential force']) + plt.close() + + solved_transmission.plot(elements = list(solved_transmission.chain[:2]), variables = ['bending stress']) plt.close() diff --git a/tests/test_units/test_angular_acceleration/test_angular_acceleration.py b/tests/test_units/test_angular_acceleration/test_angular_acceleration.py index cb61fea..5692f8d 100644 --- a/tests/test_units/test_angular_acceleration/test_angular_acceleration.py +++ b/tests/test_units/test_angular_acceleration/test_angular_acceleration.py @@ -51,6 +51,20 @@ def test_method(self, value, unit): assert str(angular_acceleration) == f'{value} {unit}' +@mark.units +class TestAngularAccelerationAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + angular_acceleration = AngularAcceleration(value = value, unit = unit) + + assert abs(angular_acceleration) == AngularAcceleration(value = abs(value), unit = unit) + assert abs(angular_acceleration).value >= 0 + @mark.units class TestAngularAccelerationAdd: diff --git a/tests/test_units/test_angular_position/test_angular_position.py b/tests/test_units/test_angular_position/test_angular_position.py index 542f0c1..0069d92 100644 --- a/tests/test_units/test_angular_position/test_angular_position.py +++ b/tests/test_units/test_angular_position/test_angular_position.py @@ -51,6 +51,20 @@ def test_method(self, value, unit): assert str(angular_position) == f'{value} {unit}' +@mark.units +class TestAngularPositionAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + angular_position = AngularPosition(value = value, unit = unit) + + assert abs(angular_position) == AngularPosition(value = abs(value), unit = unit) + assert abs(angular_position).value >= 0 + @mark.units class TestAngularPositionAdd: diff --git a/tests/test_units/test_angular_speed/test_angular_speed.py b/tests/test_units/test_angular_speed/test_angular_speed.py index 88e016a..65dbd73 100644 --- a/tests/test_units/test_angular_speed/test_angular_speed.py +++ b/tests/test_units/test_angular_speed/test_angular_speed.py @@ -51,6 +51,20 @@ def test_method(self, value, unit): assert str(angular_speed) == f'{value} {unit}' +@mark.units +class TestAngularSpeedAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + angular_speed = AngularSpeed(value = value, unit = unit) + + assert abs(angular_speed) == AngularSpeed(value = abs(value), unit = unit) + assert abs(angular_speed).value >= 0 + @mark.units class TestAngularSpeedAdd: diff --git a/tests/test_units/test_force/conftest.py b/tests/test_units/test_force/conftest.py new file mode 100644 index 0000000..f3eef2a --- /dev/null +++ b/tests/test_units/test_force/conftest.py @@ -0,0 +1,104 @@ +from gearpy.units import Force, Surface +from hypothesis.strategies import composite, floats, sampled_from +import numpy as np +from tests.conftest import types_to_check +from pytest import fixture + + +basic_force = Force(1, 'N') + + +@composite +def forces(draw): + value = draw(floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000)) + unit = draw(sampled_from(elements = list(Force._Force__UNITS.keys()))) + + return Force(value = value, unit = unit) + + +force_init_type_error_1 = [{'value': type_to_check, 'unit': 'unit'} for type_to_check in types_to_check + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)] + +force_init_type_error_2 = [{'value': 1, 'unit': types_to_check} for type_to_check in types_to_check + if not isinstance(type_to_check, str)] + +@fixture(params = [*force_init_type_error_1, + *force_init_type_error_2]) +def force_init_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_add_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_sub_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)]) +def force_mul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, np.ndarray)]) +def force_rmul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, Force) + and not isinstance(type_to_check, Surface)]) +def force_truediv_type_error(request): + return request.param + + +@fixture(params = [0, 0.0, Force(0, 'N')]) +def force_truediv_zero_division_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_eq_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_ne_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_gt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_ge_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_lt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Force)]) +def force_le_type_error(request): + return request.param + + +force_to_type_error_1 = [{'target_unit': type_to_check, 'inplace': True} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + +force_to_type_error_2 = [{'target_unit': 'target_unit', 'inplace': type_to_check} for type_to_check in types_to_check + if not isinstance(type_to_check, bool) and not isinstance(type_to_check, int)] + +@fixture(params = [*force_to_type_error_1, + *force_to_type_error_2]) +def force_to_type_error(request): + return request.param diff --git a/tests/test_units/test_force/test_force.py b/tests/test_units/test_force/test_force.py new file mode 100644 index 0000000..55ac2e1 --- /dev/null +++ b/tests/test_units/test_force/test_force.py @@ -0,0 +1,396 @@ +from gearpy.units import Force, Surface, Stress +from hypothesis.strategies import floats, sampled_from, one_of, booleans +from hypothesis import given, settings +from tests.test_units.test_force.conftest import basic_force, forces +from tests.test_units.test_surface.conftest import surfaces +from pytest import mark, raises + + +units_list = list(Force._Force__UNITS.keys()) + + +@mark.units +class TestForceInit: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + force = Force(value = value, unit = unit) + + assert force.value == value + assert force.unit == unit + + + @mark.error + def test_raises_type_error(self, force_init_type_error): + with raises(TypeError): + Force(**force_init_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + Force(value = 1, unit = fake_unit) + + +@mark.units +class TestForceRepr: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + force = Force(value = value, unit = unit) + + assert str(force) == f'{value} {unit}' + + +@mark.units +class TestForceAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + force = Force(value = value, unit = unit) + + assert abs(force) == Force(value = abs(value), unit = unit) + assert abs(force).value >= 0 + + +@mark.units +class TestForceAdd: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + force_1 = Force(value = value_1, unit = unit_1) + force_2 = Force(value = value_2, unit = unit_2) + result = force_1 + force_2 + + assert isinstance(result, Force) + assert result.value == force_1.value + force_2.to(unit_1).value + assert result.unit == force_1.unit + + + @mark.error + def test_raises_type_error(self, force_add_type_error): + with raises(TypeError): + assert basic_force + force_add_type_error + + +@mark.units +class TestForceSub: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + force_1 = Force(value = value_1, unit = unit_1) + force_2 = Force(value = value_2, unit = unit_2) + result = force_1 - force_2 + + assert isinstance(result, Force) + assert result.value == force_1.value - force_2.to(unit_1).value + assert result.unit == force_1.unit + + + @mark.error + def test_raises_type_error(self, force_sub_type_error): + with raises(TypeError): + assert basic_force - force_sub_type_error + + +@mark.units +class TestForceMul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + force = Force(value = value, unit = unit) + result = force*multiplier + + assert isinstance(result, Force) + assert result.value == force.value*multiplier + assert result.unit == force.unit + + + @mark.error + def test_raises_type_error(self, force_mul_type_error): + with raises(TypeError): + assert basic_force*force_mul_type_error + + +@mark.units +class TestForceRmul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + force = Force(value = value, unit = unit) + result = multiplier*force + + assert isinstance(result, Force) + assert result.value == force.value*multiplier + assert result.unit == force.unit + + + @mark.error + def test_raises_type_error(self, force_rmul_type_error): + with raises(TypeError): + assert force_rmul_type_error*basic_force + + +@mark.units +class TestForceTruediv: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list), + divider = one_of(floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + forces(), + surfaces())) + @settings(max_examples = 100) + def test_method(self, value, unit, divider): + force = Force(value = value, unit = unit) + + if isinstance(divider, Force): + if abs(divider.value) >= 1e-300: + result = force/divider + assert isinstance(result, float) + assert result == force.value/divider.to(unit).value + elif isinstance(divider, Surface): + if abs(divider.value) >= 1e-300: + result = force/divider + assert isinstance(result, Stress) + assert result.value == force.to('N').value/divider.to('m^2').value + assert result.unit == 'Pa' + else: + if divider != 0: + result = force/divider + assert isinstance(result, Force) + assert result.value == force.value/divider + assert result.unit == force.unit + + + @mark.error + def test_raises_type_error(self, force_truediv_type_error): + with raises(TypeError): + assert basic_force/force_truediv_type_error + + + @mark.error + def test_raises_zero_division_error(self, force_truediv_zero_division_error): + with raises(ZeroDivisionError): + assert basic_force/force_truediv_zero_division_error + + +@mark.units +class TestForceEq: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + force_1 = Force(value = value, unit = unit) + force_2 = Force(value = value, unit = unit) + + for target_unit in units_list: + assert force_1 == force_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, force_eq_type_error): + with raises(TypeError): + assert basic_force == force_eq_type_error + + +@mark.units +class TestForceNe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + force_1 = Force(value = value, unit = unit) + force_2 = Force(value = value + gap, unit = unit) + + for target_unit in units_list: + assert force_1 != force_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, force_ne_type_error): + with raises(TypeError): + assert basic_force != force_ne_type_error + + +@mark.units +class TestForceGt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + force_1 = Force(value = value + gap, unit = unit) + force_2 = Force(value = value, unit = unit) + + for target_unit in units_list: + assert force_1 > force_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, force_gt_type_error): + with raises(TypeError): + assert basic_force > force_gt_type_error + + +@mark.units +class TestForceGe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + force_1 = Force(value = value + gap, unit = unit) + force_2 = Force(value = value, unit = unit) + + for target_unit in units_list: + assert force_1 >= force_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, force_ge_type_error): + with raises(TypeError): + assert basic_force >= force_ge_type_error + + +@mark.units +class TestForceLt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + force_1 = Force(value = value, unit = unit) + force_2 = Force(value = value + gap, unit = unit) + + for target_unit in units_list: + assert force_1 < force_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, force_lt_type_error): + with raises(TypeError): + assert basic_force < force_lt_type_error + + +@mark.units +class TestForceLe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + force_1 = Force(value = value, unit = unit) + force_2 = Force(value = value + gap, unit = unit) + + for target_unit in units_list: + assert force_1 <= force_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, force_le_type_error): + with raises(TypeError): + assert basic_force <= force_le_type_error + + +@mark.units +class TestForceTo: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + inplace = booleans()) + @settings(max_examples = 100) + def test_method(self, value, unit, inplace): + if abs(value) >= 1e-10: + force = Force(value = value, unit = unit) + + for target_unit in units_list: + converted_force = force.to(target_unit = target_unit, inplace = inplace) + + assert converted_force.unit == target_unit + if Force._Force__UNITS[target_unit] != Force._Force__UNITS[unit]: + assert converted_force.value != value + assert converted_force.unit != unit + + if inplace: + assert converted_force.value == force.value + assert converted_force.unit == force.unit + else: + assert converted_force.value != force.value + assert converted_force.unit != force.unit + else: + assert converted_force == force + + + @mark.error + def test_raises_type_error(self, force_to_type_error): + with raises(TypeError): + basic_force.to(**force_to_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + basic_force.to(fake_unit) diff --git a/tests/test_units/test_inertia_moment/conftest.py b/tests/test_units/test_inertia_moment/conftest.py index 9771836..eca10a5 100644 --- a/tests/test_units/test_inertia_moment/conftest.py +++ b/tests/test_units/test_inertia_moment/conftest.py @@ -10,17 +10,17 @@ @composite def inertia_moments(draw): - value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = True, max_value = 1000)) + value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) unit = draw(sampled_from(elements = list(InertiaMoment._InertiaMoment__UNITS.keys()))) return InertiaMoment(value = value, unit = unit) inertia_moment_init_type_error_1 = [{'value': type_to_check, 'unit': 'unit'} for type_to_check in types_to_check - if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)] + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)] inertia_moment_init_type_error_2 = [{'value': 1, 'unit': types_to_check} for type_to_check in types_to_check - if not isinstance(type_to_check, str)] + if not isinstance(type_to_check, str)] @fixture(params = [*inertia_moment_init_type_error_1, *inertia_moment_init_type_error_2]) diff --git a/tests/test_units/test_inertia_moment/test_inertia_moment.py b/tests/test_units/test_inertia_moment/test_inertia_moment.py index 897a8dc..d3f0f06 100644 --- a/tests/test_units/test_inertia_moment/test_inertia_moment.py +++ b/tests/test_units/test_inertia_moment/test_inertia_moment.py @@ -57,6 +57,20 @@ def test_method(self, value, unit): assert str(inertia_moment) == f'{value} {unit}' +@mark.units +class TestInertiaMomentAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + inertia_moment = InertiaMoment(value = value, unit = unit) + + assert abs(inertia_moment) == InertiaMoment(value = abs(value), unit = unit) + assert abs(inertia_moment).value >= 0 + @mark.units class TestInertiaMomentAdd: diff --git a/tests/test_units/test_length/conftest.py b/tests/test_units/test_length/conftest.py new file mode 100644 index 0000000..8f650a6 --- /dev/null +++ b/tests/test_units/test_length/conftest.py @@ -0,0 +1,105 @@ +from gearpy.units import Length +from hypothesis.strategies import composite, floats, sampled_from +import numpy as np +from tests.conftest import types_to_check +from pytest import fixture + + +basic_length = Length(1, 'm') + + +@composite +def lengths(draw): + value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + unit = draw(sampled_from(elements = list(Length._Length__UNITS.keys()))) + + return Length(value = value, unit = unit) + + +length_init_type_error_1 = [{'value': type_to_check, 'unit': 'unit'} for type_to_check in types_to_check + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)] + +length_init_type_error_2 = [{'value': 1, 'unit': types_to_check} for type_to_check in types_to_check + if not isinstance(type_to_check, str)] + +@fixture(params = [*length_init_type_error_1, + *length_init_type_error_2]) +def length_init_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_add_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_sub_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, Length)]) +def length_mul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, np.ndarray) + and not isinstance(type_to_check, Length)]) +def length_rmul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, Length)]) +def length_truediv_type_error(request): + return request.param + + +@fixture(params = [0, 0.0]) +def length_truediv_zero_division_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_eq_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_ne_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_gt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_ge_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_lt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Length)]) +def length_le_type_error(request): + return request.param + + +length_to_type_error_1 = [{'target_unit': type_to_check, 'inplace': True} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + +length_to_type_error_2 = [{'target_unit': 'target_unit', 'inplace': type_to_check} + for type_to_check in types_to_check if not isinstance(type_to_check, bool) + and not isinstance(type_to_check, int)] + +@fixture(params = [*length_to_type_error_1, + *length_to_type_error_2]) +def length_to_type_error(request): + return request.param diff --git a/tests/test_units/test_length/test_length.py b/tests/test_units/test_length/test_length.py new file mode 100644 index 0000000..6b0ef4b --- /dev/null +++ b/tests/test_units/test_length/test_length.py @@ -0,0 +1,419 @@ +from gearpy.units import Length, Surface +from hypothesis.strategies import floats, sampled_from, one_of, booleans +from hypothesis import given, settings +from tests.test_units.test_length.conftest import basic_length, lengths +from pytest import mark, raises + + +units_list = list(Length._Length__UNITS.keys()) + + +@mark.units +class TestLengthInit: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + length = Length(value = value, unit = unit) + + assert length.value == value + assert length.unit == unit + + + @mark.error + def test_raises_type_error(self, length_init_type_error): + with raises(TypeError): + Length(**length_init_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + Length(value = 1, unit = fake_unit) + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + Length(value = -1, unit = 'm') + + +@mark.units +class TestLengthRepr: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + length = Length(value = value, unit = unit) + + assert str(length) == f'{value} {unit}' + + +@mark.units +class TestLengthAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + length = Length(value = value, unit = unit) + + assert abs(length) == Length(value = abs(value), unit = unit) + assert abs(length).value >= 0 + + +@mark.units +class TestLengthAdd: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + length_1 = Length(value = value_1, unit = unit_1) + length_2 = Length(value = value_2, unit = unit_2) + result = length_1 + length_2 + + assert isinstance(result, Length) + assert result.value == length_1.value + length_2.to(unit_1).value + assert result.unit == length_1.unit + + + @mark.error + def test_raises_type_error(self, length_add_type_error): + with raises(TypeError): + assert basic_length + length_add_type_error + + +@mark.units +class TestLengthSub: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + length_1 = Length(value = value_1, unit = unit_1) + length_2 = Length(value = value_2, unit = unit_2) + if length_1 > length_2: + result = length_1 - length_2 + + assert isinstance(result, Length) + assert result.value == length_1.value - length_2.to(unit_1).value + assert result.unit == length_1.unit + + + @mark.error + def test_raises_type_error(self, length_sub_type_error): + with raises(TypeError): + assert basic_length - length_sub_type_error + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + assert basic_length - Length(basic_length.value + 1, basic_length.unit) + + +@mark.units +class TestLengthMul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = one_of(floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + lengths())) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + length = Length(value = value, unit = unit) + result = length*multiplier + + if isinstance(multiplier, Length): + assert isinstance(result, Surface) + assert result.value == length.to('m').value*multiplier.to('m').value + assert result.unit == 'm^2' + else: + assert isinstance(result, Length) + assert result.value == length.value*multiplier + assert result.unit == length.unit + + + @mark.error + def test_raises_type_error(self, length_mul_type_error): + with raises(TypeError): + assert basic_length*length_mul_type_error + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + assert basic_length*(-1) + + +@mark.units +class TestLengthRmul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + length = Length(value = value, unit = unit) + result = multiplier*length + + assert isinstance(result, Length) + assert result.value == length.value*multiplier + assert result.unit == length.unit + + + @mark.error + def test_raises_type_error(self, length_rmul_type_error): + with raises(TypeError): + assert length_rmul_type_error*basic_length + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + assert -1*basic_length + + +@mark.units +class TestLengthTruediv: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + divider = one_of(floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + lengths())) + @settings(max_examples = 100) + def test_method(self, value, unit, divider): + length = Length(value = value, unit = unit) + + if isinstance(divider, Length): + if abs(divider.value) >= 1e-300: + result = length/divider + assert isinstance(result, float) + assert result == length.value/divider.to(unit).value + else: + if divider != 0: + result = length/divider + assert isinstance(result, Length) + assert result.value == length.value/divider + assert result.unit == length.unit + + + @mark.error + def test_raises_type_error(self, length_truediv_type_error): + with raises(TypeError): + assert basic_length/length_truediv_type_error + + + @mark.error + def test_raises_zero_division_error(self, length_truediv_zero_division_error): + with raises(ZeroDivisionError): + assert basic_length/length_truediv_zero_division_error + + +@mark.units +class TestLengthEq: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + length_1 = Length(value = value, unit = unit) + length_2 = Length(value = value, unit = unit) + + for target_unit in units_list: + assert length_1 == length_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, length_eq_type_error): + with raises(TypeError): + assert basic_length == length_eq_type_error + + +@mark.units +class TestLengthNe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + length_1 = Length(value = value, unit = unit) + length_2 = Length(value = value + gap, unit = unit) + + for target_unit in units_list: + assert length_1 != length_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, length_ne_type_error): + with raises(TypeError): + assert basic_length != length_ne_type_error + + +@mark.units +class TestLengthGt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + length_1 = Length(value = value + gap, unit = unit) + length_2 = Length(value = value, unit = unit) + + for target_unit in units_list: + assert length_1 > length_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, length_gt_type_error): + with raises(TypeError): + assert basic_length > length_gt_type_error + + +@mark.units +class TestLengthGe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + length_1 = Length(value = value + gap, unit = unit) + length_2 = Length(value = value, unit = unit) + + for target_unit in units_list: + assert length_1 >= length_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, length_ge_type_error): + with raises(TypeError): + assert basic_length >= length_ge_type_error + + +@mark.units +class TestLengthLt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + length_1 = Length(value = value, unit = unit) + length_2 = Length(value = value + gap, unit = unit) + + for target_unit in units_list: + assert length_1 < length_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, length_lt_type_error): + with raises(TypeError): + assert basic_length < length_lt_type_error + + +@mark.units +class TestLengthLe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + length_1 = Length(value = value, unit = unit) + length_2 = Length(value = value + gap, unit = unit) + + for target_unit in units_list: + assert length_1 <= length_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, length_le_type_error): + with raises(TypeError): + assert basic_length <= length_le_type_error + + +@mark.units +class TestLengthTo: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + inplace = booleans()) + @settings(max_examples = 100) + def test_method(self, value, unit, inplace): + if abs(value) >= 1e-10: + length = Length(value = value, unit = unit) + + for target_unit in units_list: + converted_inertia = length.to(target_unit = target_unit, inplace = inplace) + + assert converted_inertia.unit == target_unit + if Length._Length__UNITS[target_unit] != Length._Length__UNITS[unit]: + assert converted_inertia.value != value + assert converted_inertia.unit != unit + + if inplace: + assert converted_inertia.value == length.value + assert converted_inertia.unit == length.unit + else: + assert converted_inertia.value != length.value + assert converted_inertia.unit != length.unit + else: + assert converted_inertia == length + + + @mark.error + def test_raises_type_error(self, length_to_type_error): + with raises(TypeError): + basic_length.to(**length_to_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + basic_length.to(fake_unit) diff --git a/tests/test_units/test_stress/conftest.py b/tests/test_units/test_stress/conftest.py new file mode 100644 index 0000000..1b29c67 --- /dev/null +++ b/tests/test_units/test_stress/conftest.py @@ -0,0 +1,103 @@ +from gearpy.units import Stress +from hypothesis.strategies import composite, floats, sampled_from +import numpy as np +from tests.conftest import types_to_check +from pytest import fixture + + +basic_stress = Stress(1, 'Pa') + + +@composite +def stresses(draw): + value = draw(floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000)) + unit = draw(sampled_from(elements = list(Stress._Stress__UNITS.keys()))) + + return Stress(value = value, unit = unit) + + +stress_init_type_error_1 = [{'value': type_to_check, 'unit': 'unit'} for type_to_check in types_to_check + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)] + +stress_init_type_error_2 = [{'value': 1, 'unit': types_to_check} for type_to_check in types_to_check + if not isinstance(type_to_check, str)] + +@fixture(params = [*stress_init_type_error_1, + *stress_init_type_error_2]) +def stress_init_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_add_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_sub_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)]) +def stress_mul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, np.ndarray)]) +def stress_rmul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, Stress)]) +def stress_truediv_type_error(request): + return request.param + + +@fixture(params = [0, 0.0, Stress(0, 'Pa')]) +def stress_truediv_zero_division_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_eq_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_ne_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_gt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_ge_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_lt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Stress)]) +def stress_le_type_error(request): + return request.param + + +stress_to_type_error_1 = [{'target_unit': type_to_check, 'inplace': True} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + +stress_to_type_error_2 = [{'target_unit': 'target_unit', 'inplace': type_to_check} for type_to_check in types_to_check + if not isinstance(type_to_check, bool) and not isinstance(type_to_check, int)] + +@fixture(params = [*stress_to_type_error_1, + *stress_to_type_error_2]) +def stress_to_type_error(request): + return request.param diff --git a/tests/test_units/test_stress/test_stress.py b/tests/test_units/test_stress/test_stress.py new file mode 100644 index 0000000..e56bffe --- /dev/null +++ b/tests/test_units/test_stress/test_stress.py @@ -0,0 +1,388 @@ +from gearpy.units import Stress +from hypothesis.strategies import floats, sampled_from, one_of, booleans +from hypothesis import given, settings +from tests.test_units.test_stress.conftest import basic_stress, stresses +from pytest import mark, raises + + +units_list = list(Stress._Stress__UNITS.keys()) + + +@mark.units +class TestStressInit: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + stress = Stress(value = value, unit = unit) + + assert stress.value == value + assert stress.unit == unit + + + @mark.error + def test_raises_type_error(self, stress_init_type_error): + with raises(TypeError): + Stress(**stress_init_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + Stress(value = 1, unit = fake_unit) + + +@mark.units +class TestStressRepr: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + stress = Stress(value = value, unit = unit) + + assert str(stress) == f'{value} {unit}' + + +@mark.units +class TestStressAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + stress = Stress(value = value, unit = unit) + + assert abs(stress) == Stress(value = abs(value), unit = unit) + assert abs(stress).value >= 0 + + +@mark.units +class TestStressAdd: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + stress_1 = Stress(value = value_1, unit = unit_1) + stress_2 = Stress(value = value_2, unit = unit_2) + result = stress_1 + stress_2 + + assert isinstance(result, Stress) + assert result.value == stress_1.value + stress_2.to(unit_1).value + assert result.unit == stress_1.unit + + + @mark.error + def test_raises_type_error(self, stress_add_type_error): + with raises(TypeError): + assert basic_stress + stress_add_type_error + + +@mark.units +class TestStressSub: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + stress_1 = Stress(value = value_1, unit = unit_1) + stress_2 = Stress(value = value_2, unit = unit_2) + result = stress_1 - stress_2 + + assert isinstance(result, Stress) + assert result.value == stress_1.value - stress_2.to(unit_1).value + assert result.unit == stress_1.unit + + + @mark.error + def test_raises_type_error(self, stress_sub_type_error): + with raises(TypeError): + assert basic_stress - stress_sub_type_error + + +@mark.units +class TestStressMul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + stress = Stress(value = value, unit = unit) + result = stress*multiplier + + assert isinstance(result, Stress) + assert result.value == stress.value*multiplier + assert result.unit == stress.unit + + + @mark.error + def test_raises_type_error(self, stress_mul_type_error): + with raises(TypeError): + assert basic_stress*stress_mul_type_error + + +@mark.units +class TestStressRmul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + stress = Stress(value = value, unit = unit) + result = multiplier*stress + + assert isinstance(result, Stress) + assert result.value == stress.value*multiplier + assert result.unit == stress.unit + + + @mark.error + def test_raises_type_error(self, stress_rmul_type_error): + with raises(TypeError): + assert stress_rmul_type_error*basic_stress + + +@mark.units +class TestStressTruediv: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list), + divider = one_of(floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + stresses())) + @settings(max_examples = 100) + def test_method(self, value, unit, divider): + torque = Stress(value = value, unit = unit) + + if isinstance(divider, Stress): + if abs(divider.value) >= 1e-300: + result = torque/divider + assert isinstance(result, float) + assert result == torque.value/divider.to(unit).value + else: + if divider != 0: + result = torque/divider + assert isinstance(result, Stress) + assert result.value == torque.value/divider + assert result.unit == torque.unit + + + @mark.error + def test_raises_type_error(self, stress_truediv_type_error): + with raises(TypeError): + assert basic_stress/stress_truediv_type_error + + + @mark.error + def test_raises_zero_division_error(self, stress_truediv_zero_division_error): + with raises(ZeroDivisionError): + assert basic_stress/stress_truediv_zero_division_error + + +@mark.units +class TestStressEq: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + stress_1 = Stress(value = value, unit = unit) + stress_2 = Stress(value = value, unit = unit) + + for target_unit in units_list: + assert stress_1 == stress_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, stress_eq_type_error): + with raises(TypeError): + assert basic_stress == stress_eq_type_error + + +@mark.units +class TestStressNe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + stress_1 = Stress(value = value, unit = unit) + stress_2 = Stress(value = value + gap, unit = unit) + + for target_unit in units_list: + assert stress_1 != stress_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, stress_ne_type_error): + with raises(TypeError): + assert basic_stress != stress_ne_type_error + + +@mark.units +class TestStressGt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + stress_1 = Stress(value = value + gap, unit = unit) + stress_2 = Stress(value = value, unit = unit) + + for target_unit in units_list: + assert stress_1 > stress_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, stress_gt_type_error): + with raises(TypeError): + assert basic_stress > stress_gt_type_error + + +@mark.units +class TestStressGe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + stress_1 = Stress(value = value + gap, unit = unit) + stress_2 = Stress(value = value, unit = unit) + + for target_unit in units_list: + assert stress_1 >= stress_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, stress_ge_type_error): + with raises(TypeError): + assert basic_stress >= stress_ge_type_error + + +@mark.units +class TestStressLt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + stress_1 = Stress(value = value, unit = unit) + stress_2 = Stress(value = value + gap, unit = unit) + + for target_unit in units_list: + assert stress_1 < stress_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, stress_lt_type_error): + with raises(TypeError): + assert basic_stress < stress_lt_type_error + + +@mark.units +class TestStressLe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + stress_1 = Stress(value = value, unit = unit) + stress_2 = Stress(value = value + gap, unit = unit) + + for target_unit in units_list: + assert stress_1 <= stress_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, stress_le_type_error): + with raises(TypeError): + assert basic_stress <= stress_le_type_error + + +@mark.units +class TestStressTo: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, max_value = 1000, min_value = -1000), + unit = sampled_from(elements = units_list), + inplace = booleans()) + @settings(max_examples = 100) + def test_method(self, value, unit, inplace): + if abs(value) >= 1e-10: + stress = Stress(value = value, unit = unit) + + for target_unit in units_list: + converted_stress = stress.to(target_unit = target_unit, inplace = inplace) + + assert converted_stress.unit == target_unit + if Stress._Stress__UNITS[target_unit] != Stress._Stress__UNITS[unit]: + assert converted_stress.value != value + assert converted_stress.unit != unit + + if inplace: + assert converted_stress.value == stress.value + assert converted_stress.unit == stress.unit + else: + assert converted_stress.value != stress.value + assert converted_stress.unit != stress.unit + else: + assert converted_stress == stress + + + @mark.error + def test_raises_type_error(self, stress_to_type_error): + with raises(TypeError): + basic_stress.to(**stress_to_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + basic_stress.to(fake_unit) diff --git a/tests/test_units/test_surface/conftest.py b/tests/test_units/test_surface/conftest.py new file mode 100644 index 0000000..c2c7b01 --- /dev/null +++ b/tests/test_units/test_surface/conftest.py @@ -0,0 +1,104 @@ +from gearpy.units import Surface +from hypothesis.strategies import composite, floats, sampled_from +import numpy as np +from tests.conftest import types_to_check +from pytest import fixture + + +basic_surface = Surface(1, 'm^2') + + +@composite +def surfaces(draw): + value = draw(floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + unit = draw(sampled_from(elements = list(Surface._Surface__UNITS.keys()))) + + return Surface(value = value, unit = unit) + + +surface_init_type_error_1 = [{'value': type_to_check, 'unit': 'unit'} for type_to_check in types_to_check + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)] + +surface_init_type_error_2 = [{'value': 1, 'unit': types_to_check} for type_to_check in types_to_check + if not isinstance(type_to_check, str)] + +@fixture(params = [*surface_init_type_error_1, + *surface_init_type_error_2]) +def surface_init_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_add_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_sub_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check + if not isinstance(type_to_check, float) and not isinstance(type_to_check, int)]) +def surface_mul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, np.ndarray)]) +def surface_rmul_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) + and not isinstance(type_to_check, int) and not isinstance(type_to_check, Surface)]) +def surface_truediv_type_error(request): + return request.param + + +@fixture(params = [0, 0.0]) +def surface_truediv_zero_division_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_eq_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_ne_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_gt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_ge_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_lt_type_error(request): + return request.param + + +@fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, Surface)]) +def surface_le_type_error(request): + return request.param + + +surface_to_type_error_1 = [{'target_unit': type_to_check, 'inplace': True} + for type_to_check in types_to_check if not isinstance(type_to_check, str)] + +surface_to_type_error_2 = [{'target_unit': 'target_unit', 'inplace': type_to_check} + for type_to_check in types_to_check if not isinstance(type_to_check, bool) + and not isinstance(type_to_check, int)] + +@fixture(params = [*surface_to_type_error_1, + *surface_to_type_error_2]) +def surface_to_type_error(request): + return request.param \ No newline at end of file diff --git a/tests/test_units/test_surface/test_surface.py b/tests/test_units/test_surface/test_surface.py new file mode 100644 index 0000000..2f68b83 --- /dev/null +++ b/tests/test_units/test_surface/test_surface.py @@ -0,0 +1,413 @@ +from gearpy.units import Surface +from hypothesis.strategies import floats, sampled_from, one_of, booleans +from hypothesis import given, settings +from tests.test_units.test_surface.conftest import basic_surface, surfaces +from pytest import mark, raises + + +units_list = list(Surface._Surface__UNITS.keys()) + + +@mark.units +class TestSurfaceInit: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + surface = Surface(value = value, unit = unit) + + assert surface.value == value + assert surface.unit == unit + + + @mark.error + def test_raises_type_error(self, surface_init_type_error): + with raises(TypeError): + Surface(**surface_init_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + Surface(value = 1, unit = fake_unit) + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + Surface(value = -1, unit = 'm^2') + + +@mark.units +class TestSurfaceRepr: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + surface = Surface(value = value, unit = unit) + + assert str(surface) == f'{value} {unit}' + + +@mark.units +class TestSurfaceAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + surface = Surface(value = value, unit = unit) + + assert abs(surface) == Surface(value = abs(value), unit = unit) + assert abs(surface).value >= 0 + + +@mark.units +class TestSurfaceAdd: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + surface_1 = Surface(value = value_1, unit = unit_1) + surface_2 = Surface(value = value_2, unit = unit_2) + result = surface_1 + surface_2 + + assert isinstance(result, Surface) + assert result.value == surface_1.value + surface_2.to(unit_1).value + assert result.unit == surface_1.unit + + + @mark.error + def test_raises_type_error(self, surface_add_type_error): + with raises(TypeError): + assert basic_surface + surface_add_type_error + + +@mark.units +class TestSurfaceSub: + + + @mark.genuine + @given(value_1 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_1 = sampled_from(elements = units_list), + value_2 = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit_2 = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value_1, unit_1, value_2, unit_2): + surface_1 = Surface(value = value_1, unit = unit_1) + surface_2 = Surface(value = value_2, unit = unit_2) + if surface_1 > surface_2: + result = surface_1 - surface_2 + + assert isinstance(result, Surface) + assert result.value == surface_1.value - surface_2.to(unit_1).value + assert result.unit == surface_1.unit + + + @mark.error + def test_raises_type_error(self, surface_sub_type_error): + with raises(TypeError): + assert basic_surface - surface_sub_type_error + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + assert basic_surface - Surface(basic_surface.value + 1, basic_surface.unit) + + +@mark.units +class TestSurfaceMul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + surface = Surface(value = value, unit = unit) + result = surface*multiplier + + assert isinstance(result, Surface) + assert result.value == surface.value*multiplier + assert result.unit == surface.unit + + + @mark.error + def test_raises_type_error(self, surface_mul_type_error): + with raises(TypeError): + assert basic_surface*surface_mul_type_error + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + assert basic_surface*(-1) + + +@mark.units +class TestSurfaceRmul: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + multiplier = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, multiplier): + surface = Surface(value = value, unit = unit) + result = multiplier*surface + + assert isinstance(result, Surface) + assert result.value == surface.value*multiplier + assert result.unit == surface.unit + + + @mark.error + def test_raises_type_error(self, surface_rmul_type_error): + with raises(TypeError): + assert surface_rmul_type_error*basic_surface + + + @mark.error + def test_raises_value_error(self): + with raises(ValueError): + assert -1*basic_surface + + +@mark.units +class TestSurfaceTruediv: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + divider = one_of(floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + surfaces())) + @settings(max_examples = 100) + def test_method(self, value, unit, divider): + surface = Surface(value = value, unit = unit) + + if isinstance(divider, Surface): + if abs(divider.value) >= 1e-300: + result = surface/divider + assert isinstance(result, float) + assert result == surface.value/divider.to(unit).value + else: + if divider != 0: + result = surface/divider + assert isinstance(result, Surface) + assert result.value == surface.value/divider + assert result.unit == surface.unit + + + @mark.error + def test_raises_type_error(self, surface_truediv_type_error): + with raises(TypeError): + assert basic_surface/surface_truediv_type_error + + + @mark.error + def test_raises_zero_division_error(self, surface_truediv_zero_division_error): + with raises(ZeroDivisionError): + assert basic_surface/surface_truediv_zero_division_error + + +@mark.units +class TestSurfaceEq: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + surface_1 = Surface(value = value, unit = unit) + surface_2 = Surface(value = value, unit = unit) + + for target_unit in units_list: + assert surface_1 == surface_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, surface_eq_type_error): + with raises(TypeError): + assert basic_surface == surface_eq_type_error + + +@mark.units +class TestSurfaceNe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + surface_1 = Surface(value = value, unit = unit) + surface_2 = Surface(value = value + gap, unit = unit) + + for target_unit in units_list: + assert surface_1 != surface_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, surface_ne_type_error): + with raises(TypeError): + assert basic_surface != surface_ne_type_error + + +@mark.units +class TestSurfaceGt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + surface_1 = Surface(value = value + gap, unit = unit) + surface_2 = Surface(value = value, unit = unit) + + for target_unit in units_list: + assert surface_1 > surface_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, surface_gt_type_error): + with raises(TypeError): + assert basic_surface > surface_gt_type_error + + +@mark.units +class TestSurfaceGe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + surface_1 = Surface(value = value + gap, unit = unit) + surface_2 = Surface(value = value, unit = unit) + + for target_unit in units_list: + assert surface_1 >= surface_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, surface_ge_type_error): + with raises(TypeError): + assert basic_surface >= surface_ge_type_error + + +@mark.units +class TestSurfaceLt: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + surface_1 = Surface(value = value, unit = unit) + surface_2 = Surface(value = value + gap, unit = unit) + + for target_unit in units_list: + assert surface_1 < surface_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, surface_lt_type_error): + with raises(TypeError): + assert basic_surface < surface_lt_type_error + + +@mark.units +class TestSurfaceLe: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + gap = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1000)) + @settings(max_examples = 100) + def test_method(self, value, unit, gap): + if value != 0: + surface_1 = Surface(value = value, unit = unit) + surface_2 = Surface(value = value + gap, unit = unit) + + for target_unit in units_list: + assert surface_1 <= surface_2.to(target_unit) + + + @mark.error + def test_raises_type_error(self, surface_le_type_error): + with raises(TypeError): + assert basic_surface <= surface_le_type_error + + +@mark.units +class TestSurfaceTo: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, exclude_min = True, max_value = 1000), + unit = sampled_from(elements = units_list), + inplace = booleans()) + @settings(max_examples = 100) + def test_method(self, value, unit, inplace): + if abs(value) >= 1e-10: + surface = Surface(value = value, unit = unit) + + for target_unit in units_list: + converted_inertia = surface.to(target_unit = target_unit, inplace = inplace) + + assert converted_inertia.unit == target_unit + if Surface._Surface__UNITS[target_unit] != Surface._Surface__UNITS[unit]: + assert converted_inertia.value != value + assert converted_inertia.unit != unit + + if inplace: + assert converted_inertia.value == surface.value + assert converted_inertia.unit == surface.unit + else: + assert converted_inertia.value != surface.value + assert converted_inertia.unit != surface.unit + else: + assert converted_inertia == surface + + + @mark.error + def test_raises_type_error(self, surface_to_type_error): + with raises(TypeError): + basic_surface.to(**surface_to_type_error) + + + @mark.error + def test_raises_key_error(self): + fake_units = [f'fake {unit}' for unit in units_list] + for fake_unit in fake_units: + with raises(KeyError): + basic_surface.to(fake_unit) diff --git a/tests/test_units/test_time/test_time.py b/tests/test_units/test_time/test_time.py index 3d4ec69..2e276e5 100644 --- a/tests/test_units/test_time/test_time.py +++ b/tests/test_units/test_time/test_time.py @@ -53,6 +53,20 @@ def test_method(self, value, unit): assert str(time) == f'{value} {unit}' +@mark.units +class TestTimeAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + time = Time(value = value, unit = unit) + + assert abs(time) == Time(value = abs(value), unit = unit) + assert abs(time).value >= 0 + @mark.units class TestTimeAdd: diff --git a/tests/test_units/test_time_interval/test_time_interval.py b/tests/test_units/test_time_interval/test_time_interval.py index eee4006..e07e26d 100644 --- a/tests/test_units/test_time_interval/test_time_interval.py +++ b/tests/test_units/test_time_interval/test_time_interval.py @@ -58,6 +58,20 @@ def test_method(self, value, unit): assert str(time_interval) == f'{value} {unit}' +@mark.units +class TestTimeIntervalAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = 1e-10, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + time_interval = TimeInterval(value = value, unit = unit) + + assert abs(time_interval) == TimeInterval(value = abs(value), unit = unit) + assert abs(time_interval).value >= 0 + @mark.units class TestTimeIntervalAdd: diff --git a/tests/test_units/test_torque/conftest.py b/tests/test_units/test_torque/conftest.py index 867ecf3..aa026c6 100644 --- a/tests/test_units/test_torque/conftest.py +++ b/tests/test_units/test_torque/conftest.py @@ -1,4 +1,4 @@ -from gearpy.units import InertiaMoment, Torque +from gearpy.units import InertiaMoment, Length, Torque from hypothesis.strategies import composite, floats, sampled_from import numpy as np from tests.conftest import types_to_check @@ -52,7 +52,7 @@ def torque_rmul_type_error(request): @fixture(params = [type_to_check for type_to_check in types_to_check if not isinstance(type_to_check, float) and not isinstance(type_to_check, int) and not isinstance(type_to_check, InertiaMoment) - and not isinstance(type_to_check, Torque)]) + and not isinstance(type_to_check, Torque) and not isinstance(type_to_check, Length)]) def torque_truediv_type_error(request): return request.param diff --git a/tests/test_units/test_torque/test_torque.py b/tests/test_units/test_torque/test_torque.py index 04213e7..c3c416c 100644 --- a/tests/test_units/test_torque/test_torque.py +++ b/tests/test_units/test_torque/test_torque.py @@ -52,6 +52,20 @@ def test_method(self, value, unit): assert str(torque) == f'{value} {unit}' +@mark.units +class TestTorqueAbs: + + + @mark.genuine + @given(value = floats(allow_nan = False, allow_infinity = False, min_value = -1000, max_value = 1000), + unit = sampled_from(elements = units_list)) + @settings(max_examples = 100) + def test_method(self, value, unit): + torque = Torque(value = value, unit = unit) + + assert abs(torque) == Torque(value = abs(value), unit = unit) + assert abs(torque).value >= 0 + @mark.units class TestTorqueAdd: diff --git a/tests/test_utils/conftest.py b/tests/test_utils/conftest.py index 42e3146..90c9e2a 100644 --- a/tests/test_utils/conftest.py +++ b/tests/test_utils/conftest.py @@ -1,15 +1,15 @@ from gearpy.mechanical_object import GearBase, MotorBase, Flywheel from pytest import fixture -from tests.conftest import types_to_check, basic_spur_gear +from tests.conftest import types_to_check, basic_spur_gear_1 -add_gear_mating_type_error_1 = [{'master': type_to_check, 'slave': basic_spur_gear, 'efficiency': 0.5} +add_gear_mating_type_error_1 = [{'master': type_to_check, 'slave': basic_spur_gear_1, 'efficiency': 0.5} for type_to_check in types_to_check if not isinstance(type_to_check, GearBase)] -add_gear_mating_type_error_2 = [{'master': basic_spur_gear, 'slave': type_to_check, 'efficiency': 0.5} +add_gear_mating_type_error_2 = [{'master': basic_spur_gear_1, 'slave': type_to_check, 'efficiency': 0.5} for type_to_check in types_to_check if not isinstance(type_to_check, GearBase)] -add_gear_mating_type_error_3 = [{'master': basic_spur_gear, 'slave': basic_spur_gear, 'efficiency': type_to_check} +add_gear_mating_type_error_3 = [{'master': basic_spur_gear_1, 'slave': basic_spur_gear_1, 'efficiency': type_to_check} for type_to_check in types_to_check if not isinstance(type_to_check, float) and not isinstance(type_to_check, int) and not isinstance(type_to_check, bool)] @@ -20,16 +20,16 @@ def add_gear_mating_type_error(request): return request.param -@fixture(params = [-1, 2]) +@fixture(params = [-1, 2, 0.5]) def add_gear_mating_value_error(request): return request.param -add_fixed_joint_type_error_1 = [{'master': type_to_check, 'slave': basic_spur_gear} for type_to_check in types_to_check +add_fixed_joint_type_error_1 = [{'master': type_to_check, 'slave': basic_spur_gear_1} for type_to_check in types_to_check if not isinstance(type_to_check, GearBase) and not isinstance(type_to_check, MotorBase) and not isinstance(type_to_check, Flywheel)] -add_fixed_joint_type_error_2 = [{'master': basic_spur_gear, 'slave': type_to_check} for type_to_check in types_to_check +add_fixed_joint_type_error_2 = [{'master': basic_spur_gear_1, 'slave': type_to_check} for type_to_check in types_to_check if not isinstance(type_to_check, GearBase) and not isinstance(type_to_check, Flywheel)] @fixture(params = [*add_fixed_joint_type_error_1, diff --git a/tests/test_utils/test_utils.py b/tests/test_utils/test_utils.py index cb766c4..356d3a6 100644 --- a/tests/test_utils/test_utils.py +++ b/tests/test_utils/test_utils.py @@ -1,10 +1,10 @@ -from gearpy.mechanical_object import SpurGear -from gearpy.units import InertiaMoment +from gearpy.mechanical_object import SpurGear, MatingMaster, MatingSlave +from gearpy.units import InertiaMoment, Length from gearpy.utils import add_gear_mating, add_fixed_joint from hypothesis import given, settings from hypothesis.strategies import floats, one_of from pytest import mark, raises -from tests.conftest import dc_motors, spur_gears, flywheels +from tests.conftest import dc_motors, simple_spur_gears, flywheels @mark.utils @@ -12,8 +12,8 @@ class TestAddGearMating: @mark.genuine - @given(gear_1 = spur_gears(), - gear_2 = spur_gears(), + @given(gear_1 = simple_spur_gears(), + gear_2 = simple_spur_gears(), efficiency = floats(allow_nan = False, allow_infinity = False, min_value = 0, exclude_min = False, max_value = 1, exclude_max = False)) @settings(max_examples = 100) @@ -21,7 +21,9 @@ def test_function(self, gear_1, gear_2, efficiency): add_gear_mating(master = gear_1, slave = gear_2, efficiency = efficiency) assert gear_1.drives == gear_2 + assert gear_1.mating_role == MatingMaster assert gear_2.driven_by == gear_1 + assert gear_2.mating_role == MatingSlave assert gear_2.master_gear_ratio == gear_2.n_teeth/gear_1.n_teeth assert gear_2.master_gear_efficiency == efficiency @@ -34,10 +36,15 @@ def test_raises_type_error(self, add_gear_mating_type_error): @mark.error def test_raises_value_error(self, add_gear_mating_value_error): - gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, inertia_moment = InertiaMoment(1, 'kgm^2')) - gear_2 = SpurGear(name = 'gear 2', n_teeth = 20, inertia_moment = InertiaMoment(1, 'kgm^2')) - with raises(ValueError): - add_gear_mating(master = gear_1, slave = gear_2, efficiency = add_gear_mating_value_error) + gear_1 = SpurGear(name = 'gear 1', n_teeth = 10, module = Length(1, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) + gear_2 = SpurGear(name = 'gear 2', n_teeth = 20, module = Length(1, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) + gear_3 = SpurGear(name = 'gear 3', n_teeth = 20, module = Length(2, 'mm'), inertia_moment = InertiaMoment(1, 'kgm^2')) + if add_gear_mating_value_error < 0 or add_gear_mating_value_error > 1: + with raises(ValueError): + add_gear_mating(master = gear_1, slave = gear_2, efficiency = add_gear_mating_value_error) + else: + with raises(ValueError): + add_gear_mating(master = gear_1, slave = gear_3, efficiency = add_gear_mating_value_error) @mark.utils @@ -45,8 +52,8 @@ class TestAddFixedJoint: @mark.genuine - @given(master = one_of(dc_motors(), spur_gears(), flywheels()), - slave = one_of(spur_gears(), flywheels())) + @given(master = one_of(dc_motors(), simple_spur_gears(), flywheels()), + slave = one_of(simple_spur_gears(), flywheels())) @settings(max_examples = 100) def test_function(self, master, slave): add_fixed_joint(master = master, slave = slave) diff --git a/tox.ini b/tox.ini index 4b361ed..835bd28 100644 --- a/tox.ini +++ b/tox.ini @@ -4,12 +4,8 @@ skipsdist = True [testenv] deps = - pytest - hypothesis - coverage - pytest-cov - pytest-xdist - -rrequirements.txt + -rrequirements/common.txt + -rrequirements/tests.txt commands = python -m pytest --verbose --cov=gearpy --cov-report=html -nauto