The Marshall JCM800 2205 Load Line

In this notebook, we'll explore load line placement using EL34-Philips plate characteristic curves extracted data. We'll then add a load line from estimates derived from the schematic.



EL34 - Philips
Marshall JCM800 2205 Power Circuit


References

The Valve Wizard - The Push-Pull Power Output Stage
Aiken Amplification - Idle Current Biasing - Why 70 percent?

Data Extraction


In [1]:
%matplotlib inline
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
import sys
from scipy import interpolate
import math
import scipy.integrate as integrate


# used engauge to extract plot data from datasheet
fn = "el34-360v.csv"
datasheetCurveData = pd.read_csv(fn)

colnames = datasheetCurveData.columns.values
colcount = len(colnames)
rowcount = len(datasheetCurveData[colnames[0]])

# do a little clean up
# remove negative values of plate current
for i in range(1,colcount):
    for j in range(rowcount):
        if datasheetCurveData[colnames[i]][j] < 0.00:
            datasheetCurveData[colnames[i]][j] = 0.0
            
# engauge adds bogus points on curves where plate voltage is greater than curve
# scan through array data column at a time, find point where engauge starts duplicating data
# calculate slope and y-axis intercept (m, b) then fill data past that point with a line
for i in range(1,colcount):
    m = 0
    b = 0
    for j in range(rowcount):
        if datasheetCurveData[colnames[i]][j] > 0.01:
            try:
                if m == 0:
                    if datasheetCurveData[colnames[i]][j] == datasheetCurveData[colnames[i]][j+40]:
                        # now need to find the average of the past slopes
                        slopeCount = 10
                        sum = 0
                        for k in range(j-slopeCount,j):
                            sum += (datasheetCurveData[colnames[i]][k] - datasheetCurveData[colnames[i]][k-1])/(datasheetCurveData['PlateVoltage'][k] - datasheetCurveData['PlateVoltage'][k-1])
                        m = sum / slopeCount;
                        if m < 0.0:
                            m = 0.0;
                        b = datasheetCurveData[colnames[i]][j] - m*datasheetCurveData['PlateVoltage'][j]
                        # print j,datasheetCurveData[colnames[i]][j],m,b
                if m != 0:
                    datasheetCurveData[colnames[i]][j] = m*datasheetCurveData['PlateVoltage'][j] + b
            except KeyError:
                pass # j+2 is now > rowcount for the higher curves

In [2]:
datasheetCurveData.head(5)


Out[2]:
PlateVoltage 0V -4V -8V -12V -16V -20V -24V -28V -32V
0 0.003 0.00203 0.00051 0.00203 0.0 0.00051 0.00254 0.00051 0.00152 0.0
1 0.008 0.00203 0.00055 0.00203 0.0 0.00051 0.00254 0.00051 0.00152 0.0
2 0.010 0.00203 0.00059 0.00203 0.0 0.00051 0.00254 0.00051 0.00153 0.0
3 0.550 0.00203 0.00458 0.00203 0.0 0.00051 0.00475 0.00051 0.00244 0.0
4 0.560 0.00203 0.00460 0.00203 0.0 0.00051 0.00477 0.00051 0.00244 0.0

In [3]:
datasheetCurveData.tail(5)


Out[3]:
PlateVoltage 0V -4V -8V -12V -16V -20V -24V -28V -32V
157 713.08 0.603669 0.469231 0.341032 0.25034 0.19984 0.14910 0.09916 0.06001 0.03821
158 742.56 0.612048 0.474661 0.343368 0.25034 0.19984 0.15084 0.10036 0.06099 0.03862
159 742.99 0.612170 0.474741 0.343402 0.25034 0.19984 0.15086 0.10038 0.06099 0.03863
160 744.17 0.612506 0.474958 0.343496 0.25034 0.19984 0.15092 0.10043 0.06099 0.03863
161 748.34 0.613691 0.475726 0.343826 0.25034 0.19984 0.15092 0.10061 0.06099 0.03863


Load Line Placement


In [4]:
#initial values
Ia = 0.03  #plate current mA
Va = 453.0   #plate voltage V
Rl = 8       #speaker impedance
n  = 21      #pri/sec turns ratio

Pd = 25.0 # watts

VaMAX = 800.0
IaMAX = 0.500
GraphWidth = 840 # get these from jpg size 
GraphHeight = 560

# later, we find intersection of loadline with plate current curves by resampling
# so all have the same x values.
# http://stackoverflow.com/questions/17928452/find-all-intersections-of-xy-data-point-graph-with-numpy
# http://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html

PlateVoltages = np.arange(0,VaMAX,1.0)
saturationCurveVoltage = '0V'
cutoffCurveVoltage = '-32V' # tweak this based on lowest grid voltage curve on graph you have



# creating 1D interpolation functions from the datasheet extracted curves
iaf = {}
for i in range(1,colcount):
    iaf[colnames[i]] = {'valueAt': None,'loadLineIntersectionV':0,'loadLineIntersectionI':0}
    iaf[colnames[i]]['valueAt'] = interpolate.interp1d(datasheetCurveData['PlateVoltage'].tolist(), 
                                            datasheetCurveData[colnames[i]].tolist())

m_A = 0.0
b_A = 0.0
m_B = 0.0
b_B = 0.0
vllintersect = 0.0
illintersect = 0.0
    
def plotAB(_Ia,_Va,_Rl,_n):
    global Ia,Va,Rl,n,m_A,b_A,m_B,b_B,vllintersect,illintersect
    Ia = _Ia # set the slider values to global
    Va = _Va
    Rl = _Rl
    n  = _n

    # plot the csv colums versus plate/anode voltage
    fig = plt.figure(figsize=(15, 10))
    null = [plt.plot(datasheetCurveData['PlateVoltage'],
                     datasheetCurveData[colnames[x]],label='') for x in range(1,colcount)]
    plt.grid(linestyle='--', linewidth=0.5)
    null = plt.xticks(np.arange(0,VaMAX,20))
    null = plt.yticks(np.arange(0,IaMAX,0.02))

    # plot power dissipation limit curve
    null = plt.plot(PlateVoltages, Pd/PlateVoltages,label='Power Limit %dW'%Pd,linestyle=(0, (5.0, 5.0)))

    null = plt.xlim([0,VaMAX])
    null = plt.ylim([0,IaMAX])

    def placeLabel(plt,text,x,y,angle):
        null = plt.annotate(s=text,
                        rotation=angle,
                        xy=(x,y),
                        xycoords='data',
                        xytext=(0,0),
                        textcoords='offset points',
                        bbox=dict(boxstyle="round", fc="1.0"),
                        size=12,
                        # arrowprops=dict(arrowstyle="->",connectionstyle="angle,angleA=0,angleB=70,rad=10")
                           )


    powerLimitVoltage = 108.0 # tweak this to place the label in a good spot along PD
    slope = -Pd/(powerLimitVoltage*powerLimitVoltage)
    graphSlope = slope*(GraphHeight/IaMAX)/(GraphWidth/VaMAX)
    angle = (180.0/np.pi) * np.arctan(graphSlope)
    placeLabel(plt,"Power\n%.1fW"%Pd,powerLimitVoltage,Pd/powerLimitVoltage,angle)

    for i in range(1,colcount):
        l = colnames[i]
        for j in range(rowcount):
            if datasheetCurveData['PlateVoltage'][j]*datasheetCurveData[colnames[i]][j] > (Pd+1.0):
                placeLabel(plt,l,datasheetCurveData['PlateVoltage'][j],datasheetCurveData[colnames[i]][j],0)
                break

    plateToPlateImpedance = float(Rl * n**2)
    plateImpedance_A = plateToPlateImpedance / 2.0 # push-pull plateImpedance is 1/2 of a-a
    #                                       when running in class A
    plateImpedance_B = plateToPlateImpedance / 4.0 # push-pull plateImpedance is 1/4 of a-a
    #                                       when running in class B
  

    m_A = -1/plateImpedance_A
    b_A = Ia + Va/plateImpedance_A
    m_B = -1/plateImpedance_B
    b_B = Va/plateImpedance_B
    
    vllintersect = -(b_A - b_B)/(m_A - m_B)
    illintersect = m_A * vllintersect + b_A
    
    PlateVoltages_lt = np.array(np.where(PlateVoltages <= vllintersect))[0]
    PlateVoltages_gt = np.array(np.where(PlateVoltages >= vllintersect))[0]

    ll_A_lt = m_A*PlateVoltages_lt+b_A    
    ll_A_gt = m_A*PlateVoltages_gt+b_A
    ll_B_lt = m_B*PlateVoltages_lt+b_B
    ll_B_gt = m_B*PlateVoltages_gt+b_B

    
    # print vllintersect,illintersect
    null = plt.annotate(s="%.1fV@%.1fmA"%(vllintersect,illintersect*1000),
                        xy=(vllintersect,illintersect),
                        xycoords='data',
                        xytext=(20,20),
                        textcoords='offset points',
                        bbox=dict(boxstyle="round", fc="1.0"),
                        size=12,
                        arrowprops=dict(arrowstyle="->",
                                        connectionstyle="angle,angleA=0,angleB=70,rad=10"))

    lastplot = plt.plot(PlateVoltages_lt,ll_A_lt,linestyle='dashed')
    null = plt.plot(PlateVoltages_gt,ll_A_gt,label='Class A load %d ohm'%plateImpedance_A,linewidth=3,color=lastplot[0].get_color())
    #null = plt.plot(Va,Ia, 'or',label='Op Point',color='g')
    
    lastplot = plt.plot(PlateVoltages_gt,ll_B_gt,linestyle='dashed')
    null = plt.plot(PlateVoltages_lt,ll_B_lt,label='Class B load %d ohm'%plateImpedance_B,linewidth=3,color=lastplot[0].get_color())


    for i in range(1,colcount):
        mindiff = 10
        for v in PlateVoltages:
            try:
                ia = iaf[colnames[i]]['valueAt'](v)
                iall = m_B*v+b_B
                diff = abs(ia - iall)

                if diff < mindiff:
                    vinter = v
                    iinter = iall
                    mindiff = diff
            except ValueError:
                pass

        iaf[colnames[i]]['loadLineIntersectionV'] = vinter
        iaf[colnames[i]]['loadLineIntersectionI'] = iinter

        if colnames[i] == cutoffCurveVoltage:
            break

    vsat = iaf[colnames[1]]['loadLineIntersectionV']
    isat = iaf[colnames[1]]['loadLineIntersectionI']
     
    null = plt.annotate(s="%.1fV@%.1fmA"%(vsat,isat*1000),
                        xy=(vsat,isat),
                        xycoords='data',
                        xytext=(-120,20),
                        textcoords='offset points',
                        bbox=dict(boxstyle="round", fc="1.0"),
                        size=12,
                        arrowprops=dict(arrowstyle="->",
                                        connectionstyle="angle,angleA=0,angleB=110,rad=10"))    

    null = plt.annotate(s="%.1fV@%.1fmA"%(Va,Ia*1000),
                        xy=(Va,Ia),
                        xycoords='data',
                        xytext=(20,20),
                        textcoords='offset points',
                        bbox=dict(boxstyle="round", fc="1.0"),
                        size=12,
                        arrowprops=dict(arrowstyle="->",
                                        connectionstyle="angle,angleA=0,angleB=70,rad=10"))

    null = plt.plot(Va,Ia, 'or',label='Op Point',color='g')

    vcut = Va
    icut = 0.0
    null = plt.annotate(s="%.1fV@%.1fmA"%(vcut,icut*1000),
                        xy=(vcut,icut),
                        xycoords='data',
                        xytext=(20,20),
                        textcoords='offset points',
                        bbox=dict(boxstyle="round", fc="1.0"),
                        size=12,
                        arrowprops=dict(arrowstyle="->",
                                        connectionstyle="angle,angleA=0,angleB=70,rad=10"))



    power = (Va - vsat)**2/plateToPlateImpedance
    title = "Vg2 = 360V\nLoad = %d$\Omega$ <> %d$\Omega$\nPower = %.1fW (V^2/R = (%.1fV-%.1fV)^2)/%d$\Omega$ )"%(Rl,plateToPlateImpedance,power,Va,vsat,plateToPlateImpedance)
    null = plt.suptitle(title,fontsize=14, fontweight='bold')
    null = plt.legend()
    
null = interact(plotAB,
             _Ia=widgets.FloatSlider(min=0.01,max=0.1,step=0.0025,value=Ia),
             _Va=widgets.FloatSlider(min=50,max=500,step=5,value=Va),
             _Rl=widgets.FloatSlider(min=2,max=16,step=2,value=Rl),
             _n=widgets.FloatSlider(min=10,max=40,step=1,value=n)
                )


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-4-8ceda7309883> in <module>()
    192     null = plt.legend()
    193 
--> 194 null = interact(plotAB,
    195              _Ia=widgets.FloatSlider(min=0.01,max=0.1,step=0.0025,value=Ia),
    196              _Va=widgets.FloatSlider(min=50,max=500,step=5,value=Va),

NameError: name 'interact' is not defined

In [ ]:
def get_pd(t,v_amplitude):
    global Ia,Va,Rl,n,m_A,b_A,m_B,b_B,vllintersect,illintersect
    t = t * 2 * np.pi
    v = Va - v_amplitude*np.sin(t)
    if v <= vllintersect:
        i = m_B * v + b_B
    else:
        i = m_A * v + b_A
    if i < 0:
        i = 0
    return v*i

def plotInstanteousPowerPositive(v_amplitude):
    plt.figure(figsize=(15, 10))
    plt.grid(linestyle='--', linewidth=0.5)

    t = np.linspace(0,1,1000)
    get_pd_vect = np.vectorize(get_pd,otypes=[float])
    pd = get_pd_vect(t,v_amplitude)

    plt.plot(t,pd)
    plt.fill_between(t,0,pd,facecolor='lightgrey')
    
    pdtotal = integrate.quad(get_pd, 0, 1,args=(v_amplitude,))
    
    title = "Vamp = %dV\nPlate Dissipation = %.1fW"%(v_amplitude,pdtotal[0])
    null = plt.suptitle(title,fontsize=14, fontweight='bold')
    plt.show()
    
null = interact(plotInstanteousPowerPositive,
             v_amplitude=widgets.FloatSlider(min=0,max=360,step=5,value=100))
#plotTransientPositive(amp)

In [ ]:
voltages = np.linspace(0,360,90)
platedissipations = []
for v in voltages:
    platedissipations.append(integrate.quad(get_pd, 0, 1,args=(v,)))
platedissipations = np.array(platedissipations)

plt.figure(figsize=(15, 10))
plt.grid(linestyle='--', linewidth=0.5)
plt.plot(voltages,platedissipations)
plt.show()

In [ ]: