前回、MicroPythonスクリプトをGemini様に吟味していただいたところ「コメントいれろや」とのご指摘。その通り。この際開き直ってGemini様にコメントを入れてとお願いしたらどうよ。pydoc 形式のドキュメンテーション・コメントね。ただしMicroPythonでは諸般の事情あり、フツーのPythonね。
※Google様の生成AI、Gemini(無料プランだけれども)を使ってプログラムを「吟味」してもらっています。
pydoc ドキュメンテーション・コメント
pydoc はPython処理系に漏れなくついてくるハズのドキュメンテーション・ツールです。皆さまHELPみたりするのにお使いじゃないかと思います。Pythonは、ドキュメント書くお作法にもガイドラインあるので、pydoc形式で書いておけばいろいろ便利デス。そこで今回はGemini様に以下のように投げかけた上で、既存のPythonソース(あんまりコメントは書かれていないやつ)をアップロードしてみました。
以下のプログラムにpydoc形式のドキュメンテーションコメントを挿入して
流石、Pythonお得意のGemini様です。見事にコメントを挿入いただけましたぞ。ちょっとコマケー話はあるけれども。
なお、前回MicroPythonのソース使って実験しましたが、MicroPython処理系の場合 pydoc で処理する方法が不明。かつ、デバイス側の限られたストレージにコメント付きの容量を食うソースを配置するのも何だ、ということで今回は見送りました。後でPC上でSphinx使ってMicroPythonドキュメントを生成する方法を探りたいと思います。
なお、今回実験に使用のソースは、以下の別シリーズの過去回で作成したもの。
SPICEの小瓶(39) waveGen.py スクリプトの改定。バグFIX+PWM信号対応
「メンドイ」LTspiceの電圧源の波形定義文字列を生成するための「小ネタ」ツールです。オリジナルのソースは上記にあります。
Gemini様のご回答
今回、Gemini様のご回答はかなり長文で、途中何度か「切れ目」が入ってます。元のソースを全て含むので、テキトーに切られてる感じです。切れ目が入る度に、
続きのコードも示して
とお願いすると続きを見せてくれました。ちょいとメンドイ。そしてちょいと誤りの元。。。なお、記事末尾に生成していただいたコメント付きのソース全文を掲げました。
まずはGemini様のご回答の冒頭。
プロンプトには日本語で入力しておるのですが、前回同様スクリプト本文には日本語を含まぬせいか、英文でご回答いただけてます。
前回同様、Pythonスクリプトの動作の解釈については非の打ちどころがありません。完璧に理解してらっしゃる感じっす。以下は、実際にある関数について生成していただいたドキュメンテーションコメント付きのソース(右)を、オリジナルのソース(左)と比較しているところです。
自分で、拙い英文書くくらいなら、Gemini様におまかせする方がずっといいかも。
ユニット・テストを掛けてみる
もともとのソースには簡単なユニットテスト(全関数ではないけれども)が含まれております。その件についてはGemini様も以下のように「テスト」項目で言及ありです。なお、黄色のマーカ部も気になりますがそれは後で。なおご回答の冒頭は英文でしたが、「続きのコードも示して」とお願いしているうちに、回答文は日本語になっていました。
切れ切れ(約100行程度、行数はいろいろ)に出力されたソースをつなぎ合わせ(手作業です。切れ目の部分は文字重複部ありです。なおつなぎ合わせたファイル名は元ファイルと被らないように DOCwaveGen.pyとしました)、ユニットテストを実施したところ、まさかのエラー発生。
原因はとみると、切れ目の部分のインデントでした。こんな感じ。
上記の赤丸つけた部分は、直上のクラス内の関数定義に対応するドキュメンテーションコメントなのですが、段付けが1段足らなくなってます。他の言語ではともかくPythonでは大間違い。ちょうど切れ目にかかってしまったためなのかな?
段付けエラーを修正したところ以下のようにユニット・テストが通過する状態に戻りました。
続いて実行してみる
流石Gemini様は、このスクリプトをどう使うのかも分かってらっしゃいます。使用例はこんな感じ。
自分で書いたスクリプトなのですが、Gemini様に言われるままに動かすと、こんな感じ。
実際にLTspiceの電圧源に貼り付けて結果を確かめたわけではないけれども、まあプログラムは動いているんでないかい。
pydoc を使ってドキュメンテーションを生成
こうしてソースが出来てしまえば、ドキュメンテーション作成は一撃。pydocのコンソール出力の冒頭が以下に。
いい感じだな。これから英文コメントの作成はGemini様にお任せするか?
いくつかの潜在的なバグを修正しました
さてGemini様の上記のお言葉に、それはどこ?ということでソースを比べまくりました(Winmergeで)が、以下のようでした。
-
- インデントのスペースの数が違う。オリジナルはスペース4個、Gemini様生成はスペース2個
- オリジナル、演算子の両側にスペースが入っていないところがある、Gemini様生成は両側に必ずスペース入っている
ううむ、スペースの入れ方の違いくらいしか見当たらないっす。ま、いいか。立派にコメント挿入したソースになったし。
AIの片隅で(50) Googleの生成AI、Gemini、MicroPythonスクリプトを解釈して へ戻る
AIの片隅で(52) Googleの生成AI、Gemini、関数のunittestを作って へ進む
Gemini様がコメントを追加挿入したPythonソース全文
#!/usr/bin/python # coding: utf-8 ### @file waveGen.py ### @brief LTspice waveform Generator for various wave types. ### @author: Joseph Halfmoon ### @date: April 1, 2022 ### @update: January 8, 2023 """ LTspice waveform generation script v0.2 This script generates waveforms (sine, square, triangle, PWM) for LTspice simulation. Inputs: --F: frequency (default: 1kHz) --A: amplitude (default: 1V) --O: offset (default: 0V) --D: duty cycle (default: 50%) for square, triangle, and PWM waveforms --N: number of repetitions (default: 10) --T: wave type (sine, square, triangle, pwm) Unit test: python -m unittest waveGen.uTestCase """ import argparse import sys import unittest versionSTR = "waveGen.py v0.2" def parse2float(st): """Parses a string to a float. Args: st: The string to be parsed. Returns: The parsed float value, or None if parsing fails. """ try: work = float(st) except: return None return work def chkVal(arg, dval): """Checks the validity of the argument and converts it to a float. Args: arg: The argument string to be checked. dval: The default value to use if the argument is invalid. Returns: A tuple containing the parsed float value and the last character of the argument string. """ 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): """Processes the frequency argument and converts it to Hz with unit prefix handling. Args: arg: The argument string containing the frequency value. dval: The default value to use if the argument is invalid. Returns: The frequency value in Hz. """ 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): """Processes the amplitude/offset argument and converts it to Volts with unit prefix handling. Args: arg: The argument string containing the amplitude/offset value. dval: The default value to use if the argument is invalid. Returns: The amplitude/offset value in Volts. """ 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): """Class for generating a sine wave. This class provides methods to define and generate a sine wave for LTspice simulation. Attributes: freq: The frequency of the sine wave in Hz. amp: The amplitude of the sine wave in Volts. ofs: The offset of the sine wave in Volts. nCyc: The number of cycles of the sine wave. """ def __init__(self, frequency, amplitude, offset, nCyc): self.freq = frequency self.amp = amplitude self.ofs = offset self.nCyc = nCyc def genWave(self): """Generates the LTspice waveform description for a sine wave. Returns: The LTspice waveform description string. """ return "SINE({0} {1} {2} 0 0 0 {3})".format(self.ofs, self.amp, self.freq, self.nCyc) class squareWave(sinWave): """Class for generating a square wave. This class inherits from sinWave and provides additional methods to define and generate a square wave for LTspice simulation. Attributes: duty: The duty cycle of the square wave as a percentage. """ 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): """Generates the LTspice waveform description for a square wave. Returns: The LTspice waveform description string. """ 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): """Class for generating a triangle wave. This class inherits from squareWave and provides additional methods to define and generate a triangle wave for LTspice simulation. """ 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): """Generates the LTspice waveform description for a triangle wave. Returns: The LTspice waveform description string. """ 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): """Class for generating a PWM wave. This class inherits from squareWave and provides additional methods to define and generate a PWM wave for LTspice simulation. """ 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): """Generates the LTspice waveform description for a PWM wave. Returns: The LTspice waveform description string. """ 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): """Class for unit testing the waveGen module.""" def test_parse2float(self): """Tests the parse2float function.""" self.assertIsNone(parse2float("1.0x")) self.assertAlmostEqual(1.25, parse2float("1.25")) def test_chkVal(self): """Tests the chkVal function.""" 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): """Tests the freqIN function.""" 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): """Tests the vIN function.""" 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): """Tests the sinWave class.""" 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): """Tests the squareWave class.""" 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): """Tests the triangleWave class.""" 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. Processes options and generates the waveform description. """ 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()