Porting from Spice

Spice.NET tries to follow the original Spice framework of Spice 3f5. This section is meant for people that would like to create their own model/devices for the framework, given that they have code for the original Spice framework.

Parameter definitions

Parameters are defined in a table that maps a name to an ID, description and some flags for that parameter. In Spice.NET the same parameter table should be accessible per instance. To save some space, you can store this table in a static variable, and use ParamTable { get; } to return the static parameter table.

Let's take for example the resistor model:

lib/dev/res/res.c:
IFparm RESmPTable[] = { /* model parameters */
 IOPQ( "rsh",    RES_MOD_RSH,      IF_REAL,"Sheet resistance"),
 IOPZ( "narrow", RES_MOD_NARROW,   IF_REAL,"Narrowing of resistor"),
 IOPQ( "tc1",    RES_MOD_TC1,      IF_REAL,"First order temp. coefficient"),
 IOPQO( "tc2",    RES_MOD_TC2,      IF_REAL,"Second order temp. coefficient"),
 IOPX( "defw",   RES_MOD_DEFWIDTH, IF_REAL,"Default device width"),
 IOPXU("tnom",  RES_MOD_TNOM,     IF_REAL,"Parameter measurement temperature"),
 IP( "r",      RES_MOD_R,        IF_FLAG,"Device is a resistor model")
};

lib/dev/res/resdefs.h
/* model parameters */
#define RES_MOD_TC1 101
#define RES_MOD_TC2 102
#define RES_MOD_RSH 103
#define RES_MOD_DEFWIDTH 104
#define RES_MOD_NARROW 105
#define RES_MOD_R 106
#define RES_MOD_TNOM 107

Which is turned into the following for Spice.NET:
ResistorModel.cs:
private enum _c
{
    RES_MOD_TC1 = 101, RES_MOD_TC2 = 102, RES_MOD_RSH = 103, RES_MOD_DEFWIDTH = 104, RES_MOD_NARROW = 105, RES_MOD_R = 106, RES_MOD_TNOM = 107
}

private static Dictionary<string, IP> _pTable = new Dictionary<string, IP>
{
    { "rsh", new IP(IF.IOPQ, (int)_c.RES_MOD_RSH, T.REAL, "Sheet resistance") },
    { "narrow", new IP(IF.IOPZ, (int)_c.RES_MOD_NARROW, T.REAL, "Narrowing of resistor") },
    { "tc1", new IP(IF.IOPQ, (int)_c.RES_MOD_TC1, T.REAL, "First order temp. coefficient") },
    { "tc2", new IP(IF.IOPQO, (int)_c.RES_MOD_TC2, T.REAL, "Second order temp. coefficient") },
    { "defw", new IP(IF.IOPX, (int)_c.RES_MOD_DEFWIDTH, T.REAL, "Default device width") },
    { "tnom", new IP(IF.IOPXU, (int)_c.RES_MOD_TNOM, T.REAL, "Parameter measurement temperature") },
    { "r", new IP(IF.IP, (int)_c.RES_MOD_R, T.FLAG, "Device is a resistor model") }
};

/// <summary>
/// Get a list of parameters
/// </summary>
public override Dictionary<string, Parameterized.IP> ParamTable
{
    get { return _pTable; }
}

Parameter logic

The defined parameters can be accessed using the indexing operator (default is implemented). The indexer code will check the object type and whether or not this parameter can be accessed (eg. input-only cannot be read). If these checks are passed, the indexer will call either the Param() or Ask() method. Both are functions that Spice 3f5 also defines. Our resistor model for example will be:

lib/dev/res/resmpar.c:
int
RESmParam(param,value,inModel)
    int param;
    IFvalue *value;
    GENmodel *inModel;
{
    register RESmodel *model = (RESmodel *)inModel;
    switch(param) {
        case RES_MOD_TNOM:
            model->REStnom = value->rValue+CONSTCtoK;
            model->REStnomGiven = TRUE;
            break;
        case RES_MOD_TC1:
            model->REStempCoeff1 = value->rValue;
            model->REStc1Given = TRUE;
            break;
        case RES_MOD_TC2:
            model->REStempCoeff2 = value->rValue;
            model->REStc2Given = TRUE;
            break;
        case RES_MOD_RSH:
            model->RESsheetRes = value->rValue;
            model->RESsheetResGiven = TRUE;
            break;
        case RES_MOD_DEFWIDTH:
            model->RESdefWidth = value->rValue;
            model->RESdefWidthGiven = TRUE;
            break;
        case RES_MOD_NARROW:
            model->RESnarrow = value->rValue;
            model->RESnarrowGiven = TRUE;
            break;
        case RES_MOD_R:
            /* just being reassured by user that this is a resistor model */
            /* no-op */
            break;
        default:
            return(E_BADPARM);
    }
    return(OK);
}

This becomes:
ResistorModel.cs
/// <summary>
/// Set a parameter
/// </summary>
protected override void Param(int id, object value, Circuit ckt = null)
{
    switch ((_c)id)
    {
        case _c.RES_MOD_TNOM:
            REStnom.Par((double)value + Circuit.CONSTCtoK);
            break;
        case _c.RES_MOD_TC1:
            REStempCoeff1.Par((double)value);
            break;
        case _c.RES_MOD_TC2:
            REStempCoeff2.Par((double)value);
            break;
        case _c.RES_MOD_RSH:
            RESsheetRes.Par((double)value);
            break;
        case _c.RES_MOD_DEFWIDTH:
            RESdefWidth.Par((double)value);
            break;
        case _c.RES_MOD_NARROW:
            RESnarrow.Par((double)value);
            break;
        case _c.RES_MOD_R:
            /* just being reassured by user that this is a resistor model */
            /* no-op */
            break;
        default:
            throw new BadParameterException();
    }
}

Note: Parameters that also need to know if they were given or not, use the Parameter<> class. In order to set the value and Given flag to true, use the Par() method. They have an implicit conversion to their generic type, so you can use them as a regular variable.

The same for asking a variable:

lib/dev/res/resmask.c:
/* ARGSUSED */
int 
RESmodAsk(ckt,inModel,which,value)
    CKTcircuit *ckt;
    GENmodel *inModel;
    int which;
    IFvalue *value;
{
    RESmodel *model = (RESmodel *)inModel;
    switch(which) {
        case RES_MOD_TNOM:
            value->rValue = model->REStnom-CONSTCtoK;
            return(OK);
        case RES_MOD_TC1:
            value->rValue = model->REStempCoeff1;
            return(OK);
        case RES_MOD_TC2:
            value->rValue = model->REStempCoeff2;
            return(OK);
        case RES_MOD_RSH:
            value->rValue = model->RESsheetRes;
            return(OK);
        case RES_MOD_DEFWIDTH:
            value->rValue = model->RESdefWidth;
            return(OK);
        case RES_MOD_NARROW: 
            value->rValue = model->RESnarrow;
            return(OK);
        default:
            return(E_BADPARM);
    }
}

becomes:
ResistorModel.cs:
/// <summary>
/// Ask a parameter
/// </summary>
/// <param name="id"></param>
/// <param name="ckt"></param>
/// <returns></returns>
protected override object Ask(int id, Circuit ckt = null)
{
    switch ((_c)id)
    {
        case _c.RES_MOD_TNOM:
            return REStnom - Circuit.CONSTCtoK; // REStnom implicitly converted to double
        case _c.RES_MOD_TC1:
            return REStempCoeff1.Value; // Don't just return the object REStempCoeff1, but its value
        case _c.RES_MOD_TC2:
            return REStempCoeff2.Value;
        case _c.RES_MOD_RSH:
            return RESsheetRes.Value;
        case _c.RES_MOD_DEFWIDTH:
            return RESdefWidth.Value;
        case _c.RES_MOD_NARROW:
            return RESnarrow.Value;
        default:
            throw new BadParameterException();
    }
}

Setup instances

For Spice 3f5, the instance asks for pointers to elements in the Jacobian and indices for use in the Rhs vector (Right-Hand-Side vector). This is done differently in Spice.NET that uses OOP.

In the resistor instance, the setup function looks like this:
lib/dev/res/ressetup.c:
/* ARGSUSED */
int
RESsetup(matrix,inModel,ckt,state)
    register SMPmatrix *matrix;
    GENmodel *inModel;
    CKTcircuit*ckt;
    int *state;
        /* load the resistor structure with those pointers needed later 
         * for fast matrix loading 
         */
{
    register RESmodel *model = (RESmodel *)inModel;
    register RESinstance *here;

    /*  loop through all the resistor models */
    for( ; model != NULL; model = model->RESnextModel ) {

        /* loop through all the instances of the model */
        for (here = model->RESinstances; here != NULL ;
                here=here->RESnextInstance) {
            
/* macro to make elements with built in test for out of memory */
#define TSTALLOC(ptr,first,second) \
if((here->ptr = SMPmakeElt(matrix,here->first,here->second))==(double *)NULL){\
    return(E_NOMEM);\
}

            TSTALLOC(RESposPosptr, RESposNode, RESposNode);
            TSTALLOC(RESnegNegptr, RESnegNode, RESnegNode);
            TSTALLOC(RESposNegptr, RESposNode, RESnegNode);
            TSTALLOC(RESnegPosptr, RESnegNode, RESposNode);
        }
    }
    return(OK);
}

In Spice.NET, this is converted to the following part:
Resistor.cs:
/// <summary>
/// Setup the resistor
/// </summary>
/// <param name="ckt"></param>
/// <param name="model"></param>
/// <param name="p"></param>
public override void Setup(Circuit ckt, Model model, object p = null)
{
    // Bind nodes
    var nodes = BindNodes(ckt);
    this.RESposNode = nodes[0].Number;
    this.RESnegNode = nodes[1].Number;
}

The method BindNodes() will ask the circuit for the indices in the matrix and Rhs vector. The returned value is an array of integers which contains the indices in the order of the terminals. A resistor has 2 terminals so the returned value is an array with two elements.

It is possible at this point to ask for more nodes than the number of terminals. For example, a bipolar transistor can define an extra resistance connected to its collector and drain. In this case the instance can call
var nodes = BindNodes(ckt, NodeType.Voltage, NodeType.Voltage);
In that case, the nodes variable will contain 6 indices: the collector index, base index, emitter index, substrate index (the terminals) followed by two extra nodes of the type voltage. These extra nodes have no name!

Temperature-dependent calculations

The temperature-dependent calculations are relatively straight-forward.

lib/dev/res/restemp.c:
int
REStemp(inModel,ckt)
    GENmodel *inModel;
    register CKTcircuit *ckt;
        /* perform the temperature update to the resistors
         * calculate the conductance as a function of the
         * given nominal and current temperatures - the
         * resistance given in the struct is the nominal
         * temperature resistance
         */
{
    register RESmodel *model =  (RESmodel *)inModel;
    register RESinstance *here;
    double factor;
    double difference;


    /*  loop through all the resistor models */
    for( ; model != NULL; model = model->RESnextModel ) {

        /* Default Value Processing for Resistor Models */
        if(!model->REStnomGiven) model->REStnom = ckt->CKTnomTemp;
        if(!model->RESsheetResGiven) model->RESsheetRes = 0;
        if(!model->RESdefWidthGiven) model->RESdefWidth = 10.e-6; /*M*/
        if(!model->REStc1Given) model->REStempCoeff1 = 0;
        if(!model->REStc2Given) model->REStempCoeff2 = 0;
        if(!model->RESnarrowGiven) model->RESnarrow = 0;

        /* loop through all the instances of the model */
        for (here = model->RESinstances; here != NULL ;
                here=here->RESnextInstance) {
            
            /* Default Value Processing for Resistor Instance */
            if(!here->REStempGiven) here->REStemp = ckt->CKTtemp;
            if(!here->RESwidthGiven) here->RESwidth = model->RESdefWidth;
            if(!here->RESlengthGiven)  here->RESlength = 0 ;
            if(!here->RESresGiven)  {
                if(model->RESsheetResGiven && (model->RESsheetRes != 0) &&
                        (here->RESlength != 0)) {
                    here->RESresist = model->RESsheetRes * (here->RESlength -
                        model->RESnarrow) / (here->RESwidth - model->RESnarrow);
                } else {
                    (*(SPfrontEnd->IFerror))(ERR_WARNING,
                            "%s: resistance=0, set to 1000",&(here->RESname));
                    here->RESresist=1000;
                }
            }

            difference = here->REStemp - model->REStnom;
            factor = 1.0 + (model->REStempCoeff1)*difference + 
                    (model->REStempCoeff2)*difference*difference;

            here->RESconduct = 1.0/(here->RESresist * factor);
        }
    }
    return(OK);
}

This is ported to:
/// <summary>
/// Do temperature-dependent stuff
/// </summary>
/// <param name="model"></param>
/// <param name="ckt"></param>
public override void Temperature(Circuit ckt, Model model, object p = null)
{
    double factor;
    double difference;
    ResistorModel rmod = model as ResistorModel;

    /* Default Value Processing for Resistor Instance */
    if (!REStemp.Given) REStemp.Value = ckt.Config.Temperature;
    if (!RESwidth.Given) RESwidth.Value = rmod.RESdefWidth;
    if (!RESlength.Given) RESlength.Value = 0;
    if (!RESresist.Given)
    {
        if (rmod.RESsheetRes.Given && (rmod.RESsheetRes != 0) && (RESlength != 0))
        {
            RESresist.Value = rmod.RESsheetRes * (RESlength - rmod.RESnarrow) / (RESwidth - rmod.RESnarrow);
        }
        else
        {
            CircuitWarning.Warning(String.Format("{0}: resistance=0, set to 1000", Name));
            RESresist.Value = 1000;
        }
    }

    difference = REStemp - rmod.REStnom;
    factor = 1.0 + (rmod.REStempCoeff1) * difference + (rmod.REStempCoeff2) * difference * difference;

    RESconduct = 1.0 / (RESresist * factor);
}

The parameter object p = null can be used by the model to pass some parameters that the instance might need.

Load

The load function is probably the main function for the instance. There are a few important differences with Spice 3f5.

Precompiler settings

Spice.NET uses code with the following precompiler settings set:
#define PREDICTOR
#define NOBYPASS
#define NEWCONV

It also doesn't (yet) include sensitivity analysis. So remove any code that has anything to with this.

States

States in Spice 3f5 are accessed as follows:
*(ckt->CKTstate0+index)

In Spice.NET, this is done using
ckt.State.States[0][index]

Vectors

In Spice 3f5, the Rhs vector is filled in the load function. After loading, the solution is stored in the same Rhs vector. If the solution did not converge yet, Rhs is moved to RhsOld, which frees the Rhs vector again for loading. Spice.NET doesn't use in-place solving, so you can use the more logical OldSolution variable. The Rhs vector has the same name.

In Spice 3f5:
*(ckt->CKTrhsOld+nodeindex)
*(ckt->CKTrhs+nodeindex)

In Spice.NET:
ckt.State.OldSolution[nodeindex]
ckt.State.Rhs[nodeindex]

Matrix

The matrix is usually accessed by a pointer to the relevant element in the matrix. Spice.NET uses Math.NET SparseMatrix implementation.

Spice 3f5:
*(here->pointer_nodea_nodeb) += geq;

Spice.NET:
ckt.State.Matrix[nodea, nodeb] += geq;

Integration

Integration in Spice 3f5 is done in a function called NIintegrate(). This function also exists in Spice.NET, and is implemented by the abstract IntegrationMethod class.

Spice 3f5:
// Integrate state variable at state_index and store the result at state_index+1
error = NIintegrate(ckt, &geq, &ceq, capacitance, state_index);

Spice.NET:
// Integrate state variable at state_index and store the result at state_index+1
var result = ckt.Integration.Integrate(ckt.State, state_index, capacitance);
// result.Geq and result.Ceq contain the same values as geq and ceq in Spice 3f5

Example

For example, a capacitor has the following load function:

lib/dev/cap/capload.c: (after accounting for precompiler settings)
int
CAPload(inModel,ckt)

GENmodel *inModel;
register CKTcircuit *ckt;
/* actually load the current capacitance value into the
 * sparse matrix previously provided
 */
{
    register CAPmodel *model = (CAPmodel*)inModel;
    register CAPinstance *here;
    register int cond1;
    double vcap;
    double geq;
    double ceq;
    int error;

    /* check if capacitors are in the circuit or are open circuited */
    if (ckt->CKTmode & (MODETRAN|MODEAC|MODETRANOP) ) {
        /* evaluate device independent analysis conditions */
        cond1=
            ( ( (ckt->CKTmode & MODEDC) &&
                (ckt->CKTmode & MODEINITJCT) )
              || ( ( ckt->CKTmode & MODEUIC) &&
                   ( ckt->CKTmode & MODEINITTRAN) ) ) ;
        /*  loop through all the capacitor models */
        for ( ; model != NULL; model = model->CAPnextModel ) {

            /* loop through all the instances of the model */
            for (here = model->CAPinstances; here != NULL ;
                    here=here->CAPnextInstance) {

                if (cond1) {
                    vcap = here->CAPinitCond;
                } else {
                    vcap = *(ckt->CKTrhsOld+here->CAPposNode) -
                           *(ckt->CKTrhsOld+here->CAPnegNode) ;
                }
                if (ckt->CKTmode & (MODETRAN | MODEAC)) {
                    *(ckt->CKTstate0+here->CAPqcap) = here->CAPcapac * vcap;
                    if ((ckt->CKTmode & MODEINITTRAN)) {
                        *(ckt->CKTstate1+here->CAPqcap) =
                            *(ckt->CKTstate0+here->CAPqcap);
                    }
                    error = NIintegrate(ckt,&geq,&ceq,here->CAPcapac,
                                        here->CAPqcap);
                    if (error) return(error);
                    if (ckt->CKTmode & MODEINITTRAN) {
                        *(ckt->CKTstate1+here->CAPccap) =
                            *(ckt->CKTstate0+here->CAPccap);
                    }
                    *(here->CAPposPosptr) += geq;
                    *(here->CAPnegNegptr) += geq;
                    *(here->CAPposNegptr) -= geq;
                    *(here->CAPnegPosptr) -= geq;
                    *(ckt->CKTrhs+here->CAPposNode) -= ceq;
                    *(ckt->CKTrhs+here->CAPnegNode) += ceq;
                } else
                    *(ckt->CKTstate0+here->CAPqcap) = here->CAPcapac * vcap;
            }
        }
    }
    return(OK);
}

In Spice.NET this is turned into:

CapacitorModel.cs:
/// <summary>
/// Load the circuit with instance of this model
/// </summary>
/// <param name="ckt"></param>
public override void Load(Circuit ckt)
{
    bool cond1 = false;

    // Actually, this part is the same for all capacitor models as well, perhaps it would be possible to optimize this if necessary?
    // Check if the capacitors are in the circuit or are open-circuited
    if ((ckt.Mode & (Circuit.Modes.Tran | Circuit.Modes.Ac | Circuit.Modes.TranOp)) != 0)
    {
        /* evaluate device independent analysis conditions */
        cond1 =
            (((ckt.Mode.HasFlag(Circuit.Modes.Dc)) &&
                (ckt.Mode.HasFlag(Circuit.Modes.InitJct)))
            || ((ckt.Mode.HasFlag(Circuit.Modes.Uic)) &&
                (ckt.Mode.HasFlag(Circuit.Modes.InitTran))));
    }

    // Loop through all capacitors
    foreach (Instance inst in Instances)
        inst.Load(ckt, this, cond1);
}

Capacitor.cs:
/// <summary>
/// Load the Yn-matrix and vector with the capacitor info
/// </summary>
/// <param name="model"></param>
/// <param name="ckt"></param>
public override void Load(Circuit ckt, Model model, object p = null)
{
    double vcap;
    bool cond1 = (bool)p;
    CircuitState state = ckt.State;

    if (cond1)
    {
        vcap = CAPinitCond;
    }
    else
    {
        vcap = state.OldSolution[CAPposNode] - state.OldSolution[CAPnegNode];
    }
    if ((ckt.Mode & (Circuit.Modes.Tran | Circuit.Modes.Ac)) != 0)
    {
        state.States[0][CAPstate + CAPqcap] = CAPcapac * vcap;
        if (ckt.Mode.HasFlag(Circuit.Modes.InitTran))
        {
            state.States[1][CAPstate + CAPqcap] = state.States[0][CAPstate + CAPqcap];
        }
        var result = ckt.Integration.Integrate(state, CAPstate + CAPqcap, CAPcapac);
        if (ckt.Mode.HasFlag(Circuit.Modes.InitTran))
            state.States[1][CAPstate + CAPccap] = state.States[0][CAPstate + CAPccap];

        state.Matrix[CAPposNode, CAPposNode] += result.Geq;
        state.Matrix[CAPnegNode, CAPnegNode] += result.Geq;
        state.Matrix[CAPposNode, CAPnegNode] -= result.Geq;
        state.Matrix[CAPnegNode, CAPposNode] -= result.Geq;
        state.Rhs[CAPposNode] -= result.Ceq;
        state.Rhs[CAPnegNode] += result.Ceq;
    }
    else
        state.States[0][CAPstate + CAPqcap] = CAPcapac * vcap;
}

Last edited Feb 11 at 2:02 PM by SBoulang, version 7