mechmotum / brim Goto Github PK
View Code? Open in Web Editor NEWA Modular and Extensible Open-Source Framework for Creating Bicycle-Rider Models
Home Page: https://mechmotum.github.io/brim/
License: Creative Commons Zero v1.0 Universal
A Modular and Extensible Open-Source Framework for Creating Bicycle-Rider Models
Home Page: https://mechmotum.github.io/brim/
License: Creative Commons Zero v1.0 Universal
Just spotted that there is still a _parent
attribute in ConnectionBase
, which is left over from a previous design.
Personally, I would consider it as best practice to first define the entire graph and then define the objects. This would mainly be a safety precaution for the future as it does give the flexibility to require all submodels to be known before starting to define any objects. Of course one should still be able to change symbols as discussed in #7.
There is a huge cost on using a double cross product with normalization on computing the wheel center. In case of pure a rolling disc this quadruples the number of operations in the CSEd equations of motion. Therefore, it would be advantages if this can be fixed. While an option would be to play around with simplify or something directly, this will also cause simplification in more heavy locations like the front wheel. Another option would be to let the parent of the tyre connection have the ability to set the axis of the connection as helper.
In the current implementation of BRiM
almost no loads are defined by default to act upon the model. This is sensible as one can almost make no assumption of what loads a user may like to apply by default on a certain model. There are however rather common combinations, which one may apply on certain models. Therefore, it would be advantageous if there was a method to easily add a set of common loads to a system.
Example about which I'm especially thinking in this case is that one may like to at once apply spring dampers to the joints in the rider. An option would of course be to add to each of the models a setting what kinds of loads they like the user likes to use. In this case I would propose doing so as a key word argument as it is already required in the define objects stage. The default should in this case of course be None
.
It would however also be nice if you can just use connections to specify the loads working upon a model, especially as this would allow a more easy addition of new load models to be defined by the user. Though one can actually just apply most of these loads always afterwards. Another problem I do actually see is that connections, should not have connections, while you do actually want a joint created by a connection to also get those default load choices.
Four-bar-linkage tutorial:
P1
, ...) to the four-bar-linkage image.Get a feature to easily see what submodels and connection should be specified and how the attributes are called.
While it is really nice to always run all tests, it is preferable to get a separate slow test label for Whipple bicycle validation test and the complete bicycle rider test, as forming the equations of motion takes a few minutes.
Would be preferable to let the connection between the rear frame and pelvis, by default include a rotation angle for how the rider is positioned.
I probably mentioned this already somewhere else without a real solution, but using for example compute_contact_point
and compute_tyre_model
in WheelBase
are really annoying. It has to be specifically called by the parent at the right time to just connect the two. This is frustrating at the moment that your tyre model also influences the kinematics with auxilary speeds. This kind of results in the problem that there should be a separate call in the define kinematics.
All in all this kind of stuff is just suboptimal and it would be better if one just defines all connections etc before define_kinematics
is called as define_kinematics
and define_loads
will kind of always be called immediately after each other.
Could there be something done like solve_connections
or define_connections
it is quite similar to requirements
?
Something I'm strongly considering is to just use Dummy
variables and such to make sure objects are unique in brim
. With the get_description
there is not really a need for having nice names, which only make it more difficult. If someone wants a symbol to have a nice name it is better if they just overwrite it.
At the moment the rear and front frame use the NewtonianBodyMixin. However, this automatically makes them use only a single reference frame, while it is possible that a rotation occurs within the frame due to flexibility. Therefore it is better to use the same concept as always in the joints framework, namely a point and an interframe per joint. While this seems an easy fix, it would be ideal to create the pin joint in the WhippleBicycle just based on the interframe and the point. However, the joints framework does not yet the creation of a joint without using a body. Either a zero-weight body will have to be created or the joints framework should be updated. As the last would be the best option this issue consists out of three tasks:
namedtuple(frame=frame, point=point)
as parent
and child
steer_interframe
and wheel_interframe
propertiessteer_interframe
and wheel_interframe
propertiesWhile I did choose to use q and u all the time, I noticed that if there is only a single one of them, that I just directly made it a dynamicsymbol. This is however a bit inconvenient, because this means that the type can be different of the same method name in different models and connections. Therefore, I propose adding a q and u property to BrimBase
, similar to symbols
.
Bypassed the protection rule of the main branch by accident, so the extension is already in. It is however good to have some issue about it to name some of the design decisions.
The current setup consists out of two classes, one is the actual rider extension and one which is a mixin, that adds the rider support to the rear frame, while also making the rider a required submodel, such that the mixin can define the kinematics between the two. It should however be noted that it uses the rear x axis by default, while one will actually like to use a more longitudinal axis. This axis can however only be computed with the knowledge of the ground, so it will probably be best to maybe add a method which computes this for the user.
Other then that I have to say that I quite like how well the mixin is working to just extend a model to make it compatible with another model. The only disadvantage I currently see is that the class attribute requirements
gets entirely messed up, but this is also a problem in inheritance.
When trying to publish brim on pypi I ran into the following issue:
HTTP Error 400: Invalid value for requires_dist. Error: Can't have direct dependency: 'sympy @ git+https://github.com/sympy/sympy.git' | b"<html>\n <head>\n <title>400 Invalid value for requires_dist. Error: Can't have direct depen
dency: 'sympy @ git+https://github.com/sympy/sympy.git'\n \n <body>\n <h1>400 Invalid value for requires_dist. Error: Can't have direct dependency: 'sympy @ git+https://github.com/sympy/sympy.git'\n The server could not comply wit
h the request since it is either malformed or otherwise incorrect.<br/><br/>\nInvalid value for requires_dist. Error: Can't have direct dependency: 'sympy @ git+https://github.com/sympy/sympy.git'\n\n\n \n"
It would be good to do some benchmarking on the generation of the Whipple model:
At the moment the benchmarks are just run to make sure that they remain valid. However, they should also be used as regression tests. To do so I would like three things:
In the switch to using attachments in #103 a regression has been observed in the number of operations in the EOMs. For the default Whipple bicycle the following regression has been observed:
Implementation | Computation time |
#Operations EOMs before CSE |
#Operations EOMs after CSE |
---|---|---|---|
Manual by Moore | 230789 | 2198 | |
Manual by Stienstra | 390554 | 2389 | |
BRiM before #103 | 468290 | 2176 | |
BRiM after #103 | 352210 | 2291 |
The table also shows some other implementations as a reference, which are also in the benchmarks/test_whipple_bicycle.py
file. The first three results were specifically created over 50 runs on an Inter(R) Core(TM) i7-8750H CPU @ 2.20GHz with BRiM commit: ec02a0a. Note that the computation time does not matter too much and should be taken with a grain of salt, as the timing results differ more than the standard deviation from time to time.
I have tracked the change down to the fact that the usage of the rear_frame.steer_hub.to_valid_joint_arg()
instead of rear_frame.body
as parent argument in the steer joint results in the increase of operations after CSE. This means that the velocity graph of the points is defined a little differently. It is notable that the number of operations in the EOMs before CSE actually decreases.
In #103 I propose the following three solutions:
Point.vel
method. While it would be ideal to make this more advanced, it is quite a nasty problem to solve.optimize_velocity_definition
when exporting to a single System
, which can later be moved to System
within sympy. This is also not a simple solution, as one needs to detect when it is advantageous to compute a velocity using a certain theorem.WhippleBicycle
to optimize the velocity definition. This has the disadvantage of a constant labor.When working on the third point I noticed that I was not able to get the wanted decrease when manually specifying the velocities which would be set by PinJoint when using
rear_frame.body`. My hypothesis is that the automated velocity computation just follows a suboptimal path or multiple paths to compute the same velocity component. It would be optimal if everything always uses the same velocity component and not an alternative, but figuring out how to force this is quite a challenge.
This issue challenges us to find the exact reason for the seen regression and find an appropriate fix for it.
A lot of models of course use symbols to define there models. It would be ideal if there was a consistent way of storing the symbols. MoSCoW is used to prioritize the requirements:
The following requirements exist:
Symbol
for constant values.Function
, because that can also be useful, just think about the fact that someone may have a model with a time-varying input force.dynamicsymbols
), which should be accessed separately may have a model with a time-varying input force.dynamicsymbols
), which should be accessed separatelyfree_symbols
function or something, though would this also contain functions?define_objects
(M)lambdify
helper function or something (W, at least not for now).define_kinematics
, whether all generalized coordinates and speeds have been added to the system (C).There are several approaches:
q
and u
matrix, mutable or immutable depending on whether changes are allowed and such.descriptions
property to actually list all of the symbols as keys (retrieving the of the objects if necessary) and give a description for each as value.symbols
, which returns a SymbolsStorage
instance or something, which can either use internal SymbolDescriptor
's or just tuples or dictionaries.Currently a rather sneaky method is used to let you specify the formulation in new. It would be better to use a separate class method called from_formulation
.
It would be nice if there would be a sensible and easy way to retrieve a connection like a tyre model. This is currently not that complex, but I would expect models to get complexer over time, such that one would like to supply certain settings based on which the correct tyre model should be selected, a process which can be hidden from the user.
It would be nice if there was a method to quickly get all declared symbols, e.g. BrimBase.get_all_symbols()
. This is a relatively easy feature to implement as it only requires a simple traversal, where each object returns a set of its own symbols.
As I'm currently rather quickly implementing new load groups, I see that there is quite some room for refactoring.
_test_descriptions
function is not always used, should replace the locations where this is not done. A possibility would be to just write one test somewhere, which automatically goes through the registry and tests every objects descriptions.class A(ModelBase):
requirements = (Requirement('submodel1', SubModel),)
class Mixin:
requirements = (Requirement('submodel2', SubModel),)
a = A('test')
a.add_mixin(Mixin)
a.submodel1 # -> None
a.submodel2 # -> None
a.requirements # (Requirement('submodel2', SubModel),)
requirements
is also used to check what submodels there exist, so this is actually a problem.
There are some class names, which redundantly mention that they are a connection or model. These names can be shortened.
TyreModelBase
--> TyreBase
NonHolonomicTyreModel
--> NonHolonomicTyre
PedalConnectionBase
--> PedalsBase
HolonomicPedalsConnection
--> HolonomicPedals
SeatConnectionBase
--> stay the same, or make it RearToPelvisBase
, or just SeatBase
SideLeanConnection
--> SideLean
SteerConnectionBase
--> stay the same, or make it SteerToArmsBase
, HandGripBase
or GripBase
HolonomicSteerConnection
--> HolonomicHandGrip
SimpleRigidPelvis
--> PlanarPelvis
SimpleRigidTorso
--> PlanarTorso
...PelvisToTorso
--> ...Sacrum
PedalsBase
--> CranksBase
SimplePedals
--> MasslessCranks
When designing the core, it was currently decided to choose a kind of graph structure. While this is a quite natural way to think about a system. One could also argue for a graph structure, where the different models are connected with possible loops and maybe even without an overseeer. I did not see how to really get this working nicely, but for the riders I'm actually starting to dynamically add mixins to models to add for example the points around which the rider rotates with respect to the frame.
It is mainly just an idea or vague concept, but it might be quite nice and a viable way, which can be expanded to actually do all the interactions between different objects as well.
It seems that the BicycleParameters
library adjusts the position of the rear frame's CoM automatically based on the rider. This should not be done, as the inertia of the rider is taken into account for within the rider.
This issue discusses the initial design of brim
(this is mainly just a scratchpad). The image below shows a general design, this however needs to be specified a lot further.
When defining a model using sympy.physics.mechanics
one can in general split up the process in the following steps within which the order is less important:
symbols
and dynamicsymbols
;ReferenceFrame
, Point
, RigidBody
, Particle
;An important aspect in this modular programming is how the responsibilities of these steps are shared among the extensions. An important part to keep in mind here is encapsulation, which in this case will mean that a child will not know anything about the parent, but the parent does know about the child.
Best way is to look to study a case, such as the definition of the front wheel in the WhippleBicycle
.
WhippleBicycle
may import a WheelBase
. Specific implementation of a wheel like KnifeEdgeWheel
should not be imported.WheelBase
. The exact use of a wheel is not known to a wheel. Therefore it will be the WhippleBicycle
, which has to define the dynamicsymbol
for the rotation angle of the wheel.RigidBody
a Point
for the contact point shall be initiated by the KnifeEdgeWheel
, where the WheelBase
describes how they can be accessed by for example the WhippleBicycle
.FrontFrameBase
. The WhippleBicycle
will know about both, so it is the responsibility of the WhippleBicycle
to create the relation based on the axis supplied by both the front frame and the wheel. Determining the location of the ground contact point is more complex. The computation is based on information, which is known by the wheel and by the ground. In this case a choice is made to use add an abstract method WheelBase.set_pos_contact_point(self, ground: Type[GroundBase])
.TyreModelBase
...WhippleBicycle
. So the creation of the constraint is a responsibility of WhippleBicycle
.KanesMethod
, the parsing to System
is handled by handled by WhippleBicycle
, which gathers all necessary objects.As step 1 and 2 are the responsibility of the extensions themselves, which can be ran in the __init__
and later be overwritten by the user if preferred, I'll merge the together in one function, such that one can also use the automatic generation of symbols in for example RigidBody
. A user should be overwriting such a symbol directly in the body and not on the wheel. A good function name is define_objects
(other option would be initialize_objects
).
Probably best to write some user stories first.
from brim import *
bike = WhippleBicycle('bike', formulation='moore')
bike.rear_wheel = KnifeEdgeWheel('rear_wheel')
bike.rear_frame = RigidRearFrame('rear_frame')
bike.front_frame = RigidFrontFrame('front_frame')
bike.front_wheel = KnifeEdgeWheel('front_wheel')
# Would in a way like to not have names, but it makes unification easier
assert bike.front_wheel.radius != bike.rear_wheel.radius
# bike.rider = ...
system = bike.to_system()
system.form_eoms()
It is desirable to use a base class for each object, to simply walk through the composition of each gathering all details about for example the constants etc. This abstract class will be called ModelBase
. One of the problems is that we need to handle the submodels appropriately. An option is of course to hard code a lot. I've even started a module called templates, in which some instructions on how a class looks are written to make it easier with copy pasting. This would lead to something like the following (simplified and excluding the docstrings and typing for now):
class MyModel(ModelBase):
def __init__(self, name):
super().__init__(name)
self._submodel1 = None
@property
def submodels(self):
return frozenset((submodel for submodel in (
self.submodel1, ...) if submodel is not None))
@property
def submodel1(self):
return self._submodel1
@submodel1.setter
def submodel1(self, submodel1):
if not (submodel1 is None or isinstance(submodel1, SubModelBase)):
raise TypeError
self._submodel1 - submodel1
As visible this is not something I would really like. Therefore it would nice if you can just specify what are the submodels with their datatypes etc and it should just create them. An option would be something like:
class MyModel(ModelBase):
requirements = (
Requirement('submodel1', (SubModelBase,), 'Some description', ...),
)
This already looks a lot nicer, but there are of course some difficulties. It would first of all be best if the properties were to be defined on definition of the class. Secondly it would be quite nice if an IDE would actually see that those attributes would exist and if it would be possible to get the typing annotations in. There are two approaches to get the desired behaviour:
ModelBase
inherit from a different meta class, which fixes the problem in its __new__
method.dataclass
does.attrs
, which would be an extra dependency (though a quite nice one).It is desirable to use a base class for each object, to simply walk through the composition of each gathering all details about for example the constants etc. This abstract class will be called ModelBase
. There exist the following requirements for this class:
ModelBase
a name
property that is used as a prefix for each symbol.Dummy
's can be used, but will result in equations that are difficult to read.extensions: set
.define_objects
: only initiates all the objects, e.g. bodies and points. This should already be ran in the __init__
.define_kinematics
: defines the kinematic relationships between the different objects, while also adding them to System
.define_loads
: defines and adds the loads to System
.Some important questions to answer are:
wheel.radius = symbols('r')
wheel.body = RigidBody('wheel')
WhippleBicycle
as a class, but there are multiple formulations, which would lead to different implementations.
__new__
in WhippleBicycle
which just selects the right formulation for you.In a feedback session Jason Moore mentioned that there are standards with biomechanical modeling on how to orient reference frames w.r.t. the body and that it would be better to follow those within BRiM even though the current orientation, which follows the bicycle convention, has other practical advantages. Overall, I agree with him on this point. The fix should not be to difficult as it is just a rotation about the X-axis of 180 degrees. However, special attention is required to make sure no mistakes also in the automatic parameterization are made.
Noticed that there are multiple broken links within BRiM's online documentation. It would be best if nitpicky
would be set to True
in docs/conf.py
. However, that will require to first fix those broken links.
This is an issue tracker, that follows up on the preliminary merged PR #111.
Two tutorials should be added:
The current plotting structure is suboptimal, as it doesn't make use of the tree structures that well. It would be more advantageous to also have the plot description in the brim classes themselves, similar to the parametrization. To overall get it working I would expect that the following should be done:
PlotModel
PlotConnection
PlotLoadGroup
, though this is not that necessaryPlotter.add_model
and Plotter.add_connection
BrimBase.get_plot_objects(self, *args, **kwargs)
. Should be further thought out.Some problems are that you can make a way nicer plot of the rear frame if you know about the pedals. Should put in some thought how to keep this quite well.
Here is some starting code:
class PlotModel(PlotBase):
def __init__(self, inertial_frame: ReferenceFrame, zero_point: Point,
model: ModelBase, add_submodels: bool = True):
super().__init__(inertial_frame, zero_point, model.system.origin, model.name)
self._model = model
if add_submodels:
self._children.append(
[PlotModel(inertial_frame, zero_point, submodel)
for submodel in model.submodels] + [
PlotConnection(inertial_frame, zero_point, connection, False)
for connection in model.connections]
)
@property
def model(self) -> ModelBase:
"""Return the model."""
return self._model
class PlotConnection(PlotBase):
def __init__(self, inertial_frame: ReferenceFrame, zero_point: Point,
connection: ConnectionBase, add_submodels: bool = True):
super().__init__(inertial_frame, zero_point, connection.system.origin,
connection.name)
self._connection = connection
if add_submodels:
self._children.append(
[PlotModel(inertial_frame, zero_point, submodel)
for submodel in model.submodels]
)
@property
def connection(self) -> ConnectionBase:
"""Return the connection."""
return self._connection
As with #19 this is mainly just for extra decouplement and safety for the future. In the end there should be on method anyway which you'll call to run all define methods in order.
It is a rather strange phenomenon that the system instance inside the model of by example a WhippleBicycleMoore
instance may not include all bodies and other entities. It is the case that some of them are only defined in its submodels.
The reason this is the case is that a parent model does not actually know what things are defined in each of the submodel's systems. Therefore, one actually needs to use the to_system
method to obtain the system.
Overall I do find this a sensible decision, as fixing this would require a constant merging of systems. However it would be ideal to specify this somewhere to avoid confusement.
Okay, I'm just rewriting this comment to make a list of things that should be done to finish the documentation. In this list I use MoSCoW to list a bit of the importance. This may of course change a bit:
Below the list of documentation features:
Today I spend some time to discuss the problems regarding the current version of brim
and discussed those also with my supervisor Sam. This comment starts with a general overview of brim
's workings (see #2 for more information), followed by the problems encountered, each with an example. Some problems do only arise when implementing a certain solution, but a lot of them could also be caused by different effects.
brim
leverages the use of the option to choose a system boundary. One can choose a simple system boundary excluding all aspects but a single object or series of aspects. A really simple model would for example be a rear frame, consisting of only a body an a few points describing its geometry. These models, submodels as I will call them, can after creating them be connected by a parent model. This results in the following tree structure:
m1
, e.g. bicycle
sm1
, e.g. wheel
sm2
, e.g. rear_frame
This is of course a very minimal representation. To further ordain the process of defining a model, we can split up the definition process in the following steps:
define_objects
define_kinematics
define_loads
define_constraints
Of these it is currently decided to group the last two, though this can be changed in the future.
There are several problems/challenges encountered in this design:
There are several solution approaches and solution parts, which involve solving one or more of the above problems:
compute_...
calls being made by the parent, this is however susceptible to problem 2.define_connections
stage, where submodels can learn about other submodels through specification by the parent.add_mixin
feature introduced previously.Saw that I left in a bit of a random probably deprecated model. The model is in src/brim/rider/rider_lean.py
and the accompanying connection in src/brim/rider/connections.py
. The idea once was to have the option to extend a bicycle with a simple leaning rider model. However, I'm not sure whether that is still necessary as one can just choose to only specify the pelvis and choose the correct values. Also the model seems to be a bit old (from before the implementation of load groups). There are two options:
Rider
model.In a tutorial session of BRiM (#122), it was noted that it would be nice if one could easily see what components should still be specified. This is in general not a difficult feature to implement. However, we should make a good choice of what the most optimal output is. Some options are:
ModelBase.get_unspecified_components
and have it return:
Here is an example implementation:
def get_unspecified_components(self, optional: bool = False) -> tuple[str]:
"""Get the unspecified components of the model.
Parameters
----------
optional : bool, optional
Whether to include the optional components, by default False.
"""
return tuple(
req.attribute_name
for req in (self.required_models + self.required_connections)
if getattr(self, req.attribute_name) is None and (optional or req.hard)
)
This would give:
import brim as bm
bike = bm.WhippleBicycle("bike")
print(bike.get_unspecified_components())
# -> ('rear_frame', 'front_frame', 'rear_wheel', 'front_wheel', 'ground', 'front_tire', 'rear_tire')
bike.rear_frame = bm.RigidRearFrame("rear_frame")
bike.front_frame = bm.RigidFrontFrame("front_frame")
bike.rear_wheel = bm.KnifeEdgeWheel("rear_wheel")
bike.front_wheel = bm.KnifeEdgeWheel("front_wheel")
bike.rear_tire = bm.NonHolonomicTire("rear_tire")
print(bike.get_unspecified_components()) # -> ('ground', 'front_tire')
print(bike.get_unspecified_components(True)) # -> ('cranks', 'ground', 'front_tire')
# Advantage of returning requirements would be
print((bike.required_models[0],)) # A lot of data
# -> (ModelRequirement(attribute_name='rear_frame', types=(<class 'brim.bicycle.rear_frames.RearFrameBase'>,), description='Submodel of the rear frame.', full_name='Rear frame', type_name='RearFrameBase'),)
# And it is easy to combine with the registry.
from brim.core import Registry
reg = Registry()
print(reg.get_from_requirement(bike.required_models[0]))
# -> [<class 'brim.bicycle.rear_frames.RigidRearFrameMoore'>, <class 'brim.bicycle.rear_frames.RigidRearFrame'>]
@moorepants what is your preference?
There are a lot of constants, of course this will differ among users, but it would be nice to have something, which converts the constants from Yeadon
, such that you can easily use those.
There are a few models, which should preferably be renamed:
brim/core/model_base.py
-> brim/core/base_classes.py
brim/bicycle/tyre_models.py
-> brim/bicycle/tyres.py
Current choose to use a frozenset for the submodels and connections properties. However a possible nicer idea is to use frozen dictionaries or dictionaries. Point on making it frozen is that someone should not expect that they can just add submodels or connections that way.
If this change is preferred it should also be done for load groups. The practical implication would be:
rolling_disc = RollingDisc("model")
rolling_disc.disc = KnifeEdgeWheel("wheel")
rolling_disc.ground = FlatGround("ground")
rolling_disc.tyre = NonHolonomicTyre("tyre")
# Currently
rolling_disc.submodels --> frozenset({KnifeEdgeWheel("wheel"), FlatGround("ground")})
rolling_disc.connections--> frozenset({NonHolonomicTyre("tyre")})
# Proposal
rolling_disc.submodels --> frozendict({"wheel": KnifeEdgeWheel("wheel"), "ground": FlatGround("ground")})
rolling_disc.connections--> frozendict({"tyre": NonHolonomicTyre("tyre")})
Disadvantage of frozendict
is that it is not built-in
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.