Grouping muscle activations

OpenSim Moco is a software toolkit to solve optimal control problems with musculoskeletal models defined in OpenSim using the direct collocation method.
User avatar
Ana de Sousa
Posts: 67
Joined: Thu Apr 07, 2016 4:21 pm

Grouping muscle activations

Post by Ana de Sousa » Tue Aug 27, 2024 2:35 am

Hi everyone!

I'm working on a project, and I need some advice. I want to group muscles in a specific area to share the same activation. Is there a way to do this in OpenSim Moco, or does it need to be handled in OpenSim itself (by modelling)?

For example, my model has two muscles: the rectus femoris and the vastus lateralis. Each muscle has its activation variable, so when I run MocoInverse, I get separate results for each muscle. I want to combine these two muscles into one group - let's call it 'quadriceps' - so that there is just one activation variable for the entire group, and I get a unified solution.

Does anyone know how to achieve this in Moco, or are there any constraints or techniques that might help?

Thanks in advance for any suggestions!

User avatar
Pasha van Bijlert
Posts: 226
Joined: Sun May 10, 2020 3:15 am

Re: Grouping muscle activations

Post by Pasha van Bijlert » Wed Aug 28, 2024 2:59 am

EDIT: my suggestion was based on the incorrect assumption that you can add multiple muscles to a "Controller" to achieve the desired result, but this was not correct, Nick suggested later in the thread to use the newly added SynergyController().
/EDIT


Hi Ana,

If you load a model into a problem that you solve with Moco, I think Moco automatically adds a controller to each actuator that doesn't already have one. I think if you first manually define a controller, then add the muscles you want it to control, you circumvent this. The code would look something like this (but I haven't tested it):

Code: Select all

mycontroller = Controller()
mycontroller.setActuator(muscle_one)
mycontroller.setActuator(muscle_two)
model.addController(mycontroller)

model.initSystem()
You may have to SafeDownCast the muscles first, and/or check whether the other (uncontrolled) actuators still get their own controller automatically.


Cheers,
Pasha
Last edited by Pasha van Bijlert on Tue Sep 03, 2024 11:32 am, edited 2 times in total.

User avatar
Ana de Sousa
Posts: 67
Joined: Thu Apr 07, 2016 4:21 pm

Re: Grouping muscle activations

Post by Ana de Sousa » Thu Aug 29, 2024 9:32 am

Hi Pasha, thanks a lot for the response!

I need to understand more about OpenSim's structure to implement this properly. For instance, I'm a bit confused about the roles of muscles, actuators, and controllers in the framework.

I was playing around with your code today:
1. I cannot seem to be able to use Controller directly, so I tried ControllerSet() instead.
2. It seems that setActuator does not exist, so I tried setActuators.

The problem is that setActuators() only works with actuators, not muscles. With this code:

Code: Select all

muscle_one = model.getMuscles().get("rect_fem_r")
mycontroller = osim.ControllerSet()
mycontroller.setActuators(muscle_one)
I get this error:

Code: Select all

TypeError: in method 'ControllerSet_setActuators', argument 2 of type 'OpenSim::Set< OpenSim::Actuator > &'
What am I missing or doing wrong here?

User avatar
Pasha van Bijlert
Posts: 226
Joined: Sun May 10, 2020 3:15 am

Re: Grouping muscle activations

Post by Pasha van Bijlert » Fri Aug 30, 2024 2:14 am

HI Ana,

Looking at the documentation again, it looks like it should have been addActuator(muscle_one) etc, not setActuator (which doesn't exist, like you said).

Code: Select all

mycontroller = Controller()
mycontroller.addActuator(muscle_one)
mycontroller.addActuator(muscle_two)
model.addController(mycontroller)

model.initSystem()
Muscles are actuators (in the sense that they are a subclass of actuators). The error you're getting is because setActuators requires an ActuatorSet (=a set of more than one actuator) as an input, and instead you were providing it with a single Actuator (muscle) as an input.

Bear in mind, I've never manually added controllers like this, there may be other things you have to do like bounding the control values between 0 - 1 in the problem.

Cheers,
Pasha

User avatar
Ana de Sousa
Posts: 67
Joined: Thu Apr 07, 2016 4:21 pm

Re: Grouping muscle activations

Post by Ana de Sousa » Fri Aug 30, 2024 6:30 am

Hey Pasha, thanks again for your suggestions!

Unfortunately, I can't create a Controller because it's an abstract class and can't be instantiated. I tried a quick workaround using PrescribedController:

Code: Select all

    
    my_controller = osim.PrescribedController()
    my_controller.addActuator(muscle_rect_fem_r)
    model.addController(my_controller)
However, it turns out that MocoCasADiSolver doesn't support models with controllers. :cry:

Do you have any other suggestions?

User avatar
Nicholas Bianco
Posts: 1043
Joined: Thu Oct 04, 2012 8:09 pm

Re: Grouping muscle activations

Post by Nicholas Bianco » Fri Aug 30, 2024 3:31 pm

Hi Ana,

What you looking for is a "synergy" controller. Luckily for you, this has just been added via OpenSim 4.5.1! I'll probably make a forum-wide post about the update soon, but if you subscribe to the OpenSim announcement mailing list then you should have already received an email about it.

Best,
Nick

User avatar
Pasha van Bijlert
Posts: 226
Joined: Sun May 10, 2020 3:15 am

Re: Grouping muscle activations

Post by Pasha van Bijlert » Sun Sep 01, 2024 7:46 am

Hi Nick,

What would adding a regular Controller with multiple muscles added to it as I described achieve?

Cheers,
Pasha

User avatar
Nicholas Bianco
Posts: 1043
Joined: Thu Oct 04, 2012 8:09 pm

Re: Grouping muscle activations

Post by Nicholas Bianco » Tue Sep 03, 2024 10:08 am

Hi Pasha,

The "Controller" class is an abstract class, so you can't use it directly in a model. You can use classes that derive from Controller (e.g., PrescribedController) since these classes implement the Controller interface.

If you add a controller per Ana's code snippet,

Code: Select all

my_controller = osim.PrescribedController()
my_controller.addActuator(muscle_rect_fem_r)
model.addController(my_controller)
then the control for "muscle_rect_fem_r" will be removed from the MocoProblem and prescribed directly based on the PrescribedController.

Best,
Nick

User avatar
Pasha van Bijlert
Posts: 226
Joined: Sun May 10, 2020 3:15 am

Re: Grouping muscle activations

Post by Pasha van Bijlert » Tue Sep 03, 2024 11:24 am

Ah got it, thanks! I'll edit my previous post to point out I was wrong.

Cheers,
Pasha

User avatar
Ana de Sousa
Posts: 67
Joined: Thu Apr 07, 2016 4:21 pm

Re: Grouping muscle activations

Post by Ana de Sousa » Wed Sep 04, 2024 9:04 am

Hi Nick!

Thank you so much for your response, it couldn't have come at a better time for my project! The recent release was exactly what I needed. :D

I just updated my Conda environment to the latest version and downloaded the new Moco inverse example from the official OpenSim documentation. Then, I adapted the code to fit the specific needs of my application.

To make things more manageable, I created a more straightforward example (for my application), which you can check out here: https://github.com/anacsousa1/moco-tuto ... rs.py#L300

In this example, I define muscle groups that work together as synergies.

Here's a quick summary of what I did:

First, I defined the muscle groups (I can also exclude some later):

Code: Select all

    muscle_groups = {
        "hamst": ["/forceset/bifemlh_r", "/forceset/bifemsh_r"],
        "glut": ["/forceset/glut_max2_r"],
        "quads": ["/forceset/rect_fem_r", "/forceset/vas_int_r"],
        "psoas": ["/forceset/psoas_r"],
        "gast": ["/forceset/med_gas_r", "/forceset/soleus_r"],
        "tibialis": ["/forceset/tib_ant_r"],
    }

    # Specify the muscle groups to be excluded from activation
    exclude_groups = {"glut", "tibialis", "psoas"}

    # The number of synergies is equal to the number of muscle groups.
    numSynergies = len(muscle_groups)

I built a list to store the names of all muscles in the model and then exported the control signals (muscle activations). Afterwards, I looped over each muscle group, combining them with the controllers.

Later, one of the more challenging parts was applying Non-negative Matrix Factorization (NNMF) to generate the synergies. This is new to me, but I think I got somewhere interesting. My approach is a bit different from the official example because I used muscle groups and excluded certain groups from activation:

Code: Select all

    # Apply Non-negative Matrix Factorization (NNMF) to the aggregated control data to find synergies.
    nmf = NMF(n_components=numSynergies, init='random', random_state=0)
    W = nmf.fit_transform(A)  # Synergy excitations over time
    H = nmf.components_  # Synergy vectors that combine muscle controls

    # Scale the synergies for normalisation.
    scaleVec = 0.5 * np.ones(H.shape[1])
    for i in range(numSynergies):
        scale_r = np.linalg.norm(scaleVec) / np.linalg.norm(H[i, :])
        H[i, :] *= scale_r
        W[:, i] /= scale_r

    # Create a SynergyController to control the muscles based on synergies.
    right_controller = osim.SynergyController()
    right_controller.setName("synergy_controller_right_leg")

    # Link each muscle in the right leg to the SynergyController.
    for name in right_control_names:
        right_controller.addActuator(osim.Muscle.safeDownCast(model.getComponent(name)))

    # Expand synergy vectors to match the number of actuators
    for i in range(numSynergies):
        synergy_vector = osim.Vector(len(right_control_names), 0.0)
        idx = 0
        for group_name, muscle_list in muscle_groups.items():
            if group_name in exclude_groups:
                # Zero out the synergy vector for excluded groups
                continue
            for muscle in muscle_list:
                if muscle in right_control_names:
                    synergy_vector.set(right_control_names.index(muscle), H[i, idx])
            idx += 1
        right_controller.addSynergyVector(synergy_vector)
Since it's not easy to share code in full here, I've explained the process in greater detail here: https://anacsousa.notion.site/Moco-inve ... 6ba?pvs=74

I hope this gives you a better idea of how I implemented these if others need it.

Anyway, what I don't understand is that after updating to OpenSim 4.5.1, I've started getting the following error, even in examples that don't use controllers:

Code: Select all

[error] Model unable to assemble: SimTK Exception thrown at Optimizer.h:133:
Value out of range in  OptimizerSystem  Constructor: expected 1 <= number of parameters <= 2.14748e+09 but number of parameters=0..Model relaxing constraints and trying again.

While this error doesn't seem to affect the optimisation process (at least from what I can tell), I'm curious why it's happening.

Thanks again for your help!

POST REPLY