Introduction
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.
![modular_bicycle_model_explanation](https://user-images.githubusercontent.com/97806294/224776385-e21a6606-4e50-4341-b650-d6192099b82e.png)
Overview
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:
- Imports;
- Define
symbols
and dynamicsymbols
;
- Define objects/nodes, such as
ReferenceFrame
, Point
, RigidBody
, Particle
;
- Define kinematics/relations, such as the orientation from one frame w.r.t. another;
- Define loads, i.e. fores, torques (before constraints as they may alter velocities);
- Define constraints, i.e. holonomic and non-holonomic constraints;
- Form equations of motion.
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
.
- Both modules can import sympy objects, but only the
WhippleBicycle
may import a WheelBase
. Specific implementation of a wheel like KnifeEdgeWheel
should not be imported.
- Symbols like the radius, mass, inertia can be defined by the implementation of a
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.
- The instances of a
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
.
- The rotation angle of the wheel is defined with respect to another object, a subclass of
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])
.
- Loads generally act between objects and need to know more about the general kinematics and objects created by different extensions. In case of noncontributing forces it should be noticed that they do actually change velocities by introducing auxilary speeds.
TyreModelBase
...
- If there is even a holonomic constraint is the question, which can only be answered by the bicycle, e.g.
WhippleBicycle
. So the creation of the constraint is a responsibility of WhippleBicycle
.
- The equations of motion are in the end formed by
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
).
User stories
Probably best to write some user stories first.
Simple Whipple bicycle
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()
Core
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:
- Let
ModelBase
inherit from a different meta class, which fixes the problem in its __new__
method.
- Put a decorator before every class, which would do something similar as
dataclass
does.
I'm gonna try to do the first. For a few reasons. First of it has to be applied to every subclass, so it is quite nice if it works via inheritance and not via a decorator before each subclass. Besides that it is probably not exactly possible to get the desired behaviour with something like attrs
, which would be an extra dependency (though a quite nice one).
Base classes
ModelBase
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:
- It needs to handle the symbols, associated with itself, while also being able to retrieve the ones from its child's. Some things to keep in mind are:
- What types of symbols are there?
- Constants
- Dynamicsymbols
- Coordinates
- Speeds
- Time-dependent loads
- Symbols should be unique, as multiple instances of one object may exist.
- Give each
ModelBase
a name
property that is used as a prefix for each symbol.
- Allow the user to specify each symbol.
Dummy
's can be used, but will result in equations that are difficult to read.
- There should be an easy way to retrieve a description of a symbol.
- There should be a part on propagation to the child extensions.
- There should be a property with all the child extensions, i.e.
extensions: set
.
- An option would be to have abstract methods for building the extension with the child's. It could be split in a few parts:
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:
- Should it be possible to overwrite a certain symbol? For example
wheel.radius = symbols('r')
- Should it be possible to overwrite a certain object? For example
wheel.body = RigidBody('wheel')
- Would quite like using the
WhippleBicycle
as a class, but there are multiple formulations, which would lead to different implementations.
- An option might be to have different classes for each formulation, but have a
__new__
in WhippleBicycle
which just selects the right formulation for you.