Frame¶
A Frame is a container of named Block tables plus free-form metadata (e.g., timestep, units, provenance).
Use a Frame when you want to keep multiple related tables (atoms, bonds, box, …) together and pass them through analysis or IO as a single object.
import molpy as mp
import numpy as np
1. Creating a Frame¶
You can create a Frame from:
- explicit
Blockobjects - nested dicts (auto-converted to
Block)
Extra keyword arguments are stored in frame.metadata.
frame = mp.Frame(
blocks={
"atoms": {
"id": [1, 2, 3],
"element": ["O", "H", "H"],
"x": [0.000, 0.957, -0.239],
"y": [0.000, 0.000, 0.927],
"z": [0.000, 0.000, 0.000],
},
"bonds": {
"i": [0, 0],
"j": [1, 2],
},
},
timestep=0,
description="water",
)
frame
Frame( [atoms] id: shape=(3,) [atoms] element: shape=(3,) [atoms] x: shape=(3,) [atoms] y: shape=(3,) [atoms] z: shape=(3,) [bonds] i: shape=(2,) [bonds] j: shape=(2,) )
2. Reading a Block by Name¶
A frame behaves like a mapping: block_name -> Block.
Once you have a Block, you use the same operations as in the Block tutorial.
atoms = frame["atoms"]
print("atoms:", atoms)
print("nrows:", atoms.nrows)
print("xyz:\n", atoms[["x", "y", "z"]])
atoms: Block(id: shape=(3,), element: shape=(3,), x: shape=(3,), y: shape=(3,), z: shape=(3,)) nrows: 3 xyz: [[ 0. 0. 0. ] [ 0.957 0. 0. ] [-0.239 0.927 0. ]]
3. Inspect: What Blocks Exist?¶
A stable, public way is to inspect the serialized form (to_dict()), which exposes block keys.
print(sorted(frame.to_dict()["blocks"].keys()))
['atoms', 'bonds']
4. Update: Add or Replace a Block¶
Assigning a nested dict auto-converts it to a Block. This is convenient for adding computed tables.
frame2 = frame.copy()
frame2["tags"] = {"name": ["O", "H", "H"], "is_h": [False, True, True]}
print("type(tags):", type(frame2["tags"]))
print("tags:\n", frame2["tags"])
type(tags): <class 'molpy.core.frame.Block'> tags: Block(name: shape=(3,), is_h: shape=(3,))
5. Delete: Remove a Block¶
Deletion is explicit, and membership checks use "atoms" in frame.
frame3 = frame.copy()
del frame3["bonds"]
print("'bonds' in frame3:", "bonds" in frame3)
print("blocks:", sorted(frame3.to_dict()["blocks"].keys()))
'bonds' in frame3: False blocks: ['atoms']
6. Metadata Is Part of the State¶
frame.metadata is a plain dict. Use it for timestep, description, provenance, units, etc.
frame4 = frame.copy()
frame4.metadata["note"] = "prepared in tutorial"
print("metadata (with note):", frame4.metadata)
del frame4.metadata["note"]
print("metadata (after delete):", frame4.metadata)
metadata (with note): {'timestep': 0, 'description': 'water', 'note': 'prepared in tutorial'}
metadata (after delete): {'timestep': 0, 'description': 'water'}
7. Copy Semantics (Important)¶
Frame.copy() is shallow:
- blocks are copied as
Block.copy() - underlying NumPy arrays are not copied
So in-place mutation of arrays can affect both frames. If you need deep copies, copy the arrays explicitly.
f_shallow = frame.copy()
f_shallow["atoms"]["x"][0] = 777.0
# Shallow copy means the original sees the same underlying array.
print("frame atoms x:", frame["atoms"]["x"])
print("shallow atoms x:", f_shallow["atoms"]["x"])
frame atoms x: [ 7.77e+02 9.57e-01 -2.39e-01] shallow atoms x: [ 7.77e+02 9.57e-01 -2.39e-01]
# Rebuild a clean frame for the rest of the tutorial
frame = mp.Frame(
blocks={
"atoms": {
"id": [1, 2, 3],
"element": ["O", "H", "H"],
"x": [0.000, 0.957, -0.239],
"y": [0.000, 0.000, 0.927],
"z": [0.000, 0.000, 0.000],
},
"bonds": {
"i": [0, 0],
"j": [1, 2],
},
},
timestep=0,
description="water",
)
# One pragmatic deep-copy approach: round-trip via dict and copy each column array.
deep_blocks = {}
for name, blk_dict in frame.to_dict()["blocks"].items():
deep_blocks[name] = mp.Block.from_dict({col: np.asarray(values).copy() for col, values in blk_dict.items()})
deep_frame = mp.Frame(blocks=deep_blocks, **frame.metadata)
deep_frame["atoms"]["x"][0] = 999.0
print("frame atoms x:", frame["atoms"]["x"])
print("deep_frame atoms x:", deep_frame["atoms"]["x"])
frame atoms x: [ 0. 0.957 -0.239] deep_frame atoms x: [ 9.99e+02 9.57e-01 -2.39e-01]
8. Serialize / Deserialize¶
Frame.to_dict() produces a JSON-friendly structure with two keys:
blocks: mapping block-name → block-dictmetadata: plain dict
Frame.from_dict(...) reconstructs a new Frame.
payload = frame.to_dict()
restored = mp.Frame.from_dict(payload)
print("restored blocks:", sorted(restored.to_dict()["blocks"].keys()))
print("restored metadata:", restored.metadata)
restored blocks: ['atoms', 'bonds']
restored metadata: {'timestep': 0, 'description': 'water'}
9. Iterate Blocks and Columns¶
A typical reporting/debugging pattern is to list all blocks and their columns.
for block_name in sorted(frame.to_dict()["blocks"].keys()):
print(block_name, "->", list(frame[block_name].keys()))
atoms -> ['id', 'element', 'x', 'y', 'z'] bonds -> ['i', 'j']