Git Product home page Git Product logo

brim's People

Contributors

dependabot[bot] avatar jtheinen avatar tjstienstra avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

Forkers

jtheinen uinone

brim's Issues

Remove define_objects from __init__

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.

Simplify contact point computation

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.

Common loads

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.

Feedback BRiM tutorials session

Four-bar-linkage tutorial:

  • Add points (P1, ...) to the four-bar-linkage image.
  • Make sure to specify what all symbols mean
  • Possibly, remove some methods of doing the same thing

Get a feature to easily see what submodels and connection should be specified and how the attributes are called.

Skip slow tests

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.

Default forward lean angle

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.

Improved method to get connections between models

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?

Discussion switch to dummy symbols

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.

Rear and Front frame should not use NewtonianBodyMixin

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:

  • Implement support in the joints framework of sympy.physics.mechanics for namedtuple(frame=frame, point=point) as parent and child
  • Change the rear frame to use an interframe for each joints, removing the NewtonianBodyMixin, and update the bicycle classes
    • Add steer_interframe and wheel_interframe properties
  • Change the front frame to use an interframe for each joints, removing the NewtonianBodyMixin, and update the bicycle classes
    • Add steer_interframe and wheel_interframe properties

Make q, u default Matrix properties

While 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.

Rider lean extension

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.

Releasing brim on pypi

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&#x27;t have direct dependency: &#x27;sympy @ git+https://github.com/sympy/sympy.git&#x27;\n\n\n \n"

Benchmarking Whipple model generation

It would be good to do some benchmarking on the generation of the Whipple model:

  • Benchmark the number of operations before CSE
  • Benchmark the number of operations after CSE
  • Benchmark the time to generate the model

Use benchmark results

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:

  • Show the number of operations in the benchmark table.
  • Show the benchmark results automatically in each PR.
  • Add a constraint on the regression, such that the test fails if the number of operations in the EOMs gets more than previously.

Regression of the number of operations in the EOMs after CSE

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
$\mu \pm \sigma$ (s)
#Operations EOMs
before CSE
#Operations EOMs
after CSE
Manual by Moore $3.5\pm0.2$ 230789 2198
Manual by Stienstra $6.6\pm0.3$ 390554 2389
BRiM before #103 $5.8\pm0.3$ 468290 2176
BRiM after #103 $7.3\pm0.8$ 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:

  1. Upgrade the Point.vel method. While it would be ideal to make this more advanced, it is quite a nasty problem to solve.
  2. Add an 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.
  3. Add a manual set of velocity theorems in 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.

Symbol storage

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:

  • Must: this feature must be implemented.
  • Should: this feature should be implemented, but if there is no time it can be left out.
  • Could: this feature could be implemented, so it would be nice. It is however not that much of a problem if it is not implemented.
  • Won't: this feature won't be implemented.

The following requirements exist:

  • Several types of symbols must be stored (M):
    • 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.
    • Generalized coordinates (dynamicsymbols), which should be accessed separately may have a model with a time-varying input force.
    • Generalized speeds(dynamicsymbols), which should be accessed separately
  • The user must be able to retrieve a description of a symbol (M)
    • Otherwise you'll really have to use extremely clear names for everything, which makes things only more difficult.
    • A difficulty is that symbols might be changed, changes of symbols defining a body, can be changed within a body.
  • It would be nice if the user is able to change the symbol name (C)
    • Problem is however that some symbols already get used in the init. Possible fix is to only support it for some
  • The user should be able to retrieve what symbols are being used (S)
    • Would allow for some free_symbols function or something, though would this also contain functions?
  • There should be a consistent storage/retrieval method across all models (S)
  • The symbols must be defined in define_objects (M)
  • It could be nice if a user is able to also specify values, which can automatically be extracted if one were to implement a fancy lambdify helper function or something (W, at least not for now).
  • There could be some autochecking after define_kinematics, whether all generalized coordinates and speeds have been added to the system (C).

There are several approaches:

  • For generalized coordinates and speeds, just use a q and u matrix, mutable or immutable depending on whether changes are allowed and such.
  • One can for example use a protected dictionary or tuple to store some of the symbols and to be able to retrieve them. While using a 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.
  • The above approach can also be adopted by introducing one more general name, which uses a mutable storage type like a dictionary.
  • The most flexible approach would be to add a property like symbols, which returns a SymbolsStorage instance or something, which can either use internal SymbolDescriptor's or just tuples or dictionaries.

Change to from_formulation class method

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.

Set up a registry system

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.

Get all symbols

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.

Refactor test suite

As I'm currently rather quickly implementing new load groups, I see that there is quite some room for refactoring.

  • The _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.
  • The spherical load groups have quite a bit of overlapping implementation.
  • The spring dampers also have some overlaps.

requirements variable loses requirements in mixin inheritance

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.

Improve class names

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

Change core to cyclic graph using mixins

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.

Initial design

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

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:

  1. Imports;
  2. Define symbols and dynamicsymbols;
  3. Define objects/nodes, such as ReferenceFrame, Point, RigidBody, Particle;
  4. Define kinematics/relations, such as the orientation from one frame w.r.t. another;
  5. Define loads, i.e. fores, torques (before constraints as they may alter velocities);
  6. Define constraints, i.e. holonomic and non-holonomic constraints;
  7. 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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]).
  5. 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 ...
  6. 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.
  7. 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:

  1. Let ModelBase inherit from a different meta class, which fixes the problem in its __new__ method.
  2. 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.

Change base orientation of frames in the rider

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.

Fix broken links

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.

Add tutorials

This is an issue tracker, that follows up on the preliminary merged PR #111.

Two tutorials should be added:

  • The third tutorial, which is the tutorial for model developers to create their own models, connections, and load groups. This can follow Appendix C from my thesis, where the rolling disc is implemented from scratch.
  • Either a separate tutorial or an addition to the second BRiM users tutorial, such that it is more clear how to do on-the-fly customizations of BRiM models.

Redesign plotting structure

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:

  • Plot objects should be created:
    • PlotModel
    • PlotConnection
    • Possibly PlotLoadGroup, though this is not that necessary
  • These objects should be created in Plotter.add_model and Plotter.add_connection
  • There should be something like 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

Confusement on the ModelBase.system instance

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.

Add documentation

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:

  • Must: this feature must be implemented.
  • Should: this feature should be implemented, but if there is no time it can be left out.
  • Could: this feature could be implemented, so it would be nice. It is however not that much of a problem if it is not implemented.
  • Won't: this feature won't be implemented.

Below the list of documentation features:

  • (M) Auto generated API reference of all classes and functions.
  • (S) Add an explanation section to each class
  • (M) Add an attributes section to each class, if it has non-property attributes.
  • (M) Check documentation on the non-property attributes.
  • (S) Add a guide on contributing
  • (S) Add an explanation of the core, can mostly be copied from the conference abstract or the thesis.
    • (S) Add a diagram showing the idea of how the tree works.
      • (C) Make the diagram both dark as well as light theme compatible.
  • (C) Add some UML diagrams.
  • (C) Choose a nice theme.

Problem solution summary 6-4-2023

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.

Overview

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.

Problems

There are several problems/challenges encountered in this design:

  1. Submodels may have parts that depend on other submodels in the same layer, where the parent does not know how the computation is done.
  • Computing the contact point is an example, where it is specific to the ground wheel combination, where the wheel really knows most and should do the computation. One could argue that you should have a separate connector here (more on that later).
  • Computing the tyre model is another example, where it can be the case that at first when designing you just make a nonholonomic constraint. This will need the wheel and the ground as reference models (where the wheel may actually be the parent). An additional problem in this example is that one may actually introduce new kinematics in this computation, dependent on the tyre model.
  1. The last example of 1 leads to the related problem that the parent may not even know that a certain computation should be done.
  2. There is a high and logical sensitivity to breaking the tree structure, as one would normally see the submodels related more like a bidirectional graph.
  • An example where this breaking will occur is with loops between the models. The bicycle and the rider may be connected on multiple locations, which will vary in connection definition. Locations that can be thought of are the saddle, the handlebar and the pedals.
  1. New models might require new properties in other submodels, which is custom to them.
  • An example is the leaning rider, which requires the rear frame or saddle to have a specific leaning axis and leaning point.
  1. Last but not least is that everything should not be too complex for future developers.

Solutions

There are several solution approaches and solution parts, which involve solving one or more of the above problems:

  1. Problem 1 is currently being solved by having specific compute_... calls being made by the parent, this is however susceptible to problem 2.
  2. Problem 1 and 2 can be solved by introducing a define_connections stage, where submodels can learn about other submodels through specification by the parent.
  • This leads to the problem that definitions have a specific order. You should for example first orient the rear frame to the ground and the rear wheel to the ground, before defining the contact point location and auxilary velocity by the tyre model. This can be solved by adding an order decorator or something, such that one can choose when it is ran.
    • Just a quick not on the order is that, it is probably best to add an argument to the define functions of what order is currently being used, as some may require some definitions to be ran at different stages, as some parts are internal and some are also with other submodels.
  • Other note is that this is in a way breaking the tree structure
  1. Problem 3 can simply be solved by using directed graph algorithms instead of tree algorithms
  2. Solution 1 and 2 don't actually solve 4, this can be done with the add_mixin feature introduced previously.
  3. Change to use a registry and separate objects for defining connections, will be specifying this further in a minute

RiderLean model

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:

  1. Keep it and update it, removing the not support actuator property. It will probably just work.
  2. Remove it and force users to the more advanced Rider model.

Feature request: get unspecified components

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:

  • Create a method ModelBase.get_unspecified_components and have it return:
    • An iterable of attribute strings, which should be specified (advantage is that it is nice and short, but lacks extra information, which can of course be requested)
    • An iterable of unsatisfied requirements (disadvantage is that printing this gives quite a repr)
  • Other options ...

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?

Default values

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.

Improve module names

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

Change frozenset usage to frozen dictionaries?

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

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.