Q:

Render semi-transparent self-intersecting surface in cycles

I am looking at mesh surfaces that intersect themselves and show both sides (inside/outside). It's easier to put one here than to explain. So far, I can not cycles render it with transparency. It renders completely opaque for me. I'm toggling alt-z and it changes from semi-transparent to opaque in normal view (object mode) but always opaque in render. It is interesting to watch when toggling backface culling.
I give the shape here, because there may be some strange thing due to the variety of surface normal directions.
I don't want glass or refraction, I just want to see the surfaces themselves in some way that I can see inside.
NOTE: it DOES render semi-transparent in blender render. Using 2.74. I think I will have to play with multiple lights and diffuseness before it looks excellent.
 
import bpy
import numpy as np

nth, nph = 101, 200
th  = np.linspace(-0.5*np.pi, 0.5*np.pi, nth)
phi = np.linspace(-np.pi, np.pi, nph)
y3 = np.sin(th) - 1.8*np.sin(th)**3
X = np.cos(phi)[None,:] * np.cos(th)[:,None]
Y = np.sin(phi)[None,:] * np.cos(th)[:,None]
Z = np.zeros_like(X)            + y3[:,None]

verts = [tuple(thing) for thing in zip(X.flatten(), Y.flatten(), Z.flatten())]
faces = []
for ith in range(nth-1):
    for iph in range(nph-1):
        v1 = nph*ith + iph
        v2 = v1 + 1
        v3 = v2 + nph
        v4 = v3 - 1
        faces.append((v1, v2, v3, v4))

purple = bpy.data.materials.new("transPurple")
purple.diffuse_color = (1.0, 0.0, 0.67)
purple.alpha = 0.2
purple.use_transparency = True

me = bpy.data.meshes.new('wow')
ob = bpy.data.objects.new('wow', me)

ob.location = (0.0, 0.0, 2.0) 
bpy.context.scene.objects.link(ob)
me.from_pydata(verts,[],faces)
ob.active_material = purple
ob.show_transparent = True
bpy.data.objects['wow'].select = False
bpy.data.objects['wow'].select = True
bpy.ops.object.shade_smooth()

A:

two useful functions 
import bmesh
import bpy

def set_smooth(ob):
    mesh = ob.data
    smooth_states = [True] * len(mesh.polygons)
    mesh.polygons.foreach_set('use_smooth', smooth_states)
    mesh.update()

def remove_doubles(ob, d=0.00001):
    bm = bmesh.new()
    bm.from_mesh(ob.data)
    bmesh.ops.remove_doubles(bm, verts=bm.verts[:], dist=d)
    bm.to_mesh(ob.data)
    bm.free()

Those two functions aren't really meant for real-time stuff, but neither is bpy.ops stuff. They can however be integrated in a key-framed and rendered animation, no problem.
import bmesh
import bpy
import numpy as np

# set_smooth() and remove_doubles() are declared elsewhere

nth, nph = 101, 200
th  = np.linspace(-0.5*np.pi, 0.5*np.pi, nth)
phi = np.linspace(-np.pi, np.pi, nph)
y3 = np.sin(th) - 1.8*np.sin(th)**3
X = np.cos(phi)[None,:] * np.cos(th)[:,None]
Y = np.sin(phi)[None,:] * np.cos(th)[:,None]
Z = np.zeros_like(X)            + y3[:,None]

verts = [tuple(thing) for thing in zip(X.flatten(), Y.flatten(), Z.flatten())]
faces = []
for ith in range(nth-1):
    for iph in range(nph-1):
        v1 = nph*ith + iph
        v2 = v1 + 1
        v3 = v2 + nph
        v4 = v3 - 1
        faces.append((v1, v2, v3, v4))

me = bpy.data.meshes.new('wow')
me.from_pydata(verts,[],faces)
ob = bpy.data.objects.new('wow', me)
ob.location = (0.0, 0.0, 2.0) 
bpy.context.scene.objects.link(ob)
set_smooth(ob)
remove_doubles(ob)

setting up a cycles material via script is a little bit involved, and you might ask yourself, how much of this should I be doing via script or manually setting up the material and only adjust certain parameters via script at render time..
import bpy

scene = bpy.context.scene
scene.render.engine = 'CYCLES'

my_mat = bpy.data.materials.new('cool_mat')
my_mat.use_nodes = True
nodes = my_mat.node_tree.nodes

# material output and diffuse nodes are present already, get them by name
mat_output = nodes.get("Material Output")
diffuse = nodes.get("Diffuse BSDF") # new(type='ShaderNodeBsdfDiffuse')
# mix and transparent are added by the script
mix = nodes.new(type='ShaderNodeMixShader')
transp = nodes.new(type='ShaderNodeBsdfTransparent')

mat_output.location = 300, 0
mix.location = 0, 0
diffuse.location = -200, 50
transp.location = -200, -100

''' hook up shaders '''

# link mix_shader to material output
a = mix.outputs['Shader']
b = mat_output.inputs['Surface']
my_mat.node_tree.links.new(a, b)

# link diffuse to mix
a = diffuse.outputs['BSDF']
b = mix.inputs[1]  # 'Shader' is used twice, so you access via index.
my_mat.node_tree.links.new(a, b)

# link transparent to mix
a = transp.outputs['BSDF']
b = mix.inputs[2]
my_mat.node_tree.links.new(a, b)

''' small shader adjustments and mixing '''

mix.inputs[0].default_value = 0.8  # factor
diffuse.inputs['Color'].default_value = [0.8, 0.0, 0.191713, 1.0]
transp.inputs['Color'].default_value = [1.0, 0.152801, 0.582658, 1.0]

which combined, gives something like this:

I suspect a node setup like this might be a little more interesting: (note: I have ambient occlusion on!)

The idea is to make the color or factor a function of the cross product between surface normal and the up vector (0,0,z). Using the nodes allows you to embrace vector mathematics for the adjustment of the shader at local points.
or taking the viewing angle into account too:

And notice there is a backfacing output on the Geometry node, worth exploring

