Basic Usage
===========

Importing Terms as classes
--------------------------

The *pathology-image-features* library is structured according to the ontological terms which are defined in the Pathology Image Features Ontology.
Therefore, to import a class, you can follow the subclass hierarchy to determine the path:

For example, the class "image data set" (OBI:0003327) has the following parent term structure:

- information content entity (IAO:0000030)
    - data entity (IAO:0000027)
        - homogenous data collection (IAO:0000100)
            - image data set (OBI:0003327)

Therefore, it **can** be imported from *pathology-image-features* using the following:

.. code-block:: python

    from pathology_image_features.ontology.classes.information_content_entity.data_entity.homogenous_data_collection.image_data_set import image_data_set

    image_data_set_instance = image_data_set()


However, as that method results in very long paths, "image data set" (as well as all other terms) can be imported directly from the ontology graph using the following:

.. code-block:: python

    from pathology_image_features.ontology import PIFO

    image_data_set = PIFO.to_python('http://purl.obolibrary.org/obo/OBI_0003327')
    image_data_set_instance = image_data_set()


Adding Instance attributes
--------------------------

Properties can be added to certain classes using the following equivalent methods:

.. code-block:: python

    from pathology_image_features.ontology import PIFO

    image_data_set = PIFO.to_python('http://purl.obolibrary.org/obo/OBI_0003327')

    # Method 1: As keyword arguments on initialization
    image_data_set_instance = image_data_set(has_filepath = '/path/to/slide.svs')
    # Method 2: As attributes after initialization
    image_data_set_instance = image_data_set()
    image_data_set_instance.has_filepath = '/path/to/slide.svs'
    # Method 3: Using a construct method (implemented for some classes)
    image_data_set_instance = image_data_set.construct(filepath = '/path/to/slide.svs')

Construct methods enable the "construction" of properties linking instances typically described in a Python data type serialization. For example,
to fully define a "FeatureCollection" (GEO:FeatureCollection) you could use the following:

.. code-block:: python

    from pathology_image_features.ontology import PIFO

    FeatureCollection = PIFO.to_python('http://www.opengis.net/ont/geosparql#FeatureCollection')
    Feature = PIFO.to_python('http://www.opengis.net/ont/geosparql#Feature')
    Geometry = PIFO.to_python('http://www.opengis.net/ont/geosparql#Geometry')

    feature_collection_instance = FeatureCollection()
    for feature in feature_collection_dict['features']:
        geometry_instance = Geometry(asGeoJSON = json.dumps(feature['geometry']))
        feature_instance = Feature(hasGeometry = geometry_instance, member = feature_collection_instance)

    # OR:
    feature_collection_instance = FeatureCollection.construct(feature_collection_dict)

Dynamic Subclass creation
--------------------------

Any time you are working with an object which could more accurately be described as a "type of" some base class rather than an "instance of" some base class, it might be more appropriate to use a subclass of that class.

Creating a subclass of a term in *pathology-image-features* is as simple as subclassing the base class and adding any additional properties or methods you need.

For example, this is how you would create a new "type of" image data set:

.. code-block:: python

    class MyImageDataSet(image_data_set):
        label = "My Image Data Set"
        definition = "A custom type of image data set"

        def custom_method(self, *args,**kwargs):
            # This is a new method not present in the base class
            pass

        @classmethod
        def construct(cls, *args, **kwargs):
            # This is a new method which overrides an existing method in the base class
            return super().construct(*args, **kwargs)


    # OR using types.new_class:
    import types
    my_image_data_set = types.new_class(name = "MyImageDataSet", bases = (image_data_set,))


Data Transformations
--------------------

One of the primary uses of *pathology-image-features* is the creation of interoperable computational pathology pipelines. These are built off subclasses of the "data transformation" (OBI:0200000) class and the "image data set analysis" (OBI:0003355) class.

While some of the methods which are included in the "data transformation" class are required in order to enable downstream functionality, the following methods may be updated in order to implement custom behavior:

For example, the following code would be an implementation of a data transformation which accesses the "
" of an input, adds 2, and returns a new "value specification" instance.

.. code-block:: python

    class Add2toNumber(data_transformation):
        label = "Add 2 to Number"
        definition = "This process adds 2 to the specified_numeric_value of an input data entity."

        def _pull(self, input_data_item):
            """The _pull method is used to access data of an item stored in some property.
            """
            return input_data_item.has_specified_numeric_value

        def _run(self, input_data_item):
            """"The _run method is where the primary functionality of the data transformation is implemented.
            """
            input_value = self._pull(input_data_item)
            output = input_value + 2
            return output

        def _cast_output(self, input, output, output_dir):
            """The _cast_output method is used to create a new data item from the output of the _run method.
            """

            # Adding properties to the new value specification instance
            output_value_spec = value_specification(
                has_specified_numeric_value = output, # This is the output of the _run method
                is_specified_output_of = self, # This specifies that this new value specification is the output of this process.
                is_about = input, # This specifies that this new value specification is about the input data item.
            )

            # Serializing the new value specification to the output directory if required.
            namespace, name = PIFO.split_uri(output_value_spec.iri)
            output_value_spec.serialize(filepath = os.path.join(output_dir, f'{name}.ttl'))

            return output_value_spec


As a further example, the following math equation can be implemented as a data transformation:

- Output = ((Input + 2) ** 2) / 3

.. code-block:: python

    # Creating a parent class of numeric operations, subclasses only need to update the _run method
    class NumericOperation(data_transformation):
        label = 'Numeric Operation'
        definition = "Parent class of mathematical operations performed on numeric values."

        def _pull(self, input_data_item):
            return self.has_specified_numeric_value

        def _cast_output(self, input, output, output_dir):
            output_value_spec = value_specification(
                has_specified_numeric_value = output,
                is_specified_output_of = self,
                is_about = input,
            )

            # Serializing the new value specification to the output directory if required.
            namespace, name = PIFO.split_uri(output_value_spec.iri)
            output_value_spec.serialize(filepath = os.path.join(output_dir, f'{name}.ttl'))

            return output_value_spec

    class Add2toNumber(NumericOperation):
        label = 'Add 2 to Number'
        definition = 'This process adds 2 to the specified_numeric_value of an input data entity.'

        def _run(self, input):
            input = self._pull(input)
            return input + 2

    class SquareOfNumber(NumericOperation):
        label = 'Square of Number'
        definition = 'This process squares the specified_numeric_value of an input data entity.'

        def _run(self, input):
            input = self._pull(input)
            return input ** 2

    class DivideBy3(NumericOperation):
        label = 'Divide by 3'
        definition = 'This process divides the specified_numeric_value of an input data entity by 3.'

        def _run(self, input):
            input = self._pull(input)
            return input / 3

    # By default, transformations part order is assumed to be the order in which they are provided (no PEMDAS here!)
    combined_transformation = data_transformation(
        has_part = [
            Add2toNumber(),
            SquareOfNumber(),
            DivideBy3(),
        ]
    )

    # Alternatively, you can specify the order of operations using the 'preceded by' property.
    add_2_to_number = Add2toNumber()
    square_of_number = SquareOfNumber(preceded_by = add_2_to_number)
    divide_by_3 = DivideBy3(preceded_by = square_of_number)

    # Shuffling has no effect on the output.
    from numpy import random
    combined_transformation = data_transformation(
        has_part = random.shuffle([
            add_2_to_number,
            square_of_number,
            divide_by_3,
        ])
    )
