Skip to content

graphld.surrogates

Surrogate marker assignment for missing variants.

When variants are missing from GWAS summary statistics, graphREML can assign surrogate markers in high LD with those variants. This is particularly useful when working with HapMap3 SNPs only (~1.1M SNPs).

surrogates

Generate surrogate-marker mapping files for LD graphical models.

Each output is an HDF5 file per population, with one int32 1-D dataset per LD block. The dataset length is the full loaded LDGM size, and both dataset positions and values are full loaded LDGM row indices. For variants already present in the training sumstats, the surrogate index equals the variant index.

The heavy surrogate-marker search therefore needs to be performed once per population and block; later GraphREML runs can look up the stored mapping.

Surrogates

Bases: ParallelProcessor

prepare_block_data classmethod

prepare_block_data(metadata: DataFrame, **kwargs) -> list[dict]

Partition dataframe with nonmissing variants among blocks and attach block names.

Source code in src/graphld/surrogates.py
@classmethod
def prepare_block_data(cls, metadata: pl.DataFrame, **kwargs) -> list[dict]:
    """Partition dataframe with nonmissing variants among blocks and attach block names."""
    nonmissing_variant_ids: pl.DataFrame = kwargs.get('nonmissing_variant_ids')
    dfs = partition_variants(metadata, nonmissing_variant_ids)
    names = metadata.get_column('name').to_numpy().tolist()
    return [
        {'df': df, 'block_name': name}
        for df, name in zip(dfs, names, strict=False)
    ]

create_shared_memory classmethod

create_shared_memory(metadata, block_data, **kwargs) -> SharedData

No shared memory needed; return empty container for interface compliance.

Source code in src/graphld/surrogates.py
@classmethod
def create_shared_memory(cls, metadata, block_data, **kwargs) -> SharedData:
    """No shared memory needed; return empty container for interface compliance."""
    return SharedData({})

supervise classmethod

supervise(manager: WorkerManager, shared_data: SharedData, block_data: list[dict], **kwargs) -> np.ndarray

Wait for all workers to finish and return output path.

Source code in src/graphld/surrogates.py
@classmethod
def supervise(
    cls,
    manager: WorkerManager,
    shared_data: SharedData,
    block_data: list[dict],
    **kwargs
) -> np.ndarray:
    """Wait for all workers to finish and return output path."""
    manager.start_workers()
    manager.await_workers()
    return kwargs.get('output_path')

process_block classmethod

process_block(ldgm, flag, shared_data: SharedData, block_offset: int, block_data: dict, worker_params: dict)

Compute surrogate markers and save to file.

Source code in src/graphld/surrogates.py
@classmethod
def process_block(
    cls,
    ldgm,
    flag,
    shared_data: SharedData,
    block_offset: int,
    block_data: dict,
    worker_params: dict,
):
    """Compute surrogate markers and save to file."""
    # Use merge_snplists to match the merge logic in heritability.py
    # This ensures allele matching is applied consistently
    sumstats_df = block_data['df']
    match_by_position = worker_params.get('match_by_position', False)

    num_indices = ldgm.shape[0]
    mapping = np.arange(num_indices, dtype=np.int32)

    if len(sumstats_df) == 0:
        candidate_indices = np.array([], dtype=np.int64)
    else:
        # Do not modify ldgm in place: the output map is keyed by full LDGM row
        # coordinates, while merge_snplists renumbers variant_info["index"].
        merged_ldgm, _ = merge_snplists(
            ldgm, sumstats_df,
            match_by_position=match_by_position,
            pos_col='POS',
            ref_allele_col='REF',
            alt_allele_col='ALT',
            modify_in_place=False
        )

        # Candidates are unique full-LDGM row indices among non-missing variants.
        candidate_indices = np.array(
            merged_ldgm._which_indices
            if merged_ldgm._which_indices is not None
            else np.arange(merged_ldgm.shape[0]),
            dtype=np.int64,
        )
    candidates = pl.DataFrame({'index': candidate_indices}).with_row_index(
        name='surrogate_nr'
    )

    if len(candidates) > 0:
        missing_indices = np.setdiff1d(np.arange(num_indices), candidates['index'].to_numpy())
        for mi in missing_indices:
            surrogate = _surrogate_marker(ldgm, mi, candidates)
            mapping[mi] = int(surrogate['index'])
    else:
        print(f"No non-missing variants found in block {block_data['block_name']}. Skipping.")

    # Persist to HDF5: one dataset per block named by metadata 'name'
    out_path = worker_params['output_path']

    # Ensure directory exists
    out_path.parent.mkdir(parents=True, exist_ok=True)

    # Serialize writes across processes
    lock = FileLock(str(out_path) + ".lock")
    with lock:
        with h5py.File(out_path, 'a') as h5:
            _ensure_surrogate_hdf5_contract(h5, out_path)
            dset_name = str(block_data['block_name'])
            if dset_name in h5:
                raise ValueError(f"Dataset {dset_name} already exists in {out_path}")
            h5.create_dataset(dset_name, data=mapping, compression='lzf', chunks=True)

get_surrogate_markers

get_surrogate_markers(metadata_path: Union[str, PathLike], nonmissing_variant_ids: DataFrame, *, population: str, run_serial: bool = False, num_processes: Optional[int] = None, output_path: Optional[Union[str, PathLike]] = None, chromosomes: Optional[int] = None) -> Path

Create an HDF5 file with one dataset per LD block containing index-level surrogates.

Returns the path to the HDF5 file.

Source code in src/graphld/surrogates.py
def get_surrogate_markers(
    metadata_path: Union[str, os.PathLike],
    nonmissing_variant_ids: pl.DataFrame,
    *,
    population: str,
    run_serial: bool = False,
    num_processes: Optional[int] = None,
    output_path: Optional[Union[str, os.PathLike]] = None,
    chromosomes: Optional[int] = None,
) -> Path:
    """Create an HDF5 file with one dataset per LD block containing index-level surrogates.

    Returns the path to the HDF5 file.
    """
    run_fn = Surrogates.run_serial if run_serial else Surrogates.run

    # Default output path: alongside metadata file
    if output_path is None:
        output_path = Path(metadata_path).parent / f"surrogates.{population}.h5"

    result_path = run_fn(
        ldgm_metadata_path=metadata_path,
        populations=population,
        chromosomes=chromosomes,
        nonmissing_variant_ids=nonmissing_variant_ids,
        num_processes=num_processes,
        worker_params={'output_path': Path(output_path)},
        output_path=Path(output_path),
    )

    return result_path