2011 Oct 02



// ==========================================================
// Filename:    space.cpp
// Description: Contains the python wrappers for the Cartesian::space
//              objects.
//
//              Hand written in C/C++ with a lot of repition, but
//              this is a proof of concept demonstration.
//
//              Consider boost.python or SWIG for production.
//
//              This demonstrates a C++ way around how python wrapped
//              in C uses staticforward. new_SpaceType(Space** a_space)
//              wraps PyObject_New(Space, &SpaceType) so as_number
//              operators defined in SpaceType can hold a reference for
//              how to construct a new SpaceType before the definition
//              SpaceType is complete. Similarlly for is_SpaceType().
//
//
// See also:   http://docs.python.org/extending/newtypes.html
//             http://docs.python.org/c-api/complex.html
//
//
// Tests:       >>> help(orbits) # to see doc strings after import.
//
// Author:      L.R. McFarland
// Created:     2011aug14
//
//  This file is part of lrm's Orbits software library.
//
//  Orbits is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  Orbits is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with Orbits.  If not, see <http://www.gnu.org/licenses/>.
// ==========================================================

#include <Python.h> // must be first
#include <structmember.h> // part of python

#include <sstream>

#include <space.h>


// ========================
// ===== constructors =====
// ========================

PyObject* space_exception;

// Space object definition.
typedef struct {
  PyObject_HEAD
  Cartesian::space m_space;
} Space;


// Forward declarations for as_number methods. Wraps SpaceType definition.
static void new_SpaceType(Space** a_space);
static int is_SpaceType(PyObject* a_space);


static PyObject* Space_new(PyTypeObject* type, PyObject* args, PyObject* kwds) {

  // caller owns this, m_space takes care of own memory, no py objects
  // created

  Space* self(NULL);
  self = (Space*)type->tp_alloc(type, 0); // TODO new style cast?

  // TOOD init x,y,z?

  return (PyObject*)self;
}

static int Space_init(Space* self, PyObject* args, PyObject* kwds) {

  // Cartesian::space data members are private and not directly
  // accessible, so a wrapper is needed.

  double x(0);
  double y(0);
  double z(0);

  static char* kwlist[] = {"x", "y", "z", NULL};

  if (! PyArg_ParseTupleAndKeywords(args, kwds, "|ddd", kwlist, &x, &y, &z))
    return -1;

  self->m_space.x(x);
  self->m_space.y(y);
  self->m_space.z(z);

  return 0;
}

static void Space_dealloc(Space* self) {
  self->ob_type->tp_free((PyObject*)self);
}

// =================
// ===== print =====
// =================

PyObject* Space_str(PyObject* self) {
  std::stringstream result;
  result << ((Space*)self)->m_space;
  return PyString_FromString(result.str().c_str());
}

PyObject* Space_repr(PyObject* self) {
  const Cartesian::space& a_space(((Space*)self)->m_space);
  std::stringstream result;
  result << "("
         << a_space.x() << ", "
         << a_space.y() << ", "
         << a_space.z() << ")";
  return PyString_FromString(result.str().c_str());
}

// ===============================
// ===== getters and setters =====
// ===============================

// TODO template for x, y and z

// -------------
// ----- X -----
// -------------

static PyObject* Space_getx(Space* self, void* closure) {
  // TODO reference count? exception?
  return PyFloat_FromDouble(self->m_space.x());
}

static int Space_setx(Space* self, PyObject* value, void* closure) {

  if (value == NULL) {
    PyErr_SetString(space_exception, "Cannot delete x");
    return NULL;
  }

  if (!PyFloat_Check(value) && !PyInt_Check(value)) {
    PyErr_SetString(space_exception, "x must be a float");
    return NULL;
  }

  self->m_space.x(PyFloat_AsDouble(value));

  return 0;
}

// -------------
// ----- Y -----
// -------------

static PyObject* Space_gety(Space* self, void* closure) {
  // TODO reference count? exception?
  return PyFloat_FromDouble(self->m_space.y());
}

static int Space_sety(Space* self, PyObject* value, void* closure) {

  if (value == NULL) {
    PyErr_SetString(space_exception, "Cannot delete y");
    return NULL;
  }

  if (!PyFloat_Check(value) && !PyInt_Check(value)) {
    PyErr_SetString(space_exception, "y must be a float");
    return NULL;
  }

  self->m_space.y(PyFloat_AsDouble(value));

  return 0;
}

// -------------
// ----- Z -----
// -------------

static PyObject* Space_getz(Space* self, void* closure) {
  // TODO reference count? exception?
  return PyFloat_FromDouble(self->m_space.z());
}

static int Space_setz(Space* self, PyObject* value, void* closure) {

  if (value == NULL) {
    PyErr_SetString(space_exception, "Cannot delete z");
    return NULL;
  }

  if (!PyFloat_Check(value) && !PyInt_Check(value)) {
    PyErr_SetString(space_exception, "z must be a float");
    return NULL;
  }

  self->m_space.z(PyFloat_AsDouble(value));

  return 0;
}


// ==========================
// ===== number methods =====
// ==========================


static PyObject* nb_add(PyObject* o1, PyObject* o2) {

  if (!is_SpaceType(o1) || !is_SpaceType(o2)) {
    Py_INCREF(Py_NotImplemented);
    return Py_NotImplemented;
  }

  Space* result_space(NULL);

  new_SpaceType(&result_space);

  if (result_space == NULL) {
    PyErr_SetString(space_exception, "nb_add failed to create space");
    return NULL;
  }

  Cartesian::space the_sum(((Space*)o1)->m_space + ((Space*)o2)->m_space);

  // copy because m_space constructor has already run.
  result_space->m_space = the_sum;

  return (PyObject*) result_space;

}

static PyObject* nb_subtract(PyObject* o1, PyObject* o2) {

  if (!is_SpaceType(o1) || !is_SpaceType(o2)) {
    Py_INCREF(Py_NotImplemented);
    return Py_NotImplemented;
  }

  Space* result_space(NULL);

  new_SpaceType(&result_space);

  if (result_space == NULL) {
    PyErr_SetString(space_exception, "nb_subtract failed to create space");
    return NULL;
  }

  Cartesian::space the_difference(((Space*)o1)->m_space - ((Space*)o2)->m_space);

  // copy because m_space constructor has already run.
  result_space->m_space = the_difference;

  return (PyObject*) result_space;

}


static PyObject* nb_multiply(PyObject* o1, PyObject* o2) {
  // This returns the dot product of the space vectors as a double.
  // This will not act as scale since it returns a double not a Space
  // object.

  if (!is_SpaceType(o1) || !is_SpaceType(o2)) {
    Py_INCREF(Py_NotImplemented);
    return Py_NotImplemented;
  }
  double a_dot_product(((Space*)o1)->m_space * ((Space*)o2)->m_space);
  return Py_BuildValue("d", a_dot_product);
}


static PyObject* nb_divide(PyObject* o1, PyObject* o2) {
  // This returns a Space object scaled by the divisor.
  // o1 must be SpaceType, o2 a float or int.

  // TODO let double / space be scale the reciprocal

  Space* result_space(NULL);

  new_SpaceType(&result_space);

  if (result_space == NULL) {
    PyErr_SetString(space_exception, "nb_divide failed to create space");
    return NULL;
  }

  if (is_SpaceType(o1) && (PyFloat_Check(o2) || PyInt_Check(o2))) {

    try {
      result_space->m_space = ((Space*)o1)->m_space / PyFloat_AsDouble(o2);
    } catch (Cartesian::DivideZeroError& err) {
      Py_DECREF(result_space);
      PyErr_SetString(space_exception, "nb_divide attempted divide by zero");
      return NULL;
    }

  } else {
    Py_DECREF(result_space);
    Py_INCREF(Py_NotImplemented);
    return Py_NotImplemented;
  }

  return (PyObject*) result_space;
}


// ---------------------------
// ----- inplace methods -----
// ---------------------------

static PyObject* nb_inplace_add(PyObject* o1, PyObject* o2) {
  // TODO can this be implement directly using space::operator+=()?
  // problem with refence going out of scope, segfault.
  return nb_add(o1, o2);
}

static PyObject* nb_inplace_subtract(PyObject* o1, PyObject* o2) {
  // TOOD implement directly?
  return nb_subtract(o1, o2);
}

static PyObject* nb_inplace_multiply(PyObject* o1, PyObject* o2) {
  // TOOD implement directly?
  return nb_multiply(o1, o2);
}

static PyObject* nb_inplace_divide(PyObject* o1, PyObject* o2) {
  // TOOD implement directly?
  return nb_divide(o1, o2);
}

// ==========================
// ===== Python structs =====
// ==========================


static PyMethodDef Space_methods[] = {
    {NULL}  /* Sentinel */
};


static PyMemberDef Space_members[] = {
    {NULL}  /* Sentinel */
};


static PyGetSetDef Space_getseters[] = {
    {"x", (getter)Space_getx, (setter)Space_setx, "x", NULL},
    {"y", (getter)Space_gety, (setter)Space_sety, "y", NULL},
    {"z", (getter)Space_getz, (setter)Space_setz, "z", NULL},
    {NULL}  /* Sentinel */
};

// see http://docs.python.org/c-api/typeobj.html
static PyNumberMethods space_as_number = {
  (binaryfunc) nb_add,
  (binaryfunc) nb_subtract,
  (binaryfunc) nb_multiply,
  (binaryfunc) nb_divide,
  (binaryfunc) 0,  // nb_remainder
  (binaryfunc) 0,  // nb_divmod
  (ternaryfunc) 0, // nb_power
  (unaryfunc) 0,   // nb_negative
  (unaryfunc) 0,   // nb_positive
  (unaryfunc) 0,   // nb_absolute
  (inquiry) 0,     // nb_nonzero. Used by PyObject_IsTrue.
  (unaryfunc) 0,   // nb_invert
  (binaryfunc) 0,  // nb_lshift
  (binaryfunc) 0,  // nb_rshift
  (binaryfunc) 0,  // nb_and
  (binaryfunc) 0,  // nb_xor
  (binaryfunc) 0,  // nb_or
  (coercion) 0,    // Used by the coerce() function
  (unaryfunc) 0,   // nb_int
  (unaryfunc) 0,   // nb_long
  (unaryfunc) 0,   // nb_float
  (unaryfunc) 0,   // nb_oct
  (unaryfunc) 0,   // nb_hex

  // added in release 2.0

  (binaryfunc) nb_inplace_add,
  (binaryfunc) nb_inplace_subtract,
  (binaryfunc) nb_inplace_multiply,
  (binaryfunc) nb_inplace_divide,
  (binaryfunc) 0,  // nb_inplace_remainder
  (ternaryfunc) 0, // nb_inplace_power
  (binaryfunc) 0,  // nb_inplace_lshift
  (binaryfunc) 0,  // nb_inplace_rshift
  (binaryfunc) 0,  // nb_inplace_and
  (binaryfunc) 0,  // nb_inplace_xor
  (binaryfunc) 0,  // nb_inplace_or

  // added in release 2.2
  (binaryfunc) 0,  // nb_floor_divide
  (binaryfunc) 0,  // nb_true_divide
  (binaryfunc) 0,  // nb_inplace_floor_divide
  (binaryfunc) 0,  // nb_inplace_true_divide

};


PyTypeObject SpaceType = {
  PyObject_HEAD_INIT(NULL)
  0,                                        /* ob_size */
  "orbits.space",                           /* tp_name */
  sizeof(Space),                            /* tp_basicsize */
  0,                                        /* tp_itemsize */
  (destructor) Space_dealloc,               /* tp_dealloc */
  0,                                        /* tp_print */
  0,                                        /* tp_getattr */
  0,                                        /* tp_setattr */
  0,                                        /* tp_compare */
  Space_repr,                               /* tp_repr */
  &space_as_number,                         /* tp_as_number */
  0,                                        /* tp_as_sequence */
  0,                                        /* tp_as_mapping */
  0,                                        /* tp_hash */
  0,                                        /* tp_call */
  Space_str,                                /* tp_str */
  0,                                        /* tp_getattro */
  0,                                        /* tp_setattro */
  0,                                        /* tp_as_buffer */
  Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_CHECKTYPES, /* tp_flags */
  "Space objects",                          /* tp_doc */
  0,                                        /* tp_traverse */
  0,                                        /* tp_clear */
  0,                                        /* tp_richcompare */
  0,                                        /* tp_weaklistoffset */
  0,                                        /* tp_iter */
  0,                                        /* tp_iternext */
  Space_methods,                            /* tp_methods */
  Space_members,                            /* tp_members */
  Space_getseters,                          /* tp_getset */
  0,                                        /* tp_base */
  0,                                        /* tp_dict */
  0,                                        /* tp_descr_get */
  0,                                        /* tp_descr_set */
  0,                                        /* tp_dictoffset */
  (initproc)Space_init,                     /* tp_init */
  0,                                        /* tp_alloc */
  Space_new,                                /* tp_new */
};


// Create new objects with PyObject_New() for binary operators that
// return a new instance of Space, like add.
static void new_SpaceType(Space** a_space) {
  *a_space = PyObject_New(Space, &SpaceType); // alloc and inits?
}

static int is_SpaceType(PyObject* a_space) {
  //wrapper for type check
  return PyObject_TypeCheck(a_space, &SpaceType);
}

// ==========================
// ===== module methods =====
// ==========================

// -------------------------
// ----- cross product -----
// -------------------------

PyDoc_STRVAR(space_cross__doc__,
             "Returns the cross product of two space objects");

static PyObject* cross(PyObject* self, PyObject *args) {

  Space* first_space(NULL);
  Space* second_space(NULL);
  Space* result_space(NULL);

  // O is borrowed reference
  if (!PyArg_ParseTuple(args, "OO", &first_space, &second_space))
    return NULL;

  result_space = PyObject_New(Space, &SpaceType); // alloc and inits

  if (result_space == NULL) {
    PyErr_SetString(space_exception, "cross failed to create space.");
    return NULL;
  }

  Cartesian::space a_cross(Cartesian::cross(first_space->m_space,
					    second_space->m_space));

  result_space->m_space = a_cross;

  return (PyObject*) result_space;

}

// ---------------------
// ----- magnitude -----
// ---------------------

PyDoc_STRVAR(space_magnitude__doc__,
             "Returns the magnitude of the space object");

static PyObject* magnitude(PyObject* self, PyObject *args) {

  Space* a_space(NULL);

  // O is borrowed reference
  if (!PyArg_ParseTuple(args, "O", &a_space))
    return NULL;

  double a_magnitude(a_space->m_space.magnitude());

  return (PyObject*)  Py_BuildValue("d", a_magnitude);

}

// ----------------------
// ----- normalized -----
// ----------------------

PyDoc_STRVAR(space_normalized__doc__,
             "Returns the normalized version of the space object");

static PyObject* normalized(PyObject* self, PyObject *args) {

  Space* a_space(NULL);
  Space* result_space(NULL);

  // O is borrowed reference
  if (!PyArg_ParseTuple(args, "O", &a_space))
    return NULL;

  result_space = PyObject_New(Space, &SpaceType); // alloc and inits

  if (result_space == NULL) {
    PyErr_SetString(space_exception, "normalized failed to create space.");
    return NULL;
  }

  Cartesian::space a_normalized(a_space->m_space.normalized());

  result_space->m_space = a_normalized;

  return (PyObject*) result_space;

}


// -----------------------
// ----- method list -----
// -----------------------

PyMethodDef space_module_methods[] = {
  {"cross", (PyCFunction) cross, METH_VARARGS, space_cross__doc__},
  {"magnitude", (PyCFunction) magnitude, METH_VARARGS, space_magnitude__doc__},
  {"normalized", (PyCFunction) normalized, METH_VARARGS, space_normalized__doc__},
  {NULL, NULL}  /* Sentinel */
};

// ----------------------------
// ----- module constants -----
// ----------------------------


PyObject* space_create(const Cartesian::space& a_space) {
  // Creates a python space object from a Cartesian::space object.
  // Intended for use in the module initorbits function to generate
  // space constants with Oribts::space::U[oxyz]

  // TODO borrowed reference?
  Space* py_space(NULL);

  py_space = PyObject_New(Space, &SpaceType); // alloc and inits

  // TODO exception handle this
  if (py_space == NULL){
    PyErr_SetString(space_exception, "cross failed to create space.");
    return NULL;
  }

  py_space->m_space.x(a_space.x());
  py_space->m_space.y(a_space.y());
  py_space->m_space.z(a_space.z());

  return (PyObject*) py_space;
}


// ================
// ===== init =====
// ================

// PyMODINIT_FUNC declares extern "C" too.
PyMODINIT_FUNC initspace(void) {

  PyObject* m;

  SpaceType.tp_new = PyType_GenericNew;
  if (PyType_Ready(&SpaceType) < 0)
    return;

  m = Py_InitModule3("space", space_module_methods,
		     "python wrappers for space objects.");

  Py_INCREF(&SpaceType);
  PyModule_AddObject(m, "space", (PyObject *)&SpaceType);


  // errors
  space_exception = PyErr_NewException("space.error", NULL, NULL);
  Py_INCREF(space_exception);
  PyModule_AddObject(m, "space_error", space_exception);

  // constants
  PyObject* space_Uo(NULL);
  space_Uo = space_create(Cartesian::space::Uo);
  Py_INCREF(space_Uo);
  PyModule_AddObject(m, "Uo", (PyObject*)space_Uo);

  PyObject* space_Ux(NULL);
  space_Ux = space_create(Cartesian::space::Ux);
  Py_INCREF(space_Ux);
  PyModule_AddObject(m, "Ux", (PyObject*)space_Ux);

  PyObject* space_Uy(NULL);
  space_Uy = space_create(Cartesian::space::Uy);
  Py_INCREF(space_Uy);
  PyModule_AddObject(m, "Uy", (PyObject*)space_Uy);

  PyObject* space_Uz(NULL);
  space_Uz = space_create(Cartesian::space::Uz);
  Py_INCREF(space_Uz);
  PyModule_AddObject(m, "Uz", (PyObject*)space_Uz);

  // TODO other objects

}