diff --git a/atomrdf/graph.py b/atomrdf/graph.py index 2c75385..8d21fca 100644 --- a/atomrdf/graph.py +++ b/atomrdf/graph.py @@ -6,8 +6,18 @@ NOTES ----- - To ensure domain and range checking works as expected, always add type before adding further properties! + +Classes +------- +- KnowledgeGraph: Represents a knowledge graph that stores and annotates structure objects. + +Attributes +---------- +- defstyledict: A dictionary containing default styles for visualizing the graph. + """ + from rdflib import Graph, Literal, XSD, RDF, RDFS, BNode, URIRef import os @@ -95,8 +105,51 @@ def _prepare_log(file): logger.propagate = False return logger - class KnowledgeGraph: + """ + Represents a knowledge graph. + + Parameters + ---------- + graph_file : str, optional + The path to the graph file to be parsed. Default is None. + store : str, optional + The type of store to use. Default is "Memory". + store_file : str, optional + The path to the store file. Default is None. + identifier : str, optional + The identifier for the graph. Default is "http://default_graph". + ontology : Ontology, optional + The ontology object to be used. Default is None. + structure_store : StructureStore, optional + The structure store object to be used. Default is None. + enable_log : bool, optional + Whether to enable logging. Default is False. + If true, a log file named atomrdf.log will be created in the current working directory. + + Attributes + ---------- + graph : rdflib.Graph + The RDF graph. + sgraph : rdflib.Graph + The structure graph for a single chosen sample + ontology : Ontology + The ontology object. + terms : dict + The dictionary of ontology terms. + store : str + The type of store used. + + Methods + ------- + add_structure(structure) + Add a structure to the knowledge graph. + add(triple, validate=True) + Add a triple to the knowledge graph. + triples(triple) + Return the triples in the knowledge graph that match the given triple pattern. + """ + def __init__( self, graph_file=None, @@ -107,6 +160,26 @@ def __init__( structure_store=None, enable_log=False, ): + """ + Initialize the KnowledgeGraph object. + + Parameters + ---------- + graph_file : str, optional + The path to the graph file to be parsed. Default is None. + store : str, optional + The type of store to use. Default is "Memory". + store_file : str, optional + The path to the store file. Default is None. + identifier : str, optional + The identifier for the graph. Default is "http://default_graph". + ontology : Ontology, optional + The ontology object to be used. Default is None. + structure_store : StructureStore, optional + The structure store object to be used. Default is None. + enable_log : bool, optional + Whether to enable logging. Default is False. + """ create_store( self, @@ -131,20 +204,33 @@ def __init__( if os.path.exists(graph_file): self.graph.parse(graph_file) - self.sample = None - self.material = None - self.sysdict = None self.sgraph = None if ontology is None: ontology = read_ontology() self.ontology = ontology self.terms = self.ontology.terms - self._atom_ids = None self.store = store self._n_triples = 0 self._initialize_graph() def add_structure(self, structure): + """ + Add a structure to the knowledge graph. + + Parameters + ---------- + structure : Structure + The structure object to be added. + + Returns + ------- + None + + Notes + ----- + This method adds a structure object to the knowledge graph. The structure object should be an instance of the Structure class. + The structure object is assigned to the graph and converted to RDF format. + """ structure.graph = self structure.to_graph() @@ -310,8 +396,29 @@ def _check_range(self, triple): def add(self, triple, validate=True): """ - Force assumes that you are passing rdflib terms, defined with - RDFLib Namespace + Add a triple to the knowledge graph. + + Parameters + ---------- + triple : tuple + The triple to be added in the form (subject, predicate, object). + validate : bool, optional + Whether to validate the triple against the domain and range. Default is True. + + Returns + ------- + None + + Notes + ----- + This method adds a triple to the knowledge graph. The triple should be provided as a tuple in the form (subject, predicate, object). + By default, the triple is validated against the domain and range. If the `validate` parameter is set to False, the validation is skipped. + + Examples + -------- + >>> graph = Graph() + >>> graph.add(("Alice", "likes", "Bob")) + >>> graph.add(("Bob", "age", 25), validate=False) """ modified_triple = self._modify_triple(triple) @@ -334,18 +441,108 @@ def add(self, triple, validate=True): self.log("added") def triples(self, triple): + """ + Return the triples in the knowledge graph that match the given triple pattern. + + Parameters + ---------- + triple : tuple + The triple pattern to match in the form (subject, predicate, object). + + Returns + ------- + generator + A generator that yields the matching triples. + + Examples + -------- + >>> graph = KnowledgeGraph() + >>> graph.add(("Alice", "likes", "Bob")) + >>> graph.add(("Alice", "dislikes", "Charlie")) + >>> graph.add(("Bob", "likes", "Alice")) + >>> for triple in graph.triples(("Alice", None, None)): + ... print(triple) + ('Alice', 'likes', 'Bob') + ('Alice', 'dislikes', 'Charlie') + """ modified_triple = self._modify_triple(triple) return self.graph.triples(modified_triple) def value(self, arg1, arg2): + """ + Get the value of a triple in the knowledge graph. + + Parameters + ---------- + arg1 : object + The subject of the triple. + arg2 : object + The predicate of the triple. + + Returns + ------- + object or None + The value of the triple if it exists, otherwise None. + + Notes + ----- + This method retrieves the value of a triple in the knowledge graph. The triple is specified by providing the subject and predicate as arguments. + If the triple exists in the graph, the corresponding value is returned. If the triple does not exist, None is returned. + + Examples + -------- + >>> graph = KnowledgeGraph() + >>> graph.add(("Alice", "likes", "Bob")) + >>> value = graph.value("Alice", "likes") + >>> print(value) + Bob + """ modified_double = self._modify_triple((arg1, arg2)) return self.graph.value(modified_double[0], modified_double[1]) def remove(self, triple): + """ + Remove a triple from the knowledge graph. + + Parameters + ---------- + triple : tuple + The triple to be removed in the form (subject, predicate, object). + + Returns + ------- + None + + Notes + ----- + This method removes a triple from the knowledge graph. The triple should be provided as a tuple in the form (subject, predicate, object). + + Examples + -------- + >>> graph = KnowledgeGraph() + >>> graph.add(("Alice", "likes", "Bob")) + >>> graph.remove(("Alice", "likes", "Bob")) + """ modified_triple = self._modify_triple(triple) return self.graph.remove(modified_triple) def create_node(self, namestring, classtype): + """ + Create a new node in the graph. + + Parameters + ---------- + namestring : str + The name of the node. + classtype : Object from a given ontology + The class type of the node. + + Returns + ------- + URIRef + The newly created node. + + """ item = URIRef(namestring) self.add((item, RDF.type, classtype)) return item @@ -385,6 +582,35 @@ def _initialize_graph(self): ) def add_calculated_quantity(self, sample, propertyname, value, unit=None): + """ + Add a calculated quantity to a sample. + + Parameters + ---------- + sample : URIRef + The URIRef of the sample to which the calculated quantity is being added. + propertyname : str + The name of the calculated property. + value : str + The value of the calculated property. + unit : str, optional + The unit of the calculated property. Default is None. + The unit should be from QUDT. See http://qudt.org/vocab/unit/ + + Returns + ------- + None + + Notes + ----- + This method adds a calculated quantity to a sample in the knowledge graph. The calculated quantity is represented as a triple with the sample as the subject, the calculated property as the predicate, and the value as the object. The calculated property is created as a node in the graph with the given name and value. If a unit is provided, it is also added as a property of the calculated property node. + + Examples + -------- + >>> graph = KnowledgeGraph() + >>> sample = graph.create_node("Sample1", CMSO.Sample) + >>> graph.add_calculated_quantity(sample, "energy", "10.5", "eV") + """ prop = self.create_node(propertyname, CMSO.CalculatedProperty) self.add((sample, CMSO.hasCalculatedProperty, prop)) self.add((prop, RDFS.label, Literal(propertyname))) @@ -393,6 +619,19 @@ def add_calculated_quantity(self, sample, propertyname, value, unit=None): self.add((prop, CMSO.hasUnit, URIRef(f"http://qudt.org/vocab/unit/{unit}"))) def inspect_sample(self, sample): + """ + Inspects a sample and retrieves information about its atoms, material, defects, composition, + crystal structure, space group, calculated properties, and units. + + Parameters + ---------- + sample: The sample to inspect. + + Returns + ------- + string: A string containing the information about the sample. + + """ natoms = self.value(sample, CMSO.hasNumberOfAtoms).toPython() material = list([k[2] for k in self.triples((sample, CMSO.hasMaterial, None))])[ 0 @@ -437,45 +676,18 @@ def inspect_sample(self, sample): def visualize(self, *args, **kwargs): """ - Vosualise the RDF tree of the Graph + Visualizes the graph using the specified arguments. + + This method is a wrapper around the `visualise` method and passes the same arguments to it. Parameters ---------- - backend: string, {'ipycytoscape', 'graphviz'} - Chooses the backend with which the graph will be plotted. ipycytoscape provides an interactive, - but slow visualisation, whereas graphviz provides a non-interactive fast visualisation. - - edge_color: string - Edge color of the boxes - - styledict: dict - If provided, allows customisation of color and other properties. - - graph_attr: dict - further attributes that allow customisation of graphs - - layoutname: string - name of the layout for graph + *args: Variable length argument list. + **kwargs: Arbitrary keyword arguments. Returns ------- - - Notes - ----- - styledict has the following options. Refer to graphviz and ipycytoscape - documentation for more details - BNode: - color: - shape: - style: - URIRef: - color: - shape: - style: - Literal: - color: - shape: - style: + dot: The visualization of the RDF tree. """ self.visualise(*args, **kwargs) @@ -489,41 +701,56 @@ def visualise( layout="neato", ): """ - Vosualise the RDF tree of the Graph + Visualize the RDF tree of the Graph. Parameters ---------- - edge_color: string - Edge color of the boxes - - styledict: dict - If provided, allows customisation of color and other properties. - - graph_attr: dict - further attributes that allow customisation of graphs - - layoutname: string - name of the layout for graph + styledict : dict, optional + If provided, allows customization of color and other properties. + rankdir : str, optional + The direction of the graph layout. Default is "BT" (bottom to top). + hide_types : bool, optional + Whether to hide the types in the visualization. Default is False. + workflow_view : bool, optional + Whether to enable the workflow view. Default is False. + size : tuple, optional + The size of the visualization. Default is None. + layout : str, optional + The name of the layout algorithm for the graph. Default is "neato". Returns ------- + graphviz.dot.Digraph + The visualization of the RDF tree. Notes ----- - styledict has the following options. Refer to graphviz and ipycytoscape - documentation for more details + The `styledict` parameter allows customization of the visualization style. + It has the following options: + BNode: - color: - shape: - style: + color : str + The color of the BNode boxes. + shape : str + The shape of the BNode boxes. + style : str + The style of the BNode boxes. + URIRef: - color: - shape: - style: + color : str + The color of the URIRef boxes. + shape : str + The shape of the URIRef boxes. + style : str + The style of the URIRef boxes. + Literal: - color: - shape: - style: + color : str + The color of the Literal boxes. + shape : str + The shape of the Literal boxes. + style : str + The style of the Literal boxes. """ if size is not None: size = f"{size[0]},{size[1]}" @@ -564,7 +791,39 @@ def write(self, filename, format="json-ld"): def archive(self, package_name, format="turtle", compress=True): """ - Publish a dataset from graph including per atom quantities + Publish a dataset from graph including per atom quantities. + + Parameters: + ----------- + package_name : str + The name of the package to be created. + format : str, optional + The format in which the dataset should be written. Default is "turtle". + compress : bool, optional + Whether to compress the package into a tarball. Default is True. + + Raises: + ------- + ValueError + If the package_name already exists or if the tarball already exists. + + Notes: + ------ + This method creates a package containing a dataset from the graph, including per atom quantities. + The package consists of a folder named package_name, which contains the dataset and related files. + If compress is True, the package is compressed into a tarball. + + The method performs the following steps: + 1. Checks if the package_name already exists. If it does, raises a ValueError. + 2. If compress is True, checks if the tarball already exists. If it does, raises a ValueError. + 3. Creates a folder named package_name. + 4. Creates a subfolder named rdf_structure_store within the package folder. + 5. Copies the files associated with each sample to the rdf_structure_store folder, while fixing the paths. + 6. Updates the paths in the graph to point to the copied files. + 7. Writes the dataset to a file named "triples" within the package folder. + 8. If compress is True, compresses the package folder into a tarball. + 9. Removes the package folder. + """ # first step make a folder if os.path.exists(package_name): @@ -614,6 +873,37 @@ def unarchive( identifier="http://default_graph", ontology=None, ): + """ + Unarchives a package and returns an instance of the Graph class. + + Parameters + ---------- + package_name : str + The name of the package to unarchive. + compress : bool, optional + Whether to compress the package. Defaults to True. + store : str, optional + The type of store to use. Defaults to "Memory". + store_file : str, optional + The file to use for the store. Defaults to None. + identifier : str, optional + The identifier for the graph. Defaults to "http://default_graph". + ontology : str, optional + The ontology to use. Defaults to None. + + Returns + ------- + Graph + An instance of the Graph class. + + Raises + ------ + FileNotFoundError + If the package file is not found. + tarfile.TarError + If there is an error while extracting the package. + + """ if compress: package_base_name = ".".join(package_name.split(".")[:-2]) with tarfile.open(package_name) as fin: @@ -661,7 +951,28 @@ def auto_query( return_query=False, enforce_types=None, ): + """ + Automatically generates and executes a query based on the provided parameters. + Parameters + ---------- + source : OntoTerm + The source of the query. + destination : OntoTerm + The destination of the query. + condition :str, optional + The condition to be applied in the query. Defaults to None. + return_query : bool, optional + If True, returns the generated query instead of executing it. Defaults to False. + enforce_types : bool, optional + If provided, enforces the specified type for the query. Defaults to None. + + Returns + ------- + pandas DataFrame or str + The result of the query execution. If `return_query` is True, returns the generated query as a string. + Otherwise, returns the result of the query execution as a pandas DataFrame. + """ if enforce_types is None: for val in [True, False]: query = self.ontology.create_query( @@ -688,6 +999,26 @@ def auto_query( def query_sample( self, destination, condition=None, return_query=False, enforce_types=None ): + """ + Query the knowledge graph for atomic scale samples. + + Parameters + ---------- + destination : OntoTerm + The destination of the query. + condition : str, optional + The condition to be applied in the query. Defaults to None. + return_query : bool, optional + If True, returns the generated query instead of executing it. Defaults to False. + enforce_types : bool, optional + If provided, enforces the specified type for the query. Defaults to None. + + Returns + ------- + pandas DataFrame or str + The result of the query execution. If `return_query` is True, returns the generated query as a string. Otherwise, returns the result of the query execution as a pandas DataFrame. + + """ return self.auto_query( self.ontology.terms.cmso.AtomicScaleSample, destination, @@ -713,6 +1044,21 @@ def samples(self): return [x[0] for x in self.triples((None, RDF.type, CMSO.AtomicScaleSample))] def iterate_graph(self, item, create_new_graph=False): + """ + Iterate through the graph starting from the given item. + + Parameters + ---------- + item : object + The item to start the iteration from. + create_new_graph : bool, optional + If True, create a new KnowledgeGraph object to store the iteration results. + Default is False. The results are stored in `self.sgraph`. + + Returns + ------- + None + """ if create_new_graph: self.sgraph = KnowledgeGraph() triples = list(self.triples((item, None, None))) @@ -722,7 +1068,7 @@ def iterate_graph(self, item, create_new_graph=False): def get_sample(self, sample, no_atoms=False): """ - Get the Sample as an RDFGraph + Get the Sample as a KnowledgeGraph Parameters ---------- @@ -736,6 +1082,8 @@ def get_sample(self, sample, no_atoms=False): ------- sgraph: :py:class:`RDFGraph` the RDFGraph of the queried sample + + na: int, only retured if no_atoms is True """ @@ -747,7 +1095,7 @@ def get_sample(self, sample, no_atoms=False): def get_system_from_sample(self, sample): """ - Get a pyscal :py:class:`pyscal.core.System` from the selected sample + Get a pyscal :py:class:`atomrdf.structure.System` from the selected sample Parameters ---------- @@ -756,7 +1104,7 @@ def get_system_from_sample(self, sample): Returns ------- - system: :py:class:`pyscal.core.System` + system: :py:class:`atomrdf.structure.System` corresponding system """ @@ -804,6 +1152,7 @@ def to_file(self, sample, filename=None, format="poscar"): name of output file format: string, {"lammps-dump","lammps-data", "poscar"} + or any format supported by ase Returns ------- diff --git a/atomrdf/namespace.py b/atomrdf/namespace.py index dc32c32..09c9b04 100644 --- a/atomrdf/namespace.py +++ b/atomrdf/namespace.py @@ -1,3 +1,14 @@ +""" +This module provides the Namespace class for managing namespaces in the AtomRDF library. + +The Namespace class extends the rdflib.Namespace class and provides additional functionality for working with namespaces. + +Classes +------- +Namespace + A class representing a namespace in the AtomRDF library. +""" + import os from rdflib import Literal, URIRef from rdflib import Namespace as RDFLibNamespace @@ -7,13 +18,39 @@ class Namespace(AttrSetter, RDFLibNamespace): + """A class representing a namespace in the AtomRDF library. + + This class extends the `rdflib.Namespace` classes. + + Parameters + ---------- + infile : str + The input file path. + delimiter : str, optional + The delimiter used in the input file. Defaults to "/". + + Attributes + ---------- + network : OntologyNetwork + The ontology network associated with the namespace. + name : str + The name of the namespace. + """ + def __init__(self, infile, delimiter="/"): + """ + Initialize the Namespace class. + + Parameters + ---------- + infile : str + The input file path. + delimiter : str, optional + The delimiter used in the input file. Defaults to "/". + """ AttrSetter.__init__(self) self.network = OntologyNetwork(infile, delimiter=delimiter) - # print(type(self.network.onto.tree.base_iri)) - # self.namespace = RDFLibNamespace(self.network.onto.tree.base_iri) RDFLibNamespace.__init__(self.network.onto.tree.base_iri) - # self.namespace = RDFLibNamespace("http://purls.helmholtz-metadaten.de/cmso/") self.name = self.network.onto.tree.name mapdict = {} diff --git a/atomrdf/network/network.py b/atomrdf/network/network.py index 0aae253..88aa94b 100644 --- a/atomrdf/network/network.py +++ b/atomrdf/network/network.py @@ -1,3 +1,5 @@ + + import networkx as nx import graphviz import matplotlib.pyplot as plt @@ -81,6 +83,26 @@ def __radd__(self, ontonetwork): return self.__add__(ontonetwork) def get_shortest_path(self, source, target, triples=False): + """ + Compute the shortest path between two nodes in the graph. + + Parameters: + ----------- + source : node + The starting node for the path. + target : node + The target node for the path. + triples : bool, optional + If True, returns the path as a list of triples. Each triple consists of three consecutive nodes in the path. + If False, returns the path as a list of nodes. + + Returns: + -------- + path : list + The shortest path between the source and target nodes. If `triples` is True, the path is returned as a list of triples. + If `triples` is False, the path is returned as a list of nodes. + + """ path = nx.shortest_path(self.g, source=source, target=target) if triples: triple_list = [] @@ -115,7 +137,20 @@ def _add_data_properties(self): def add_namespace(self, namespace_name, namespace_iri): """ - Add a new namespace + Add a new namespace. + + Parameters + ---------- + namespace_name : str + The name of the namespace to add. + namespace_iri : str + The IRI of the namespace. + + Raises + ------ + KeyError + If the namespace already exists. + """ if namespace_name not in self.onto.namespaces.keys(): self.onto.namespaces[namespace_name] = namespace_iri @@ -127,17 +162,40 @@ def add_term( uri, node_type, namespace=None, - dm=[], - rn=[], + dm=(), + rn=(), data_type=None, node_id=None, delimiter="/", ): """ - Add a node + Add a node. + + Parameters + ---------- + uri : str + The URI of the node. + node_type : str + The type of the node. + namespace : str, optional + The namespace of the node. + dm : list, optional + The domain metadata of the node. + rn : list, optional + The range metadata of the node. + data_type : str, optional + The data type of the node. + node_id : str, optional + The ID of the node. + delimiter : str, optional + The delimiter used for parsing the URI. + + Raises + ------ + ValueError + If the namespace is not found. + """ - # namespace = strip_name(uri, delimiter, get_what="namespace") - # name = strip_name(uri, delimiter, get_what="name") term = OntoTerm( uri, namespace=namespace, @@ -155,11 +213,24 @@ def add_term( def add_path(self, triple): """ - Add a triple as path. Note that all attributes of the triple should already - exist in the graph. The ontology itself is not modified. Only the graph - representation of it is. - The expected use is to bridge between two(or more) different ontologies. + Add a triple as path. + + Note that all attributes of the triple should already exist in the graph. + The ontology itself is not modified. Only the graph representation of it is. + The expected use is to bridge between two (or more) different ontologies. Therefore, mapping can only be between classes. + + Parameters + ---------- + triple : tuple + A tuple representing the triple to be added. The tuple should contain three elements: + subject, predicate, and object. + + Raises + ------ + ValueError + If the subject or object of the triple is not found in the attributes of the ontology. + """ sub = triple[0] pred = triple[1] @@ -190,15 +261,38 @@ def add_path(self, triple): else: raise ValueError(f"{pred} not found in self.attributes") - def draw( - self, + def draw(self, styledict={ "class": {"shape": "box"}, "object_property": {"shape": "ellipse"}, "data_property": {"shape": "ellipse"}, "literal": {"shape": "parallelogram"}, - }, - ): + },): + """ + Draw the network graph using graphviz. + + Parameters + ---------- + styledict : dict, optional + A dictionary specifying the styles for different node types. + The keys of the dictionary are the node types, and the values are dictionaries + specifying the shape for each node type. Defaults to None. + + Returns + ------- + graphviz.Digraph + The graph object representing the network graph. + + Example + ------- + styledict = { + "class": {"shape": "box"}, + "object_property": {"shape": "ellipse"}, + "data_property": {"shape": "ellipse"}, + "literal": {"shape": "parallelogram"}, + } + network.draw(styledict) + """ dot = graphviz.Digraph() node_list = list(self.g.nodes(data="node_type")) edge_list = list(self.g.edges) @@ -212,6 +306,19 @@ def draw( return dot def get_path_from_sample(self, target): + """ + Get the shortest path from the 'cmso:ComputationalSample' node to the target node. + + Parameters + ---------- + target : OntoTerm + The target node to find the shortest path to. + + Returns + ------- + list + A list of triples representing the shortest path from 'cmso:ComputationalSample' to the target node. + """ path = self.get_shortest_path( source="cmso:ComputationalSample", target=target, triples=True ) @@ -219,7 +326,24 @@ def get_path_from_sample(self, target): def create_query(self, source, destinations, condition=None, enforce_types=True): """ - values is a dict with keys value, operation + Create a SPARQL query string based on the given source, destinations, condition, and enforce_types. + + Parameters + ---------- + source : Node + The source node from which the query starts. + destinations : list or Node + The destination node(s) to which the query should reach. If a single node is provided, it will be converted to a list. + condition : Condition, optional + The condition to be applied in the query. Defaults to None. + enforce_types : bool, optional + Whether to enforce the types of the source and destination nodes in the query. Defaults to True. + + Returns + ------- + str + The generated SPARQL query string. + """ if not isinstance(destinations, list): destinations = [destinations] diff --git a/atomrdf/network/ontology.py b/atomrdf/network/ontology.py index 2944c4c..07c3f04 100644 --- a/atomrdf/network/ontology.py +++ b/atomrdf/network/ontology.py @@ -3,6 +3,13 @@ def read_ontology(): + """ + Read in ontologies and perform necessary operations. + + Returns + ------- + combo: OntologyNetwork, Combined ontology network. + """ # read in ontologies file_location = os.path.dirname(__file__).split("/") file_location = "/".join(file_location[:-1]) diff --git a/atomrdf/network/term.py b/atomrdf/network/term.py index a2a61b5..558fa2c 100644 --- a/atomrdf/network/term.py +++ b/atomrdf/network/term.py @@ -55,18 +55,7 @@ def __init__( node_id=None, delimiter="/", ): - """ - This is class that represents an ontology element. - - Parameters - ----------- - uri: string - uri of the ontology term - namespace: string, optional - if provided this will be used as namespace - - """ self.uri = uri # type: can be object property, data property, or class self.node_type = node_type @@ -89,6 +78,14 @@ def __init__( @property def uri(self): + """ + Get the URI of the ontology term. + + Returns + ------- + str + The URI of the ontology term. + """ return self._uri @uri.setter @@ -97,6 +94,14 @@ def uri(self, val): @property def name_without_prefix(self): + """ + Get the name without the namespace prefix. + + Returns + ------- + str + The name of the term without the namespace prefix. + """ name = _get_name(self.uri, self.delimiter) name = name.replace("–", "") name = name.replace("-", "") @@ -104,12 +109,28 @@ def name_without_prefix(self): @property def name(self): + """ + Get the name of the term. + + Returns + ------- + str + The name of the term. + """ return strip_name( self.uri, self.delimiter, namespace=self._namespace, get_what="name" ) @property def namespace(self): + """ + Get the namespace of the term. + + Returns + ------- + str + The namespace of the term. + """ if self._namespace is not None: return self._namespace else: @@ -117,6 +138,14 @@ def namespace(self): @property def namespace_with_prefix(self): + """ + Get the namespace of the term with the prefix. + + Returns + ------- + str + The namespace of the term with the prefix. + """ uri_split = self.uri.split(self.delimiter) if len(uri_split) > 1: namespace = self.delimiter.join(uri_split[:-1]) + self.delimiter @@ -126,6 +155,15 @@ def namespace_with_prefix(self): @property def namespace_object(self): + """ + Get the namespace object for the term. + + Returns + ------- + object + The namespace object for the term. + + """ uri_split = self.uri.split(self.delimiter) if len(uri_split) > 1: namespace = self.delimiter.join(uri_split[:-1]) + self.delimiter @@ -137,7 +175,17 @@ def namespace_object(self): @property def query_name(self): """ - What it is called in a sparql query + Get the name of the term as it appears in a SPARQL query. + + Returns + ------- + str + The name of the term in a SPARQL query. + + Notes + ----- + If the term is a data property, the name will be appended with "value". + """ if self.node_type == "data_property": return self.name + "value" @@ -146,7 +194,16 @@ def query_name(self): @property def query_name_without_prefix(self): """ - What it is called in a sparql query + Get the name of the term as it appears in a SPARQL query without prefix. + + Returns + ------- + str + The name of the term in a SPARQL query. + + Notes + ----- + If the term is a data property, the name will be suffixed with "value". """ if self.node_type == "data_property": return self.name_without_prefix + "value" diff --git a/atomrdf/properties.py b/atomrdf/properties.py index ce2bbd5..c12de0d 100644 --- a/atomrdf/properties.py +++ b/atomrdf/properties.py @@ -17,26 +17,110 @@ # SIMCELL properties # -------------------------------------------- def get_chemical_composition(system): + """ + Get the chemical composition of the system. + + Parameters + ---------- + system : object + The system object. + + Returns + ------- + composition : dict + A dictionary containing the chemical elements as keys and their corresponding counts as values. + + """ return system.composition def get_cell_volume(system): + """ + Get the volume of the simulation cell. + + Parameters + ---------- + system : object + The system object. + + Returns + ------- + volume : float + The volume of the simulation cell. + + """ return system.volume def get_number_of_atoms(system): + """ + Get the number of atoms in the system. + + Parameters + ---------- + system : object + The system object. + + Returns + ------- + natoms : int + The number of atoms in the system. + + """ return system.natoms def get_simulation_cell_length(system): + """ + Get the length of the simulation cell. + + Parameters + ---------- + system : object + The system object. + + Returns + ------- + length : list + A list containing the length of each dimension of the simulation cell. + + """ return system.box_dimensions def get_simulation_cell_vector(system): + """ + Get the simulation cell vector of the given system. + + Parameters + ---------- + system : object + The system object containing the simulation cell information. + + Returns + ------- + numpy.ndarray + The simulation cell vector of the system. + + """ return system.box def get_simulation_cell_angle(system): + """ + Get the angles between the vectors of the simulation cell. + + Parameters + ---------- + system : object + The system object containing the simulation cell information. + + Returns + ------- + angles : list + A list containing the angles between the vectors of the simulation cell. + + """ return [ _get_angle(system.box[0], system.box[1]), _get_angle(system.box[1], system.box[2]), @@ -49,6 +133,20 @@ def get_simulation_cell_angle(system): def get_lattice_angle(system): + """ + Calculate the lattice angles of a given system. + + Parameters + ---------- + system : object + The system object containing the structure information. + + Returns + ------- + list + A list of three lattice angles in degrees. If the structure information is not available, [None, None, None] is returned. + + """ if system._structure_dict is None: return [None, None, None] if "box" in system._structure_dict.keys(): @@ -68,6 +166,35 @@ def get_lattice_angle(system): def get_lattice_parameter(system): + """ + Calculate the lattice parameters of a system. + + Parameters + ---------- + system : object + The system object containing information about the atoms and structure. + + Returns + ------- + list + A list containing the lattice parameters of the system. If the lattice constant is not available, + [None, None, None] is returned. If the system structure is available, the lattice parameters are + calculated based on the box dimensions. Otherwise, the lattice constant is returned for all three + dimensions. + + Examples + -------- + >>> system = System() + >>> system.atoms._lattice_constant = 3.5 + >>> system._structure_dict = {"box": [[1, 0, 0], [0, 1, 0], [0, 0, 1]]} + >>> get_lattice_parameter(system) + [3.5, 3.5, 3.5] + + >>> system.atoms._lattice_constant = None + >>> get_lattice_parameter(system) + [None, None, None] + """ + if system.atoms._lattice_constant is None: return [None, None, None] else: @@ -89,12 +216,40 @@ def get_lattice_parameter(system): def get_crystal_structure_name(system): + """ + Get the name of the crystal structure for a given system. + + Parameters + ---------- + system : object + The system object containing the crystal structure information. + + Returns + ------- + str or None + The name of the crystal structure if available, otherwise None. + + """ if system._structure_dict is None: return None return system.atoms._lattice def get_bravais_lattice(system): + """ + Get the Bravais lattice of a given system. + + Parameters + ---------- + system : object + The system object for which the Bravais lattice is to be determined. + + Returns + ------- + str or None + The Bravais lattice of the system, or None if the system's structure dictionary is not available or the lattice is not found in the dictionary. + + """ if system._structure_dict is None: return None if system.atoms._lattice in bravais_lattice_dict.keys(): @@ -103,6 +258,19 @@ def get_bravais_lattice(system): def get_basis_positions(system): + """ + Get the basis positions from the given system. + + Parameters + ---------- + system : object + The system object containing the structure dictionary. + + Returns + ------- + numpy.ndarray or None + The basis positions if available, otherwise None. + """ if system._structure_dict is None: return None if "positions" in system._structure_dict.keys(): @@ -126,6 +294,21 @@ def get_basis_positions(system): def get_lattice_vector(system): + """ + Get the lattice vector of a system. + + Parameters + ---------- + system : object + The system object containing the structure information. + + Returns + ------- + list + A list representing the lattice vector of the system. If the structure + dictionary is not available or the lattice vector is not defined, it + returns [None, None, None]. + """ if system._structure_dict is None: return [None, None, None] if "box" in system._structure_dict.keys(): @@ -134,6 +317,15 @@ def get_lattice_vector(system): def get_spacegroup_symbol(system): + """ + Get the symbol of the spacegroup for a given system. + + Parameters: + system (object): The system object for which to retrieve the spacegroup symbol. + + Returns: + str: The symbol of the spacegroup if available, otherwise None. + """ if system._structure_dict is None: return None try: @@ -144,6 +336,19 @@ def get_spacegroup_symbol(system): def get_spacegroup_number(system): + """ + Get the spacegroup number of a given system. + + Parameters + ---------- + system : object + The system object for which the spacegroup number is to be determined. + + Returns + ------- + int or None + The spacegroup number of the system if it is available, otherwise None. + """ if system._structure_dict is None: return None try: @@ -156,10 +361,38 @@ def get_spacegroup_number(system): # ATOM attributes # -------------------------------------------- def get_position(system): + """ + Get the positions of the atoms in the system. + + Parameters + ---------- + system : object + The system object containing the atom positions. + + Returns + ------- + numpy.ndarray or None + The positions of the atoms if available, otherwise None. + + """ return system.atoms.positions def get_species(system): + """ + Get the species of atoms in the given system. + + Parameters + ---------- + system : System + The system object containing atoms. + + Returns + ------- + list + A list of species of atoms in the system. + + """ return system.atoms.species diff --git a/atomrdf/stores.py b/atomrdf/stores.py index 97740f1..4066e95 100644 --- a/atomrdf/stores.py +++ b/atomrdf/stores.py @@ -9,7 +9,28 @@ def create_store(kg, store, identifier, store_file=None, structure_store=None): - + """ + Create a store based on the given parameters. + + Parameters: + ----------- + kg : KnowledgeGraph + The knowledge graph object. + store : str or Project + The type of store to create. It can be either "Memory", "SQLAlchemy", or a pyiron Project object. + identifier : str + The identifier for the store. + store_file : str, optional + The file path to store the data (only applicable for certain store types). + structure_store : str, optional + The structure store to use (only applicable for certain store types). + + Raises: + ------- + ValueError + If an unknown store type is provided. + + """ kg.store_file = store_file if store == "Memory": store_memory( @@ -40,12 +61,57 @@ def create_store(kg, store, identifier, store_file=None, structure_store=None): def store_memory(kg, store, identifier, store_file=None, structure_store=None): + """ + Store the knowledge graph in memory. + + Parameters + ---------- + kg : KnowledgeGraph + The knowledge graph to be stored. + store : str + The type of store to use for storing the graph. + identifier : str + The identifier for the graph. + store_file : str, optional + The file to store the graph in. Defaults to None. + structure_store : str, optional + The structure store to use. Defaults to None. + + Returns + ------- + None + """ graph = Graph(store="Memory", identifier=identifier) kg.graph = graph kg.structure_store = _setup_structure_store(structure_store=structure_store) def store_alchemy(kg, store, identifier, store_file=None, structure_store=None): + """ + Store the knowledge graph using SQLAlchemy. + + Parameters + ---------- + kg : KnowledgeGraph + The knowledge graph to be stored. + store : str + The type of store to be used. + identifier : str + The identifier for the graph. + store_file : str, optional + The file path for the store. Required if store is not 'memory'. + structure_store : str, optional + The structure store to be used. + + Raises + ------ + ValueError + If store_file is None and store is not 'memory'. + + Returns + ------- + None + """ _check_if_sqlalchemy_is_available() if store_file is None: raise ValueError("store file is needed if store is not memory") @@ -57,6 +123,27 @@ def store_alchemy(kg, store, identifier, store_file=None, structure_store=None): def store_pyiron(kg, store, identifier, store_file=None, structure_store=None): + """ + Store the pyiron knowledge graph in a database. + + Parameters + ---------- + kg : pyiron.atomistics.structure.pyiron_atomistics.structure.AtomisticStructure + The pyiron knowledge graph to be stored. + store : pyiron.atomistics.structure.pyiron_atomistics.structure.AtomisticStructure + The store object where the knowledge graph will be stored. + identifier : str + The identifier for the knowledge graph. + store_file : str, optional + The path to the store file. If not provided, a default path will be used. + structure_store : str, optional + The path to the structure store. If not provided, a default path will be used. + + Returns + ------- + None + + """ structure_store = os.path.join(store.path, "rdf_structure_store") kg.structure_store = _setup_structure_store(structure_store=structure_store) store_file = os.path.join(store.path, f"{store.name}.db") diff --git a/atomrdf/structure.py b/atomrdf/structure.py index fdf957c..2d01f1f 100644 --- a/atomrdf/structure.py +++ b/atomrdf/structure.py @@ -1,6 +1,9 @@ """ -StructureGraph is the central object in atomrdf which combines all the functionality -of :py:class:`atomrdf.graph.RDFGraph` along with easy structural creation routines. +This module provides functions for creating and manipulating atomic structures. It includes functions for creating crystals, +general lattices, dislocations, grain boundaries, and reading structures from files. The module also provides functionality for +adding interstitial impurities, substituting atoms, deleting atoms, and adding vacancies. +The structures can be converted to RDF graphs using the atomrdf library. +The main object in this module is the System class, which extends the functionality of the pyscal3.core.System class and provides additional methods for working with atomic structures. """ import numpy as np @@ -45,7 +48,36 @@ def _make_crystal( graph=None, names=False, ): - + """ + Create a crystal structure using the specified parameters. + + Parameters: + ----------- + structure : str + The crystal structure to create. + lattice_constant : float, optional + The lattice constant of the crystal. Default is 1.00. + repetitions : tuple or None, optional + The number of repetitions of the crystal structure in each direction. Default is None. + ca_ratio : float, optional + The c/a ratio of the crystal. Default is 1.633. + noise : float, optional + The amount of noise to add to each atom position. Default is 0. + element : str or None, optional + The element to use for the crystal. Default is None. + primitive : bool, optional + Whether to create a primitive cell. Default is False. + graph : atomrdf.KnowledgeGraph, optional + The graph object to use for the crystal. Default is None. + The structure is added to the KnowledgeGraph only if this option is provided. + names : bool, optional + If provided, human-readable names will be assigned to each property. If False, random IDs will be used. Default is False. + + Returns: + -------- + s : object + The atomrdf.Structure object representing the generated crystal structure. + """ atoms, box, sdict = pcs.make_crystal( structure, lattice_constant=lattice_constant, @@ -78,7 +110,37 @@ def _make_general_lattice( graph=None, names=False, ): + """ + Generate a general lattice structure. + + Parameters: + ----------- + positions : array_like + The atomic positions in the lattice. + types : array_like + The atomic types corresponding to the positions. + box : array_like + The box dimensions of the lattice. + lattice_constant : float, optional + The lattice constant, defaults to 1.00. + repetitions : array_like, optional + The number of repetitions of the lattice in each direction. + noise : float, optional + The amount of noise to add to the lattice positions, defaults to 0. + element : str, optional + The chemical elements associated with the atoms. Should be equal to the number of unique types. + graph : atomrdf.KnowledgeGraph, optional + The graph object to store the lattice structure, defaults to None. + The structure is added to the KnowledgeGraph only if this option is provided. + names : bool, optional + If True, human readable names instead of random ids will be created. Default is False. + + Returns: + -------- + s : object + The atomrdf.Structure object representing the generated lattice structure. + """ atoms, box, sdict = pcs.general_lattice( positions, types, @@ -116,7 +178,62 @@ def _make_dislocation( graph=None, names=False, ): + """ + Generate a dislocation structure. Wraps the atomman.defect.Dislocation class. + + Parameters + ---------- + burgers_vector : numpy array of length 3 + The Burgers vector of the dislocation. + slip_vector : numpy array of length 3 + The slip vector of the dislocation. + dislocation_line : numpy array of length 3 + The dislocation line direction. + elastic_constant_dict : dict + Dictionary of elastic constants. + dislocation_type : str, optional + The type of dislocation to generate. Default is "monopole". + structure : crystal lattice to be used + The crystal structure to use to create the bulk system. Either structure or element must be given. + element : str, optional + The atomic symbol according to which the bulk structure is to be created. Either structure or element must be given. + lattice_constant : float, optional + The lattice constant to use for the generated crystal structure. Default is 1.00. + repetitions : tuple, optional + The number of times to repeat the unit cell in each of the three Cartesian directions. Default is None. + ca_ratio : float, optional + The c/a ratio to use for the generated crystal structure. Default is 1.633. Used only if structure is hcp. + noise : float, optional + Magnitude of random noise to add to the atomic positions. Default is 0. + primitive : bool, optional + If True, the generated crystal structure will be converted to a primitive cell. Default is False. + graph :atomrdf.KnowledgeGraph, optional + A graph object representing the crystal structure. Default is None. + names : bool, optional + If True, the returned System will have atom names assigned based on the element and index. Default is False. + + Returns + ------- + output_structure : atomrdf.System + The generated dislocation structure. + + Raises + ------ + ValueError + If neither structure nor element is provided. + + Notes + ----- + This function requires the atomman Python package to be installed. + + The elastic_constant_dict parameter should be a dictionary of elastic constants with keys corresponding to the + following Voigt notation: "C11", "C12", "C13", "C14", "C15", "C16", "C22", "C23", "C24", "C25", "C26", "C33", "C34", + "C35", "C36", "C44", "C45", "C46", "C55", "C56", "C66". The values should be given in GPa. + The dislocation_type parameter can be set to "monopole" or "periodicarray". If set to "monopole", a single dislocation + will be generated. If set to "periodicarray", a periodic array of dislocations will be generated. + + """ from atomman.defect.Dislocation import Dislocation import atomman as am import atomman.unitconvert as uc @@ -150,13 +267,6 @@ def _make_dislocation( else: raise ValueError("Provide either structure or element") - # create the elastic constant object - # possible_keys = ["C11", "C12", "C13", "C14", "C15", "C16", - # "C22", "C23", "C24", "C25", "C26", - # "C33", "C34", "C35", "C36", - # "C44", "C45", "C46", - # "C55", "C56", - # "C66"] for key, val in elastic_constant_dict.items(): elastic_constant_dict[key] = uc.set_in_units(val, "GPa") C = am.ElasticConstants(**elastic_constant_dict) @@ -220,7 +330,39 @@ def _make_grain_boundary( graph=None, names=False, ): + """ + Create a grain boundary system. + + Parameters: + ----------- + axis : tuple or list + The rotation axis of the grain boundary. + sigma : int + The sigma value of the grain boundary. + gb_plane : tuple or list + The Miller indices of the grain boundary plane. + structure : the lattice structure to be used to create the GB, optional + The lattice structure to populate the grain boundary with. + element : str, optional + The element symbol to populate the grain boundary with. + lattice_constant : float, optional + The lattice constant of the structure. + repetitions : tuple or list, optional + The number of repetitions of the grain boundary structure in each direction. + overlap : float, optional + The overlap between adjacent grain boundaries. + graph : atomrdf.KnowledgeGraph, optional + The graph object to store the system. + The system is only added to the KnowledgeGraph if this option is provided. + names : bool, optional + If True human readable names will be assigned to each property. If False random ids will be used. Default is False. + + Returns: + -------- + atomrdf.System + The grain boundary system. + """ gb = GrainBoundary() gb.create_grain_boundary(axis=axis, sigma=sigma, gb_plane=gb_plane) @@ -272,10 +414,10 @@ def _read_structure( filename: string name of file - format: optional, string + format: string, optional format of the file - graph: optional + graph: atomrdf.KnowledgeGraph, optional if provided, the structure will be added to the graph names: bool, optional @@ -301,7 +443,7 @@ def _read_structure( Returns ------- - Structure + atomrdf.System """ datadict = {} if lattice is not None: @@ -433,7 +575,29 @@ def __init__( self.schema._add_attribute(mapdict) def delete(self, ids=None, indices=None, condition=None, selection=False): + """ + Delete atoms from the structure. + Parameters + ---------- + ids : list, optional + A list of atom IDs to delete. Default is None. + indices : list, optional + A list of atom indices to delete. Default is None. + condition : str, optional + A condition to select atoms to delete. Default is None. + selection : bool, optional + If True, delete atoms based on the current selection. Default is False. + + Returns + ------- + None + + Notes + ----- + Deletes atoms from the structure based on the provided IDs, indices, condition, or selection. + If the structure has a graph associated with it, the graph will be updated accordingly. + """ masks = self.atoms._generate_bool_list( ids=ids, indices=indices, condition=condition, selection=selection ) @@ -533,6 +697,39 @@ def substitute_atoms( condition=None, selection=False, ): + """ + Substitute atoms in the structure with a given element. + + Parameters + ---------- + substitution_element : str + The element to substitute the atoms with. + ids : list, optional + A list of atom IDs to consider for substitution. Defaults to None. + indices : list, optional + A list of atom indices to consider for substitution. Defaults to None. + condition : callable, optional + A callable that takes an atom as input and returns a boolean indicating whether the atom should be considered for substitution. Defaults to None. + selection : bool, optional + If True, only selected atoms will be considered for substitution. Defaults to False. + + Returns + ------- + None + + Notes + ----- + - This method substitutes atoms in the structure with a given element. + - The substitution is performed based on the provided IDs, indices, condition, and selection parameters. + - The substituted atoms will have their species and types updated accordingly. + - If the graph is not None, the method also operates on the graph by removing existing elements and adding new ones based on the composition of the substituted atoms. + - The method also cleans up items in the file associated with the graph. + + Examples + -------- + # Substitute selected atoms with nitrogen + structure.substitute_atoms("N", ids=[1, 3, 5]) + """ masks = self.atoms._generate_bool_list( ids=ids, indices=indices, condition=condition, selection=selection ) @@ -630,7 +827,7 @@ def add_interstitial_impurities( two impurities void_type: string - type of void to be added. Currently only `tetrahedral` + type of void to be added. {`tetrahedral`, `octahedral`} Returns ------- @@ -774,12 +971,33 @@ def add_interstitial_impurities( return sysn def __delitem__(self, val): + """ + Delete item(s) from the structure. + + Parameters + ---------- + val : int or list of int + The index(es) of the item(s) to be deleted. + + Notes + ----- + If `val` is an integer, it is converted to a list with a single element. + The graph is then updated accordingly based on the deleted indices. + + """ if isinstance(val, int): val = [val] # now the graph has to be updated accordingly self.delete(indices=list(val)) def to_graph(self): + """ + Converts the structure object to a graph representation. + + Returns + ------- + None + """ if self.graph is None: return @@ -1151,14 +1369,20 @@ def _add_unit_cell(self): self.unit_cell = unit_cell def _add_bravais_lattice(self, bv): - # add bravais lattice + """ + Add a Bravais lattice to the unit cell. + + Parameters: + bv (str): The URI of the Bravais lattice. + + Returns: + None + """ bv = URIRef(bv) self.graph.add( ( self.unit_cell, - Namespace( - "http://purls.helmholtz-metadaten.de/cmso/" - ).hasBravaisLattice, + Namespace("http://purls.helmholtz-metadaten.de/cmso/").hasBravaisLattice, bv, ) ) @@ -1229,10 +1453,29 @@ def _add_lattice_properties(self, lattice_parameter_value, lattice_angle_value): ) def _save_atom_attributes(self, position_identifier, species_identifier): - # if self.store == 'pyiron': - # pass - # else: - # #this is the file based store system + """ + Save the atom attributes to a file. + + Parameters + ---------- + position_identifier : str + The identifier for the position attribute. + species_identifier : str + The identifier for the species attribute. + + Returns + ------- + str + The relative path to the saved file. + + Notes + ----- + This method saves the atom attributes to a file in the file-based store system. + The attributes are stored in a dictionary with the position identifier and species identifier as keys. + The dictionary is then written to a JSON file using the `json_io.write_file` function. + The file is saved in the structure store directory with the name of the structure as the filename. + The method returns the relative path to the saved file. + """ datadict = { position_identifier: { "value": self.schema.atom_attribute.position(), @@ -1255,11 +1498,11 @@ def _add_atoms(self): Parameters ---------- - name - if provided, the name will be used instead of random identifier + None Returns ------- + None Notes ----- @@ -1353,11 +1596,12 @@ def add_vacancy(self, concentration, number=None): concentration: float vacancy concentration, value should be between 0-1 - name - if provided, the name will be used instead of random identifier + number: int + Number of atoms that were deleted, optional Returns ------- + None """ if self.graph is None: return @@ -1386,16 +1630,24 @@ def add_gb(self, gb_dict): Parameters ---------- - gb_dict: dict - dict containing details about the grain boundary - - name - if provided, the name will be used instead of random identifier + gb_dict : dict + A dictionary containing details about the grain boundary. + It should have the following keys: + - "GBType" (str): The type of grain boundary. Possible values are "Twist", "Tilt", "Symmetric Tilt", and "Mixed". + - "sigma" (int): The sigma value of the grain boundary. + - "GBPlane" (str): The plane of the grain boundary. + - "RotationAxis" (list): The rotation axis of the grain boundary. + - "MisorientationAngle" (float): The misorientation angle of the grain boundary. Returns ------- - """ + None + Notes + ----- + This method adds grain boundary details to the structure and annotates it using PLDO ontology. + The grain boundary type, sigma value, GB plane, rotation axis, and misorientation angle are stored as attributes of the grain boundary node in the graph. + """ # mark that the structure has a defect if self.graph is None: return diff --git a/atomrdf/visualize.py b/atomrdf/visualize.py index 5ec4235..cd95cbb 100644 --- a/atomrdf/visualize.py +++ b/atomrdf/visualize.py @@ -11,9 +11,19 @@ def get_title_from_BNode(x): def get_string_from_URI(x): """ - Extract a presentable string from URI - - Also differentiate between fixed notes and URIs, and assign color + Extract a presentable string from URI. + + Parameters + ---------- + x : rdflib.term.URIRef + The URI object to extract the string from. + + Returns + ------- + tuple + A tuple containing the presentable string representation of the URI and its type. + The string representation is the last part of the URI after splitting by '#' or '/'. + The type can be either "URIRef" or "BNode". """ raw = x.toPython() # first try splitting by # @@ -38,11 +48,25 @@ def get_string_from_URI(x): if len(rawsplit) > 1: return ".".join(rawsplit[-2:]), "URIRef" - # none of the conditions, worked, which means its a hex string + # none of the conditions worked, which means it's a hex string return raw, "BNode" def parse_object(x): + """ + Parse the given object and return its title and type. + + Parameters + ---------- + x : RDF term + The RDF term to parse. + + Returns + ------- + tuple + A tuple containing the title of the object and its type. + + """ if isinstance(x, BNode): return get_title_from_BNode(x), "BNode" elif isinstance(x, URIRef): @@ -81,7 +105,31 @@ def visualize_graph( size=None, layout="dot", ): - + """ + Visualizes a graph using Graphviz. + + Parameters + ---------- + g : dict + The graph to visualize. + styledict : dict, optional + A dictionary containing styles for different types of nodes and edges. Default is `styledict`. + rankdir : str, optional + The direction of the graph layout. Default is "TB" (top to bottom). + hide_types : bool, optional + Whether to hide nodes with the "type" attribute. Default is False. + workflow_view : bool, optional + Whether to enable the workflow view. Default is False. + size : str, optional + The size of the graph. Default is None. + layout : str, optional + The layout algorithm to use. Default is "dot". + + Returns + ------- + dot : graphviz.Digraph + The graph visualization. + """ dot = graphviz.Digraph() dot.attr( diff --git a/atomrdf/workflow/pyiron.py b/atomrdf/workflow/pyiron.py index d412345..a5ad871 100644 --- a/atomrdf/workflow/pyiron.py +++ b/atomrdf/workflow/pyiron.py @@ -132,8 +132,21 @@ def _identify_method(job): mdict["outputs"] = quantdict return mdict - def extract_calculated_quantities(job): + """ + Extracts calculated quantities from a job. + + Parameters + ---------- + job : pyiron.Job + The job object containing the calculated quantities. + + Returns + ------- + list + A list of dictionaries, each containing the label, value, unit, and associate_to_sample of a calculated quantity. + + """ aen = np.mean(job.output.energy_tot) avol = np.mean(job.output.volume) outputs = [] diff --git a/atomrdf/workflow/workflow.py b/atomrdf/workflow/workflow.py index 4c2d0da..4229bf8 100644 --- a/atomrdf/workflow/workflow.py +++ b/atomrdf/workflow/workflow.py @@ -172,6 +172,20 @@ def _add_inherited_properties( def add_structural_relation( self, ): + """ + Add structural relation between samples. + + This method adds the structural relation between the current sample and its parent sample. + It also retrieves lattice properties and adds inherited properties, such as defect information. + + Parameters + ---------- + None + + Returns + ------- + None + """ self.kg.add((self.sample, RDF.type, PROV.Entity)) if self.parent_sample is not None: self.kg.add((self.parent_sample, RDF.type, PROV.Entity)) @@ -180,12 +194,27 @@ def add_structural_relation( self._add_inherited_properties() def add_method( - self, - ): + self, + ): """ - mdict + Add the computational method and related information to the knowledge graph. + + Parameters + ---------- + None + + Returns + ------- + None + + Notes ----- - md: + This method adds the computational method and related information to the knowledge graph. + It creates an activity node representing the method and adds it to the graph. + The method is associated with the main activity using the `ASMO.hasComputationalMethod` property. + The type of the method is determined based on the value of the `method` key in the `mdict` dictionary. + The method-specific items are added to the graph based on the method type. + The structure generation information is also added to the graph. """ if self.mdict is None: @@ -238,6 +267,15 @@ def add_method( self._add_software(method) def to_graph(self, workflow_object): + """ + Converts a workflow object to a graph representation. + + Parameters: + - workflow_object: The workflow object to convert. + + Returns: + - None + """ self._prepare_job(workflow_object) self.add_structural_relation() self.add_method()