Frame & Block¶
Ready to dive into MolPy's core data structures? Let's explore Block and Frame - the building blocks that make everything work!
What are Blocks and Frames?¶
- Block: Think of it as a smart dictionary that automatically converts everything to NumPy arrays. Super handy for storing columns of data!
- Frame: A container that holds multiple Blocks (like
atoms,bonds, etc.) plus metadata. It's your go-to structure for molecular systems.
Let's get started:
In [1]:
Copied!
import molpy as mp
import molpy as mp
Working with Blocks¶
Blocks are super simple - just like dictionaries, but they automatically convert values to NumPy arrays:
In [2]:
Copied!
# Create an empty block
from molpy.core.frame import Block
block = Block()
# Add data (automatically converted to numpy arrays)
block["x"] = [0.0, 1.0, 2.0]
block["y"] = [0.0, 0.0, 0.0]
block["z"] = [0.0, 0.0, 0.0]
print(f"Block keys: {list(block.keys())}")
print(f"x values: {block['x']}")
print(f"x type: {type(block['x'])}")
# Create an empty block
from molpy.core.frame import Block
block = Block()
# Add data (automatically converted to numpy arrays)
block["x"] = [0.0, 1.0, 2.0]
block["y"] = [0.0, 0.0, 0.0]
block["z"] = [0.0, 0.0, 0.0]
print(f"Block keys: {list(block.keys())}")
print(f"x values: {block['x']}")
print(f"x type: {type(block['x'])}")
Block keys: ['x', 'y', 'z'] x values: [0. 1. 2.] x type: <class 'numpy.ndarray'>
Block Indexing¶
Blocks support various indexing methods:
In [3]:
Copied!
# Create a block with more data
block = Block(
{
"x": [0.0, 1.0, 2.0, 3.0],
"y": [0.0, 1.0, 2.0, 3.0],
"z": [0.0, 0.0, 0.0, 0.0],
"element": ["C", "C", "C", "H"],
"type": [1, 1, 1, 2],
}
)
# Access by key
print(f"x values: {block['x']}")
# Access single row by index
print(f"First atom: {block[0]}")
# Access multiple rows by slice
print(f"First two atoms: {block[0:2]}")
# Concatenate multiple columns
xyz = block[["x", "y", "z"]]
print(f"XYZ coordinates shape: {xyz.shape}")
print(f"XYZ:\n{xyz}")
# Create a block with more data
block = Block(
{
"x": [0.0, 1.0, 2.0, 3.0],
"y": [0.0, 1.0, 2.0, 3.0],
"z": [0.0, 0.0, 0.0, 0.0],
"element": ["C", "C", "C", "H"],
"type": [1, 1, 1, 2],
}
)
# Access by key
print(f"x values: {block['x']}")
# Access single row by index
print(f"First atom: {block[0]}")
# Access multiple rows by slice
print(f"First two atoms: {block[0:2]}")
# Concatenate multiple columns
xyz = block[["x", "y", "z"]]
print(f"XYZ coordinates shape: {xyz.shape}")
print(f"XYZ:\n{xyz}")
x values: [0. 1. 2. 3.]
First atom: {'x': 0.0, 'y': 0.0, 'z': 0.0, 'element': 'C', 'type': 1}
First two atoms: {'x': array([0., 1.]), 'y': array([0., 1.]), 'z': array([0., 0.]), 'element': array(['C', 'C'], dtype='<U1'), 'type': array([1, 1])}
XYZ coordinates shape: (4, 3)
XYZ:
[[0. 0. 0.]
[1. 1. 0.]
[2. 2. 0.]
[3. 3. 0.]]
Working with Frames¶
Frames are where the magic happens! You can add data using the tuple syntax frame["block_name", "variable_name"]:
In [4]:
Copied!
# Create a frame
frame = mp.Frame()
# Add atoms block with data - super clean!
frame["atoms"] = mp.Block(
{
"x": [0.0, 1.0, 2.0],
"y": [0.0, 0.0, 0.0],
"z": [0.0, 0.0, 0.0],
"element": ["C", "C", "C"],
"type": [1, 1, 1],
}
)
# Add a box to metadata
frame.metadata["box"] = mp.Box.cubic(10.0)
print(f"Frame has {frame['atoms'].nrows} atoms")
print(f"Box: {frame.metadata['box']}")
# Create a frame
frame = mp.Frame()
# Add atoms block with data - super clean!
frame["atoms"] = mp.Block(
{
"x": [0.0, 1.0, 2.0],
"y": [0.0, 0.0, 0.0],
"z": [0.0, 0.0, 0.0],
"element": ["C", "C", "C"],
"type": [1, 1, 1],
}
)
# Add a box to metadata
frame.metadata["box"] = mp.Box.cubic(10.0)
print(f"Frame has {frame['atoms'].nrows} atoms")
print(f"Box: {frame.metadata['box']}")
Frame has 3 atoms Box: <Orthogonal Box: [10. 10. 10.]>
Accessing Frame Data¶
Accessing data is super intuitive - get the block first, then access variables:
In [5]:
Copied!
# Get the atoms block
atoms = frame["atoms"]
print(f"Atoms block keys: {list(atoms.keys())}")
# Access individual variables
x_coords = atoms["x"]
print(f"X coordinates: {x_coords}")
y_coords = atoms["y"]
print(f"Y coordinates: {y_coords}")
# Get coordinates as a combined array
xyz = atoms[["x", "y", "z"]]
print(f"XYZ shape: {xyz.shape}")
print(f"XYZ:\n{xyz}")
# Get the atoms block
atoms = frame["atoms"]
print(f"Atoms block keys: {list(atoms.keys())}")
# Access individual variables
x_coords = atoms["x"]
print(f"X coordinates: {x_coords}")
y_coords = atoms["y"]
print(f"Y coordinates: {y_coords}")
# Get coordinates as a combined array
xyz = atoms[["x", "y", "z"]]
print(f"XYZ shape: {xyz.shape}")
print(f"XYZ:\n{xyz}")
Atoms block keys: ['x', 'y', 'z', 'element', 'type'] X coordinates: [0. 1. 2.] Y coordinates: [0. 0. 0.] XYZ shape: (3, 3) XYZ: [[0. 0. 0.] [1. 0. 0.] [2. 0. 0.]]
Indexing and Slicing:¶
Since Block is aligned, you can use all the usual indexing and slicing tricks:
In [6]:
Copied!
# index one row and return a dict
print(block[0])
# slice a range of rows return as a Block
print(block[0:2])
# index by list of keys
print(block[["x", "y", "z"]])
# index one row and return a dict
print(block[0])
# slice a range of rows return as a Block
print(block[0:2])
# index by list of keys
print(block[["x", "y", "z"]])
{'x': 0.0, 'y': 0.0, 'z': 0.0, 'element': 'C', 'type': 1}
{'x': array([0., 1.]), 'y': array([0., 1.]), 'z': array([0., 0.]), 'element': array(['C', 'C'], dtype='<U1'), 'type': array([1, 1])}
[[0. 0. 0.]
[1. 1. 0.]
[2. 2. 0.]
[3. 3. 0.]]
Modifying Frame Data¶
Updating data is just as easy:
In [7]:
Copied!
# Add a new column to the atoms block
frame["atoms"]["mass"] = [12.0, 12.0, 12.0]
print(f"New keys: {list(frame['atoms'].keys())}")
# Modify existing data
frame["atoms"]["x"] = [0.0, 2.0, 4.0]
print(f"Updated x: {frame['atoms']['x']}")
# Add a new column to the atoms block
frame["atoms"]["mass"] = [12.0, 12.0, 12.0]
print(f"New keys: {list(frame['atoms'].keys())}")
# Modify existing data
frame["atoms"]["x"] = [0.0, 2.0, 4.0]
print(f"Updated x: {frame['atoms']['x']}")
New keys: ['x', 'y', 'z', 'element', 'type', 'mass'] Updated x: [0. 2. 4.]