molstar / molstar

A comprehensive macromolecular library
https://molstar.org
MIT License
599 stars 140 forks source link

Apply TransformStructureConformation on StructureSelection (subset) #1121

Open Simard302 opened 1 month ago

Simard302 commented 1 month ago

Is it possible to apply a TransformStructureConformation on a StructureSelection? So far, I have only been able to get it to work on a whole structure.

My goal is to select a few atoms and apply a transform on them. I currently create a StructureElement.Bundle for the atoms I want to transform. I then create a StructureSelection using StateTransforms.Model.StructureSelectionFromBundle. Finally, I apply my StateTransforms.Model.TransformStructureConformation. Here is the code:

const builder =  plugin.state.data.build()
    let ref = builder
        .to(plugin.helpers.substructureParent.get(struct)!)
        .apply(StateTransforms.Model.StructureSelectionFromBundle, 
            { bundle: aBundle, label: '[Atom Selection]'}, {tags: 'atom-selection'})
        .selector;
    builder.to(ref)
        .apply(StateTransforms.Representation.StructureRepresentation3D)

    builder.to(ref)
        .apply(StateTransforms.Model.TransformStructureConformation,
            { transform: {
                name: 'matrix' as const,
                params: {data: t, transpose: false}
            }});

    builder.commit();

After reading how the transform is actually applied, I think it might only work on entire structures (correct me if I am wrong). If that is the case, is there another way to apply a transform to a subset of atoms? Would I need to write something along the lines of:

loci = StructureElement.Bundle.toLoci(aBundle, struct)
units = []
for (ele in loci.elements) {
    op = SymmetryOperator.create(...) // create op from transform
    units.push(ele.unit.applyOperator(...)) // apply operation to unit
}
units.concat(unitsInStructNotInLoci) // add all other units from struct to units
s = Structure.create(unit, ...)
SO.Molecule.Structure(s, ...)

Additionally, once I have applied the transform to these atoms, how do I create a new representation of the entire structure that uses the post-transformed coordinates? Would I get the entire original structure, remove my selected atoms, and add them back with the post-transformed data?

dsehnal commented 3 weeks ago

Hi, yes, this is possible:

image

You have to apply the visual node to the transformed part:

const xform = builder.to(ref)
        .apply(StateTransforms.Model.TransformStructureConformation,
            { transform: {
                name: 'matrix' as const,
                params: {data: t, transpose: false}
            }});

builder.commit();

// ... 
const update = plugin.state.data.build();
update.to(xform).apply(Represention3D, ...) // this could be part of the original builder too

So if you just want to "move away a few atoms" and still keep the original representation, you will have to apply 100% transparency overpaint to the relevant part on the original structure as well (or manage your selections and their representation differently).

frederickparsons-tandemai commented 3 weeks ago

@dsehnal thanks for this insight! We are trying to test out a proof-of-concept editor using Mol*. We are looking to update the coordinates of the subset of atoms and then have the option to export the model using the new coordinates. Would this approach you just shared be the best way to do this?

We have also considered creating a "editing" trajectory layer to do this instead.

What would be your recommendation on the best way to build these editing features in Mol*?

dsehnal commented 3 weeks ago

Using the conformation transforms like this isn't a way to do an editor due to how that is implemented.

I've previously built an editor by providing a custom data source (essentially a JSON representation of a subset of mmCIF file), modifying it on the fly and letting the state reconciliation do the rest.

There is a basic editor available in Envision too which is built over Mol* if you have an academic account (although the source code isn't available for that currently).

frederickparsons-tandemai commented 3 weeks ago

Thanks @dsehnal, so the custom data source is currently the only supported way to modify a structure's coordinates at runtime? Is it possible to override the coordinates in the store programmatically instead, then force a re-render?

dsehnal commented 3 weeks ago

There are other ways too, don't need to go full out custom data model.

I don't the requirements for you app but, for example, you can keep track of the coordinates separately and merge them into a single structure node. This would be similar to the "custom data" but only focused on the conformation.

frederickparsons-tandemai commented 3 weeks ago

Thank you again @dsehnal! The requirements are actually quite simple.

  1. Be able to update a model's coordinate at runtime and have the 3D view update in real-time.
  2. Be able to export this new model to file with the updated coordinates.

(down the road we may want to do full on editing including change atom element type or even growing the molecule in place but we could do that by preparing a new SDF file and reloading it replacing the old one)

When you refer to "merge them into a single structure node" do you mean updating the structure node itself? Or creating a new merged structure like in the docking-viewer example?

image

I am having a hard time mapping the "update coordinate" concept to a concrete molstar function call.

dsehnal commented 3 weeks ago

Something like this (pseudocode):

const WithConformation = Transform({
  from: Model,
  to: Model,
  params: { conformation: PD.Value<{ x, y, z }>() }
})({
  apply({ a, params }) {
    return { ...a.data, conformation: params.conformation };
  }
})

// build state tree
const model = ...;
const conf = to(model).apply(WithConformation, { conformation: model.data.conformation} )
const structure = to(conf)....;
...

// later
conf.update({ conformation: newConformation })

It is also possible to store the coordianates as data inside another cell instead of the value params, then you need to sue the { dependsOn } approach.

Possible optimization is also to store just the coordinates that changed in the WithConformation xform.

Simard302 commented 3 weeks ago

I tried implementing the pseudocode you showed above. I can see the conformation data gets updated but the rendering of the new coordinates are not reflected in the rendering in the viewer. What would I need to do to use those new coordinates for my representation?

const WithConformation = Transform({
    name: 'coords-test-transform',
    from: PluginStateObject.Molecule.Model,
    to: PluginStateObject.Molecule.Model,
    params: { conformation: PD.Value<{ x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number>}>({x: [], y: [], z: []})}
})({
    apply({ a, params}) {
        // Create a model object but with the new coordinates
        return {...a, data: {...a.data, atomicConformation: {
            ...a.data.atomicConformation,
            ...params.conformation // this is the new coordinates
        }}};
    }
})
let builder = plugin.state.data.build();
const conf = builder.to(model.properties?.cell.sourceRef!).apply(WithConformation, { conformation: originalCoordinates})
builder.to(conf.ref).apply(StateTransforms.Model.StructureFromModel);
await builder.commit();

const newCoords = ...

builder = plugin.state.data.build();
builder.to(conf.ref).update({conformation: newCoords});
await builder.commit();
frederickparsons-tandemai commented 3 weeks ago

@dsehnal with your previous pseudocode, like Adam showed I think we are getting pretty close! Like Adam explained, we see the coordinates update in the state but the view is not rendering the change.

I was looking at the built in transform-structure-conformation Transformer to try and better understand what may be going wrong. There, it seems the approach to update the coordinates are done using a new Structure:

image

The limitation is that only rotations and translations can be applied. Is my understanding correct that this why you proposed to create a model Transform instead? Like that we can manipulate any atom coordinate as we want?

I guess the only missing part we have is how do we force a re-render of the structure so that the new coordinates are reflected in the viewer?

dsehnal commented 3 weeks ago

Models store, among other things, the raw coordinate data. Structures are more fancy with instancing etc. The reason only rotation/translations are supported there is that we can do nearest neighbor queries on the instances atoms quite effectively.

The re-rendering happens automatically if you update a parent cell with the new data... the Mol* state works similar to react.


@frederickparsons-tandemai do you plan to make at least parts of this open source?

frederickparsons-tandemai commented 3 weeks ago

That's awesome to hear! How do we define parent cells? Are they only the models? In our case it seems we were successfully updating the coordinates in the model but the viewer wasn't updating. I am not so sure yet why, does anything blatantly wrong jump out from that code? I will investigate it later this afternoon.


@dsehnal Indeed we do! The first plan is to contribute to the project by creating a gallery of examples to teach users like in NGL: https://nglviewer.org/ngl/gallery/ That's why I was setting up Mol* in CodePen last week :) For instance, once we have the coordinates update working we will create an example that will teach users how to do this through a nice concise CodePen example.

I don't think the team is well versed enough with the Mol code to contribute to the project just yet. However, when we will have helper functions to update coordinates (rotate a bond, dihedral, etc.) we'll be happy to share them with the Mol team to incorporate in the project if you are inclined to do so.

I will post later in the week our plan for the gallery in the Documentation channel. We would like to get feedback on this plan from the Mol* team.

dsehnal commented 3 weeks ago

Yes, it's in this part

const conf = builder.to(model.properties?.cell.sourceRef!).apply(WithConformation, { conformation: originalCoordinates})
builder.to(conf.ref).apply(StateTransforms.Model.StructureFromModel);
await builder.commit();

In case this is a modification of the existing state, you need to do

to(model).insert(WithConformation, ...)

This will put the new code as parent of the subtree of the model node, whereas your code just adds a new model leaf. The pseudocode I shared was assuming you are building the state from scratch.

Also, for creating the new Model I would base it on this code and add a new id field to indicate this is actually a different model.


@frederickparsons-tandemai Good to hear about open sourcing this. Perhaps we should hop a on a call sometimes to chat and align about this if you want.

frederickparsons-tandemai commented 3 weeks ago

Thank you very much! This is very useful.


Yes! We are currently putting together a plan for the "fundamentals" gallery and will post in the documentation channel soon. Looking forward to chat and align on it.