Introduction

pyiron_ontology uses the owlready2 library to build up pyiron-specific ontologies, and provides some extra tools to help you leverage these.

At present, the only ontology implemented is for the realm of atomistic calculations, and the scope of this ontology is still fairly limited.

First, let’s import pyiron_ontology and grab the atomistics ontology (an owlready2.namespace.Ontology object) we define in there

[1]:
import owlready2 as owl
import pyiron_ontology as po
* Owlready2 * Warning: optimized Cython parser module 'owlready2_optimized' is not available, defaulting to slower Python implementation
[2]:
onto = po.dynamic.atomistics()

We can look at various properties of the ontology, just like other owl ontologies, e.g. the classes and individuals defined in this space.

There are four key-classes common to all ontologies made in the scope of pyiron_ontology: Generic, Input, Function, and Output. Generic is the parent class used for defining domain knowledge; the remaining three are used to represent how computations are performed in any knowledge-space. (You’ll also see PyironOntoThing, Parameter, WorkflowThing, and IO are parent classes used under the hood).

Generic will be heavily sub-classed in each specific ontology, and then instantiated and paired with inputs and outputs so that we will know what sort of information is moving around our computation graphs. The workflow elements will (so far) only be instantiated, defining all the possible calculation available.

Let’s first look at the classes for our atomistics knowledge-space:

[3]:
list(onto.classes())
[3]:
[atomistics.PyironOntoThing,
 atomistics.Parameter,
 atomistics.Generic,
 atomistics.WorkflowThing,
 atomistics.Function,
 atomistics.IO,
 atomistics.Output,
 atomistics.Input,
 atomistics.AtomisticsFunction,
 atomistics.UserInput,
 atomistics.PyironObject,
 atomistics.PhysicalProperty,
 atomistics.Energy,
 atomistics.Force,
 atomistics.ChemicalElement,
 atomistics.MaterialProperty,
 atomistics.BulkModulus,
 atomistics.BPrime,
 atomistics.SurfaceEnergy,
 atomistics.Dimensional,
 atomistics.OneD,
 atomistics.TwoD,
 atomistics.ThreeD,
 atomistics.Structure,
 atomistics.Defected,
 atomistics.HasDislocation,
 atomistics.HasVacancy,
 atomistics.HasInterface,
 atomistics.HasGB,
 atomistics.HasSurface,
 atomistics.HasPB,
 atomistics.Bulk,
 atomistics.PyironProject,
 atomistics.AtomisticsProject,
 atomistics.PyironJob,
 atomistics.AtomisticsJob,
 atomistics.Lammps,
 atomistics.Vasp]

We can also look at the individuals. Some of these should have very descriptive names – these are the Input, Output, and Function individuals. The rest are instances of our Generic class (and its children) and receive their name automatically.

[4]:
list(onto.individuals())
[4]:
[atomistics.project,
 atomistics.userinput1,
 atomistics.project_input_name,
 atomistics.atomisticsproject1,
 atomistics.project_output_atomistics_project,
 atomistics.bulk_structure,
 atomistics.generic1,
 atomistics.bulk_structure_input_element,
 atomistics.structure1,
 atomistics.bulk_structure_output_structure,
 atomistics.surface_structure,
 atomistics.generic2,
 atomistics.surface_structure_input_element,
 atomistics.structure2,
 atomistics.surface_structure_output_structure,
 atomistics.lammps,
 atomistics.atomisticsproject2,
 atomistics.lammps_input_project,
 atomistics.structure3,
 atomistics.lammps_input_structure,
 atomistics.lammps1,
 atomistics.lammps_output_job,
 atomistics.vasp,
 atomistics.atomisticsproject3,
 atomistics.vasp_input_project,
 atomistics.generic3,
 atomistics.vasp_input_structure,
 atomistics.vasp1,
 atomistics.vasp_output_job,
 atomistics.atomistic_taker,
 atomistics.atomisticsjob1,
 atomistics.structure4,
 atomistics.atomistic_taker_job,
 atomistics.energy1,
 atomistics.atomistic_taker_output_energy_pot,
 atomistics.force1,
 atomistics.atomistic_taker_output_forces,
 atomistics.murnaghan,
 atomistics.atomisticsproject4,
 atomistics.murnaghan_input_project,
 atomistics.atomisticsjob2,
 atomistics.structure5,
 atomistics.murnaghan_input_job,
 atomistics.bulkmodulus1,
 atomistics.murnaghan_output_bulk_modulus,
 atomistics.bprime1,
 atomistics.murnaghan_output_b_prime,
 atomistics.surface_energy,
 atomistics.structure6,
 atomistics.surface_energy_input_bulk_structure,
 atomistics.energy2,
 atomistics.structure7,
 atomistics.surface_energy_input_bulk_energy,
 atomistics.structure8,
 atomistics.surface_energy_input_slab_structure,
 atomistics.energy3,
 atomistics.structure9,
 atomistics.surface_energy_input_slab_energy,
 atomistics.surfaceenergy1,
 atomistics.surface_energy_output_surface_energy]

We can make the usual owlready queries of these objects, e.g.

[5]:
onto.vasp_input_structure.INDIRECT_is_a
[5]:
[atomistics.IO,
 atomistics.Input,
 owl.Thing,
 atomistics.Parameter,
 atomistics.PyironOntoThing,
 atomistics.WorkflowThing]
[6]:
onto.vasp_input_structure.generic.INDIRECT_is_a
[6]:
[atomistics.ThreeD,
 atomistics.Dimensional,
 owl.Thing,
 atomistics.Parameter,
 atomistics.Structure,
 atomistics.Generic,
 atomistics.PyironObject,
 atomistics.PyironOntoThing]

We can also look into some of the atomistics-specific relationships that have been defined:

[7]:
onto.vasp.mandatory_inputs, onto.optional_inputs, onto.vasp.inputs
[7]:
([atomistics.vasp_input_project, atomistics.vasp_input_structure],
 None,
 [atomistics.vasp_input_project, atomistics.vasp_input_structure])
[8]:
onto.vasp.outputs
[8]:
[atomistics.vasp_output_job]

and we can chain these queries together in meaningful ways:

[9]:
some_code = onto.vasp
first_input = some_code.mandatory_inputs[0]
appears_elsewhere = first_input.generic.parameters
can_come_from = first_input.get_sources()
which_is_produced_by = can_come_from[0].output_of
print('some_code', some_code)
print('first_input', first_input)
print('appears_elsewhere', appears_elsewhere)
print('can_come_from', can_come_from)
print('which_is_produced_by', which_is_produced_by)
some_code atomistics.vasp
first_input atomistics.vasp_input_project
appears_elsewhere [atomistics.vasp_input_project]
can_come_from [atomistics.project_output_atomistics_project]
which_is_produced_by atomistics.project

This is powerful, but can be a bit unwieldly.

pyiron_ontology also comes with helper tools for building this sort of chain, or “workflow” in a guided or automatic way.

First, let’s see all the possible chains for getting input to a Lammps calculation:

[10]:
onto.lammps.get_source_tree().render()
lammps
        lammps_input_project
                project_output_atomistics_project
                        project
        lammps_input_structure
                surface_structure_output_structure
                        surface_structure
                bulk_structure_output_structure
                        bulk_structure

This tool also passes requirements upstream in the workflow. For instance, we see above that Lammps can take either bulk-like or non-bulk-like structure input. Instead of querying the ontology about what’s needed to run a particular code, let’s ask for a workflow to produce a particular material property: the bulk modulus. In this case, we know the workflow only makes sense if the structures going into it are bulk-like!

When we ask for this workflow, we again see Lammps (and Vasp) coming up as part of our tree, but now we see that it is precluded from taking surface structures because the condition for a bulk-like structure got passed up through our workflow!

(Note, these tools only work on individuals, so we’ll just reinstantiate a copy of our BulkModulus generic class and query that)

[11]:
onto.BulkModulus().get_source_tree().render()
bulkmodulus2
        murnaghan_output_bulk_modulus
                murnaghan
                        murnaghan_input_project
                                project_output_atomistics_project
                                        project
                        murnaghan_input_job
                                lammps_output_job
                                        lammps
                                                lammps_input_project
                                                        project_output_atomistics_project
                                                                project
                                                lammps_input_structure
                                                        bulk_structure_output_structure
                                                                bulk_structure
                                vasp_output_job
                                        vasp
                                                vasp_input_project
                                                        project_output_atomistics_project
                                                                project
                                                vasp_input_structure
                                                        bulk_structure_output_structure
                                                                bulk_structure

Instead of seeing all possible paths, we can build one particular path iteratively, looking at the choices available at each step and selecting which one we want:

[12]:
b_prime = onto.BPrime()
[13]:
b_prime.get_source_path()
[13]:
(<pyiron_ontology.workflow.NodeTree at 0x136f91990>,
 [atomistics.murnaghan_output_b_prime])
[14]:
b_prime.get_source_path(0)
[14]:
(<pyiron_ontology.workflow.NodeTree at 0x136f928d0>, [atomistics.murnaghan])
[15]:
b_prime.get_source_path(0, 0)
[15]:
(<pyiron_ontology.workflow.NodeTree at 0x136f92790>,
 [atomistics.murnaghan_input_project, atomistics.murnaghan_input_job])

The project is a bit of a boring path to follow, so let’s choose 1 here to follow the job path:

[16]:
b_prime.get_source_path(0, 0, 1)
[16]:
(<pyiron_ontology.workflow.NodeTree at 0x136f62590>,
 [atomistics.lammps_output_job, atomistics.vasp_output_job])
[17]:
b_prime.get_source_path(0, 0, 1, 1)
[17]:
(<pyiron_ontology.workflow.NodeTree at 0x136f63f50>, [atomistics.vasp])
[18]:
b_prime.get_source_path(0, 0, 1, 1, 0)
[18]:
(<pyiron_ontology.workflow.NodeTree at 0x136f73010>,
 [atomistics.vasp_input_project, atomistics.vasp_input_structure])
[19]:
b_prime.get_source_path(0, 0, 1, 1, 0, 0)
[19]:
(<pyiron_ontology.workflow.NodeTree at 0x136f61290>,
 [atomistics.project_output_atomistics_project])
[20]:
b_prime.get_source_path(0, 0, 1, 1, 0, 0, 0)
[20]:
(<pyiron_ontology.workflow.NodeTree at 0x136f73dd0>, [atomistics.project])
[21]:
b_prime.get_source_path(0, 0, 1, 1, 0, 0, 0, 0)
[21]:
(<pyiron_ontology.workflow.NodeTree at 0x136f78c90>, [])
[22]:
path, _ = b_prime.get_source_path(0, 0, 1, 1, 0, 0, 0, 0)
path.render()
bprime2
        murnaghan_output_b_prime
                murnaghan
                        murnaghan_input_job
                                vasp_output_job
                                        vasp
                                                vasp_input_project
                                                        project_output_atomistics_project
                                                                project

Note: this only traces one path of the required input to get to the result we originally queried – as noted above where we ignored the project input; you need to choose paths for all the required input at each Function step of the path.

Working with pyiron data

We also have tools for leveraging the ontology to search through existing pyiron data in your storage and database

Here we’ll need import pyiron_atomistics.Project so we can create some data to work with.

[23]:
from pyiron_ontology import AtomisticsReasoner
from pyiron_atomistics import Project
import numpy as np
[24]:
reasoner = AtomisticsReasoner(onto)

Next, we’ll produce some data and then use the a tool on the reasoner to search over it for data that matches a particular ontological property.

First, we’ll need to produce some data to search over. In this case, let’s calculate the bulk modulus for a couple of alloys with varying Nickle content. On a single-core laptop, this might take two or three minutes.

[25]:
pr = Project('example')
pr.remove_jobs(silently=True, recursive=True)

So that we can compare results for different compositions, let’s quickly find a potential contains all our elements and use it consistently.

[26]:
host = "Cu"
solutes = ["Ag", "Ni"]
all_species = pr.atomistics.structure.bulk(host, cubic=True)
all_species[0] = solutes[0]
all_species[1] = solutes[1]

from pyiron_atomistics.lammps import list_potentials
potential = list_potentials(all_species)[0]
[27]:
for solute in solutes:
    for frac in [0., 0.10, 0.25]:
        ref = pr.atomistics.job.Lammps(f"Lammps_{host}_{solute}_frac{frac:.2f}".replace(".", "d"))
        ref.structure = pr.atomistics.structure.bulk(host, cubic=True).repeat(3)
        random_ids = np.random.choice(
            np.arange(len(ref.structure), dtype=int),
            int(frac * len(ref.structure))
        )
        ref.structure[random_ids] = solute
        ref.potential = potential

        murn = pr.atomistics.job.Murnaghan(f"Murn_{host}_{solute}_frac{frac:.2f}".replace(".", "d"))
        murn.input['num_points']=7
        murn.ref_job = ref
        try:
            murn.run()
        except:
            with open(
                f"./{pr.name}/{murn.name}_hdf5/{murn.name}_0_9_hdf5/{murn.name}_0_9/error.msg"
            ) as f:
                msg = f.readlines()
            raise RuntimeError(msg)
The job Murn_Cu_Ag_frac0d00 was saved and received the ID: 7418
The job Murn_Cu_Ag_frac0d00_0_9 was saved and received the ID: 7419
The job Murn_Cu_Ag_frac0d00_0_9333333 was saved and received the ID: 7420
The job Murn_Cu_Ag_frac0d00_0_9666667 was saved and received the ID: 7421
The job Murn_Cu_Ag_frac0d00_1_0 was saved and received the ID: 7422
The job Murn_Cu_Ag_frac0d00_1_0333333 was saved and received the ID: 7423
The job Murn_Cu_Ag_frac0d00_1_0666667 was saved and received the ID: 7424
The job Murn_Cu_Ag_frac0d00_1_1 was saved and received the ID: 7425
The job Murn_Cu_Ag_frac0d10 was saved and received the ID: 7426
The job Murn_Cu_Ag_frac0d10_0_9 was saved and received the ID: 7427
The job Murn_Cu_Ag_frac0d10_0_9333333 was saved and received the ID: 7428
The job Murn_Cu_Ag_frac0d10_0_9666667 was saved and received the ID: 7429
The job Murn_Cu_Ag_frac0d10_1_0 was saved and received the ID: 7430
The job Murn_Cu_Ag_frac0d10_1_0333333 was saved and received the ID: 7431
The job Murn_Cu_Ag_frac0d10_1_0666667 was saved and received the ID: 7432
The job Murn_Cu_Ag_frac0d10_1_1 was saved and received the ID: 7433
The job Murn_Cu_Ag_frac0d25 was saved and received the ID: 7434
The job Murn_Cu_Ag_frac0d25_0_9 was saved and received the ID: 7435
The job Murn_Cu_Ag_frac0d25_0_9333333 was saved and received the ID: 7436
The job Murn_Cu_Ag_frac0d25_0_9666667 was saved and received the ID: 7437
The job Murn_Cu_Ag_frac0d25_1_0 was saved and received the ID: 7438
The job Murn_Cu_Ag_frac0d25_1_0333333 was saved and received the ID: 7439
The job Murn_Cu_Ag_frac0d25_1_0666667 was saved and received the ID: 7440
The job Murn_Cu_Ag_frac0d25_1_1 was saved and received the ID: 7441
2023-05-08 11:18:22,628 - pyiron_log - WARNING - Minimum could not be found!
The job Murn_Cu_Ni_frac0d00 was saved and received the ID: 7442
The job Murn_Cu_Ni_frac0d00_0_9 was saved and received the ID: 7443
The job Murn_Cu_Ni_frac0d00_0_9333333 was saved and received the ID: 7444
The job Murn_Cu_Ni_frac0d00_0_9666667 was saved and received the ID: 7445
The job Murn_Cu_Ni_frac0d00_1_0 was saved and received the ID: 7446
The job Murn_Cu_Ni_frac0d00_1_0333333 was saved and received the ID: 7447
The job Murn_Cu_Ni_frac0d00_1_0666667 was saved and received the ID: 7448
The job Murn_Cu_Ni_frac0d00_1_1 was saved and received the ID: 7449
The job Murn_Cu_Ni_frac0d10 was saved and received the ID: 7450
The job Murn_Cu_Ni_frac0d10_0_9 was saved and received the ID: 7451
The job Murn_Cu_Ni_frac0d10_0_9333333 was saved and received the ID: 7452
The job Murn_Cu_Ni_frac0d10_0_9666667 was saved and received the ID: 7453
The job Murn_Cu_Ni_frac0d10_1_0 was saved and received the ID: 7454
The job Murn_Cu_Ni_frac0d10_1_0333333 was saved and received the ID: 7455
The job Murn_Cu_Ni_frac0d10_1_0666667 was saved and received the ID: 7456
The job Murn_Cu_Ni_frac0d10_1_1 was saved and received the ID: 7457
The job Murn_Cu_Ni_frac0d25 was saved and received the ID: 7458
The job Murn_Cu_Ni_frac0d25_0_9 was saved and received the ID: 7459
The job Murn_Cu_Ni_frac0d25_0_9333333 was saved and received the ID: 7460
The job Murn_Cu_Ni_frac0d25_0_9666667 was saved and received the ID: 7461
The job Murn_Cu_Ni_frac0d25_1_0 was saved and received the ID: 7462
The job Murn_Cu_Ni_frac0d25_1_0333333 was saved and received the ID: 7463
The job Murn_Cu_Ni_frac0d25_1_0666667 was saved and received the ID: 7464
The job Murn_Cu_Ni_frac0d25_1_1 was saved and received the ID: 7465

Now let’s search the pyiron database for instances of some of our physically-meaningful properties:

[28]:
reasoner.search_database_for_property(onto.BulkModulus(), pr)
[28]:
Chemical Formula atomistics.BulkModulus unit Engine
0 Cu108 141.949581 GPa Lammps
1 Ag9Cu99 134.392608 GPa Lammps
2 Ag24Cu84 NaN GPa Lammps
3 Cu108 141.949581 GPa Lammps
4 Cu99Ni9 145.901172 GPa Lammps
5 Cu83Ni25 152.635624 GPa Lammps

We can also filter our search by chemistry:

[29]:
reasoner.search_database_for_property(onto.BPrime(), pr, select_alloy="Cu")
[29]:
Chemical Formula atomistics.BPrime unit Engine
0 Cu108 4.393195 None Lammps
1 Ag9Cu99 6.157580 None Lammps
2 Ag24Cu84 NaN None Lammps
3 Cu108 4.393195 None Lammps
4 Cu99Ni9 4.309860 None Lammps
5 Cu83Ni25 4.112948 None Lammps

Cleanup

[30]:
pr.remove_jobs_silently(recursive=True)
pr.remove(enable=True)
/var/folders/ff/j3764z6n37386m647kqygyzw0000gp/T/ipykernel_94195/3270375829.py:1: DeprecationWarning: pyiron_base.project.generic.remove_jobs_silently is deprecated: Use pr.remove_jobs(silently=True) rather than pr.remove_jobs_silently()..
  pr.remove_jobs_silently(recursive=True)
[ ]: