'''This Python code is an automatically generated wrapper
for Fortran code made by 'fmodpy'. The original documentation
for the Fortran source code follows.

! TODO:
!
! - Unify memory allocation so that threads don't have to allocate their
!   own temporary space.
!
! - Enable a model that has no internal states (for linear regression).
!
! - Enable a apositional without a following model.
!
! - Center and fit inputs and outputs to spherical points (unit length).
!
! - Change initialization of shift terms to be based on the assumption
!   that input data has spherical shape. Internal shift terms use
!   previous shift and weight matrices to estimate value ranges for
!   each basis function, with largest functions getting most negative
!   shift and smallest functions getting most positive shift.
!
! - Create 'condition_model' subroutine that takes values and gradients
!   at all internal states, uses linear transformations to identify and
!   remove redundant basis functions and initialize new basis functions
!   that align most with the error function.
!
! - Get stats on the internal values within the network during training.
!   - step size progression
!   - shift values
!   - vector magnitudes for each node
!   - output weights magnitude for each node
!   - internal node contributions to MSE
!   - data distributions at internal nodes (% less and greater than 0)
!


! Module for matrix multiplication (absolutely crucial for APOS speed).
'''

import os
import ctypes
import platform
import numpy

# --------------------------------------------------------------------
#               CONFIGURATION
# 
_verbose = True
_fort_compiler = "gfortran"
_shared_object_name = "apos." + platform.machine() + ".so"
_this_directory = os.path.dirname(os.path.abspath(__file__))
_path_to_lib = os.path.join(_this_directory, _shared_object_name)
_compile_options = ['-fPIC', '-shared', '-O3', '-lblas', '-fopenmp', '-fcheck=bounds', '-llapack']
_ordered_dependencies = ['apos.f90', 'apos_c_wrapper.f90']
# 
# --------------------------------------------------------------------
#               AUTO-COMPILING
#
# Try to import the existing object. If that fails, recompile and then try.
try:
    clib = ctypes.CDLL(_path_to_lib)
except:
    # Remove the shared object if it exists, because it is faulty.
    if os.path.exists(_shared_object_name):
        os.remove(_shared_object_name)
    # Compile a new shared object.
    _command = " ".join([_fort_compiler] + _compile_options + ["-o", _shared_object_name] + _ordered_dependencies)
    if _verbose:
        print("Running system command with arguments")
        print("  ", _command)
    # Run the compilation command.
    import subprocess
    subprocess.run(_command, shell=True, cwd=_this_directory)
    # Import the shared object file as a C library with ctypes.
    clib = ctypes.CDLL(_path_to_lib)
# --------------------------------------------------------------------


class matrix_operations:
    ''''''

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine GEMM
    
    def gemm(self, op_a, op_b, out_rows, out_cols, inner_dim, ab_mult, a, a_rows, b, b_rows, c_mult, c, c_rows):
        '''! Convenience wrapper routine for calling matrix multiply.'''
        
        # Setting up "op_a"
        if (type(op_a) is not ctypes.c_char): op_a = ctypes.c_char(op_a)
        
        # Setting up "op_b"
        if (type(op_b) is not ctypes.c_char): op_b = ctypes.c_char(op_b)
        
        # Setting up "out_rows"
        if (type(out_rows) is not ctypes.c_int): out_rows = ctypes.c_int(out_rows)
        
        # Setting up "out_cols"
        if (type(out_cols) is not ctypes.c_int): out_cols = ctypes.c_int(out_cols)
        
        # Setting up "inner_dim"
        if (type(inner_dim) is not ctypes.c_int): inner_dim = ctypes.c_int(inner_dim)
        
        # Setting up "ab_mult"
        if (type(ab_mult) is not ctypes.c_float): ab_mult = ctypes.c_float(ab_mult)
        
        # Setting up "a"
        if ((not issubclass(type(a), numpy.ndarray)) or
            (not numpy.asarray(a).flags.f_contiguous) or
            (not (a.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'a' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            a = numpy.asarray(a, dtype=ctypes.c_float, order='F')
        a_dim_1 = ctypes.c_int(a.shape[0])
        a_dim_2 = ctypes.c_int(a.shape[1])
        
        # Setting up "a_rows"
        if (type(a_rows) is not ctypes.c_int): a_rows = ctypes.c_int(a_rows)
        
        # Setting up "b"
        if ((not issubclass(type(b), numpy.ndarray)) or
            (not numpy.asarray(b).flags.f_contiguous) or
            (not (b.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'b' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            b = numpy.asarray(b, dtype=ctypes.c_float, order='F')
        b_dim_1 = ctypes.c_int(b.shape[0])
        b_dim_2 = ctypes.c_int(b.shape[1])
        
        # Setting up "b_rows"
        if (type(b_rows) is not ctypes.c_int): b_rows = ctypes.c_int(b_rows)
        
        # Setting up "c_mult"
        if (type(c_mult) is not ctypes.c_float): c_mult = ctypes.c_float(c_mult)
        
        # Setting up "c"
        if ((not issubclass(type(c), numpy.ndarray)) or
            (not numpy.asarray(c).flags.f_contiguous) or
            (not (c.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'c' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            c = numpy.asarray(c, dtype=ctypes.c_float, order='F')
        c_dim_1 = ctypes.c_int(c.shape[0])
        c_dim_2 = ctypes.c_int(c.shape[1])
        
        # Setting up "c_rows"
        if (type(c_rows) is not ctypes.c_int): c_rows = ctypes.c_int(c_rows)
    
        # Call C-accessible Fortran wrapper.
        clib.c_gemm(ctypes.byref(op_a), ctypes.byref(op_b), ctypes.byref(out_rows), ctypes.byref(out_cols), ctypes.byref(inner_dim), ctypes.byref(ab_mult), ctypes.byref(a_dim_1), ctypes.byref(a_dim_2), ctypes.c_void_p(a.ctypes.data), ctypes.byref(a_rows), ctypes.byref(b_dim_1), ctypes.byref(b_dim_2), ctypes.c_void_p(b.ctypes.data), ctypes.byref(b_rows), ctypes.byref(c_mult), ctypes.byref(c_dim_1), ctypes.byref(c_dim_2), ctypes.c_void_p(c.ctypes.data), ctypes.byref(c_rows))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return c

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine ORTHONORMALIZE
    
    def orthonormalize(self, a):
        '''! Orthogonalize and normalize column vectors of A in order.'''
        
        # Setting up "a"
        if ((not issubclass(type(a), numpy.ndarray)) or
            (not numpy.asarray(a).flags.f_contiguous) or
            (not (a.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'a' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            a = numpy.asarray(a, dtype=ctypes.c_float, order='F')
        a_dim_1 = ctypes.c_int(a.shape[0])
        a_dim_2 = ctypes.c_int(a.shape[1])
    
        # Call C-accessible Fortran wrapper.
        clib.c_orthonormalize(ctypes.byref(a_dim_1), ctypes.byref(a_dim_2), ctypes.c_void_p(a.ctypes.data))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return a

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine RANDOM_UNIT_VECTORS
    
    def random_unit_vectors(self, column_vectors):
        '''! Generate randomly distributed vectors on the N-sphere.'''
        
        # Setting up "column_vectors"
        if ((not issubclass(type(column_vectors), numpy.ndarray)) or
            (not numpy.asarray(column_vectors).flags.f_contiguous) or
            (not (column_vectors.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'column_vectors' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            column_vectors = numpy.asarray(column_vectors, dtype=ctypes.c_float, order='F')
        column_vectors_dim_1 = ctypes.c_int(column_vectors.shape[0])
        column_vectors_dim_2 = ctypes.c_int(column_vectors.shape[1])
    
        # Call C-accessible Fortran wrapper.
        clib.c_random_unit_vectors(ctypes.byref(column_vectors_dim_1), ctypes.byref(column_vectors_dim_2), ctypes.c_void_p(column_vectors.ctypes.data))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return column_vectors

matrix_operations = matrix_operations()


class apos:
    '''! An apositional (/aggregate) and positional piecewise linear regression model.'''
    
    # This defines a C structure that can be used to hold this defined type.
    class MODEL_CONFIG(ctypes.Structure):
        # (name, ctype) fields for this structure.
        _fields_ = [("mdi", ctypes.c_int), ("mds", ctypes.c_int), ("mns", ctypes.c_int), ("mdo", ctypes.c_int), ("mne", ctypes.c_int), ("mde", ctypes.c_int), ("adi", ctypes.c_int), ("ads", ctypes.c_int), ("ans", ctypes.c_int), ("ado", ctypes.c_int), ("ane", ctypes.c_int), ("ade", ctypes.c_int), ("total_size", ctypes.c_int), ("num_vars", ctypes.c_int), ("msiv", ctypes.c_int), ("meiv", ctypes.c_int), ("msis", ctypes.c_int), ("meis", ctypes.c_int), ("mssv", ctypes.c_int), ("mesv", ctypes.c_int), ("msss", ctypes.c_int), ("mess", ctypes.c_int), ("msov", ctypes.c_int), ("meov", ctypes.c_int), ("msev", ctypes.c_int), ("meev", ctypes.c_int), ("asiv", ctypes.c_int), ("aeiv", ctypes.c_int), ("asis", ctypes.c_int), ("aeis", ctypes.c_int), ("assv", ctypes.c_int), ("aesv", ctypes.c_int), ("asss", ctypes.c_int), ("aess", ctypes.c_int), ("asov", ctypes.c_int), ("aeov", ctypes.c_int), ("asev", ctypes.c_int), ("aeev", ctypes.c_int), ("miss", ctypes.c_int), ("mise", ctypes.c_int), ("moss", ctypes.c_int), ("mose", ctypes.c_int), ("aiss", ctypes.c_int), ("aise", ctypes.c_int), ("discontinuity", ctypes.c_float), ("initial_shift_range", ctypes.c_float), ("initial_output_scale", ctypes.c_float), ("initial_step", ctypes.c_float), ("initial_step_mean_change", ctypes.c_float), ("initial_step_curv_change", ctypes.c_float), ("faster_rate", ctypes.c_float), ("slower_rate", ctypes.c_float), ("min_steps_to_stability", ctypes.c_int), ("num_threads", ctypes.c_int), ("keep_best", ctypes.c_bool), ("early_stop", ctypes.c_bool)]
        # Define an "__init__" that can take a class or keyword arguments as input.
        def __init__(self, value=0, **kwargs):
            # From whatever object (or dictionary) was given, assign internal values.
            self.mdi = kwargs.get("mdi", getattr(value, "mdi", value))
            self.mds = kwargs.get("mds", getattr(value, "mds", value))
            self.mns = kwargs.get("mns", getattr(value, "mns", value))
            self.mdo = kwargs.get("mdo", getattr(value, "mdo", value))
            self.mne = kwargs.get("mne", getattr(value, "mne", value))
            self.mde = kwargs.get("mde", getattr(value, "mde", value))
            self.adi = kwargs.get("adi", getattr(value, "adi", value))
            self.ads = kwargs.get("ads", getattr(value, "ads", value))
            self.ans = kwargs.get("ans", getattr(value, "ans", value))
            self.ado = kwargs.get("ado", getattr(value, "ado", value))
            self.ane = kwargs.get("ane", getattr(value, "ane", value))
            self.ade = kwargs.get("ade", getattr(value, "ade", value))
            self.total_size = kwargs.get("total_size", getattr(value, "total_size", value))
            self.num_vars = kwargs.get("num_vars", getattr(value, "num_vars", value))
            self.msiv = kwargs.get("msiv", getattr(value, "msiv", value))
            self.meiv = kwargs.get("meiv", getattr(value, "meiv", value))
            self.msis = kwargs.get("msis", getattr(value, "msis", value))
            self.meis = kwargs.get("meis", getattr(value, "meis", value))
            self.mssv = kwargs.get("mssv", getattr(value, "mssv", value))
            self.mesv = kwargs.get("mesv", getattr(value, "mesv", value))
            self.msss = kwargs.get("msss", getattr(value, "msss", value))
            self.mess = kwargs.get("mess", getattr(value, "mess", value))
            self.msov = kwargs.get("msov", getattr(value, "msov", value))
            self.meov = kwargs.get("meov", getattr(value, "meov", value))
            self.msev = kwargs.get("msev", getattr(value, "msev", value))
            self.meev = kwargs.get("meev", getattr(value, "meev", value))
            self.asiv = kwargs.get("asiv", getattr(value, "asiv", value))
            self.aeiv = kwargs.get("aeiv", getattr(value, "aeiv", value))
            self.asis = kwargs.get("asis", getattr(value, "asis", value))
            self.aeis = kwargs.get("aeis", getattr(value, "aeis", value))
            self.assv = kwargs.get("assv", getattr(value, "assv", value))
            self.aesv = kwargs.get("aesv", getattr(value, "aesv", value))
            self.asss = kwargs.get("asss", getattr(value, "asss", value))
            self.aess = kwargs.get("aess", getattr(value, "aess", value))
            self.asov = kwargs.get("asov", getattr(value, "asov", value))
            self.aeov = kwargs.get("aeov", getattr(value, "aeov", value))
            self.asev = kwargs.get("asev", getattr(value, "asev", value))
            self.aeev = kwargs.get("aeev", getattr(value, "aeev", value))
            self.miss = kwargs.get("miss", getattr(value, "miss", value))
            self.mise = kwargs.get("mise", getattr(value, "mise", value))
            self.moss = kwargs.get("moss", getattr(value, "moss", value))
            self.mose = kwargs.get("mose", getattr(value, "mose", value))
            self.aiss = kwargs.get("aiss", getattr(value, "aiss", value))
            self.aise = kwargs.get("aise", getattr(value, "aise", value))
            self.discontinuity = kwargs.get("discontinuity", getattr(value, "discontinuity", value))
            self.initial_shift_range = kwargs.get("initial_shift_range", getattr(value, "initial_shift_range", value))
            self.initial_output_scale = kwargs.get("initial_output_scale", getattr(value, "initial_output_scale", value))
            self.initial_step = kwargs.get("initial_step", getattr(value, "initial_step", value))
            self.initial_step_mean_change = kwargs.get("initial_step_mean_change", getattr(value, "initial_step_mean_change", value))
            self.initial_step_curv_change = kwargs.get("initial_step_curv_change", getattr(value, "initial_step_curv_change", value))
            self.faster_rate = kwargs.get("faster_rate", getattr(value, "faster_rate", value))
            self.slower_rate = kwargs.get("slower_rate", getattr(value, "slower_rate", value))
            self.min_steps_to_stability = kwargs.get("min_steps_to_stability", getattr(value, "min_steps_to_stability", value))
            self.num_threads = kwargs.get("num_threads", getattr(value, "num_threads", value))
            self.keep_best = kwargs.get("keep_best", getattr(value, "keep_best", value))
            self.early_stop = kwargs.get("early_stop", getattr(value, "early_stop", value))
    

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine NEW_MODEL_CONFIG
    
    def new_model_config(self, mdi, mdo, adi, mds=None, mns=None, mne=None, mde=None, ado=None, ads=None, ans=None, ane=None, ade=None, num_threads=None):
        '''! Generate a model configuration given state parameters for the model.
! Size related parameters.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "mdi"
        if (type(mdi) is not ctypes.c_int): mdi = ctypes.c_int(mdi)
        
        # Setting up "mdo"
        if (type(mdo) is not ctypes.c_int): mdo = ctypes.c_int(mdo)
        
        # Setting up "mds"
        mds_present = ctypes.c_bool(True)
        if (mds is None):
            mds_present = ctypes.c_bool(False)
            mds = ctypes.c_int()
        else:
            mds = ctypes.c_int(mds)
        if (type(mds) is not ctypes.c_int): mds = ctypes.c_int(mds)
        
        # Setting up "mns"
        mns_present = ctypes.c_bool(True)
        if (mns is None):
            mns_present = ctypes.c_bool(False)
            mns = ctypes.c_int()
        else:
            mns = ctypes.c_int(mns)
        if (type(mns) is not ctypes.c_int): mns = ctypes.c_int(mns)
        
        # Setting up "mne"
        mne_present = ctypes.c_bool(True)
        if (mne is None):
            mne_present = ctypes.c_bool(False)
            mne = ctypes.c_int()
        else:
            mne = ctypes.c_int(mne)
        if (type(mne) is not ctypes.c_int): mne = ctypes.c_int(mne)
        
        # Setting up "mde"
        mde_present = ctypes.c_bool(True)
        if (mde is None):
            mde_present = ctypes.c_bool(False)
            mde = ctypes.c_int()
        else:
            mde = ctypes.c_int(mde)
        if (type(mde) is not ctypes.c_int): mde = ctypes.c_int(mde)
        
        # Setting up "adi"
        if (type(adi) is not ctypes.c_int): adi = ctypes.c_int(adi)
        
        # Setting up "ado"
        ado_present = ctypes.c_bool(True)
        if (ado is None):
            ado_present = ctypes.c_bool(False)
            ado = ctypes.c_int()
        else:
            ado = ctypes.c_int(ado)
        if (type(ado) is not ctypes.c_int): ado = ctypes.c_int(ado)
        
        # Setting up "ads"
        ads_present = ctypes.c_bool(True)
        if (ads is None):
            ads_present = ctypes.c_bool(False)
            ads = ctypes.c_int()
        else:
            ads = ctypes.c_int(ads)
        if (type(ads) is not ctypes.c_int): ads = ctypes.c_int(ads)
        
        # Setting up "ans"
        ans_present = ctypes.c_bool(True)
        if (ans is None):
            ans_present = ctypes.c_bool(False)
            ans = ctypes.c_int()
        else:
            ans = ctypes.c_int(ans)
        if (type(ans) is not ctypes.c_int): ans = ctypes.c_int(ans)
        
        # Setting up "ane"
        ane_present = ctypes.c_bool(True)
        if (ane is None):
            ane_present = ctypes.c_bool(False)
            ane = ctypes.c_int()
        else:
            ane = ctypes.c_int(ane)
        if (type(ane) is not ctypes.c_int): ane = ctypes.c_int(ane)
        
        # Setting up "ade"
        ade_present = ctypes.c_bool(True)
        if (ade is None):
            ade_present = ctypes.c_bool(False)
            ade = ctypes.c_int()
        else:
            ade = ctypes.c_int(ade)
        if (type(ade) is not ctypes.c_int): ade = ctypes.c_int(ade)
        
        # Setting up "num_threads"
        num_threads_present = ctypes.c_bool(True)
        if (num_threads is None):
            num_threads_present = ctypes.c_bool(False)
            num_threads = ctypes.c_int()
        else:
            num_threads = ctypes.c_int(num_threads)
        if (type(num_threads) is not ctypes.c_int): num_threads = ctypes.c_int(num_threads)
        
        # Setting up "config"
        config = MODEL_CONFIG()
    
        # Call C-accessible Fortran wrapper.
        clib.c_new_model_config(ctypes.byref(mdi), ctypes.byref(mdo), ctypes.byref(mds_present), ctypes.byref(mds), ctypes.byref(mns_present), ctypes.byref(mns), ctypes.byref(mne_present), ctypes.byref(mne), ctypes.byref(mde_present), ctypes.byref(mde), ctypes.byref(adi), ctypes.byref(ado_present), ctypes.byref(ado), ctypes.byref(ads_present), ctypes.byref(ads), ctypes.byref(ans_present), ctypes.byref(ans), ctypes.byref(ane_present), ctypes.byref(ane), ctypes.byref(ade_present), ctypes.byref(ade), ctypes.byref(num_threads_present), ctypes.byref(num_threads), ctypes.byref(config))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return config

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine INIT_MODEL
    
    def init_model(self, config, model, seed=None):
        '''! Initialize the weights for a model, optionally provide a random seed.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "config"
        if (type(config) is not MODEL_CONFIG): config = MODEL_CONFIG(config)
        
        # Setting up "model"
        if ((not issubclass(type(model), numpy.ndarray)) or
            (not numpy.asarray(model).flags.f_contiguous) or
            (not (model.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'model' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            model = numpy.asarray(model, dtype=ctypes.c_float, order='F')
        model_dim_1 = ctypes.c_int(model.shape[0])
        
        # Setting up "seed"
        seed_present = ctypes.c_bool(True)
        if (seed is None):
            seed_present = ctypes.c_bool(False)
            seed = ctypes.c_int()
        else:
            seed = ctypes.c_int(seed)
        if (type(seed) is not ctypes.c_int): seed = ctypes.c_int(seed)
    
        # Call C-accessible Fortran wrapper.
        clib.c_init_model(ctypes.byref(config), ctypes.byref(model_dim_1), ctypes.c_void_p(model.ctypes.data), ctypes.byref(seed_present), ctypes.byref(seed))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return model

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine CHECK_SHAPE
    
    def check_shape(self, config, model, y, x, xi, ax, axi, sizes):
        '''! Returnn nonzero INFO if any shapes or values do not match expectations.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "config"
        if (type(config) is not MODEL_CONFIG): config = MODEL_CONFIG(config)
        
        # Setting up "model"
        if ((not issubclass(type(model), numpy.ndarray)) or
            (not numpy.asarray(model).flags.f_contiguous) or
            (not (model.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'model' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            model = numpy.asarray(model, dtype=ctypes.c_float, order='F')
        model_dim_1 = ctypes.c_int(model.shape[0])
        
        # Setting up "y"
        if ((not issubclass(type(y), numpy.ndarray)) or
            (not numpy.asarray(y).flags.f_contiguous) or
            (not (y.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'y' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            y = numpy.asarray(y, dtype=ctypes.c_float, order='F')
        y_dim_1 = ctypes.c_int(y.shape[0])
        y_dim_2 = ctypes.c_int(y.shape[1])
        
        # Setting up "x"
        if ((not issubclass(type(x), numpy.ndarray)) or
            (not numpy.asarray(x).flags.f_contiguous) or
            (not (x.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'x' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            x = numpy.asarray(x, dtype=ctypes.c_float, order='F')
        x_dim_1 = ctypes.c_int(x.shape[0])
        x_dim_2 = ctypes.c_int(x.shape[1])
        
        # Setting up "xi"
        if ((not issubclass(type(xi), numpy.ndarray)) or
            (not numpy.asarray(xi).flags.f_contiguous) or
            (not (xi.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'xi' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            xi = numpy.asarray(xi, dtype=ctypes.c_int, order='F')
        xi_dim_1 = ctypes.c_int(xi.shape[0])
        xi_dim_2 = ctypes.c_int(xi.shape[1])
        
        # Setting up "ax"
        if ((not issubclass(type(ax), numpy.ndarray)) or
            (not numpy.asarray(ax).flags.f_contiguous) or
            (not (ax.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ax' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ax = numpy.asarray(ax, dtype=ctypes.c_float, order='F')
        ax_dim_1 = ctypes.c_int(ax.shape[0])
        ax_dim_2 = ctypes.c_int(ax.shape[1])
        
        # Setting up "axi"
        if ((not issubclass(type(axi), numpy.ndarray)) or
            (not numpy.asarray(axi).flags.f_contiguous) or
            (not (axi.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'axi' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            axi = numpy.asarray(axi, dtype=ctypes.c_int, order='F')
        axi_dim_1 = ctypes.c_int(axi.shape[0])
        axi_dim_2 = ctypes.c_int(axi.shape[1])
        
        # Setting up "sizes"
        if ((not issubclass(type(sizes), numpy.ndarray)) or
            (not numpy.asarray(sizes).flags.f_contiguous) or
            (not (sizes.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'sizes' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            sizes = numpy.asarray(sizes, dtype=ctypes.c_int, order='F')
        sizes_dim_1 = ctypes.c_int(sizes.shape[0])
        
        # Setting up "info"
        info = ctypes.c_int()
    
        # Call C-accessible Fortran wrapper.
        clib.c_check_shape(ctypes.byref(config), ctypes.byref(model_dim_1), ctypes.c_void_p(model.ctypes.data), ctypes.byref(y_dim_1), ctypes.byref(y_dim_2), ctypes.c_void_p(y.ctypes.data), ctypes.byref(x_dim_1), ctypes.byref(x_dim_2), ctypes.c_void_p(x.ctypes.data), ctypes.byref(xi_dim_1), ctypes.byref(xi_dim_2), ctypes.c_void_p(xi.ctypes.data), ctypes.byref(ax_dim_1), ctypes.byref(ax_dim_2), ctypes.c_void_p(ax.ctypes.data), ctypes.byref(axi_dim_1), ctypes.byref(axi_dim_2), ctypes.c_void_p(axi.ctypes.data), ctypes.byref(sizes_dim_1), ctypes.c_void_p(sizes.ctypes.data), ctypes.byref(info))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return info.value

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine EMBED
    
    def embed(self, config, model, x, xi, ax, axi, xxi, axxi):
        '''! Given a model and mixed real and integer inputs, embed the inputs
!  into their appropriate real-value-only formats.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "config"
        if (type(config) is not MODEL_CONFIG): config = MODEL_CONFIG(config)
        
        # Setting up "model"
        if ((not issubclass(type(model), numpy.ndarray)) or
            (not numpy.asarray(model).flags.f_contiguous) or
            (not (model.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'model' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            model = numpy.asarray(model, dtype=ctypes.c_float, order='F')
        model_dim_1 = ctypes.c_int(model.shape[0])
        
        # Setting up "x"
        if ((not issubclass(type(x), numpy.ndarray)) or
            (not numpy.asarray(x).flags.f_contiguous) or
            (not (x.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'x' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            x = numpy.asarray(x, dtype=ctypes.c_float, order='F')
        x_dim_1 = ctypes.c_int(x.shape[0])
        x_dim_2 = ctypes.c_int(x.shape[1])
        
        # Setting up "xi"
        if ((not issubclass(type(xi), numpy.ndarray)) or
            (not numpy.asarray(xi).flags.f_contiguous) or
            (not (xi.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'xi' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            xi = numpy.asarray(xi, dtype=ctypes.c_int, order='F')
        xi_dim_1 = ctypes.c_int(xi.shape[0])
        xi_dim_2 = ctypes.c_int(xi.shape[1])
        
        # Setting up "ax"
        if ((not issubclass(type(ax), numpy.ndarray)) or
            (not numpy.asarray(ax).flags.f_contiguous) or
            (not (ax.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ax' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ax = numpy.asarray(ax, dtype=ctypes.c_float, order='F')
        ax_dim_1 = ctypes.c_int(ax.shape[0])
        ax_dim_2 = ctypes.c_int(ax.shape[1])
        
        # Setting up "axi"
        if ((not issubclass(type(axi), numpy.ndarray)) or
            (not numpy.asarray(axi).flags.f_contiguous) or
            (not (axi.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'axi' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            axi = numpy.asarray(axi, dtype=ctypes.c_int, order='F')
        axi_dim_1 = ctypes.c_int(axi.shape[0])
        axi_dim_2 = ctypes.c_int(axi.shape[1])
        
        # Setting up "xxi"
        if ((not issubclass(type(xxi), numpy.ndarray)) or
            (not numpy.asarray(xxi).flags.f_contiguous) or
            (not (xxi.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'xxi' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            xxi = numpy.asarray(xxi, dtype=ctypes.c_float, order='F')
        xxi_dim_1 = ctypes.c_int(xxi.shape[0])
        xxi_dim_2 = ctypes.c_int(xxi.shape[1])
        
        # Setting up "axxi"
        if ((not issubclass(type(axxi), numpy.ndarray)) or
            (not numpy.asarray(axxi).flags.f_contiguous) or
            (not (axxi.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'axxi' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            axxi = numpy.asarray(axxi, dtype=ctypes.c_float, order='F')
        axxi_dim_1 = ctypes.c_int(axxi.shape[0])
        axxi_dim_2 = ctypes.c_int(axxi.shape[1])
    
        # Call C-accessible Fortran wrapper.
        clib.c_embed(ctypes.byref(config), ctypes.byref(model_dim_1), ctypes.c_void_p(model.ctypes.data), ctypes.byref(x_dim_1), ctypes.byref(x_dim_2), ctypes.c_void_p(x.ctypes.data), ctypes.byref(xi_dim_1), ctypes.byref(xi_dim_2), ctypes.c_void_p(xi.ctypes.data), ctypes.byref(ax_dim_1), ctypes.byref(ax_dim_2), ctypes.c_void_p(ax.ctypes.data), ctypes.byref(axi_dim_1), ctypes.byref(axi_dim_2), ctypes.c_void_p(axi.ctypes.data), ctypes.byref(xxi_dim_1), ctypes.byref(xxi_dim_2), ctypes.c_void_p(xxi.ctypes.data), ctypes.byref(axxi_dim_1), ctypes.byref(axxi_dim_2), ctypes.c_void_p(axxi.ctypes.data))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return xxi, axxi

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine COMPUTE_BATCHES
    
    def compute_batches(self, num_batches, x, ax, sizes, batcha, batchm, info):
        '''! Given a number of batches, compute the batch start and ends for
!  the apositional and positional inputs. Store in (2,_) arrays.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "num_batches"
        if (type(num_batches) is not ctypes.c_int): num_batches = ctypes.c_int(num_batches)
        
        # Setting up "x"
        if ((not issubclass(type(x), numpy.ndarray)) or
            (not numpy.asarray(x).flags.f_contiguous) or
            (not (x.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'x' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            x = numpy.asarray(x, dtype=ctypes.c_float, order='F')
        x_dim_1 = ctypes.c_int(x.shape[0])
        x_dim_2 = ctypes.c_int(x.shape[1])
        
        # Setting up "ax"
        if ((not issubclass(type(ax), numpy.ndarray)) or
            (not numpy.asarray(ax).flags.f_contiguous) or
            (not (ax.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ax' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ax = numpy.asarray(ax, dtype=ctypes.c_float, order='F')
        ax_dim_1 = ctypes.c_int(ax.shape[0])
        ax_dim_2 = ctypes.c_int(ax.shape[1])
        
        # Setting up "sizes"
        if ((not issubclass(type(sizes), numpy.ndarray)) or
            (not numpy.asarray(sizes).flags.f_contiguous) or
            (not (sizes.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'sizes' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            sizes = numpy.asarray(sizes, dtype=ctypes.c_int, order='F')
        sizes_dim_1 = ctypes.c_int(sizes.shape[0])
        
        # Setting up "batcha"
        if ((not issubclass(type(batcha), numpy.ndarray)) or
            (not numpy.asarray(batcha).flags.f_contiguous) or
            (not (batcha.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'batcha' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            batcha = numpy.asarray(batcha, dtype=ctypes.c_int, order='F')
        batcha_dim_1 = ctypes.c_int(batcha.shape[0])
        batcha_dim_2 = ctypes.c_int(batcha.shape[1])
        
        # Setting up "batchm"
        if ((not issubclass(type(batchm), numpy.ndarray)) or
            (not numpy.asarray(batchm).flags.f_contiguous) or
            (not (batchm.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'batchm' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            batchm = numpy.asarray(batchm, dtype=ctypes.c_int, order='F')
        batchm_dim_1 = ctypes.c_int(batchm.shape[0])
        batchm_dim_2 = ctypes.c_int(batchm.shape[1])
        
        # Setting up "info"
        if (type(info) is not ctypes.c_int): info = ctypes.c_int(info)
    
        # Call C-accessible Fortran wrapper.
        clib.c_compute_batches(ctypes.byref(num_batches), ctypes.byref(x_dim_1), ctypes.byref(x_dim_2), ctypes.c_void_p(x.ctypes.data), ctypes.byref(ax_dim_1), ctypes.byref(ax_dim_2), ctypes.c_void_p(ax.ctypes.data), ctypes.byref(sizes_dim_1), ctypes.c_void_p(sizes.ctypes.data), ctypes.byref(batcha_dim_1), ctypes.byref(batcha_dim_2), ctypes.c_void_p(batcha.ctypes.data), ctypes.byref(batchm_dim_1), ctypes.byref(batchm_dim_2), ctypes.c_void_p(batchm.ctypes.data), ctypes.byref(info))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return batcha, batchm, info.value

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine EVALUATE
    
    def evaluate(self, config, model, y, x, ax, sizes, m_states, a_states, ay, info, shift=None, threads=None):
        '''! Evaluate the piecewise linear regression model, assume already-embedded inputs.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "config"
        if (type(config) is not MODEL_CONFIG): config = MODEL_CONFIG(config)
        
        # Setting up "model"
        if ((not issubclass(type(model), numpy.ndarray)) or
            (not numpy.asarray(model).flags.f_contiguous) or
            (not (model.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'model' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            model = numpy.asarray(model, dtype=ctypes.c_float, order='F')
        model_dim_1 = ctypes.c_int(model.shape[0])
        
        # Setting up "y"
        if ((not issubclass(type(y), numpy.ndarray)) or
            (not numpy.asarray(y).flags.f_contiguous) or
            (not (y.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'y' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            y = numpy.asarray(y, dtype=ctypes.c_float, order='F')
        y_dim_1 = ctypes.c_int(y.shape[0])
        y_dim_2 = ctypes.c_int(y.shape[1])
        
        # Setting up "x"
        if ((not issubclass(type(x), numpy.ndarray)) or
            (not numpy.asarray(x).flags.f_contiguous) or
            (not (x.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'x' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            x = numpy.asarray(x, dtype=ctypes.c_float, order='F')
        x_dim_1 = ctypes.c_int(x.shape[0])
        x_dim_2 = ctypes.c_int(x.shape[1])
        
        # Setting up "ax"
        if ((not issubclass(type(ax), numpy.ndarray)) or
            (not numpy.asarray(ax).flags.f_contiguous) or
            (not (ax.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ax' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ax = numpy.asarray(ax, dtype=ctypes.c_float, order='F')
        ax_dim_1 = ctypes.c_int(ax.shape[0])
        ax_dim_2 = ctypes.c_int(ax.shape[1])
        
        # Setting up "sizes"
        if ((not issubclass(type(sizes), numpy.ndarray)) or
            (not numpy.asarray(sizes).flags.f_contiguous) or
            (not (sizes.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'sizes' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            sizes = numpy.asarray(sizes, dtype=ctypes.c_int, order='F')
        sizes_dim_1 = ctypes.c_int(sizes.shape[0])
        
        # Setting up "m_states"
        if ((not issubclass(type(m_states), numpy.ndarray)) or
            (not numpy.asarray(m_states).flags.f_contiguous) or
            (not (m_states.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'm_states' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            m_states = numpy.asarray(m_states, dtype=ctypes.c_float, order='F')
        m_states_dim_1 = ctypes.c_int(m_states.shape[0])
        m_states_dim_2 = ctypes.c_int(m_states.shape[1])
        m_states_dim_3 = ctypes.c_int(m_states.shape[2])
        
        # Setting up "a_states"
        if ((not issubclass(type(a_states), numpy.ndarray)) or
            (not numpy.asarray(a_states).flags.f_contiguous) or
            (not (a_states.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'a_states' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            a_states = numpy.asarray(a_states, dtype=ctypes.c_float, order='F')
        a_states_dim_1 = ctypes.c_int(a_states.shape[0])
        a_states_dim_2 = ctypes.c_int(a_states.shape[1])
        a_states_dim_3 = ctypes.c_int(a_states.shape[2])
        
        # Setting up "ay"
        if ((not issubclass(type(ay), numpy.ndarray)) or
            (not numpy.asarray(ay).flags.f_contiguous) or
            (not (ay.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ay' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ay = numpy.asarray(ay, dtype=ctypes.c_float, order='F')
        ay_dim_1 = ctypes.c_int(ay.shape[0])
        ay_dim_2 = ctypes.c_int(ay.shape[1])
        
        # Setting up "info"
        if (type(info) is not ctypes.c_int): info = ctypes.c_int(info)
        
        # Setting up "shift"
        shift_present = ctypes.c_bool(True)
        if (shift is None):
            shift_present = ctypes.c_bool(False)
            shift = ctypes.c_bool()
        else:
            shift = ctypes.c_bool(shift)
        if (type(shift) is not ctypes.c_bool): shift = ctypes.c_bool(shift)
        
        # Setting up "threads"
        threads_present = ctypes.c_bool(True)
        if (threads is None):
            threads_present = ctypes.c_bool(False)
            threads = ctypes.c_int()
        else:
            threads = ctypes.c_int(threads)
        if (type(threads) is not ctypes.c_int): threads = ctypes.c_int(threads)
    
        # Call C-accessible Fortran wrapper.
        clib.c_evaluate(ctypes.byref(config), ctypes.byref(model_dim_1), ctypes.c_void_p(model.ctypes.data), ctypes.byref(y_dim_1), ctypes.byref(y_dim_2), ctypes.c_void_p(y.ctypes.data), ctypes.byref(x_dim_1), ctypes.byref(x_dim_2), ctypes.c_void_p(x.ctypes.data), ctypes.byref(ax_dim_1), ctypes.byref(ax_dim_2), ctypes.c_void_p(ax.ctypes.data), ctypes.byref(sizes_dim_1), ctypes.c_void_p(sizes.ctypes.data), ctypes.byref(m_states_dim_1), ctypes.byref(m_states_dim_2), ctypes.byref(m_states_dim_3), ctypes.c_void_p(m_states.ctypes.data), ctypes.byref(a_states_dim_1), ctypes.byref(a_states_dim_2), ctypes.byref(a_states_dim_3), ctypes.c_void_p(a_states.ctypes.data), ctypes.byref(ay_dim_1), ctypes.byref(ay_dim_2), ctypes.c_void_p(ay.ctypes.data), ctypes.byref(info), ctypes.byref(shift_present), ctypes.byref(shift), ctypes.byref(threads_present), ctypes.byref(threads))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return y, x, ax, m_states, a_states, ay, info.value

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine BASIS_GRADIENT
    
    def basis_gradient(self, config, model, y, x, ax, sizes, m_states, a_states, ay, grad, skip_embeddings=None):
        '''! Given the values at all internal states in the model and an output
!  gradient, propogate the output gradient through the model and
!  return the gradient of all basis functions.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "config"
        if (type(config) is not MODEL_CONFIG): config = MODEL_CONFIG(config)
        
        # Setting up "model"
        if ((not issubclass(type(model), numpy.ndarray)) or
            (not numpy.asarray(model).flags.f_contiguous) or
            (not (model.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'model' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            model = numpy.asarray(model, dtype=ctypes.c_float, order='F')
        model_dim_1 = ctypes.c_int(model.shape[0])
        
        # Setting up "y"
        if ((not issubclass(type(y), numpy.ndarray)) or
            (not numpy.asarray(y).flags.f_contiguous) or
            (not (y.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'y' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            y = numpy.asarray(y, dtype=ctypes.c_float, order='F')
        y_dim_1 = ctypes.c_int(y.shape[0])
        y_dim_2 = ctypes.c_int(y.shape[1])
        
        # Setting up "x"
        if ((not issubclass(type(x), numpy.ndarray)) or
            (not numpy.asarray(x).flags.f_contiguous) or
            (not (x.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'x' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            x = numpy.asarray(x, dtype=ctypes.c_float, order='F')
        x_dim_1 = ctypes.c_int(x.shape[0])
        x_dim_2 = ctypes.c_int(x.shape[1])
        
        # Setting up "ax"
        if ((not issubclass(type(ax), numpy.ndarray)) or
            (not numpy.asarray(ax).flags.f_contiguous) or
            (not (ax.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ax' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ax = numpy.asarray(ax, dtype=ctypes.c_float, order='F')
        ax_dim_1 = ctypes.c_int(ax.shape[0])
        ax_dim_2 = ctypes.c_int(ax.shape[1])
        
        # Setting up "sizes"
        if ((not issubclass(type(sizes), numpy.ndarray)) or
            (not numpy.asarray(sizes).flags.f_contiguous) or
            (not (sizes.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'sizes' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            sizes = numpy.asarray(sizes, dtype=ctypes.c_int, order='F')
        sizes_dim_1 = ctypes.c_int(sizes.shape[0])
        
        # Setting up "m_states"
        if ((not issubclass(type(m_states), numpy.ndarray)) or
            (not numpy.asarray(m_states).flags.f_contiguous) or
            (not (m_states.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'm_states' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            m_states = numpy.asarray(m_states, dtype=ctypes.c_float, order='F')
        m_states_dim_1 = ctypes.c_int(m_states.shape[0])
        m_states_dim_2 = ctypes.c_int(m_states.shape[1])
        m_states_dim_3 = ctypes.c_int(m_states.shape[2])
        
        # Setting up "a_states"
        if ((not issubclass(type(a_states), numpy.ndarray)) or
            (not numpy.asarray(a_states).flags.f_contiguous) or
            (not (a_states.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'a_states' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            a_states = numpy.asarray(a_states, dtype=ctypes.c_float, order='F')
        a_states_dim_1 = ctypes.c_int(a_states.shape[0])
        a_states_dim_2 = ctypes.c_int(a_states.shape[1])
        a_states_dim_3 = ctypes.c_int(a_states.shape[2])
        
        # Setting up "ay"
        if ((not issubclass(type(ay), numpy.ndarray)) or
            (not numpy.asarray(ay).flags.f_contiguous) or
            (not (ay.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ay' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ay = numpy.asarray(ay, dtype=ctypes.c_float, order='F')
        ay_dim_1 = ctypes.c_int(ay.shape[0])
        ay_dim_2 = ctypes.c_int(ay.shape[1])
        
        # Setting up "grad"
        if ((not issubclass(type(grad), numpy.ndarray)) or
            (not numpy.asarray(grad).flags.f_contiguous) or
            (not (grad.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'grad' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            grad = numpy.asarray(grad, dtype=ctypes.c_float, order='F')
        grad_dim_1 = ctypes.c_int(grad.shape[0])
        
        # Setting up "skip_embeddings"
        skip_embeddings_present = ctypes.c_bool(True)
        if (skip_embeddings is None):
            skip_embeddings_present = ctypes.c_bool(False)
            skip_embeddings = ctypes.c_bool()
        else:
            skip_embeddings = ctypes.c_bool(skip_embeddings)
        if (type(skip_embeddings) is not ctypes.c_bool): skip_embeddings = ctypes.c_bool(skip_embeddings)
    
        # Call C-accessible Fortran wrapper.
        clib.c_basis_gradient(ctypes.byref(config), ctypes.byref(model_dim_1), ctypes.c_void_p(model.ctypes.data), ctypes.byref(y_dim_1), ctypes.byref(y_dim_2), ctypes.c_void_p(y.ctypes.data), ctypes.byref(x_dim_1), ctypes.byref(x_dim_2), ctypes.c_void_p(x.ctypes.data), ctypes.byref(ax_dim_1), ctypes.byref(ax_dim_2), ctypes.c_void_p(ax.ctypes.data), ctypes.byref(sizes_dim_1), ctypes.c_void_p(sizes.ctypes.data), ctypes.byref(m_states_dim_1), ctypes.byref(m_states_dim_2), ctypes.byref(m_states_dim_3), ctypes.c_void_p(m_states.ctypes.data), ctypes.byref(a_states_dim_1), ctypes.byref(a_states_dim_2), ctypes.byref(a_states_dim_3), ctypes.c_void_p(a_states.ctypes.data), ctypes.byref(ay_dim_1), ctypes.byref(ay_dim_2), ctypes.c_void_p(ay.ctypes.data), ctypes.byref(grad_dim_1), ctypes.c_void_p(grad.ctypes.data), ctypes.byref(skip_embeddings_present), ctypes.byref(skip_embeddings))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return x, ax, m_states, a_states, ay, grad

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine EMBEDDING_GRADIENT
    
    def embedding_gradient(self, mde, mne, int_inputs, grad, embedding_grad):
        '''! Compute the gradient with respect to embeddings given the input
!  gradient by aggregating over the repeated occurrences of the embedding.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "mde"
        if (type(mde) is not ctypes.c_int): mde = ctypes.c_int(mde)
        
        # Setting up "mne"
        if (type(mne) is not ctypes.c_int): mne = ctypes.c_int(mne)
        
        # Setting up "int_inputs"
        if ((not issubclass(type(int_inputs), numpy.ndarray)) or
            (not numpy.asarray(int_inputs).flags.f_contiguous) or
            (not (int_inputs.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'int_inputs' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            int_inputs = numpy.asarray(int_inputs, dtype=ctypes.c_int, order='F')
        int_inputs_dim_1 = ctypes.c_int(int_inputs.shape[0])
        int_inputs_dim_2 = ctypes.c_int(int_inputs.shape[1])
        
        # Setting up "grad"
        if ((not issubclass(type(grad), numpy.ndarray)) or
            (not numpy.asarray(grad).flags.f_contiguous) or
            (not (grad.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'grad' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            grad = numpy.asarray(grad, dtype=ctypes.c_float, order='F')
        grad_dim_1 = ctypes.c_int(grad.shape[0])
        grad_dim_2 = ctypes.c_int(grad.shape[1])
        
        # Setting up "embedding_grad"
        if ((not issubclass(type(embedding_grad), numpy.ndarray)) or
            (not numpy.asarray(embedding_grad).flags.f_contiguous) or
            (not (embedding_grad.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'embedding_grad' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            embedding_grad = numpy.asarray(embedding_grad, dtype=ctypes.c_float, order='F')
        embedding_grad_dim_1 = ctypes.c_int(embedding_grad.shape[0])
        embedding_grad_dim_2 = ctypes.c_int(embedding_grad.shape[1])
    
        # Call C-accessible Fortran wrapper.
        clib.c_embedding_gradient(ctypes.byref(mde), ctypes.byref(mne), ctypes.byref(int_inputs_dim_1), ctypes.byref(int_inputs_dim_2), ctypes.c_void_p(int_inputs.ctypes.data), ctypes.byref(grad_dim_1), ctypes.byref(grad_dim_2), ctypes.c_void_p(grad.ctypes.data), ctypes.byref(embedding_grad_dim_1), ctypes.byref(embedding_grad_dim_2), ctypes.c_void_p(embedding_grad.ctypes.data))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return embedding_grad

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine SQUARED_ERROR_GRADIENT
    
    def squared_error_gradient(self, targets, outputs):
        '''! Compute the sum of squared error, store the gradient in the OUTPUTS.
!   TARGETS - row vectors containing target values
!   OUTPUTS - column vectors containing model predictions'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "targets"
        if ((not issubclass(type(targets), numpy.ndarray)) or
            (not numpy.asarray(targets).flags.f_contiguous) or
            (not (targets.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'targets' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            targets = numpy.asarray(targets, dtype=ctypes.c_float, order='F')
        targets_dim_1 = ctypes.c_int(targets.shape[0])
        targets_dim_2 = ctypes.c_int(targets.shape[1])
        
        # Setting up "outputs"
        if ((not issubclass(type(outputs), numpy.ndarray)) or
            (not numpy.asarray(outputs).flags.f_contiguous) or
            (not (outputs.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'outputs' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            outputs = numpy.asarray(outputs, dtype=ctypes.c_float, order='F')
        outputs_dim_1 = ctypes.c_int(outputs.shape[0])
        outputs_dim_2 = ctypes.c_int(outputs.shape[1])
    
        # Call C-accessible Fortran wrapper.
        clib.c_squared_error_gradient(ctypes.byref(targets_dim_1), ctypes.byref(targets_dim_2), ctypes.c_void_p(targets.ctypes.data), ctypes.byref(outputs_dim_1), ctypes.byref(outputs_dim_2), ctypes.c_void_p(outputs.ctypes.data))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return outputs

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine TRUE_VALUE_GRADIENT
    
    def true_value_gradient(self, targets, outputs):
        '''! Produce the true values as the gradient (which will show large
!  magnitudes for parameters in the model that align with values).'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "targets"
        if ((not issubclass(type(targets), numpy.ndarray)) or
            (not numpy.asarray(targets).flags.f_contiguous) or
            (not (targets.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'targets' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            targets = numpy.asarray(targets, dtype=ctypes.c_float, order='F')
        targets_dim_1 = ctypes.c_int(targets.shape[0])
        targets_dim_2 = ctypes.c_int(targets.shape[1])
        
        # Setting up "outputs"
        if ((not issubclass(type(outputs), numpy.ndarray)) or
            (not numpy.asarray(outputs).flags.f_contiguous) or
            (not (outputs.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'outputs' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            outputs = numpy.asarray(outputs, dtype=ctypes.c_float, order='F')
        outputs_dim_1 = ctypes.c_int(outputs.shape[0])
        outputs_dim_2 = ctypes.c_int(outputs.shape[1])
    
        # Call C-accessible Fortran wrapper.
        clib.c_true_value_gradient(ctypes.byref(targets_dim_1), ctypes.byref(targets_dim_2), ctypes.c_void_p(targets.ctypes.data), ctypes.byref(outputs_dim_1), ctypes.byref(outputs_dim_2), ctypes.c_void_p(outputs.ctypes.data))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return outputs

    
    # ----------------------------------------------
    # Wrapper for the Fortran subroutine MINIMIZE_MSE
    
    def minimize_mse(self, config, model, y, x, xi, ax, axi, sizes, steps, record=None):
        '''! Fit input / output pairs by minimizing mean squared error.'''
        MODEL_CONFIG = apos.MODEL_CONFIG
        
        # Setting up "config"
        if (type(config) is not MODEL_CONFIG): config = MODEL_CONFIG(config)
        
        # Setting up "model"
        if ((not issubclass(type(model), numpy.ndarray)) or
            (not numpy.asarray(model).flags.f_contiguous) or
            (not (model.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'model' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            model = numpy.asarray(model, dtype=ctypes.c_float, order='F')
        model_dim_1 = ctypes.c_int(model.shape[0])
        
        # Setting up "y"
        if ((not issubclass(type(y), numpy.ndarray)) or
            (not numpy.asarray(y).flags.f_contiguous) or
            (not (y.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'y' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            y = numpy.asarray(y, dtype=ctypes.c_float, order='F')
        y_dim_1 = ctypes.c_int(y.shape[0])
        y_dim_2 = ctypes.c_int(y.shape[1])
        
        # Setting up "x"
        if ((not issubclass(type(x), numpy.ndarray)) or
            (not numpy.asarray(x).flags.f_contiguous) or
            (not (x.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'x' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            x = numpy.asarray(x, dtype=ctypes.c_float, order='F')
        x_dim_1 = ctypes.c_int(x.shape[0])
        x_dim_2 = ctypes.c_int(x.shape[1])
        
        # Setting up "xi"
        if ((not issubclass(type(xi), numpy.ndarray)) or
            (not numpy.asarray(xi).flags.f_contiguous) or
            (not (xi.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'xi' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            xi = numpy.asarray(xi, dtype=ctypes.c_int, order='F')
        xi_dim_1 = ctypes.c_int(xi.shape[0])
        xi_dim_2 = ctypes.c_int(xi.shape[1])
        
        # Setting up "ax"
        if ((not issubclass(type(ax), numpy.ndarray)) or
            (not numpy.asarray(ax).flags.f_contiguous) or
            (not (ax.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'ax' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            ax = numpy.asarray(ax, dtype=ctypes.c_float, order='F')
        ax_dim_1 = ctypes.c_int(ax.shape[0])
        ax_dim_2 = ctypes.c_int(ax.shape[1])
        
        # Setting up "axi"
        if ((not issubclass(type(axi), numpy.ndarray)) or
            (not numpy.asarray(axi).flags.f_contiguous) or
            (not (axi.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'axi' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            axi = numpy.asarray(axi, dtype=ctypes.c_int, order='F')
        axi_dim_1 = ctypes.c_int(axi.shape[0])
        axi_dim_2 = ctypes.c_int(axi.shape[1])
        
        # Setting up "sizes"
        if ((not issubclass(type(sizes), numpy.ndarray)) or
            (not numpy.asarray(sizes).flags.f_contiguous) or
            (not (sizes.dtype == numpy.dtype(ctypes.c_int)))):
            import warnings
            warnings.warn("The provided argument 'sizes' was not an f_contiguous NumPy array of type 'ctypes.c_int' (or equivalent). Automatically converting (probably creating a full copy).")
            sizes = numpy.asarray(sizes, dtype=ctypes.c_int, order='F')
        sizes_dim_1 = ctypes.c_int(sizes.shape[0])
        
        # Setting up "steps"
        if (type(steps) is not ctypes.c_int): steps = ctypes.c_int(steps)
        
        # Setting up "sum_squared_error"
        sum_squared_error = ctypes.c_float()
        
        # Setting up "record"
        record_present = ctypes.c_bool(True)
        if (record is None):
            record_present = ctypes.c_bool(False)
            record = numpy.zeros(shape=(1,1), dtype=ctypes.c_float, order='F')
        elif (type(record) == bool) and (record):
            record = numpy.zeros(shape=(4, steps), dtype=ctypes.c_float, order='F')
        elif ((not issubclass(type(record), numpy.ndarray)) or
              (not numpy.asarray(record).flags.f_contiguous) or
              (not (record.dtype == numpy.dtype(ctypes.c_float)))):
            import warnings
            warnings.warn("The provided argument 'record' was not an f_contiguous NumPy array of type 'ctypes.c_float' (or equivalent). Automatically converting (probably creating a full copy).")
            record = numpy.asarray(record, dtype=ctypes.c_float, order='F')
        if (record_present):
            record_dim_1 = ctypes.c_int(record.shape[0])
            record_dim_2 = ctypes.c_int(record.shape[1])
        else:
            record_dim_1 = ctypes.c_int()
            record_dim_2 = ctypes.c_int()
        
        # Setting up "info"
        info = ctypes.c_int()
    
        # Call C-accessible Fortran wrapper.
        clib.c_minimize_mse(ctypes.byref(config), ctypes.byref(model_dim_1), ctypes.c_void_p(model.ctypes.data), ctypes.byref(y_dim_1), ctypes.byref(y_dim_2), ctypes.c_void_p(y.ctypes.data), ctypes.byref(x_dim_1), ctypes.byref(x_dim_2), ctypes.c_void_p(x.ctypes.data), ctypes.byref(xi_dim_1), ctypes.byref(xi_dim_2), ctypes.c_void_p(xi.ctypes.data), ctypes.byref(ax_dim_1), ctypes.byref(ax_dim_2), ctypes.c_void_p(ax.ctypes.data), ctypes.byref(axi_dim_1), ctypes.byref(axi_dim_2), ctypes.c_void_p(axi.ctypes.data), ctypes.byref(sizes_dim_1), ctypes.c_void_p(sizes.ctypes.data), ctypes.byref(steps), ctypes.byref(sum_squared_error), ctypes.byref(record_present), ctypes.byref(record_dim_1), ctypes.byref(record_dim_2), ctypes.c_void_p(record.ctypes.data), ctypes.byref(info))
    
        # Return final results, 'INTENT(OUT)' arguments only.
        return model, y, x, ax, sum_squared_error.value, (record if record_present else None), info.value

apos = apos()

