BicycleParameters 1.1.0__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0/BicycleParameters.egg-info}/PKG-INFO +11 -3
  2. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/BicycleParameters.egg-info/SOURCES.txt +1 -0
  3. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/CHANGELOG.rst +24 -0
  4. {bicycleparameters-1.1.0/BicycleParameters.egg-info → bicycleparameters-1.2.0}/PKG-INFO +11 -3
  5. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/README.rst +4 -1
  6. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/RELEASE.rst +2 -2
  7. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/com.py +1 -1
  8. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/geometry.py +2 -2
  9. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/inertia.py +1 -1
  10. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/io.py +16 -16
  11. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/main.py +21 -32
  12. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/models.py +29 -6
  13. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/period.py +46 -59
  14. bicycleparameters-1.2.0/bicycleparameters/tests/test_io.py +26 -0
  15. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/version.py +1 -1
  16. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/docs/data.rst +26 -4
  17. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/docs/examples.rst +14 -0
  18. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/setup.py +9 -1
  19. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/AUTHORS +0 -0
  20. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/BicycleParameters.egg-info/dependency_links.txt +0 -0
  21. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/BicycleParameters.egg-info/entry_points.txt +0 -0
  22. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/BicycleParameters.egg-info/requires.txt +0 -0
  23. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/BicycleParameters.egg-info/top_level.txt +0 -0
  24. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/LICENSE.txt +0 -0
  25. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/MANIFEST.in +0 -0
  26. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/__init__.py +0 -0
  27. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Benchmark/Parameters/BenchmarkBenchmark.txt +0 -0
  28. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Browser/Parameters/BrowserBenchmark.txt +0 -0
  29. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Browserins/Parameters/BrowserinsBenchmark.txt +0 -0
  30. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Crescendo/Parameters/CrescendoBenchmark.txt +0 -0
  31. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Fisher/Parameters/FisherBenchmark.txt +0 -0
  32. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Pista/Parameters/PistaBenchmark.txt +0 -0
  33. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Rigid/Parameters/RigidBenchmark.txt +0 -0
  34. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Silver/Parameters/SilverBenchmark.txt +0 -0
  35. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Yellow/Parameters/YellowBenchmark.txt +0 -0
  36. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app-data/bicycles/Yellowrev/Parameters/YellowrevBenchmark.txt +0 -0
  37. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/app.py +0 -0
  38. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/assets/app-explanation.md +0 -0
  39. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/assets/styles.css +0 -0
  40. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/bicycle.py +0 -0
  41. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/conversions.py +0 -0
  42. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/parameter_dicts.py +0 -0
  43. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/parameter_sets.py +0 -0
  44. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/plot.py +0 -0
  45. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/rider.py +0 -0
  46. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tables.py +0 -0
  47. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/__init__.py +0 -0
  48. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/benchmark-benchmark.yml +0 -0
  49. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/benchmark-browser.yml +0 -0
  50. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/benchmark-extendedoptc.yml +0 -0
  51. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/benchmark-pista.yml +0 -0
  52. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/benchmark-pistarider.yml +0 -0
  53. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/benchmark-pistarideroptimized3ms.yml +0 -0
  54. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/benchmark-realizedopttwo.yml +0 -0
  55. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/principal-browserjason.yml +0 -0
  56. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/parameter_sets/principal-extendedoptf.yml +0 -0
  57. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/test_bicycle.py +0 -0
  58. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/test_bicycleparameters.py +0 -0
  59. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/test_models.py +0 -0
  60. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/bicycleparameters/tests/test_parameter_sets.py +0 -0
  61. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/docs/app.rst +0 -0
  62. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/docs/bicycleparameters.rst +0 -0
  63. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/docs/description.rst +0 -0
  64. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/docs/index.rst +0 -0
  65. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/docs/installation.rst +0 -0
  66. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/requirements.txt +0 -0
  67. {bicycleparameters-1.1.0 → bicycleparameters-1.2.0}/setup.cfg +0 -0
@@ -1,17 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: BicycleParameters
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Generates and manipulates the physical parameters of a bicycle.
5
5
  Home-page: http://pypi.python.org/pypi/BicycleParameters
6
- Author: Jason Keith Moore
6
+ Author: Jason K. Moore
7
7
  Author-email: moorepants@gmail.com
8
8
  License: LICENSE.txt
9
+ Project-URL: Web Application, https://bicycle-dynamics.onrender.com
10
+ Project-URL: Documentation, http://bicycleparameters.readthedocs.io
11
+ Project-URL: Source Code, https://github.com/moorepants/BicycleParameters
12
+ Project-URL: Issue Tracker, https://github.com/moorepants/BicycleParameters/issues
9
13
  Classifier: Programming Language :: Python
10
14
  Classifier: Programming Language :: Python :: 3.8
11
15
  Classifier: Programming Language :: Python :: 3.9
12
16
  Classifier: Programming Language :: Python :: 3.10
13
17
  Classifier: Programming Language :: Python :: 3.11
14
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
15
20
  Classifier: Operating System :: OS Independent
16
21
  Classifier: Development Status :: 5 - Production/Stable
17
22
  Classifier: Intended Audience :: Science/Research
@@ -55,7 +60,7 @@ of the Whipple-Carvallo bicycle model.
55
60
  * - CI Status
56
61
  - |GHCI|
57
62
  * - Render App
58
- - `Bicycle Dynamics App <https://bicycle-dynamics.onrender.com>`_
63
+ - |Render|
59
64
 
60
65
  .. |PyPi| image:: https://img.shields.io/pypi/v/BicycleParameters.svg
61
66
  :target: https://pypi.org/project/BicycleParameters/
@@ -69,6 +74,9 @@ of the Whipple-Carvallo bicycle model.
69
74
  :target: https://bicycleparameters.readthedocs.io/en/latest/?badge=latest
70
75
  :alt: Documentation Status
71
76
 
77
+ .. |Render| image:: https://img.shields.io/badge/Bicycle_Dynamics_App-Render.io-blue
78
+ :target: https://bicycle-dynamics.onrender.com
79
+
72
80
  Dependencies
73
81
  ============
74
82
 
@@ -44,6 +44,7 @@ bicycleparameters/assets/styles.css
44
44
  bicycleparameters/tests/__init__.py
45
45
  bicycleparameters/tests/test_bicycle.py
46
46
  bicycleparameters/tests/test_bicycleparameters.py
47
+ bicycleparameters/tests/test_io.py
47
48
  bicycleparameters/tests/test_models.py
48
49
  bicycleparameters/tests/test_parameter_sets.py
49
50
  bicycleparameters/tests/parameter_sets/benchmark-benchmark.yml
@@ -1,6 +1,30 @@
1
1
  Release Notes
2
2
  =============
3
3
 
4
+ 1.2.0
5
+ -----
6
+
7
+ - Support Python 3.13.
8
+ - Revert to ``unumpy.matrix`` in mass center and inertia calculations.
9
+ - Updated the raw data mat file loading to work with current Python versions.
10
+ - Support raw data pendulum measurement files with time as an array as well as
11
+ a fixed sample rate (as before).
12
+ - Remove the unnecessary ``__new__`` method from ``Bicycle``.
13
+ - Added options to show stable ranges and to hide zero eigenvalues in the
14
+ ``Model`` eigenvalues parts plot.
15
+ - Skip data truncation on raw pendulum time series that have the explicit time
16
+ array (this is a hack for the Balanceassistv1 data to have valid handlebar
17
+ inertia).
18
+ - Modernized some internal plotting code.
19
+ - Added measured and calculated parameter data for the Balanceassistv1 bike we
20
+ measured in the summer of 2024. This is a Gazelle Grenoble C8 HMB electric
21
+ bicycle with a custom headtube with a steering motor.
22
+
23
+ 1.1.1
24
+ -----
25
+
26
+ - Ensure that the app's data files are installed.
27
+
4
28
  1.1.0
5
29
  -----
6
30
 
@@ -1,17 +1,22 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: BicycleParameters
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: Generates and manipulates the physical parameters of a bicycle.
5
5
  Home-page: http://pypi.python.org/pypi/BicycleParameters
6
- Author: Jason Keith Moore
6
+ Author: Jason K. Moore
7
7
  Author-email: moorepants@gmail.com
8
8
  License: LICENSE.txt
9
+ Project-URL: Web Application, https://bicycle-dynamics.onrender.com
10
+ Project-URL: Documentation, http://bicycleparameters.readthedocs.io
11
+ Project-URL: Source Code, https://github.com/moorepants/BicycleParameters
12
+ Project-URL: Issue Tracker, https://github.com/moorepants/BicycleParameters/issues
9
13
  Classifier: Programming Language :: Python
10
14
  Classifier: Programming Language :: Python :: 3.8
11
15
  Classifier: Programming Language :: Python :: 3.9
12
16
  Classifier: Programming Language :: Python :: 3.10
13
17
  Classifier: Programming Language :: Python :: 3.11
14
18
  Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
15
20
  Classifier: Operating System :: OS Independent
16
21
  Classifier: Development Status :: 5 - Production/Stable
17
22
  Classifier: Intended Audience :: Science/Research
@@ -55,7 +60,7 @@ of the Whipple-Carvallo bicycle model.
55
60
  * - CI Status
56
61
  - |GHCI|
57
62
  * - Render App
58
- - `Bicycle Dynamics App <https://bicycle-dynamics.onrender.com>`_
63
+ - |Render|
59
64
 
60
65
  .. |PyPi| image:: https://img.shields.io/pypi/v/BicycleParameters.svg
61
66
  :target: https://pypi.org/project/BicycleParameters/
@@ -69,6 +74,9 @@ of the Whipple-Carvallo bicycle model.
69
74
  :target: https://bicycleparameters.readthedocs.io/en/latest/?badge=latest
70
75
  :alt: Documentation Status
71
76
 
77
+ .. |Render| image:: https://img.shields.io/badge/Bicycle_Dynamics_App-Render.io-blue
78
+ :target: https://bicycle-dynamics.onrender.com
79
+
72
80
  Dependencies
73
81
  ============
74
82
 
@@ -16,7 +16,7 @@ of the Whipple-Carvallo bicycle model.
16
16
  * - CI Status
17
17
  - |GHCI|
18
18
  * - Render App
19
- - `Bicycle Dynamics App <https://bicycle-dynamics.onrender.com>`_
19
+ - |Render|
20
20
 
21
21
  .. |PyPi| image:: https://img.shields.io/pypi/v/BicycleParameters.svg
22
22
  :target: https://pypi.org/project/BicycleParameters/
@@ -30,6 +30,9 @@ of the Whipple-Carvallo bicycle model.
30
30
  :target: https://bicycleparameters.readthedocs.io/en/latest/?badge=latest
31
31
  :alt: Documentation Status
32
32
 
33
+ .. |Render| image:: https://img.shields.io/badge/Bicycle_Dynamics_App-Render.io-blue
34
+ :target: https://bicycle-dynamics.onrender.com
35
+
33
36
  Dependencies
34
37
  ============
35
38
 
@@ -2,11 +2,11 @@ These are the instructions for updating the package.
2
2
 
3
3
  1. Merge all commits from changes.
4
4
  2. Update the version info in the README
5
- 3. Update the version number in bicycleparameters.__init__.py
5
+ 3. Update the version number in bicycleparameters/version.py
6
6
  4. Update the version number in docs/conf.py.
7
7
  5. Make sure the docs build.
8
8
  6. Git tag for the new version.
9
- 7. python setup.py regsiter sdist upload
9
+ 7. python setup.py register sdist upload
10
10
  8. Upload the Documentation files.
11
11
 
12
12
  - cd docs/_build/html
@@ -91,7 +91,7 @@ def center_of_mass(slopes, intercepts):
91
91
  # for each line intersection...
92
92
  for j, row in enumerate(comb):
93
93
  sl = np.array([slopes[row[0]], slopes[row[1]]])
94
- a = unumpy.array(np.vstack((-sl, np.ones((2)))).T)
94
+ a = unumpy.matrix(np.vstack((-sl, np.ones((2)))).T)
95
95
  b = np.array([intercepts[row[0]], intercepts[row[1]]])
96
96
  lineX[j] = np.dot(a.I, b)
97
97
  com = np.mean(lineX, axis=0)
@@ -82,11 +82,11 @@ def calculate_abc_geometry(h, d):
82
82
  '''
83
83
  # extract the values
84
84
  h1, h2, h3, h4, h5 = h
85
- d1, d2, d3, d4, d = d
85
+ d1, d2, d3, d4, d_ = d
86
86
  # get the perpendicular distances
87
87
  a = h1 + h2 - h3 + .5 * d1 - .5 * d2
88
88
  b = h4 - .5 * d3 - h5 + .5 * d4
89
- c = umath.sqrt(-(a - b)**2 + (d + .5 * (d2 + d3))**2)
89
+ c = umath.sqrt(-(a - b)**2 + (d_ + .5 * (d2 + d3))**2)
90
90
  return a, b, c
91
91
 
92
92
 
@@ -114,7 +114,7 @@ def inertia_components(jay, beta):
114
114
  '''
115
115
  sb = unumpy.sin(beta)
116
116
  cb = unumpy.cos(beta)
117
- betaMat = unumpy.array(np.vstack((cb**2, -2 * sb * cb, sb**2)).T)
117
+ betaMat = unumpy.matrix(np.vstack((cb**2, -2 * sb * cb, sb**2)).T)
118
118
  eye = np.squeeze(np.asarray(np.dot(betaMat.I, jay)))
119
119
  return eye
120
120
 
@@ -79,23 +79,23 @@ def load_pendulum_mat_file(pathToFile):
79
79
 
80
80
  '''
81
81
  pendDat = {}
82
- loadmat(pathToFile, mdict=pendDat)
82
+ loadmat(pathToFile, mdict=pendDat, squeeze_me=True, chars_as_strings=True)
83
83
  # clean up the matlab imports
84
- del(pendDat['__globals__'], pendDat['__header__'], pendDat['__version__'])
85
- for k, v in pendDat.items():
86
- try:
87
- # change to an ascii string
88
- pendDat[k] = v[0].encode('ascii')
89
- except:
90
- # if an array of a single number
91
- if np.shape(v)[0] == 1:
92
- pendDat[k] = v[0][0]
93
- # else if the notes are empty
94
- elif np.shape(v)[0] == 0:
95
- pendDat[k] = ''
96
- # else it is the data which needs to be a one dimensional array
97
- else:
98
- pendDat[k] = v.reshape((len(v),))
84
+ del pendDat['__globals__']
85
+ del pendDat['__header__']
86
+ del pendDat['__version__']
87
+ # If notes is empty it loads like `array([], dtype='<U1')`, so make it an
88
+ # empty string.
89
+ if not isinstance(pendDat['notes'], str):
90
+ if pendDat['notes'].shape == (0,):
91
+ pendDat['notes'] = ''
92
+ # If a time array is present, it may be variable sample rate so delete the
93
+ # sampleRate and duration entries if they are there.
94
+ if 'time' in pendDat:
95
+ if 'sampleRate' in pendDat:
96
+ del pendDat['sampleRate']
97
+ if 'duration' in pendDat:
98
+ del pendDat['duration']
99
99
  return pendDat
100
100
 
101
101
 
@@ -40,29 +40,7 @@ class Bicycle(object):
40
40
 
41
41
  """
42
42
 
43
- def __new__(cls, bicycleName, pathToData='.', forceRawCalc=False,
44
- forcePeriodCalc=False):
45
- '''Returns a NoneType object if there is no directory for the
46
- bicycle.'''
47
- # is there a data directory for this bicycle? if not, tell the user to
48
- # put some data in the folder so we have something to work with!
49
- try:
50
- pathToBicycle = os.path.join(pathToData, 'bicycles', bicycleName)
51
- if os.path.isdir(pathToBicycle):
52
- print("We have foundeth a directory named: " +
53
- "{0}.".format(pathToBicycle))
54
- return super(Bicycle, cls).__new__(cls)
55
- else:
56
- raise ValueError
57
- except:
58
- mes = """Are you nuts?! Make a directory called '{0}' with basic
59
- data for your bicycle in this directory: '{1}'. Then I can actually create a
60
- bicycle object. You may either need to change to the correct directory or reset
61
- the pathToData argument.""".format(bicycleName, pathToData)
62
- print(mes)
63
- return None
64
-
65
- def __init__(self, bicycleName, pathToData='.', forceRawCalc=False,
43
+ def __init__(self, bicycleName, pathToData=os.curdir, forceRawCalc=False,
66
44
  forcePeriodCalc=False):
67
45
  """
68
46
  Creates a bicycle object and sets the parameters based on the available
@@ -73,8 +51,8 @@ the pathToData argument.""".format(bicycleName, pathToData)
73
51
  bicycleName : string
74
52
  The short name of your bicicleta. It should be one word with the
75
53
  first letter capitalized and all other letters lower case. You
76
- should have a matching directory under `<pathToData>/bicycles/`.
77
- For example: `<pathToData>/bicycles/Shortname`.
54
+ should have a matching directory under ``<pathToData>/bicycles/``.
55
+ For example: ``<pathToData>/bicycles/Shortname``.
78
56
  pathToData : string
79
57
  This is the path to the folder where the bicycle/rider parameters
80
58
  and raw data are stored. The default is the current working
@@ -96,6 +74,7 @@ the pathToData argument.""".format(bicycleName, pathToData)
96
74
  pathToBicycles = os.path.join(pathToData, 'bicycles')
97
75
  # the directory where the files for this bicycle are stored
98
76
  self.directory = os.path.join(pathToBicycles, bicycleName)
77
+ self._check_for_bicycle_directory()
99
78
 
100
79
  # bicycles are assumed not to have a rider when initially loaded
101
80
  self.hasRider = False
@@ -145,7 +124,7 @@ the pathToData argument.""".format(bicycleName, pathToData)
145
124
  if conOne or conTwo:
146
125
  print("Recalcuting the parameters.")
147
126
  par, extras = self.calculate_from_measured(
148
- forcePeriodCalc=forcePeriodCalc)
127
+ forcePeriodCalc=forcePeriodCalc)
149
128
  self.parameters['Benchmark'] = par
150
129
  self.extras = extras
151
130
  print("The glory of the %s parameters are upon you!"
@@ -160,7 +139,7 @@ the pathToData argument.""".format(bicycleName, pathToData)
160
139
  'Measured.txt')
161
140
  try:
162
141
  self.parameters['Measured'] = \
163
- io.load_parameter_text_file(pathToRawFile)
142
+ io.load_parameter_text_file(pathToRawFile)
164
143
  except IOError:
165
144
  pass
166
145
  else:
@@ -169,6 +148,17 @@ the pathToData argument.""".format(bicycleName, pathToData)
169
148
  bicycle/{sn}/RawData/ with pendulum data mat files and the
170
149
  {sn}Measured.txt file'''.format(sn=bicycleName))
171
150
 
151
+ def _check_for_bicycle_directory(self):
152
+ # is there a data directory for this bicycle? if not, tell the user to
153
+ # put some data in the folder so we have something to work with!
154
+ msg = ("Are you nuts?! Make a directory called '{0}' with basic data "
155
+ "for your bicycle in this directory: '{1}'. Then I can "
156
+ "actually create a bicycle object. You may either need to "
157
+ "change to the correct directory or reset the pathToData "
158
+ "argument.")
159
+ if not os.path.isdir(self.directory):
160
+ raise ValueError(msg.format(self.bicycleName, self.directory))
161
+
172
162
  def __str__(self):
173
163
  if self.hasRider:
174
164
  desc = "{0} with {1} on board.".format(self.bicycleName,
@@ -1171,7 +1161,7 @@ the pathToData argument.""".format(bicycleName, pathToData)
1171
1161
 
1172
1162
  Parameters
1173
1163
  ----------
1174
- speeds : ndarray, shape (n,) or float
1164
+ speeds : array_like, shape (n,) or float
1175
1165
  The speed at which to calculate the eigenvalues.
1176
1166
 
1177
1167
  Returns
@@ -1188,11 +1178,10 @@ the pathToData argument.""".format(bicycleName, pathToData)
1188
1178
  model.
1189
1179
 
1190
1180
  '''
1191
- # this allows you to enter a float
1192
- try:
1193
- speeds.shape
1194
- except AttributeError:
1181
+ if isinstance(speeds, float):
1195
1182
  speeds = np.array([speeds])
1183
+ else:
1184
+ speeds = np.asarray(speeds)
1196
1185
 
1197
1186
  par = io.remove_uncertainties(self.parameters['Benchmark'])
1198
1187
 
@@ -20,7 +20,7 @@ class _Model(ABC):
20
20
 
21
21
 
22
22
  class Meijaard2007Model(_Model):
23
- """Whipple-Carvallo model presented in [Meijaard2007]_. It is both linear
23
+ """Carvallo-Whipple model presented in [Meijaard2007]_. It is both linear
24
24
  and the minimal model in terms of states and coordinates that fully
25
25
  describe the vehicle's dynamics: self-stability and non-minimum phase
26
26
  behavior.
@@ -549,6 +549,7 @@ class Meijaard2007Model(_Model):
549
549
  return axes
550
550
 
551
551
  def plot_eigenvalue_parts(self, ax=None, colors=None,
552
+ show_stable_regions=True, hide_zeros=False,
552
553
  **parameter_overrides):
553
554
  """Returns a matplotlib axis of the real and imaginary parts of the
554
555
  eigenvalues plotted against the provided parameter.
@@ -559,6 +560,11 @@ class Meijaard2007Model(_Model):
559
560
  Matplotlib axes.
560
561
  colors : sequence, len(4)
561
562
  Matplotlib colors for the 4 modes.
563
+ show_stable_regions : boolean, optional
564
+ If true, a grey shaded background will indicate stable regions.
565
+ hide_zeros : boolean or float, optional
566
+ If true, real or imaginary parts that are smaller than 1e-12 will
567
+ not be plotted. Providing a float will set the tolerance.
562
568
  **parameter_overrides : dictionary
563
569
  Parameter keys that map to floats or array_like of floats
564
570
  shape(n,). All keys that map to array_like must be of the same
@@ -588,7 +594,9 @@ class Meijaard2007Model(_Model):
588
594
  if len(evals.shape) > 1:
589
595
  evals, evecs = sort_eigenmodes(evals, evecs)
590
596
  else:
591
- evals, evecs = [evals], [evecs]
597
+ evals, evecs = np.array([evals]), np.array([evecs])
598
+
599
+ tol = hide_zeros if isinstance(hide_zeros, float) else 1e-12
592
600
 
593
601
  par, array_keys, _ = self._parse_parameter_overrides(
594
602
  **parameter_overrides)
@@ -597,15 +605,30 @@ class Meijaard2007Model(_Model):
597
605
  colors = ['C0', 'C1', 'C2', 'C3']
598
606
  legend = ['Mode 1', 'Mode 2', 'Mode 3', 'Mode 4',
599
607
  'Mode 1', 'Mode 2', 'Mode 3', 'Mode 4']
608
+
609
+ if show_stable_regions:
610
+ ax.fill_between(par[array_keys[0]],
611
+ np.min([np.min(evals.real), np.min(evals.imag)]),
612
+ np.max([np.max(evals.real), np.max(evals.imag)]),
613
+ where=np.all(evals.real < 0.0, axis=1),
614
+ color='grey',
615
+ alpha=0.25,
616
+ transform=ax.get_xaxis_transform())
617
+
600
618
  # imaginary components
601
619
  for eval_sequence, color, label in zip(evals.T, colors, legend):
602
- ax.plot(par[array_keys[0]], np.abs(np.imag(eval_sequence)),
603
- color=color, label=label, linestyle='--')
620
+ imag_vals = np.abs(np.imag(eval_sequence))
621
+ if hide_zeros:
622
+ imag_vals[np.abs(imag_vals) < tol] = np.nan
623
+ ax.plot(par[array_keys[0]], imag_vals, color=color, label=label,
624
+ linestyle='--')
604
625
 
605
626
  # plot the real parts of the eigenvalues
606
627
  for eval_sequence, color, label in zip(evals.T, colors, legend):
607
- ax.plot(par[array_keys[0]], np.real(eval_sequence), color=color,
608
- label=label)
628
+ real_vals = np.real(eval_sequence)
629
+ if hide_zeros:
630
+ real_vals[np.abs(real_vals) < tol] = np.nan
631
+ ax.plot(par[array_keys[0]], real_vals, color=color, label=label)
609
632
 
610
633
  # set labels and limits
611
634
  ax.set_ylabel('Real and Imaginary Parts of the Eigenvalue [1/s]')
@@ -87,22 +87,9 @@ def calc_periods_for_files(directory, filenames, forkIsSplit):
87
87
 
88
88
  periods = {}
89
89
 
90
- def pathParts(path):
91
- '''Splits a path into a list of its parts.'''
92
- components = []
93
- while True:
94
- (path, tail) = os.path.split(path)
95
- if tail == "":
96
- components.reverse()
97
- return components
98
- components.append(tail)
99
-
100
- pathToRawDataParts = pathParts(directory)
101
- pathToRawDataParts.pop()
102
- pathToBicycleDir = os.path.join(pathToRawDataParts[0],
103
- pathToRawDataParts[1],
104
- pathToRawDataParts[2])
105
- pathToPlotDir = os.path.join(pathToBicycleDir, 'Plots', 'PendulumFit')
90
+ # directory is /path/to/data/bicycles/BikeName/RawData
91
+ path_to_bicycle_dir = os.sep.join(directory.split(os.sep)[:-1])
92
+ pathToPlotDir = os.path.join(path_to_bicycle_dir, 'Plots', 'PendulumFit')
106
93
 
107
94
  # make sure there is a place to save the plots
108
95
  if not os.path.exists(pathToPlotDir):
@@ -116,12 +103,17 @@ def calc_periods_for_files(directory, filenames, forkIsSplit):
116
103
  # generate a variable name for this period
117
104
  periodKey = get_period_key(matData, forkIsSplit)
118
105
  # calculate the period
119
- sampleRate = get_sample_rate(matData)
120
106
  pathToPlotFile = os.path.join(pathToPlotDir,
121
107
  os.path.splitext(f)[0] + '.png')
122
- period = get_period_from_truncated(matData['data'],
123
- sampleRate,
124
- pathToPlotFile)
108
+ if 'time' in matData:
109
+ period = get_period_from_truncated(matData['data'],
110
+ matData['time'],
111
+ pathToPlotFile)
112
+ else:
113
+ sampleRate = get_sample_rate(matData)
114
+ period = get_period_from_truncated(matData['data'],
115
+ sampleRate,
116
+ pathToPlotFile)
125
117
  print("The period is:", period, "\n")
126
118
  # either append the the period or if it isn't there yet, then
127
119
  # make a new list
@@ -212,16 +204,17 @@ def fit_goodness(ym, yp):
212
204
  return rsq, SSE, SST, SSR
213
205
 
214
206
 
215
- def get_period(data, sampleRate, pathToPlotFile):
207
+ def get_period(data, sample_rate_or_time, pathToPlotFile):
216
208
  '''Returns the period and uncertainty for data resembling a decaying
217
209
  oscillation.
218
210
 
219
211
  Parameters
220
212
  ----------
221
- data : ndarray, shape(n,)
213
+ data : array_like, shape(n,)
222
214
  A time series that resembles a decaying oscillation.
223
- sampleRate : int
224
- The frequency that data was sampled at.
215
+ sample_rate_or_time : int or array_like, shape(n,)
216
+ Either the frequency in Hertz that data was sampled at or a time array
217
+ that corresponds to ``data``.
225
218
  pathToPlotFile : string
226
219
  A path to the file to print the plots.
227
220
 
@@ -233,7 +226,12 @@ def get_period(data, sampleRate, pathToPlotFile):
233
226
  '''
234
227
 
235
228
  y = data
236
- x = np.linspace(0., (len(y) - 1) / float(sampleRate), num=len(y))
229
+ if isinstance(sample_rate_or_time, int):
230
+ sample_rate = sample_rate_or_time
231
+ x = np.linspace(0.0, (len(y) - 1)/float(sample_rate), num=len(y))
232
+ else:
233
+ x = sample_rate_or_time
234
+ sample_rate = int(1.0/np.mean(np.diff(x))) # approximate
237
235
 
238
236
  def fitfunc(p, t):
239
237
  '''Decaying oscillation function.'''
@@ -246,7 +244,7 @@ def get_period(data, sampleRate, pathToPlotFile):
246
244
  # initial guesses
247
245
  # p0 = np.array([1.35, -.5, -.75, 0.01, 3.93]) # guess from delft
248
246
  # p0 = np.array([2.5, -.75, -.75, 0.001, 4.3]) # guess from ucd
249
- p0 = make_guess(data, sampleRate) # tries to make a good guess
247
+ p0 = make_guess(data, sample_rate) # tries to make a good guess
250
248
 
251
249
  # create the error function
252
250
  errfunc = lambda p, t, y: fitfunc(p, t) - y
@@ -284,9 +282,9 @@ def get_period(data, sampleRate, pathToPlotFile):
284
282
  T = 1. / fd
285
283
 
286
284
  # plot the data and save it to file
287
- fig = plt.figure()
285
+ fig, ax = plt.subplots(layout='constrained')
288
286
  plot_osfit(x, y, lscurve, p1, rsq, T, m=np.max(x), fig=fig)
289
- plt.savefig(pathToPlotFile)
287
+ fig.savefig(pathToPlotFile)
290
288
  plt.close()
291
289
 
292
290
  # return the period
@@ -296,7 +294,15 @@ def get_period(data, sampleRate, pathToPlotFile):
296
294
  def get_period_from_truncated(data, sampleRate, pathToPlotFile):
297
295
  # dataRec = average_rectified_sections(data)
298
296
  dataRec = data
299
- dataGood = select_good_data(dataRec, 0.1)
297
+ # Don't truncate if the time array is provided in the measurements. This is
298
+ # present because truncating causes bad inertia values for the
299
+ # Balanceassistv1 fork. This is a bit of a hack. It would be better to have
300
+ # optional data processing steps per trials to maximize good period
301
+ # estimates. This is trying to be a catch all as designed.
302
+ if isinstance(sampleRate, np.ndarray): # time array
303
+ dataGood = dataRec
304
+ else:
305
+ dataGood = select_good_data(dataRec, 0.1)
300
306
  return get_period(dataGood, sampleRate, pathToPlotFile)
301
307
 
302
308
 
@@ -456,7 +462,7 @@ def plot_osfit(t, ym, yf, p, rsq, T, m=None, fig=None):
456
462
  The measured voltage
457
463
  yf : ndarray (n,)
458
464
  p : ndarray (5,)
459
- The fit parameters for the decaying osicallation fucntion
465
+ The fit parameters for the decaying oscillation function.
460
466
  rsq : float
461
467
  The r squared value of y (the fit)
462
468
  T : float
@@ -469,38 +475,20 @@ def plot_osfit(t, ym, yf, p, rsq, T, m=None, fig=None):
469
475
  fig : the figure
470
476
 
471
477
  '''
472
- # figure properties
473
- figwidth = 4. # in inches
474
- goldenMean = (np.sqrt(5) - 1.0) / 2.0
475
- figsize = [figwidth, figwidth * goldenMean]
476
- params = {#'backend': 'ps',
477
- 'axes.labelsize': 8,
478
- 'axes.titlesize': 8,
479
- 'text.fontsize': 8,
480
- 'legend.fontsize': 8,
481
- 'xtick.labelsize': 6,
482
- 'ytick.labelsize': 6,
483
- 'text.usetex': True,
484
- #'figure.figsize': figsize
485
- }
486
478
  if fig:
487
- fig = fig
479
+ ax1 = fig.axes[0]
488
480
  else:
489
- fig = plt.figure(2)
490
- fig.set_size_inches(figsize)
491
- plt.rcParams.update(params)
492
- ax1 = plt.axes([0.125, 0.125, 0.9-0.125, 0.65])
493
- #if m == None:
494
- #end = len(t)
495
- #else:
496
- #end = t[round(m/t[-1]*len(t))]
497
- ax1.plot(t, ym, '.', markersize=2)
481
+ fig, ax1 = plt.subplots(layout='constrained')
482
+ ax1.plot(t, ym, '.', markersize=4)
498
483
  plt.plot(t, yf, 'k-')
499
484
  plt.xlabel('Time [s]')
500
485
  plt.ylabel('Amplitude [V]')
501
- equation = r'$f(t)={0:1.2f}+e^{{-({3:1.3f})({4:1.1f})t}}\left[{1:1.2f}\sin{{\sqrt{{1-{3:1.3f}^2}}{4:1.1f}t}}+{2:1.2f}\cos{{\sqrt{{1-{3:1.3f}^2}}{4:1.1f}t}}\right]$'.format(p[0], p[1], p[2], p[3], p[4])
502
- rsquare = '$r^2={0:1.3f}$'.format(rsq)
503
- period = '$T={0} s$'.format(T)
486
+ equation = (r'$f(t)={0:1.2f}+e^{{-({3:1.3f})({4:1.1f})t}}\left[{1:1.2f}'
487
+ r'\sin{{\sqrt{{1-{3:1.3f}^2}}{4:1.1f}t}}+{2:1.2f}'
488
+ r'\cos{{\sqrt{{1-{3:1.3f}^2}}{4:1.1f}t}}\right]$')
489
+ equation = equation.format(p[0], p[1], p[2], p[3], p[4])
490
+ rsquare = r'$r^2={0:1.3f}$'.format(rsq)
491
+ period = r'$T={0} s$'.format(T)
504
492
  plt.title(equation + '\n' + rsquare + ', ' + period)
505
493
  plt.legend(['Measured', 'Fit'])
506
494
  if m is not None:
@@ -511,13 +499,12 @@ def plot_osfit(t, ym, yf, p, rsq, T, m=None, fig=None):
511
499
 
512
500
 
513
501
  def select_good_data(data, percent):
514
-
515
502
  '''Returns a slice of the data from the index at maximum value to the index
516
503
  at a percent of the maximum value.
517
504
 
518
505
  Parameters
519
506
  ----------
520
- data : ndarray, shape(1,)
507
+ data : ndarray, shape(n,)
521
508
  This should be a decaying function.
522
509
  percent : float
523
510
  The percent of the maximum to clip.
@@ -0,0 +1,26 @@
1
+ import os
2
+ import numpy as np
3
+ from bicycleparameters.io import load_pendulum_mat_file
4
+
5
+ TESTS_DIR = os.path.dirname(__file__)
6
+
7
+
8
+ def test_load_pendulum_mat_file():
9
+ path = os.path.join(TESTS_DIR, 'sample_data',
10
+ 'BrowserForkCompoundFirst1.mat')
11
+
12
+ d = load_pendulum_mat_file(path)
13
+
14
+ assert d['angle'] == 'First'
15
+ assert d['ActualRate'] == 1000
16
+ assert d['bicycle'] == 'BrowserIns'
17
+ assert d['data'].shape == (30000,)
18
+ np.testing.assert_allclose(
19
+ d['data'][0:5],
20
+ np.array([0.33034183, 0.32525861, 0.31509219, 0.31509219, 0.33034183]))
21
+ assert d['duration'] == 30
22
+ assert d['filename'] == 'BrowserInsForkCompoundFirst1'
23
+ assert d['notes'] == ''
24
+ assert d['part'] == 'Fork'
25
+ assert d['pendulum'] == 'Compound'
26
+ assert d['trial'] == '1'
@@ -1,2 +1,2 @@
1
- __version_info__ = (1, 1, 0)
1
+ __version_info__ = (1, 2, 0)
2
2
  __version__ = '.'.join(map(str, __version_info__))
@@ -211,14 +211,31 @@ Notes
211
211
 
212
212
  Pendulum Data Files
213
213
  ===================
214
+
214
215
  If you have raw signal data that the periods can be estimated from, then these
215
216
  should be included in the ``RawData`` directory. There should be at least one
216
217
  file for every period typically found in ``<bicycle name>Measured.txt`` file. The
217
218
  signals collected should exhibit very typical decayed oscillations. Currently
218
219
  the only supported file is a Matlab mat file with these variables:
219
220
 
220
- - ``data`` : signal vector of a decaying oscillation
221
- - ``sampleRate`` : sample rate of data in hertz
221
+ - ``bicycle``, char : short name of the bicycle, e.g. ``'Browser'``
222
+ - ``part``, char : examples are ``'Fork'``, ``'Frame'``, ``'Rwheel'``
223
+ - ``pendulum``, char : either ``'Compound'`` or ``'Torsional'``
224
+ - ``angle`` or ``angleOrder``, char : the ``'First'``, ``'Second'``, ...,
225
+ ``'Sixth'`` orientation
226
+ - ``trial``, char : the integer number of the repetition ``'1'``, ``'2'``, etc.
227
+ - ``filename``, char : filename constructed from above variables (see below for
228
+ explanation)
229
+ - ``notes``, char : any notes recorded for that measurement
230
+ - ``data``, double size n x 1 : signal vector of a decaying oscillation (from
231
+ rate gyro)
232
+ - Time information, either ``time`` OR both ``duration`` and ``sampleRate``:
233
+
234
+ - ``duration``, double size 1 x 1 : time in seconds for the measurement
235
+ - ``sampleRate`` or ``ActualRate``, double size 1 x 1 (integer) : sample
236
+ rate of data in Hertz
237
+ - ``time``, double size n x 1 : time values in seconds that correspond to
238
+ ``data``
222
239
 
223
240
  The files should be named in this manner ``<short
224
241
  name><part><pendulum><orientation><trial>.mat`` where:
@@ -230,6 +247,11 @@ name><part><pendulum><orientation><trial>.mat`` where:
230
247
  ``Fifth``, or ``Sixth``
231
248
  - ``<trial>`` is an integer greater than or equal to 1
232
249
 
250
+ These data files were originally collected with Matlab and a National
251
+ Instruments USB-6218 using a Silicon Sensing CRS03-04S single axis angular rate
252
+ gyro and a `data collection script
253
+ <https://github.com/moorepants/PhysicalParameters/blob/master/PhysicalParameters/matlab/acquire_data.m>`_.
254
+
233
255
  Notes
234
256
  -----
235
257
 
@@ -273,7 +295,7 @@ files.
273
295
  riders/<rider name>/RawData/
274
296
  ============================
275
297
 
276
- **These files must follow the YAML format used in the `yeadon package`_.**
298
+ **These files must follow the YAML format used in the yeadon package.**
277
299
 
278
300
  <rider name><bicycle name>YeadonCFG.txt
279
301
  ---------------------------------------
@@ -291,4 +313,4 @@ all of the geometric measurements of the rider. See the `yeadon documentation`_
291
313
  for more details.
292
314
 
293
315
  .. _yeadon package: http://pypi.python.org/pypi/yeadon
294
- .. _yeadon documentation : http://packages.python.org/yeadon/
316
+ .. _yeadon documentation : https://yeadon.readthedocs.io
@@ -617,6 +617,20 @@ plotted:
617
617
 
618
618
  model.plot_eigenvalue_parts(v=speeds)
619
619
 
620
+ There are several common customization options available:
621
+
622
+ .. plot::
623
+ :include-source: True
624
+ :context: close-figs
625
+
626
+ speeds = np.linspace(0.0, 10.0, num=200)
627
+
628
+ ax = model.plot_eigenvalue_parts(v=speeds,
629
+ colors=['C0', 'C0', 'C1', 'C2'],
630
+ show_stable_regions=False,
631
+ hide_zeros=True)
632
+ ax.set_ylim((-10.0, 10.0))
633
+
620
634
  You can choose any parameter in the dictionary to generate the root locus and
621
635
  also override other parameters.
622
636
 
@@ -7,13 +7,20 @@ exec(open('bicycleparameters/version.py').read())
7
7
  setup(
8
8
  name='BicycleParameters',
9
9
  version=__version__,
10
- author='Jason Keith Moore',
10
+ author='Jason K. Moore',
11
11
  author_email='moorepants@gmail.com',
12
12
  packages=find_packages(),
13
13
  url='http://pypi.python.org/pypi/BicycleParameters',
14
14
  license='LICENSE.txt',
15
15
  description='Generates and manipulates the physical parameters of a bicycle.',
16
16
  long_description=open('README.rst').read(),
17
+ project_urls={
18
+ 'Web Application': 'https://bicycle-dynamics.onrender.com',
19
+ 'Documentation': 'http://bicycleparameters.readthedocs.io',
20
+ 'Source Code': 'https://github.com/moorepants/BicycleParameters',
21
+ 'Issue Tracker': 'https://github.com/moorepants/BicycleParameters/issues',
22
+ },
23
+ include_package_data=True, # includes things in MANIFEST.in
17
24
  install_requires=[
18
25
  'DynamicistToolKit>=0.5.3',
19
26
  'matplotlib>=3.5.1',
@@ -43,6 +50,7 @@ setup(
43
50
  'Programming Language :: Python :: 3.10',
44
51
  'Programming Language :: Python :: 3.11',
45
52
  'Programming Language :: Python :: 3.12',
53
+ 'Programming Language :: Python :: 3.13',
46
54
  'Operating System :: OS Independent',
47
55
  'Development Status :: 5 - Production/Stable',
48
56
  'Intended Audience :: Science/Research',