├── MANIFEST.in
├── .flake8
├── examples
├── envs
│ ├── __init__.py
│ ├── assets
│ │ ├── ballonstring.xml
│ │ ├── arm.xml
│ │ └── hopper.xml
│ ├── muscled_hopper.py
│ ├── mujoco_env.py
│ └── pymunk_arm.py
├── minimal-physio-example.py
└── pymunk-gym-example.py
├── pymuscle
├── vis
│ ├── __init__.py
│ └── potvin_charts.py
├── __version__.py
├── model.py
├── __init__.py
├── hill_type.py
├── pymuscle_fibers.py
├── muscle.py
├── potvin_fuglevand_2017_motor_neuron_pool.py
└── potvin_fuglevand_2017_muscle_fibers.py
├── docs
├── src
│ ├── images
│ │ ├── motor-unit-diagram.png
│ │ ├── minimal-example-chart.png
│ │ └── minimal-physio-example-chart.png
│ └── readme-template.md
├── Makefile
├── index.rst
├── assemble.py
└── conf.py
├── tests
├── test_module.py
├── test_model.py
├── benchmarks
│ ├── util.py
│ └── bench_potvin_fuglevand_muscle.py
├── test_potvin_fuglevand_muscle.py
├── test_potvin_fuglevand_2017_muscle_fibers.py
├── test_pymuscle_fibers.py
├── test_potvin_fuglevand_2017_motor_neuron_pool.py
└── test_standard_muscle.py
├── .pyre_configuration
├── .travis.yml
├── requirements-dev.txt
├── .github
└── ISSUE_TEMPLATE
│ ├── feature_request.md
│ ├── enhancement-request.md
│ ├── research-submission.md
│ └── bug_report.md
├── .gitignore
├── LICENSE
├── setup.py
├── physio-readme.md
└── README.md
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.md LICENSE
2 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = D203, E125, E121, W503
3 |
--------------------------------------------------------------------------------
/examples/envs/__init__.py:
--------------------------------------------------------------------------------
1 | from .pymunk_arm import PymunkArmEnv # noqa
--------------------------------------------------------------------------------
/pymuscle/vis/__init__.py:
--------------------------------------------------------------------------------
1 | from .potvin_charts import PotvinChart # noqa: F401
2 |
--------------------------------------------------------------------------------
/docs/src/images/motor-unit-diagram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iandanforth/pymuscle/HEAD/docs/src/images/motor-unit-diagram.png
--------------------------------------------------------------------------------
/docs/src/images/minimal-example-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iandanforth/pymuscle/HEAD/docs/src/images/minimal-example-chart.png
--------------------------------------------------------------------------------
/pymuscle/__version__.py:
--------------------------------------------------------------------------------
1 | """
2 | PyMuscle Library
3 | """
4 | VERSION = (0, 1, 2)
5 |
6 | __version__ = '.'.join(map(str, VERSION))
7 |
--------------------------------------------------------------------------------
/docs/src/images/minimal-physio-example-chart.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/iandanforth/pymuscle/HEAD/docs/src/images/minimal-physio-example-chart.png
--------------------------------------------------------------------------------
/tests/test_module.py:
--------------------------------------------------------------------------------
1 | import re
2 | import pymuscle
3 |
4 |
5 | def test_version():
6 | assert re.match(r'\d\.\d\.\d', pymuscle.__version__)
7 |
--------------------------------------------------------------------------------
/.pyre_configuration:
--------------------------------------------------------------------------------
1 | {
2 | "binary": "/Users/iandanforth/.pyenv/versions/3.6.4/bin/pyre.bin",
3 | "source_directories": [
4 | "pymuscle"
5 | ],
6 | "typeshed": "/Users/iandanforth/.pyenv/versions/3.6.4/lib/pyre_check/typeshed/stdlib/"
7 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | sudo: required
3 | dist: xenial
4 | python:
5 | - "3.6"
6 | - "3.7"
7 | # command to install dependencies
8 | install:
9 | - pip install .
10 | - pip install -r requirements-dev.txt
11 | # command to run tests
12 | script:
13 | - pytest # or py.test for Python versions 3.5 and below
14 |
--------------------------------------------------------------------------------
/requirements-dev.txt:
--------------------------------------------------------------------------------
1 | # Why only a requirements-dev.txt?
2 | # https://caremad.io/posts/2013/07/setup-vs-requirement/
3 | # Standard install:
4 | # `pip install pymuscle` # From PyPi
5 | # `pip install .` # Locally
6 | # `python setup.py install` # Same as above
7 | pytest >= 3.6
8 | pylint
9 | coverage
10 | pytest-cov
11 | sphinx-rtd-theme
12 |
--------------------------------------------------------------------------------
/tests/test_model.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pymuscle import Model
3 |
4 |
5 | def test_init():
6 | with pytest.raises(TypeError):
7 | m = Model()
8 |
9 | motor_unit_count = 120
10 | m = Model(120)
11 |
12 | assert m.motor_unit_count == motor_unit_count
13 |
14 |
15 | def test_step():
16 | m = Model(100)
17 |
18 | with pytest.raises(TypeError):
19 | m.step()
20 |
21 | with pytest.raises(NotImplementedError):
22 | m.step(20, 1)
23 |
--------------------------------------------------------------------------------
/pymuscle/model.py:
--------------------------------------------------------------------------------
1 | from numpy import ndarray
2 |
3 |
4 | class Model(object):
5 | """
6 | Base model class from which other models should inherit
7 | """
8 | def __init__(
9 | self,
10 | motor_unit_count: int
11 | ):
12 | self.motor_unit_count = motor_unit_count
13 |
14 | def step(self, inputs: ndarray, step_size: float):
15 | """
16 | Child classes must implement this method.
17 | """
18 | raise NotImplementedError
19 |
--------------------------------------------------------------------------------
/tests/benchmarks/util.py:
--------------------------------------------------------------------------------
1 | from functools import wraps
2 | from time import time
3 |
4 |
5 | def timing(f):
6 | """
7 | From https://stackoverflow.com/a/27737385/1775741
8 | """
9 | @wraps(f)
10 | def wrap(*args, **kw):
11 | ts = time()
12 | result = f(*args, **kw)
13 | te = time()
14 | dur = te - ts
15 | print('func:{!r} args:[{!r}, {!r}] took: {:2.4f} sec'.format(
16 | f.__name__, args, kw, dur)
17 | )
18 | return dur, result
19 | return wrap
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/pymuscle/__init__.py:
--------------------------------------------------------------------------------
1 | from .__version__ import __version__ # noqa: F401
2 | from .model import Model # noqa: F401
3 | from .muscle import Muscle # noqa: F401
4 | from .muscle import PotvinFuglevandMuscle # noqa: F401
5 | from .muscle import StandardMuscle # noqa: F401
6 | from .potvin_fuglevand_2017_muscle_fibers import PotvinFuglevand2017MuscleFibers # noqa: F401
7 | from .potvin_fuglevand_2017_motor_neuron_pool import PotvinFuglevand2017MotorNeuronPool # noqa: F401
8 | from .pymuscle_fibers import PyMuscleFibers # noqa: F401
9 | from .hill_type import (
10 | contractile_element_force_length_curve,
11 | contractile_element_force_velocity_curve
12 | )
13 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/enhancement-request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Enhancement request
3 | about: Suggest an idea for this project
4 |
5 | ---
6 |
7 | **Is your feature request related to a problem? Please describe.**
8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
9 |
10 | **Describe the solution you'd like**
11 | A clear and concise description of what you want to happen.
12 |
13 | **Describe alternatives you've considered**
14 | A clear and concise description of any alternative solutions or features you've considered.
15 |
16 | **Additional context**
17 | Add any other context or screenshots about the feature request here.
18 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/research-submission.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Research Submission
3 | about: Submit a reference to new or existing research.
4 |
5 | ---
6 |
7 | **What aspect of the model do you feel should be changed?**
8 | A clear and concise description of what the issue with the current behavior and how you feel it should be changed.
9 |
10 | **Describe the expected impact on the performance of the library here.**
11 |
12 | **Please link to supporting research papers here.**
13 | (Do not submit sci-hub links, sci-hub is too convenient and useful and is bad for other reasons too)
14 |
15 | **Additional context**
16 | Add any other context or screenshots about the proposal here.
17 |
--------------------------------------------------------------------------------
/docs/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | SPHINXPROJ = PyMuscle
8 | SOURCEDIR = .
9 | BUILDDIR = ../../pymuscle-docs/
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 |
5 | ---
6 |
7 | **Describe the bug**
8 | A clear and concise description of what the bug is.
9 |
10 | **To Reproduce**
11 | Steps to reproduce the behavior:
12 | 1. Go to '...'
13 | 2. Click on '....'
14 | 3. Scroll down to '....'
15 | 4. See error
16 |
17 | **Expected behavior**
18 | A clear and concise description of what you expected to happen.
19 |
20 | **Screenshots**
21 | If applicable, add screenshots to help explain your problem.
22 |
23 | **Desktop (please complete the following information):**
24 | - OS: [e.g. iOS]
25 | - Python Version [e.g. 3.4]
26 | - PyMuscle Version [e.g. 0.01]
27 |
28 |
29 | **Additional context**
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. PyMuscle documentation master file, created by
2 | sphinx-quickstart on Fri Jun 29 10:59:38 2018.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to PyMuscle's documentation!
7 | ====================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 | .. automodule:: pymuscle
14 |
15 | .. autoclass:: Muscle
16 | :members:
17 |
18 | .. autoclass:: PotvinFuglevandMuscle
19 | :members:
20 |
21 | .. autoclass:: Model
22 | :members:
23 |
24 | .. autoclass:: PotvinFuglevand2017MotorNeuronPool
25 | :members:
26 |
27 | .. autoclass:: PotvinFuglevand2017MuscleFibers
28 | :members:
29 |
30 | .. automodule:: pymuscle.hill_type
31 | :members:
32 |
33 | Indices and tables
34 | ==================
35 |
36 | * :ref:`genindex`
37 | * :ref:`modindex`
38 | * :ref:`search`
39 |
--------------------------------------------------------------------------------
/docs/assemble.py:
--------------------------------------------------------------------------------
1 | #! /usr/bin/env/python
2 |
3 | """
4 | Adds the ability to 'include' files in markdown templates. Just main README.md
5 | for now.
6 | """
7 |
8 | import os
9 | import re
10 |
11 | file_pairs = [
12 | # Source -------------------- Destination
13 | ["src/readme-template.md", "../README.md"],
14 | ]
15 |
16 | include_syntax = r'\{\{\'(.+)\'\}\}'
17 |
18 | for source, destination in file_pairs:
19 | source_path = os.path.abspath(os.path.dirname(source))
20 | # Read the template
21 | with open(source) as f:
22 | template_contents = f.read()
23 | m = re.search(include_syntax, template_contents)
24 | include_path = m.group(1)
25 | include_path = os.path.join(source_path, include_path)
26 | # Read the included file
27 | with open(include_path) as f:
28 | include_contents = f.read()
29 | # Write the combined file
30 | with open(destination, "w+") as f:
31 | output = re.sub(include_syntax, include_contents, template_contents)
32 | f.write(output)
33 |
--------------------------------------------------------------------------------
/tests/test_potvin_fuglevand_muscle.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | from pymuscle import PotvinFuglevandMuscle as Muscle
4 |
5 |
6 | def test_init():
7 | with pytest.raises(TypeError):
8 | m = Muscle()
9 |
10 | motor_unit_count = 120
11 | m = Muscle(motor_unit_count)
12 |
13 | assert m.motor_unit_count == motor_unit_count
14 |
15 |
16 | def test_step():
17 | motor_unit_count = 120
18 | m = Muscle(motor_unit_count)
19 |
20 | with pytest.raises(TypeError):
21 | m.step()
22 |
23 | with pytest.raises(AssertionError):
24 | m.step(np.ones(3), 1)
25 |
26 | # No excitation
27 | output = m.step(np.zeros(motor_unit_count), 1.0)
28 | assert output == pytest.approx(0.0)
29 |
30 | # Moderate
31 | m = Muscle(motor_unit_count)
32 | moderate_input = 40.0
33 | moderate_output = 1311.86896
34 | output = m.step(np.full(motor_unit_count, moderate_input), 1.0)
35 | assert output == pytest.approx(moderate_output)
36 |
37 | # Moderate - single value
38 | m = Muscle(motor_unit_count)
39 | output = m.step(moderate_input, 1.0)
40 | assert output == pytest.approx(moderate_output)
41 |
42 | # Max
43 | m = Muscle(motor_unit_count)
44 | max_input = m.max_excitation
45 | max_output = 2215.98114
46 | output = m.step(np.full(motor_unit_count, max_input), 1.0)
47 | assert output == pytest.approx(max_output)
48 |
49 | # Above
50 | m = Muscle(motor_unit_count)
51 | output = m.step(np.full(motor_unit_count, max_input + 40), 1.0)
52 | assert output == pytest.approx(max_output)
53 |
--------------------------------------------------------------------------------
/tests/benchmarks/bench_potvin_fuglevand_muscle.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from pymuscle import PotvinFuglevandMuscle as Muscle
3 | import plotly.graph_objs as go
4 | from plotly.offline import plot
5 | from util import timing
6 |
7 |
8 | @timing
9 | def instantiation(a, n):
10 | for _ in range(a):
11 | m = Muscle(n)
12 |
13 |
14 | @timing
15 | def step(a, n):
16 | moderate_input = 40.0
17 | in_vec = np.full(n, moderate_input)
18 | m = Muscle(n)
19 | for _ in range(a):
20 | m.step(in_vec, 1.0)
21 |
22 |
23 | def main():
24 | step_durations = []
25 | instantiation_durations = []
26 | loops = 10000
27 | exps = list(range(10, 16))
28 | for i in exps:
29 | n = 2 ** i
30 | inst_dur, _ = instantiation(loops, n)
31 | instantiation_durations.append(inst_dur)
32 | step_dur, _ = step(loops, n)
33 | step_durations.append(step_dur)
34 |
35 | instantiation_durations = np.log(instantiation_durations)
36 | step_durations = np.log(step_durations)
37 |
38 | # Log plot of durations by exponents
39 |
40 | layout = go.Layout(
41 | title="Log Plot of Durations for N=2^X",
42 | xaxis={"title": "Number of Motor Units - N=2^X"},
43 | yaxis={"title": "Duration (log(seconds)) of 10000 loops"}
44 | )
45 | data = [
46 | {'y': instantiation_durations, 'x': exps, 'title': 'Instantiation'},
47 | {'y': step_durations, 'x': exps, 'title': '.step()'}
48 | ]
49 | fig = go.Figure(
50 | data=data,
51 | layout=layout
52 | )
53 | plot(fig)
54 |
55 |
56 | if __name__ == '__main__':
57 | main()
58 |
--------------------------------------------------------------------------------
/tests/test_potvin_fuglevand_2017_muscle_fibers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | from pymuscle import PotvinFuglevand2017MuscleFibers as Fibers
4 |
5 |
6 | def test_init():
7 | # Missing arguments
8 | with pytest.raises(TypeError):
9 | f = Fibers()
10 |
11 | motor_unit_count = 120
12 | f = Fibers(motor_unit_count)
13 | assert f.motor_unit_count == motor_unit_count
14 |
15 |
16 | def test_step():
17 | motor_unit_count = 120
18 | f = Fibers(motor_unit_count)
19 |
20 | # Missing arguments
21 | with pytest.raises(TypeError):
22 | f.step()
23 |
24 | # Bad type
25 | with pytest.raises(TypeError):
26 | f.step(33.0, 1)
27 |
28 | # Wrong shape
29 | with pytest.raises(AssertionError):
30 | f.step(np.ones(3), 1)
31 |
32 | # No excitation
33 | output = f.step(np.zeros(motor_unit_count), 1.0)
34 | assert output == pytest.approx(0.0)
35 |
36 | # Moderate
37 | f = Fibers(motor_unit_count)
38 | moderate_input = 40.0
39 | moderate_output = 2586.7530897
40 | output = f.step(np.full(motor_unit_count, moderate_input), 1.0)
41 | output_sum = np.sum(output)
42 | assert output_sum == pytest.approx(moderate_output)
43 |
44 | # Max
45 | f = Fibers(motor_unit_count)
46 | max_input = 67.0
47 | max_output = 2609.0308816
48 | output = f.step(np.full(motor_unit_count, max_input), 1.0)
49 | output_sum = np.sum(output)
50 | assert output_sum == pytest.approx(max_output)
51 |
52 | # Above
53 | f = Fibers(motor_unit_count)
54 | output = f.step(np.full(motor_unit_count, max_input + 40), 1.0)
55 | output_sum = np.sum(output)
56 | assert output_sum == pytest.approx(max_output)
57 |
--------------------------------------------------------------------------------
/examples/minimal-physio-example.py:
--------------------------------------------------------------------------------
1 | from pymuscle import PotvinFuglevandMuscle as Muscle
2 | from pymuscle.vis import PotvinChart
3 |
4 | # Create a Muscle with small number of motor units.
5 | motor_unit_count = 120
6 | muscle = Muscle(motor_unit_count)
7 |
8 | # Set up the simulation parameters
9 | sim_duration = 200 # seconds
10 | frames_per_second = 50
11 | step_size = 1 / frames_per_second
12 | total_steps = int(sim_duration / step_size)
13 |
14 | # Use a constant level of excitation to more easily observe fatigue
15 | excitation = 40.0
16 |
17 | total_outputs = []
18 | outputs_by_unit = []
19 | print("Starting simulation ...")
20 | for i in range(total_steps):
21 | # Calling step() updates the simulation and returns the total output
22 | # produced by the muscle during this step for the given excitation level.
23 | total_output = muscle.step(excitation, step_size)
24 | total_outputs.append(total_output)
25 | # You can also introspect the muscle to see the forces being produced
26 | # by each motor unit.
27 | output_by_unit = muscle.current_forces
28 | outputs_by_unit.append(output_by_unit)
29 | if (i % (frames_per_second * 10)) == 0:
30 | print("Sim time - {} seconds ...".format(int(i / frames_per_second)))
31 |
32 | # Visualize the behavior of the motor units over time
33 | print("Creating chart ...")
34 | chart = PotvinChart(
35 | outputs_by_unit,
36 | step_size
37 | )
38 | # Things to note in the chart:
39 | # - Some motor units (purple) are never recruited at this level of excitation
40 | # - Some motor units become completely fatigued in this short time
41 | # - Some motor units stabilize and decrease their rate of fatigue
42 | # - Forces from the weakest motor units are almost constant the entire time
43 | chart.display()
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # PyMuscle Specific
2 | *.html
3 |
4 | # Byte-compiled / optimized / DLL files
5 | __pycache__/
6 | *.py[cod]
7 | *$py.class
8 |
9 | # C extensions
10 | *.so
11 |
12 | # Distribution / packaging
13 | .Python
14 | env/
15 | build/
16 | develop-eggs/
17 | dist/
18 | downloads/
19 | eggs/
20 | .eggs/
21 | lib/
22 | lib64/
23 | parts/
24 | sdist/
25 | var/
26 | wheels/
27 | *.egg-info/
28 | .installed.cfg
29 | *.egg
30 | .pyre/
31 |
32 | # PyInstaller
33 | # Usually these files are written by a python script from a template
34 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
35 | *.manifest
36 | *.spec
37 |
38 | # Installer logs
39 | pip-log.txt
40 | pip-delete-this-directory.txt
41 |
42 | # Unit test / coverage reports
43 | htmlcov/
44 | .tox/
45 | .coverage
46 | .coverage.*
47 | .cache
48 | nosetests.xml
49 | coverage.xml
50 | *.cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 |
62 | # Flask stuff:
63 | instance/
64 | .webassets-cache
65 |
66 | # Scrapy stuff:
67 | .scrapy
68 |
69 | # Sphinx documentation
70 | docs/_build/
71 |
72 | # PyBuilder
73 | target/
74 |
75 | # Jupyter Notebook
76 | .ipynb_checkpoints
77 |
78 | # pyenv
79 | .python-version
80 |
81 | # celery beat schedule file
82 | celerybeat-schedule
83 |
84 | # SageMath parsed files
85 | *.sage.py
86 |
87 | # dotenv
88 | .env
89 |
90 | # virtualenv
91 | .venv
92 | venv/
93 | ENV/
94 |
95 | # Spyder project settings
96 | .spyderproject
97 | .spyproject
98 |
99 | # Rope project settings
100 | .ropeproject
101 |
102 | # mkdocs documentation
103 | /site
104 |
105 | # mypy
106 | .mypy_cache/
107 |
108 | # OSX
109 | .DS_Store
110 |
111 | # VSCode
112 | .vscode
113 |
--------------------------------------------------------------------------------
/examples/pymunk-gym-example.py:
--------------------------------------------------------------------------------
1 | """
2 | Arm Curl
3 |
4 | The goal here is to keep the arm tracking a moving target smoothly.
5 | Watch as the effort required slowly increases for all portions of the motion,
6 | until the muscle is no longer able to support the arm. Here a hand tuned
7 | PID controller takes care of managing this effort.
8 | """
9 |
10 | import sys
11 | import os
12 | sys.path.insert(0, os.path.abspath(os.path.dirname(__file__)))
13 | from envs import PymunkArmEnv
14 |
15 |
16 | def main():
17 | env = PymunkArmEnv(apply_fatigue=True)
18 |
19 | # Set up the simulation parameters
20 | sim_duration = 120 # seconds
21 | frames_per_second = 50
22 | step_size = 1 / frames_per_second
23 | total_steps = int(sim_duration / step_size)
24 |
25 | # Here we are going to send a constant excitation to the tricep and
26 | # vary the excitation of the bicep as we try to hit a target location.
27 | brachialis_input = 0.4 # Percent of max input
28 | tricep_input = 0.2
29 | hand_target_y = 360
30 | target_delta = 10
31 | # Hand tuned PID params.
32 | Kp = 0.0001
33 | Ki = 0.00004
34 | Kd = 0.0001
35 | prev_y = None
36 | print("Fraction of max excitation ...")
37 | for i in range(total_steps):
38 | hand_x, hand_y = env.step(
39 | [brachialis_input, tricep_input],
40 | step_size, debug=False
41 | )
42 |
43 | # PID Control
44 | if prev_y is None:
45 | prev_y = hand_y
46 |
47 | # Proportional component
48 | error = hand_target_y - hand_y
49 | alpha = Kp * error
50 | # Add integral component
51 | i_c = Ki * (error * step_size)
52 | alpha -= i_c
53 | # Add in differential component
54 | d_c = Kd * ((hand_y - prev_y) / step_size)
55 | alpha -= d_c
56 |
57 | prev_y = hand_y
58 | brachialis_input += alpha
59 |
60 | if brachialis_input > 1.0:
61 | brachialis_input = 1.0
62 | if brachialis_input < 0.0:
63 | brachialis_input = 0.0
64 |
65 | # Vary our set point and display the excitation required
66 | if i % frames_per_second == 0:
67 | print(brachialis_input)
68 | hand_target_y += target_delta
69 |
70 | # Switch directions every 5 seconds
71 | if i > 0 and i % (frames_per_second * 5) == 0:
72 | target_delta *= -1
73 |
74 | env.render()
75 |
76 |
77 | if __name__ == '__main__':
78 | main()
79 |
--------------------------------------------------------------------------------
/pymuscle/vis/potvin_charts.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import colorlover as cl
3 | from numpy import ndarray
4 | from plotly.offline import plot
5 |
6 |
7 | class PotvinChart(object):
8 |
9 | def __init__(self, time_by_forces: ndarray, step_size: float):
10 |
11 | forces_by_time = np.array(time_by_forces).T
12 | motor_unit_count, steps = forces_by_time.shape
13 | sim_duration = steps * step_size
14 | times = np.arange(0.0, sim_duration, step_size)
15 |
16 | # Setting colors for plot.
17 | potvin_scheme = [
18 | 'rgb(115, 0, 0)',
19 | 'rgb(252, 33, 23)',
20 | 'rgb(230, 185, 43)',
21 | 'rgb(107, 211, 100)',
22 | 'rgb(52, 211, 240)',
23 | 'rgb(36, 81, 252)',
24 | 'rgb(0, 6, 130)'
25 | ]
26 |
27 | # It's hacky but also sorta cool.
28 | c = cl.to_rgb(cl.interp(potvin_scheme, motor_unit_count))
29 | c = [val.replace('rgb', 'rgba') for val in c]
30 | c = [val.replace(')', ',{})') for val in c]
31 |
32 | # Assing non-public attributes
33 | self._step_size = step_size
34 | self._forces_by_time = forces_by_time
35 | self._c = c
36 | self._times = times
37 |
38 | # Assign public attributes
39 | self.motor_unit_count = motor_unit_count
40 |
41 | def _get_color(self, trace_index: int) -> str:
42 | # The first and every 20th trace should be full opacity
43 | alpha = 0.2
44 | if trace_index == 0 or ((trace_index + 1) % 20 == 0):
45 | alpha = 1.0
46 | color = self._c[trace_index].format(alpha)
47 | return color
48 |
49 | def display(self) -> None:
50 | # Per Motor Unit Force
51 | data = []
52 | for i, t in enumerate(self._forces_by_time):
53 | trace = dict(
54 | x=self._times,
55 | y=t,
56 | name=i + 1,
57 | marker=dict(
58 | color=self._get_color(i)
59 | ),
60 | )
61 | data.append(trace)
62 |
63 | layout = dict(
64 | title='Motor Unit Forces by Time',
65 | yaxis=dict(
66 | title='Motor unit force (relative to MU1 tetanus)'
67 | ),
68 | xaxis=dict(
69 | title='Time (s)'
70 | )
71 | )
72 |
73 | fig = dict(
74 | data=data,
75 | layout=layout
76 | )
77 | plot(fig, filename='forces-by-time.html', validate=False)
78 |
--------------------------------------------------------------------------------
/examples/envs/assets/ballonstring.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License + No Military Use
2 |
3 | Copyright (c) 2018 Ian Danforth
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | Using this software in source or binary format, with or without modification,
16 | to develop, modify or improve software, products or programs for military use
17 | is not permitted without specific prior written permission.
18 |
19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25 | SOFTWARE.
26 |
27 | Explanation of the No Military Use Clause
28 |
29 | This meaning of this phrase includes any use by a military, not just for
30 | combat. It also includes work by contractors explicitly for a military.
31 |
32 | Example Use
33 |
34 | You may incorporate this code into your own products so long as those products
35 | are not primarily for military use. If you use this code in a drafting program
36 | and the military used it, this would be permitted use. If you used it in a
37 | drafting program for nuclear submarines, this would be a prohibited use.
38 |
39 | Developing a general use program, product or piece of software and then
40 | actively selling or marketing that program, product or software to a military
41 | organization would be a violation of the No Military Use clause.
42 |
43 | OSI / FSF
44 |
45 | The MIT + No Military Use License is not OSI or FSF approved. Users of the
46 | MIT + No Military Use License believe that software development is not a
47 | neutral act. In addition users believe that software development imposes a
48 | responsiblity on developers to consider the consequences of the use of their
49 | software and a responsibility to prevent its misuse. The unlimited freedoms of
50 | use codified by the OSI and FSF are incompatible with this view and are
51 | violated by this license.
52 |
--------------------------------------------------------------------------------
/examples/envs/muscled_hopper.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from gym import utils
3 | from . import mujoco_env
4 | from pymuscle import PotvinFuglevandMuscle as Muscle
5 |
6 |
7 | class MuscledHopperEnv(mujoco_env.MujocoEnv, utils.EzPickle):
8 | def __init__(self, apply_fatigue=False):
9 | # Instantiate the PyMuscles (not real muscle names)
10 | hamstring_motor_unit_count = 300
11 | self.hamstring_muscle = Muscle(hamstring_motor_unit_count, apply_fatigue)
12 | thigh_motor_unit_count = 300
13 | self.thigh_muscle = Muscle(thigh_motor_unit_count, apply_fatigue)
14 | calf_motor_unit_count = 200
15 | self.calf_muscle = Muscle(calf_motor_unit_count, apply_fatigue)
16 | shin_motor_unit_count = 100
17 | self.shin_muscle = Muscle(shin_motor_unit_count, apply_fatigue)
18 | self.muscles = [
19 | self.hamstring_muscle,
20 | self.thigh_muscle,
21 | self.calf_muscle,
22 | self.shin_muscle
23 | ]
24 |
25 | # Initialize parents
26 | mujoco_env.MujocoEnv.__init__(self, 'hopper.xml', 4)
27 | utils.EzPickle.__init__(self)
28 |
29 | def step(self, a):
30 | posbefore = self.sim.data.qpos[0]
31 |
32 | # Get output from muscles
33 | outputs = [muscle.step(a[i], 0.002 * self.frame_skip) for
34 | i, muscle in enumerate(self.muscles)]
35 |
36 | self.do_simulation(outputs, self.frame_skip)
37 | posafter, height, ang = self.sim.data.qpos[0:3]
38 | alive_bonus = 1.0
39 | reward = (posafter - posbefore) / self.dt
40 | reward += alive_bonus
41 | reward -= 1e-3 * np.square(a).sum()
42 | s = self.state_vector()
43 | done = not (np.isfinite(s).all() and (np.abs(s[2:]) < 100).all() and
44 | (height > .7) and (abs(ang) < .2))
45 | ob = self._get_obs()
46 | return ob, reward, done, {}
47 |
48 | def _get_obs(self):
49 | return np.concatenate([
50 | self.sim.data.qpos.flat[1:],
51 | np.clip(self.sim.data.qvel.flat, -10, 10)
52 | ])
53 |
54 | def reset_model(self):
55 | qpos = self.init_qpos + self.np_random.uniform(
56 | low=-.005,
57 | high=.005,
58 | size=self.model.nq
59 | )
60 | qvel = self.init_qvel + self.np_random.uniform(
61 | low=-.005,
62 | high=.005,
63 | size=self.model.nv
64 | )
65 | self.set_state(qpos, qvel)
66 | return self._get_obs()
67 |
68 | def viewer_setup(self):
69 | self.viewer.cam.trackbodyid = 2
70 | self.viewer.cam.distance = self.model.stat.extent * 0.75
71 | self.viewer.cam.lookat[2] += .8
72 | self.viewer.cam.elevation = -20
73 |
--------------------------------------------------------------------------------
/pymuscle/hill_type.py:
--------------------------------------------------------------------------------
1 | """
2 | Implementations of equations used in Hill-Type muscle models
3 |
4 | Not all equations for a traditional Hill-Type model are provided here. It is
5 | expected that passive elements will be modeled by a physics simulator selected
6 | by PyMuscle users.
7 |
8 | Also it should be noted that these equations do not take pennetation angle
9 | into account (which is common in Hill-type models). Users should check to
10 | make sure their physics simulator is handling forces correctly to provide values
11 | which vary by angle.
12 | """
13 | import numpy as np
14 |
15 |
16 | def contractile_element_force_length_curve(
17 | rest_length: float,
18 | current_length: float,
19 | curve_width_factor: float=17.33,
20 | peak_force_length: float=1.1
21 | ) -> float:
22 | """
23 | This normalizes length to the resting length of the tendon
24 | Anderson and others normalize by the length that would generate the
25 | most force. This is a gaussian-like relationship.
26 |
27 | :param rest_length: The resting length of the muscle
28 | :param current_length: The current length of the muscle
29 | :param curve_width_factor:
30 | A curve shape paramater which describes where force begins to approach
31 | 0 for muscles shorter or longer than resting length.
32 | See https://www.desmos.com/calculator/qosweyyqfk for graph.
33 | :param peak_force_length:
34 | The ratio of the length of the muscle when it can generate maximum
35 | contractile force (sometimes known as the optimal length) and the
36 | resting length of the muscle.
37 | """
38 | norm_length = current_length / rest_length
39 | peak_force_length = 1.10 # Aubert 1951
40 | exponent = curve_width_factor * (-1 * (abs(norm_length - peak_force_length) ** 3))
41 | percentage_of_max_force = np.exp(exponent)
42 | return percentage_of_max_force
43 |
44 |
45 | def contractile_element_force_velocity_curve(
46 | rest_length: float,
47 | current_length: float,
48 | prev_length: float,
49 | time_step: float,
50 | max_velocity: float = 3.0,
51 | max_exccentric_multiple: float = 1.8
52 | ) -> float:
53 | """
54 | Returns a value (0 < value < max_eccentric_multiple). This is a sigmoidal
55 | relationship.
56 |
57 | :param rest_length: The resting length of the muscle
58 | :param current_length: The current length of the muscle
59 |
60 | See https://www.desmos.com/calculator/gkcdsdcuyh for graph
61 | """
62 | velocity = ((prev_length - current_length) / rest_length) / time_step
63 | norm_velocity = velocity / max_velocity
64 | exponent = (0.04 - norm_velocity) / 0.18 # Values to make (0, 1)
65 | denominator = 1 + np.exp(exponent)
66 | force_multiple = max_exccentric_multiple - (max_exccentric_multiple / denominator)
67 | return force_multiple
68 |
--------------------------------------------------------------------------------
/tests/test_pymuscle_fibers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | from copy import copy
4 | from pymuscle import PyMuscleFibers as Fibers
5 |
6 |
7 | def test_init():
8 | motor_unit_count = 120
9 | f = Fibers(120)
10 |
11 | # Check calculated number of motor units
12 | assert f.motor_unit_count == 120
13 | assert np.equal(f.current_forces, np.zeros(motor_unit_count)).all()
14 |
15 |
16 | def test_step():
17 | motor_unit_count = 120
18 | f = Fibers(motor_unit_count)
19 |
20 | # Missing arguments
21 | with pytest.raises(TypeError):
22 | f.step()
23 |
24 | with pytest.raises(TypeError):
25 | f.step(33.0)
26 |
27 | # Bad type
28 | with pytest.raises(TypeError):
29 | f.step(33.0, 1)
30 |
31 | # Wrong shape
32 | with pytest.raises(AssertionError):
33 | f.step(np.ones(3), 1)
34 |
35 | # No excitation
36 | output = f.step(np.zeros(motor_unit_count), 1.0)
37 | assert output == pytest.approx(0.0)
38 |
39 | # Moderate
40 | f = Fibers(motor_unit_count)
41 | moderate_input = 40.0
42 | moderate_output = 2586.7530897
43 | output = f.step(np.full(motor_unit_count, moderate_input), 1.0)
44 | output_sum = np.sum(output)
45 | assert output_sum == pytest.approx(moderate_output)
46 |
47 | # Max
48 | f = Fibers(motor_unit_count)
49 | max_input = 67.0
50 | max_output = 2609.0308816
51 | output = f.step(np.full(motor_unit_count, max_input), 1.0)
52 | output_sum = np.sum(output)
53 | assert output_sum == pytest.approx(max_output)
54 |
55 | # Above
56 | f = Fibers(motor_unit_count)
57 | output = f.step(np.full(motor_unit_count, max_input + 40), 1.0)
58 | output_sum = np.sum(output)
59 | assert output_sum == pytest.approx(max_output)
60 |
61 |
62 | def test_fatigue():
63 | motor_unit_count = 120
64 | f = Fibers(motor_unit_count)
65 |
66 | # With zero inputs fatigue shouldn't change
67 | f = Fibers(motor_unit_count)
68 | ctf_before = copy(f.current_peak_forces)
69 | for i in range(100):
70 | f.step(np.zeros(motor_unit_count), 1.0)
71 |
72 | ctf_after = f.current_peak_forces
73 | assert np.equal(ctf_before, ctf_after).all()
74 |
75 | # With max input all ctfs should decrease
76 | f = Fibers(motor_unit_count)
77 | ctf_before = copy(f.current_peak_forces)
78 | max_input = 67.0
79 | for i in range(100):
80 | f.step(np.full(motor_unit_count, max_input), 1.0)
81 |
82 | ctf_after = f.current_peak_forces
83 | assert np.greater(ctf_before, ctf_after).all()
84 |
85 | # With fatigue off no ctfs should change
86 | f = Fibers(motor_unit_count, apply_fatigue=False)
87 | max_input = 67.0
88 | for i in range(100):
89 | f.step(np.full(motor_unit_count, max_input), 1.0)
90 |
91 | ctf_after = f.current_peak_forces
92 | assert np.equal(ctf_before, ctf_after).all()
93 |
--------------------------------------------------------------------------------
/examples/envs/assets/arm.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/tests/test_potvin_fuglevand_2017_motor_neuron_pool.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | from pymuscle import PotvinFuglevand2017MotorNeuronPool as Pool
4 |
5 |
6 | def test_init():
7 | # Missing arguments
8 | with pytest.raises(TypeError):
9 | p = Pool()
10 |
11 | motor_unit_count = 120
12 | p = Pool(motor_unit_count)
13 | assert p.motor_unit_count == motor_unit_count
14 |
15 | # Check calculated max excitation
16 | assert p.max_excitation == 67.0
17 |
18 |
19 | def test_step():
20 | motor_unit_count = 120
21 | p = Pool(motor_unit_count)
22 |
23 | # Missing arguments
24 | with pytest.raises(TypeError):
25 | p.step()
26 |
27 | # Bad type
28 | with pytest.raises(TypeError):
29 | p.step(33.0, 1)
30 |
31 | # Wrong shape
32 | with pytest.raises(AssertionError):
33 | p.step(np.ones(3), 1)
34 |
35 | # No excitation
36 | output = p.step(np.zeros(motor_unit_count), 1.0)
37 | output_sum = sum(output)
38 | assert output_sum == pytest.approx(0.0)
39 |
40 | # Moderate
41 | p = Pool(motor_unit_count)
42 | moderate_input = 40.0
43 | moderate_output = 3503.58881
44 | output = p.step(np.full(motor_unit_count, moderate_input), 1.0)
45 | output_sum = np.sum(output)
46 | assert output_sum == pytest.approx(moderate_output)
47 |
48 | # Max
49 | p = Pool(motor_unit_count)
50 | max_input = 67.0
51 | max_output = 3915.06787
52 | output = p.step(np.full(motor_unit_count, max_input), 1.0)
53 | output_sum = np.sum(output)
54 | assert output_sum == pytest.approx(max_output)
55 |
56 | # Above
57 | p = Pool(motor_unit_count)
58 | output = p.step(np.full(motor_unit_count, max_input + 40), 1.0)
59 | output_sum = np.sum(output)
60 | assert output_sum == pytest.approx(max_output)
61 |
62 |
63 | def test_fatigue_values():
64 | motor_unit_count = 120
65 |
66 | p = Pool(motor_unit_count)
67 | max_input = 67.0
68 | first_output = p.step(np.full(motor_unit_count, max_input), 1.0)
69 | first_output_sum = np.sum(first_output)
70 |
71 | # Advance the simulation 10 seconds
72 | for _ in range(10):
73 | adapted_output = p.step(np.full(motor_unit_count, max_input), 1.0)
74 | adapted_output_sum = np.sum(adapted_output)
75 |
76 | # Should have changed
77 | assert adapted_output_sum != pytest.approx(first_output_sum)
78 |
79 | # To this value
80 | expected_adapted_output = 3749.91061
81 | assert adapted_output_sum == pytest.approx(expected_adapted_output)
82 |
83 |
84 | def test_fatigue_disabled():
85 | motor_unit_count = 120
86 |
87 | p = Pool(motor_unit_count, apply_fatigue=False)
88 | max_input = 67.0
89 | first_output = p.step(np.full(motor_unit_count, max_input), 1.0)
90 | first_output_sum = np.sum(first_output)
91 |
92 | # Advance the simulation 10 seconds
93 | for _ in range(10):
94 | next_output = p.step(np.full(motor_unit_count, max_input), 1.0)
95 | next_output_sum = np.sum(next_output)
96 |
97 | # Should NOT have changed
98 | assert next_output_sum == pytest.approx(first_output_sum)
99 |
--------------------------------------------------------------------------------
/tests/test_standard_muscle.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import pytest
3 | from pymuscle import StandardMuscle as Muscle
4 |
5 |
6 | def test_init():
7 | max_force = 32.0
8 | m = Muscle(max_force)
9 |
10 | # Check calculated number of motor units
11 | assert m.motor_unit_count == 120
12 |
13 | # Check calculated max_output
14 | max_output = 2609.0309
15 | assert m.max_arb_output == pytest.approx(max_output)
16 |
17 | max_force = 90.0
18 | m = Muscle(max_force)
19 |
20 | # Check calculated number of motor units
21 | assert m.motor_unit_count == 340
22 | max_output = 7338.29062
23 | assert m.max_arb_output == pytest.approx(max_output)
24 |
25 |
26 | def test_step():
27 | max_force = 32.0
28 | m = Muscle(max_force)
29 |
30 | with pytest.raises(TypeError):
31 | m.step()
32 |
33 | with pytest.raises(AssertionError):
34 | m.step(np.ones(3), 1)
35 |
36 | # No excitation
37 | output = m.step(np.zeros(m.motor_unit_count), 1.0)
38 | assert output == pytest.approx(0.0)
39 |
40 | # Moderate
41 | m = Muscle(max_force)
42 | moderate_input = 0.5
43 | moderate_output = 0.39096348
44 | output = m.step(np.full(m.motor_unit_count, moderate_input), 1.0)
45 | assert output == pytest.approx(moderate_output)
46 |
47 | # Moderate - single value
48 | m = Muscle(max_force)
49 | output = m.step(moderate_input, 1.0)
50 | assert output == pytest.approx(moderate_output)
51 |
52 | # Max
53 | m = Muscle(max_force)
54 | max_input = 1.0
55 | max_output = 0.84935028
56 | output = m.step(np.full(m.motor_unit_count, max_input), 1.0)
57 | assert output == pytest.approx(max_output)
58 |
59 | # Above
60 | m = Muscle(max_force)
61 | output = m.step(np.full(m.motor_unit_count, max_input + 40), 1.0)
62 | assert output == pytest.approx(max_output)
63 |
64 |
65 | def test_fatigue():
66 | # With fatigue off the return should always be the same
67 | max_force = 32.0
68 | m = Muscle(max_force, apply_peripheral_fatigue=False)
69 |
70 | # As measured by fatigue
71 | fatigue_before = m.get_peripheral_fatigue()
72 | assert fatigue_before == 0.0
73 |
74 | # As measured by output
75 | moderate_input = 0.5
76 | moderate_output = 0.39096348
77 | for i in range(100):
78 | output = m.step(moderate_input, 1.0)
79 | assert output == pytest.approx(moderate_output)
80 |
81 | # And again by fatigue after
82 | fatigue_after = m.get_peripheral_fatigue()
83 | assert fatigue_after == 0.0
84 |
85 | # Fatigue ON
86 |
87 | # You should see no change with zero activation
88 | m = Muscle(max_force, apply_peripheral_fatigue=True)
89 |
90 | # As measured by fatigue
91 | fatigue_before = m.get_peripheral_fatigue()
92 | assert fatigue_before == 0.0
93 |
94 | for i in range(100):
95 | output = m.step(0.0, 1.0)
96 | assert output == pytest.approx(0.0)
97 |
98 | # As measured by fatgue after
99 | fatigue_after = m.get_peripheral_fatigue()
100 | assert fatigue_after == 0.0
101 |
102 | # You should see increasing fatigue and decreasing output
103 | # with non-zero inputs.
104 |
105 | # Measured by output
106 | fatigued_output = 0.29151827
107 | for i in range(100):
108 | output = m.step(moderate_input, 1.0)
109 | assert output == pytest.approx(fatigued_output)
110 |
111 | # As measured by fatgue after
112 | expected_fatigue = 0.18028181
113 | fatigue_after = m.get_peripheral_fatigue()
114 | assert pytest.approx(fatigue_after, expected_fatigue)
115 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Note: To use the 'upload' functionality of this file, you must:
5 | # $ pip install twine
6 |
7 | import io
8 | import os
9 | import sys
10 | from shutil import rmtree
11 |
12 | from setuptools import find_packages, setup, Command
13 |
14 | # Package meta-data.
15 | NAME = 'pymuscle'
16 | DESCRIPTION = 'A motor unit based model of skeletal muscle and fatigue'
17 | URL = 'https://github.com/iandanforth/pymuscle'
18 | EMAIL = 'iandanforth@gmail.com'
19 | AUTHOR = 'Ian Danforth'
20 | REQUIRES_PYTHON = '>=3.6.0'
21 | VERSION = None
22 |
23 | # What packages are required for this module to be executed?
24 | # This should be kept in sync with *runtime* packages, but not
25 | # *development* packages.
26 | REQUIRED = [
27 | 'numpy',
28 | 'plotly',
29 | 'colorlover'
30 | ]
31 |
32 | # The rest you shouldn't have to touch too much :)
33 | # ------------------------------------------------
34 | # Except, perhaps the License and Trove Classifiers!
35 | # If you do change the License, remember to change the Trove Classifier for that!
36 |
37 | here = os.path.abspath(os.path.dirname(__file__))
38 |
39 | # Import the README and use it as the long-description.
40 | # Note: this will only work if 'README.md' is present in your MANIFEST.in file!
41 | with io.open(os.path.join(here, 'README.md'), encoding='utf-8') as f:
42 | long_description = '\n' + f.read()
43 |
44 | # Load the package's __version__.py module as a dictionary.
45 | about = {}
46 | if not VERSION:
47 | with open(os.path.join(here, NAME, '__version__.py')) as f:
48 | exec(f.read(), about)
49 | else:
50 | about['__version__'] = VERSION
51 |
52 |
53 | class UploadCommand(Command):
54 | """Support setup.py upload."""
55 |
56 | description = 'Build and publish the package.'
57 | user_options = []
58 |
59 | @staticmethod
60 | def status(s):
61 | """Prints things in bold."""
62 | print('\033[1m{0}\033[0m'.format(s))
63 |
64 | def initialize_options(self):
65 | pass
66 |
67 | def finalize_options(self):
68 | pass
69 |
70 | def run(self):
71 | try:
72 | self.status('Removing previous builds…')
73 | rmtree(os.path.join(here, 'dist'))
74 | except OSError:
75 | pass
76 |
77 | self.status('Building Source and Wheel (universal) distribution…')
78 | os.system('{0} setup.py sdist bdist_wheel --universal'.format(sys.executable))
79 |
80 | self.status('Uploading the package to PyPi via Twine…')
81 | os.system('twine upload dist/*')
82 |
83 | # self.status('Pushing git tags…')
84 | # os.system('git tag v{0}'.format(about['__version__']))
85 | # os.system('git push --tags')
86 |
87 | sys.exit()
88 |
89 |
90 | # Where the magic happens:
91 | setup(
92 | name=NAME,
93 | version=about['__version__'],
94 | description=DESCRIPTION,
95 | long_description=long_description,
96 | long_description_content_type='text/markdown',
97 | author=AUTHOR,
98 | author_email=EMAIL,
99 | python_requires=REQUIRES_PYTHON,
100 | url=URL,
101 | packages=find_packages(exclude=('tests',)),
102 | # If your package is a single module, use this instead of 'packages':
103 | py_modules=[NAME],
104 |
105 | # entry_points={
106 | # 'console_scripts': ['mycli=mymodule:cli'],
107 | # },
108 | install_requires=REQUIRED,
109 | include_package_data=True,
110 | license='MIT + No Military Use',
111 | classifiers=[
112 | # Trove classifiers
113 | # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers
114 | 'Programming Language :: Python',
115 | 'Programming Language :: Python :: 3',
116 | 'Programming Language :: Python :: 3.6',
117 | 'Programming Language :: Python :: Implementation :: CPython',
118 | 'Programming Language :: Python :: Implementation :: PyPy'
119 | ],
120 | # $ setup.py publish support.
121 | cmdclass={
122 | 'upload': UploadCommand,
123 | },
124 | )
--------------------------------------------------------------------------------
/examples/envs/assets/hopper.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/physio-readme.md:
--------------------------------------------------------------------------------
1 | # Getting Started for Physiologists
2 |
3 | and other non-ml researchers.
4 |
5 | If you're interested in replicating or extending the results of of Potvin and
6 | Fuglevand, 2017 please start with reviewing [their official repository.](https://github.com/JimPotvin/Potvin_Fuglevand_Muscle_Fatigue_Model)
7 |
8 | The matlab code in that repository was re-implemented in Python for this library. (Forked at [15462f](https://github.com/JimPotvin/Potvin_Fuglevand_Muscle_Fatigue_Model/commit/15462f85106ed9ebde3d78ab6fe665c88bf8b32e)) Results
9 | from either set of code should be identical to within floating-point accuracy. Any errors
10 | in this code are entirely mine and not those of the paper's authors.
11 |
12 | ### Minimal example
13 |
14 | The Muscle class provides the primary API for the library. A Muscle can be
15 | heavily customized but here we use mainly default values. A PotvinFuglevandMuscle
16 | instantiated with 120 motor units has the distribution of strengths, recruitment
17 | thresholds, and fatigue properties as used in the experiments of Potvin and
18 | Fuglevand, 2017. That model is, in turn, based on experimental measurements of
19 | the first dorsal interossei.
20 |
21 | ```python
22 | from pymuscle import PotvinFuglevandMuscle as Muscle
23 | from pymuscle.vis import PotvinChart
24 |
25 | # Create a Muscle
26 | motor_unit_count = 120
27 | muscle = Muscle(motor_unit_count)
28 |
29 | # Set up the simulation parameters
30 | sim_duration = 200 # seconds
31 | frames_per_second = 50
32 | step_size = 1 / frames_per_second
33 | total_steps = int(sim_duration / step_size)
34 |
35 | # Use a constant level of excitation to more easily observe fatigue
36 | excitation = 40.0
37 |
38 | total_outputs = []
39 | outputs_by_unit = []
40 | print("Starting simulation ...")
41 | for i in range(total_steps):
42 | # Calling step() updates the simulation and returns the total output
43 | # produced by the muscle during this step for the given excitation level.
44 | total_output = muscle.step(excitation, step_size)
45 | total_outputs.append(total_output)
46 | # You can also introspect the muscle to see the forces being produced
47 | # by each motor unit.
48 | output_by_unit = muscle.current_forces
49 | outputs_by_unit.append(output_by_unit)
50 | if (i % (frames_per_second * 10)) == 0:
51 | print("Sim time - {} seconds ...".format(int(i / frames_per_second)))
52 |
53 | # Visualize the behavior of the motor units over time
54 | print("Creating chart ...")
55 | chart = PotvinChart(
56 | outputs_by_unit,
57 | step_size
58 | )
59 | # Things to note in the chart:
60 | # - Some motor units (purple) are never recruited at this level of excitation
61 | # - Some motor units become completely fatigued in this short time
62 | # - Some motor units stabilize and decrease their rate of fatigue
63 | # - Forces from the weakest motor units are almost constant the entire time
64 | chart.display()
65 |
66 | ```
67 |
68 | This will open a browser window with the produced chart. It should look like this:
69 |
70 |
71 |
72 | # Limitations
73 |
74 | ## Scope
75 |
76 | PyMuscle is concerned with inputs to motor unit neurons, the outputs of those
77 | motor units, and the changes to that system over time. It does not model the
78 | dynamics of the muscle body itself or the impact of dynamic motion on this
79 | motor unit input/output relationship.
80 |
81 | Separately, functions implementing force-length and force-velocity relationships
82 | of Hill-type muscles are provided in [hill_type.py](https://github.com/iandanforth/pymuscle/blob/master/pymuscle/hill_type.py)
83 |
84 | ## Recovery
85 |
86 | Potvin and Fuglevand 2017 explicitly models fatigue but *not* recovery. We
87 | eagerly await the updated model from Potvin which will included a model of
88 | recovery.
89 |
90 | Until then the `StandardMuscle` class, which builds on the Potvin and Fuglevand
91 | base classes, implements peripheral (muscle fiber) recovery as this is a
92 | relatively simple process but disables central (motor unit fatigue). If you use
93 | instances of `StandardMuscle` or its children please be aware you are stepping off
94 | peer-reviewed ground.
95 |
96 | ## Proprioception
97 |
98 | This library does not directly provide any feedback signals for control. The
99 | example projects show how to integrate PyMuscle with a physics simulation to
100 | get simulated output forces and stretch and strain values derived from the
101 | state of the simulated muscle body. (In the example this is a damped spring
102 | but a Hill-type, or more complex model could also be used.)
103 |
104 | Fatigue could be used as a feedback signal but this will need to be calculated
105 | from the states of the motor units.
106 |
--------------------------------------------------------------------------------
/examples/envs/mujoco_env.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from gym import error, spaces
4 | from gym.utils import seeding
5 | import numpy as np
6 | from os import path
7 | import gym
8 | import six
9 |
10 | try:
11 | import mujoco_py
12 | except ImportError as e:
13 | raise error.DependencyNotInstalled("{}. (HINT: you need to install mujoco_py, and also perform the setup instructions here: https://github.com/openai/mujoco-py/.)".format(e))
14 |
15 | DEFAULT_SIZE = 500
16 |
17 |
18 | class MujocoEnv(gym.Env):
19 | """Superclass for all MuJoCo environments.
20 | """
21 |
22 | def __init__(self, model_path, frame_skip):
23 | if model_path.startswith("/"):
24 | fullpath = model_path
25 | else:
26 | fullpath = os.path.join(os.path.dirname(__file__), "assets", model_path)
27 | if not path.exists(fullpath):
28 | raise IOError("File %s does not exist" % fullpath)
29 | self.frame_skip = frame_skip
30 | self.model = mujoco_py.load_model_from_path(fullpath)
31 | self.sim = mujoco_py.MjSim(self.model)
32 | self.data = self.sim.data
33 | self.viewer = None
34 | self._viewers = {}
35 |
36 | self.metadata = {
37 | 'render.modes': ['human', 'rgb_array'],
38 | 'video.frames_per_second': int(np.round(1.0 / self.dt))
39 | }
40 |
41 | self.init_qpos = self.sim.data.qpos.ravel().copy()
42 | self.init_qvel = self.sim.data.qvel.ravel().copy()
43 | observation, _reward, done, _info = self.step(np.zeros(self.model.nu))
44 | assert not done
45 | self.obs_dim = observation.size
46 |
47 | bounds = self.model.actuator_ctrlrange.copy()
48 | low = bounds[:, 0]
49 | high = bounds[:, 1]
50 | self.action_space = spaces.Box(low=low, high=high)
51 |
52 | high = np.inf*np.ones(self.obs_dim)
53 | low = -high
54 | self.observation_space = spaces.Box(low, high)
55 |
56 | self.seed()
57 |
58 | def seed(self, seed=None):
59 | self.np_random, seed = seeding.np_random(seed)
60 | return [seed]
61 |
62 | # methods to override:
63 | # ----------------------------
64 |
65 | def reset_model(self):
66 | """
67 | Reset the robot degrees of freedom (qpos and qvel).
68 | Implement this in each subclass.
69 | """
70 | raise NotImplementedError
71 |
72 | def viewer_setup(self):
73 | """
74 | This method is called when the viewer is initialized and after every reset
75 | Optionally implement this method, if you need to tinker with camera position
76 | and so forth.
77 | """
78 | pass
79 |
80 | # -----------------------------
81 |
82 | def reset(self):
83 | self.sim.reset()
84 | ob = self.reset_model()
85 | old_viewer = self.viewer
86 | for v in self._viewers.values():
87 | self.viewer = v
88 | self.viewer_setup()
89 | self.viewer = old_viewer
90 | return ob
91 |
92 | def set_state(self, qpos, qvel):
93 | assert qpos.shape == (self.model.nq,) and qvel.shape == (self.model.nv,)
94 | old_state = self.sim.get_state()
95 | new_state = mujoco_py.MjSimState(old_state.time, qpos, qvel,
96 | old_state.act, old_state.udd_state)
97 | self.sim.set_state(new_state)
98 | self.sim.forward()
99 |
100 | @property
101 | def dt(self):
102 | return self.model.opt.timestep * self.frame_skip
103 |
104 | def do_simulation(self, ctrl, n_frames):
105 | self.sim.data.ctrl[:] = ctrl
106 | for _ in range(n_frames):
107 | self.sim.step()
108 |
109 | def render(self, mode='human', width=DEFAULT_SIZE, height=DEFAULT_SIZE):
110 | if mode == 'rgb_array':
111 | self._get_viewer(mode).render(width, height)
112 | # window size used for old mujoco-py:
113 | data = self._get_viewer(mode).read_pixels(width, height, depth=False)
114 | # original image is upside-down, so flip it
115 | return data[::-1, :, :]
116 | elif mode == 'human':
117 | self._get_viewer(mode).render()
118 |
119 | def close(self):
120 | if self.viewer is not None:
121 | # self.viewer.finish()
122 | self.viewer = None
123 | self._viewers = {}
124 |
125 | def _get_viewer(self, mode):
126 | self.viewer = self._viewers.get(mode)
127 | if self.viewer is None:
128 | if mode == 'human':
129 | self.viewer = mujoco_py.MjViewer(self.sim)
130 | elif mode == 'rgb_array':
131 | self.viewer = mujoco_py.MjRenderContextOffscreen(self.sim, 0)
132 | self.viewer_setup()
133 | self._viewers[mode] = self.viewer
134 | return self.viewer
135 |
136 | def get_body_com(self, body_name):
137 | return self.data.get_body_xpos(body_name)
138 |
139 | def state_vector(self):
140 | return np.concatenate([
141 | self.sim.data.qpos.flat,
142 | self.sim.data.qvel.flat
143 | ])
144 |
--------------------------------------------------------------------------------
/pymuscle/pymuscle_fibers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy import ndarray
3 |
4 | from .potvin_fuglevand_2017_muscle_fibers import PotvinFuglevand2017MuscleFibers
5 |
6 |
7 | class PyMuscleFibers(PotvinFuglevand2017MuscleFibers):
8 | """
9 | Encapsulates the muscle fibers portions of the motor unit model. Currently
10 | a thin wrapper to implement fiber recovery.
11 |
12 | This is the standard muscle fiber model for PyMuscle. Which underlying
13 | model it extends is expected to change over time. This class exists to
14 | support the needs of users in the machine learning community. Users who
15 | belong to the physiology, biomechanical, or medical communities should
16 | use one of the base classes (such as PotvinFuglevand2017MuscleFibers) as
17 | those are strict implementations of published work.
18 |
19 | :param motor_unit_count: Number of motor units in the muscle
20 | :param force_conversion_factor: The ratio of newtons to arbitrary force
21 | units. All peak twitch forces are calculated internally to lie in a
22 | range of 0 to 100 arbitrary force units. The maximum force these
23 | fibers can theoretically produce is the sum of those peak twitch forces.
24 | To relate the arbitrary force units to SI units you need to provide
25 | a conversion factor. Increasing this value is essentially saying that
26 | a given motor unit produces more force than the default value would
27 | suggest.
28 | :param max_twitch_amplitude: Max twitch force within the pool
29 | :param max_contraction_time:
30 | [milliseconds] Maximum contraction time for a motor unit
31 | :param contraction_time_range:
32 | The scale between the fastest contraction time and the slowest
33 | :fatigue_factor_first_unit:
34 | The nominal fatigability of the first motor unit in percent / second
35 | :fatigability_range:
36 | The scale between the fatigability of the first motor unit and the last
37 | :contraction_time_change_ratio:
38 | For each percent of force lost during fatigue, what percentage should
39 | contraction increase?
40 |
41 | .. todo::
42 | The argument naming isn't consistent. Sometimes we use 'max' and other
43 | times we use 'last unit'. Can these be made consistent?
44 |
45 | Usage::
46 |
47 | from pymuscle import PyMuscleFibers as Fibers
48 |
49 | motor_unit_count = 60
50 | fibers = Fibers(motor_unit_count)
51 | motor_neuron_firing_rates = np.rand(motor_unit_count) * 10.0
52 | step_size = 0.01
53 | force = fibers.step(motor_neuron_firing_rates, step_size)
54 | """
55 | def __init__(
56 | self,
57 | *args,
58 | force_conversion_factor: float=0.028,
59 | **kwargs
60 | ):
61 | super().__init__(*args, **kwargs)
62 |
63 | # Ratio of newtons (N) to internal arbitrary force units
64 | self.force_conversion_factor = force_conversion_factor
65 |
66 | # Define recovery rates
67 | # Averaged from data in Liu et al. 2002, Table 2
68 | max_recovery_rate = self._max_fatigue_rate / 2.53
69 | # Recovery should ~= fatigue for small units, <= for medium units and
70 | # << for largest units
71 | # Re-uses the same method as calculating fatigabilities.
72 | recovery_range = max_recovery_rate / self._nominal_fatigabilities[0]
73 | self._recovery_rates = self._calc_nominal_fatigabilities(
74 | self.motor_unit_count,
75 | recovery_range,
76 | max_recovery_rate,
77 | self._peak_twitch_forces
78 | )
79 |
80 | def _update_fatigue(
81 | self,
82 | normalized_forces: ndarray,
83 | step_size: float
84 | ) -> None:
85 | """
86 | Updates current twitch forces and contraction times. This overrides
87 | the parent method to add in recovery calculations.
88 |
89 | :param normalized_forces:
90 | Array of scaled forces. Used to weight how much fatigue will be
91 | generated in this step.
92 | :param step_size: How far time has advanced in this step.
93 | """
94 | fatigues = (self._nominal_fatigabilities * normalized_forces) * step_size
95 | self._current_peak_forces -= fatigues
96 |
97 | # Apply recovery for units producing no force
98 | self._apply_recovery(normalized_forces, step_size)
99 |
100 | # Zero out negative values
101 | self._current_peak_forces[self._current_peak_forces < 0] = 0.0
102 |
103 | # Clip max values
104 | over = self._current_peak_forces > self._peak_twitch_forces
105 | self._current_peak_forces[over] = self._peak_twitch_forces[over]
106 |
107 | # Apply fatigue to contraction times
108 | self._update_contraction_times()
109 |
110 | def _apply_recovery(
111 | self,
112 | normalized_forces: ndarray,
113 | step_size: float
114 | ) -> None:
115 | """
116 | Apply recovery to motor units not producing force in this step.
117 |
118 | TODO - Finalize the strategy used below
119 | """
120 | # Find the indices of valid, recovering units.
121 | recovering = normalized_forces <= 0
122 |
123 | # Strategy 1 - Linear recovery at fatigue rates
124 | # recovery = self._nominal_fatigabilities[recovering] * step_size
125 |
126 | # Strategy 2 - Uses inverse of fatigue rates in an asymptotic approach
127 | # peak = self._peak_twitch_forces[recovering]
128 | # current = self._current_peak_forces[recovering]
129 | # recovery_ratio = (peak - current) / peak
130 | # recovery = (self._nominal_fatigabilities[recovering] * recovery_ratio) * step_size
131 |
132 | # Strategy 3 - Uses linear approach with calculated recovery rates
133 | # recovery = self._recovery_rates[recovering] * step_size
134 |
135 | # Strategy 4 - Combine 2 and 3
136 | peak = self._peak_twitch_forces[recovering]
137 | current = self._current_peak_forces[recovering]
138 | recovery_ratio = (peak - current) / peak
139 | recovery = (self._recovery_rates[recovering] * recovery_ratio) * step_size
140 |
141 | self._current_peak_forces[recovering] += recovery
142 |
--------------------------------------------------------------------------------
/docs/conf.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | #
3 | # Configuration file for the Sphinx documentation builder.
4 | #
5 | # This file does only contain a selection of the most common options. For a
6 | # full list see the documentation:
7 | # http://www.sphinx-doc.org/en/master/config
8 |
9 | # -- Path setup --------------------------------------------------------------
10 |
11 | # If extensions (or modules to document with autodoc) are in another directory,
12 | # add these directories to sys.path here. If the directory is relative to the
13 | # documentation root, use os.path.abspath to make it absolute, like shown here.
14 | #
15 | import os
16 | import sys
17 | sys.path.insert(0, os.path.abspath('..'))
18 |
19 | import pymuscle
20 |
21 |
22 | # -- Project information -----------------------------------------------------
23 |
24 | project = 'PyMuscle'
25 | copyright = '2018, Ian Danforth'
26 | author = 'Ian Danforth'
27 |
28 | # The short X.Y version
29 | version = ''
30 | # The full version, including alpha/beta/rc tags
31 | release = '0.0.1'
32 |
33 |
34 | # -- General configuration ---------------------------------------------------
35 |
36 | # If your documentation needs a minimal Sphinx version, state it here.
37 | #
38 | # needs_sphinx = '1.0'
39 |
40 | # Add any Sphinx extension module names here, as strings. They can be
41 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
42 | # ones.
43 | extensions = [
44 | 'sphinx.ext.autosummary',
45 | 'sphinx.ext.autodoc',
46 | 'sphinx_autodoc_typehints',
47 | 'sphinx.ext.intersphinx',
48 | 'sphinx.ext.todo',
49 | 'sphinx.ext.coverage',
50 | 'sphinx.ext.mathjax',
51 | 'sphinx.ext.viewcode',
52 | 'sphinx.ext.githubpages',
53 | ]
54 |
55 | # Autodoc
56 | autodoc_default_flags = ['members']
57 |
58 | # Autosummary
59 | autosummary_generate = True
60 |
61 | # Add any paths that contain templates here, relative to this directory.
62 | templates_path = ['_templates']
63 |
64 | # The suffix(es) of source filenames.
65 | # You can specify multiple suffix as a list of string:
66 | #
67 | # source_suffix = ['.rst', '.md']
68 | source_suffix = '.rst'
69 |
70 | # The master toctree document.
71 | master_doc = 'index'
72 |
73 | # The language for content autogenerated by Sphinx. Refer to documentation
74 | # for a list of supported languages.
75 | #
76 | # This is also used if you do content translation via gettext catalogs.
77 | # Usually you set "language" from the command line for these cases.
78 | language = None
79 |
80 | # List of patterns, relative to source directory, that match files and
81 | # directories to ignore when looking for source files.
82 | # This pattern also affects html_static_path and html_extra_path .
83 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
84 |
85 | # The name of the Pygments (syntax highlighting) style to use.
86 | pygments_style = 'sphinx'
87 |
88 |
89 | # -- Options for HTML output -------------------------------------------------
90 |
91 | # The theme to use for HTML and HTML Help pages. See the documentation for
92 | # a list of builtin themes.
93 | #
94 | # html_theme = 'alabaster'
95 | html_theme = "sphinx_rtd_theme"
96 |
97 | # Theme options are theme-specific and customize the look and feel of a theme
98 | # further. For a list of options available for each theme, see the
99 | # documentation.
100 | #
101 | # html_theme_options = {}
102 |
103 | # Add any paths that contain custom static files (such as style sheets) here,
104 | # relative to this directory. They are copied after the builtin static files,
105 | # so a file named "default.css" will overwrite the builtin "default.css".
106 | html_static_path = ['_static']
107 |
108 | # Custom sidebar templates, must be a dictionary that maps document names
109 | # to template names.
110 | #
111 | # The default sidebars (for documents that don't match any pattern) are
112 | # defined by theme itself. Builtin themes are using these templates by
113 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
114 | # 'searchbox.html']``.
115 | #
116 | # html_sidebars = {}
117 |
118 |
119 | # -- Options for HTMLHelp output ---------------------------------------------
120 |
121 | # Output file base name for HTML help builder.
122 | htmlhelp_basename = 'PyMuscledoc'
123 |
124 |
125 | # -- Options for LaTeX output ------------------------------------------------
126 |
127 | latex_elements = {
128 | # The paper size ('letterpaper' or 'a4paper').
129 | #
130 | # 'papersize': 'letterpaper',
131 |
132 | # The font size ('10pt', '11pt' or '12pt').
133 | #
134 | # 'pointsize': '10pt',
135 |
136 | # Additional stuff for the LaTeX preamble.
137 | #
138 | # 'preamble': '',
139 |
140 | # Latex figure (float) alignment
141 | #
142 | # 'figure_align': 'htbp',
143 | }
144 |
145 | # Grouping the document tree into LaTeX files. List of tuples
146 | # (source start file, target name, title,
147 | # author, documentclass [howto, manual, or own class]).
148 | latex_documents = [
149 | (master_doc, 'PyMuscle.tex', 'PyMuscle Documentation',
150 | 'Ian Danforth', 'manual'),
151 | ]
152 |
153 |
154 | # -- Options for manual page output ------------------------------------------
155 |
156 | # One entry per manual page. List of tuples
157 | # (source start file, name, description, authors, manual section).
158 | man_pages = [
159 | (master_doc, 'pymuscle', 'PyMuscle Documentation',
160 | [author], 1)
161 | ]
162 |
163 |
164 | # -- Options for Texinfo output ----------------------------------------------
165 |
166 | # Grouping the document tree into Texinfo files. List of tuples
167 | # (source start file, target name, title, author,
168 | # dir menu entry, description, category)
169 | texinfo_documents = [
170 | (master_doc, 'PyMuscle', 'PyMuscle Documentation',
171 | author, 'PyMuscle', 'One line description of project.',
172 | 'Miscellaneous'),
173 | ]
174 |
175 |
176 | # -- Extension configuration -------------------------------------------------
177 |
178 | # -- Options for intersphinx extension ---------------------------------------
179 |
180 | # Example configuration for intersphinx: refer to the Python standard library.
181 | intersphinx_mapping = {'https://docs.python.org/': None}
182 |
183 | # -- Options for todo extension ----------------------------------------------
184 |
185 | # If true, `todo` and `todoList` produce output, else they produce nothing.
186 | todo_include_todos = True
--------------------------------------------------------------------------------
/examples/envs/pymunk_arm.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import gym
3 | import pygame
4 | import pymunk
5 | import pymunk.pygame_util
6 | import numpy as np
7 | from pygame.locals import (QUIT, KEYDOWN, K_ESCAPE)
8 | from pymuscle import StandardMuscle as Muscle
9 |
10 |
11 | class PymunkArmEnv(gym.Env):
12 | """
13 | Single joint arm with opposing muscles physically simulated by Pymunk in
14 | a Pygame wrapper.
15 | """
16 |
17 | def __init__(self, apply_fatigue=False):
18 | # Set up our 2D physics simulation
19 | self._init_sim()
20 |
21 | # Add a simulated arm consisting of:
22 | # - bones (rigid bodies)
23 | # - muscle bodies (damped spring constraints)
24 | self.brach, self.tricep = self._add_arm()
25 |
26 | # Instantiate the PyMuscles
27 | self.brach_muscle = Muscle(
28 | apply_peripheral_fatigue=apply_fatigue
29 | )
30 | self.tricep_muscle = Muscle(
31 | apply_peripheral_fatigue=False # Tricep never gets tired in this env
32 | )
33 |
34 | self.frames = 0
35 |
36 | def _init_sim(self):
37 | pygame.init()
38 | screen_width = screen_height = 600
39 | self.screen = pygame.display.set_mode((screen_width, screen_height))
40 | pygame.display.set_caption("Curl Sim")
41 | self.draw_options = pymunk.pygame_util.DrawOptions(self.screen)
42 | self.draw_options.flags = 1 # Disable constraint drawing
43 | self.space = pymunk.Space()
44 | self.space.gravity = (0.0, -980.0)
45 |
46 | def _add_arm(self):
47 | config = {
48 | "arm_center": (self.screen.get_width() / 2,
49 | self.screen.get_height() / 2),
50 | "lower_arm_length": 170,
51 | "lower_arm_starting_angle": 15,
52 | "lower_arm_mass": 10,
53 | "brach_rest_length": 5,
54 | "brach_stiffness": 450,
55 | "brach_damping": 200,
56 | "tricep_rest_length": 30,
57 | "tricep_stiffness": 50,
58 | "tricep_damping": 400
59 | }
60 |
61 | # Upper Arm
62 | upper_arm_length = 200
63 | upper_arm_body = pymunk.Body(body_type=pymunk.Body.STATIC)
64 | upper_arm_body.position = config["arm_center"]
65 | upper_arm_body.angle = np.deg2rad(-45)
66 | upper_arm_line = pymunk.Segment(upper_arm_body, (0, 0), (-upper_arm_length, 0), 5)
67 | upper_arm_line.sensor = True # Disable collision
68 |
69 | self.space.add(upper_arm_body)
70 | self.space.add(upper_arm_line)
71 |
72 | # Lower Arm
73 | lower_arm_body = pymunk.Body(0, 0) # Pymunk will calculate moment based on mass of attached shape
74 | lower_arm_body.position = config["arm_center"]
75 | lower_arm_body.angle = np.deg2rad(config["lower_arm_starting_angle"])
76 | elbow_extension_length = 20
77 | lower_arm_start = (-elbow_extension_length, 0)
78 | lower_arm_line = pymunk.Segment(
79 | lower_arm_body,
80 | lower_arm_start,
81 | (config["lower_arm_length"], 0),
82 | 5
83 | )
84 | lower_arm_line.mass = config["lower_arm_mass"]
85 | lower_arm_line.friction = 1.0
86 |
87 | self.space.add(lower_arm_body)
88 | self.space.add(lower_arm_line)
89 |
90 | # Hand
91 | hand_width = hand_height = 15
92 | start_x = config["lower_arm_length"]
93 | start_y = 14
94 | self.hand_shape = pymunk.Circle(
95 | lower_arm_body,
96 | 20,
97 | (start_x, start_y)
98 | )
99 | self.space.add(self.hand_shape)
100 |
101 | # Pivot (Elbow)
102 | elbow_body = pymunk.Body(body_type=pymunk.Body.STATIC)
103 | elbow_body.position = config["arm_center"]
104 | elbow_joint = pymunk.PivotJoint(elbow_body, lower_arm_body, config["arm_center"])
105 | self.space.add(elbow_joint)
106 |
107 | # Spring (Brachialis Muscle)
108 | brach_spring = pymunk.constraint.DampedSpring(
109 | upper_arm_body,
110 | lower_arm_body,
111 | (-(upper_arm_length * (1 / 2)), 0), # Connect half way up the upper arm
112 | (config["lower_arm_length"] / 5, 0), # Connect near the bottom of the lower arm
113 | config["brach_rest_length"],
114 | config["brach_stiffness"],
115 | config["brach_damping"]
116 | )
117 | self.space.add(brach_spring)
118 |
119 | # Spring (Tricep Muscle)
120 | tricep_spring = pymunk.constraint.DampedSpring(
121 | upper_arm_body,
122 | lower_arm_body,
123 | (-(upper_arm_length * (3 / 4)), 0),
124 | lower_arm_start,
125 | config["tricep_rest_length"],
126 | config["tricep_stiffness"],
127 | config["tricep_damping"]
128 | )
129 | self.space.add(tricep_spring)
130 |
131 | # Elbow stop (prevent under/over extension)
132 | elbow_stop_point = pymunk.Circle(
133 | upper_arm_body,
134 | radius=5,
135 | offset=(-elbow_extension_length, -3)
136 | )
137 | elbow_stop_point.friction = 1.0
138 | self.space.add(elbow_stop_point)
139 |
140 | return brach_spring, tricep_spring
141 |
142 | @staticmethod
143 | def _handle_keys():
144 | for event in pygame.event.get():
145 | if event.type == QUIT:
146 | sys.exit(0)
147 | elif event.type == KEYDOWN and event.key == K_ESCAPE:
148 | sys.exit(0)
149 |
150 | def step(self, input_array, step_size, debug=True):
151 | # Check for user input
152 | self._handle_keys()
153 |
154 | # Scale input to match the expected range of the muscle sim
155 | input_array = np.array(input_array)
156 |
157 | if debug:
158 | print(input_array)
159 |
160 | # Advance the simulation
161 | self.space.step(step_size)
162 | self.frames += 1
163 |
164 | # Advance muscle sim and sync with physics sim
165 | brach_output = self.brach_muscle.step(input_array[0], step_size)
166 | tricep_output = self.tricep_muscle.step(input_array[1], step_size)
167 |
168 | gain = 500
169 | self.brach.stiffness = brach_output * gain
170 | self.tricep.stiffness = tricep_output * gain
171 |
172 | if debug:
173 | print("Brach Total Output: ", brach_output)
174 | print("Tricep Total Output: ", tricep_output)
175 | print("Brach Stiffness: ", self.brach.stiffness)
176 | print("Tricep Stiffness: ", self.tricep.stiffness)
177 |
178 | hand_location = self.hand_shape.body.local_to_world((170, 0))
179 | return hand_location
180 |
181 | def render(self, debug=True):
182 | if debug and (self.draw_options.flags is not 3):
183 | self.draw_options.flags = 3 # Enable constraint drawing
184 |
185 | self.screen.fill((255, 255, 255))
186 | self.space.debug_draw(self.draw_options)
187 | pygame.display.flip()
188 |
189 | def reset(self):
190 | if self.space:
191 | del self.space
192 | self._init_sim()
193 |
--------------------------------------------------------------------------------
/docs/src/readme-template.md:
--------------------------------------------------------------------------------
1 | ### *** Warning: PyMuscle is early stage and under active development. ***
2 |
3 | # PyMuscle
4 | [](https://travis-ci.org/iandanforth/pymuscle)
5 |
6 | PyMuscle provides a motor unit based model of skeletal muscle. It simulates the
7 | relationship between excitatory input and motor-unit output as well as fatigue
8 | over time.
9 |
10 | It is compatible with [OpenAI Gym](https://gym.openai.com) environments and is
11 | intended to be useful for researchers in the machine learning community.
12 |
13 | PyMuscle can be used to enhance the realism of motor control for simulated
14 | agents. To get you started we provide a [toy example project](https://github.com/iandanforth/pymuscle/tree/master/examples)
15 | which uses PyMuscle in a simulation of arm curl and extension.
16 |
17 | Out of the box we provide a motor neuron pool model and a muscle fiber model
18 | based on "A motor unit-based model of muscle fatigue"
19 | ([Potvin and Fuglevand, 2017](http://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1005581)).
20 | If you use this library as part of your research please cite that paper.
21 |
22 | We hope to extend this model and support alternative models in the future.
23 |
24 | ## More about PyMuscle
25 |
26 | Motor control in biological creatures is complex. PyMuscle allows you to capture
27 | some of that complexity while remaining [performant](#performance). It provides
28 | greater detail than sending torque values to simulated motors-as-joints but
29 | less detail (and computational cost) than a full biochemical model.
30 |
31 | PyMuscle is not tied to a particular physics library and can be used with a
32 | variety of muscle body simulations. PyMuscle focuses on the relationship between
33 | control signals (excitatory inputs to motor neurons) and per-motor-unit output.
34 |
35 | Motor unit output is dimensionless but can be interpreted as force. It can also
36 | be used as a proxy for the contractile state of muscle bodies in the physics
37 | sim of your choice.
38 |
39 | # Background
40 |
41 | ## Motor Units
42 |
43 |
44 |
45 | A motor unit is the combination of a motor neuron and the muscle fibers to which
46 | the neuron makes connections. Skeletal muscles are made up of many muscle fibers.
47 | For a given motor unit a single motor neuron will have an axon that branches
48 | and innervates a subset of the fibers in a muscle. Muscle fibers usually
49 | belong to only one motor unit.
50 |
51 | Muscles may have anywhere from a few dozen to thousands of motor units. The
52 | human arm, for example, has 30 some muscles and is innervated by [approximately 35,000 axons](https://onlinelibrary.wiley.com/doi/abs/10.1002/ana.25018)
53 | from motor neurons.
54 |
55 | The brain controls muscles by sending signals to motor units and receiving
56 | signals from mechanoreceptors embedded in muscles and the skin. In animals all
57 | the motor units an animal will ever have are present from birth and learning to
58 | produce smooth coordinated motion through control of those units is a significant
59 | part of the developmental process.
60 |
61 | ## Control
62 |
63 | Motor units are recruited in an orderly fashion to produce varying levels of
64 | muscle force.
65 |
66 | The cell bodies of motor neurons for a given muscle cluster together in the
67 | spinal cord in what are known as motor neuron pools, columns, or nuclei.
68 | Generally motor neurons in a pool can be thought of as all getting the same
69 | activation inputs. This input is the combination of dozens if not hundreds of
70 | separate inputs from interneurons and upper motor neurons carrying signals from
71 | the brain and mechanoreceptors in the body.
72 |
73 | In a voluntary contraction of a muscle, say in curling your arm, the input
74 | to the motor neuron pool for the bicep muscle will ramp up, recruiting more
75 | and more motor units, starting from the weakest motor units to stronger ones.
76 |
77 | Over time motor neurons and muscle fibers can't produce the same level of force
78 | for the same level of activation input. This is called fatigue. The brain must
79 | compensate for the fatigue if it wants to maintain a given force or perform
80 | the same action again and again in the same way.
81 |
82 | # Installation
83 |
84 | ## Requirements
85 |
86 | Python 3.6+
87 |
88 | ## Install
89 |
90 | ```
91 | pip install pymuscle
92 | ```
93 |
94 | # Getting Started
95 |
96 | ### Minimal example
97 |
98 | The Muscle class provides the primary API for the library. A Muscle can be
99 | heavily customized but here we use mainly default values. A PotvinMuscle
100 | instantiated with 120 motor units has the distribution of strengths, recruitment
101 | thresholds, and fatigue properties as used in the experiments of Potvin and
102 | Fuglevand, 2017.
103 |
104 | ```python
105 | {{'../../examples/minimal-example.py'}}
106 | ```
107 |
108 | This will open a browser window with the produced chart. It should look like this:
109 |
110 |
111 |
112 | ### Familiar with OpenAI's Gym?
113 |
114 | Make sure you have the following installed
115 |
116 | ```
117 | pip install gym pygame pymunk
118 | ```
119 |
120 | then try out the [example project](https://github.com/iandanforth/pymuscle/tree/master/examples)
121 |
122 | # Versioning Plan
123 |
124 | PyMuscle is in a pre-alpha state. Expect regular breaking changes.
125 |
126 | We expect to stabilize the API for 1.0 and introduce breaking changes only
127 | during major releases.
128 |
129 | This library tries to provide empirically plausible behavior. As new research is
130 | released or uncovered we will update the underlying model. Non-bug-fix changes
131 | that would alter the output of the library will be integrated in major releases.
132 |
133 | If you know of results you believe should be integrated please let us know. See
134 | the [Contributing](#contributing) section.
135 |
136 | # Contributing
137 |
138 | We encourage you to contribute! Specifically we'd love to hear about and feature
139 | projects using PyMuscle.
140 |
141 | For all issues please search the [existing issues](https://github.com/iandanforth/pymuscle/issues) before submitting.
142 |
143 | - [Bug Reports](https://github.com/iandanforth/pymuscle/issues/new?template=bug_report.md)
144 | - [Enhancement requests](https://github.com/iandanforth/pymuscle/issues/new?template=feature_request.md)
145 | - [Suggest research](https://github.com/iandanforth/pymuscle/issues/new?template=research-submission.md) that can better inform the model
146 |
147 | _Before_ opening a pull request please:
148 |
149 | - See if there is an open ticket for this issue
150 | - If the ticket is tagged 'Help Needed' comment to note that you intend to work on this issue
151 | - If the ticket is *not* tagged, comment that you would like to work on the issue
152 | - We will then discuss the priority, timing and expectations around the issue.
153 | - If there is no open ticket, create one
154 | - We prefer to discuss the implications of changes before you write code!
155 |
156 |
157 | ## Development
158 |
159 | If you want to help develop the PyMuscle library itself the following may help.
160 |
161 | Clone this repository
162 |
163 | ```
164 | git clone git@github.com:iandanforth/pymuscle.git
165 | cd pymuscle
166 | ```
167 |
168 | Install [pipenv](https://docs.pipenv.org/). (The modern combination of pip and
169 | virtual environments.)
170 |
171 | ```
172 | pip install pipenv
173 | ```
174 |
175 | If this throws a permissions error you will need to to run this with 'sudo'
176 |
177 | ```
178 | sudo pip install pipenv
179 | ```
180 |
181 | Install dependencies and start a clean python environment
182 |
183 | ```
184 | pipenv install
185 | pipenv shell
186 | ```
187 |
188 | To exit this python environment
189 |
190 | ```
191 | exit
192 | ```
193 |
194 | or close your terminal and start a new one.
195 |
196 | # Performance
197 |
198 | PyMuscle aims to be fast. We use Numpy to get fast vector computation. PyMuscle
199 | uses only a single process today but may be extended to multi-process in
200 | the future and to GPUs through the integration of [PyTorch](https://pytorch.org/).
201 |
202 | # Limitations
203 |
204 | ## Scope
205 |
206 | PyMuscle is concerned with inputs to motor unit neurons, the outputs of those
207 | motor units, and the changes to that system over time. It does not model the
208 | dynamics of the muscle body itself or the impact of dynamic motion on this
209 | motor unit input/output relationship.
210 |
211 | ## Recovery
212 |
213 | Potvin and Fuglevand 2017 explicitly models fatigue but *not* recovery. We
214 | eagerly await the updated model from Potvin which will included a model of
215 | recovery.
216 |
217 | ## Proprioception
218 |
219 | This library does not directly provide any feedback signals for control. The
220 | example project shows how to integrate PyMuscle with a physics simulation to
221 | get simulated output forces and stretch and strain values derived from the
222 | state of the simulated muscle body. (In the example this is a damped spring
223 | but a Hill-type, or more complex model could also be used.)
224 |
225 | Fatigue could be used as a feedback signal but this will need to be calculated
226 | from the states of the motor units.
227 |
--------------------------------------------------------------------------------
/pymuscle/muscle.py:
--------------------------------------------------------------------------------
1 | """
2 | Contains base Muscle class and its immediate descendants.
3 | """
4 |
5 | import numpy as np
6 | from typing import Union
7 |
8 | from .potvin_fuglevand_2017_muscle_fibers import PotvinFuglevand2017MuscleFibers
9 | from .potvin_fuglevand_2017_motor_neuron_pool import PotvinFuglevand2017MotorNeuronPool
10 | from .pymuscle_fibers import PyMuscleFibers
11 | from .model import Model
12 |
13 |
14 | class Muscle(object):
15 | """
16 | A user-created :class:`Muscle ` object.
17 |
18 | Used to simulate the input-output relationship between motor neuron
19 | excitation and muscle fibers contractile state over time.
20 |
21 | :param motor_neuron_pool_model:
22 | The motor neuron pool implementation to use with this muscle.
23 | :param muscle_fibers_model:
24 | The muscle fibers model implementation to use with this muscle.
25 | :param motor_unit_count: How many motor units comprise this muscle.
26 |
27 | Usage::
28 |
29 | from pymuscle import (Muscle,
30 | PotvinFuglevand2017MotorNeuronPool as Pool,
31 | PotvinFuglevand2017MuscleFibers as Fibers)
32 |
33 | motor_unit_count = 60
34 | muscle = Muscle(
35 | Pool(motor_unit_count),
36 | Fibers(motor_unit_count),
37 | )
38 | excitation = 32.0
39 | force = muscle.step(excitation, 1 / 50.0)
40 | """
41 |
42 | def __init__(
43 | self,
44 | motor_neuron_pool_model: Model,
45 | muscle_fibers_model: Model,
46 | ):
47 | assert motor_neuron_pool_model.motor_unit_count == \
48 | muscle_fibers_model.motor_unit_count
49 |
50 | self._pool = motor_neuron_pool_model
51 | self._fibers = muscle_fibers_model
52 |
53 | @property
54 | def motor_unit_count(self):
55 | return self._pool.motor_unit_count
56 |
57 | @property
58 | def max_excitation(self):
59 | return self._pool.max_excitation
60 |
61 | @property
62 | def current_forces(self):
63 | return self._fibers.current_forces
64 |
65 | def step(
66 | self,
67 | motor_pool_input: Union[int, float, np.ndarray],
68 | step_size: float
69 | ) -> float:
70 | """
71 | Advances the muscle model one step.
72 |
73 | :param motor_pool_input:
74 | Either a single value or an array of values representing the
75 | excitatory input to the motor neuron pool for this muscle
76 | :param step_size:
77 | How far to advance the simulation in time for this step.
78 | """
79 |
80 | # Expand a single input to the muscle to a full array
81 | if isinstance(motor_pool_input, float) or \
82 | isinstance(motor_pool_input, int):
83 | motor_pool_input = np.full(
84 | self._pool.motor_unit_count,
85 | motor_pool_input
86 | )
87 |
88 | # Ensure we're really passing an ndarray to _pool.step()
89 | input_as_array = np.array(motor_pool_input)
90 |
91 | motor_pool_output = self._pool.step(input_as_array, step_size)
92 | return self._fibers.step(motor_pool_output, step_size)
93 |
94 |
95 | class PotvinFuglevandMuscle(Muscle):
96 | """
97 | A thin wrapper around :class:`Muscle ` which pre-selects the
98 | Potvin fiber and motor neuron models.
99 | """
100 |
101 | def __init__(
102 | self,
103 | motor_unit_count: int,
104 | apply_central_fatigue: bool = True,
105 | apply_peripheral_fatigue: bool = True
106 | ):
107 | pool = PotvinFuglevand2017MotorNeuronPool(
108 | motor_unit_count,
109 | apply_fatigue=apply_central_fatigue
110 | )
111 | fibers = PotvinFuglevand2017MuscleFibers(
112 | motor_unit_count,
113 | apply_fatigue=apply_peripheral_fatigue
114 | )
115 |
116 | super().__init__(
117 | motor_neuron_pool_model=pool,
118 | muscle_fibers_model=fibers
119 | )
120 |
121 |
122 | class StandardMuscle(Muscle):
123 | """
124 | A wrapper around :class:`Muscle ` which pre-selects the
125 | Potvin motor neuron model and the PyMuscle specific fiber model.
126 |
127 | In addition this class implements an API (through the primary step()
128 | method) where inputs to and outputs from the muscle are in the range
129 | 0.0 to 1.0.
130 |
131 | The API for this class is oriented toward use along side physics
132 | simulations.
133 |
134 | This muscle does *not* include central (motor neuron) fatigue as the
135 | equations for recovery are not yet available.
136 |
137 | This muscle does include both *peripheral* fatigue and recovery.
138 |
139 | :param max_force: Maximum voluntary isometric force this muscle can produce
140 | when fully rested and at maximum excitation. (Newtons)
141 | This along with the force_conversion_factor will determine the number
142 | of simulated motor units.
143 | :param force_conversion_factor: This library uses biological defaults to
144 | convert the desired max_force into a number of motor units which
145 | make up a muscle. This can result in a large number of motor units
146 | which may be slow. To improve performance (but diverge from biology)
147 | you can change this force conversion factor.
148 |
149 | Note: It is likely the default value here will change with major
150 | versions as better biological data is found.
151 | """
152 | def __init__(
153 | self,
154 | max_force: float = 32.0,
155 | force_conversion_factor: float = 0.0123,
156 | apply_central_fatigue: bool = False,
157 | apply_peripheral_fatigue: bool = True
158 | ):
159 |
160 | # Maximum voluntary isometric force this muscle will be able to produce
161 | self.max_force = max_force
162 |
163 | # Ratio of newtons (N) to internal arbitrary force units
164 | self.force_conversion_factor = force_conversion_factor
165 |
166 | motor_unit_count = self.force_to_motor_unit_count(
167 | self.max_force,
168 | self.force_conversion_factor,
169 | )
170 |
171 | pool = PotvinFuglevand2017MotorNeuronPool(
172 | motor_unit_count,
173 | apply_fatigue=apply_central_fatigue
174 | )
175 | fibers = PyMuscleFibers(
176 | motor_unit_count,
177 | force_conversion_factor=force_conversion_factor,
178 | apply_fatigue=apply_peripheral_fatigue
179 | )
180 |
181 | super().__init__(
182 | motor_neuron_pool_model=pool,
183 | muscle_fibers_model=fibers
184 | )
185 |
186 | # Max output in arbitrary units
187 | self.max_arb_output = sum(self._fibers._peak_twitch_forces)
188 |
189 | @staticmethod
190 | def force_to_motor_unit_count(
191 | max_force: float,
192 | conversion_factor: float
193 | ) -> int:
194 | # This takes the relationship between force production
195 | # and number of motor units from Fuglevand 93 and solves
196 | # for motor units given desired force.
197 |
198 | # The reference muscle is the first dorsal interossei
199 | # https://en.wikipedia.org/wiki/Dorsal_interossei_of_the_hand
200 |
201 | # The number of motor units for the FDI was estimated by
202 | # Feinstein et al. (1955) at 119.
203 | # https://www.ncbi.nlm.nih.gov/pubmed/14349537
204 | #
205 | # Caveats:
206 | # - This estimate was from 1 dissection of an adult male
207 | # - This esimate counted 'large nerve fibers' and then assumed
208 | # 40% of them would be afferent.
209 |
210 | # The Fuglevand experiments use 120 motor units. The peak twitch
211 | # forces are calculated in the range of 0-100 arbitrary force units
212 | # with each motor unit being assigned a peak twitch force according
213 | # to the exponential function outlined in Fuglevand 93.
214 | #
215 | # The total possible force units this muscle could produce, after
216 | # assignment of peak twitch forces, is the sum of those peak twitch
217 | # forces or ~2609.03 arbitrary units.
218 |
219 | # The maximum voluntary force (MVC) for the FDI was taken from
220 | # Jahanmir-Zezhad et al.
221 | # https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5450824/
222 | # MVC = 32 Newtons (N)
223 |
224 | # Thus if a modeled FDI with 120 motor units produces an MVC of
225 | # 2,216 units and a real FDI produces an MVC of 32N we can
226 | # relate them as follows
227 | #
228 | # conversion_factor = 32N / 2609.03 units = 0.01226509469 N/unit
229 |
230 | # See: https://www.desmos.com/calculator/b9xzsaqs1g
231 | # m = max_force
232 | # c = conversion_factor
233 | # u = motor unit count
234 | # u = 1 / ln( ((1-m/c) / (e^4.6 - m/c)))^(1/4.6) )
235 | r = (max_force / conversion_factor)
236 | n2 = 1 - r
237 | d2 = np.exp(4.6) - r
238 | inner = np.power((n2 / d2), (1 / 4.6))
239 | d = np.log(inner) # This is natural log by default (ln)
240 | muf = 1 / d
241 | muc = int(np.ceil(muf))
242 |
243 | return muc
244 |
245 | def get_central_fatigue(self):
246 | raise NotImplementedError
247 |
248 | def get_peripheral_fatigue(self) -> float:
249 | """
250 | Returns fatigue level in the range 0.0 to 1.0 where:
251 |
252 | 0.0 - Completely rested
253 | 1.0 - Completely fatigued
254 | """
255 | fatigue = 1 - sum(self._fibers.current_peak_forces) / self.max_arb_output
256 | return fatigue
257 |
258 | def step(
259 | self,
260 | motor_pool_input: Union[int, float, np.ndarray],
261 | step_size: float
262 | ) -> float:
263 | """
264 | Advances the muscle model one step.
265 |
266 | :param motor_pool_input:
267 | Either a single value or an array of values representing the
268 | excitatory input to the motor neuron pool for this muscle.
269 | Range is 0.0 - 1.0.
270 | :param step_size:
271 | How far to advance the simulation in time for this step.
272 | """
273 |
274 | # Rescale the input to the underlying range for the motor pool
275 | motor_pool_input *= self.max_excitation
276 | arb_output = super().step(motor_pool_input, step_size)
277 | # Rescale the output such that it is in the range 0.0 - 1.0
278 | scaled_output = arb_output / self.max_arb_output
279 | return scaled_output
280 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PyMuscle
2 | [](https://travis-ci.org/iandanforth/pymuscle)
3 |
4 | PyMuscle provides a motor unit based model of skeletal muscle. It simulates the
5 | relationship between excitatory input and motor-unit output as well as fatigue
6 | over time.
7 |
8 | It is compatible with [OpenAI Gym](https://gym.openai.com) environments and is
9 | intended to be useful for researchers in the machine learning community.
10 |
11 | PyMuscle can be used to enhance the realism of motor control for simulated
12 | agents. To get you started we provide a [toy example project](https://github.com/iandanforth/pymuscle/tree/master/examples)
13 | which uses PyMuscle in a simulation of arm curl and extension.
14 |
15 | Out of the box we provide a motor neuron pool model and a muscle fiber model
16 | based on "A motor unit-based model of muscle fatigue"
17 | ([Potvin and Fuglevand, 2017](http://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1005581)).
18 | If you use this library as part of your research please cite that paper.
19 |
20 | We hope to extend this model and support alternative models in the future.
21 |
22 | ## More about PyMuscle
23 |
24 | Motor control in biological creatures is complex. PyMuscle allows you to capture
25 | some of that complexity while remaining [performant](#performance). It provides
26 | greater detail than sending torque values to simulated motors-as-joints but
27 | less detail (and computational cost) than a full biochemical model.
28 |
29 | PyMuscle is not tied to a particular physics library and can be used with a
30 | variety of muscle body simulations. PyMuscle focuses on the relationship between
31 | control signals (excitatory inputs to motor neurons) and per-motor-unit output.
32 |
33 | Motor unit output is dimensionless but can be interpreted as force. It can also
34 | be used as a proxy for the contractile state of muscle bodies in the physics
35 | sim of your choice.
36 |
37 | # Background
38 |
39 | ## Motor Units
40 |
41 |
42 |
43 | A motor unit is the combination of a motor neuron and the muscle fibers to which
44 | the neuron makes connections. Skeletal muscles are made up of many muscle fibers.
45 | For a given motor unit a single motor neuron will have an axon that branches
46 | and innervates a subset of the fibers in a muscle. Muscle fibers usually
47 | belong to only one motor unit.
48 |
49 | Muscles may have anywhere from a few dozen to thousands of motor units. The
50 | human arm, for example, has 30 some muscles and is innervated by [approximately 35,000 axons](https://onlinelibrary.wiley.com/doi/abs/10.1002/ana.25018)
51 | from motor neurons.
52 |
53 | The brain controls muscles by sending signals to motor units and receiving
54 | signals from mechanoreceptors embedded in muscles and the skin. In animals all
55 | the motor units an animal will ever have are present from birth and learning to
56 | produce smooth coordinated motion through control of those units is a significant
57 | part of the developmental process.
58 |
59 | ## Control
60 |
61 | Motor units are recruited in an orderly fashion to produce varying levels of
62 | muscle force.
63 |
64 | The cell bodies of motor neurons for a given muscle cluster together in the
65 | spinal cord in what are known as motor neuron pools, columns, or nuclei.
66 | Generally motor neurons in a pool can be thought of as all getting the same
67 | activation inputs. This input is the combination of dozens if not hundreds of
68 | separate inputs from interneurons and upper motor neurons carrying signals from
69 | the brain and mechanoreceptors in the body.
70 |
71 | In a voluntary contraction of a muscle, say in curling your arm, the input
72 | to the motor neuron pool for the bicep muscle will ramp up, recruiting more
73 | and more motor units, starting from the weakest motor units to stronger ones.
74 |
75 | Over time motor neurons and muscle fibers can't produce the same level of force
76 | for the same level of activation input. This is called fatigue. The brain must
77 | compensate for the fatigue if it wants to maintain a given force or perform
78 | the same action again and again in the same way.
79 |
80 | # Installation
81 |
82 | ## Requirements
83 |
84 | Python 3.6+
85 |
86 | ## Install
87 |
88 | ```
89 | pip install pymuscle
90 | ```
91 |
92 | # Getting Started
93 |
94 | #### Not a Machine Learning researcher? Please see [Getting Started for Physiologists](physio-readme.md)
95 |
96 | ### Minimal example
97 |
98 | The Muscle class provides the primary API for the library. A Muscle can be
99 | heavily customized but here we use mainly default values. A PotvinMuscle
100 | instantiated with 120 motor units has the distribution of strengths, recruitment
101 | thresholds, and fatigue properties as used in the experiments of Potvin and
102 | Fuglevand, 2017.
103 |
104 | ```python
105 | from pymuscle import StandardMuscle as Muscle
106 | from pymuscle.vis import PotvinChart
107 |
108 | muscle = Muscle()
109 |
110 | # Set up the simulation parameters
111 | sim_duration = 200 # seconds
112 | frames_per_second = 50
113 | step_size = 1 / frames_per_second
114 | total_steps = int(sim_duration / step_size)
115 |
116 | # Use a constant level of excitation to more easily observe fatigue
117 | excitation = 0.6 # Range is 0.0 to 1.0
118 |
119 | total_outputs = []
120 | outputs_by_unit = []
121 | print("Starting simulation ...")
122 | for i in range(total_steps):
123 | # Calling step() updates the simulation and returns the total output
124 | # produced by the muscle during this step for the given excitation level.
125 | total_output = muscle.step(excitation, step_size)
126 | total_outputs.append(total_output)
127 | # You can also introspect the muscle to see the forces being produced
128 | # by each motor unit.
129 | output_by_unit = muscle.current_forces
130 | outputs_by_unit.append(output_by_unit)
131 | if (i % (frames_per_second * 10)) == 0:
132 | print("Sim time - {} seconds ...".format(int(i / frames_per_second)))
133 |
134 | # Visualize the behavior of the motor units over time
135 | print("Creating chart ...")
136 | chart = PotvinChart(
137 | outputs_by_unit,
138 | step_size
139 | )
140 | # Things to note in the chart:
141 | # - Some motor units (purple) are never recruited at this level of excitation
142 | # - Some motor units become completely fatigued in this short time
143 | # - Some motor units stabilize and decrease their rate of fatigue
144 | # - Forces from the weakest motor units are almost constant the entire time
145 | chart.display()
146 |
147 | ```
148 |
149 | This will open a browser window with the produced chart. It should look like this:
150 |
151 |
152 |
153 | ### Familiar with OpenAI's Gym?
154 |
155 | Make sure you have the following installed
156 |
157 | ```
158 | pip install gym pygame pymunk
159 | ```
160 |
161 | then try out the [example project](https://github.com/iandanforth/pymuscle/tree/master/examples)
162 |
163 | # Versioning Plan
164 |
165 | PyMuscle is in an alpha state. Expect regular breaking changes.
166 |
167 | We expect to stabilize the API for 1.0 and introduce breaking changes only
168 | during major releases.
169 |
170 | This library tries to provide empirically plausible behavior. As new research is
171 | released or uncovered we will update the underlying model. Non-bug-fix changes
172 | that would alter the output of the library will be integrated in major releases.
173 |
174 | If you know of results you believe should be integrated please let us know. See
175 | the [Contributing](#contributing) section.
176 |
177 | # Contributing
178 |
179 | We encourage you to contribute! Specifically we'd love to hear about and feature
180 | projects using PyMuscle.
181 |
182 | For all issues please search the [existing issues](https://github.com/iandanforth/pymuscle/issues) before submitting.
183 |
184 | - [Bug Reports](https://github.com/iandanforth/pymuscle/issues/new?template=bug_report.md)
185 | - [Enhancement requests](https://github.com/iandanforth/pymuscle/issues/new?template=feature_request.md)
186 | - [Suggest research](https://github.com/iandanforth/pymuscle/issues/new?template=research-submission.md) that can better inform the model
187 |
188 | _Before_ opening a pull request please:
189 |
190 | - See if there is an open ticket for this issue
191 | - If the ticket is tagged 'Help Needed' comment to note that you intend to work on this issue
192 | - If the ticket is *not* tagged, comment that you would like to work on the issue
193 | - We will then discuss the priority, timing and expectations around the issue.
194 | - If there is no open ticket, create one
195 | - We prefer to discuss the implications of changes before you write code!
196 |
197 |
198 | ## Development
199 |
200 | If you want to help develop the PyMuscle library itself the following may help.
201 |
202 | Clone this repository
203 |
204 | ```
205 | git clone git@github.com:iandanforth/pymuscle.git
206 | cd pymuscle
207 | pip install -r requirements-dev.txt
208 | python setup.py develop
209 | pytest
210 | ```
211 |
212 | # Performance
213 |
214 | PyMuscle aims to be fast. We use Numpy to get fast vector computation. If you
215 | find that some part of the library is not fast enough for your use case please
216 | [open a ticket](https://github.com/iandanforth/pymuscle/issues) and let us know.
217 |
218 | # Limitations
219 |
220 | ## Scope
221 |
222 | PyMuscle is concerned with inputs to motor unit neurons, the outputs of those
223 | motor units, and the changes to that system over time. It does not model the
224 | dynamics of the muscle body itself or the impact of dynamic motion on this
225 | motor unit input/output relationship.
226 |
227 | ## Recovery
228 |
229 | Potvin and Fuglevand 2017 explicitly models fatigue but *not* recovery. We
230 | eagerly await the updated model from Potvin which will included a model of
231 | recovery.
232 |
233 | Until then the `StandardMuscle` class, which builds on the Potvin and Fuglevand
234 | base classes, implements peripheral (muscle fiber) recovery as this is a
235 | relatively simple process but disables central (motor unit fatigue).
236 |
237 | ## Proprioception
238 |
239 | Instances of `StandardMuscle` implement a `get_peripheral_fatigue()` method
240 | to allow users to track fatigue state of each muscle. Other than that this
241 | library does not directly provide any feedback signals for control. The
242 | example projects show how to integrate PyMuscle with a physics simulation to
243 | get simulated output forces and stretch and strain values derived from the
244 | state of the simulated muscle body. (In the example this is a damped spring
245 | but a Hill-type, or more complex model could also be used.)
246 |
--------------------------------------------------------------------------------
/pymuscle/potvin_fuglevand_2017_motor_neuron_pool.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | from numpy import ndarray
3 | from typing import Dict, Any
4 |
5 | from .model import Model
6 |
7 |
8 | class PotvinFuglevand2017MotorNeuronPool(Model):
9 | """
10 | Encapsulates the motor neuron portion of the motor unit model.
11 |
12 | The name of each parameter as it appears in Potvin, 2017 is in parentheses.
13 | If a parameter does not appear in the paper but does appear in the
14 | accompanying Matlab code, the variable name from the Matlab code is used in
15 | the parentheses.
16 |
17 | :param motor_unit_count: Number of motor units in the muscle (n)
18 | :param max_recruitment_threshold:
19 | Max excitation required by a motor unit within the pool before
20 | firing (RR)
21 | :param firing_gain:
22 | The slope of firing rate by excitation above threshold (g)
23 | :param min_firing_rate:
24 | The minimum firing rate for a motor neuron above threshold (minR)
25 | :param max_firing_rate_first_unit:
26 | Max firing rate for the first motor unit (maxR(1))
27 | :param max_firing_rate_last_unit:
28 | Max firing rate for the last motor unit (maxR(last))
29 | :param derecruitment_delta:
30 | Absolute minimum firing rate = min_firing_rate - derecruitment_delta
31 | (d)
32 | :param adaptation_magnitude:
33 | Magnitude of adaptation for different levels of excitation.(phi)
34 | :param adaptation_time_constant:
35 | Time constant for motor neuron adaptation (tau). Default based on
36 | Revill & Fuglevand (2011)
37 | :param max_duration:
38 | Longest duration of motor unit activity that will be recorded. The
39 | default value should be >> than the time it takes to fatigue all
40 | fibers. Helps prevent unbounded values.
41 | :apply_fatigue: Whether to calculate and apply central fatigue.
42 |
43 | Usage::
44 |
45 | from pymuscle import PotvinFuglevand2017MotorNeuronPool as Pool
46 |
47 | motor_unit_count = 60
48 | pool = Pool(motor_unit_count)
49 | excitation = np.full(motor_unit_count, 10.0)
50 | step_size = 1 / 50.0
51 | firing_rates = pool.step(excitation, step_size)
52 | """
53 | def __init__(
54 | self,
55 | motor_unit_count: int,
56 | max_recruitment_threshold: int = 50,
57 | firing_gain: float = 1.0,
58 | min_firing_rate: int = 8,
59 | max_firing_rate_first_unit: int = 35,
60 | max_firing_rate_last_unit: int = 25,
61 | derecruitment_delta: int = 2,
62 | adaptation_magnitude: float = 0.67,
63 | adaptation_time_constant: float = 22.0,
64 | max_duration: float = 20000.0,
65 | apply_fatigue: bool = True
66 | ):
67 | self._recruitment_thresholds = self._calc_recruitment_thresholds(
68 | motor_unit_count,
69 | max_recruitment_threshold
70 | )
71 |
72 | self._peak_firing_rates = self._calc_peak_firing_rates(
73 | max_firing_rate_first_unit,
74 | max_firing_rate_last_unit,
75 | max_recruitment_threshold,
76 | self._recruitment_thresholds
77 | )
78 |
79 | self._recruitment_durations = np.zeros(motor_unit_count)
80 |
81 | # Assign additional non-public attributes
82 | self._max_recruitment_threshold = max_recruitment_threshold
83 | self._firing_gain = firing_gain
84 | self._min_firing_rate = min_firing_rate
85 | self._derecruitment_delta = derecruitment_delta
86 | self._adaptation_magnitude = adaptation_magnitude
87 | self._adaptation_time_constant = adaptation_time_constant
88 | self._max_duration = max_duration
89 | self._apply_fatigue = apply_fatigue
90 |
91 | # Assign public attributes
92 | self.motor_unit_count = motor_unit_count
93 |
94 | # Calculate the excitation required to bring the pool to
95 | # maximum firing.
96 | m_e = self._max_recruitment_threshold \
97 | + (max_firing_rate_last_unit - self._min_firing_rate) \
98 | / self._firing_gain
99 | self.max_excitation = m_e
100 |
101 | # Pre-calculate firing rates for all motor neurons across a range of
102 | # possible excitation levels.
103 | self._firing_rates_by_excitation: Dict[float, Any] = {}
104 |
105 | def _calc_adapted_firing_rates(
106 | self,
107 | excitations: ndarray,
108 | step_size: float
109 | ) -> ndarray:
110 | """
111 | Calculate the firing rate for the given excitation including motor
112 | neuron fatigue (adaptation).
113 |
114 | :param excitations:
115 | Array of excitation levels to use as input to motor neurons.
116 | :param step_size: How far to advance time in this step.
117 |
118 | """
119 | firing_rates = self._calc_firing_rates(excitations)
120 | adaptations = self._calc_adaptations(firing_rates)
121 | adapted_firing_rates = firing_rates - adaptations
122 |
123 | # Apply fatigue as a last step
124 | self._update_recruitment_durations(firing_rates, step_size)
125 |
126 | return adapted_firing_rates
127 |
128 | def _calc_firing_rates(self, excitations: ndarray) -> ndarray:
129 | """
130 | Calculates firing rates on a per motor neuron basis for the given
131 | array of excitations.
132 |
133 | :param excitations:
134 | Array of excitation levels to use as input to motor neurons.
135 | """
136 | if self._firing_rates_by_excitation:
137 | excitation = excitations[0] # TODO - Support variations
138 | firing_rates = self._firing_rates_by_excitation[excitation]
139 | else:
140 | firing_rates = self._inner_calc_firing_rates(
141 | excitations,
142 | self._recruitment_thresholds,
143 | self._firing_gain,
144 | self._min_firing_rate,
145 | self._peak_firing_rates
146 | )
147 |
148 | return firing_rates
149 |
150 | @staticmethod
151 | def _inner_calc_firing_rates(
152 | excitations: ndarray,
153 | thresholds: ndarray,
154 | gain: float,
155 | min_firing_rate: int,
156 | peak_firing_rates: ndarray
157 | ) -> ndarray:
158 | """
159 | Pure function to do actual calculation of firing rates.
160 |
161 | :param excitations:
162 | Array of excitation levels to use as input to motor neurons.
163 | :param thresholds:
164 | Array of minimum firing rates required for motor neuron activity
165 | :param gain: How firing rates scale with excitations
166 | :param peak_firing_rates: Maximum allowed firing rates per neuron
167 | """
168 |
169 | firing_rates = excitations - thresholds
170 | firing_rates += min_firing_rate
171 | below_thresh_indices = firing_rates < min_firing_rate
172 | firing_rates[below_thresh_indices] = 0
173 | firing_rates *= gain
174 |
175 | # Check for max values
176 | above_peak_indices = firing_rates > peak_firing_rates
177 | firing_rates[above_peak_indices] = peak_firing_rates[above_peak_indices]
178 |
179 | return firing_rates
180 |
181 | def _update_recruitment_durations(
182 | self,
183 | firing_rates: ndarray,
184 | step_size: float
185 | ) -> None:
186 | """
187 | Increment the on duration for each on motor unit by step_size
188 |
189 | :param firing_rates: Array of activities for each motor neuron.
190 | :param step_size: How far to advance time in this step.
191 | """
192 | # If we don't update durations, no fatigue will occur.
193 | if not self._apply_fatigue:
194 | return
195 |
196 | on = firing_rates > 0
197 | self._recruitment_durations[on] += step_size
198 | # TODO: Enable as a recovery mechanism
199 | # off = firing_rates = 0
200 | # self._recruitment_durations[off] -= 0
201 | # Prevent overflows
202 | over = self._recruitment_durations > self._max_duration
203 | self._recruitment_durations[over] = self._max_duration
204 | # Can't be less than zero
205 | under = self._recruitment_durations < 0
206 | self._recruitment_durations[under] = 0
207 |
208 | def _calc_adaptations(self, firing_rates: ndarray) -> ndarray:
209 | """
210 | Calculate the adaptation rates for each neuron based on current
211 | activity levels. Applies central fatigue.
212 |
213 | :param firing_rates: Array of activities for each motor neuron.
214 | """
215 | adapt_curve = self._calc_adaptations_curve(firing_rates)
216 | # From Eq. (12)
217 | exponent = -1 * (self._recruitment_durations / self._adaptation_time_constant)
218 | adapt_scale = 1 - np.exp(exponent)
219 | adaptations = adapt_curve * adapt_scale
220 | # Zero out negative values
221 | adaptations[adaptations < 0] = 0.0
222 | return adaptations
223 |
224 | def _calc_adaptations_curve(self, firing_rates: ndarray) -> ndarray:
225 | """
226 | Calculates q(i) from Eq. (13). This is the baseline adaptation curve
227 | for each neuron based on activity.
228 |
229 | :param firing_rates: Array of activities for each motor neuron.
230 | """
231 | ratios = (self._recruitment_thresholds - 1) / (self._max_recruitment_threshold - 1)
232 | adaptations = self._adaptation_magnitude * (firing_rates - self._min_firing_rate + self._derecruitment_delta) * ratios
233 | return adaptations
234 |
235 | @staticmethod
236 | def _calc_peak_firing_rates(
237 | max_firing_rate_first_unit: int,
238 | max_firing_rate_last_unit: int,
239 | max_recruitment_threshold: int,
240 | recruitment_thresholds: ndarray,
241 | ) -> ndarray:
242 | """
243 | Calculate peak firing rates for each motor neuron
244 |
245 | :param max_firing_rate_first_unit:
246 | Maximum allowed activity of the 'first' neuron in the pool.
247 | :param max_firing_rate_last_unit:
248 | Maximum allowed activity of the 'last' neuron in the pool.
249 | :param max_recruitment_threshold:
250 | Excitation level above which the 'last' neuron in the pool will
251 | become active.
252 | :param recruitment_thresholds:
253 | Array of all excitation levels above which each neuron will become
254 | active.
255 | """
256 | firing_rate_range = max_firing_rate_first_unit - max_firing_rate_last_unit
257 | rates = max_firing_rate_first_unit \
258 | - (firing_rate_range
259 | * ((recruitment_thresholds - recruitment_thresholds[0])
260 | / (max_recruitment_threshold - recruitment_thresholds[0])))
261 | return rates
262 |
263 | @staticmethod
264 | def _calc_recruitment_thresholds(
265 | motor_unit_count: int,
266 | max_recruitment_threshold: int
267 | ) -> ndarray:
268 | """
269 | Pure function to calculate recruitment thresholds for each motor
270 | neuron.
271 |
272 | :param motor_unit_count: The number of motor units in the pool.
273 | :param max_recruitment_threshold:
274 | Excitation level above which the 'last' neuron in the pool will
275 | become active.
276 | """
277 | motor_unit_indices = np.arange(1, motor_unit_count + 1)
278 |
279 | r_log = np.log(max_recruitment_threshold)
280 | r_exponent = (r_log * (motor_unit_indices - 1)) / (motor_unit_count - 1)
281 | return np.exp(r_exponent)
282 |
283 | def step(self, motor_pool_input: ndarray, step_size: float) -> ndarray:
284 | """
285 | Advance the motor neuron pool simulation one step.
286 |
287 | Returns firing rates on a per motor neuron basis for the given
288 | array of excitations. Takes into account fatigue over time.
289 |
290 | :param excitations:
291 | Array of excitation levels to use as input to motor neurons.
292 | :param step_size: How far to advance time in this step.
293 | """
294 | assert (len(motor_pool_input) == self.motor_unit_count)
295 | return self._calc_adapted_firing_rates(motor_pool_input, step_size)
296 |
--------------------------------------------------------------------------------
/pymuscle/potvin_fuglevand_2017_muscle_fibers.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 | import math # noqa
3 | from numpy import ndarray
4 | from copy import copy
5 |
6 | from .model import Model
7 |
8 |
9 | class PotvinFuglevand2017MuscleFibers(Model):
10 | """
11 | Encapsulates the muscle fibers portions of the motor unit model.
12 |
13 | The name of each parameter as it appears in Potvin, 2017 is in parentheses.
14 | If a parameter does not appear in the paper but does appear in the Matlab
15 | code, the variable name from the Matlab code is in parentheses.
16 |
17 | :param motor_unit_count: Number of motor units in the muscle (n)
18 | :param max_twitch_amplitude: Max twitch force within the pool (RP)
19 | :param max_contraction_time:
20 | [milliseconds] Maximum contraction time for a motor unit (tL)
21 | :param contraction_time_range:
22 | The scale between the fastest contraction time and the slowest (rt)
23 | :max_fatigue_rate: [percent per second] The rate at which the largest
24 | motor unit will fatigue at maximum excitation.
25 | :fatigability_range:
26 | The scale between the fatigability of the first motor unit and the last
27 | :contraction_time_change_ratio:
28 | For each percent of force lost during fatigue, what percentage should
29 | contraction increase? Based on Shields et al (1997)
30 | :apply_fatigue: Whether to calculate and apply peripheral fatigue.
31 |
32 | .. todo::
33 | The argument naming isn't consistent. Sometimes we use 'max' and other
34 | times we use 'last unit'. Can these be made consistent?
35 |
36 | Usage::
37 |
38 | from pymuscle import PotvinFuglevand2017MuscleFibers as Fibers
39 |
40 | motor_unit_count = 60
41 | fibers = Fibers(motor_unit_count)
42 | motor_neuron_firing_rates = np.rand(motor_unit_count) * 10.0
43 | step_size = 0.01
44 | force = fibers.step(motor_neuron_firing_rates, step_size)
45 | """
46 | def __init__(
47 | self,
48 | motor_unit_count: int,
49 | max_twitch_amplitude: int = 100,
50 | max_contraction_time: int = 90,
51 | contraction_time_range: int = 3,
52 | max_fatigue_rate: float = 0.0225,
53 | fatigability_range: int = 180,
54 | contraction_time_change_ratio: float = 0.379,
55 | apply_fatigue: bool = True
56 | ):
57 | self._peak_twitch_forces = self._calc_peak_twitch_forces(
58 | motor_unit_count,
59 | max_twitch_amplitude
60 | )
61 |
62 | # These will change with fatigue.
63 | self._current_peak_forces = copy(self._peak_twitch_forces)
64 |
65 | self._contraction_times = self._calc_contraction_times(
66 | max_twitch_amplitude,
67 | max_contraction_time,
68 | contraction_time_range,
69 | self._peak_twitch_forces
70 | )
71 |
72 | # These will change with fatigue
73 | self._current_contraction_times = copy(self._contraction_times)
74 |
75 | # The maximum rates at which motor units will fatigue
76 | self._nominal_fatigabilities = self._calc_nominal_fatigabilities(
77 | motor_unit_count,
78 | fatigability_range,
79 | max_fatigue_rate,
80 | self._peak_twitch_forces
81 | )
82 |
83 | # Assign other non-public attributes
84 | self._contraction_time_change_ratio = contraction_time_change_ratio
85 | self._apply_fatigue = apply_fatigue
86 | self._max_fatigue_rate = max_fatigue_rate
87 |
88 | # Assign public attributes
89 | self.motor_unit_count = motor_unit_count
90 | self.current_forces = np.zeros(motor_unit_count)
91 |
92 | @property
93 | def current_peak_forces(self):
94 | return self._current_peak_forces
95 |
96 | def _update_fatigue(
97 | self,
98 | normalized_forces: ndarray,
99 | step_size: float
100 | ) -> None:
101 | """
102 | Updates current twitch forces and contraction times.
103 |
104 | :param normalized_forces:
105 | Array of scaled forces. Used to weight how much fatigue will be
106 | generated in this step.
107 | :param step_size: How far time has advanced in this step.
108 | """
109 | # Instantaneous fatigue rate
110 | fatigues = (self._nominal_fatigabilities * normalized_forces) * step_size
111 | self._current_peak_forces -= fatigues
112 |
113 | # Zero out negative values
114 | self._current_peak_forces[self._current_peak_forces < 0] = 0.0
115 | self._update_contraction_times()
116 |
117 | def _update_contraction_times(self) -> None:
118 | """
119 | Update our current contraction times as a function of our current
120 | force capacity relative to our peak force capacity.
121 | From Eq. (11)
122 | """
123 | force_loss_pcts = 1 - (self._current_peak_forces / self._peak_twitch_forces)
124 | inc_pcts = 1 + self._contraction_time_change_ratio * force_loss_pcts
125 | self._current_contraction_times = self._contraction_times * inc_pcts
126 |
127 | @staticmethod
128 | def _calc_contraction_times(
129 | max_twitch_amplitude: int,
130 | max_contraction_time: int,
131 | contraction_time_range: int,
132 | peak_twitch_forces: ndarray
133 | ) -> ndarray:
134 | """
135 | Calculate the contraction times for each motor unit
136 | Results in a smooth range from max_contraction_time at the first
137 | motor unit down to max_contraction_time / contraction_time range
138 | for the last motor unit
139 |
140 | :param max_twitch_amplitude:
141 | Largest force a motor unit in this muscle can produce.
142 | :param max_contraction_time:
143 | Slowest contraction time for a motor unit in this muscle.
144 | :param contraction_time_range:
145 | The ratio between the slowest and the fastest contraction times
146 | in this muscle.
147 | :param peak_twitch_forces:
148 | An array of all the largest forces that each motor unit can
149 | produce.
150 | """
151 |
152 | # Fuglevand 93 version - very slightly different values
153 | # twitch_force_range = peak_twitch_forces[-1] / peak_twitch_forces[0]
154 | # scale = math.log(twitch_force_range, contraction_time_range)
155 |
156 | # Potvin 2017 version
157 | scale = np.log(max_twitch_amplitude) / np.log(contraction_time_range)
158 |
159 | mantissa = 1 / peak_twitch_forces
160 | exponent = 1 / scale
161 | return max_contraction_time * np.power(mantissa, exponent)
162 |
163 | @staticmethod
164 | def _calc_peak_twitch_forces(
165 | motor_unit_count: int,
166 | max_twitch_amplitude: int
167 | ) -> ndarray:
168 | """
169 | Pure function to calculate the peak twitch force for each motor unit.
170 |
171 | :param motor_unit_count: The number of motor units in the pool.
172 | :param max_twitch_amplitude:
173 | Largest force a motor unit in this muscle can produce. (Arbitrary
174 | units.)
175 | """
176 | motor_unit_indices = np.arange(1, motor_unit_count + 1)
177 | t_log = np.log(max_twitch_amplitude)
178 | t_exponent = (t_log * (motor_unit_indices - 1)) / (motor_unit_count - 1)
179 | return np.exp(t_exponent)
180 |
181 | @staticmethod
182 | def _calc_nominal_fatigabilities(
183 | motor_unit_count: int,
184 | fatigability_range: int,
185 | max_fatigue_rate: float,
186 | peak_twitch_forces: ndarray
187 | ) -> ndarray:
188 | """
189 | Pure function to calculate nominal fatigue factors for each motor unit.
190 |
191 | Taken more from the matlab code than the paper.
192 |
193 | :param motor_unit_count: The number of motor units in this muscle.
194 | :param fatigability_range:
195 | The ratio between the maximum fatigue rate of the strongest motor
196 | unit (which fatigues the fastest) and the fatigue rate of the
197 | weakest motor unit.
198 | :param max_fatigue_rate:
199 | Largest percentage drop per unit time of twitch strength for the
200 | strongest motor unit.
201 | :param peak_twitch_forces:
202 | An array of all the largest forces that each motor unit can
203 | produce.
204 | """
205 | motor_unit_indices = np.arange(1, motor_unit_count + 1)
206 | f_log = np.log(fatigability_range)
207 | motor_unit_fatigue_curve = np.exp((f_log / (motor_unit_count - 1)) * (motor_unit_indices - 1))
208 | fatigue_rates = motor_unit_fatigue_curve * (max_fatigue_rate / fatigability_range) * peak_twitch_forces
209 | return fatigue_rates
210 |
211 | def _normalize_firing_rates(self, firing_rates: ndarray) -> ndarray:
212 | """
213 | Calculate the effective impact of a given set of firing rates on
214 | muscle fibers which have diverse contraction times and may be fatigued.
215 |
216 | :param firing_rates: Should be the result of pool._calc_adapted_firing_rates()
217 | """
218 | # Divide by 1000 here as firing rates are per second where contraction
219 | # times are in milliseconds.
220 | return self._current_contraction_times * (firing_rates / 1000)
221 |
222 | @staticmethod
223 | def _calc_normalized_forces(normalized_firing_rates: ndarray) -> ndarray:
224 | """
225 | Calculate motor unit force, relative to its peak force. Force grows
226 | in a linear fashion up to 0.4 normalized firing rate and then in a
227 | sigmoid curve afterward.
228 |
229 | :param normalized_firing_rates:
230 | An array of firing rates scaled by the current contraction times
231 | for each motor unit.
232 | """
233 | normalized_forces = copy(normalized_firing_rates)
234 | linear_threshold = 0.4 # Values are non-linear above this value
235 | below_thresh_indices = normalized_forces <= linear_threshold
236 | above_thresh_indices = normalized_forces > linear_threshold
237 | # The next two lines are strange and magical
238 | # In the paper they are simplified to *= 0.3
239 | # This is the equivalent of the Matlab code
240 | normalized_forces[below_thresh_indices] /= 0.4
241 | normalized_forces[below_thresh_indices] *= 1 - np.exp(-2 * (0.4 ** 3))
242 | exponent = -2 * np.power(
243 | normalized_forces[above_thresh_indices],
244 | 3
245 | )
246 | normalized_forces[above_thresh_indices] = 1 - np.exp(exponent)
247 | return normalized_forces
248 |
249 | def _calc_current_forces(self, normalized_forces: ndarray) -> ndarray:
250 | """
251 | Scales the normalized forces for each motor unit by their current
252 | remaining twitch force capacity.
253 |
254 | This method also updates the public Muscle.current_forces array.
255 |
256 | :param normalized_forces: An array of forces scaled between 0 and 1
257 | """
258 | self.current_forces = normalized_forces * self._current_peak_forces
259 | return self.current_forces
260 |
261 | def _calc_total_fiber_force(
262 | self,
263 | firing_rates: ndarray,
264 | step_size: float
265 | ) -> float:
266 | """
267 | Calculates the total instantaneous force produced by all fibers for
268 | the given instantaneous firing rates.
269 | :param firing_rates:
270 | An array of firing rates calculated by a compatible Pool class.
271 | :param step_size: How far time has advanced in this step.
272 | """
273 | normalized_firing_rates = self._normalize_firing_rates(firing_rates)
274 | normalized_forces = self._calc_normalized_forces(normalized_firing_rates)
275 | current_forces = self._calc_current_forces(normalized_forces)
276 | total_force = np.sum(current_forces)
277 |
278 | # Apply fatigue as last step
279 | if self._apply_fatigue:
280 | self._update_fatigue(normalized_forces, step_size)
281 |
282 | return total_force
283 |
284 | def step(
285 | self,
286 | motor_pool_output: ndarray,
287 | step_size: float
288 | ) -> float:
289 | """
290 | Advance the muscle fibers simulation one step.
291 |
292 | Returns the total instantaneous force produced by all fibers for
293 | the given input from the motor neuron pool.
294 |
295 | :param motor_pool_output:
296 | An array of firing rates calculated by a compatible Pool class.
297 | :param step_size: How far time has advanced in this step.
298 | """
299 | assert (len(motor_pool_output) == self.motor_unit_count)
300 | return self._calc_total_fiber_force(motor_pool_output, step_size)
301 |
--------------------------------------------------------------------------------