Index: sasmodels/compare.py
===================================================================
--- sasmodels/compare.py	(revision af92b736ef07ae4becfadd8b6368696e118aeae9)
+++ sasmodels/compare.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
@@ -369,5 +369,5 @@
             model = MultiplicationModel(P, S)
         else:
-            raise ValueError("mixture models not handled yet")
+            raise ValueError("sasview mixture models not supported by compare")
     else:
         model = get_model(model_info['oldname'])
@@ -424,20 +424,10 @@
     Return a model calculator using the OpenCL calculation engine.
     """
-    def builder(model_info):
-        try:
-            return core.build_model(model_info, dtype=dtype, platform="ocl")
-        except Exception as exc:
-            print(exc)
-            print("... trying again with single precision")
-            return core.build_model(model_info, dtype='single', platform="ocl")
-    if model_info['composition']:
-        composition_type, parts = model_info['composition']
-        if composition_type == 'product':
-            P, S = [builder(p) for p in parts]
-            model = product.ProductModel(P, S)
-        else:
-            raise ValueError("mixture models not handled yet")
-    else:
-        model = builder(model_info)
+    try:
+        model = core.build_model(model_info, dtype=dtype, platform="ocl")
+    except Exception as exc:
+        print(exc)
+        print("... trying again with single precision")
+        model = core.build_model(model_info, dtype='single', platform="ocl")
     calculator = DirectModel(data, model, cutoff=cutoff)
     calculator.engine = "OCL%s"%DTYPE_MAP[dtype]
@@ -450,16 +440,5 @@
     if dtype == 'quad':
         dtype = 'longdouble'
-    def builder(model_info):
-        return core.build_model(model_info, dtype=dtype, platform="dll")
-
-    if model_info['composition']:
-        composition_type, parts = model_info['composition']
-        if composition_type == 'product':
-            P, S = [builder(p) for p in parts]
-            model = product.ProductModel(P, S)
-        else:
-            raise ValueError("mixture models not handled yet")
-    else:
-        model = builder(model_info)
+    model = core.build_model(model_info, dtype=dtype, platform="dll")
     calculator = DirectModel(data, model, cutoff=cutoff)
     calculator.engine = "OMP%s"%DTYPE_MAP[dtype]
@@ -715,19 +694,11 @@
         print("expected parameters: model N1 N2")
 
-    def _get_info(name):
-        try:
-            model_info = core.load_model_info(name)
-        except ImportError, exc:
-            print(str(exc))
-            print("Use one of:\n    " + models)
-            sys.exit(1)
-        return model_info
-
     name = args[0]
-    if '*' in name:
-        parts = [_get_info(k) for k in name.split('*')]
-        model_info = product.make_product_info(*parts)
-    else:
-        model_info = _get_info(name)
+    try:
+        model_info = core.load_model_info(name)
+    except ImportError, exc:
+        print(str(exc))
+        print("Could not find model; use one of:\n    " + models)
+        sys.exit(1)
 
     invalid = [o[1:] for o in flags
@@ -749,5 +720,5 @@
         'res'       : 0.0,
         'accuracy'  : 'Low',
-        'cutoff'    : 1e-5,
+        'cutoff'    : 0.0,
         'seed'      : -1,  # default to preset
         'mono'      : False,
Index: sasmodels/core.py
===================================================================
--- sasmodels/core.py	(revision d5ba8419b0a797faa3fe41fde9334c5a19282992)
+++ sasmodels/core.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
@@ -3,6 +3,7 @@
 """
 
-from os.path import basename, dirname, join as joinpath
+from os.path import basename, dirname, join as joinpath, splitext
 from glob import glob
+import imp
 
 import numpy as np
@@ -11,5 +12,8 @@
 from . import weights
 from . import generate
-
+# TODO: remove circular references between product and core
+# product uses call_ER/call_VR, core uses make_product_info/ProductModel
+#from . import product
+from . import mixture
 from . import kernelpy
 from . import kerneldll
@@ -46,28 +50,44 @@
     Load model info and build model.
     """
+    return build_model(load_model_info(model_name), **kw)
+
+def load_model_info_from_path(path):
+    # Pull off the last .ext if it exists; there may be others
+    name = basename(splitext(path)[0])
+
+    # Not cleaning name since don't need to be able to reload this
+    # model later
+    # Should probably turf the model from sys.modules after we are done...
+
+    # Placing the model in the 'sasmodels.custom' name space, even
+    # though it doesn't actually exist.  imp.load_source doesn't seem
+    # to care.
+    kernel_module = imp.load_source('sasmodels.custom.'+name, path)
+
+    # Now that we have the module, we can load it as usual
+    model_info = generate.make_model_info(kernel_module)
+    return model_info
+
+def load_model_info(model_name):
+    """
+    Load a model definition given the model name.
+
+    This returns a handle to the module defining the model.  This can be
+    used with functions in generate to build the docs or extract model info.
+    """
     parts = model_name.split('+')
     if len(parts) > 1:
-        from .mixture import MixtureModel
-        models = [load_model(p, **kw) for p in parts]
-        return MixtureModel(models)
+        model_info_list = [load_model_info(p) for p in parts]
+        return mixture.make_mixture_info(model_info_list)
 
     parts = model_name.split('*')
     if len(parts) > 1:
+        from . import product
         # Note: currently have circular reference
-        from .product import ProductModel
         if len(parts) > 2:
             raise ValueError("use P*S to apply structure factor S to model P")
-        P, Q = [load_model(p, **kw) for p in parts]
-        return ProductModel(P, Q)
-
-    return build_model(load_model_info(model_name), **kw)
-
-def load_model_info(model_name):
-    """
-    Load a model definition given the model name.
-
-    This returns a handle to the module defining the model.  This can be
-    used with functions in generate to build the docs or extract model info.
-    """
+        P_info, Q_info = [load_model_info(p) for p in parts]
+        return product.make_product_info(P_info, Q_info)
+
     #import sys; print "\n".join(sys.path)
     __import__('sasmodels.models.'+model_name)
@@ -95,9 +115,16 @@
     otherwise it uses the default "ocl".
     """
-    source = generate.make_source(model_info)
-    if dtype is None:
-        dtype = 'single' if model_info['single'] else 'double'
-    if callable(model_info.get('Iq', None)):
-        return kernelpy.PyModel(model_info)
+    composition = model_info.get('composition', None)
+    if composition is not None:
+        composition_type, parts = composition
+        models = [build_model(p, dtype=dtype, platform=platform) for p in parts]
+        if composition_type == 'mixture':
+            return mixture.MixtureModel(model_info, models)
+        elif composition_type == 'product':
+            from . import product
+            P, S = parts
+            return product.ProductModel(model_info, P, S)
+        else:
+            raise ValueError('unknown mixture type %s'%composition_type)
 
     ## for debugging:
@@ -109,5 +136,9 @@
     # open(model_info['name']+'.c','w').write(source)
     # source = open(model_info['name']+'.cl','r').read()
-
+    source = generate.make_source(model_info)
+    if dtype is None:
+        dtype = 'single' if model_info['single'] else 'double'
+    if callable(model_info.get('Iq', None)):
+        return kernelpy.PyModel(model_info)
     if (platform == "dll"
             or not HAVE_OPENCL
Index: sasmodels/mixture.py
===================================================================
--- sasmodels/mixture.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
+++ sasmodels/mixture.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
@@ -0,0 +1,145 @@
+"""
+Mixture model
+-------------
+
+The product model multiplies the structure factor by the form factor,
+modulated by the effective radius of the form.  The resulting model
+has a attributes of both the model description (with parameters, etc.)
+and the module evaluator (with call, release, etc.).
+
+To use it, first load form factor P and structure factor S, then create
+*ProductModel(P, S)*.
+"""
+from copy import copy
+import numpy as np
+
+from .generate import process_parameters, COMMON_PARAMETERS, Parameter
+
+SCALE=0
+BACKGROUND=1
+EFFECT_RADIUS=2
+VOLFRACTION=3
+
+def make_mixture_info(parts):
+    """
+    Create info block for product model.
+    """
+    flatten = []
+    for part in parts:
+        if part['composition'] and part['composition'][0] == 'mixture':
+            flatten.extend(part['compostion'][1])
+        else:
+            flatten.append(part)
+    parts = flatten
+
+    # Build new parameter list
+    pars = COMMON_PARAMETERS + []
+    for k, part in enumerate(parts):
+        # Parameter prefix per model, A_, B_, ...
+        prefix = chr(ord('A')+k) + '_'
+        for p in part['parameters']:
+            # No background on the individual mixture elements
+            if p.name == 'background':
+                continue
+            # TODO: promote Parameter to a full class
+            # this code knows too much about the implementation!
+            p = list(p)
+            if p[0] == 'scale':  # allow model subtraction
+                p[3] = [-np.inf, np.inf]  # limits
+            p[0] = prefix+p[0]   # relabel parameters with A_, ...
+            pars.append(p)
+
+    model_info = {}
+    model_info['id'] = '+'.join(part['id'])
+    model_info['name'] = ' + '.join(part['name'])
+    model_info['filename'] = None
+    model_info['title'] = 'Mixture model with ' + model_info['name']
+    model_info['description'] = model_info['title']
+    model_info['docs'] = model_info['title']
+    model_info['category'] = "custom"
+    model_info['parameters'] = pars
+    #model_info['single'] = any(part['single'] for part in parts)
+    model_info['structure_factor'] = False
+    model_info['variant_info'] = None
+    #model_info['tests'] = []
+    #model_info['source'] = []
+    # Iq, Iqxy, form_volume, ER, VR and sesans
+    # Remember the component info blocks so we can build the model
+    model_info['composition'] = ('mixture', parts)
+    process_parameters(model_info)
+    return model_info
+
+
+class MixtureModel(object):
+    def __init__(self, model_info, parts):
+        self.info = model_info
+        self.parts = parts
+
+    def __call__(self, q_vectors):
+        # Note: may be sending the q_vectors to the n times even though they
+        # are only needed once.  It would mess up modularity quite a bit to
+        # handle this optimally, especially since there are many cases where
+        # separate q vectors are needed (e.g., form in python and structure
+        # in opencl; or both in opencl, but one in single precision and the
+        # other in double precision).
+        kernels = [part(q_vectors) for part in self.parts]
+        return MixtureKernel(self.info, kernels)
+
+    def release(self):
+        """
+        Free resources associated with the model.
+        """
+        for part in self.parts:
+            part.release()
+
+
+class MixtureKernel(object):
+    def __init__(self, model_info, kernels):
+        dim = '2d' if kernels[0].q_input.is_2d else '1d'
+
+        # fixed offsets starts at 2 for scale and background
+        fixed_pars, pd_pars = [], []
+        offsets = [[2, 0]]
+        #vol_index = []
+        def accumulate(fixed, pd, volume):
+            # subtract 1 from fixed since we are removing background
+            fixed_offset, pd_offset = offsets[-1]
+            #vol_index.extend(k+pd_offset for k,v in pd if v in volume)
+            offsets.append([fixed_offset + len(fixed) - 1, pd_offset + len(pd)])
+            pd_pars.append(pd)
+        if dim == '2d':
+            for p in kernels:
+                partype = p.info['partype']
+                accumulate(partype['fixed-2d'], partype['pd-2d'], partype['volume'])
+        else:
+            for p in kernels:
+                partype = p.info['partype']
+                accumulate(partype['fixed-1d'], partype['pd-1d'], partype['volume'])
+
+        #self.vol_index = vol_index
+        self.offsets = offsets
+        self.fixed_pars = fixed_pars
+        self.pd_pars = pd_pars
+        self.info = model_info
+        self.kernels = kernels
+        self.results = None
+
+    def __call__(self, fixed_pars, pd_pars, cutoff=1e-5):
+        scale, background = fixed_pars[0:2]
+        total = 0.0
+        self.results = []  # remember the parts for plotting later
+        for k in range(len(self.offsets)-1):
+            start_fixed, start_pd = self.offsets[k]
+            end_fixed, end_pd = self.offsets[k+1]
+            part_fixed = [fixed_pars[start_fixed], 0.0] + fixed_pars[start_fixed+1:end_fixed]
+            part_pd = [pd_pars[start_pd], 0.0] + pd_pars[start_pd+1:end_pd]
+            part_result = self.kernels[k](part_fixed, part_pd)
+            total += part_result
+            self.results.append(scale*sum+background)
+
+        return scale*total + background
+
+    def release(self):
+        self.p_kernel.release()
+        self.q_kernel.release()
+
Index: sasmodels/product.py
===================================================================
--- sasmodels/product.py	(revision d5ba8419b0a797faa3fe41fde9334c5a19282992)
+++ sasmodels/product.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
@@ -68,8 +68,8 @@
 
 class ProductModel(object):
-    def __init__(self, P, S):
+    def __init__(self, model_info, P, S):
+        self.info = model_info
         self.P = P
         self.S = S
-        self.info = make_product_info(P.info, S.info)
 
     def __call__(self, q_vectors):
Index: sasmodels/sasview_model.py
===================================================================
--- sasmodels/sasview_model.py	(revision 08376e707b88225bb8c73e3058792f08aabb9b07)
+++ sasmodels/sasview_model.py	(revision 72a081dea6caa8fa493057133a6232f6b118c89a)
@@ -16,5 +16,4 @@
 import math
 from copy import deepcopy
-import warnings
 import collections
 
@@ -22,9 +21,12 @@
 
 from . import core
+from . import generate
+from . import custom
 
 def standard_models():
     return [make_class(model_name) for model_name in core.list_models()]
 
-def make_class(model_name, namestyle='name'):
+# TODO: rename to make_class_from_name and update sasview
+def make_class(model_name):
     """
     Load the sasview model defined in *kernel_module*.
@@ -37,8 +39,15 @@
     """
     model_info = core.load_model_info(model_name)
+    return make_class_from_info(model_info)
+
+def make_class_from_file(path):
+    model_info = core.load_model_info_from_path(path)
+    return make_class_from_info(model_info)
+
+def make_class_from_info(model_info):
     def __init__(self, multfactor=1):
         SasviewModel.__init__(self)
     attrs = dict(__init__=__init__, _model_info=model_info)
-    ConstructedModel = type(model_info[namestyle], (SasviewModel,), attrs)
+    ConstructedModel = type(model_info['name'], (SasviewModel,), attrs)
     return ConstructedModel
 
