use std::collections::HashMap;
use std::fs::File;
use std::io::Read;
use std::ops::BitOr;
use std::path::Path;
use crate::annotations::{Gene, GeneId};
use crate::annotations::{OmimDisease, OmimDiseaseId};
use crate::parser;
use crate::term::internal::{BinaryTermBuilder, HpoTermInternal};
use crate::term::{HpoParents, HpoTerm};
use crate::u32_from_bytes;
use crate::HpoResult;
use crate::{HpoError, HpoTermId};
use core::fmt::Debug;
mod termarena;
use termarena::Arena;
#[cfg_attr(doc, aquamarine::aquamarine)]
/// `Ontology` is the main interface of the `hpo` crate and contains all data
///
/// The [`Ontology`] struct holds all information about the ontology
/// and the ownership of all [`HpoTerm`]s, [`Gene`]s and [`OmimDisease`]s.
///
/// It is recommended to use the public methods [`Ontology::from_standard`]
/// to build the ontology
/// from standard annotation data from Jax. You will need to download
/// the data from [HPO](https://hpo.jax.org/) itself.
///
/// This crate also provides a snapshot of all relevant data in binary format
/// in `tests/ontology.hpo` which can be loaded via [`Ontology::from_binary`].
/// You should check how up-to-date the snapshot is, though.
///
/// ```mermaid
/// erDiagram
/// ONTOLOGY ||--|{ HPOTERM : contains
/// HPOTERM ||--|{ HPOTERM : is_a
/// HPOTERM }|--o{ DISEASE : phenotype_of
/// HPOTERM }|--o{ GENE : phenotype_of
/// HPOTERM {
/// str name
/// HpoTermId id
/// HpoTerms parents
/// HpoTerms children
/// Genes genes
/// OmimDiseases omim_diseases
/// }
/// DISEASE {
/// str name
/// OmimDiseaseId id
/// HpoGroup hpo_terms
/// }
/// GENE {
/// str name
/// GeneId id
/// HpoGroup hpo_terms
/// }
/// ```
#[derive(Default)]
pub struct Ontology {
hpo_terms: Arena,
genes: HashMap<GeneId, Gene>,
omim_diseases: HashMap<OmimDiseaseId, OmimDisease>,
}
impl Debug for Ontology {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Ontology with {} terns", self.hpo_terms.len())
}
}
/// Public API of the Ontology
///
/// Those methods are all safe to use
impl Ontology {
/// Initialize the [`Ontology`] from data provided by [Jax HPO](https://hpo.jax.org/)
///
/// You must download:
///
/// - Actual OBO data: [hp.obo](https://hpo.jax.org/app/data/ontology)
/// - Links between HPO and OMIM diseases: [phenotype.hpoa](https://hpo.jax.org/app/data/annotations)
/// - Links between HPO and Genes: [phenotype_to_genes.txt](http://purl.obolibrary.org/obo/hp/hpoa/phenotype_to_genes.txt)
///
/// and then specify the folder where the data is stored.
///
/// # Errors
///
/// This method can fail for various reasons:
///
/// - obo file not present or available: [`HpoError::CannotOpenFile`]
/// - add_genes failed (TODO)
/// - add_omim_disease failed (TODO)
///
/// # Examples
///
/// ```no_run
/// use hpo::Ontology;
/// use hpo::HpoTermId;
///
/// let ontology = Ontology::from_standard("./example_data/").unwrap();
///
/// assert!(ontology.len() > 15_000);
///
/// let absent_term = HpoTermId::try_from("HP:9999999").unwrap();
/// assert!(ontology.hpo(absent_term).is_none());
///
/// let present_term = HpoTermId::try_from("HP:0000001").unwrap();
/// let root_term = ontology.hpo(present_term).unwrap();
/// assert_eq!(root_term.name(), "Phenotypical abnormality");
/// ```
///
pub fn from_standard(folder: &str) -> HpoResult<Self> {
let mut ont = Ontology::default();
let path = Path::new(folder);
let obo = path.join(crate::OBO_FILENAME);
let gene = path.join(crate::GENE_FILENAME);
let disease = path.join(crate::DISEASE_FILENAME);
parser::load_from_standard_files(&obo, &gene, &disease, &mut ont)?;
ont.calculate_information_content()?;
Ok(ont)
}
/// Build an Ontology from a binary data blob
///
/// The data must be in the proper format, as defined in
/// [`Ontology::as_bytes`]. This method adds all terms, creates the
/// parent-child structure of the ontology, adds genes and Omim diseases
/// and ensures proper inheritance of gene/disease annotations.
/// It also calculates the InformationContent for every term.
///
/// # Errors
///
/// This method can fail for various reasons:
///
/// - Binary file not available: [`HpoError::CannotOpenFile`]
/// - add_genes_from_bytes failed (TODO)
/// - add_omim_disease_from_bytes failed (TODO)
/// - add_terms_from_bytes
/// - add_parent_from_bytes
/// - Size of binary data does not match the content: [`HpoError::ParseBinaryError`]
///
/// # Examples
///
/// ```
/// use hpo::{Ontology, HpoTermId};
///
/// let ontology = Ontology::from_binary("./tests/ontology.hpo").unwrap();
///
/// assert!(ontology.len() > 15_000);
///
/// let absent_term = HpoTermId::try_from("HP:9999999").unwrap();
/// assert!(ontology.hpo(absent_term).is_none());
///
/// let present_term = HpoTermId::try_from("HP:0000001").unwrap();
/// let root_term = ontology.hpo(present_term).unwrap();
/// assert_eq!(root_term.name(), "All");
/// ```
pub fn from_binary<P: AsRef<Path>>(filename: P) -> HpoResult<Self> {
let mut ont = Ontology::default();
let bytes = match File::open(filename) {
Ok(mut file) => {
let len = file
.metadata()
.map_err(|_| {
HpoError::CannotOpenFile(
"unable to get filesize of binary file".to_string(),
)
})?
.len();
let mut bytes = Vec::with_capacity(len.try_into()?);
file.read_to_end(&mut bytes).map_err(|_| {
HpoError::CannotOpenFile("unable to read from binary file".to_string())
})?;
bytes
}
Err(_) => {
return Err(crate::HpoError::CannotOpenFile(
"unable to open binary file".to_string(),
))
}
};
let mut section_start = 0;
let mut section_end: usize;
// Terms
let mut section_len = u32_from_bytes(&bytes[section_start..]) as usize;
section_end = 4 + section_len;
ont.add_terms_from_bytes(&bytes[4..section_end]);
section_start += section_len + 4;
// Term - Parents
section_len = u32_from_bytes(&bytes[section_start..]) as usize;
section_end += 4 + section_len;
ont.add_parent_from_bytes(&bytes[section_start + 4..section_end]);
ont.create_cache();
section_start += section_len + 4;
// Genes
section_len = u32_from_bytes(&bytes[section_start..]) as usize;
section_end += 4 + section_len;
ont.add_genes_from_bytes(&bytes[section_start + 4..section_end])?;
section_start += section_len + 4;
// Omim Diseases
section_len = u32_from_bytes(&bytes[section_start..]) as usize;
section_end += 4 + section_len;
ont.add_omim_disease_from_bytes(&bytes[section_start + 4..section_end])?;
section_start += section_len + 4;
if section_start == bytes.len() {
ont.calculate_information_content()?;
Ok(ont)
} else {
Err(HpoError::ParseBinaryError)
}
}
/// Returns a binary representation of the Ontology
///
/// The binary data is separated into sections:
///
/// - Terms (Names + IDs) (see `HpoTermInternal::as_bytes`)
/// - Term - Parent connection (Child ID - Parent ID)
/// (see `HpoTermInternal::parents_as_byte`)
/// - Genes (Names + IDs + Connected HPO Terms) ([`Gene::as_bytes`])
/// - OMIM Diseases (Names + IDs + Connected HPO Terms)
/// ([`OmimDisease::as_bytes`])
///
/// Every section starts with 4 bytes to indicate its size
/// (big-endian encoded `u32`)
///
/// This method is only useful if you use are modifying the ontology
/// and want to save data for later re-use.
///
/// # Panics
///
/// Panics when the buffer length of any subsegment larger than `u32::MAX`
pub fn as_bytes(&self) -> Vec<u8> {
fn usize_to_u32(n: usize) -> u32 {
n.try_into().expect("unable to convert {n} to u32")
}
let mut res = Vec::new();
// All HPO Terms
let mut buffer = Vec::new();
for term in self.hpo_terms.values() {
buffer.append(&mut term.as_bytes());
}
res.append(&mut usize_to_u32(buffer.len()).to_be_bytes().to_vec());
res.append(&mut buffer);
// All Term - Parent connections
buffer.clear();
for term in self.hpo_terms.values() {
buffer.append(&mut term.parents_as_byte());
}
res.append(&mut usize_to_u32(buffer.len()).to_be_bytes().to_vec());
res.append(&mut buffer);
// Genes and Gene-Term connections
buffer.clear();
for gene in self.genes.values() {
buffer.append(&mut gene.as_bytes());
}
res.append(&mut usize_to_u32(buffer.len()).to_be_bytes().to_vec());
res.append(&mut buffer);
// OMIM Disease and Disease-Term connections
buffer.clear();
for omim_disease in self.omim_diseases.values() {
buffer.append(&mut omim_disease.as_bytes());
}
res.append(&mut usize_to_u32(buffer.len()).to_be_bytes().to_vec());
res.append(&mut buffer);
res
}
/// Returns the number of HPO-Terms in the Ontology
pub fn len(&self) -> usize {
self.hpo_terms.len()
}
/// Returns `true` if the Ontology does not contain any HPO-Terms
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the [`HpoTerm`] of the provided [`HpoTermId`]
///
/// If no such term is present in the Ontolgy, `None` is returned
pub fn hpo(&self, term_id: HpoTermId) -> Option<HpoTerm> {
HpoTerm::try_new(self, term_id).ok()
}
/// Returns an Iterator of all [`HpoTerm`]s from the Ontology
pub fn hpos(&self) -> OntologyIterator {
OntologyIterator {
inner: self.hpo_terms.values().iter(),
ontology: self,
}
}
/// Returns a reference to the [`Gene`] of the provided [`GeneId`]
///
/// If no such gene is present, `None` is returned
pub fn gene(&self, gene_id: &GeneId) -> Option<&Gene> {
self.genes.get(gene_id)
}
/// Returns a mutable reference to the [`Gene`] of the provided [`GeneId`]
///
/// If no such gene is present, `None` is returned
pub fn gene_mut(&mut self, gene_id: &GeneId) -> Option<&mut Gene> {
self.genes.get_mut(gene_id)
}
/// Returns a reference to the [`Gene`] with the provided symbol / name
///
/// If no such gene is present, `None` is returned
///
/// # Note
///
/// `Gene`s are not index by name, so this method searches through all
/// genes. If you can, prefer using the [`GeneId`] and [`Ontology.gene`].
pub fn gene_by_name(&self, symbol: &str) -> Option<&Gene> {
self.genes.values().find(|&gene| gene.name() == symbol)
}
/// Returns an Iterator of all [`Gene`]s from the Ontology
///
/// It is likely that the return type will change to a dedicated Iterator
pub fn genes(&self) -> std::collections::hash_map::Values<'_, GeneId, Gene> {
self.genes.values()
}
/// Returns a reference to the [`OmimDisease`] of the provided [`OmimDiseaseId`]
///
/// If no such disease is present, `None` is returned
pub fn omim_disease(&self, omim_disease_id: &OmimDiseaseId) -> Option<&OmimDisease> {
self.omim_diseases.get(omim_disease_id)
}
/// Returns a mutable reference to the [`OmimDisease`] of the provided [`OmimDiseaseId`]
///
/// If no such disease is present, `None` is returned
pub fn omim_disease_mut(
&mut self,
omim_disease_id: &OmimDiseaseId,
) -> Option<&mut OmimDisease> {
self.omim_diseases.get_mut(omim_disease_id)
}
/// Returns an Iterator of all [`OmimDisease`]s from the Ontology
///
/// It is likely that the return type will change to a dedicated Iterator
pub fn omim_diseases(
&self,
) -> std::collections::hash_map::Values<'_, OmimDiseaseId, OmimDisease> {
self.omim_diseases.values()
}
}
/// Methods to add annotations
///
/// These methods should rarely (if ever) be used by clients.
/// Calling these functions might disrupt the Ontology and associated terms.
impl Ontology {
/// Add a gene to the Ontology. and return the [`GeneId`]
///
/// If the gene does not yet exist, a new [`Gene`] entity is created
/// and stored in the Ontology.
/// If the gene already exists in the ontology, it is not added again.
///
/// # Note
///
/// Adding a gene does not connect it to any HPO terms.
/// Use [`Ontology::link_gene_term`] for creating connections.
///
/// # Errors
///
/// If the `gene_id` is invalid, an [`HpoError::ParseIntError`] is returned
pub fn add_gene(&mut self, gene_name: &str, gene_id: &str) -> HpoResult<GeneId> {
let id = GeneId::try_from(gene_id)?;
match self.genes.entry(id) {
std::collections::hash_map::Entry::Occupied(_) => Ok(id),
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(Gene::new(id, gene_name));
Ok(id)
}
}
}
/// Add a OMIM disease to the Ontology. and return the [`OmimDiseaseId`]
///
/// If the disease does not yet exist, a new [`OmimDisease`] entity is
/// created and stored in the Ontology.
/// If the disease already exists in the ontology, it is not added again.
///
/// # Note
///
/// Adding a disease does not connect it to any HPO terms.
/// Use [`Ontology::link_omim_disease_term`] for creating connections.
///
/// # Errors
///
/// If the `omim_disease_id` is invalid, an [`HpoError::ParseIntError`] is returned
pub fn add_omim_disease(
&mut self,
omim_disease_name: &str,
omim_disease_id: &str,
) -> HpoResult<OmimDiseaseId> {
let id = OmimDiseaseId::try_from(omim_disease_id)?;
match self.omim_diseases.entry(id) {
std::collections::hash_map::Entry::Occupied(_) => Ok(id),
std::collections::hash_map::Entry::Vacant(entry) => {
entry.insert(OmimDisease::new(id, omim_disease_name));
Ok(id)
}
}
}
/// Add the [`Gene`] as annotation to the [`HpoTerm`]
///
/// The gene will be recursively connected to all parent `HpoTerms` as well.
///
/// This method does not add the HPO-term to the [`Gene`], this must be handled
/// by the client.
///
/// # Errors
///
/// If the HPO term is not present, an [`HpoError::DoesNotExist`] is returned
pub fn link_gene_term(&mut self, term_id: HpoTermId, gene_id: GeneId) -> HpoResult<()> {
let term = self.get_mut(term_id).ok_or(HpoError::DoesNotExist)?;
if term.add_gene(gene_id) {
// If the gene is already associated to the term, this branch will
// be skipped. That is desired, because by definition
// all parent terms are already linked as well
let parents = term.all_parents().clone();
for parent in &parents {
self.link_gene_term(parent, gene_id)?;
}
}
Ok(())
}
/// Add the [`OmimDisease`] as annotation to the [`HpoTerm`]
///
/// The disease will be recursively connected to all parent `HpoTerms` as well.
///
/// This method does not add the HPO-term to the [`OmimDisease`], this
/// must be handled by the client.
///
/// # Errors
///
/// If the HPO term is not present, an [`HpoError`] is returned
pub fn link_omim_disease_term(
&mut self,
term_id: HpoTermId,
omim_disease_id: OmimDiseaseId,
) -> HpoResult<()> {
let term = self.get_mut(term_id).ok_or(HpoError::DoesNotExist)?;
if term.add_omim_disease(omim_disease_id) {
// If the disease is already associated to the term, this branch will
// be skipped. That is desired, because by definition
// all parent terms are already linked as well
let parents = term.all_parents().clone();
for parent in &parents {
self.link_omim_disease_term(parent, omim_disease_id)?;
}
}
Ok(())
}
/// Calculates the [`crate::term::InformationContent`]s for every term
///
/// This method should only be called **after** all terms are added,
/// connected and all genes and diseases are linked as well.
///
/// It can be called repeatedly, all values are recalculated each time,
/// as long as the Ontology contains at least 1 gene/disease.
/// When no genes/diseases are present, the IC is not calculated nor updated.
///
/// # Errors
///
/// This method returns an error if there are more Genes or Terms than `u16::MAX`
/// because larger numbers can't be safely converted to `f32`
pub fn calculate_information_content(&mut self) -> HpoResult<()> {
self.calculate_gene_ic()?;
self.calculate_omim_disease_ic()?;
Ok(())
}
/// Calculates the gene-specific Information Content for every term
///
/// If no genes are present in the Ontology, no IC are calculated
fn calculate_gene_ic(&mut self) -> HpoResult<()> {
let n_genes = self.genes.len();
for term in self.hpo_terms.values_mut() {
let current_genes = term.genes().len();
term.information_content_mut()
.set_gene(n_genes, current_genes)?;
}
Ok(())
}
/// Calculates the Omim-Disease-specific Information Content for every term
///
/// If no diseases are present in the Ontology, no IC are calculated
fn calculate_omim_disease_ic(&mut self) -> HpoResult<()> {
let n_omim_diseases = self.omim_diseases.len();
for term in self.hpo_terms.values_mut() {
let current_diseases = term.omim_diseases().len();
term.information_content_mut()
.set_omim_disease(n_omim_diseases, current_diseases)?;
}
Ok(())
}
}
/// Crate-only functions for setting up and building the Ontology
///
/// Those methods should not be exposed publicly
impl Ontology {
/// Adds an HpoTerm to the ontology
///
/// This method is part of the Ontology-building, based on the binary
/// data format and requires a specified data layout.
///
/// The method assumes that the data is in the right format and also
/// assumes that the caller takes care of handling all consistencies
/// like parent-child connection etc.
///
/// See [`HpoTermInternal::as_bytes`] for explanation of the binary layout.
fn add_terms_from_bytes(&mut self, bytes: &[u8]) {
for term in BinaryTermBuilder::new(bytes) {
self.add_term(term);
}
}
/// Connects an HpoTerm to its parent term
///
/// This method is part of the Ontology-building, based on the binary
/// data format and requires a specified data layout.
///
/// The method assumes that the data is in the right format and also
/// assumes that the caller will populate the all_parents caches for
/// each term.
///
/// See [`HpoTermInternal::parents_as_byte`] for explanation of the binary layout.
///
/// # Panics
///
/// This method will panic if the length of bytes does not exactly correspond
/// to the contained data
fn add_parent_from_bytes(&mut self, bytes: &[u8]) {
let mut idx: usize = 0;
loop {
if idx == bytes.len() {
break;
}
let n_parents = u32_from_bytes(&bytes[idx..]) as usize;
idx += 4;
let term =
HpoTermId::from([bytes[idx], bytes[idx + 1], bytes[idx + 2], bytes[idx + 3]]);
idx += 4;
for _ in 0..n_parents {
let parent =
HpoTermId::from([bytes[idx], bytes[idx + 1], bytes[idx + 2], bytes[idx + 3]]);
self.add_parent(parent, term);
idx += 4;
}
}
}
/// Adds genes to the ontoloigy and connects them to connected terms
///
/// This method is part of the Ontology-building, based on the binary
/// data format and requires a specified data layout.
///
/// It connects all connected terms and their parents properly. The
/// method assumes that the bytes encode all gene-term connections.
///
/// See [`Gene::as_bytes`] for explanation of the binary layout
fn add_genes_from_bytes(&mut self, bytes: &[u8]) -> HpoResult<()> {
let mut idx: usize = 0;
loop {
if idx >= bytes.len() {
break;
}
let gene_len = u32_from_bytes(&bytes[idx..]) as usize;
let gene = Gene::try_from(&bytes[idx..idx + gene_len])?;
for term in gene.hpo_terms() {
self.link_gene_term(term, *gene.id())?;
}
self.genes.insert(*gene.id(), gene);
idx += gene_len;
}
Ok(())
}
/// Adds OmimDiseases to the ontoloigy and connects them to connected terms
///
/// This method is part of the Ontology-building, based on the binary
/// data format and requires a specified data layout.
///
/// It connects all connected terms and their parents properly. The
/// method assumes that the bytes encode all Disease-term connections.
///
/// See [`OmimDisease::as_bytes`] for explanation of the binary layout
fn add_omim_disease_from_bytes(&mut self, bytes: &[u8]) -> HpoResult<()> {
let mut idx: usize = 0;
loop {
if idx >= bytes.len() {
break;
}
let disease_len = u32_from_bytes(&bytes[idx..]) as usize;
let disease = OmimDisease::try_from(&bytes[idx..idx + disease_len])?;
for term in disease.hpo_terms() {
self.link_omim_disease_term(term, *disease.id())?;
}
self.omim_diseases.insert(*disease.id(), disease);
idx += disease_len;
}
Ok(())
}
/// This method is part of the cache creation to link all terms to their
/// direct and indirect parents (grandparents)
///
/// # Panics
///
/// This method will panic if the `term_id` is not present in the Ontology
fn all_grandparents(&mut self, term_id: HpoTermId) -> &HpoParents {
if !self.get_unchecked(term_id).parents_cached() {
self.create_cache_of_grandparents(term_id);
}
let term = self.get_unchecked(term_id);
term.all_parents()
}
/// This method is part of the cache creation to link all terms to their
/// direct and indirect parents (grandparents)
///
/// It will (somewhat) recursively iterate all parents and copy all their parents.
/// During this recursion, the list of all_parents is cached in each term that was
/// iterated.
///
/// The logic is that the recursion bubbles up all the way to the top of the ontolgy
/// and then caches the list of direct and indirect parents for every term bubbling
/// back down. The recursion does not reach the top level again, because it will stop
/// once it reaches a term with already cached `all_parents`.
///
/// # Panics
///
/// This method will panic if the `term_id` is not present in the Ontology
fn create_cache_of_grandparents(&mut self, term_id: HpoTermId) {
let mut res = HpoParents::default();
let parents = self.get_unchecked(term_id).parents().clone();
for parent in &parents {
let grandparents = self.all_grandparents(parent);
for gp in grandparents {
res.insert(gp);
}
}
let term = self.get_unchecked_mut(term_id);
*term.all_parents_mut() = res.bitor(&parents);
}
/// Crates and caches the `all_parents` values for every term
///
/// This method can only be called once and afterwards no new terms
/// should be added to the Ontology anymore and no new term-parent connection
/// should be created.
/// Since this method caches the results, rerunning it will not cause a new
/// calculation.
pub(crate) fn create_cache(&mut self) {
let term_ids: Vec<HpoTermId> = self.hpo_terms.keys();
for id in term_ids {
self.create_cache_of_grandparents(id);
}
}
/// Insert an HpoTerm (`HpoTermInternal`) to the ontology
///
/// This method does not link the term to its parents or to any annotations
pub(crate) fn add_term(&mut self, term: HpoTermInternal) -> HpoTermId {
let id = *term.id();
self.hpo_terms.insert(term);
id
}
/// Add a connection from an HpoTerm to its parent
///
/// This method is called once for every dependency in the Ontology during the initialization.
///
/// There should rarely be a need to call this method outside of the ontology building
///
/// # Panics
///
/// This method will panic if the `parent_id` or `child_id` is not present in the Ontology
pub(crate) fn add_parent(&mut self, parent_id: HpoTermId, child_id: HpoTermId) {
let parent = self.get_unchecked_mut(parent_id);
parent.add_child(child_id);
let child = self.get_unchecked_mut(child_id);
child.add_parent(parent_id);
}
/// Returns the `HpoTermInternal` with the given `HpoTermId`
///
/// Returns `None` if no such term is present
pub(crate) fn get(&self, term_id: HpoTermId) -> Option<&HpoTermInternal> {
self.hpo_terms.get(term_id)
}
/// Returns the `HpoTermInternal` with the given `HpoTermId`
///
/// This method should only be called if the caller is sure that the term actually
/// exists, e.g. during an iteration of all `HpoTermId`s.
///
/// # Panics
///
/// This method will panic if the `term_id` is not present in the Ontology
pub(crate) fn get_unchecked(&self, term_id: HpoTermId) -> &HpoTermInternal {
self.hpo_terms.get_unchecked(term_id)
}
/// Returns a mutable reference to the `HpoTermInternal` with the given `HpoTermId`
///
/// Returns `None` if no such term is present
fn get_mut(&mut self, term_id: HpoTermId) -> Option<&mut HpoTermInternal> {
self.hpo_terms.get_mut(term_id)
}
/// Returns a mutable reference to the `HpoTermInternal` with the given `HpoTermId`
///
/// This method should only be called if the caller is sure that the term actually
/// exists, e.g. during an iteration of all `HpoTermId`s.
///
/// # Panics
///
/// This method will panic if the `term_id` is not present in the Ontology
fn get_unchecked_mut(&mut self, term_id: HpoTermId) -> &mut HpoTermInternal {
self.hpo_terms.get_unchecked_mut(term_id)
}
}
/// An iterator of [`HpoTerm`]s
pub struct OntologyIterator<'a> {
inner: std::slice::Iter<'a, HpoTermInternal>,
ontology: &'a Ontology,
}
impl<'a> std::iter::Iterator for OntologyIterator<'a> {
type Item = HpoTerm<'a>;
fn next(&mut self) -> Option<Self::Item> {
match self.inner.next() {
Some(term) => Some(HpoTerm::new(self.ontology, term)),
None => None,
}
}
}
impl<'a> IntoIterator for &'a Ontology {
type Item = HpoTerm<'a>;
type IntoIter = OntologyIterator<'a>;
fn into_iter(self) -> Self::IntoIter {
self.hpos()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn add_terms() {
let test_terms = [
("t1", 1u32),
("Term with a very long name", 2u32),
("", 3u32),
("Abnormality", 4u32),
];
let mut ont = Ontology::default();
let mut v: Vec<u8> = Vec::new();
for (name, id) in test_terms {
let t = HpoTermInternal::new(String::from(name), id.into());
v.append(&mut t.as_bytes());
}
ont.add_terms_from_bytes(&v);
assert_eq!(ont.len(), 4);
}
#[test]
fn add_parents() {
let test_terms = [
("t1", 1u32),
("Term with a very long name", 2u32),
("", 3u32),
("Abnormality", 4u32),
];
let mut ont = Ontology::default();
let mut v: Vec<u8> = Vec::new();
for (name, id) in test_terms {
let t = HpoTermInternal::new(String::from(name), id.into());
v.append(&mut t.as_bytes());
}
ont.add_terms_from_bytes(&v);
assert_eq!(ont.len(), 4);
// The fake term has the same HpoTermId as one of of the Test ontology
let mut fake_term = HpoTermInternal::new(String::from(""), 3u32.into());
fake_term.add_parent(1u32.into());
fake_term.add_parent(2u32.into());
let bytes = fake_term.parents_as_byte();
ont.add_parent_from_bytes(&bytes[..]);
assert_eq!(ont.get_unchecked(3u32.into()).parents().len(), 2);
assert_eq!(ont.get_unchecked(1u32.into()).children().len(), 1);
assert_eq!(ont.get_unchecked(2u32.into()).children().len(), 1);
}
}