AIの片隅で(51) Googleの生成AI、Gemini、Pythonスクリプトにコメント

Joseph Halfmoon

前回、MicroPythonスクリプトをGemini様に吟味していただいたところ「コメントいれろや」とのご指摘。その通り。この際開き直ってGemini様にコメントを入れてとお願いしたらどうよ。pydoc 形式のドキュメンテーション・コメントね。ただしMicroPythonでは諸般の事情あり、フツーのPythonね。

AIの片隅で 投稿順index

※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様のご回答の冒頭。

A1_start

プロンプトには日本語で入力しておるのですが、前回同様スクリプト本文には日本語を含まぬせいか、英文でご回答いただけてます。

前回同様、Pythonスクリプトの動作の解釈については非の打ちどころがありません。完璧に理解してらっしゃる感じっす。以下は、実際にある関数について生成していただいたドキュメンテーションコメント付きのソース(右)を、オリジナルのソース(左)と比較しているところです。MOD1

自分で、拙い英文書くくらいなら、Gemini様におまかせする方がずっといいかも。

ユニット・テストを掛けてみる

もともとのソースには簡単なユニットテスト(全関数ではないけれども)が含まれております。その件についてはGemini様も以下のように「テスト」項目で言及ありです。なお、黄色のマーカ部も気になりますがそれは後で。なおご回答の冒頭は英文でしたが、「続きのコードも示して」とお願いしているうちに、回答文は日本語になっていました。A1Notes

切れ切れ(約100行程度、行数はいろいろ)に出力されたソースをつなぎ合わせ(手作業です。切れ目の部分は文字重複部ありです。なおつなぎ合わせたファイル名は元ファイルと被らないように DOCwaveGen.pyとしました)、ユニットテストを実施したところ、まさかのエラー発生。

原因はとみると、切れ目の部分のインデントでした。こんな感じ。A1_p0

上記の赤丸つけた部分は、直上のクラス内の関数定義に対応するドキュメンテーションコメントなのですが、段付けが1段足らなくなってます。他の言語ではともかくPythonでは大間違い。ちょうど切れ目にかかってしまったためなのかな?

段付けエラーを修正したところ以下のようにユニット・テストが通過する状態に戻りました。utestOK

続いて実行してみる

流石Gemini様は、このスクリプトをどう使うのかも分かってらっしゃいます。使用例はこんな感じ。A1Usage

自分で書いたスクリプトなのですが、Gemini様に言われるままに動かすと、こんな感じ。RUNSOK

実際にLTspiceの電圧源に貼り付けて結果を確かめたわけではないけれども、まあプログラムは動いているんでないかい。

pydoc を使ってドキュメンテーションを生成

こうしてソースが出来てしまえば、ドキュメンテーション作成は一撃。pydocのコンソール出力の冒頭が以下に。documentGenerated

 

いい感じだな。これから英文コメントの作成は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()