Skip to content

Commit

Permalink
Alpha version 0.1.2.
Browse files Browse the repository at this point in the history
This commit/version mainly bring fixes.
[Added]
 - Changelog is now display as a separate link on PyPi, and link
to the GitHub page.
 - A small flag in top of the README.md show the PyPi version.

[Changed]
 - The ``step`` function execution speed has been increased by
25% when ``return_res`` is ``True``! Small performance
improvement when ``return_res`` is ``False``.
 - The ``size`` argument of ``step`` function is now known as
``witdh``.
 - We now require pylinkage>=0.4.0.

[Fixed]
 - Files in ``leggedsnake/examples/`` were not included in the
PyPi package.
 - The example was incompatible with
[pylinkage](https://pypi.org/project/pylinkage/) 0.4.0.
 - Test suite was unusable by tox.
 - Tests fixed.

[Security]
 - Tests with `tox.ini`` now include Python 3.9 and Flake 8.
  • Loading branch information
HugoFara committed Jul 7, 2021
1 parent 8e64bb7 commit 72fc71c
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 83 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.2-alpha] - 2021-07-07
### Changed
- The ``step`` function execution speed has been increased by 25% when ``return_res`` is ``True``! Small performance improvement when ``return_res`` is ``False``.
- The ``size`` argument of ``step`` function is now known as ``witdh``.
- We now require pylinkage>=0.4.0.

### Fixed
- Files in ``leggedsnake/examples/`` were not included in the PyPi package.
- The example was incompatible with [pylinkage](https://pypi.org/project/pylinkage/) 0.4.0.
- Test suite was unusable by tox.
- Tests fixed.

### Security
- Tests with `tox.ini`` now include Python 3.9 and Flake 8.

## [0.1.1-alpha] - 2021-06-26
### Added
- The example file ``examples/strider.py`` is now shipped with the Python package.
Expand Down
1 change: 1 addition & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
MIT License
===========

Copyright (c) 2021 Hugo FARAJALLAH

Expand Down
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[![PyPI version fury.io](https://badge.fury.io/py/leggedsnake.svg)](https://pypi.python.org/pypi/leggedsnake/)
# leggedsnake

This package aims to provide reliable computation techniques in Python to build, simulate and optimize planar [leg mechanisms](https://en.wikipedia.org/wiki/Leg_mechanism). It is divided in three main parts:
LeggedSnake is a Python library providing reliable computationnal techniques to build, simulate and optimize planar [leg mechanisms](https://en.wikipedia.org/wiki/Leg_mechanism). It is divided in three main parts:
* Linkage conception in simple Python and kinematic optimization relying on [pylinkage](https://github.com/HugoFara/pylinkage).
* Leg mechanism definition, with ``Walker`` heriting from the ``Linkage`` class.
* Dynamic simulation and its optimization thanks to genetic algorithms.
Expand All @@ -13,6 +14,8 @@ The package is hosted on PyPi as [leggedsnake](https://pypi.org/project/leggedsn
### Setting up Virtual Environment
We provide an [environment.yml](https://github.com/HugoFara/leggedsnake/blob/master/environment.yml) file for conda. Use ``conda env update --file environment.yml --name leggedsnake-env`` to install the requirements in a separate environment.

If you are looking for a development version, check the GitHub repo under [HugoFara/leggedsnake](https://github.com/HugoFara/leggedsnake).

## Requirements

Python 3, numpy for calculation, matplotlib for drawing, and standard libraries.
Expand Down Expand Up @@ -47,10 +50,10 @@ my_walker = Walker(
### Kinematic optimization using Particle Swarm Optimization (PSO)
No change compared to a classic linkage optimization. You should use the ``step`` and ``stride`` method from the [utility module](https://github.com/HugoFara/leggedsnake/blob/master/leggedsnake/utility.py) as fitness functions.
This set of rules should work well for a stride **maximisation** problem:
#. Rebuild the Walker with the provided set of dimensions, and do a complete turn.
#. If the Walker raise an UnbuildableError, its score is 0 (or ``- float('inf')`` if you use other evaluation functions.
#. Verify if it can pass a certain obstacke using ``step`` function. If not, its score is 0.
#. Eventually mesure the length of its stide with the ``stride`` function. Return this length as its score.
1. Rebuild the Walker with the provided set of dimensions, and do a complete turn.
1. If the Walker raise an UnbuildableError, its score is 0 (or ``- float('inf')`` if you use other evaluation functions.
1. Verify if it can pass a certain obstacke using ``step`` function. If not, its score is 0.
1. Eventually mesure the length of its stide with the ``stride`` function. Return this length as its score.

### Dynamic Optimization using Genetic Algorithm (GA)
Kinematic optimization is fast, however it can return weird results, and it has no sense of gravity while walking heavily relies on gravity. This is why you may need to use dynamic optimization thanks to [Pymunk](http://www.pymunk.org/en/latest/index.html). However the calculation is much more slower, and you can no longer tests millions of linkages as in PSO (or you will need time). This is why we use [genetic algorithm](https://en.wikipedia.org/wiki/Genetic_algorithm), because it can provide good results with less parents.
Expand Down
12 changes: 6 additions & 6 deletions environment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ channels:
- conda-forge
- defaults
dependencies:
- matplotlib>=3.3.4=py38h06a4308_0
- matplotlib-base>=3.3.4=py38h62a2d02_0
- numpy>=1.20.2=py38h2d18471_0
- numpy-base>=1.20.2=py38hfae3a4d_0
- numpydoc>=1.1.0=pyhd3eb1b0_1
- matplotlib>=3.3.4
- matplotlib-base>=3.3.4
- numpy>=1.20.2
- numpy-base>=1.20.2
- numpydoc>=1.1.0
- pymunk>=6.0.0
- pip
- pip:
- pygad>=2.10.0
- pylinkage
- pylinkage>=0.4.0
74 changes: 47 additions & 27 deletions leggedsnake/dynamiclinkage.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def __generate_body__(self, index=0):
if (
hasattr(self, 'joint' + sindex)
and isinstance(getattr(self, 'joint' + sindex), linkage.Joint)
):
):
body = pm.Body()
body.mass = 1
parent_pos = pm.Vec2d(*getattr(self, 'joint' + sindex).coord())
Expand Down Expand Up @@ -205,7 +205,7 @@ def __init__(self, x=0, y=0, joint0=None, space=None,
if (
isinstance(self.joint0, linkage.Joint) and
isinstance(self.joint1, linkage.Joint)
):
):
linkage.Fixed.reload(self)
if self.joint0 is not None:
self.set_anchor_a(self.joint0, self.r, self.angle)
Expand Down Expand Up @@ -284,11 +284,11 @@ def __init__(self, x=0, y=0, joint0=None, space=None,
joint0=joint0, joint1=joint1,
name=name,
distance0=distance0, distance1=distance1
)
)
DynamicJoint.__init__(
self, space=space, radius=radius, density=density,
shape_filter=shape_filter)

shape_filter=shape_filter
)
if self.joint0 is not None:
self.set_anchor_a(self.joint0, self.r0)
if self.joint1 is not None:
Expand Down Expand Up @@ -426,7 +426,7 @@ def __generate_body__(self):
"""Generate the crank body only."""
if hasattr(self, 'joint0') and isinstance(self.joint0, linkage.Joint):
body = pm.Body()
body.position = (pm.Vec2d(*self.coord()) + self.joint0.coord())/2
body.position = (pm.Vec2d(*self.coord()) + self.joint0.coord()) / 2
seg = self.__generate_link__(body, self.joint0.coord())
self._a = self._b = body
self._anchor_b = body.world_to_local(self.coord())
Expand Down Expand Up @@ -549,32 +549,52 @@ def convert_to_dynamic_joints(self, joints):
raise Exception('Linkage {} Space not defined yet!'.format(self))
dynajoints = []
conversion_dict = {}
common = {'space': self.space, 'radius': self._thickness,
'density': self.density, 'shape_filter': self.filter}
common = {
'space': self.space,
'radius': self._thickness,
'density': self.density,
'shape_filter': self.filter
}
for joint in joints:
common.update({'x': joint.x, 'y': joint.y, 'name': joint.name})
if isinstance(joint, DynamicJoint):
djoint = joint
elif isinstance(joint, linkage.Static):
djoint = Nail(body=self.body, **common)
elif isinstance(joint, linkage.Fixed):
djoint = PinUp(distance=joint.r, angle=joint.angle,
joint0=conversion_dict[joint.joint0],
joint1=conversion_dict[joint.joint1],
**common)
elif isinstance(joint, linkage.Crank):
djoint = Motor(
joint0=conversion_dict[joint.joint0],
distance=joint.r, angle=joint.angle,
**common
)
elif isinstance(joint, linkage.Pivot):
djoint = DynamicPivot(
joint0=conversion_dict[joint.joint0],
joint1=conversion_dict[joint.joint1],
distance0=joint.r0, distance1=joint.r1,
**common
)
# Joints with at least one reference
else:
"""
Useless while qe don't support quick joint definition
if (
isinstance(joint.joint0, linkage.Static)
and joint.joint0 not in conversion_dict
):
conversion_dict[joint.joint0] = joint.joint0
if (
hasattr(joint, "joint1")
and isinstance(joint.joint1, linkage.Static)
and joint.joint1 not in conversion_dict
):
conversion_dict[joint.joint1] = joint.joint1
"""
if isinstance(joint, linkage.Fixed):
djoint = PinUp(distance=joint.r, angle=joint.angle,
joint0=conversion_dict[joint.joint0],
joint1=conversion_dict[joint.joint1],
**common)
elif isinstance(joint, linkage.Crank):
djoint = Motor(
joint0=conversion_dict[joint.joint0],
distance=joint.r, angle=joint.angle,
**common
)
elif isinstance(joint, linkage.Pivot):
djoint = DynamicPivot(
joint0=conversion_dict[joint.joint0],
joint1=conversion_dict[joint.joint1],
distance0=joint.r0, distance1=joint.r1,
**common
)
dynajoints.append(djoint)
conversion_dict[joint] = djoint
self.joints = tuple(dynajoints)
Expand All @@ -587,7 +607,7 @@ def build_load(self, position, load_mass):
segs = []
for i, vertex in enumerate(vertices):
segs.append(pm.Segment(load, vertex,
vertices[(i+1) % len(vertices)],
vertices[(i + 1) % len(vertices)],
self._thickness))
segs[-1].density = self.density
# Rigodbodies in this group won't collide
Expand Down
24 changes: 12 additions & 12 deletions leggedsnake/examples/strider.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def complete_strider(constraints, prev):
name="Strider"
)
strider.set_coords(prev)
strider.set_num_constraints(constraints)
strider.set_num_constraints(constraints, flat=False)
return strider


Expand Down Expand Up @@ -207,7 +207,7 @@ def strider_builder(constraints, prev, n_leg_pairs=1, minimal=False):
prev.pop(-1)
constraints.pop(-1)
strider.set_coords(prev)
strider.set_num_constraints(constraints)
strider.set_num_constraints(constraints, flat=False)
if n_leg_pairs > 1:
strider.add_legs(n_leg_pairs - 1)
return strider
Expand All @@ -234,7 +234,7 @@ def show_physics(linkage, prev=None, debug=False, duration=40, save=False):

def sym_stride_evaluator(linkage, dims, pos):
"""Give score to each dimension set for symmetric strider."""
linkage.set_num_constraints(param2dimensions(dims))
linkage.set_num_constraints(param2dimensions(dims), flat=False)
linkage.set_coords(pos)
try:
points = 12
Expand Down Expand Up @@ -437,7 +437,7 @@ def fitness(dna, linkage_hollow):
List of two elements: score (a float), and initial positions.
Score is -float('inf') when mechanism building is impossible.
"""
linkage_hollow.set_num_constraints(param2dimensions(dna[0]))
linkage_hollow.set_num_constraints(param2dimensions(dna[0]), flat=False)
linkage_hollow.rebuild(dna[2])
# Check if mecanism is buildable
try:
Expand Down Expand Up @@ -523,27 +523,27 @@ def show_optimized(linkage, data, n_show=10, duration=5, symmetric=True):
if datum[0] == 0:
continue
if symmetric:
linkage.set_num_constraints(param2dimensions(datum[1]))
linkage.set_num_constraints(param2dimensions(datum[1]), flat=False)
else:
linkage.set_num_constraints(datum[1])
linkage.set_num_constraints(datum[1], flat=False)
visu.show_linkage(linkage, prev=begin, title=str(datum[0]),
duration=10)


#from cProfile import run
strider = complete_strider(param2dimensions(param), begin)
#wu.step([(0, 0), (-1, 0), (-1, 1), (0, 1), (0, .5)], 0, .5)
from cProfile import run
#strider = complete_strider(param2dimensions(param), begin)
strider = strider_builder(param2dimensions(param), begin,
n_leg_pairs=19, minimal=False)
n_leg_pairs=5, minimal=False)
#o = swarm_optimizer(show=1, save_each=1, age=10, ite=10, blind_ite=10)
#run('swarm_optimizer(show=False, save_each=0, age=30, ite=400)')
run('sym_stride_evaluator(strider, param, begin)')
#optimized_striders = wo.exhaustive_optimization(
# sym_stride_evaluator, strider, param, delta_dim=.5)
#optimized_striders = swarm_optimizer(strider, show=1, save_each=0, age=250,
# ite=200, blind_ite=1, bounds=bounds)
#show_optimized(strider, optimized_striders)
#strider.add_legs(3)
#visu.show_linkage(strider, save=False, duration=10, iteration_factor=n)
show_physics(strider, debug=False, duration=40, save=False)
#show_physics(strider, debug=False, duration=40, save=False)
#o = evolutive_optimizer(
# strider, dims=param, prev=begin, pop=10, ite=100, init_pop=100,
# save=False, startnstop=False)
5 changes: 5 additions & 0 deletions leggedsnake/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""
This module is the test suite of leggedsnake.
It uses unit test.
"""
32 changes: 22 additions & 10 deletions leggedsnake/tests/test_utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,23 @@ class TestStride(unittest.TestCase):
locus = [(0, 0), (-1, 0), (-1, 1), (0, 1), (0, .5)]

def test_minimal_stride(self):
"""Test if we only get the three lowest points."""
result = stride(self.locus, height=.1)
self.assertEqual(len(result), 2)
self.assertEqual(result, self.locus[:2])
self.assertEqual(len(result), 3)
self.assertEqual(result, self.locus[-1:] + self.locus[:2])

def test_ambiguous_stride(self):
"""
Test if we only get all points but not the highest.
A point on the limit shoud be discarded when some points are on the
limit.
"""
result = stride(self.locus, height=.5)
self.assertEqual(result, self.locus[:2] + self.locus[-1:])
self.assertEqual(result, self.locus[-2:] + self.locus[0:2])

def test_maximal_stride(self):
"""Test if all points are retrivewed."""
result = stride(self.locus, height=2)
self.assertEqual(result, self.locus)

Expand All @@ -35,17 +43,21 @@ class TestStep(unittest.TestCase):
locus = [(0, 0), (-1, 0), (-1, 1), (0, 1), (0, .5)]

def test_minimal_step(self):
result = step(self.locus, height=0, size=.5)
"""Test if we can pass an obstacle small enough."""
result = step(self.locus, height=0, width=.5)
self.assertTrue(result)

def test_ambiguous_step(self):
result = step(self.locus, height=1, size=1)
"""Test if successfully to pass an obstacle of the size of the locus."""
result = step(self.locus, height=1, width=1)
self.assertTrue(result)

def test_maximal_step(self):
result = stride(self.locus, height=1, size=2)
def test_streched_step(self):
"""Test if we fail to pass an obstacle to big."""
result = step(self.locus, height=1, width=2)
self.assertFalse(result)


if __name__ == '__main__':
unittest.main()
def test_dwarf_step(self):
"""Test if we fail to pass an obstacle to high."""
result = step(self.locus, height=2, width=.5)
self.assertFalse(result)
Loading

0 comments on commit 72fc71c

Please sign in to comment.