SPICEの小瓶(39) waveGen.py スクリプトの改定。バグFIX+PWM信号対応

Joseph Halfmoon

前回、ハーフブリッジ回路もどきを駆動するシミュレーションを行うためにPWM波形を使用したいと思いました。ハーフブリッジなのでノンオーバラップ期間のある2相ね。ところが電圧源でPULSE波形を指定するのに数値指定に難渋しました。忘却力の頭では暗算できんと。そこで1年数か月ぶりに波形生成スクリプトに手を入れることに。

※「SPICEの小瓶」投稿順インデックスはこちら

※ Analog Devices, Inc. LTspice を使用させていただいて動作確認しております。今回使用のバージョンは以下です。

XVII(x64) (17.0.37.0)

waveGen.pyスクリプト

LTspiceでシミュレーションを行う場合、電圧源(Voltage Source)が備える各種の関数にパラメータを与えて所望の波形を得ることが多いと思います。正弦波は勿論、PULSE波やら各種の波形を生成可能です。しかし「メンドイ」のが各種パラメータを数値で与えないといけないことです。それも三角波とか矩形波とかパターンジェネレータだったら一発で生成できそうなシンプルな奴らが以外とメンドイっす。その対策でかねてより使っております自前ツールが waveGen.pyというPython3のスクリプトです。コマンドラインで周波数とか振幅とか波形タイプなどを与えるとLTspiceの電圧源のところにコピペできる行を生成するだけのもの。-h を与えて起動すると以下のように使用方法が表示されます。wavegenHelp

今回更新内容

約2年近くぶりの更新内容は以下です。

    1. Dutyを指定したときに発生するバグフィックス
    2. –T pwm を指定することでPWM波形(2相)を出力
    3. –N n オプション追加。繰り返し回数指定可能とした

2番のPWM波形(2相分)を生成するのがモチベーションになりましたが、何気にずっとほったらかしてあった1番と3番にも手をいれました。

PWM波形は、SQUARE波形とは異なり0Vから振幅指定までの「デジタル」っぽい波形です。オフセットは無視。デューティを指定することで所望の波形を得るのですが、その際「逆相」のPWM波形も同時に生成します。例えばデューティ0.4と指定すると第1の波が0.4の期間ONになった後OFFったのち、0.1のノンオーバラップ期間を置いて第2の波が0.4の期間ONになりという具合です。今のところは対称な波形のみ。

ソース

Python3(動作確認バージョンは3.10.11)のソースが以下に。

#!/usr/bin/python
# coding: utf-8
### @file  waveGen.py
### @brief LTspice waveform Generator
###
### @author: Joseph Halfmoon
### @date:   April 1, 2022
### @update: January 8, 2023
"""
LTspice waveform generation script v0.2

input
=====
--F frequency
--A amplitude
--O offset
--D duty
--N number of repetition
--T type. (sine square triangle pwm)

unittest
========
python -m unittest waveGen.uTestCase

"""
import argparse
import sys
import unittest

versionSTR = "waveGen.py v0.2"

def parse2float(st):
    """parse string to float
    
    Returns at Fail: None
    """
    try:
        work = float(st)
    except:
        return None
    return work

def chkVal(arg, dval):
    vl = parse2float(arg)
    if vl is None:
        nLen = len(arg)
        if nLen > 1:
            bdy = arg[0:nLen-1]
            lastChar = arg[nLen-1]
            vl = parse2float(bdy)
        if vl is None:
            vl = dval
    else:
        lastChar = "0"
    return (vl, lastChar)

def freqIN(arg, dval):
    prefixes = 'mkMG'
    vl, lastChar = chkVal(arg, dval)
    pfx = prefixes.find(lastChar)
    if pfx == 0:
        ex = 1e-3
    elif pfx == 1:
        ex = 1e3
    elif pfx == 2:
        ex = 1e6
    elif pfx == 3:
        ex = 1e9
    else:
        ex = 1
    return vl * ex

def vIN(arg, dval):
    prefixes = 'm'
    vl, lastChar = chkVal(arg, dval)
    pfx = prefixes.find(lastChar)
    if pfx == 0:
        ex = 1e-3
    else:
        ex = 1
    return vl * ex

class sinWave(object):
    """Wave Generation base class
    
    generate sine wave.
    """
    def __init__(self, frequency, amplitude, offset, nCyc):
        self.freq = frequency
        self.amp = amplitude
        self.ofs = offset
        self.nCyc = nCyc

    def genWave(self):
        return "SINE({0} {1} {2} 0 0 0 {3})".format(self.ofs, self.amp, self.freq, self.nCyc)

class squareWave(sinWave):
    """Square wave Generation class
    
    squareWave 0.0.
    """
    def __init__(self, frequency, amplitude, offset, nCyc, duty):
        super(squareWave, self).__init__(frequency, amplitude, offset, nCyc)
        self.risTIM = 1e-9
        self.falTIM = 1e-9
        self.tperiod = 1.0 / self.freq
        self.ton = self.tperiod * duty
        self.duty = duty
        self.vinit = self.ofs - self.amp
        self.von   = self.ofs + self.amp

    def genWave(self):
        return "PULSE({0} {1} 0 {2} {3} {4} {5} {6})".format(self.vinit, self.von, self.risTIM, self.falTIM, self.ton, self.tperiod, self.nCyc)

class triangleWave(squareWave):
    """Triangle wave Generation class
    
    triangleWave 0.0.
    """
    def __init__(self, frequency, amplitude, offset, nCyc, duty):
        super(triangleWave, self).__init__(frequency, amplitude, offset, nCyc, duty)
        self.risTIM = self.tperiod * self.duty
        self.falTIM = self.tperiod * (1.0 - self.duty)
        self.ton = 0.0

    def genWave(self):
        return "PULSE({0} {1} 0 {2} {3} {4} {5} {6})".format(self.vinit, self.von, self.risTIM, self.falTIM, self.ton, self.tperiod, self.nCyc)

class pwmWave(squareWave):
    """PWM wave Generation class
    
    pwmWave 0.0.
    """
    def __init__(self, frequency, amplitude, offset, nCyc, duty):
        super(pwmWave, self).__init__(frequency, amplitude, offset, nCyc, duty)
        self.vinit = 0
        self.von = self.amp
        self.dly1 = 0.0
        self.dly2 = self.ton + (self.tperiod * (1-duty) - self.ton) / 2

    def genWave(self):
        w1 = "PULSE({0} {1} {2} {3} {4} {5} {6} {7})".format(self.vinit, self.von, self.dly1, self.risTIM, self.falTIM, self.ton, self.tperiod, self.nCyc)
        w2 = "PULSE({0} {1} {2} {3} {4} {5} {6} {7})".format(self.vinit, self.von, self.dly2, self.risTIM, self.falTIM, self.ton, self.tperiod, self.nCyc)
        return w1 + "\n" + w2

class uTestCase(unittest.TestCase):

    def test_parse2float(self):
        self.assertIsNone(parse2float("1.0x"))
        self.assertAlmostEqual(1.25, parse2float("1.25"))

    def test_chkVal(self):
        vl, lc = chkVal("ABC", 1.11)
        self.assertAlmostEqual(1.11, vl)
        self.assertEqual("C", lc)
        vl, lc = chkVal("1.25", 0)
        self.assertAlmostEqual(1.25, vl)
        self.assertEqual("0", lc)
        vl, lc = chkVal("1.5m", 0)
        self.assertAlmostEqual(1.5, vl)
        self.assertEqual("m", lc)

    def test_freqIN(self):
        self.assertAlmostEqual(1000, freqIN("ABC", 1000))
        self.assertAlmostEqual(100, freqIN("100X", 2000))
        self.assertAlmostEqual(1.5, freqIN("1.5", 0))
        self.assertAlmostEqual(1.23e3, freqIN("1.23k", 0))
        self.assertAlmostEqual(1.23e-3, freqIN("1.23m", 0))
        self.assertAlmostEqual(1.23e6, freqIN("1.23M", 0))
        self.assertAlmostEqual(1.23e9, freqIN("1.23G", 0))

    def test_vIN(self):
        self.assertAlmostEqual(1.1, vIN("XYZ", 1.1))
        self.assertAlmostEqual(1.55, vIN("1.55X", 1.2))
        self.assertAlmostEqual(1.5, vIN("1.5", 0))
        self.assertAlmostEqual(100e-3, vIN("100m", 0))

    def test_sinWave(self):
        wav = sinWave(1000, 1.0, 0.0, 10)
        self.assertEqual("SINE(0.0 1.0 1000 0 0 0 10)", wav.genWave())

    def test_squareWave(self):
        wav = squareWave(1000, 1.0, 0.0, 10, 0.5)
        self.assertEqual("PULSE(-1.0 1.0 0 1e-09 1e-09 0.0005 0.001 10)", wav.genWave())

    def test_triangleWave(self):
        wav = triangleWave(1000, 1.0, 0.0, 10, 0.5)
        self.assertEqual("PULSE(-1.0 1.0 0 0.0005 0.0005 0.0 0.001 10)", wav.genWave())

def main():
    """main program.
    
    process options
    """
    parser = argparse.ArgumentParser(description=versionSTR)
    parser.add_argument('--F', nargs=1, help='frequency (default=1k[Hz])')
    parser.add_argument('--A', nargs=1, help='amplitude (defalut=1[v])')
    parser.add_argument('--O', nargs=1, help='offset (defalut=0[v])')
    parser.add_argument('--D', nargs=1, help='duty (defalut=0.5)')
    parser.add_argument('--N', nargs=1, help='number of repetition (defalut=10)')
    parser.add_argument('--T', nargs=1, help='type (sine square triangle pwm defalut=sine)')
    parser.add_argument('-V', dest='VERSION', help='Show Version, then exit', action='store_true', default=False)
    args = parser.parse_args()

    if args.VERSION:
        print( versionSTR )
        sys.exit(0)

    freq = 1.0e3
    if args.F is not None:
        freq = freqIN(args.F[0], freq)
    amp = 1.0
    if args.A is not None:
        amp = vIN(args.A[0], amp)
    ofs = 0.0
    if args.O is not None:
        ofs = vIN(args.O[0], ofs)
    dty = 0.5
    if args.D is not None:
        dty = chkVal(args.D[0], dty)[0]
    nRep = 10
    if args.N is not None:
        try:
            nRep = int(args.N[0])
        except:
            nRep = 10

    wav = sinWave(freq, amp, ofs, nRep)
    if args.T is not None:
        if args.T[0].startswith("sq"):
            wav = squareWave(freq, amp, ofs, nRep, dty)
        elif args.T[0].startswith("tr"):
            wav = triangleWave(freq, amp, ofs, nRep, dty)
        elif args.T[0].startswith("pw"):
            wav = pwmWave(freq, amp, ofs, nRep, dty)

    print(wav.genWave())

    sys.exit(0)

if __name__ == "__main__":
    main()
使用例

今回の更新の目玉のPWM波形の指定をした場合が以下に。デューティ40%のPWMということ以外は後はデフォルト値(周波数1kHz、ピークツーピークの振幅1V、繰り返し回数10回)です。pwmWaveSample

2行出力されたPULSE指定の行をLTspiceの2つの電圧源にコピペして得た信号が以下に。PWMwave

手で書くとかなりメンドイのよ。

SPICEの小瓶(38) 小信号用の手元部品でハーフブリッジ・インバータ「もどき」 へ戻る

SPICEの小瓶(40) LTspiceのテキスト形式入出力、コマケー話なんだが へ進む