From 7a8e2036f60a12da1f03da2e5b38e7ea62e0be2b Mon Sep 17 00:00:00 2001
From: Adam Paquette <acpaquette@usgs.gov>
Date: Tue, 11 Dec 2018 14:18:45 -0700
Subject: [PATCH] Adds stripped down version of pfeffernusse

---
 .gitignore                                    |  24 +
 minipf/__init__.py                            |   0
 minipf/config.py                              |   9 +
 minipf/controllers/__init__.py                |   0
 minipf/controllers/default_controller.py      |  56 ++
 minipf/controllers/util.py                    |   0
 minipf/drivers/__init__.py                    |  31 +
 minipf/drivers/base.py                        | 391 +++++++++
 minipf/drivers/cassini_driver.py              |  69 ++
 minipf/drivers/distortion.py                  |  25 +
 minipf/drivers/lro_driver.py                  |  38 +
 minipf/drivers/mdis_driver.py                 |  58 ++
 minipf/drivers/mro_driver.py                  |  37 +
 minipf/models/__init__.py                     |  19 +
 minipf/models/base_model_.py                  |  69 ++
 minipf/models/data.py                         |  90 ++
 minipf/models/isd200.py                       | 820 ++++++++++++++++++
 minipf/models/isd200_detector_center.py       |  94 ++
 minipf/models/isd200_focal_length_model.py    | 124 +++
 minipf/models/isd200_radii.py                 | 120 +++
 minipf/models/isd200_reference_height.py      | 124 +++
 minipf/models/isd200_sensor_position.py       | 117 +++
 minipf/models/isd200_sun_position.py          | 117 +++
 minipf/models/optical_distortion.py           |  92 ++
 minipf/models/optical_distortion_radial.py    |  64 ++
 .../models/optical_distortion_transverse.py   |  90 ++
 minipf/models/quaternion.py                   |  37 +
 minipf/models/quaternions.py                  |  68 ++
 minipf/models/request_isd.py                  |  64 ++
 minipf/models/xyz.py                          |  37 +
 minipf/util.py                                | 215 +++++
 setup.py                                      |  27 +
 32 files changed, 3126 insertions(+)
 create mode 100644 minipf/__init__.py
 create mode 100644 minipf/config.py
 create mode 100644 minipf/controllers/__init__.py
 create mode 100644 minipf/controllers/default_controller.py
 create mode 100644 minipf/controllers/util.py
 create mode 100644 minipf/drivers/__init__.py
 create mode 100644 minipf/drivers/base.py
 create mode 100644 minipf/drivers/cassini_driver.py
 create mode 100644 minipf/drivers/distortion.py
 create mode 100644 minipf/drivers/lro_driver.py
 create mode 100644 minipf/drivers/mdis_driver.py
 create mode 100644 minipf/drivers/mro_driver.py
 create mode 100644 minipf/models/__init__.py
 create mode 100644 minipf/models/base_model_.py
 create mode 100644 minipf/models/data.py
 create mode 100644 minipf/models/isd200.py
 create mode 100644 minipf/models/isd200_detector_center.py
 create mode 100644 minipf/models/isd200_focal_length_model.py
 create mode 100644 minipf/models/isd200_radii.py
 create mode 100644 minipf/models/isd200_reference_height.py
 create mode 100644 minipf/models/isd200_sensor_position.py
 create mode 100644 minipf/models/isd200_sun_position.py
 create mode 100644 minipf/models/optical_distortion.py
 create mode 100644 minipf/models/optical_distortion_radial.py
 create mode 100644 minipf/models/optical_distortion_transverse.py
 create mode 100644 minipf/models/quaternion.py
 create mode 100644 minipf/models/quaternions.py
 create mode 100644 minipf/models/request_isd.py
 create mode 100644 minipf/models/xyz.py
 create mode 100644 minipf/util.py
 create mode 100644 setup.py

diff --git a/.gitignore b/.gitignore
index 28a356b..189b0c2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,30 @@
 # Prerequisites
 *.d
 
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
 # Compiled Object files
 *.slo
 *.lo
diff --git a/minipf/__init__.py b/minipf/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/minipf/config.py b/minipf/config.py
new file mode 100644
index 0000000..33f9395
--- /dev/null
+++ b/minipf/config.py
@@ -0,0 +1,9 @@
+"""
+Config File
+"""
+
+# Directory with metakernals
+spice_root = "/data/spice/"
+cassini = '/data/big/spice/co-s_j_e_v-spice-6-v1.0/cosp_1000/extras/mk'
+mdis = '/data/spice/mess-e_v_h-spice-6-v1.0/messsp_1000/extras/mk' # Messenger
+mro = '/data/spice/mro-m-spice-6-v1.0/mrosp_1000/extras/mk' # Mars Reconnaissance Orbiter
diff --git a/minipf/controllers/__init__.py b/minipf/controllers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/minipf/controllers/default_controller.py b/minipf/controllers/default_controller.py
new file mode 100644
index 0000000..c536b23
--- /dev/null
+++ b/minipf/controllers/default_controller.py
@@ -0,0 +1,56 @@
+# import connexion
+# import six
+
+# from minipf.models.data import Data  # noqa: E501
+# from minipf.models.isd200 import ISD200  # noqa: E501
+# from minipf.models.request_isd import RequestISD  # noqa: E501
+from minipf import util
+from minipf import drivers
+
+def create_isd(lbl_file):  # noqa: E501
+    """Converts Image Labels to ISDs
+
+    Adds an item to the system # noqa: E501
+
+    :param request_isd:
+    :type request_isd: dict | bytes
+
+    :rtype: ISD200
+    """
+    with open(lbl_file, "r") as fp:
+        lines = fp.read()
+
+    return drivers.load(lines)
+
+def get_metakernel(mission, year, version):  # noqa: E501
+    """Get a specific kernel
+
+     # noqa: E501
+
+    :param mission:
+    :type mission: str
+    :param year:
+    :type year: str
+    :param version:
+    :type version: str
+
+    :rtype: Data
+    """
+    if connexion.request.is_json:
+        request_isd = RequestISD.from_dict(connexion.request.get_json())  # noqa: E501
+
+    return util.get_metakernels(missions=mission, years=year, versions=version)
+
+
+def metakernel_catalog():  # noqa: E501
+    """Access Product Information
+
+    Get Available Products and Related Metadata # noqa: E501
+
+
+    :rtype: Data
+    """
+    return 'do some magic!'
+
+if __name__ == "__main__":
+    print("Banana")
diff --git a/minipf/controllers/util.py b/minipf/controllers/util.py
new file mode 100644
index 0000000..e69de29
diff --git a/minipf/drivers/__init__.py b/minipf/drivers/__init__.py
new file mode 100644
index 0000000..137ebaf
--- /dev/null
+++ b/minipf/drivers/__init__.py
@@ -0,0 +1,31 @@
+import pvl
+import zlib
+
+import importlib
+import inspect
+import itertools
+from itertools import chain
+import os
+from glob import glob
+
+from abc import ABC
+
+# dynamically load drivers
+__all__ = [os.path.splitext(os.path.basename(d))[0] for d in glob(os.path.join(os.path.dirname(__file__), '*_driver.py'))]
+__driver_modules__ = [importlib.import_module('.'+m, package='minipf.drivers') for m in __all__]
+
+drivers = dict(chain.from_iterable(inspect.getmembers(dmod, lambda x: inspect.isclass(x) and "_driver" in x.__module__) for dmod in __driver_modules__))
+
+def load(label):
+    for name, driver in drivers.items():
+        try:
+            print("TRYING:", driver)
+            res = driver(label)
+            if res.is_valid():
+                with res as r:
+                    return res
+
+        except Exception as e:
+            import traceback
+            traceback.print_exc()
+    raise Exception('No Such Driver for Label')
diff --git a/minipf/drivers/base.py b/minipf/drivers/base.py
new file mode 100644
index 0000000..92a270e
--- /dev/null
+++ b/minipf/drivers/base.py
@@ -0,0 +1,391 @@
+from abc import ABC, abstractmethod
+
+from dateutil import parser
+import numpy as np
+import pvl
+import spiceypy as spice
+
+from minipf.drivers import distortion
+from minipf.models.isd200 import ISD200
+
+class Base(ABC):
+    """
+    Abstract base class for all PDS label parsing. Implementations should override
+    properties where a kernel provider deviates from the most broadly adopted
+    approach.
+
+    Methods that must be provided:
+    - instrument_id
+    - metakernel
+
+    """
+    def __init__(self, label, *args, **kwargs):
+        self.label = pvl.loads(label)
+
+    def __enter__(self):
+        """
+        Called when the context is created. This is used
+        to get the kernels furnished.
+        """
+        print("IN FURNISH")
+        if self.metakernel:
+            spice.furnsh(self.metakernel)
+        return self
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """
+        Called when the context goes out of scope. Once
+        this is done, the object is out of scope and the
+        kernels can be unloaded.
+        """
+        spice.unload(self.metakernel)
+
+    def __str__(self):
+        return str(self.to_dict())
+
+    def is_valid(self):
+        try:
+            iid = self.instrument_id
+            return True
+        except Exception as e:
+            return False
+
+    def to_dict(self):
+        return {p:getattr(self, p) for p in dir(self) if not p.startswith('__') or p.startswith('_')}
+
+    def to_pfeffer_response(self):
+        """
+        Parse the data into a valid minipf response
+        """
+        data = self.to_dict()
+        # Take the flat reponse and create the minipf obj dicts
+        data['detector_center'] = {'line': data['detector_center'][0],
+                                'sample': data['detector_center'][1]}
+
+        # Parse the distortion object out of the
+
+        if isinstance(self, distortion.RadialDistortion):
+            data['optical_distortion'] = {'radial':{'coefficients':data['odtk']}}
+        elif isinstance(self, distortion.TransverseDistortion):
+            data['optical_distortion'] = {'transverse':{'x':data['odtx'],
+                                                        'y':data['odty']}}
+
+        data['focal_length_model'] = {'focal_length': data['focal_length']}
+        if hasattr(self, 'focal_epsilon'):
+            data['focal_length_model']['focal_epsilon'] = data['focal_epsilon']
+
+        data['reference_height'] = {'minheight': data['reference_height'][0],
+                                    'maxheight': data['reference_height'][1],
+                                    'unit': 'm'}
+
+        data['sensor_position'] = {'unit':'m',
+                                   'velocities': data['sensor_velocity'],
+                                   'positions': data['sensor_position']}
+
+        data['sun_position'] = {'unit': 'm',
+                                'positions': data['sun_position'],
+                                'velocities': data['sun_velocity']}
+
+        data['sensor_orientation'] = {'quaternions':data['sensor_orientation']}
+
+        data['radii'] = {'semimajor':data['semimajor'],
+                         'semiminor':data['semiminor'],
+                         'unit': 'km'}
+
+        data['name_platform'] = data['spacecraft_name']
+        data['name_sensor'] = data['instrument_id']
+        return ISD200.from_dict(data)
+
+    def _compute_ephemerides(self):
+        """
+        Helper function to pull position and velocity in one pass
+        so that the results can then be cached in the associated
+        properties.
+        """
+        eph = np.empty((self.number_of_ephemerides, 3))
+        eph_rates = np.empty(eph.shape)
+        current_et = self.starting_ephemeris_time
+        for i in range(self.number_of_ephemerides):
+            state, _ = spice.spkezr(self.spacecraft_name,
+                                    current_et,
+                                    self.reference_frame,
+                                    'NONE',
+                                    self.target_name,) # If this is the sensor, insufficient, if this is the spacecraft, it works? Huh?
+            eph[i] = state[:3]
+            eph_rates[i] = state[3:]
+            current_et += getattr(self, 'dt_ephemeris', 0)
+        # By default, spice works in km
+        eph *= 1000
+        eph_rates *= 1000
+        self._sensor_velocity = eph_rates
+        self._sensor_position = eph
+
+
+    @property
+    @abstractmethod
+    def metakernel(self):
+        pass
+
+    @property
+    @abstractmethod
+    def instrument_id(self):
+        pass
+
+
+    @property
+    def start_time(self):
+        return self.label['START_TIME']
+
+    @property
+    def image_lines(self):
+        return self.label['IMAGE']['LINES']
+
+    @property
+    def image_samples(self):
+        return self.label['IMAGE']['LINE_SAMPLES']
+
+    @property
+    def interpolation_method(self):
+        return 'lagrange'
+
+    @property
+    def number_of_ephemerides(self):
+        return 1
+
+    @property
+    def target_name(self):
+        return self.label['TARGET_NAME']
+
+    @property
+    def _target_id(self):
+        return spice.bodn2c(self.label['TARGET_NAME'])
+
+    @property
+    def starting_ephemeris_time(self):
+        if not hasattr(self, '_starting_ephemeris_time'):
+            sclock = self.label['SPACECRAFT_CLOCK_START_COUNT']
+            self._starting_ephemeris_time = spice.scs2e(self.spacecraft_id, sclock)
+        return self._starting_ephemeris_time
+
+    @property
+    def _exposure_duration(self):
+        return self.label['EXPOSURE_DURATION'].value * 0.001  # Scale to seconds
+
+    @property
+    def spacecraft_clock_stop_count(self):
+        sc = self.label.get('SPACECRAFT_CLOCK_STOP_COUNT', None)
+        if sc == 'N/A':
+            sc = None
+        return sc
+
+    @property
+    def ending_ephemeris_time(self):
+        if not hasattr(self, '_ending_ephemeris_time'):
+            self._ending_ephemeris_time = (self.image_lines * self._exposure_duration) + self.starting_ephemeris_time
+        return self._ending_ephemeris_time
+
+    @property
+    def center_ephemeris_time(self):
+        if not hasattr(self, '_center_ephemeris_time'):
+            self._center_ephemeris_time = (self.starting_ephemeris_time + self.ending_ephemeris_time)/2
+        return self._center_ephemeris_time
+
+    @property
+    def detector_center(self):
+        return list(spice.gdpool('INS{}_CCD_CENTER'.format(self.ikid), 0, 2))
+
+    @property
+    def spacecraft_name(self):
+        return self.label['MISSION_NAME']
+
+    @property
+    def ikid(self):
+        return spice.bods2c(self.instrument_id)
+
+    @property
+    def fikid(self):
+        fn = self.label.get('FILTER_NUMBER', 0)
+        if fn == 'N/A':
+            fn = 0
+        return self.ikid - int(fn)
+
+    @property
+    def spacecraft_id(self):
+        return spice.bods2c(self.spacecraft_name)
+
+    @property
+    def focal2pixel_lines(self):
+        return spice.gdpool('INS{}_ITRANSL'.format(self.fikid), 0, 3)
+
+    @property
+    def focal2pixel_samples(self):
+        return spice.gdpool('INS{}_ITRANSS'.format(self.fikid), 0, 3)
+
+    @property
+    def focal_length(self):
+        return float(spice.gdpool('INS{}_FOCAL_LENGTH'.format(self.ikid), 0, 1)[0])
+
+    @property
+    def starting_detector_line(self):
+        return 1
+
+    @property
+    def starting_detector_sample(self):
+        return 1
+
+    @property
+    def detector_sample_summing(self):
+        return 1
+
+    @property
+    def detector_line_summing(self):
+        return self.label.get('SAMPLING_FACTOR', 1)
+
+    @property
+    def semimajor(self):
+        rad = spice.bodvrd(self.label['TARGET_NAME'], 'RADII', 3)
+        return rad[1][1]
+
+    @property
+    def semiminor(self):
+        rad = spice.bodvrd(self.label['TARGET_NAME'], 'RADII', 3)
+        return rad[1][0]
+
+    @property
+    def reference_frame(self):
+        return 'IAU_{}'.format(self.label['TARGET_NAME'])
+
+    @property
+    def sun_position(self):
+        sun_state, _ = spice.spkezr("SUN",
+                                     self.center_ephemeris_time,
+                                     self.reference_frame,
+                                     'NONE',
+                                     self.label['TARGET_NAME'])
+
+        return [sun_state[:4].tolist()]
+
+    @property
+    def sun_velocity(self):
+        sun_state, lt = spice.spkezr("SUN",
+                                     self.center_ephemeris_time,
+                                     self.reference_frame,
+                                     'NONE',
+                                     self.label['TARGET_NAME'])
+
+        return [sun_state[3:6].tolist()]
+
+    @property
+    def sensor_position(self):
+        if not hasattr(self, '_sensor_position'):
+            self._compute_ephemerides()
+        return self._sensor_position.tolist()
+
+    @property
+    def sensor_velocity(self):
+        if not hasattr(self, '_sensor_velocity'):
+            self._compute_ephemerides()
+        return self._sensor_velocity.tolist()
+
+    @property
+    def sensor_orientation(self):
+        if not hasattr(self, '_sensor_orientation'):
+            current_et = self.starting_ephemeris_time
+            qua = np.empty((self.number_of_ephemerides, 4))
+            for i in range(self.number_of_quaternions):
+                # Find the rotation matrix
+                camera2bodyfixed = spice.pxform(self.instrument_id,
+                                                self.reference_frame,
+                                                current_et)
+                q = spice.m2q(camera2bodyfixed)
+                qua[i,:3] = q[1:]
+                qua[i,3] = q[0]
+                current_et += getattr(self, 'dt_quaternion', 0)
+            self._sensor_orientation = qua
+        return self._sensor_orientation.tolist()
+
+    @property
+    def reference_height(self):
+        # TODO: This should be a reasonable #
+        return 0, 100
+
+class LineScanner(Base):
+
+    @property
+    def name_model(self):
+        return "USGS_ASTRO_LINE_SCANNER_SENSOR_MODEL"
+
+    @property
+    def _exposure_duration(self):
+        return self.label['LINE_EXPOSURE_DURATION'].value * 0.001  # Scale to seconds
+
+    @property
+    def line_scan_rate(self):
+        """
+        In the form: [start_line, line_time, exposure_duration]
+        The form below is for a fixed rate line scanner.
+        """
+        return [[float(self.starting_detector_line), self.t0_ephemeris, self._exposure_duration]]
+
+    @property
+    def detector_center(self):
+        if not hasattr(self, '_detector_center'):
+            center_line = float(spice.gdpool('INS{}_BORESIGHT_LINE'.format(self.ikid), 0, 1)[0])
+            center_sample = float(spice.gdpool('INS{}_BORESIGHT_SAMPLE'.format(self.ikid), 0, 1)[0])
+            self._detector_center = [center_line, center_sample]
+        return self._detector_center
+
+    @property
+    def center_ephemeris_time(self):
+        """
+        The center ephemeris time for a fixed rate line scanner.
+        """
+        if not hasattr(self, '_center_ephemeris_time'):
+            halflines = self.image_lines / 2
+            center_sclock = self.starting_ephemeris_time + halflines * self._exposure_duration
+            self._center_ephemeris_time = center_sclock
+        return self._center_ephemeris_time
+
+    @property
+    def t0_ephemeris(self):
+        return self.starting_ephemeris_time - self.center_ephemeris_time
+
+    @property
+    def t0_quaternion(self):
+        return self.starting_ephemeris_time - self.center_ephemeris_time
+
+    @property
+    def dt_ephemeris(self):
+        return (self.ending_ephemeris_time - self.starting_ephemeris_time) / self.number_of_ephemerides
+
+    @property
+    def dt_quaternion(self):
+        return (self.ending_ephemeris_time - self.starting_ephemeris_time) / self.number_of_ephemerides
+
+    @property
+    def number_of_ephemerides(self):
+        return 909
+
+    @property
+    def number_of_quaternions(self):
+        return 909
+
+
+
+    @property
+    def _exposure_duration(self):
+        return self.label['LINE_EXPOSURE_DURATION'].value * 0.001  # Scale to seconds
+
+class Framer(Base):
+
+    @property
+    def name_model(self):
+        return "USGS_ASTRO_FRAME_SENSOR_MODEL"
+
+    @property
+    def number_of_ephemerides(self):
+        return 1
+
+    @property
+    def number_of_quaternions(self):
+        return 1
diff --git a/minipf/drivers/cassini_driver.py b/minipf/drivers/cassini_driver.py
new file mode 100644
index 0000000..600187f
--- /dev/null
+++ b/minipf/drivers/cassini_driver.py
@@ -0,0 +1,69 @@
+from glob import glob
+import os
+
+import pvl
+import spiceypy as spice
+import numpy as np
+
+from minipf import config
+from minipf.drivers.base import Framer
+from minipf.drivers.distortion import RadialDistortion
+
+
+class CassiniISS(Framer, RadialDistortion):
+    id_lookup = {
+        "ISSNA" : "CASSINI_ISS_NAC",
+        "ISSWA" : "CASSINI_ISS_WAC"
+    }
+
+    @property
+    def metakernel(self):
+        metakernel_dir = config.cassini
+        mks = sorted(glob(os.path.join(metakernel_dir,'*.tm')))
+        if not hasattr(self, '_metakernel'):
+            for mk in mks:
+                if str(self.start_time.year) in os.path.basename(mk):
+                    self._metakernel = mk
+        return self._metakernel
+
+    @property
+    def instrument_id(self):
+        return self.id_lookup[self.label['INSTRUMENT_ID']]
+
+    @property
+    def focal_epsilon(self):
+        return float(spice.gdpool('INS{}_FL_UNCERTAINTY'.format(self.ikid), 0, 1)[0])
+
+    @property
+    def spacecraft_name(self):
+        return 'CASSINI'
+
+    @property
+    def focal2pixel_samples(self):
+        # Microns to mm
+        pixel_size = spice.gdpool('INS{}_PIXEL_SIZE'.format(self.ikid), 0, 1)[0] * 0.001
+        return [0.0, 1/pixel_size, 0.0]
+
+    @property
+    def focal2pixel_lines(self):
+        pixel_size = spice.gdpool('INS{}_PIXEL_SIZE'.format(self.ikid), 0, 1)[0] * 0.001
+        return [0.0, 0.0, 1/pixel_size]
+
+    @property
+    def _exposure_duration(self):
+        # labels do not specify a unit explicitly
+        return self.label['EXPOSURE_DURATION'] * 0.001  # Scale to seconds
+
+    @property
+    def odtk(self):
+        """
+        The radial distortion coeffs are not defined in the ik kernels, instead
+        they are defined in the ISS Data User Guide (Knowles). Therefore, we
+        manually specify the codes here.
+        """
+        if self.instrument_id == 'CASSINI_ISS_WAC':
+            # WAC
+            return [float('-6.2e-5'), 0, 0]
+        elif self.instrument_id == 'CASSINI_ISS_NAC':
+            # NAC
+            return [float('-8e-6'), 0, 0]
diff --git a/minipf/drivers/distortion.py b/minipf/drivers/distortion.py
new file mode 100644
index 0000000..b55c06e
--- /dev/null
+++ b/minipf/drivers/distortion.py
@@ -0,0 +1,25 @@
+from abc import ABC
+import spiceypy as spice
+
+from minipf.models.optical_distortion_transverse import OpticalDistortionTransverse
+
+
+class TransverseDistortion(ABC):
+    """
+    Exposes the properties that are used to describe a transverse distortion model.
+    """
+    @property
+    def odtx(self):
+        return spice.gdpool('INS{}_OD_T_X'.format(self.ikid),0, 10)
+    
+    @property 
+    def odty(self):
+        return spice.gdpool('INS{}_OD_T_Y'.format(self.ikid), 0, 10)
+
+class RadialDistortion(ABC):
+    """
+    Exposes the properties that are used to describe a radial distortion model.
+    """
+    @property
+    def odtk(self):
+        return spice.gdpool('INS{}_OD_K'.format(self.ikid),0, 3)
\ No newline at end of file
diff --git a/minipf/drivers/lro_driver.py b/minipf/drivers/lro_driver.py
new file mode 100644
index 0000000..a3043ff
--- /dev/null
+++ b/minipf/drivers/lro_driver.py
@@ -0,0 +1,38 @@
+from glob import glob
+import os
+
+import numpy as np
+import pvl
+import spiceypy as spice
+
+from minipf.util import get_metakernels
+from minipf.drivers.base import LineScanner
+from minipf.drivers.distortion import RadialDistortion
+
+class LRO_LROC(LineScanner, RadialDistortion):
+
+    @property
+    def metakernel(self):
+        metakernels = get_metakernels(years=self.start_time.year, missions='lro', versions='latest')
+        self._metakernel = metakernels['data'][0]['path']
+        return self._metakernel
+
+    @property
+    def instrument_id(self):
+        """
+        Ignores Wide Angle for now
+        """
+
+        instrument = self.label.get("INSTRUMENT_ID")
+
+        # should be left or right
+        frame_id = self.label.get("FRAME_ID")
+
+        if instrument == "LROC" and frame_id == "LEFT":
+            return "LRO_LROCNACL"
+        elif instrument == "LROC" and frame_id == "RIGHT":
+            return "LRO_LROCNACR"
+
+    @property
+    def spacecraft_name(self):
+        return "LRO"
diff --git a/minipf/drivers/mdis_driver.py b/minipf/drivers/mdis_driver.py
new file mode 100644
index 0000000..b014622
--- /dev/null
+++ b/minipf/drivers/mdis_driver.py
@@ -0,0 +1,58 @@
+from glob import glob
+import os
+
+import pvl
+import spiceypy as spice
+import numpy as np
+
+from minipf import config
+from minipf.drivers.base import Framer
+from minipf.drivers.distortion import TransverseDistortion
+
+
+class Messenger(Framer, TransverseDistortion):
+    id_lookup = {
+        'MDIS-WAC': 'MSGR_MDIS_WAC',
+        'MDIS-NAC':'MSGR_MDIS_NAC',
+        'MERCURY DUAL IMAGING SYSTEM NARROW ANGLE CAMERA':'MSGR_MDIS_NAC',
+        'MERCURY DUAL IMAGING SYSTEM WIDE ANGLE CAMERA':'MSGR_MDIS_WAC'
+    }
+
+    @property
+    def metakernel(self):
+        metakernel_dir = config.mdis
+        mks = sorted(glob(os.path.join(metakernel_dir,'*.tm')))
+        if not hasattr(self, '_metakernel'):
+            for mk in mks:
+                if str(self.start_time.year) in os.path.basename(mk):
+                    self._metakernel = mk
+        return self._metakernel
+
+    @property
+    def instrument_id(self):
+        return self.id_lookup[self.label['INSTRUMENT_ID']]
+
+    @property
+    def focal_length(self):
+        """
+        """
+        coeffs = spice.gdpool('INS{}_FL_TEMP_COEFFS '.format(self.fikid), 0, 5)
+
+        # reverse coeffs, mdis coeffs are listed a_0, a_1, a_2 ... a_n where
+        # numpy wants them a_n, a_n-1, a_n-2 ... a_0
+        f_t = np.poly1d(coeffs[::-1])
+
+        # eval at the focal_plane_tempature
+        return f_t(self.label['FOCAL_PLANE_TEMPERATURE'].value)
+
+    @property 
+    def focal_epsilon(self):
+        return float(spice.gdpool('INS{}_FL_UNCERTAINTY'.format(self.ikid), 0, 1)[0])
+
+    @property
+    def starting_detector_sample(self):
+        return int(spice.gdpool('INS{}_FPUBIN_START_SAMPLE'.format(self.ikid), 0, 1)[0])
+    
+    @property
+    def starting_detector_line(self):
+        return int(spice.gdpool('INS{}_FPUBIN_START_LINE'.format(self.ikid), 0, 1)[0])
diff --git a/minipf/drivers/mro_driver.py b/minipf/drivers/mro_driver.py
new file mode 100644
index 0000000..c3a6754
--- /dev/null
+++ b/minipf/drivers/mro_driver.py
@@ -0,0 +1,37 @@
+from glob import glob
+import os
+
+import numpy as np
+import pvl
+import spiceypy as spice
+from minipf import config
+
+from minipf.drivers.base import LineScanner
+from minipf.drivers.distortion import RadialDistortion
+
+class MRO_CTX(LineScanner, RadialDistortion):
+    id_lookup = {
+            'CONTEXT CAMERA':'MRO_CTX'
+    }
+
+    @property
+    def metakernel(self):
+        metakernel_dir = config.mro
+        mks = sorted(glob(os.path.join(metakernel_dir,'*.tm')))
+        if not hasattr(self, '_metakernel'):
+            self._metakernel = None
+            for mk in mks:
+                if str(self.start_time.year) in os.path.basename(mk):
+                    self._metakernel = mk
+        return self._metakernel
+
+    @property
+    def instrument_id(self):
+        return self.id_lookup[self.label['INSTRUMENT_NAME']]
+    
+    @property
+    def spacecraft_name(self):
+        name_lookup = {
+            'MARS_RECONNAISSANCE_ORBITER': 'MRO'
+        }
+        return name_lookup[self.label['SPACECRAFT_NAME']]
diff --git a/minipf/models/__init__.py b/minipf/models/__init__.py
new file mode 100644
index 0000000..c1b09b0
--- /dev/null
+++ b/minipf/models/__init__.py
@@ -0,0 +1,19 @@
+# coding: utf-8
+
+# flake8: noqa
+from __future__ import absolute_import
+# import models into model package
+from minipf.models.data import Data
+from minipf.models.isd200 import ISD200
+from minipf.models.isd200_detector_center import ISD200DetectorCenter
+from minipf.models.isd200_focal_length_model import ISD200FocalLengthModel
+from minipf.models.isd200_radii import ISD200Radii
+from minipf.models.isd200_reference_height import ISD200ReferenceHeight
+from minipf.models.isd200_sensor_position import ISD200SensorPosition
+from minipf.models.isd200_sun_position import ISD200SunPosition
+from minipf.models.optical_distortion import OpticalDistortion
+from minipf.models.optical_distortion_radial import OpticalDistortionRadial
+from minipf.models.optical_distortion_transverse import OpticalDistortionTransverse
+from minipf.models.quaternions import Quaternions
+from minipf.models.request_isd import RequestISD
+from minipf.models.xyz import XYZ
diff --git a/minipf/models/base_model_.py b/minipf/models/base_model_.py
new file mode 100644
index 0000000..6535006
--- /dev/null
+++ b/minipf/models/base_model_.py
@@ -0,0 +1,69 @@
+import pprint
+
+import six
+import typing
+
+from minipf import util
+
+T = typing.TypeVar('T')
+
+
+class Model(object):
+    # openapiTypes: The key is attribute name and the
+    # value is attribute type.
+    openapi_types = {}
+
+    # attributeMap: The key is attribute name and the
+    # value is json key in definition.
+    attribute_map = {}
+
+    @classmethod
+    def from_dict(cls: typing.Type[T], dikt) -> T:
+        """Returns the dict as a model"""
+        return util.deserialize_model(dikt, cls)
+
+    def to_dict(self):
+        """Returns the model properties as a dict
+
+        :rtype: dict
+        """
+        result = {}
+
+        for attr, _ in six.iteritems(self.openapi_types):
+            value = getattr(self, attr)
+            if isinstance(value, list):
+                result[attr] = list(map(
+                    lambda x: x.to_dict() if hasattr(x, "to_dict") else x,
+                    value
+                ))
+            elif hasattr(value, "to_dict"):
+                result[attr] = value.to_dict()
+            elif isinstance(value, dict):
+                result[attr] = dict(map(
+                    lambda item: (item[0], item[1].to_dict())
+                    if hasattr(item[1], "to_dict") else item,
+                    value.items()
+                ))
+            else:
+                result[attr] = value
+
+        return result
+
+    def to_str(self):
+        """Returns the string representation of the model
+
+        :rtype: str
+        """
+        return pprint.pformat(self.to_dict())
+
+    def __repr__(self):
+        """For `print` and `pprint`"""
+        return self.to_str()
+
+    def __eq__(self, other):
+        """Returns true if both objects are equal"""
+        return self.__dict__ == other.__dict__
+
+    def __ne__(self, other):
+        """Returns true if both objects are not equal"""
+        return not self == other
diff --git a/minipf/models/data.py b/minipf/models/data.py
new file mode 100644
index 0000000..5ffae90
--- /dev/null
+++ b/minipf/models/data.py
@@ -0,0 +1,90 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class Data(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, count: int=None, data: List[object]=None):  # noqa: E501
+        """Data - a model defined in OpenAPI
+
+        :param count: The count of this Data.  # noqa: E501
+        :type count: int
+        :param data: The data of this Data.  # noqa: E501
+        :type data: List[object]
+        """
+        self.openapi_types = {
+            'count': int,
+            'data': List[object]
+        }
+
+        self.attribute_map = {
+            'count': 'count',
+            'data': 'data'
+        }
+
+        self._count = count
+        self._data = data
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'Data':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The data of this Data.  # noqa: E501
+        :rtype: Data
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def count(self) -> int:
+        """Gets the count of this Data.
+
+
+        :return: The count of this Data.
+        :rtype: int
+        """
+        return self._count
+
+    @count.setter
+    def count(self, count: int):
+        """Sets the count of this Data.
+
+
+        :param count: The count of this Data.
+        :type count: int
+        """
+
+        self._count = count
+
+    @property
+    def data(self) -> List[object]:
+        """Gets the data of this Data.
+
+
+        :return: The data of this Data.
+        :rtype: List[object]
+        """
+        return self._data
+
+    @data.setter
+    def data(self, data: List[object]):
+        """Sets the data of this Data.
+
+
+        :param data: The data of this Data.
+        :type data: List[object]
+        """
+
+        self._data = data
diff --git a/minipf/models/isd200.py b/minipf/models/isd200.py
new file mode 100644
index 0000000..51ad544
--- /dev/null
+++ b/minipf/models/isd200.py
@@ -0,0 +1,820 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf.models.isd200_detector_center import ISD200DetectorCenter  # noqa: F401,E501
+from minipf.models.isd200_focal_length_model import ISD200FocalLengthModel  # noqa: F401,E501
+from minipf.models.isd200_radii import ISD200Radii  # noqa: F401,E501
+from minipf.models.isd200_reference_height import ISD200ReferenceHeight  # noqa: F401,E501
+from minipf.models.isd200_sensor_position import ISD200SensorPosition  # noqa: F401,E501
+from minipf.models.isd200_sun_position import ISD200SunPosition  # noqa: F401,E501
+from minipf.models.optical_distortion import OpticalDistortion  # noqa: F401,E501
+from minipf.models.quaternions import Quaternions  # noqa: F401,E501
+from minipf import util
+
+
+class ISD200(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, name_platform: str=None, name_sensor: str=None, detector_center: ISD200DetectorCenter=None, center_ephemeris_time: float=None, name_model: str=None, starting_ephemeris_time: float=None, focal_length_model: ISD200FocalLengthModel=None, image_lines: float=None, image_samples: float=None, radii: ISD200Radii=None, optical_distortion: OpticalDistortion=None, starting_detector_sample: float=None, starting_detector_line: float=None, focal2pixel_samples: List[float]=None, focal2pixel_lines: List[float]=None, sensor_position: ISD200SensorPosition=None, sun_position: ISD200SunPosition=None, sensor_orientation: Quaternions=None, line_scan_rate: List[List[float]]=None, detector_sample_summing: float=None, detector_line_summing: float=None, dt_ephemeris: float=None, t0_ephemeris: float=None, dt_quaternion: float=None, t0_quaternion: float=None, reference_height: ISD200ReferenceHeight=None, interpolation_method: str=None):  # noqa: E501
+        """ISD200 - a model defined in OpenAPI
+
+        :param name_platform: The name_platform of this ISD200.  # noqa: E501
+        :type name_platform: str
+        :param name_sensor: The name_sensor of this ISD200.  # noqa: E501
+        :type name_sensor: str
+        :param detector_center: The detector_center of this ISD200.  # noqa: E501
+        :type detector_center: ISD200DetectorCenter
+        :param center_ephemeris_time: The center_ephemeris_time of this ISD200.  # noqa: E501
+        :type center_ephemeris_time: float
+        :param name_model: The name_model of this ISD200.  # noqa: E501
+        :type name_model: str
+        :param starting_ephemeris_time: The starting_ephemeris_time of this ISD200.  # noqa: E501
+        :type starting_ephemeris_time: float
+        :param focal_length_model: The focal_length_model of this ISD200.  # noqa: E501
+        :type focal_length_model: ISD200FocalLengthModel
+        :param image_lines: The image_lines of this ISD200.  # noqa: E501
+        :type image_lines: float
+        :param image_samples: The image_samples of this ISD200.  # noqa: E501
+        :type image_samples: float
+        :param radii: The radii of this ISD200.  # noqa: E501
+        :type radii: ISD200Radii
+        :param optical_distortion: The optical_distortion of this ISD200.  # noqa: E501
+        :type optical_distortion: OpticalDistortion
+        :param starting_detector_sample: The starting_detector_sample of this ISD200.  # noqa: E501
+        :type starting_detector_sample: float
+        :param starting_detector_line: The starting_detector_line of this ISD200.  # noqa: E501
+        :type starting_detector_line: float
+        :param focal2pixel_samples: The focal2pixel_samples of this ISD200.  # noqa: E501
+        :type focal2pixel_samples: List[float]
+        :param focal2pixel_lines: The focal2pixel_lines of this ISD200.  # noqa: E501
+        :type focal2pixel_lines: List[float]
+        :param sensor_position: The sensor_position of this ISD200.  # noqa: E501
+        :type sensor_position: ISD200SensorPosition
+        :param sun_position: The sun_position of this ISD200.  # noqa: E501
+        :type sun_position: ISD200SunPosition
+        :param sensor_orientation: The sensor_orientation of this ISD200.  # noqa: E501
+        :type sensor_orientation: Quaternions
+        :param line_scan_rate: The line_scan_rate of this ISD200.  # noqa: E501
+        :type line_scan_rate: List[List[float]]
+        :param detector_sample_summing: The detector_sample_summing of this ISD200.  # noqa: E501
+        :type detector_sample_summing: float
+        :param detector_line_summing: The detector_line_summing of this ISD200.  # noqa: E501
+        :type detector_line_summing: float
+        :param dt_ephemeris: The dt_ephemeris of this ISD200.  # noqa: E501
+        :type dt_ephemeris: float
+        :param t0_ephemeris: The t0_ephemeris of this ISD200.  # noqa: E501
+        :type t0_ephemeris: float
+        :param dt_quaternion: The dt_quaternion of this ISD200.  # noqa: E501
+        :type dt_quaternion: float
+        :param t0_quaternion: The t0_quaternion of this ISD200.  # noqa: E501
+        :type t0_quaternion: float
+        :param reference_height: The reference_height of this ISD200.  # noqa: E501
+        :type reference_height: ISD200ReferenceHeight
+        :param interpolation_method: The interpolation_method of this ISD200.  # noqa: E501
+        :type interpolation_method: str
+        """
+        self.openapi_types = {
+            'name_platform': str,
+            'name_sensor': str,
+            'detector_center': ISD200DetectorCenter,
+            'center_ephemeris_time': float,
+            'name_model': str,
+            'starting_ephemeris_time': float,
+            'focal_length_model': ISD200FocalLengthModel,
+            'image_lines': float,
+            'image_samples': float,
+            'radii': ISD200Radii,
+            'optical_distortion': OpticalDistortion,
+            'starting_detector_sample': float,
+            'starting_detector_line': float,
+            'focal2pixel_samples': List[float],
+            'focal2pixel_lines': List[float],
+            'sensor_position': ISD200SensorPosition,
+            'sun_position': ISD200SunPosition,
+            'sensor_orientation': Quaternions,
+            'line_scan_rate': List[List[float]],
+            'detector_sample_summing': float,
+            'detector_line_summing': float,
+            'dt_ephemeris': float,
+            't0_ephemeris': float,
+            'dt_quaternion': float,
+            't0_quaternion': float,
+            'reference_height': ISD200ReferenceHeight,
+            'interpolation_method': str
+        }
+
+        self.attribute_map = {
+            'name_platform': 'name_platform',
+            'name_sensor': 'name_sensor',
+            'detector_center': 'detector_center',
+            'center_ephemeris_time': 'center_ephemeris_time',
+            'name_model': 'name_model',
+            'starting_ephemeris_time': 'starting_ephemeris_time',
+            'focal_length_model': 'focal_length_model',
+            'image_lines': 'image_lines',
+            'image_samples': 'image_samples',
+            'radii': 'radii',
+            'optical_distortion': 'optical_distortion',
+            'starting_detector_sample': 'starting_detector_sample',
+            'starting_detector_line': 'starting_detector_line',
+            'focal2pixel_samples': 'focal2pixel_samples',
+            'focal2pixel_lines': 'focal2pixel_lines',
+            'sensor_position': 'sensor_position',
+            'sun_position': 'sun_position',
+            'sensor_orientation': 'sensor_orientation',
+            'line_scan_rate': 'line_scan_rate',
+            'detector_sample_summing': 'detector_sample_summing',
+            'detector_line_summing': 'detector_line_summing',
+            'dt_ephemeris': 'dt_ephemeris',
+            't0_ephemeris': 't0_ephemeris',
+            'dt_quaternion': 'dt_quaternion',
+            't0_quaternion': 't0_quaternion',
+            'reference_height': 'reference_height',
+            'interpolation_method': 'interpolation_method'
+        }
+
+        self._name_platform = name_platform
+        self._name_sensor = name_sensor
+        self._detector_center = detector_center
+        self._center_ephemeris_time = center_ephemeris_time
+        self._name_model = name_model
+        self._starting_ephemeris_time = starting_ephemeris_time
+        self._focal_length_model = focal_length_model
+        self._image_lines = image_lines
+        self._image_samples = image_samples
+        self._radii = radii
+        self._optical_distortion = optical_distortion
+        self._starting_detector_sample = starting_detector_sample
+        self._starting_detector_line = starting_detector_line
+        self._focal2pixel_samples = focal2pixel_samples
+        self._focal2pixel_lines = focal2pixel_lines
+        self._sensor_position = sensor_position
+        self._sun_position = sun_position
+        self._sensor_orientation = sensor_orientation
+        self._line_scan_rate = line_scan_rate
+        self._detector_sample_summing = detector_sample_summing
+        self._detector_line_summing = detector_line_summing
+        self._dt_ephemeris = dt_ephemeris
+        self._t0_ephemeris = t0_ephemeris
+        self._dt_quaternion = dt_quaternion
+        self._t0_quaternion = t0_quaternion
+        self._reference_height = reference_height
+        self._interpolation_method = interpolation_method
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'ISD200':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The ISD200 of this ISD200.  # noqa: E501
+        :rtype: ISD200
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def name_platform(self) -> str:
+        """Gets the name_platform of this ISD200.
+
+        The name of the sensor platform, e.g., the spacecraft.  # noqa: E501
+
+        :return: The name_platform of this ISD200.
+        :rtype: str
+        """
+        return self._name_platform
+
+    @name_platform.setter
+    def name_platform(self, name_platform: str):
+        """Sets the name_platform of this ISD200.
+
+        The name of the sensor platform, e.g., the spacecraft.  # noqa: E501
+
+        :param name_platform: The name_platform of this ISD200.
+        :type name_platform: str
+        """
+        if name_platform is None:
+            raise ValueError("Invalid value for `name_platform`, must not be `None`")  # noqa: E501
+
+        self._name_platform = name_platform
+
+    @property
+    def name_sensor(self) -> str:
+        """Gets the name_sensor of this ISD200.
+
+        The name of the sensor, e.g., MSGR_MDIS_NAC.  # noqa: E501
+
+        :return: The name_sensor of this ISD200.
+        :rtype: str
+        """
+        return self._name_sensor
+
+    @name_sensor.setter
+    def name_sensor(self, name_sensor: str):
+        """Sets the name_sensor of this ISD200.
+
+        The name of the sensor, e.g., MSGR_MDIS_NAC.  # noqa: E501
+
+        :param name_sensor: The name_sensor of this ISD200.
+        :type name_sensor: str
+        """
+        if name_sensor is None:
+            raise ValueError("Invalid value for `name_sensor`, must not be `None`")  # noqa: E501
+
+        self._name_sensor = name_sensor
+
+    @property
+    def detector_center(self) -> ISD200DetectorCenter:
+        """Gets the detector_center of this ISD200.
+
+
+        :return: The detector_center of this ISD200.
+        :rtype: ISD200DetectorCenter
+        """
+        return self._detector_center
+
+    @detector_center.setter
+    def detector_center(self, detector_center: ISD200DetectorCenter):
+        """Sets the detector_center of this ISD200.
+
+
+        :param detector_center: The detector_center of this ISD200.
+        :type detector_center: ISD200DetectorCenter
+        """
+
+        self._detector_center = detector_center
+
+    @property
+    def center_ephemeris_time(self) -> float:
+        """Gets the center_ephemeris_time of this ISD200.
+
+        The center ephemeris time of the image  # noqa: E501
+
+        :return: The center_ephemeris_time of this ISD200.
+        :rtype: float
+        """
+        return self._center_ephemeris_time
+
+    @center_ephemeris_time.setter
+    def center_ephemeris_time(self, center_ephemeris_time: float):
+        """Sets the center_ephemeris_time of this ISD200.
+
+        The center ephemeris time of the image  # noqa: E501
+
+        :param center_ephemeris_time: The center_ephemeris_time of this ISD200.
+        :type center_ephemeris_time: float
+        """
+
+        self._center_ephemeris_time = center_ephemeris_time
+
+    @property
+    def name_model(self) -> str:
+        """Gets the name_model of this ISD200.
+
+        The name of the model to be instantiated  # noqa: E501
+
+        :return: The name_model of this ISD200.
+        :rtype: str
+        """
+        return self._name_model
+
+    @name_model.setter
+    def name_model(self, name_model: str):
+        """Sets the name_model of this ISD200.
+
+        The name of the model to be instantiated  # noqa: E501
+
+        :param name_model: The name_model of this ISD200.
+        :type name_model: str
+        """
+        if name_model is None:
+            raise ValueError("Invalid value for `name_model`, must not be `None`")  # noqa: E501
+
+        self._name_model = name_model
+
+    @property
+    def starting_ephemeris_time(self) -> float:
+        """Gets the starting_ephemeris_time of this ISD200.
+
+        The starting ephemeris time of the image  # noqa: E501
+
+        :return: The starting_ephemeris_time of this ISD200.
+        :rtype: float
+        """
+        return self._starting_ephemeris_time
+
+    @starting_ephemeris_time.setter
+    def starting_ephemeris_time(self, starting_ephemeris_time: float):
+        """Sets the starting_ephemeris_time of this ISD200.
+
+        The starting ephemeris time of the image  # noqa: E501
+
+        :param starting_ephemeris_time: The starting_ephemeris_time of this ISD200.
+        :type starting_ephemeris_time: float
+        """
+        if starting_ephemeris_time is None:
+            raise ValueError("Invalid value for `starting_ephemeris_time`, must not be `None`")  # noqa: E501
+
+        self._starting_ephemeris_time = starting_ephemeris_time
+
+    @property
+    def focal_length_model(self) -> ISD200FocalLengthModel:
+        """Gets the focal_length_model of this ISD200.
+
+
+        :return: The focal_length_model of this ISD200.
+        :rtype: ISD200FocalLengthModel
+        """
+        return self._focal_length_model
+
+    @focal_length_model.setter
+    def focal_length_model(self, focal_length_model: ISD200FocalLengthModel):
+        """Sets the focal_length_model of this ISD200.
+
+
+        :param focal_length_model: The focal_length_model of this ISD200.
+        :type focal_length_model: ISD200FocalLengthModel
+        """
+        if focal_length_model is None:
+            raise ValueError("Invalid value for `focal_length_model`, must not be `None`")  # noqa: E501
+
+        self._focal_length_model = focal_length_model
+
+    @property
+    def image_lines(self) -> float:
+        """Gets the image_lines of this ISD200.
+
+        The number of lines in the image  # noqa: E501
+
+        :return: The image_lines of this ISD200.
+        :rtype: float
+        """
+        return self._image_lines
+
+    @image_lines.setter
+    def image_lines(self, image_lines: float):
+        """Sets the image_lines of this ISD200.
+
+        The number of lines in the image  # noqa: E501
+
+        :param image_lines: The image_lines of this ISD200.
+        :type image_lines: float
+        """
+        if image_lines is None:
+            raise ValueError("Invalid value for `image_lines`, must not be `None`")  # noqa: E501
+
+        self._image_lines = image_lines
+
+    @property
+    def image_samples(self) -> float:
+        """Gets the image_samples of this ISD200.
+
+        The the number of samples in the image  # noqa: E501
+
+        :return: The image_samples of this ISD200.
+        :rtype: float
+        """
+        return self._image_samples
+
+    @image_samples.setter
+    def image_samples(self, image_samples: float):
+        """Sets the image_samples of this ISD200.
+
+        The the number of samples in the image  # noqa: E501
+
+        :param image_samples: The image_samples of this ISD200.
+        :type image_samples: float
+        """
+        if image_samples is None:
+            raise ValueError("Invalid value for `image_samples`, must not be `None`")  # noqa: E501
+
+        self._image_samples = image_samples
+
+    @property
+    def radii(self) -> ISD200Radii:
+        """Gets the radii of this ISD200.
+
+
+        :return: The radii of this ISD200.
+        :rtype: ISD200Radii
+        """
+        return self._radii
+
+    @radii.setter
+    def radii(self, radii: ISD200Radii):
+        """Sets the radii of this ISD200.
+
+
+        :param radii: The radii of this ISD200.
+        :type radii: ISD200Radii
+        """
+        if radii is None:
+            raise ValueError("Invalid value for `radii`, must not be `None`")  # noqa: E501
+
+        self._radii = radii
+
+    @property
+    def optical_distortion(self) -> OpticalDistortion:
+        """Gets the optical_distortion of this ISD200.
+
+
+        :return: The optical_distortion of this ISD200.
+        :rtype: OpticalDistortion
+        """
+        return self._optical_distortion
+
+    @optical_distortion.setter
+    def optical_distortion(self, optical_distortion: OpticalDistortion):
+        """Sets the optical_distortion of this ISD200.
+
+
+        :param optical_distortion: The optical_distortion of this ISD200.
+        :type optical_distortion: OpticalDistortion
+        """
+
+        self._optical_distortion = optical_distortion
+
+    @property
+    def starting_detector_sample(self) -> float:
+        """Gets the starting_detector_sample of this ISD200.
+
+        The sample on the detector where the data starts  # noqa: E501
+
+        :return: The starting_detector_sample of this ISD200.
+        :rtype: float
+        """
+        return self._starting_detector_sample
+
+    @starting_detector_sample.setter
+    def starting_detector_sample(self, starting_detector_sample: float):
+        """Sets the starting_detector_sample of this ISD200.
+
+        The sample on the detector where the data starts  # noqa: E501
+
+        :param starting_detector_sample: The starting_detector_sample of this ISD200.
+        :type starting_detector_sample: float
+        """
+        if starting_detector_sample is None:
+            raise ValueError("Invalid value for `starting_detector_sample`, must not be `None`")  # noqa: E501
+
+        self._starting_detector_sample = starting_detector_sample
+
+    @property
+    def starting_detector_line(self) -> float:
+        """Gets the starting_detector_line of this ISD200.
+
+        The line on the detector where the data starts  # noqa: E501
+
+        :return: The starting_detector_line of this ISD200.
+        :rtype: float
+        """
+        return self._starting_detector_line
+
+    @starting_detector_line.setter
+    def starting_detector_line(self, starting_detector_line: float):
+        """Sets the starting_detector_line of this ISD200.
+
+        The line on the detector where the data starts  # noqa: E501
+
+        :param starting_detector_line: The starting_detector_line of this ISD200.
+        :type starting_detector_line: float
+        """
+        if starting_detector_line is None:
+            raise ValueError("Invalid value for `starting_detector_line`, must not be `None`")  # noqa: E501
+
+        self._starting_detector_line = starting_detector_line
+
+    @property
+    def focal2pixel_samples(self) -> List[float]:
+        """Gets the focal2pixel_samples of this ISD200.
+
+        Transformation x coefficients from focal plane (mm) to detector pixel coordinates  # noqa: E501
+
+        :return: The focal2pixel_samples of this ISD200.
+        :rtype: List[float]
+        """
+        return self._focal2pixel_samples
+
+    @focal2pixel_samples.setter
+    def focal2pixel_samples(self, focal2pixel_samples: List[float]):
+        """Sets the focal2pixel_samples of this ISD200.
+
+        Transformation x coefficients from focal plane (mm) to detector pixel coordinates  # noqa: E501
+
+        :param focal2pixel_samples: The focal2pixel_samples of this ISD200.
+        :type focal2pixel_samples: List[float]
+        """
+        if focal2pixel_samples is None:
+            raise ValueError("Invalid value for `focal2pixel_samples`, must not be `None`")  # noqa: E501
+
+        self._focal2pixel_samples = focal2pixel_samples
+
+    @property
+    def focal2pixel_lines(self) -> List[float]:
+        """Gets the focal2pixel_lines of this ISD200.
+
+        Transformation y coefficients from focal plane (mm) to detector pixel coordinates  # noqa: E501
+
+        :return: The focal2pixel_lines of this ISD200.
+        :rtype: List[float]
+        """
+        return self._focal2pixel_lines
+
+    @focal2pixel_lines.setter
+    def focal2pixel_lines(self, focal2pixel_lines: List[float]):
+        """Sets the focal2pixel_lines of this ISD200.
+
+        Transformation y coefficients from focal plane (mm) to detector pixel coordinates  # noqa: E501
+
+        :param focal2pixel_lines: The focal2pixel_lines of this ISD200.
+        :type focal2pixel_lines: List[float]
+        """
+        if focal2pixel_lines is None:
+            raise ValueError("Invalid value for `focal2pixel_lines`, must not be `None`")  # noqa: E501
+
+        self._focal2pixel_lines = focal2pixel_lines
+
+    @property
+    def sensor_position(self) -> ISD200SensorPosition:
+        """Gets the sensor_position of this ISD200.
+
+
+        :return: The sensor_position of this ISD200.
+        :rtype: ISD200SensorPosition
+        """
+        return self._sensor_position
+
+    @sensor_position.setter
+    def sensor_position(self, sensor_position: ISD200SensorPosition):
+        """Sets the sensor_position of this ISD200.
+
+
+        :param sensor_position: The sensor_position of this ISD200.
+        :type sensor_position: ISD200SensorPosition
+        """
+
+        self._sensor_position = sensor_position
+
+    @property
+    def sun_position(self) -> ISD200SunPosition:
+        """Gets the sun_position of this ISD200.
+
+
+        :return: The sun_position of this ISD200.
+        :rtype: ISD200SunPosition
+        """
+        return self._sun_position
+
+    @sun_position.setter
+    def sun_position(self, sun_position: ISD200SunPosition):
+        """Sets the sun_position of this ISD200.
+
+
+        :param sun_position: The sun_position of this ISD200.
+        :type sun_position: ISD200SunPosition
+        """
+
+        self._sun_position = sun_position
+
+    @property
+    def sensor_orientation(self) -> Quaternions:
+        """Gets the sensor_orientation of this ISD200.
+
+
+        :return: The sensor_orientation of this ISD200.
+        :rtype: Quaternions
+        """
+        return self._sensor_orientation
+
+    @sensor_orientation.setter
+    def sensor_orientation(self, sensor_orientation: Quaternions):
+        """Sets the sensor_orientation of this ISD200.
+
+
+        :param sensor_orientation: The sensor_orientation of this ISD200.
+        :type sensor_orientation: Quaternions
+        """
+        if sensor_orientation is None:
+            raise ValueError("Invalid value for `sensor_orientation`, must not be `None`")  # noqa: E501
+
+        self._sensor_orientation = sensor_orientation
+
+    @property
+    def line_scan_rate(self) -> List[List[float]]:
+        """Gets the line_scan_rate of this ISD200.
+
+        The line rates of the detector map with the associated start line and time  # noqa: E501
+
+        :return: The line_scan_rate of this ISD200.
+        :rtype: List[List[float]]
+        """
+        return self._line_scan_rate
+
+    @line_scan_rate.setter
+    def line_scan_rate(self, line_scan_rate: List[List[float]]):
+        """Sets the line_scan_rate of this ISD200.
+
+        The line rates of the detector map with the associated start line and time  # noqa: E501
+
+        :param line_scan_rate: The line_scan_rate of this ISD200.
+        :type line_scan_rate: List[List[float]]
+        """
+
+        self._line_scan_rate = line_scan_rate
+
+    @property
+    def detector_sample_summing(self) -> float:
+        """Gets the detector_sample_summing of this ISD200.
+
+        The summing in the sample-direction. A summing of 1 indicates no summing, summing of 2 indicates 2 pixel summing,4 indicates 4 pixel summing, etc   # noqa: E501
+
+        :return: The detector_sample_summing of this ISD200.
+        :rtype: float
+        """
+        return self._detector_sample_summing
+
+    @detector_sample_summing.setter
+    def detector_sample_summing(self, detector_sample_summing: float):
+        """Sets the detector_sample_summing of this ISD200.
+
+        The summing in the sample-direction. A summing of 1 indicates no summing, summing of 2 indicates 2 pixel summing,4 indicates 4 pixel summing, etc   # noqa: E501
+
+        :param detector_sample_summing: The detector_sample_summing of this ISD200.
+        :type detector_sample_summing: float
+        """
+
+        self._detector_sample_summing = detector_sample_summing
+
+    @property
+    def detector_line_summing(self) -> float:
+        """Gets the detector_line_summing of this ISD200.
+
+        The summing in the line-direction  # noqa: E501
+
+        :return: The detector_line_summing of this ISD200.
+        :rtype: float
+        """
+        return self._detector_line_summing
+
+    @detector_line_summing.setter
+    def detector_line_summing(self, detector_line_summing: float):
+        """Sets the detector_line_summing of this ISD200.
+
+        The summing in the line-direction  # noqa: E501
+
+        :param detector_line_summing: The detector_line_summing of this ISD200.
+        :type detector_line_summing: float
+        """
+
+        self._detector_line_summing = detector_line_summing
+
+    @property
+    def dt_ephemeris(self) -> float:
+        """Gets the dt_ephemeris of this ISD200.
+
+        The time between each ephemeris point.  # noqa: E501
+
+        :return: The dt_ephemeris of this ISD200.
+        :rtype: float
+        """
+        return self._dt_ephemeris
+
+    @dt_ephemeris.setter
+    def dt_ephemeris(self, dt_ephemeris: float):
+        """Sets the dt_ephemeris of this ISD200.
+
+        The time between each ephemeris point.  # noqa: E501
+
+        :param dt_ephemeris: The dt_ephemeris of this ISD200.
+        :type dt_ephemeris: float
+        """
+
+        self._dt_ephemeris = dt_ephemeris
+
+    @property
+    def t0_ephemeris(self) -> float:
+        """Gets the t0_ephemeris of this ISD200.
+
+        First ephemeris time - center image time  # noqa: E501
+
+        :return: The t0_ephemeris of this ISD200.
+        :rtype: float
+        """
+        return self._t0_ephemeris
+
+    @t0_ephemeris.setter
+    def t0_ephemeris(self, t0_ephemeris: float):
+        """Sets the t0_ephemeris of this ISD200.
+
+        First ephemeris time - center image time  # noqa: E501
+
+        :param t0_ephemeris: The t0_ephemeris of this ISD200.
+        :type t0_ephemeris: float
+        """
+
+        self._t0_ephemeris = t0_ephemeris
+
+    @property
+    def dt_quaternion(self) -> float:
+        """Gets the dt_quaternion of this ISD200.
+
+        Time spacing of quaternions.  # noqa: E501
+
+        :return: The dt_quaternion of this ISD200.
+        :rtype: float
+        """
+        return self._dt_quaternion
+
+    @dt_quaternion.setter
+    def dt_quaternion(self, dt_quaternion: float):
+        """Sets the dt_quaternion of this ISD200.
+
+        Time spacing of quaternions.  # noqa: E501
+
+        :param dt_quaternion: The dt_quaternion of this ISD200.
+        :type dt_quaternion: float
+        """
+
+        self._dt_quaternion = dt_quaternion
+
+    @property
+    def t0_quaternion(self) -> float:
+        """Gets the t0_quaternion of this ISD200.
+
+        First quaternion time - center image time.  # noqa: E501
+
+        :return: The t0_quaternion of this ISD200.
+        :rtype: float
+        """
+        return self._t0_quaternion
+
+    @t0_quaternion.setter
+    def t0_quaternion(self, t0_quaternion: float):
+        """Sets the t0_quaternion of this ISD200.
+
+        First quaternion time - center image time.  # noqa: E501
+
+        :param t0_quaternion: The t0_quaternion of this ISD200.
+        :type t0_quaternion: float
+        """
+
+        self._t0_quaternion = t0_quaternion
+
+    @property
+    def reference_height(self) -> ISD200ReferenceHeight:
+        """Gets the reference_height of this ISD200.
+
+
+        :return: The reference_height of this ISD200.
+        :rtype: ISD200ReferenceHeight
+        """
+        return self._reference_height
+
+    @reference_height.setter
+    def reference_height(self, reference_height: ISD200ReferenceHeight):
+        """Sets the reference_height of this ISD200.
+
+
+        :param reference_height: The reference_height of this ISD200.
+        :type reference_height: ISD200ReferenceHeight
+        """
+        if reference_height is None:
+            raise ValueError("Invalid value for `reference_height`, must not be `None`")  # noqa: E501
+
+        self._reference_height = reference_height
+
+    @property
+    def interpolation_method(self) -> str:
+        """Gets the interpolation_method of this ISD200.
+
+        The type of interpolation method to use.  # noqa: E501
+
+        :return: The interpolation_method of this ISD200.
+        :rtype: str
+        """
+        return self._interpolation_method
+
+    @interpolation_method.setter
+    def interpolation_method(self, interpolation_method: str):
+        """Sets the interpolation_method of this ISD200.
+
+        The type of interpolation method to use.  # noqa: E501
+
+        :param interpolation_method: The interpolation_method of this ISD200.
+        :type interpolation_method: str
+        """
+        allowed_values = ["lagrange"]  # noqa: E501
+        if interpolation_method not in allowed_values:
+            raise ValueError(
+                "Invalid value for `interpolation_method` ({0}), must be one of {1}"
+                .format(interpolation_method, allowed_values)
+            )
+
+        self._interpolation_method = interpolation_method
diff --git a/minipf/models/isd200_detector_center.py b/minipf/models/isd200_detector_center.py
new file mode 100644
index 0000000..c44ad37
--- /dev/null
+++ b/minipf/models/isd200_detector_center.py
@@ -0,0 +1,94 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class ISD200DetectorCenter(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, line: float=None, sample: float=None):  # noqa: E501
+        """ISD200DetectorCenter - a model defined in OpenAPI
+
+        :param line: The line of this ISD200DetectorCenter.  # noqa: E501
+        :type line: float
+        :param sample: The sample of this ISD200DetectorCenter.  # noqa: E501
+        :type sample: float
+        """
+        self.openapi_types = {
+            'line': float,
+            'sample': float
+        }
+
+        self.attribute_map = {
+            'line': 'line',
+            'sample': 'sample'
+        }
+
+        self._line = line
+        self._sample = sample
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'ISD200DetectorCenter':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The ISD200_detector_center of this ISD200DetectorCenter.  # noqa: E501
+        :rtype: ISD200DetectorCenter
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def line(self) -> float:
+        """Gets the line of this ISD200DetectorCenter.
+
+
+        :return: The line of this ISD200DetectorCenter.
+        :rtype: float
+        """
+        return self._line
+
+    @line.setter
+    def line(self, line: float):
+        """Sets the line of this ISD200DetectorCenter.
+
+
+        :param line: The line of this ISD200DetectorCenter.
+        :type line: float
+        """
+        if line is None:
+            raise ValueError("Invalid value for `line`, must not be `None`")  # noqa: E501
+
+        self._line = line
+
+    @property
+    def sample(self) -> float:
+        """Gets the sample of this ISD200DetectorCenter.
+
+
+        :return: The sample of this ISD200DetectorCenter.
+        :rtype: float
+        """
+        return self._sample
+
+    @sample.setter
+    def sample(self, sample: float):
+        """Sets the sample of this ISD200DetectorCenter.
+
+
+        :param sample: The sample of this ISD200DetectorCenter.
+        :type sample: float
+        """
+        if sample is None:
+            raise ValueError("Invalid value for `sample`, must not be `None`")  # noqa: E501
+
+        self._sample = sample
diff --git a/minipf/models/isd200_focal_length_model.py b/minipf/models/isd200_focal_length_model.py
new file mode 100644
index 0000000..01bcf31
--- /dev/null
+++ b/minipf/models/isd200_focal_length_model.py
@@ -0,0 +1,124 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class ISD200FocalLengthModel(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, key: str=None, focal_length: float=None, focal_epsilon: float=None):  # noqa: E501
+        """ISD200FocalLengthModel - a model defined in OpenAPI
+
+        :param key: The key of this ISD200FocalLengthModel.  # noqa: E501
+        :type key: str
+        :param focal_length: The focal_length of this ISD200FocalLengthModel.  # noqa: E501
+        :type focal_length: float
+        :param focal_epsilon: The focal_epsilon of this ISD200FocalLengthModel.  # noqa: E501
+        :type focal_epsilon: float
+        """
+        self.openapi_types = {
+            'key': str,
+            'focal_length': float,
+            'focal_epsilon': float
+        }
+
+        self.attribute_map = {
+            'key': 'key',
+            'focal_length': 'focal_length',
+            'focal_epsilon': 'focal_epsilon'
+        }
+
+        self._key = key
+        self._focal_length = focal_length
+        self._focal_epsilon = focal_epsilon
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'ISD200FocalLengthModel':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The ISD200_focal_length_model of this ISD200FocalLengthModel.  # noqa: E501
+        :rtype: ISD200FocalLengthModel
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def key(self) -> str:
+        """Gets the key of this ISD200FocalLengthModel.
+
+        A key to get information about time/temp dependent focal length models  # noqa: E501
+
+        :return: The key of this ISD200FocalLengthModel.
+        :rtype: str
+        """
+        return self._key
+
+    @key.setter
+    def key(self, key: str):
+        """Sets the key of this ISD200FocalLengthModel.
+
+        A key to get information about time/temp dependent focal length models  # noqa: E501
+
+        :param key: The key of this ISD200FocalLengthModel.
+        :type key: str
+        """
+
+        self._key = key
+
+    @property
+    def focal_length(self) -> float:
+        """Gets the focal_length of this ISD200FocalLengthModel.
+
+        The focal length in mm  # noqa: E501
+
+        :return: The focal_length of this ISD200FocalLengthModel.
+        :rtype: float
+        """
+        return self._focal_length
+
+    @focal_length.setter
+    def focal_length(self, focal_length: float):
+        """Sets the focal_length of this ISD200FocalLengthModel.
+
+        The focal length in mm  # noqa: E501
+
+        :param focal_length: The focal_length of this ISD200FocalLengthModel.
+        :type focal_length: float
+        """
+        if focal_length is None:
+            raise ValueError("Invalid value for `focal_length`, must not be `None`")  # noqa: E501
+
+        self._focal_length = focal_length
+
+    @property
+    def focal_epsilon(self) -> float:
+        """Gets the focal_epsilon of this ISD200FocalLengthModel.
+
+        The uncertainty of the focal length in mm  # noqa: E501
+
+        :return: The focal_epsilon of this ISD200FocalLengthModel.
+        :rtype: float
+        """
+        return self._focal_epsilon
+
+    @focal_epsilon.setter
+    def focal_epsilon(self, focal_epsilon: float):
+        """Sets the focal_epsilon of this ISD200FocalLengthModel.
+
+        The uncertainty of the focal length in mm  # noqa: E501
+
+        :param focal_epsilon: The focal_epsilon of this ISD200FocalLengthModel.
+        :type focal_epsilon: float
+        """
+
+        self._focal_epsilon = focal_epsilon
diff --git a/minipf/models/isd200_radii.py b/minipf/models/isd200_radii.py
new file mode 100644
index 0000000..c7bb518
--- /dev/null
+++ b/minipf/models/isd200_radii.py
@@ -0,0 +1,120 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class ISD200Radii(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, semimajor: float=None, semiminor: float=None, unit: str=None):  # noqa: E501
+        """ISD200Radii - a model defined in OpenAPI
+
+        :param semimajor: The semimajor of this ISD200Radii.  # noqa: E501
+        :type semimajor: float
+        :param semiminor: The semiminor of this ISD200Radii.  # noqa: E501
+        :type semiminor: float
+        :param unit: The unit of this ISD200Radii.  # noqa: E501
+        :type unit: str
+        """
+        self.openapi_types = {
+            'semimajor': float,
+            'semiminor': float,
+            'unit': str
+        }
+
+        self.attribute_map = {
+            'semimajor': 'semimajor',
+            'semiminor': 'semiminor',
+            'unit': 'unit'
+        }
+
+        self._semimajor = semimajor
+        self._semiminor = semiminor
+        self._unit = unit
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'ISD200Radii':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The ISD200_radii of this ISD200Radii.  # noqa: E501
+        :rtype: ISD200Radii
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def semimajor(self) -> float:
+        """Gets the semimajor of this ISD200Radii.
+
+
+        :return: The semimajor of this ISD200Radii.
+        :rtype: float
+        """
+        return self._semimajor
+
+    @semimajor.setter
+    def semimajor(self, semimajor: float):
+        """Sets the semimajor of this ISD200Radii.
+
+
+        :param semimajor: The semimajor of this ISD200Radii.
+        :type semimajor: float
+        """
+        if semimajor is None:
+            raise ValueError("Invalid value for `semimajor`, must not be `None`")  # noqa: E501
+
+        self._semimajor = semimajor
+
+    @property
+    def semiminor(self) -> float:
+        """Gets the semiminor of this ISD200Radii.
+
+
+        :return: The semiminor of this ISD200Radii.
+        :rtype: float
+        """
+        return self._semiminor
+
+    @semiminor.setter
+    def semiminor(self, semiminor: float):
+        """Sets the semiminor of this ISD200Radii.
+
+
+        :param semiminor: The semiminor of this ISD200Radii.
+        :type semiminor: float
+        """
+
+        self._semiminor = semiminor
+
+    @property
+    def unit(self) -> str:
+        """Gets the unit of this ISD200Radii.
+
+
+        :return: The unit of this ISD200Radii.
+        :rtype: str
+        """
+        return self._unit
+
+    @unit.setter
+    def unit(self, unit: str):
+        """Sets the unit of this ISD200Radii.
+
+
+        :param unit: The unit of this ISD200Radii.
+        :type unit: str
+        """
+        if unit is None:
+            raise ValueError("Invalid value for `unit`, must not be `None`")  # noqa: E501
+
+        self._unit = unit
diff --git a/minipf/models/isd200_reference_height.py b/minipf/models/isd200_reference_height.py
new file mode 100644
index 0000000..d82034d
--- /dev/null
+++ b/minipf/models/isd200_reference_height.py
@@ -0,0 +1,124 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class ISD200ReferenceHeight(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, minheight: float=None, maxheight: float=None, unit: str=None):  # noqa: E501
+        """ISD200ReferenceHeight - a model defined in OpenAPI
+
+        :param minheight: The minheight of this ISD200ReferenceHeight.  # noqa: E501
+        :type minheight: float
+        :param maxheight: The maxheight of this ISD200ReferenceHeight.  # noqa: E501
+        :type maxheight: float
+        :param unit: The unit of this ISD200ReferenceHeight.  # noqa: E501
+        :type unit: str
+        """
+        self.openapi_types = {
+            'minheight': float,
+            'maxheight': float,
+            'unit': str
+        }
+
+        self.attribute_map = {
+            'minheight': 'minheight',
+            'maxheight': 'maxheight',
+            'unit': 'unit'
+        }
+
+        self._minheight = minheight
+        self._maxheight = maxheight
+        self._unit = unit
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'ISD200ReferenceHeight':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The ISD200_reference_height of this ISD200ReferenceHeight.  # noqa: E501
+        :rtype: ISD200ReferenceHeight
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def minheight(self) -> float:
+        """Gets the minheight of this ISD200ReferenceHeight.
+
+
+        :return: The minheight of this ISD200ReferenceHeight.
+        :rtype: float
+        """
+        return self._minheight
+
+    @minheight.setter
+    def minheight(self, minheight: float):
+        """Sets the minheight of this ISD200ReferenceHeight.
+
+
+        :param minheight: The minheight of this ISD200ReferenceHeight.
+        :type minheight: float
+        """
+        if minheight is None:
+            raise ValueError("Invalid value for `minheight`, must not be `None`")  # noqa: E501
+
+        self._minheight = minheight
+
+    @property
+    def maxheight(self) -> float:
+        """Gets the maxheight of this ISD200ReferenceHeight.
+
+
+        :return: The maxheight of this ISD200ReferenceHeight.
+        :rtype: float
+        """
+        return self._maxheight
+
+    @maxheight.setter
+    def maxheight(self, maxheight: float):
+        """Sets the maxheight of this ISD200ReferenceHeight.
+
+
+        :param maxheight: The maxheight of this ISD200ReferenceHeight.
+        :type maxheight: float
+        """
+        if maxheight is None:
+            raise ValueError("Invalid value for `maxheight`, must not be `None`")  # noqa: E501
+
+        self._maxheight = maxheight
+
+    @property
+    def unit(self) -> str:
+        """Gets the unit of this ISD200ReferenceHeight.
+
+        The unit that heights are provided in  # noqa: E501
+
+        :return: The unit of this ISD200ReferenceHeight.
+        :rtype: str
+        """
+        return self._unit
+
+    @unit.setter
+    def unit(self, unit: str):
+        """Sets the unit of this ISD200ReferenceHeight.
+
+        The unit that heights are provided in  # noqa: E501
+
+        :param unit: The unit of this ISD200ReferenceHeight.
+        :type unit: str
+        """
+        if unit is None:
+            raise ValueError("Invalid value for `unit`, must not be `None`")  # noqa: E501
+
+        self._unit = unit
diff --git a/minipf/models/isd200_sensor_position.py b/minipf/models/isd200_sensor_position.py
new file mode 100644
index 0000000..11d6146
--- /dev/null
+++ b/minipf/models/isd200_sensor_position.py
@@ -0,0 +1,117 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf.models.xyz import XYZ  # noqa: F401,E501
+from minipf import util
+
+
+class ISD200SensorPosition(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, velocities: List[XYZ]=None, unit: str=None, positions: List[XYZ]=None):  # noqa: E501
+        """ISD200SensorPosition - a model defined in OpenAPI
+
+        :param velocities: The velocities of this ISD200SensorPosition.  # noqa: E501
+        :type velocities: List[XYZ]
+        :param unit: The unit of this ISD200SensorPosition.  # noqa: E501
+        :type unit: str
+        :param positions: The positions of this ISD200SensorPosition.  # noqa: E501
+        :type positions: List[XYZ]
+        """
+        self.openapi_types = {
+            'velocities': List[XYZ],
+            'unit': str,
+            'positions': List[XYZ]
+        }
+
+        self.attribute_map = {
+            'velocities': 'velocities',
+            'unit': 'unit',
+            'positions': 'positions'
+        }
+
+        self._velocities = velocities
+        self._unit = unit
+        self._positions = positions
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'ISD200SensorPosition':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The ISD200_sensor_position of this ISD200SensorPosition.  # noqa: E501
+        :rtype: ISD200SensorPosition
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def velocities(self) -> List[XYZ]:
+        """Gets the velocities of this ISD200SensorPosition.
+
+
+        :return: The velocities of this ISD200SensorPosition.
+        :rtype: List[XYZ]
+        """
+        return self._velocities
+
+    @velocities.setter
+    def velocities(self, velocities: List[XYZ]):
+        """Sets the velocities of this ISD200SensorPosition.
+
+
+        :param velocities: The velocities of this ISD200SensorPosition.
+        :type velocities: List[XYZ]
+        """
+
+        self._velocities = velocities
+
+    @property
+    def unit(self) -> str:
+        """Gets the unit of this ISD200SensorPosition.
+
+
+        :return: The unit of this ISD200SensorPosition.
+        :rtype: str
+        """
+        return self._unit
+
+    @unit.setter
+    def unit(self, unit: str):
+        """Sets the unit of this ISD200SensorPosition.
+
+
+        :param unit: The unit of this ISD200SensorPosition.
+        :type unit: str
+        """
+
+        self._unit = unit
+
+    @property
+    def positions(self) -> List[XYZ]:
+        """Gets the positions of this ISD200SensorPosition.
+
+
+        :return: The positions of this ISD200SensorPosition.
+        :rtype: List[XYZ]
+        """
+        return self._positions
+
+    @positions.setter
+    def positions(self, positions: List[XYZ]):
+        """Sets the positions of this ISD200SensorPosition.
+
+
+        :param positions: The positions of this ISD200SensorPosition.
+        :type positions: List[XYZ]
+        """
+
+        self._positions = positions
diff --git a/minipf/models/isd200_sun_position.py b/minipf/models/isd200_sun_position.py
new file mode 100644
index 0000000..62e54db
--- /dev/null
+++ b/minipf/models/isd200_sun_position.py
@@ -0,0 +1,117 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf.models.xyz import XYZ  # noqa: F401,E501
+from minipf import util
+
+
+class ISD200SunPosition(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, velocities: XYZ=None, unit: str=None, positions: XYZ=None):  # noqa: E501
+        """ISD200SunPosition - a model defined in OpenAPI
+
+        :param velocities: The velocities of this ISD200SunPosition.  # noqa: E501
+        :type velocities: XYZ
+        :param unit: The unit of this ISD200SunPosition.  # noqa: E501
+        :type unit: str
+        :param positions: The positions of this ISD200SunPosition.  # noqa: E501
+        :type positions: XYZ
+        """
+        self.openapi_types = {
+            'velocities': XYZ,
+            'unit': str,
+            'positions': XYZ
+        }
+
+        self.attribute_map = {
+            'velocities': 'velocities',
+            'unit': 'unit',
+            'positions': 'positions'
+        }
+
+        self._velocities = velocities
+        self._unit = unit
+        self._positions = positions
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'ISD200SunPosition':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The ISD200_sun_position of this ISD200SunPosition.  # noqa: E501
+        :rtype: ISD200SunPosition
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def velocities(self) -> XYZ:
+        """Gets the velocities of this ISD200SunPosition.
+
+
+        :return: The velocities of this ISD200SunPosition.
+        :rtype: XYZ
+        """
+        return self._velocities
+
+    @velocities.setter
+    def velocities(self, velocities: XYZ):
+        """Sets the velocities of this ISD200SunPosition.
+
+
+        :param velocities: The velocities of this ISD200SunPosition.
+        :type velocities: XYZ
+        """
+
+        self._velocities = velocities
+
+    @property
+    def unit(self) -> str:
+        """Gets the unit of this ISD200SunPosition.
+
+
+        :return: The unit of this ISD200SunPosition.
+        :rtype: str
+        """
+        return self._unit
+
+    @unit.setter
+    def unit(self, unit: str):
+        """Sets the unit of this ISD200SunPosition.
+
+
+        :param unit: The unit of this ISD200SunPosition.
+        :type unit: str
+        """
+
+        self._unit = unit
+
+    @property
+    def positions(self) -> XYZ:
+        """Gets the positions of this ISD200SunPosition.
+
+
+        :return: The positions of this ISD200SunPosition.
+        :rtype: XYZ
+        """
+        return self._positions
+
+    @positions.setter
+    def positions(self, positions: XYZ):
+        """Sets the positions of this ISD200SunPosition.
+
+
+        :param positions: The positions of this ISD200SunPosition.
+        :type positions: XYZ
+        """
+
+        self._positions = positions
diff --git a/minipf/models/optical_distortion.py b/minipf/models/optical_distortion.py
new file mode 100644
index 0000000..19c360c
--- /dev/null
+++ b/minipf/models/optical_distortion.py
@@ -0,0 +1,92 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf.models.optical_distortion_radial import OpticalDistortionRadial  # noqa: F401,E501
+from minipf.models.optical_distortion_transverse import OpticalDistortionTransverse  # noqa: F401,E501
+from minipf import util
+
+
+class OpticalDistortion(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, transverse: OpticalDistortionTransverse=None, radial: OpticalDistortionRadial=None):  # noqa: E501
+        """OpticalDistortion - a model defined in OpenAPI
+
+        :param transverse: The transverse of this OpticalDistortion.  # noqa: E501
+        :type transverse: OpticalDistortionTransverse
+        :param radial: The radial of this OpticalDistortion.  # noqa: E501
+        :type radial: OpticalDistortionRadial
+        """
+        self.openapi_types = {
+            'transverse': OpticalDistortionTransverse,
+            'radial': OpticalDistortionRadial
+        }
+
+        self.attribute_map = {
+            'transverse': 'transverse',
+            'radial': 'radial'
+        }
+
+        self._transverse = transverse
+        self._radial = radial
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'OpticalDistortion':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The optical_distortion of this OpticalDistortion.  # noqa: E501
+        :rtype: OpticalDistortion
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def transverse(self) -> OpticalDistortionTransverse:
+        """Gets the transverse of this OpticalDistortion.
+
+
+        :return: The transverse of this OpticalDistortion.
+        :rtype: OpticalDistortionTransverse
+        """
+        return self._transverse
+
+    @transverse.setter
+    def transverse(self, transverse: OpticalDistortionTransverse):
+        """Sets the transverse of this OpticalDistortion.
+
+
+        :param transverse: The transverse of this OpticalDistortion.
+        :type transverse: OpticalDistortionTransverse
+        """
+
+        self._transverse = transverse
+
+    @property
+    def radial(self) -> OpticalDistortionRadial:
+        """Gets the radial of this OpticalDistortion.
+
+
+        :return: The radial of this OpticalDistortion.
+        :rtype: OpticalDistortionRadial
+        """
+        return self._radial
+
+    @radial.setter
+    def radial(self, radial: OpticalDistortionRadial):
+        """Sets the radial of this OpticalDistortion.
+
+
+        :param radial: The radial of this OpticalDistortion.
+        :type radial: OpticalDistortionRadial
+        """
+
+        self._radial = radial
diff --git a/minipf/models/optical_distortion_radial.py b/minipf/models/optical_distortion_radial.py
new file mode 100644
index 0000000..0c419cd
--- /dev/null
+++ b/minipf/models/optical_distortion_radial.py
@@ -0,0 +1,64 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class OpticalDistortionRadial(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, coefficients: List[float]=None):  # noqa: E501
+        """OpticalDistortionRadial - a model defined in OpenAPI
+
+        :param coefficients: The coefficients of this OpticalDistortionRadial.  # noqa: E501
+        :type coefficients: List[float]
+        """
+        self.openapi_types = {
+            'coefficients': List[float]
+        }
+
+        self.attribute_map = {
+            'coefficients': 'coefficients'
+        }
+
+        self._coefficients = coefficients
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'OpticalDistortionRadial':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The optical_distortion_radial of this OpticalDistortionRadial.  # noqa: E501
+        :rtype: OpticalDistortionRadial
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def coefficients(self) -> List[float]:
+        """Gets the coefficients of this OpticalDistortionRadial.
+
+
+        :return: The coefficients of this OpticalDistortionRadial.
+        :rtype: List[float]
+        """
+        return self._coefficients
+
+    @coefficients.setter
+    def coefficients(self, coefficients: List[float]):
+        """Sets the coefficients of this OpticalDistortionRadial.
+
+
+        :param coefficients: The coefficients of this OpticalDistortionRadial.
+        :type coefficients: List[float]
+        """
+
+        self._coefficients = coefficients
diff --git a/minipf/models/optical_distortion_transverse.py b/minipf/models/optical_distortion_transverse.py
new file mode 100644
index 0000000..f875153
--- /dev/null
+++ b/minipf/models/optical_distortion_transverse.py
@@ -0,0 +1,90 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class OpticalDistortionTransverse(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, x: List[float]=None, y: List[float]=None):  # noqa: E501
+        """OpticalDistortionTransverse - a model defined in OpenAPI
+
+        :param x: The x of this OpticalDistortionTransverse.  # noqa: E501
+        :type x: List[float]
+        :param y: The y of this OpticalDistortionTransverse.  # noqa: E501
+        :type y: List[float]
+        """
+        self.openapi_types = {
+            'x': List[float],
+            'y': List[float]
+        }
+
+        self.attribute_map = {
+            'x': 'x',
+            'y': 'y'
+        }
+
+        self._x = x
+        self._y = y
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'OpticalDistortionTransverse':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The optical_distortion_transverse of this OpticalDistortionTransverse.  # noqa: E501
+        :rtype: OpticalDistortionTransverse
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def x(self) -> List[float]:
+        """Gets the x of this OpticalDistortionTransverse.
+
+
+        :return: The x of this OpticalDistortionTransverse.
+        :rtype: List[float]
+        """
+        return self._x
+
+    @x.setter
+    def x(self, x: List[float]):
+        """Sets the x of this OpticalDistortionTransverse.
+
+
+        :param x: The x of this OpticalDistortionTransverse.
+        :type x: List[float]
+        """
+
+        self._x = x
+
+    @property
+    def y(self) -> List[float]:
+        """Gets the y of this OpticalDistortionTransverse.
+
+
+        :return: The y of this OpticalDistortionTransverse.
+        :rtype: List[float]
+        """
+        return self._y
+
+    @y.setter
+    def y(self, y: List[float]):
+        """Sets the y of this OpticalDistortionTransverse.
+
+
+        :param y: The y of this OpticalDistortionTransverse.
+        :type y: List[float]
+        """
+
+        self._y = y
diff --git a/minipf/models/quaternion.py b/minipf/models/quaternion.py
new file mode 100644
index 0000000..ea0cb62
--- /dev/null
+++ b/minipf/models/quaternion.py
@@ -0,0 +1,37 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class Quaternion(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self):  # noqa: E501
+        """Quaternion - a model defined in OpenAPI
+
+        """
+        self.openapi_types = {
+        }
+
+        self.attribute_map = {
+        }
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'Quaternion':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The Quaternion of this Quaternion.  # noqa: E501
+        :rtype: Quaternion
+        """
+        return util.deserialize_model(dikt, cls)
diff --git a/minipf/models/quaternions.py b/minipf/models/quaternions.py
new file mode 100644
index 0000000..57acf3c
--- /dev/null
+++ b/minipf/models/quaternions.py
@@ -0,0 +1,68 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class Quaternions(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, quaternions: List[List[float]]=None):  # noqa: E501
+        """Quaternions - a model defined in OpenAPI
+
+        :param quaternions: The quaternions of this Quaternions.  # noqa: E501
+        :type quaternions: List[List[float]]
+        """
+        self.openapi_types = {
+            'quaternions': List[List[float]]
+        }
+
+        self.attribute_map = {
+            'quaternions': 'quaternions'
+        }
+
+        self._quaternions = quaternions
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'Quaternions':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The Quaternions of this Quaternions.  # noqa: E501
+        :rtype: Quaternions
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def quaternions(self) -> List[List[float]]:
+        """Gets the quaternions of this Quaternions.
+
+        Orientation of the sensor provided in quaternions  # noqa: E501
+
+        :return: The quaternions of this Quaternions.
+        :rtype: List[List[float]]
+        """
+        return self._quaternions
+
+    @quaternions.setter
+    def quaternions(self, quaternions: List[List[float]]):
+        """Sets the quaternions of this Quaternions.
+
+        Orientation of the sensor provided in quaternions  # noqa: E501
+
+        :param quaternions: The quaternions of this Quaternions.
+        :type quaternions: List[List[float]]
+        """
+        if quaternions is None:
+            raise ValueError("Invalid value for `quaternions`, must not be `None`")  # noqa: E501
+
+        self._quaternions = quaternions
diff --git a/minipf/models/request_isd.py b/minipf/models/request_isd.py
new file mode 100644
index 0000000..e48d088
--- /dev/null
+++ b/minipf/models/request_isd.py
@@ -0,0 +1,64 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class RequestISD(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self, label: str=None):  # noqa: E501
+        """RequestISD - a model defined in OpenAPI
+
+        :param label: The label of this RequestISD.  # noqa: E501
+        :type label: str
+        """
+        self.openapi_types = {
+            'label': str
+        }
+
+        self.attribute_map = {
+            'label': 'label'
+        }
+
+        self._label = label
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'RequestISD':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The request_ISD of this RequestISD.  # noqa: E501
+        :rtype: RequestISD
+        """
+        return util.deserialize_model(dikt, cls)
+
+    @property
+    def label(self) -> str:
+        """Gets the label of this RequestISD.
+
+
+        :return: The label of this RequestISD.
+        :rtype: str
+        """
+        return self._label
+
+    @label.setter
+    def label(self, label: str):
+        """Sets the label of this RequestISD.
+
+
+        :param label: The label of this RequestISD.
+        :type label: str
+        """
+
+        self._label = label
diff --git a/minipf/models/xyz.py b/minipf/models/xyz.py
new file mode 100644
index 0000000..7e18041
--- /dev/null
+++ b/minipf/models/xyz.py
@@ -0,0 +1,37 @@
+# coding: utf-8
+
+from __future__ import absolute_import
+from datetime import date, datetime  # noqa: F401
+
+from typing import List, Dict  # noqa: F401
+
+from minipf.models.base_model_ import Model
+from minipf import util
+
+
+class XYZ(Model):
+    """NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
+
+    Do not edit the class manually.
+    """
+
+    def __init__(self):  # noqa: E501
+        """XYZ - a model defined in OpenAPI
+
+        """
+        self.openapi_types = {
+        }
+
+        self.attribute_map = {
+        }
+
+    @classmethod
+    def from_dict(cls, dikt) -> 'XYZ':
+        """Returns the dict as a model
+
+        :param dikt: A dict.
+        :type: dict
+        :return: The XYZ of this XYZ.  # noqa: E501
+        :rtype: XYZ
+        """
+        return util.deserialize_model(dikt, cls)
diff --git a/minipf/util.py b/minipf/util.py
new file mode 100644
index 0000000..75e2345
--- /dev/null
+++ b/minipf/util.py
@@ -0,0 +1,215 @@
+import datetime
+
+import os
+import six
+import typing
+import numpy as np
+from glob import glob
+from os import path
+from itertools import groupby, filterfalse
+from minipf import config
+
+
+
+def _deserialize(data, klass):
+    """Deserializes dict, list, str into an object.
+
+    :param data: dict, list or str.
+    :param klass: class literal, or string of class name.
+
+    :return: object.
+    """
+    if data is None:
+        return None
+    if klass in six.integer_types or klass in (float, str, bool):
+        return _deserialize_primitive(data, klass)
+    elif klass == object:
+        return _deserialize_object(data)
+    elif klass == datetime.date:
+        return deserialize_date(data)
+    elif klass == datetime.datetime:
+        return deserialize_datetime(data)
+    elif type(klass) == typing._GenericAlias:
+        if klass.__origin__ == list:
+            return _deserialize_list(data, klass.__args__[0])
+        if klass.__origin__ == dict:
+            return _deserialize_dict(data, klass.__args__[1])
+    else:
+        return deserialize_model(data, klass)
+
+
+def _deserialize_primitive(data, klass):
+    """Deserializes to primitive type.
+
+    :param data: data to deserialize.
+    :param klass: class literal.
+
+    :return: int, long, float, str, bool.
+    :rtype: int | long | float | str | bool
+    """
+    try:
+        value = klass(data)
+    except UnicodeEncodeError:
+        value = six.u(data)
+    except TypeError:
+        value = data
+    return value
+
+
+def _deserialize_object(value):
+    """Return an original value.
+
+    :return: object.
+    """
+    return value
+
+
+def deserialize_date(string):
+    """Deserializes string to date.
+
+    :param string: str.
+    :type string: str
+    :return: date.
+    :rtype: date
+    """
+    try:
+        from dateutil.parser import parse
+        return parse(string).date()
+    except ImportError:
+        return string
+
+
+def deserialize_datetime(string):
+    """Deserializes string to datetime.
+
+    The string should be in iso8601 datetime format.
+
+    :param string: str.
+    :type string: str
+    :return: datetime.
+    :rtype: datetime
+    """
+    try:
+        from dateutil.parser import parse
+        return parse(string)
+    except ImportError:
+        return string
+
+
+def deserialize_model(data, klass):
+    """Deserializes list or dict to model.
+
+    :param data: dict, list.
+    :type data: dict | list
+    :param klass: class literal.
+    :return: model object.
+    """
+    instance = klass()
+
+    if not instance.openapi_types:
+        return data
+
+    for attr, attr_type in six.iteritems(instance.openapi_types):
+        if data is not None \
+                and instance.attribute_map[attr] in data \
+                and isinstance(data, (list, dict)):
+            value = data[instance.attribute_map[attr]]
+            setattr(instance, attr, _deserialize(value, attr_type))
+
+    return instance
+
+
+def _deserialize_list(data, boxed_type):
+    """Deserializes a list and its elements.
+
+    :param data: list to deserialize.
+    :type data: list
+    :param boxed_type: class literal.
+
+    :return: deserialized list.
+    :rtype: list
+    """
+    return [_deserialize(sub_data, boxed_type)
+            for sub_data in data]
+
+
+def _deserialize_dict(data, boxed_type):
+    """Deserializes a dict and its elements.
+
+    :param data: dict to deserialize.
+    :type data: dict
+    :param boxed_type: class literal.
+
+    :return: deserialized dict.
+    :rtype: dict
+    """
+    return {k: _deserialize(v, boxed_type)
+            for k, v in six.iteritems(data)}
+
+
+def get_metakernels(spice_dir=config.spice_root, missions=set(), years=set(), versions=set()):
+    """
+    Given a root directory, get any subdirectory containing metakernels,
+    assume spice directory structure.
+
+    Mostly doing filtering here, might be worth using Pandas?
+    """
+    if not missions or missions == "all":
+        missions = set()
+    if not years or years == "all":
+        years = set()
+    if not versions or versions == "all":
+        versions = set()
+
+    if isinstance(missions, str):
+        missions = {missions}
+
+    if isinstance(years, str) or isinstance(years, int):
+        years = {str(years)}
+    else:
+        years = {str(year) for year in years}
+
+    avail = {
+        'count': 0,
+        'data': []
+    }
+
+    mission_dirs = list(filter(os.path.isdir, glob(os.path.join(spice_dir, '*'))))
+
+    for md in mission_dirs:
+        # Assuming spice root has the same name as the original on NAIF website"
+        mission = os.path.basename(md).split('-')[0]
+        if missions and mission not in missions:
+            continue
+
+        metakernel_keys = ['mission', 'year', 'version', 'path']
+
+        # recursive glob to make metakernel search more robust to subtle directory structure differences
+        metakernel_paths = sorted(glob(os.path.join(md, '**','*.tm'), recursive=True))
+
+        metakernels = [dict(zip(metakernel_keys, [mission]+path.splitext(path.basename(k))[0].split('_')[1:3] + [k])) for k in metakernel_paths]
+
+        # naive filter, do we really need anything else?
+        if years:
+            metakernels = list(filter(lambda x:x['year'] in years, metakernels))
+        if versions:
+            if versions == 'latest':
+                latest = []
+                # Panda's groupby is overrated
+                for k, g in groupby(metakernels, lambda x:x['year']):
+                    items = list(g)
+                    latest.append(max(items, key=lambda x:x['version']))
+                metakernels = latest
+            else:
+                metakernels = list(filter(lambda x:x['version'] in versions, metakernels))
+
+        avail['data'].extend(metakernels)
+
+    avail['count'] = len(avail['data'])
+    if not avail:
+        avail = {
+            'count' : 0,
+            'data' : 'ERROR: NONE OF {} ARE VALID MISSIONS'.format(missions)
+        }
+
+    return avail
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..167fa74
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,27 @@
+# coding: utf-8
+
+import sys
+from setuptools import setup, find_packages
+
+NAME = "minipf"
+VERSION = "0.0.1"
+
+# To install the library, run the following
+#
+# python setup.py install
+#
+# prerequisite: setuptools
+# http://pypi.python.org/pypi/setuptools
+
+setup(
+    name=NAME,
+    version=VERSION,
+    description="Barebones version of pfeffernusse",
+    author_email="jlaura@usgs.gov",
+    url="",
+    keywords=["Pfeffernusse"],
+    packages=find_packages(),
+    long_description="""\
+    A SpiceAPI for extracting NAIF Spice Data
+    """
+)
-- 
GitLab