1 Introduction

In this post, we’re going to see how the operating point of a circuit can be found in the presence of non-linear elements. Before reading further, make sure you have read my previous post on the Newton-Raphson method.

2 Circuit Description

To illustrate how the process works, we’re going to consider a simple circuit with components whose derivatives are easy to calculate.

Figure 1. Schematic of circuit analyzed in this document

Figure 1. Schematic of circuit analyzed in this document

The description of the circuit is given below.

  1. A resistor $R_{1}$ connected between node 1 ($V_{1}$) and an independent voltage source ($V_{\operatorname{in}}$).
  2. A non-linear device $\phi _{1}$, whose current is explicitly given by $\phi _{1} (V_{1}, V_{2}) = a \cdot (V_{1} - V_{2})^2$, connected between node 1 ($V_{1}$) and node 2 ($V_{2}$).
  3. A capacitor $C_{1}$ connected between node 1 ($V_{1}$) and node 2 ($V_{2}$). This can be thought of as the parasitic capacitance associated with $\phi _{1}$.
  4. A resistor $R_{2}$ connected between node 2 ($V_{2}$) and ground.
  5. A non-linear device $\phi _{2}$, whose current is explicitly given by $\phi _{2} (V_{2}) = b \cdot (V_{2})^2$, connected between node 2 ($V_{2}$) and ground.
  6. A capacitor $C_{2}$ connected between node 2 ($V_{2}$) and ground. This can be thought of as the parasitic capacitance associated with $\phi _{2}$.

3 Operating Point Analysis

The operating point analysis calculates the steady-state solutions of the circuit. By definition, after all transients die out, the voltages across inductors and the currents through capacitors become equal to zero. Consequently, branches containing capacitances are replaced with independent zero current sources while inductances are replaced with independent zero voltage sources.

Let’s write the Kirchhoff’s Current Law (KCL) equations for nodes 1 and 2:

  1. At node 1:

    \begin{equation} \frac{V_{1} - V_{\operatorname{in}}}{R_{1}} + a \cdot (V_{1} - V_{2})^2 = 0 \tag{1} \end{equation}

  2. At node 2:

    \begin{equation} \frac{V_{2}}{R_{2}} + b \cdot (V_{2})^2 + a \cdot (V_{1} V_{2})^2 = 0 \tag{2} \end{equation}

To apply the Newton-Raphson method, we start with an initial guess for the unknown voltages, and then iteratively update the guess using the Newton-Raphson update equation:

\begin{equation} \boldsymbol{V}^{(k + 1)} =\boldsymbol{V}^{(k)} - (J (\boldsymbol{V}^{(k)}))^{- 1} F (\boldsymbol{V}^{(k)}) \tag{3} \end{equation}

The function vector is given by:

\begin{equation} F (\boldsymbol{V}) = \left[ \begin{array}{c} f_{1} (V_{1}, V_{2})\\\ f_{2} (V_{1}, V_{2}) \end{array} \right] = \left[ \begin{array}{c} \frac{V_{1} - V_{\operatorname{in}}}{R_{1}} + a \cdot (V_{1} - V_{2})^2\\\ \frac{V_{2}}{R_{2}} + b \cdot (V_{2})^2 - a \cdot (V_{1} - V_{2})^2 \end{array} \right] \tag{4} \end{equation}

The Jacobian matrix is given by:

\begin{equation} J = \left[ \begin{array}{cc} \frac{\partial f_{1}}{\partial V_{1}} & \frac{\partial f_{1}}{\partial V_{2}}\\\ \frac{\partial f_{2}}{\partial V_{1}} & \frac{\partial f_{2}}{\partial V_{2}} \end{array} \right] = \left[ \begin{array}{cc} \frac{1}{R_{1}} + 2 a \cdot (V_{1} - V_{2}) & - 2 a \cdot (V_{1} - V_{2})\\\ - 2 a \cdot (V_{1} - V_{2}) & \frac{1}{R_{2}} + 2 b \cdot V_{2} + 2 a \cdot (V_{1} - V_{2}) \end{array} \right] \tag{5} \end{equation}

Now, you can iteratively update the voltage vector $\boldsymbol{V}$ until convergence is achieved (i.e., when the change in $V$ between iterations is below a certain threshold).

3.1 Python Implementation

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import numpy as np

def f(V, Vin):
    V1, V2 = V
    F = np.empty(shape=(2,))
    F[0] = (V1 - Vin) / R1 + a * (V1 - V2) ** 2
    F[1] = V2 / R2 + b * V2**2 - a * (V1 - V2) ** 2
    return F

def jacobian(V):
    V1, V2 = V
    J = np.empty(shape=(2, 2))
    J[0, 0] = 1 / R1 + 2 * a * (V1 - V2)
    J[0, 1] = -2 * a * (V1 - V2)
    J[1, 0] = -2 * a * (V1 - V2)
    J[1, 1] = 1 / R2 + 2 * b * V2 + 2 * a * (V1 - V2)
    return J

def newton_raphson(V_init, Vin, tol=1e-6, max_iter=100):
    V = V_init
    for i in range(max_iter):
        delta_V = np.linalg.solve(jacobian(V), -f(V, Vin))
        V += delta_V
        print(f"Iteration {i+1}: {V}")
        if np.linalg.norm(delta_V) < tol:
            break
    return V

if __name__ == "__main__":
    # circuit parameters
    R1 = 500
    R2 = 200
    a = 0.002
    b = 0.0025

    # solve
    Vin = 10
    V_init = np.array([0.0, 0.0])
    newton_raphson(V_init, Vin)

The solution converged after 7 iterations.

1
2
3
4
5
6
7
Iteration 1: [10.  0.]
Iteration 2: [6.55172414 1.37931034]
Iteration 3: [4.45438848 1.3321058 ]
Iteration 4: [3.9485912  1.41838181]
Iteration 5: [3.89663859 1.42543581]
Iteration 6: [3.89611585 1.42551177]
Iteration 7: [3.89611579 1.42551177]

4 DC Sweep Anlysis

A DC sweep analysis is used to analyze the behavior of a circuit over a range of DC voltage or current values. It helps to understand how the circuit performs when the DC input varies. Performing a DC sweep analysis involves performing an operating point analysis for each sweep value. This can be time-consuming, especially for large or complex circuits. In many cases, the circuit being analyzed may have only small changes in the component between successive analysis runs. As a result, the operating point of the circuit may not change significantly from one analysis to the next. In such cases, using the previous solution as an initial guess can lead to faster convergence, since it provides a good starting point for the solver to refine the solution.

4.1 Python Implementation

The implementation is the same as the one for the operating point analysis.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
if __name__ == "__main__":
    # circuit parameters
    R1 = 500
    R2 = 200
    a = 0.002
    b = 0.0025

    # sweep parameters
    Vin = np.linspace(1, 10, 5)
    V_init = np.array([0.0, 0.00])

    # solve
    for v in Vin:
        print(f"Solving for Vin={v}")
        V_init = newton_raphson(V_init, v)

The result of the simulation is given below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Solving for Vin=1.0
Iteration 1: [1. 0.]
Iteration 2: [0.73684211 0.10526316]
Iteration 3: [0.68461785 0.11915083]
Iteration 4: [0.68285533 0.11969462]
Iteration 5: [0.68285317 0.11969526]
Iteration 6: [0.68285317 0.11969526]
Solving for Vin=3.25
Iteration 1: [1.9306664  0.47771652]
Iteration 2: [1.73508572 0.48728713]
Iteration 3: [1.72496666 0.48997782]
Iteration 4: [1.72492551 0.48998644]
Iteration 5: [1.72492551 0.48998644]
Solving for Vin=5.5
Iteration 1: [2.63030701 0.85096113]
Iteration 2: [2.55116651 0.83286504]
Iteration 3: [2.55038884 0.83294543]
Iteration 4: [2.5503887  0.83294545]
Solving for Vin=7.75
Iteration 1: [3.3096891  1.15825783]
Iteration 2: [3.26152139 1.14266795]
Iteration 3: [3.26130549 1.14265154]
Iteration 4: [3.26130548 1.14265154]
Solving for Vin=10.0
Iteration 1: [3.92978715 1.43789662]
Iteration 2: [3.89620546 1.42552852]
Iteration 3: [3.89611579 1.42551177]
Iteration 4: [3.89611579 1.42551177]