Skip to main content

mn-render

How to render images and animations of molecules from a Python script with the molecularnodes package (Canvas, engines, framing, styles, materials, colour, trajectories, deterministic renders for tests). Load before writing or debugging any script that calls mn.Canvas, snapshot, animation or record.

معلومات المصدر

المستودع
BradyAJohnston/MolecularNodes
آخر نشاط في المصدر
٢٥ أكتوبر ٢٠٢٦ في ١٧:٠٣
لغة SKILL.md المكتشفة
الإنجليزية
النجوم
١٬٣٩٧
التفرعات
١٣٧

خيارات التثبيت

يُحدَّد Prompt الذي يراجع المصدر أولًا بشكل افتراضي. يمكنك التبديل إلى أمر مباشر أو تنزيل نسخة محلية.

مراجعة ملفات المصدر

اقرأ SKILL.md وأي ملفات مرافقة يعرضها SkillsMP قبل أن تقرر التثبيت.

عرض SKILL.md

SKILL.md
تعليمات المصدر · معاينة للقراءة فقط
name
mn-render
description
How to render images and animations of molecules from a Python script with the molecularnodes package (Canvas, engines, framing, styles, materials, colour, trajectories, deterministic renders for tests). Load before writing or debugging any script that calls mn.Canvas, snapshot, animation or record.
# Rendering with Molecular Nodes from a Python script ## 1. Mental model - **One `Canvas`, many entities.** `mn.Canvas` wraps the active Blender scene: engine, resolution, camera, world lighting, compositor and output. Molecules are `mn.Molecule` entities that you load, style and then frame with `canvas.look_at`. - **Create the Canvas before loading anything.** It loads the Molecular Nodes scene preset (camera, lights, world, compositor) into an empty scene. Calling `mn.Canvas()` again is safe: it binds to the existing scene and only reloads the preset when the scene holds no molecules. - **Styles are node trees.** `add_style` builds a small node branch per call; the render shows whatever the evaluated geometry is. `look_at` frames that evaluated geometry, so a molecule with one chain styled is framed on that chain. - **Units.** Positions are Å times `world_scale = 0.1`, so 1 Å is 0.1 Blender units. - There is no `mn.Trajectory` any more; MD topologies and trajectories load into `mn.Molecule` as well. ## 2. Environment for a script Always via `uv run` (never bare `python`). In a script that imports `bpy` directly: ```python import os, tempfile os.environ.setdefault( "BLENDER_USER_EXTENSIONS", tempfile.mkdtemp() ) # before import bpy import bpy # mathutils is importable only after this import molecularnodes as mn canvas = mn.Canvas(mn.scene.Cycles(samples=64, device="CPU"), resolution=(1200, 900)) canvas.compositor.device = ( "CPU" # Blender 5 defaults the compositor to GPU; aborts without one ) ``` Without the `BLENDER_USER_EXTENSIONS` line, an installed MN extension's bundled wheels shadow the venv. `tests/conftest.py` does the same thing. Keep scratch scripts outside the repo. **Inside Blender with the installed extension** (`blender -b --python script.py`), the package is `import bl_ext.blender_org.molecularnodes as mn`; `import molecularnodes` fails there. Install headless with `blender --online-mode --command extension install --enable molecularnodes` (without `--online-mode` Blender refuses to reach the platform). Do not call `bpy.ops.wm.read_factory_settings()` to reset between renders: it has been reported to strip the extension's bundled wheels mid-run, so the next `Molecule.load()` fails on a missing biotite file. Delete the default objects by hand, or use one Blender process per render. Don't name a scratch script after a standard-library module (`inspect.py`, `code.py`, `types.py`, ...). Importing `bpy` imports that module, finds the script instead, and Blender initialises twice and aborts with `Check failed: !IsGoogleLoggingInitialized()`. ## 3. Minimal still ```python mol = mn.Molecule.fetch("4ozs") # .bcif from RCSB, cached mol.add_style("cartoon", material=mn.material.AmbientOcclusion()) canvas.look_at(mol, viewpoint="front") canvas.snapshot("4ozs.png") # returns an IPython Image for PNG/JPEG ``` `snapshot(path=None, frame=None, file_format="PNG", render_scale=100)`. With no path it renders to a temp file and returns the image (notebooks display it). `frame=` renders one scene frame and restores the current frame afterwards. TIFF/EXR write the file but return `None`. The canonical docs versions are in `docs/api/index.qmd` and `docs/api/rendering.qmd`. ## 4. Canvas settings | member | meaning | | --- | --- | | `engine` | `mn.scene.EEVEE(samples=64, raytracing=True)` or `mn.scene.Cycles(samples=256, device="GPU", denoise=True, denoise_gpu=True)`; also accepts `"EEVEE"` / `"CYCLES"`. Constructor values are written to the scene on assignment. | | `resolution` | `(x, y)` pixels. `render_scale` is the percentage. | | `samples` | samples on the active engine. | | `transparent` | film transparency (alpha background). | | `background` | world background RGBA; shortcut into the `MN_world_shader` node. | | `world.hdri_strength` | lighting strength of the preset HDRI. | | `view_transform`, `exposure`, `gamma`, `look` | colour management. Blender defaults to AgX; use `"Standard"` when colours must come out as specified. `mn.Canvas(view_transform="Standard")` sets it on construction. | | `passes` | render passes: combined, z, mist, normal, position, vector, diffuse_color, emit, environment, ambient_occlusion. | | `frame`, `frame_start`, `frame_end`, `frame_range`, `fps` | timeline. Setting `frame` uses `frame_set`, so trajectories update. | | `camera` | `lens`, `clip_start`, `clip_end`, `rotation` (XYZ Euler in degrees), `basis`, `set_viewpoint(...)`, `frame_points(points, margin)`. | | `compositor` | `device`, `precision`, denoise settings, `reset()`, `clear()`, `add_annotations()`. | - `canvas.clear()` deletes every object except cameras and lights (including the preset backdrop), purges orphans recursively, and keeps engine, world, compositor and render settings. `canvas.load_preset()` reloads the whole preset. - `Cycles(device="GPU")` tries OPTIX, CUDA, METAL, HIP, ONEAPI and falls back to CPU with a warning. EEVEE needs a GPU; the GitHub runners do not have one, so CI-facing scripts use Cycles CPU. - `world.reset()` and `compositor.reset()` remove the preset nodes: after them `canvas.background` raises `ValueError` and the annotation overlay is gone (`compositor.add_annotations()` restores it). ## 5. Loading entities ```python mol = mn.Molecule.fetch("9MD2") # format=".bcif", database="rcsb" af = mn.Molecule.fetch("Q8W3K0", database="alphafold") pdb = mn.Molecule.load("path/protein.pdb", style="cartoon") # single structure traj = mn.Molecule.load("topol.tpr", "traj.xtc", name="md") # MD topology + coordinates u_mol = mn.Molecule(u) # from an MDAnalysis Universe ``` `fetch(code, format=".bcif", cache=download.CACHE_DIR, database="rcsb")`. `load` routes single files through biotite and topology plus coordinates through MDAnalysis; `style=` is optional and defaults to no style, leaving the tree empty. In tests use the `fetch` fixture instead, which loads from `tests/data` and never downloads. ## 6. Styles, materials, colour ```python mol.add_style( "surface", selection="protein", material=mn.material.Flat(), color="common" ) mol.add_style("ball_and_stick", selection="not protein", color=(1.0, 0.5, 0.0, 1.0)) mol.add_style( lambda: mg.StyleCartoon(quality=5, loop_radius=0.6), color=lambda: mg.ColorRainbow() ) ``` - **Style names:** `spheres`, `cartoon`, `ribbon`, `surface`, `sticks`, `ball_and_stick`. Extra kwargs go to the style node's inputs (`quality`, `scale`, `loop_radius`, `sphere`, ...); unknown names raise `TypeError`, unknown style strings `ValueError`. `add_style` returns the molecule, so calls chain. - **Callable style** (`lambda: mg.StyleX(...)`) builds the node itself, so it cannot be combined with `selection`, `material` or kwargs (`TypeError`); `color`, `assembly` and `name` are fine. - **Selection** is an existing boolean attribute name first, then an MDAnalysis phrase (stored as a managed `sel_N` attribute), an `AtomGroup`, or a callable returning a boolean socket (`lambda: mg.IsPeptide() & mg.IsSideChain()`). - **`assembly=True`** instances the style over the file's biological assembly. - **Materials.** Presets in `mn.material`: `Default(roughness, ao_distance, ao_exponent)`, `AmbientOcclusion(distance, exponent)`, `Flat(outline, threshold, thickness)` (no outline unless `outline=True`), `Squishy(subsurface_scale, roughness)`, `Transparent(transparency, fresnel, outline_color)`. Each instantiation builds an independent datablock (`"Flat.001"`), and its parameters are live: `mat.distance = 0.15` changes the next render. Passing the string `material="Flat"` reuses one shared datablock, so edits affect every style using it. `"Fresnel Outline"` exists only as `mn.material.append_material("Fresnel Outline")`. `Transparent` needs the blended surface method, which its recipe sets. To swap the material on an existing style: `style.i.material.default_value = mat.material`. - **Colour.** `color=` takes `"common"`/`"default"` (elements, carbons random per chain), `"plddt"`, the name of an existing *colour* attribute, an RGBA sequence, or a callable returning a colour socket. Any other string warns and applies nothing (this replaced a silent black render). Arrays go on directly with `mol["Color"] = rgba_array`. - **Colouring part of a representation.** Do not add the same style once per selection to colour regions (a cartoon per section, sticks per nucleotide): each style builds its own geometry, so a cartoon or ribbon is cut at every boundary (blocky helix ends, stubs at the cuts). Add each style once and colour it from a per-atom attribute: ```python mol["Color"][mol.universe.select_atoms("chainID A and resid 19-26").ix, :3] = ( 0.1, 0.7, 0.7, ) # color=None reads Color; or store under another name and pass color="name" mol.add_style("cartoon") mol.add_style("sticks", selection="chainID A and resid 19-26") ``` A new attribute needs `atype="FLOAT_COLOR"`: `mol["name"] = rgba` infers a `FLOAT4`, which `add_style` rejects (it warns and applies nothing). Cartoon colour follows the backbone atoms, so colouring only side-chain atoms leaves the cartoon unchanged. - **Spheres in EEVEE.** Point clouds are ray-traced only by Cycles; on other engines `add_style` switches `StyleSpheres` from `sphere="Point"` to `"Instance"` for you. If you build the tree by hand, set `sphere="Instance"` or `"Mesh"` yourself, and use instances whenever point clouds with different materials are joined. ## 7. Framing ```python canvas.look_at(mol) # keep current direction, fit the subject canvas.look_at( mol, viewpoint="top", margin=0.15 ) # front, back, top, bottom, left, right, default canvas.look_at(mol, viewpoint=(90, 0, 45)) # XYZ Euler degrees canvas.look_at(mol.get_view("chainID A and resid 1-40")) canvas.look_at(points_xyz) # any (N, 3) array in world units ``` - `look_at(target, viewpoint=None, margin=0.05)` fits the camera to the evaluated geometry without changing where it points; `margin=0` is exact, negative crops. - `camera.lens` changes after `look_at` invalidate the fit: set the lens first, or call `look_at` again. - **Stale camera basis.** `set_viewpoint` writes `rotation_euler`, but the fit reads `matrix_world`, which only updates with the depsgraph. `look_at(entity_or_object, viewpoint=...)` is safe because reading the evaluated geometry updates it first. `look_at(points_array, viewpoint=...)`, or `set_viewpoint(...)` followed by a separate `look_at`, fits against the previous orientation and renders empty or off-centre (measured 2026-09-18). Call `bpy.context.view_layer.update()` between the two until `look_at` does it itself. - `frame_points` extends `clip_end` when the subject would be clipped; if a hand-placed camera renders nothing, check `camera.clip_end` first. - Rotating the subject instead of the camera: leave the camera at `"default"` and rotate the object, or use the `viewpoint` tuple in a loop for orbits (section 9). ## 8. Trajectories Per-molecule playback properties, all stored on the Blender object: `frame`, `subframes`, `offset`, `average`, `correct_periodic`, `interpolate`. A trajectory with `n` universe frames and `subframes = s` spans `n * (s + 1) - 1` scene frames. ```python traj = mn.Molecule(u) # secondary structure per frame, if cartoon/ribbon needs it traj.calculations.dssp(updating=True) traj.subframes, traj.interpolate, traj.average = 1, True, 1 traj.add_style("cartoon", color="common") canvas.fps = 24 canvas.frame_range = (0, u.trajectory.n_frames * 2 - 1) canvas.frame = 0 canvas.look_at(traj, viewpoint="top", margin=0.35) ``` `Molecule.frames_to_collection(start=0, stop=None, step=1)` bakes frames into a collection for the *Animate Frames* node when the timeline route is not wanted. `set_frame` is what the frame-change handler calls; user code sets `canvas.frame`. ## 9. Animations and image sequences Three routes, pick by what changes between frames: 1. **Timeline only changes** (trajectory playback, keyframed values, `SceneTime` in a tree): `canvas.animation(path="out.mp4", frame_start=None, frame_end=None, render_scale=100, fps=None, format=None)`. Format is inferred from a `.gif` suffix; MP4 is H.264; GIF needs pillow. It locks the interface, renders to a temp directory and restores frame, fps, scale and output settings afterwards. 2. **Camera, style or colour change per frame:** ```python with canvas.record("orbit.mp4", fps=12) as movie: for i in range(36): canvas.look_at(mol, viewpoint=(90, 0, i * 10)) movie.render(render_scale=50) ``` `record(path=None, fps=None, render_scale=100, frames_dir=None, overwrite=True)` returns a `FrameRecorder`; `render()` writes `%05d.png`, `finalize(path, fps, format)` assembles them. The context manager finalises only on a clean exit and only when a path was given. `frames_dir=` plus `overwrite=False` resumes an interrupted run. All frames must share one resolution. 3. **A few stills at chosen frames:** `canvas.snapshot(path, frame=f)` in a loop. ## 10. Deterministic renders and tests - Same-seed Cycles CPU renders are pixel-identical on one platform. Recipe: ```python canvas.engine = mn.scene.Cycles(samples=32, device="CPU", denoise=False) canvas.compositor.device = "CPU" ``` Compare decoded pixels, not PNG bytes (metadata differs). Exactness does not hold across platforms or Blender versions.
عرض على GitHub
ملف SKILL.md هذا كبير جدا، لذلك يعرض SkillsMP القسم الاول فقط هنا. عرض على GitHub