Index: sasmodels/details.py
===================================================================
--- sasmodels/details.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/details.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -258,4 +258,7 @@
     # type: (...) -> Sequence[np.ndarray]
     """
+    **Deprecated** Theta weights will be computed in the kernel wrapper if
+    they are needed.
+
     If there is a theta parameter, update the weights of that parameter so that
     the cosine weighting required for polar integration is preserved.
@@ -272,5 +275,4 @@
     Returns updated weights vectors
     """
-    # TODO: explain in a comment why scale and background are missing
     # Apparently the parameters.theta_offset similarly skips scale and
     # and background, so the indexing works out, but they are still shipped
@@ -279,5 +281,4 @@
         index = parameters.theta_offset
         theta = dispersity[index]
-        # TODO: modify the dispersity vector to avoid the theta=-90,90,270,...
         theta_weight = abs(cos(radians(theta)))
         weights = tuple(theta_weight*w if k == index else w
Index: sasmodels/generate.py
===================================================================
--- sasmodels/generate.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/generate.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -7,10 +7,17 @@
     particular dimensions averaged over all orientations.
 
-    *Iqxy(qx, qy, p1, p2, ...)* returns the scattering at qx, qy for a form
-    with particular dimensions for a single orientation.
-
-    *Imagnetic(qx, qy, result[], p1, p2, ...)* returns the scattering for the
-    polarized neutron spin states (up-up, up-down, down-up, down-down) for
-    a form with particular dimensions for a single orientation.
+    *Iqac(qab, qc, p1, p2, ...)* returns the scattering at qab, qc
+    for a rotationally symmetric form with particular dimensions.
+    qab, qc are determined from shape orientation and scattering angles.
+    This call is used if the shape has orientation parameters theta and phi.
+
+    *Iqabc(qa, qb, qc, p1, p2, ...)* returns the scattering at qa, qb, qc
+    for a form with particular dimensions.  qa, qb, qc are determined from
+    shape orientation and scattering angles. This call is used if the shape
+    has orientation parameters theta, phi and psi.
+
+    *Iqxy(qx, qy, p1, p2, ...)* returns the scattering at qx, qy.  Use this
+    to create an arbitrary 2D theory function, needed for q-dependent
+    background functions and for models with non-uniform magnetism.
 
     *form_volume(p1, p2, ...)* returns the volume of the form with particular
@@ -31,13 +38,12 @@
 scale and background parameters for each model.
 
-*Iq*, *Iqxy*, *Imagnetic* and *form_volume* should be stylized C-99
-functions written for OpenCL.  All functions need prototype declarations
-even if the are defined before they are used.  OpenCL does not support
-*#include* preprocessor directives, so instead the list of includes needs
-to be given as part of the metadata in the kernel module definition.
-The included files should be listed using a path relative to the kernel
-module, or if using "lib/file.c" if it is one of the standard includes
-provided with the sasmodels source.  The includes need to be listed in
-order so that functions are defined before they are used.
+C code should be stylized C-99 functions written for OpenCL. All functions
+need prototype declarations even if the are defined before they are used.
+Although OpenCL supports *#include* preprocessor directives, the list of
+includes should be given as part of the metadata in the kernel module
+definition. The included files should be listed using a path relative to the
+kernel module, or if using "lib/file.c" if it is one of the standard includes
+provided with the sasmodels source. The includes need to be listed in order
+so that functions are defined before they are used.
 
 Floating point values should be declared as *double*.  For single precision
@@ -107,9 +113,9 @@
     present, the volume ratio is 1.
 
-    *form_volume*, *Iq*, *Iqxy*, *Imagnetic* are strings containing the
-    C source code for the body of the volume, Iq, and Iqxy functions
+    *form_volume*, *Iq*, *Iqac*, *Iqabc* are strings containing
+    the C source code for the body of the volume, Iq, and Iqac functions
     respectively.  These can also be defined in the last source file.
 
-    *Iq* and *Iqxy* also be instead be python functions defining the
+    *Iq*, *Iqac*, *Iqabc* also be instead be python functions defining the
     kernel.  If they are marked as *Iq.vectorized = True* then the
     kernel is passed the entire *q* vector at once, otherwise it is
@@ -168,4 +174,5 @@
 from zlib import crc32
 from inspect import currentframe, getframeinfo
+import logging
 
 import numpy as np  # type: ignore
@@ -181,4 +188,6 @@
     pass
 # pylint: enable=unused-import
+
+logger = logging.getLogger(__name__)
 
 # jitter projection to use in the kernel code.  See explore/jitter.py
@@ -626,6 +635,6 @@
 
 """
-def _gen_fn(name, pars, body, filename, line):
-    # type: (str, List[Parameter], str, str, int) -> str
+def _gen_fn(model_info, name, pars):
+    # type: (ModelInfo, str, List[Parameter]) -> str
     """
     Generate a function given pars and body.
@@ -639,7 +648,11 @@
     """
     par_decl = ', '.join(p.as_function_argument() for p in pars) if pars else 'void'
+    body = getattr(model_info, name)
+    filename = model_info.filename
+    # Note: if symbol is defined strangely in the module then default it to 1
+    lineno = model_info.lineno.get(name, 1)
     return _FN_TEMPLATE % {
         'name': name, 'pars': par_decl, 'body': body,
-        'filename': filename.replace('\\', '\\\\'), 'line': line,
+        'filename': filename.replace('\\', '\\\\'), 'line': lineno,
     }
 
@@ -656,34 +669,40 @@
 
 # type in IQXY pattern could be single, float, double, long double, ...
-_IQXY_PATTERN = re.compile("^((inline|static) )? *([a-z ]+ )? *Iqxy *([(]|$)",
+_IQXY_PATTERN = re.compile(r"(^|\s)double\s+I(?P<mode>q(ab?c|xy))\s*[(]",
                            flags=re.MULTILINE)
-def _have_Iqxy(sources):
+def find_xy_mode(source):
     # type: (List[str]) -> bool
     """
-    Return true if any file defines Iqxy.
+    Return the xy mode as qa, qac, qabc or qxy.
 
     Note this is not a C parser, and so can be easily confused by
     non-standard syntax.  Also, it will incorrectly identify the following
-    as having Iqxy::
+    as having 2D models::
 
         /*
-        double Iqxy(qx, qy, ...) { ... fill this in later ... }
+        double Iqac(qab, qc, ...) { ... fill this in later ... }
         */
 
-    If you want to comment out an Iqxy function, use // on the front of the
-    line instead.
-    """
-    for _path, code in sources:
-        if _IQXY_PATTERN.search(code):
-            return True
-    return False
-
-
-def _add_source(source, code, path):
+    If you want to comment out the function, use // on the front of the
+    line::
+
+        /*
+        // double Iqac(qab, qc, ...) { ... fill this in later ... }
+        */
+
+    """
+    for code in source:
+        m = _IQXY_PATTERN.search(code)
+        if m is not None:
+            return m.group('mode')
+    return 'qa'
+
+
+def _add_source(source, code, path, lineno=1):
     """
     Add a file to the list of source code chunks, tagged with path and line.
     """
     path = path.replace('\\', '\\\\')
-    source.append('#line 1 "%s"' % path)
+    source.append('#line %d "%s"' % (lineno, path))
     source.append(code)
 
@@ -716,9 +735,4 @@
     user_code = [(f, open(f).read()) for f in model_sources(model_info)]
 
-    # What kind of 2D model do we need?
-    xy_mode = ('qa' if not _have_Iqxy(user_code) and not isinstance(model_info.Iqxy, str)
-               else 'qac' if not partable.is_asymmetric
-               else 'qabc')
-
     # Build initial sources
     source = []
@@ -727,19 +741,41 @@
         _add_source(source, code, path)
 
+    if model_info.c_code:
+        _add_source(source, model_info.c_code, model_info.filename,
+                    lineno=model_info.lineno.get('c_code', 1))
+
     # Make parameters for q, qx, qy so that we can use them in declarations
-    q, qx, qy = [Parameter(name=v) for v in ('q', 'qx', 'qy')]
+    q, qx, qy, qab, qa, qb, qc \
+        = [Parameter(name=v) for v in 'q qx qy qab qa qb qc'.split()]
     # Generate form_volume function, etc. from body only
     if isinstance(model_info.form_volume, str):
         pars = partable.form_volume_parameters
-        source.append(_gen_fn('form_volume', pars, model_info.form_volume,
-                              model_info.filename, model_info._form_volume_line))
+        source.append(_gen_fn(model_info, 'form_volume', pars))
     if isinstance(model_info.Iq, str):
         pars = [q] + partable.iq_parameters
-        source.append(_gen_fn('Iq', pars, model_info.Iq,
-                              model_info.filename, model_info._Iq_line))
+        source.append(_gen_fn(model_info, 'Iq', pars))
     if isinstance(model_info.Iqxy, str):
-        pars = [qx, qy] + partable.iqxy_parameters
-        source.append(_gen_fn('Iqxy', pars, model_info.Iqxy,
-                              model_info.filename, model_info._Iqxy_line))
+        pars = [qx, qy] + partable.iq_parameters + partable.orientation_parameters
+        source.append(_gen_fn(model_info, 'Iqxy', pars))
+    if isinstance(model_info.Iqac, str):
+        pars = [qab, qc] + partable.iq_parameters
+        source.append(_gen_fn(model_info, 'Iqac', pars))
+    if isinstance(model_info.Iqabc, str):
+        pars = [qa, qb, qc] + partable.iq_parameters
+        source.append(_gen_fn(model_info, 'Iqabc', pars))
+
+    # What kind of 2D model do we need?  Is it consistent with the parameters?
+    xy_mode = find_xy_mode(source)
+    if xy_mode == 'qabc' and not partable.is_asymmetric:
+        raise ValueError("asymmetric oriented models need to define Iqabc")
+    elif xy_mode == 'qac' and partable.is_asymmetric:
+        raise ValueError("symmetric oriented models need to define Iqac")
+    elif not partable.orientation_parameters and xy_mode in ('qac', 'qabc'):
+        raise ValueError("Unexpected function I%s for unoriented shape"%xy_mode)
+    elif partable.orientation_parameters and xy_mode not in ('qac', 'qabc'):
+        if xy_mode == 'qxy':
+            logger.warn("oriented shapes should define Iqac or Iqabc")
+        else:
+            raise ValueError("Expected function Iqac or Iqabc for oriented shape")
 
     # Define the parameter table
@@ -767,14 +803,26 @@
     if xy_mode == 'qabc':
         pars = ",".join(["_qa", "_qb", "_qc"] + model_refs)
-        call_iqxy = "#define CALL_IQ_ABC(_qa,_qb,_qc,_v) Iqxy(%s)" % pars
+        call_iqxy = "#define CALL_IQ_ABC(_qa,_qb,_qc,_v) Iqabc(%s)" % pars
         clear_iqxy = "#undef CALL_IQ_ABC"
     elif xy_mode == 'qac':
         pars = ",".join(["_qa", "_qc"] + model_refs)
-        call_iqxy = "#define CALL_IQ_AC(_qa,_qc,_v) Iqxy(%s)" % pars
+        call_iqxy = "#define CALL_IQ_AC(_qa,_qc,_v) Iqac(%s)" % pars
         clear_iqxy = "#undef CALL_IQ_AC"
-    else:  # xy_mode == 'qa'
+    elif xy_mode == 'qa':
         pars = ",".join(["_qa"] + model_refs)
         call_iqxy = "#define CALL_IQ_A(_qa,_v) Iq(%s)" % pars
         clear_iqxy = "#undef CALL_IQ_A"
+    elif xy_mode == 'qxy':
+        orientation_refs = _call_pars("_v.", partable.orientation_parameters)
+        pars = ",".join(["_qx", "_qy"] + model_refs + orientation_refs)
+        call_iqxy = "#define CALL_IQ_XY(_qx,_qy,_v) Iqxy(%s)" % pars
+        clear_iqxy = "#undef CALL_IQ_XY"
+        if partable.orientation_parameters:
+            call_iqxy += "\n#define HAVE_THETA"
+            clear_iqxy += "\n#undef HAVE_THETA"
+        if partable.is_asymmetric:
+            call_iqxy += "\n#define HAVE_PSI"
+            clear_iqxy += "\n#undef HAVE_PSI"
+
 
     magpars = [k-2 for k, p in enumerate(partable.call_parameters)
Index: sasmodels/kernel_header.c
===================================================================
--- sasmodels/kernel_header.c	(revision 8698a0ddea5a155fcc2e3f1085f06a618b46eebf)
+++ sasmodels/kernel_header.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -150,2 +150,69 @@
 inline double cube(double x) { return x*x*x; }
 inline double sas_sinx_x(double x) { return x==0 ? 1.0 : sin(x)/x; }
+
+// CRUFT: support old style models with orientation received qx, qy and angles
+
+// To rotate from the canonical position to theta, phi, psi, first rotate by
+// psi about the major axis, oriented along z, which is a rotation in the
+// detector plane xy. Next rotate by theta about the y axis, aligning the major
+// axis in the xz plane. Finally, rotate by phi in the detector plane xy.
+// To compute the scattering, undo these rotations in reverse order:
+//     rotate in xy by -phi, rotate in xz by -theta, rotate in xy by -psi
+// The returned q is the length of the q vector and (xhat, yhat, zhat) is a unit
+// vector in the q direction.
+// To change between counterclockwise and clockwise rotation, change the
+// sign of phi and psi.
+
+#if 1
+//think cos(theta) should be sin(theta) in new coords, RKH 11Jan2017
+#define ORIENT_SYMMETRIC(qx, qy, theta, phi, q, sn, cn) do { \
+    SINCOS(phi*M_PI_180, sn, cn); \
+    q = sqrt(qx*qx + qy*qy); \
+    cn  = (q==0. ? 1.0 : (cn*qx + sn*qy)/q * sin(theta*M_PI_180));  \
+    sn = sqrt(1 - cn*cn); \
+    } while (0)
+#else
+// SasView 3.x definition of orientation
+#define ORIENT_SYMMETRIC(qx, qy, theta, phi, q, sn, cn) do { \
+    SINCOS(theta*M_PI_180, sn, cn); \
+    q = sqrt(qx*qx + qy*qy);\
+    cn = (q==0. ? 1.0 : (cn*cos(phi*M_PI_180)*qx + sn*qy)/q); \
+    sn = sqrt(1 - cn*cn); \
+    } while (0)
+#endif
+
+#if 1
+#define ORIENT_ASYMMETRIC(qx, qy, theta, phi, psi, q, xhat, yhat, zhat) do { \
+    q = sqrt(qx*qx + qy*qy); \
+    const double qxhat = qx/q; \
+    const double qyhat = qy/q; \
+    double sin_theta, cos_theta; \
+    double sin_phi, cos_phi; \
+    double sin_psi, cos_psi; \
+    SINCOS(theta*M_PI_180, sin_theta, cos_theta); \
+    SINCOS(phi*M_PI_180, sin_phi, cos_phi); \
+    SINCOS(psi*M_PI_180, sin_psi, cos_psi); \
+    xhat = qxhat*(-sin_phi*sin_psi + cos_theta*cos_phi*cos_psi) \
+         + qyhat*( cos_phi*sin_psi + cos_theta*sin_phi*cos_psi); \
+    yhat = qxhat*(-sin_phi*cos_psi - cos_theta*cos_phi*sin_psi) \
+         + qyhat*( cos_phi*cos_psi - cos_theta*sin_phi*sin_psi); \
+    zhat = qxhat*(-sin_theta*cos_phi) \
+         + qyhat*(-sin_theta*sin_phi); \
+    } while (0)
+#else
+// SasView 3.x definition of orientation
+#define ORIENT_ASYMMETRIC(qx, qy, theta, phi, psi, q, cos_alpha, cos_mu, cos_nu) do { \
+    q = sqrt(qx*qx + qy*qy); \
+    const double qxhat = qx/q; \
+    const double qyhat = qy/q; \
+    double sin_theta, cos_theta; \
+    double sin_phi, cos_phi; \
+    double sin_psi, cos_psi; \
+    SINCOS(theta*M_PI_180, sin_theta, cos_theta); \
+    SINCOS(phi*M_PI_180, sin_phi, cos_phi); \
+    SINCOS(psi*M_PI_180, sin_psi, cos_psi); \
+    cos_alpha = cos_theta*cos_phi*qxhat + sin_theta*qyhat; \
+    cos_mu = (-sin_theta*cos_psi*cos_phi - sin_psi*sin_phi)*qxhat + cos_theta*cos_psi*qyhat; \
+    cos_nu = (-cos_phi*sin_psi*sin_theta + sin_phi*cos_psi)*qxhat + sin_psi*cos_theta*qyhat; \
+    } while (0)
+#endif
Index: sasmodels/kernel_iq.c
===================================================================
--- sasmodels/kernel_iq.c	(revision 6aee3abf255884200f80256eb642d49c1e9c888e)
+++ sasmodels/kernel_iq.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -31,4 +31,5 @@
 //  CALL_IQ_AC(qa, qc, table) : call the Iqxy function for symmetric shapes
 //  CALL_IQ_ABC(qa, qc, table) : call the Iqxy function for asymmetric shapes
+//  CALL_IQ_XY(qx, qy, table) : call the Iqxy function for arbitrary models
 //  INVALID(table) : test if the current point is feesible to calculate.  This
 //      will be defined in the kernel definition file.
@@ -469,4 +470,11 @@
   #define APPLY_ROTATION() qabc_apply(rotation, qx, qy, &qa, &qb, &qc)
   #define CALL_KERNEL() CALL_IQ_ABC(qa, qb, qc, local_values.table)
+#elif defined(CALL_IQ_XY)
+  // direct call to qx,qy calculator
+  double qx, qy;
+  #define FETCH_Q() do { qx = q[2*q_index]; qy = q[2*q_index+1]; } while (0)
+  #define BUILD_ROTATION() do {} while(0)
+  #define APPLY_ROTATION() do {} while(0)
+  #define CALL_KERNEL() CALL_IQ_XY(qx, qy, local_values.table)
 #endif
 
@@ -477,4 +485,34 @@
 #if defined(CALL_IQ) || defined(CALL_IQ_A)
   #define APPLY_PROJECTION() const double weight=weight0
+#elif defined(CALL_IQ_XY)
+  // CRUFT: support oriented model which define Iqxy rather than Iqac or Iqabc
+  // Need to plug the values for the orientation angles back into parameter
+  // table in case they were overridden by the orientation offset.  This
+  // means that orientation dispersity will not work for these models, but
+  // it was broken anyway, so no matter.  Still want to provide Iqxy in case
+  // the user model wants full control of orientation/magnetism.
+  #if defined(HAVE_PSI)
+    const double theta = values[details->theta_par+2];
+    const double phi = values[details->theta_par+3];
+    const double psi = values[details->theta_par+4];
+    double weight;
+    #define APPLY_PROJECTION() do { \
+      local_values.table.theta = theta; \
+      local_values.table.phi = phi; \
+      local_values.table.psi = psi; \
+      weight=weight0; \
+    } while (0)
+  #elif defined(HAVE_THETA)
+    const double theta = values[details->theta_par+2];
+    const double phi = values[details->theta_par+3];
+    double weight;
+    #define APPLY_PROJECTION() do { \
+      local_values.table.theta = theta; \
+      local_values.table.phi = phi; \
+      weight=weight0; \
+    } while (0)
+  #else
+    #define APPLY_PROJECTION() const double weight=weight0
+  #endif
 #else // !spherosymmetric projection
   // Grab the "view" angles (theta, phi, psi) from the initial parameter table.
Index: sasmodels/kernelpy.py
===================================================================
--- sasmodels/kernelpy.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/kernelpy.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -26,4 +26,6 @@
 # pylint: enable=unused-import
 
+logger = logging.getLogger(__name__)
+
 class PyModel(KernelModel):
     """
@@ -31,14 +33,13 @@
     """
     def __init__(self, model_info):
-        # Make sure Iq and Iqxy are available and vectorized
+        # Make sure Iq is available and vectorized
         _create_default_functions(model_info)
         self.info = model_info
         self.dtype = np.dtype('d')
-        logging.info("load python model " + self.info.name)
+        logger.info("load python model " + self.info.name)
 
     def make_kernel(self, q_vectors):
         q_input = PyInput(q_vectors, dtype=F64)
-        kernel = self.info.Iqxy if q_input.is_2d else self.info.Iq
-        return PyKernel(kernel, self.info, q_input)
+        return PyKernel(self.info, q_input)
 
     def release(self):
@@ -89,5 +90,5 @@
     Callable SAS kernel.
 
-    *kernel* is the DllKernel object to call.
+    *kernel* is the kernel object to call.
 
     *model_info* is the module information
@@ -104,5 +105,5 @@
     Call :meth:`release` when done with the kernel instance.
     """
-    def __init__(self, kernel, model_info, q_input):
+    def __init__(self, model_info, q_input):
         # type: (callable, ModelInfo, List[np.ndarray]) -> None
         self.dtype = np.dtype('d')
@@ -110,5 +111,4 @@
         self.q_input = q_input
         self.res = np.empty(q_input.nq, q_input.dtype)
-        self.kernel = kernel
         self.dim = '2d' if q_input.is_2d else '1d'
 
@@ -159,6 +159,6 @@
         # Generate a closure which calls the form_volume if it exists.
         form_volume = model_info.form_volume
-        self._volume = ((lambda: form_volume(*volume_args)) if form_volume
-                        else (lambda: 1.0))
+        self._volume = ((lambda: form_volume(*volume_args)) if form_volume else
+                        (lambda: 1.0))
 
     def __call__(self, call_details, values, cutoff, magnetic):
@@ -261,6 +261,7 @@
     any functions that are not already marked as vectorized.
     """
+    # Note: must call create_vector_Iq before create_vector_Iqxy
     _create_vector_Iq(model_info)
-    _create_vector_Iqxy(model_info)  # call create_vector_Iq() first
+    _create_vector_Iqxy(model_info)
 
 
@@ -280,9 +281,10 @@
         model_info.Iq = vector_Iq
 
+
 def _create_vector_Iqxy(model_info):
     """
     Define Iqxy as a vector function if it exists, or default it from Iq().
     """
-    Iq, Iqxy = model_info.Iq, model_info.Iqxy
+    Iqxy = getattr(model_info, 'Iqxy', None)
     if callable(Iqxy):
         if not getattr(Iqxy, 'vectorized', False):
@@ -295,7 +297,8 @@
             vector_Iqxy.vectorized = True
             model_info.Iqxy = vector_Iqxy
-    elif callable(Iq):
+    else:
         #print("defaulting Iqxy")
         # Iq is vectorized because create_vector_Iq was already called.
+        Iq = model_info.Iq
         def default_Iqxy(qx, qy, *args):
             """
Index: sasmodels/modelinfo.py
===================================================================
--- sasmodels/modelinfo.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/modelinfo.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -37,5 +37,5 @@
 
 # assumptions about common parameters exist throughout the code, such as:
-# (1) kernel functions Iq, Iqxy, form_volume, ... don't see them
+# (1) kernel functions Iq, Iqxy, Iqac, Iqabc, form_volume, ... don't see them
 # (2) kernel drivers assume scale is par[0] and background is par[1]
 # (3) mixture models drop the background on components and replace the scale
@@ -256,7 +256,7 @@
 
     *type* indicates how the parameter will be used.  "volume" parameters
-    will be used in all functions.  "orientation" parameters will be used
-    in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in
-    *Imagnetic* only.  If *type* is the empty string, the parameter will
+    will be used in all functions.  "orientation" parameters are not passed,
+    but will be used to convert from *qx*, *qy* to *qa*, *qb*, *qc* in calls to
+    *Iqxy* and *Imagnetic*.  If *type* is the empty string, the parameter will
     be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters
     can automatically be promoted to magnetic parameters, each of which
@@ -386,7 +386,4 @@
       with vector parameter p sent as p[].
 
-    * [removed] *iqxy_parameters* is the list of parameters to the Iqxy(qx, qy, ...)
-      function, with vector parameter p sent as p[].
-
     * *form_volume_parameters* is the list of parameters to the form_volume(...)
       function, with vector parameter p sent as p[].
@@ -443,7 +440,6 @@
         self.iq_parameters = [p for p in self.kernel_parameters
                               if p.type not in ('orientation', 'magnetic')]
-        # note: orientation no longer sent to Iqxy, so its the same as
-        #self.iqxy_parameters = [p for p in self.kernel_parameters
-        #                        if p.type != 'magnetic']
+        self.orientation_parameters = [p for p in self.kernel_parameters
+                                       if p.type == 'orientation']
         self.form_volume_parameters = [p for p in self.kernel_parameters
                                        if p.type == 'volume']
@@ -490,9 +486,15 @@
                 if p.type != 'orientation':
                     raise TypeError("psi must be an orientation parameter")
+            elif p.type == 'orientation':
+                raise TypeError("only theta, phi and psi can be orientation parameters")
         if theta >= 0 and phi >= 0:
+            last_par = len(self.kernel_parameters) - 1
             if phi != theta+1:
                 raise TypeError("phi must follow theta")
             if psi >= 0 and psi != phi+1:
                 raise TypeError("psi must follow phi")
+            #if (psi >= 0 and psi != last_par) or (psi < 0 and phi != last_par):
+            #    raise TypeError("orientation parameters must appear at the "
+            #                    "end of the parameter table")
         elif theta >= 0 or phi >= 0 or psi >= 0:
             raise TypeError("oriented shapes must have both theta and phi and maybe psi")
@@ -715,4 +717,7 @@
 
 
+#: Set of variables defined in the model that might contain C code
+C_SYMBOLS = ['Imagnetic', 'Iq', 'Iqxy', 'Iqac', 'Iqabc', 'form_volume', 'c_code']
+
 def _find_source_lines(model_info, kernel_module):
     # type: (ModelInfo, ModuleType) -> None
@@ -720,37 +725,32 @@
     Identify the location of the C source inside the model definition file.
 
-    This code runs through the source of the kernel module looking for
-    lines that start with 'Iq', 'Iqxy' or 'form_volume'.  Clearly there are
-    all sorts of reasons why this might not work (e.g., code commented out
-    in a triple-quoted line block, code built using string concatenation,
-    or code defined in the branch of an 'if' block), but it should work
-    properly in the 95% case, and getting the incorrect line number will
-    be harmless.
-    """
-    # Check if we need line numbers at all
-    if callable(model_info.Iq):
-        return None
-
-    if (model_info.Iq is None
-            and model_info.Iqxy is None
-            and model_info.Imagnetic is None
-            and model_info.form_volume is None):
+    This code runs through the source of the kernel module looking for lines
+    that contain C code (because it is a c function definition).  Clearly
+    there are all sorts of reasons why this might not work (e.g., code
+    commented out in a triple-quoted line block, code built using string
+    concatenation, code defined in the branch of an 'if' block, code imported
+    from another file), but it should work properly in the 95% case, and for
+    the remainder, getting the incorrect line number will merely be
+    inconvenient.
+    """
+    # Only need line numbers if we are creating a C module and the C symbols
+    # are defined.
+    if (callable(model_info.Iq)
+            or not any(hasattr(model_info, s) for s in C_SYMBOLS)):
         return
 
-    # find the defintion lines for the different code blocks
+    # load the module source if we can
     try:
         source = inspect.getsource(kernel_module)
     except IOError:
         return
-    for k, v in enumerate(source.split('\n')):
-        if v.startswith('Imagnetic'):
-            model_info._Imagnetic_line = k+1
-        elif v.startswith('Iqxy'):
-            model_info._Iqxy_line = k+1
-        elif v.startswith('Iq'):
-            model_info._Iq_line = k+1
-        elif v.startswith('form_volume'):
-            model_info._form_volume_line = k+1
-
+
+    # look for symbol at the start of the line
+    for lineno, line in enumerate(source.split('\n')):
+        for name in C_SYMBOLS:
+            if line.startswith(name):
+                # Add 1 since some compilers complain about "#line 0"
+                model_info.lineno[name] = lineno + 1
+                break
 
 def make_model_info(kernel_module):
@@ -761,5 +761,5 @@
     Fill in default values for parts of the module that are not provided.
 
-    Note: vectorized Iq and Iqxy functions will be created for python
+    Note: vectorized Iq and Iqac/Iqabc functions will be created for python
     models when the model is first called, not when the model is loaded.
     """
@@ -790,4 +790,5 @@
     info.profile_axes = getattr(kernel_module, 'profile_axes', ['x', 'y'])
     info.source = getattr(kernel_module, 'source', [])
+    info.c_code = getattr(kernel_module, 'c_code', None)
     # TODO: check the structure of the tests
     info.tests = getattr(kernel_module, 'tests', [])
@@ -797,4 +798,6 @@
     info.Iq = getattr(kernel_module, 'Iq', None) # type: ignore
     info.Iqxy = getattr(kernel_module, 'Iqxy', None) # type: ignore
+    info.Iqac = getattr(kernel_module, 'Iqac', None) # type: ignore
+    info.Iqabc = getattr(kernel_module, 'Iqabc', None) # type: ignore
     info.Imagnetic = getattr(kernel_module, 'Imagnetic', None) # type: ignore
     info.profile = getattr(kernel_module, 'profile', None) # type: ignore
@@ -811,4 +814,8 @@
     info.hidden = getattr(kernel_module, 'hidden', None) # type: ignore
 
+    if callable(info.Iq) and parameters.has_2d:
+        raise ValueError("oriented python models not supported")
+
+    info.lineno = {}
     _find_source_lines(info, kernel_module)
 
@@ -824,5 +831,5 @@
 
     The structure should be mostly static, other than the delayed definition
-    of *Iq* and *Iqxy* if they need to be defined.
+    of *Iq*, *Iqac* and *Iqabc* if they need to be defined.
     """
     #: Full path to the file defining the kernel, if any.
@@ -906,9 +913,9 @@
     structure_factor = None # type: bool
     #: List of C source files used to define the model.  The source files
-    #: should define the *Iq* function, and possibly *Iqxy*, though a default
-    #: *Iqxy = Iq(sqrt(qx**2+qy**2)* will be created if no *Iqxy* is provided.
-    #: Files containing the most basic functions must appear first in the list,
-    #: followed by the files that use those functions.  Form factors are
-    #: indicated by providing a :attr:`ER` function.
+    #: should define the *Iq* function, and possibly *Iqac* or *Iqabc* if the
+    #: model defines orientation parameters. Files containing the most basic
+    #: functions must appear first in the list, followed by the files that
+    #: use those functions.  Form factors are indicated by providing
+    #: an :attr:`ER` function.
     source = None           # type: List[str]
     #: The set of tests that must pass.  The format of the tests is described
@@ -935,4 +942,8 @@
     #: See :attr:`ER` for details on the parameters.
     VR = None               # type: Optional[Callable[[np.ndarray], Tuple[np.ndarray, np.ndarray]]]
+    #: Arbitrary C code containing supporting functions, etc., to be inserted
+    #: after everything in source.  This can include Iq and Iqxy functions with
+    #: the full function signature, including all parameters.
+    c_code = None
     #: Returns the form volume for python-based models.  Form volume is needed
     #: for volume normalization in the polydispersity integral.  If no
@@ -955,6 +966,8 @@
     #: include the decimal point. See :mod:`generate` for more details.
     Iq = None               # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
-    #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
-    Iqxy = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
+    #: Returns *I(qab, qc, a, b, ...)*.  The interface follows :attr:`Iq`.
+    Iqac = None             # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
+    #: Returns *I(qa, qb, qc, a, b, ...)*.  The interface follows :attr:`Iq`.
+    Iqabc = None            # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
     #: Returns *I(qx, qy, a, b, ...)*.  The interface follows :attr:`Iq`.
     Imagnetic = None        # type: Union[None, str, Callable[[np.ndarray], np.ndarray]]
@@ -972,12 +985,6 @@
     #: Returns a random parameter set for the model
     random = None           # type: Optional[Callable[[], Dict[str, float]]]
-
-    # line numbers within the python file for bits of C source, if defined
-    # NB: some compilers fail with a "#line 0" directive, so default to 1.
-    _Imagnetic_line = 1
-    _Iqxy_line = 1
-    _Iq_line = 1
-    _form_volume_line = 1
-
+    #: Line numbers for symbols defining C code
+    lineno = None           # type: Dict[str, int]
 
     def __init__(self):
Index: sasmodels/models/_spherepy.py
===================================================================
--- sasmodels/models/_spherepy.py	(revision ef07e950965796e2a3a8156fcade98ce5e403001)
+++ sasmodels/models/_spherepy.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -88,8 +88,4 @@
 Iq.vectorized = True  # Iq accepts an array of q values
 
-def Iqxy(qx, qy, sld, sld_solvent, radius):
-    return Iq(sqrt(qx ** 2 + qy ** 2), sld, sld_solvent, radius)
-Iqxy.vectorized = True  # Iqxy accepts arrays of qx, qy values
-
 def sesans(z, sld, sld_solvent, radius):
     """
Index: sasmodels/models/barbell.c
===================================================================
--- sasmodels/models/barbell.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/barbell.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -90,5 +90,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld, double solvent_sld,
     double radius_bell, double radius, double length)
Index: sasmodels/models/bcc_paracrystal.c
===================================================================
--- sasmodels/models/bcc_paracrystal.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/bcc_paracrystal.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -107,5 +107,5 @@
 
 
-static double Iqxy(double qa, double qb, double qc,
+static double Iqabc(double qa, double qb, double qc,
     double dnn, double d_factor, double radius,
     double sld, double solvent_sld)
Index: sasmodels/models/capped_cylinder.c
===================================================================
--- sasmodels/models/capped_cylinder.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/capped_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -115,5 +115,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld, double solvent_sld, double radius,
     double radius_cap, double length)
Index: sasmodels/models/core_shell_bicelle.c
===================================================================
--- sasmodels/models/core_shell_bicelle.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/core_shell_bicelle.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -67,5 +67,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double radius,
     double thick_rim,
Index: sasmodels/models/core_shell_bicelle_elliptical.c
===================================================================
--- sasmodels/models/core_shell_bicelle_elliptical.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/core_shell_bicelle_elliptical.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -71,5 +71,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double r_minor,
     double x_core,
Index: sasmodels/models/core_shell_bicelle_elliptical_belt_rough.c
===================================================================
--- sasmodels/models/core_shell_bicelle_elliptical_belt_rough.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/core_shell_bicelle_elliptical_belt_rough.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -79,5 +79,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
           double r_minor,
           double x_core,
@@ -114,3 +114,2 @@
     return 1.0e-4 * Aq*exp(-0.5*(square(qa) + square(qb) + square(qc) )*square(sigma));
 }
-
Index: sasmodels/models/core_shell_bicelle_elliptical_belt_rough.py
===================================================================
--- sasmodels/models/core_shell_bicelle_elliptical_belt_rough.py	(revision 110f69cfa1143196495c76df1d5cff08c6ce884e)
+++ sasmodels/models/core_shell_bicelle_elliptical_belt_rough.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -149,8 +149,8 @@
     ["sld_rim",        "1e-6/Ang^2", 1, [-inf, inf], "sld",         "Cylinder rim scattering length density"],
     ["sld_solvent",    "1e-6/Ang^2", 6, [-inf, inf], "sld",         "Solvent scattering length density"],
+    ["sigma",       "Ang",        0,    [0, inf],    "",            "interfacial roughness"],
     ["theta",       "degrees",    90.0, [-360, 360], "orientation", "cylinder axis to beam angle"],
     ["phi",         "degrees",    0,    [-360, 360], "orientation", "rotation about beam"],
     ["psi",         "degrees",    0,    [-360, 360], "orientation", "rotation about cylinder axis"],
-    ["sigma",       "Ang",        0,    [0, inf],    "",            "interfacial roughness"]
     ]
 
Index: sasmodels/models/core_shell_cylinder.c
===================================================================
--- sasmodels/models/core_shell_cylinder.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/core_shell_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -48,5 +48,5 @@
 
 
-double Iqxy(double qab, double qc,
+double Iqac(double qab, double qc,
     double core_sld,
     double shell_sld,
Index: sasmodels/models/core_shell_ellipsoid.c
===================================================================
--- sasmodels/models/core_shell_ellipsoid.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/core_shell_ellipsoid.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -75,5 +75,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double radius_equat_core,
     double x_core,
Index: sasmodels/models/core_shell_parallelepiped.c
===================================================================
--- sasmodels/models/core_shell_parallelepiped.c	(revision 4493288f50c1e858772d5058d6ca027bfa72f50e)
+++ sasmodels/models/core_shell_parallelepiped.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -102,5 +102,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double core_sld,
     double arim_sld,
Index: sasmodels/models/cylinder.c
===================================================================
--- sasmodels/models/cylinder.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -45,5 +45,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld,
     double solvent_sld,
Index: sasmodels/models/ellipsoid.c
===================================================================
--- sasmodels/models/ellipsoid.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/ellipsoid.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -39,5 +39,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double sld,
     double sld_solvent,
Index: sasmodels/models/elliptical_cylinder.c
===================================================================
--- sasmodels/models/elliptical_cylinder.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/elliptical_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -54,5 +54,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
      double radius_minor, double r_ratio, double length,
      double sld, double solvent_sld)
Index: sasmodels/models/fcc_paracrystal.c
===================================================================
--- sasmodels/models/fcc_paracrystal.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/fcc_paracrystal.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -80,5 +80,5 @@
 
 
-static double Iqxy(double qa, double qb, double qc,
+static double Iqabc(double qa, double qb, double qc,
     double dnn, double d_factor, double radius,
     double sld, double solvent_sld)
Index: sasmodels/models/hollow_cylinder.c
===================================================================
--- sasmodels/models/hollow_cylinder.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/hollow_cylinder.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -52,5 +52,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double radius, double thickness, double length,
     double sld, double solvent_sld)
Index: sasmodels/models/hollow_rectangular_prism.c
===================================================================
--- sasmodels/models/hollow_rectangular_prism.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/hollow_rectangular_prism.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -85,5 +85,5 @@
 }
 
-double Iqxy(double qa, double qb, double qc,
+double Iqabc(double qa, double qb, double qc,
     double sld,
     double solvent_sld,
Index: sasmodels/models/line.py
===================================================================
--- sasmodels/models/line.py	(revision 2d81cfe3be42579d563407fb9275b7b516fee234)
+++ sasmodels/models/line.py	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -57,4 +57,5 @@
 Iq.vectorized = True # Iq accepts an array of q values
 
+
 def Iqxy(qx, qy, *args):
     """
@@ -69,4 +70,12 @@
 
 Iqxy.vectorized = True  # Iqxy accepts an array of qx qy values
+
+# uncomment the following to test Iqxy in C models
+#del Iq, Iqxy
+#c_code = """
+#static double Iq(double q, double b, double m) { return m*q+b; }
+#static double Iqxy(double qx, double qy, double b, double m)
+#{ return (m*qx+b)*(m*qy+b); }
+#"""
 
 def random():
Index: sasmodels/models/parallelepiped.c
===================================================================
--- sasmodels/models/parallelepiped.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/parallelepiped.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -53,5 +53,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double sld,
     double solvent_sld,
Index: sasmodels/models/rectangular_prism.c
===================================================================
--- sasmodels/models/rectangular_prism.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/rectangular_prism.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -71,5 +71,5 @@
 
 
-double Iqxy(double qa, double qb, double qc,
+double Iqabc(double qa, double qb, double qc,
     double sld,
     double solvent_sld,
Index: sasmodels/models/sc_paracrystal.c
===================================================================
--- sasmodels/models/sc_paracrystal.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/sc_paracrystal.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -82,5 +82,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double dnn, double d_factor, double radius,
     double sld, double solvent_sld)
Index: sasmodels/models/stacked_disks.c
===================================================================
--- sasmodels/models/stacked_disks.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/stacked_disks.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -142,5 +142,5 @@
 
 static double
-Iqxy(double qab, double qc,
+Iqac(double qab, double qc,
     double thick_core,
     double thick_layer,
Index: sasmodels/models/triaxial_ellipsoid.c
===================================================================
--- sasmodels/models/triaxial_ellipsoid.c	(revision 74768cb764a04223ec9e992e316cb8c211adfe34)
+++ sasmodels/models/triaxial_ellipsoid.c	(revision 108e70e22154db937a6d46f66be6dd70408cce6d)
@@ -46,5 +46,5 @@
 
 static double
-Iqxy(double qa, double qb, double qc,
+Iqabc(double qa, double qb, double qc,
     double sld,
     double sld_solvent,
