Generated banner for Relion
Generated logo for Relion

Relion

K-DenseCommunityCreated by K-Dense Inc.

  • Skill
  • Workflow
  • Science and research
  • MIT
  • Low risk
  • Has scripts

From the author

Validates and executes RELION single-particle cryo-EM refinement and half-map postprocessing. Supports STAR optics/acquisition checks, particle-stack consistency, gold-standard half sets, soft-mask validation, diagnostic Fourier shell correlation, and restart guidance.
— the description by K-Dense Inc.

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

The relion skill provides a structured Python-based workflow for managing single-particle cryo-EM refinement and postprocessing using RELION 5.0.1. It focuses on ensuring data consistency by validating STAR files, optics groups, and particle stacks before executing refinement. The workflow supports gold-standard half-set refinement and provides diagnostic tools for Fourier shell correlation (FSC) and soft-mask validation, helping researchers maintain rigorous standards throughout the reconstruction process.

What it does

  • Validates STAR file metadata, optics groups, and particle stack consistency.
  • Executes gold-standard 3D refinement using MPI-parallelized RELION processes.
  • Performs diagnostic Fourier shell correlation (FSC) analysis on independent half-maps.
  • Facilitates postprocessing with soft-masking to refine map quality.
  • Provides guidance for restarting interrupted refinement jobs.
  • Checks for common data errors like non-finite metadata or mismatched map grids.

When to use it

  • When performing single-particle cryo-EM refinement on extracted, CTF-annotated particles.
  • When you need to validate the integrity of your particle stacks and optics groups before processing.
  • When conducting gold-standard refinement requiring independent half-set splitting.
  • When performing postprocessing on converged unfiltered half-maps.
  • When you need to generate diagnostic FSC curves for map resolution assessment.

Advantages

  • Ensures data consistency through automated pre-refinement validation checks.
  • Standardizes the execution of complex RELION commands via a unified interface.
  • Prevents common errors by verifying pixel sizes, box dimensions, and coordinate conventions.
  • Supports reproducible research by recording command history and process logs.
  • Provides clear diagnostic outputs for FSC and mask validation.

Usage examples

“Validate my particle STAR file before starting refinement.”
The skill runs a series of checks on the STAR file, verifying optics membership, particle indices, and stack accessibility, reporting any inconsistencies.
“Run a 3D refinement on my particle subset.”
The skill executes the RELION refinement process with specified symmetry, diameter, and MPI ranks, ensuring gold-standard half-set splitting.
“Calculate the FSC for my refined half-maps.”
The skill performs a diagnostic FSC calculation between the two unfiltered half-maps and outputs the results to a TSV file.
“Postprocess my refined maps with a soft mask.”
The skill validates the mask and map grids before running the RELION postprocessing utility to generate the final sharpened map.

Install

  1. Download the skill as a ZIP:

    https://mp-staging.autogineer.com/agent-skills/k-dense-ai/relion/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip relion.zip -d .agents/skills

Documentation by K-Dense Inc.

RELION single-particle refinement

Use for a RELION single-particle project, especially extracted particles → homogeneous selected particle subset → gold-standard refinement → half-map validation and postprocessing. The bundled runner starts from CTF-annotated extracted particles and an initial 3D reference. It does not replace motion correction, picking, 2D/3D selection, or a biological interpretation of map quality. For tomography, helical reconstruction, Blush, or heterogeneous-state modeling, use the appropriate upstream workflow rather than forcing those data into this bounded SPA runner.

Preserve acquisition and coordinate conventions

Read references/acquisition-and-restarts.md when starting from movies or resuming jobs. Confirm pixel size in Å/pixel, voltage in kV, spherical aberration in mm, defocus in Å, amplitude contrast as a fraction, and the symmetry justified by the specimen. Do not “correct” a suspicious value by guessing its units.

data_optics describes acquisition/image groups; data_particles references them through _rlnOpticsGroup. Particle filenames use one-based [email protected]; leading zeros such as [email protected] are valid. Relative paths resolve from the RELION project directory, not the STAR file's directory. Keep optics groups when merging or subsetting STAR files. _rlnOriginXAngst/_rlnOriginYAngst are Å translations, not pixels.

Run from this skill directory with paths to the real project:

python scripts/spa_workflow.py validate-star project/particles.star --project project

This opens referenced stacks and checks optics membership, finite acquisition/CTF values, indices, box sizes, duplicate particle references and existing half-set assignments. Use --metadata-only only when stacks are genuinely unavailable; the JSON records stack_checks_performed: false. It does not scan every particle pixel for corruption or establish correct image normalization. Physical-range warnings are review prompts, not proof that unusual microscope settings are wrong.

Refine a selected particle population

Before running, inspect representative particles and class averages, defocus distributions, CTF fits, particle orientation distribution, and the initial reference. Ensure the map and particle boxes/pixel sizes agree after any downsampling. The runner deliberately supports one effective box/pixel size across optics groups; handle heterogeneous sampling with an explicit upstream resampling workflow. Use conventionally extracted, normalized particles that have not already been phase-flipped or Wiener-filtered; this runner does not configure those special input cases.

python scripts/spa_workflow.py refine \
  --star project/particles.star --reference project/initial.mrc \
  --project project --diameter 180 --symmetry C1 \
  --initial-lowpass 40 --mpi-ranks 3 --threads 2 --output project/RefinePilot

The diameter and low-pass filter above are illustrative Å values. Use specimen-appropriate values. Refinement executes mpirun -np 3 relion_refine_mpi with --auto_refine, --split_random_halves, --ctf, and a low-pass starting reference. Gold-standard splitting requires MPI; the plain sequential relion_refine executable cannot perform this split. Use odd ranks ≥3 (master plus balanced half-set workers), with a matching MPI installation. The CPU command is useful for a bounded pilot; choose a documented GPU/MPI launch for full data.

The runner keeps the command, native version and log in a new output directory, records an explicit random seed (default 1), surfaces runtime warnings, stops on process failure, and requires converged unfiltered half maps before reporting success. It does not automatically retry expensive jobs or silently discard failed-job artifacts. Keep _optimiser.star, model/sampling STAR files, and referenced particle paths for restart. Use the original job's optimiser rather than starting a new random split from a partially processed table.

Inspect independent half maps

Use the two independently refined unfiltered half maps, never two copies of the combined, sharpened map. Matching headers cannot establish statistical independence; the independent particle assignments and refinement history provide that evidence. Inspect directional anisotropy, preferred orientation and local resolution as well as a global FSC curve.

python scripts/spa_workflow.py fsc \
  project/RefinePilot/run_half1_class001_unfil.mrc \
  project/RefinePilot/run_half2_class001_unfil.mrc --output diagnostic-fsc.tsv

This checks map dimensions, finite values, pixel size, origin, axis order and duplicate maps, then writes an unmasked diagnostic FSC. The reported 0.143 crossing uses linear interpolation; null means no downward crossing was detected, not infinite resolution. Nyquist resolution is 2 × pixel size. This diagnostic is limited to even cubic maps ≤256³; use RELION's native relion_image_handler --fsc for larger maps. It does not substitute for mask-corrected FSC. The helper requires real-space maps with canonical axes, zero MRC start indices and orthogonal cell angles. Convert other grids explicitly with provenance; merely editing headers can misalign density. Matching headers and FSC cannot determine absolute handedness.

Postprocess with a soft mask

Construct the solvent mask from an appropriately low-pass-filtered density, with an expanded boundary and a smooth edge. Inspect all slices; a tight mask can inflate correlation. Avoid a mask derived from high-frequency noise shared between half maps.

python scripts/spa_workflow.py postprocess \
  --half1 project/RefinePilot/run_half1_class001_unfil.mrc \
  --half2 project/RefinePilot/run_half2_class001_unfil.mrc \
  --mask project/soft_mask.mrc --output project/PostProcessPilot

The helper checks a nonconstant mask in [0,1], soft-edge voxels and matching map grids, then runs relion_postprocess with explicit half maps, mask and pixel size. RELION performs its own mask/randomization correction and writes postprocess.star. The bounded command leaves the B-factor at zero (no automatic B-factor estimation); add automatic/manual sharpening only after choosing a defensible fit range and inspecting map quality. A valid range and some fractional mask voxels do not prove the mask is scientifically appropriate. Inspect the phase-randomized masked FSC near the reported resolution: residual correlation calls for a smoother/wider mask and another postprocessing run.

See references/runtime-and-validation.md for the tested native utilities and the distinction between pipeline execution and reconstruction validation.

Primary references

Files

scripts/spa_workflow.py

#!/usr/bin/env python3
"""Validate RELION SPA inputs, run refinement/postprocessing, and inspect half-map FSC."""
from __future__ import annotations

import argparse
import csv
import json
import math
from pathlib import Path
import re
import subprocess


def read_map(path: Path, max_voxels: int | None = None):
    import mrcfile
    import numpy as np
    with mrcfile.mmap(path, permissive=False) as handle:
        if np.iscomplexobj(handle.data):
            raise ValueError(f'{path}: expected a real-space density map, not complex MRC data')
        if max_voxels is not None and handle.data.size > max_voxels:
            raise ValueError("Python diagnostic limited to 256-cubed maps; use relion_image_handler --fsc for larger maps")
        data = handle.data.copy()
        voxel = tuple(float(handle.voxel_size[axis]) for axis in ('x', 'y', 'z'))
        origin = tuple(float(handle.header.origin[axis]) for axis in ('x', 'y', 'z'))
        axes = tuple(int(handle.header[axis]) for axis in ('mapc', 'mapr', 'maps'))
        starts = tuple(int(handle.header[axis]) for axis in ('nxstart', 'nystart', 'nzstart'))
        angles = tuple(float(handle.header.cellb[axis]) for axis in ('alpha', 'beta', 'gamma'))
    if data.ndim != 3 or len(set(data.shape)) != 1 or min(data.shape) < 4 or data.shape[0] % 2:
        raise ValueError(f'{path}: expected an even-sized cubic 3D map')
    if not np.isfinite(data).all() or not all(math.isfinite(x) for x in voxel + origin):
        raise ValueError(f'{path}: nonfinite map or header')
    if min(voxel) <= 0 or not np.allclose(voxel, voxel[0], rtol=1e-5):
        raise ValueError(f'{path}: positive isotropic voxel size required')
    if axes != (1, 2, 3):
        raise ValueError(f'{path}: noncanonical map axes; explicitly reorder before analysis')
    if starts != (0, 0, 0) or not np.allclose(angles, 90, rtol=0, atol=1e-4):
        raise ValueError(f'{path}: this runner requires zero MRC start indices and orthogonal cell angles; verify the grid before conversion')
    return data, voxel[0], origin


def validate_star(path: Path, project: Path, check_stacks: bool = True) -> dict:
    import mrcfile
    import numpy as np
    import starfile
    blocks = starfile.read(path, always_dict=True)
    if not {'optics', 'particles'}.issubset(blocks):
        raise ValueError('Requires RELION 3.1+ data_optics and data_particles blocks')
    optics, particles = blocks['optics'], blocks['particles']
    if not hasattr(optics, 'columns') or not hasattr(particles, 'columns'):
        raise ValueError('Optics and particles must be STAR loop tables')
    needed_optics = {'rlnOpticsGroup', 'rlnVoltage', 'rlnSphericalAberration', 'rlnAmplitudeContrast',
                     'rlnImagePixelSize', 'rlnImageSize', 'rlnImageDimensionality'}
    needed_particles = {'rlnOpticsGroup', 'rlnImageName', 'rlnDefocusU', 'rlnDefocusV', 'rlnDefocusAngle'}
    if not needed_optics.issubset(optics.columns) or not needed_particles.issubset(particles.columns):
        raise ValueError('Missing optics/acquisition/CTF columns for a 2D-particle SPA refinement')
    if len(optics) == 0 or len(particles) == 0 or optics.rlnOpticsGroup.duplicated().any():
        raise ValueError('Empty tables or duplicate optics group IDs')
    optional_numeric = {'rlnOriginXAngst', 'rlnOriginYAngst', 'rlnAngleRot', 'rlnAngleTilt',
                        'rlnAnglePsi', 'rlnCoordinateX', 'rlnCoordinateY', 'rlnPhaseShift'}
    for frame, columns in [(optics, needed_optics),
                           (particles, (needed_particles - {'rlnImageName'}) | (optional_numeric & set(particles.columns)))]:
        for column in columns:
            values = np.asarray(frame[column], dtype=float)
            if not np.isfinite(values).all():
                raise ValueError(f'Nonfinite values in {column}')
    if any(optics.rlnOpticsGroup <= 0) or any(optics.rlnOpticsGroup % 1):
        raise ValueError('Optics group IDs must be positive integers')
    if not set(particles.rlnOpticsGroup).issubset(set(optics.rlnOpticsGroup)):
        raise ValueError('Particle references an undefined optics group')
    if any(optics.rlnImagePixelSize <= 0) or any(optics.rlnVoltage <= 0) or any(optics.rlnSphericalAberration < 0):
        raise ValueError('Pixel size/voltage must be positive and spherical aberration nonnegative')
    if any((optics.rlnAmplitudeContrast < 0) | (optics.rlnAmplitudeContrast >= 1)):
        raise ValueError('Amplitude contrast must be in [0,1)')
    if any(optics.rlnImageDimensionality != 2) or any(optics.rlnImageSize < 4) or any(optics.rlnImageSize % 2):
        raise ValueError('This runner requires 2D particles with an even box size >=4')
    if any(particles.rlnDefocusU <= 0) or any(particles.rlnDefocusV <= 0):
        raise ValueError('Underfocus values must be positive Angstroms; verify CTF import units/sign')
    if particles.rlnImageName.duplicated().any():
        raise ValueError('Duplicate particle image references can leak between half sets')
    subsets = None
    if 'rlnRandomSubset' in particles:
        values = set(particles.rlnRandomSubset)
        if values != {1, 2}:
            raise ValueError('Existing rlnRandomSubset must contain both half sets, encoded 1 and 2')
        subsets = {str(k): int(v) for k, v in particles.rlnRandomSubset.value_counts().items()}
    groups = optics.set_index('rlnOpticsGroup')
    stack_cache, canonical_images = {}, set()
    for row in particles.itertuples(index=False):
        match = re.fullmatch(r'([0-9]+)@(.+)', str(row.rlnImageName))
        if not match or int(match[1]) < 1:
            raise ValueError(f'Expected 1-based [email protected]: {row.rlnImageName}')
        index, stack_name = int(match[1]), match[2]
        stack = Path(stack_name)
        stack = (stack if stack.is_absolute() else project / stack).resolve()
        identity = (index, stack)
        if identity in canonical_images:
            raise ValueError('Duplicate particle image after path normalization')
        canonical_images.add(identity)
        if check_stacks:
            if stack not in stack_cache:
                with mrcfile.mmap(stack, permissive=False) as handle:
                    if np.iscomplexobj(handle.data):
                        raise ValueError(f'{stack}: expected real-space particle images')
                    if tuple(int(handle.header[a]) for a in ('mapc', 'mapr', 'maps')) != (1, 2, 3):
                        raise ValueError(f'{stack}: noncanonical particle stack axes')
                    shape = handle.data.shape
                    # MRC may expose a single image as 2D rather than a stack of length one.
                    stack_cache[stack] = ((1, *shape) if len(shape) == 2 else shape)
            shape = stack_cache[stack]
            box = int(groups.loc[row.rlnOpticsGroup, 'rlnImageSize'])
            if len(shape) != 3 or index > shape[0] or shape[1:] != (box, box):
                raise ValueError(f'Particle stack index/box does not agree with optics: {row.rlnImageName}')
    warnings = []
    if any((optics.rlnVoltage < 60) | (optics.rlnVoltage > 400)):
        warnings.append('Unusual voltage; confirm kV rather than V')
    if any(particles.rlnDefocusU > 100000) or any(particles.rlnDefocusV > 100000):
        warnings.append('Defocus exceeds 10 micrometers; inspect CTF fits and units')
    return {'particles': len(particles), 'optics_groups': len(optics), 'half_sets': subsets,
            'stack_checks_performed': check_stacks,
            'pixel_sizes_angstrom': sorted(set(float(x) for x in optics.rlnImagePixelSize)),
            'box_sizes': sorted(set(int(x) for x in optics.rlnImageSize)), 'warnings': warnings}


def check_halves(first: Path, second: Path, mask: Path | None = None, max_voxels: int | None = None):
    import numpy as np
    a, pixel, origin = read_map(first, max_voxels)
    b, other_pixel, other_origin = read_map(second, max_voxels)
    if a.shape != b.shape or not np.isclose(pixel, other_pixel) or not np.allclose(origin, other_origin):
        raise ValueError('Half maps disagree in grid, pixel size, or origin')
    if np.array_equal(a, b):
        raise ValueError('Half maps are identical; provide independent unfiltered half reconstructions')
    if a.min() == a.max() or b.min() == b.max():
        raise ValueError('Constant half map has no measurable signal')
    if mask:
        m, mp, mo = read_map(mask)
        if m.shape != a.shape or not np.isclose(pixel, mp) or not np.allclose(origin, mo):
            raise ValueError('Mask grid differs from half maps')
        if m.min() < 0 or m.max() > 1 or m.max() == m.min():
            raise ValueError('Mask must be nonconstant and lie in [0,1]')
        if not np.any((m > 0) & (m < 1)):
            raise ValueError('Mask has no soft edge; generate a soft solvent mask')
    return a, b, pixel


def fsc(first: Path, second: Path, output: Path) -> dict:
    import numpy as np
    a, b, pixel = check_halves(first, second, max_voxels=256**3)
    n = a.shape[0]
    fa, fb = np.fft.rfftn(a), np.fft.rfftn(b)
    z, y, x = np.ogrid[-n//2:n-n//2, -n//2:n-n//2, 0:n//2+1]
    shells = np.floor(np.sqrt(z*z+y*y+x*x)+0.5).astype(int)
    # Shift only the fully represented axes; weight interior rFFT x planes twice.
    fa, fb = np.fft.fftshift(fa, axes=(0,1)), np.fft.fftshift(fb, axes=(0,1))
    weights = np.ones(fa.shape); weights[:,:,1:-1] = 2
    cross = np.bincount(shells.ravel(), weights=(weights*(fa*np.conj(fb)).real).ravel())
    power_a = np.bincount(shells.ravel(), weights=(weights*abs(fa)**2).ravel())
    power_b = np.bincount(shells.ravel(), weights=(weights*abs(fb)**2).ravel())
    counts = np.bincount(shells.ravel(), weights=weights.ravel())
    denominator = np.sqrt(power_a*power_b)
    values = np.divide(cross, denominator, out=np.full_like(cross, np.nan), where=denominator>0)
    rows = [{'shell': k, 'frequency_inv_angstrom': k/(n*pixel),
             'fsc': float(values[k]) if np.isfinite(values[k]) else None,
             'fourier_samples': int(counts[k])} for k in range(n//2+1)]
    crossing = None
    for previous, current in zip(rows[1:], rows[2:]):
        a_fsc, b_fsc = previous['fsc'], current['fsc']
        if a_fsc is not None and b_fsc is not None and a_fsc >= 0.143 > b_fsc:
            frequency = previous['frequency_inv_angstrom'] + (0.143-a_fsc)/(b_fsc-a_fsc)/(n*pixel)
            crossing = 1/frequency
            break
    output.parent.mkdir(parents=True, exist_ok=True)
    with output.open('w', newline='') as handle:
        writer = csv.DictWriter(handle, fieldnames=list(rows[0]), delimiter='\t')
        writer.writeheader(); writer.writerows(rows)
    return {'unmasked_diagnostic_fsc_0143_angstrom': crossing, 'nyquist_angstrom': 2*pixel,
            'warning': 'Diagnostic unmasked FSC; use corrected RELION postprocessing FSC for reported global resolution',
            'shells': len(rows)}


def run_native_command(command: list[str], output: Path, project: Path, executable: str) -> None:
    if output.exists() and any(output.iterdir()):
        raise ValueError('Use an empty output directory; preserve prior jobs for restart/provenance')
    version = subprocess.run([executable, '--version'], capture_output=True, text=True, check=True)
    version_text = version.stdout + version.stderr
    if not re.search(r'(?<![\w.])5\.0\.1(?![\w.])', version_text):
        raise ValueError('This runner targets RELION5.0.1; verify the installed executable before adapting it')
    output.mkdir(parents=True, exist_ok=True)
    (output/'version.txt').write_text(version_text)
    (output/'command.json').write_text(json.dumps(command, indent=2)+'\n')
    with (output/'run.log').open('w') as log:
        subprocess.run(command, cwd=project, stdout=log, stderr=subprocess.STDOUT, check=True)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    sub = parser.add_subparsers(dest='action', required=True)
    star = sub.add_parser('validate-star')
    star.add_argument('star', type=Path); star.add_argument('--project', type=Path, default=Path.cwd())
    star.add_argument('--metadata-only', action='store_true', help='Explicitly omit opening particle stacks')
    comparison = sub.add_parser('fsc')
    comparison.add_argument('half1', type=Path); comparison.add_argument('half2', type=Path)
    comparison.add_argument('--output', type=Path, required=True)
    refine = sub.add_parser('refine')
    refine.add_argument('--star', type=Path, required=True); refine.add_argument('--reference', type=Path, required=True)
    refine.add_argument('--diameter', type=float, required=True, help='Particle diameter in Angstroms')
    refine.add_argument('--symmetry', default='C1'); refine.add_argument('--initial-lowpass', type=float, default=40.0)
    refine.add_argument('--threads', type=int, default=1); refine.add_argument('--executable', default='relion_refine_mpi')
    refine.add_argument('--mpirun', default='mpirun'); refine.add_argument('--mpi-ranks', type=int, default=3)
    refine.add_argument('--seed', type=int, default=1)
    refine.add_argument('--project', type=Path, default=Path.cwd()); refine.add_argument('--output', type=Path, required=True)
    post = sub.add_parser('postprocess')
    post.add_argument('--half1', type=Path, required=True); post.add_argument('--half2', type=Path, required=True)
    post.add_argument('--mask', type=Path, required=True); post.add_argument('--output', type=Path, required=True)
    post.add_argument('--executable', default='relion_postprocess')
    args = parser.parse_args()
    try:
        if args.action == 'validate-star':
            result = validate_star(args.star, args.project, not args.metadata_only)
        elif args.action == 'fsc':
            result = fsc(args.half1, args.half2, args.output)
        elif args.action == 'refine':
            result = validate_star(args.star, args.project)
            reference, pixel, _ = read_map(args.reference)
            if result['box_sizes'] != [reference.shape[0]] or any(abs(p-pixel)>1e-5*pixel for p in result['pixel_sizes_angstrom']):
                raise ValueError('Reference box/pixel size must match all particle optics groups for this bounded runner')
            if (not math.isfinite(args.diameter) or not math.isfinite(args.initial_lowpass)
                    or not 0 < args.diameter < reference.shape[0]*pixel or args.initial_lowpass < 2*pixel
                    or args.threads < 1 or args.mpi_ranks < 3 or args.mpi_ranks % 2 == 0
                    or not 0 <= args.seed <= 2147483647):
                raise ValueError('Invalid diameter, lowpass, threads, seed (0..2147483647) or MPI ranks (use odd ranks >=3)')
            if not re.fullmatch(r'(?:[CD][1-9][0-9]*|[TO]|I[1-4]?)', args.symmetry):
                raise ValueError('Use an explicit supported RELION point-group symmetry such as C1, D2, O, I1')
            command = [args.mpirun, '-np', str(args.mpi_ranks), args.executable, '--o', str(args.output.resolve()/'run'), '--i', str(args.star.resolve()),
                       '--ref', str(args.reference.resolve()), '--auto_refine', '--split_random_halves', '--ctf',
                       '--firstiter_cc', '--ini_high', str(args.initial_lowpass), '--sym', args.symmetry,
                       '--particle_diameter', str(args.diameter), '--flatten_solvent', '--zero_mask',
                       '--oversampling', '1', '--healpix_order', '2', '--auto_local_healpix_order', '4',
                       '--offset_range', '5', '--offset_step', '2', '--j', str(args.threads), '--random_seed', str(args.seed)]
            run_native_command(command, args.output.resolve(), args.project.resolve(), args.executable)
            if not all((args.output/f'run_half{k}_class001_unfil.mrc').is_file() for k in (1,2)):
                raise ValueError('Refinement exited without converged unfiltered half maps; inspect run.log/optimiser')
            result['refined_particles'] = validate_star(args.output/'run_data.star', args.project)
            result['runtime_warnings'] = list(dict.fromkeys(line.split('WARNING:', 1)[1].strip()
                for line in (args.output/'run.log').read_text().splitlines() if 'WARNING:' in line))
            result['command'] = command
        else:
            _, _, pixel = check_halves(args.half1, args.half2, args.mask)
            command = [args.executable, '--i', str(args.half1.resolve()), '--i2', str(args.half2.resolve()),
                       '--mask', str(args.mask.resolve()), '--angpix', str(pixel), '--o', str(args.output.resolve()/'postprocess')]
            run_native_command(command, args.output.resolve(), Path.cwd(), args.executable)
            if not (args.output/'postprocess.star').is_file():
                raise ValueError('Postprocess did not produce postprocess.star')
            result = {'command': command, 'pixel_size_angstrom': pixel}
    except (ValueError, OSError, KeyError, subprocess.CalledProcessError) as exc:
        parser.exit(1, f'Error: {exc}\n')
    print(json.dumps(result, indent=2))


if __name__ == '__main__':
    main()

Requirements and compatibility

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs Python

The author says: Python 3.12+ with numpy, mrcfile and starfile for bundled validation; RELION 5.0.1 CPU/MPI executables for refinement and postprocessing. Native workflows require MPI, OpenMP and an FFT library (FFTW or MKL). GPU builds require their supported accelerator stack. Network is needed for installation only.

Security

What our scanner noticed· Low risk

  • Scripts that your agent may run

This skill includes scripts that your agent may run. Read them before you use it.

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Created by
K-Dense Inc.
Publisher
K-Dense (Community)
Source
K-Dense-AI/scientific-agent-skills · skills/relion · commit a25cf9e
Licence
MIT License (redistribution allowed)
  • LICENSE (added by AutoGineer from LICENSE.md)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by K-Dense Inc. or K-Dense.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download relion.zip