LibreOffice Calc で Python から帳票作成(テンプレートファイルの利用)

前回からの続き。

半日ほど調べて、LibreOffice Calc では、印刷用のページテンプレート(ヘッダーやフッターなど)を UNO API からは操作できないことがわかりました。ドラッグ&ドロップでコピーできないのも変だけどマクロからも操作ができませんね。

仕方がないので、あらかじめ印刷用のページテンプレートを設定したファイル(*.ots)を作っておいて、それを利用することにします。別に普通の Calc ファイル(*.ods)を使っても作れます。

ページスタイルの作成

あれこれ探すとページスタイルが作れるようになりました。
「スタイルペインを開く」→「ページスタイルアイコンをクリック」という手順でページスタイル(いわゆるシートに紐づけるスタイル)を作成&設定できます。デフォルトで「標準」と「レポート」というスタイルが作っていあるので、ここでは「帳票テンプレート」というスタイルを作成しました。

ヘッダー、フッターの仕方は Excel の設定と似たような感じです。

確か Excel では印刷プレビューから飛べたはずなのですが、LibreOffice Calc では飛べません。まあ、あまり印刷をしないのかもしれません。ただし、文書として PDF に変換することが多いでしょうから、こういう印刷スタイルも整備してほしいかなと。

このファイルを「report-template.ots」あるいは「report-template.ods」のように保存しておきます。テンプレートファイルの *.ots にしてしまうと、

C:\Users\Tomoaki\AppData\Roaming\LibreOffice\4\user\template\report-template.ots

こんな感じの LibreOffice のテンプレート用のフォルダーに保存されてしまうので結構面倒です。テンプレート文書とはいえ Python マクロから参照するだけなので、プログラムと同じ場所に置いたほうが実験がしやすいと思います。

テンプレート文書を開いて、別名で閉じる

いろいろ試したのですが、以下の手順が一番安定しています。

  1. テンプレート文書を開く
  2. データ等を書き込む
  3. 別名でファイルを保存して閉じる

途中の動作を確認しようかと思って、テンプレート文書のほうの Calc を開いたままにしようかと思ったのですが、Calc が落ちます。どうやら、UI で何か受け付けようとしてうまくいっていないようです。マクロを動かしている Calc を Hidden にしておくと安定します。

import uno
from pathlib import Path

# サーバーの起動
# & "C:\Program Files\LibreOffice\program\soffice" --accept="socket,host=localhost,port=2002;urp;" --norestore --nologo
# PowerShell での起動
# & "C:\Program Files\LibreOffice\program\python" .\calcReport002.py

"""
テンプレートから新規ドキュメントを作成して Calc レポートを生成するサンプル。

手順:
1. 「帳票テンプレート」(report-template.ots) をテンプレートとして新規ドキュメントを開く
2. サンプルデータの行数に合わせて明細行の書式を複製
3. データを書き込む
4. 別名で保存
"""


# 設定: テンプレートと出力先
TEMPLATE_NAME = "report-template.ots"
OUTPUT_NAME = "report-output.ods"
TEMPLATE_SHEET_NAME = "Template"  # テンプレート内の帳票シート名
REPORT_SHEET_NAME = "Report"      # 必要ならこの名前のシートを使用(存在しなければ先頭シートを使う)


def to_file_url(path: Path) -> str:
	return uno.systemPathToFileUrl(str(path.resolve()))


def connect_desktop():
	local_ctx = uno.getComponentContext()
	resolver = local_ctx.ServiceManager.createInstanceWithContext(
		"com.sun.star.bridge.UnoUrlResolver", local_ctx
	)
	ctx = resolver.resolve(
		"uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext"
	)
	smgr = ctx.ServiceManager
	desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
	doc = desktop.getCurrentComponent()
	if doc is None:
		raise RuntimeError("アクティブな Calc ドキュメントが見つかりません")
	return desktop, doc


def load_template(desktop, template_path: Path):
	url = to_file_url(template_path)
	props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	props[0].Name = "Hidden"
	props[0].Value = True
	return desktop.loadComponentFromURL(url, "_blank", 0, props)


def ensure_rows_with_format(sheet, template_row_idx: int, start_row: int, rows_needed: int, last_col: int):
	template_range = sheet.getCellRangeByPosition(0, template_row_idx, last_col, template_row_idx)
	source_addr = template_range.getRangeAddress()
	template_height = sheet.getRows().getByIndex(template_row_idx).Height

	for i in range(rows_needed):
		target_row = start_row + i
		if target_row == template_row_idx:
			continue
		dest_addr = uno.createUnoStruct("com.sun.star.table.CellAddress")
		dest_addr.Sheet = source_addr.Sheet
		dest_addr.Column = 0
		dest_addr.Row = target_row
		sheet.copyRange(dest_addr, source_addr)
		sheet.getRows().getByIndex(target_row).Height = template_height


def write_data(sheet, data, start_row: int = 1, start_col: int = 0):
	for r, row in enumerate(data):
		for c, value in enumerate(row):
			cell = sheet.getCellByPosition(start_col + c, start_row + r)
			if isinstance(value, (int, float)):
				cell.setValue(value)
			else:
				cell.setString(str(value))


def main():
	base_dir = Path(__file__).resolve().parent
	template_path = base_dir / TEMPLATE_NAME
	output_path = base_dir / OUTPUT_NAME

	sample_data = [
		["001", "Alice", "Sales", 1200.5],
		["002", "Bob", "Marketing", 980.0],
		["003", "Carol", "Engineering", 1540.75],
		["004", "Dave", "Support", 760.25],
	]

	if not template_path.exists():
		raise FileNotFoundError(f"テンプレートが見つかりません: {template_path}")

	desktop, _ = connect_desktop()
	doc = load_template(desktop, template_path)

	# 帳票シートを取得(指定名がなければ先頭シート)
	sheets = doc.Sheets
	if sheets.hasByName(REPORT_SHEET_NAME):
		report_sheet = sheets.getByName(REPORT_SHEET_NAME)
	elif sheets.hasByName(TEMPLATE_SHEET_NAME):
		report_sheet = sheets.getByName(TEMPLATE_SHEET_NAME)
	else:
		report_sheet = sheets.getByIndex(0)

	# データ件数に合わせて行を用意(ヘッダー1行 + データ行)
	last_col = len(sample_data[0]) - 1 if sample_data else 0
	ensure_rows_with_format(
		sheet=report_sheet,
		template_row_idx=1,
		start_row=1,
		rows_needed=len(sample_data),
		last_col=last_col,
	)

	# データを書き込み(1行目はヘッダー想定)
	write_data(report_sheet, sample_data, start_row=1, start_col=0)
	# シート名を変更
	report_sheet.Name = "給与レポート"

	# 別名で保存
	store_props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	store_props[0].Name = "FilterName"
	store_props[0].Value = "calc8"
	doc.storeToURL(to_file_url(output_path), store_props)
	print(f"レポートを保存しました: {output_path}")

	# 後処理
	try:
		doc.dispose()
	except Exception:
		pass


if __name__ == "__main__":
	try:
		main()
	except Exception as e:
		print(f"error: {e}")


後で、もう少し業務用の Python コードを整理しますが、肝は以下の部分です。

def load_template(desktop, template_path: Path):
	url = to_file_url(template_path)
	props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	props[0].Name = "Hidden"
	props[0].Value = True
	return desktop.loadComponentFromURL(url, "_blank", 0, props)

ここで Hidden の値を False にすると Calc が表示されるようになりますが、頻繁に Calc が落ちます。UNO API で操作しているときは、UI を止めたほうがよさそうです。

帳票作成

作成できた report-output.ods がこれです。

印刷プレビュー

ヘッダー、フッターの操作はできないので、あらかじめテンプレート文書に入れておきます。マクロから操作できないのは痛いのですが、社名などは固定になるし印刷したときの日時はヘッダー/フッターの機能として用意されているので、あまり不便にはならないでしょう。

複数ページになったときの設定とかは Excel と同じなので、これもいけると思います。

次は、裏にデータシートを設定して関数とかでセル参照をしているときの動作ですね。請求書とかを作るときに便利な方法です。

カテゴリー: 開発, LibreOffice | LibreOffice Calc で Python から帳票作成(テンプレートファイルの利用) はコメントを受け付けていません

LibreOffice Calc で Python から帳票作成(シートのスタイル以外は)

Excel から LibreOffice Calc に移行するときに、いわゆる帳票の作成ができるか否か私的には結構な肝になります。ちまちまと Calc で罫線を引いているのならば別なのですが、プログラムを使い自動化をしているとちょっと状況が異なります。

ClosedXMLを使って、超高速にリスト形式の帳票を作成する https://www.moonmile.net/blog/archives/9672

自分が良く使っているのは(これは逆引き大全にも入っているけど)、こんな風に C# + ClosedXML で帳票を自動作成するパターンです。帳票を作るツールはいくつかあるのですが、この場合は帳票のテンプレートをカスタムしやすくするように、テンプレートを Excel 自身で作成しています。罫線やら印刷時のタイトルなどはテンプレートとなる Excel ファイルに書いておきます。そして、シートに数値や文字列だけを C# から書き込むようにします。

このようにすると、

  • 年次のバージョンアップのように、帳票が変化してもプログラムに手を入れなくて済む
  • プログラムをつくるときにも罫線の修正や出力位置は Excel を使うことができるので、帳票政策が楽

と言う利点があります。欠点としては

  • Excel を業務 PC に入れないといけない

ですね。当然といえば当然なのですが、C# から Excel オブジェクトを扱うために(実際には ClosedXML なので、OpenXML のほうなのですが)Excel 自体が必須になってしまいます。

実は、ファイル作成だけならば Excel は必要がないのですが、印刷時に Excel オブジェクトが必要なのと、帳票のシート自体の編集に Excel が必要なので、開発者はともかくとしてお客にも Excel が必要になってしまいます。まあ、大抵は Excel を持っているからいいんですが。

Excel + C# + ClosedXML の組み合わせを LibreOffice Calc に変換できるか?

肝は、Excel + ClosedXML の組み合わせが、LibreOffice Calc + UNO API でできるか?ということです。私としては C# から UNO API を使えればよいのですが、前記事から C# から UNO API を使うには難点があるので、素直に Python でコーディングをします。

まあ、コーディングをするといってもベースを作るのは AI エージェントなので、それを動かしてちょこちょこと Python コードを修正することになります。

帳票テンプレート

まず、帳票テンプレート(report-template.ods)を用意します。

テンプレートのシートには、罫線やタイトル行の色を付けておきます。他にも行の高さも変えます。セルのフォーマットも変えておきましょう。実際の帳票を作るように試してみるのがよいです。

印刷時のヘッダーやフッター、タイトル行なんかも入れておくとよいです。

このテンプレートのシートは、作成するシートとしてコピーされてるので、このような印刷フォーマットなども使えます。先の C# + ClosedXML の例もそうなのですが、この手の細かい調節をプログラムですべてやるのは大変です。複雑な処理をしたい場合は、別途帳票ツールを購入したほうが良いでしょう。ただし、そこまで複雑すぎないとか、年単位で帳票のフォーマットが変わってしまうとか、そういうカスタムが多い場合は、こんな形で Excel あるいは Office Calc でテンプレートとなるシートを作ったほうが楽です。

実際、先の業務では20帳票位のテンプレートを Excel で自作しています。その分、プログラムコードが激減するのです。

AI エージェントに渡すプロンプト

実装するためのプロンプトは以下の通りです。


"""
テンプレートファイルを使って Calc レポートを作成するサンプル

# 実装手順

1. テンプレートファイルを開く
2. テンプレートシートをコピーして新しいシートを作成
3. サンプルデータの行数を計算する
4. シートの1行目はヘッダー行なのでそのまま
5. 2行目以降にサンプルデータをデータの行数分だけ用意する。
 5.1 行の罫線も一緒にコピーして、行数を増やす
6. データを行数分書き込む

"""

何等かのシステムならば色々目的なり設計を書いたほうがいいのですが、今回は既に作成されている Excel + ClosedXML のコードを模倣したいので、その手順を直接書いています。C# のコードを渡して「UNO API + Python に書き直して」でも通じるかもしれませんが、今回は少し丁寧に手順を書いています。業務コードのほうは、業務特有の複雑なパターンが掛かれているので、余計な情報を詰め込まないといけないので、ここではサンプル帳票にあわせます。

できあがったコード

少し手直しをしていますが、ほぼそのまま AI エージェント(GPT-5.1-Codex-Max)で書いたものです。行の高さが上手くコピーできていなかったので、後からプロンプトで足しています。

import uno
from pathlib import Path

# サーバーの起動
# & "C:\Program Files\LibreOffice\program\soffice" --accept="socket,host=localhost,port=2002;urp;" --norestore --nologo
# PowerShell での起動
# & "C:\Program Files\LibreOffice\program\python" .\calcReport001.py

"""
テンプレートファイルを使って Calc レポートを作成するサンプル

# 実装手順

1. テンプレートファイルを開く
2. テンプレートシートをコピーして新しいシートを作成
3. サンプルデータの行数を計算する
4. シートの1行目はヘッダー行なのでそのまま
5. 2行目以降にサンプルデータをデータの行数分だけ用意する。
 5.1 行の罫線も一緒にコピーして、行数を増やす
6. データを行数分書き込む

"""


# 設定: テンプレートと出力先
TEMPLATE_NAME = "report-template.ods"
OUTPUT_NAME = "report-output.ods"
TEMPLATE_SHEET_NAME = "Template"
REPORT_SHEET_NAME = "Report"


def to_file_url(path: Path) -> str:
	return uno.systemPathToFileUrl(str(path.resolve()))


def connect_desktop():
	local_ctx = uno.getComponentContext()
	resolver = local_ctx.ServiceManager.createInstanceWithContext(
		"com.sun.star.bridge.UnoUrlResolver", local_ctx
	)
	ctx = resolver.resolve(
		"uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext"
	)
	smgr = ctx.ServiceManager
	desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
	doc = desktop.getCurrentComponent()
	if doc is None:
		raise RuntimeError("アクティブな Calc ドキュメントが見つかりません")
	return desktop, doc


def load_template(desktop, template_path: Path):
	url = to_file_url(template_path)
	props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	props[0].Name = "Hidden"
	props[0].Value = True
	return desktop.loadComponentFromURL(url, "_blank", 0, props)


def import_template_sheet(target_doc, template_doc, template_sheet_name: str, report_sheet_name: str):
	target_sheets = target_doc.Sheets
	if target_sheets.hasByName(report_sheet_name):
		target_sheets.removeByName(report_sheet_name)

	# importSheet: 第3引数は挿入位置(末尾に追加)
	target_sheets.importSheet(template_doc, template_sheet_name, target_sheets.getCount())

	# 取り込んだシート名はテンプレートのものなので、目的の名前に変更
	imported_sheet = target_sheets.getByName(template_sheet_name)
	imported_sheet.Name = report_sheet_name
	return imported_sheet


def ensure_rows_with_format(sheet, template_row_idx: int, start_row: int, rows_needed: int, last_col: int):
	template_range = sheet.getCellRangeByPosition(0, template_row_idx, last_col, template_row_idx)
	source_addr = template_range.getRangeAddress()
	template_height = sheet.getRows().getByIndex(template_row_idx).Height

	for i in range(rows_needed):
		target_row = start_row + i
		if target_row == template_row_idx:
			continue  # テンプレート行は既に存在
		dest_addr = uno.createUnoStruct("com.sun.star.table.CellAddress")
		dest_addr.Sheet = source_addr.Sheet
		dest_addr.Column = 0
		dest_addr.Row = target_row
		sheet.copyRange(dest_addr, source_addr)
		sheet.getRows().getByIndex(target_row).Height = template_height


def write_data(sheet, data, start_row: int = 1, start_col: int = 0):
	for r, row in enumerate(data):
		for c, value in enumerate(row):
			cell = sheet.getCellByPosition(start_col + c, start_row + r)
			if isinstance(value, (int, float)):
				cell.setValue(value)
			else:
				cell.setString(str(value))


def main():
	base_dir = Path(__file__).resolve().parent
	template_path = base_dir / TEMPLATE_NAME
	output_path = base_dir / OUTPUT_NAME

	sample_data = [
		["001", "Alice", "Sales", 1200.5],
		["002", "Bob", "Marketing", 980.0],
		["003", "Carol", "Engineering", 1540.75],
		["004", "Dave", "Support", 760.25],
	]

	if not template_path.exists():
		raise FileNotFoundError(f"テンプレートが見つかりません: {template_path}")

	desktop, active_doc = connect_desktop()
	template_doc = load_template(desktop, template_path)

	# テンプレートドキュメントからシートをアクティブドキュメントへコピー
	report_sheet = import_template_sheet(
		target_doc=active_doc,
		template_doc=template_doc,
		template_sheet_name=TEMPLATE_SHEET_NAME,
		report_sheet_name=REPORT_SHEET_NAME,
	)

	# データ件数に合わせて行を用意(ヘッダー1行 + データ行)
	last_col = len(sample_data[0]) - 1 if sample_data else 0
	ensure_rows_with_format(
		sheet=report_sheet,
		template_row_idx=1,  # テンプレートの明細行(2行目)
		start_row=1,
		rows_needed=len(sample_data),
		last_col=last_col,
	)

	# データを書き込み(1行目はヘッダー想定)
	write_data(report_sheet, sample_data, start_row=1, start_col=0)

	# 別名で保存
	store_props = (uno.createUnoStruct("com.sun.star.beans.PropertyValue"),)
	store_props[0].Name = "FilterName"
	store_props[0].Value = "calc8"
	active_doc.storeToURL(to_file_url(output_path), store_props)
	print(f"レポートを保存しました: {output_path}")

	# 後処理: そのまま開いたままでも良いが、ここではクローズ
	template_doc.dispose()


if __name__ == "__main__":
	try:
		main()
	except Exception as e:
		print(f"error: {e}")


書き込むデータが sample_data として直書きになっていますが、これは SQLite や CSV ファイルなどから読み込むように変更すればいいでしょう。別途 Calc のシートにあるデータを参照してもよいかもしれません。別シートのデータを参照するのは、請求書などの1枚帳票でやることが多いので、後で試してみましょう。

動作確認

Python で実行させるとこんな感じです。現在開いている Calc に新しいシート「Report」が追加されます。ここでは、Calc を表示した状態にしていますが、自動でファイルに保存することができます。もともと、AI エージェントのコードは画面に表示せず、ファイルに保存するだけのものになっています。

ここでは、確認の意味もあって、Calc を表示させたままにしています。

印刷プレビューの状態

シート自体はうまくいっているのですが、印刷時のスタイルがうまくいかないようです。
どうやら、LibreOffice Calc では、シートをUIでドラッグ&ドロップしたときでもシート単位でのスタイルが設定されないようで、これは仕様みたいですね。


def import_template_sheet(target_doc, template_doc, template_sheet_name: str, report_sheet_name: str):
	target_sheets = target_doc.Sheets
	if target_sheets.hasByName(report_sheet_name):
		target_sheets.removeByName(report_sheet_name)

	# importSheet: 第3引数は挿入位置(末尾に追加)
	target_sheets.importSheet(template_doc, template_sheet_name, target_sheets.getCount())

	# 取り込んだシート名はテンプレートのものなので、目的の名前に変更
	imported_sheet = target_sheets.getByName(template_sheet_name)
	imported_sheet.Name = report_sheet_name

	# ページスタイル(印刷の向きやヘッダー/フッター等)をプロパティ単位でコピー
	template_styles = template_doc.getStyleFamilies().getByName("PageStyles")
	target_styles = target_doc.getStyleFamilies().getByName("PageStyles")
	page_style_name = imported_sheet.PageStyle

	if page_style_name:
		template_style = template_styles.getByName(page_style_name)
		if not target_styles.hasByName(page_style_name):
			new_style = target_doc.createInstance("com.sun.star.style.PageStyle")
			target_styles.insertByName(page_style_name, new_style)
		target_style = target_styles.getByName(page_style_name)

		# よく使うプロパティを安全に転写
		for prop in [
			"IsLandscape",
			"HeaderIsOn",
			"FooterIsOn",
			"PrintHeaders",
			"PrintGrid",
			"LeftMargin",
			"RightMargin",
			"TopMargin",
			"BottomMargin",
			"ScaleToPages",
			"ScaleToPagesX",
			"ScaleToPagesY",
		]:
			try:
				setattr(target_style, prop, getattr(template_style, prop))
			except Exception:
				pass

		imported_sheet.PageStyle = page_style_name
	return imported_sheet

試しにスタイルを手動でコピーするコードを入れてみたのですがうまくいきません。

【LibreOffice/Basic】Calcでアクティブシートにページスタイル(ページ設定)を追加するマクロ #LibreOfficeBasic – Qiita https://qiita.com/brunojacopteteth/items/23f614afc6b1b929c297

Basic のマクロならばできるようなので、「標準」ではなくて、新しいスタイルシートを作成して追加すればよさそうです。

「書式」→「ページスタイル」では、ページスタイルを編集できないっぽいので、マクロ専用なのか、それとも何か別な方法があるのか?

このあたりは、もうちょっと調べる必要あり。

カテゴリー: 開発, LibreOffice | LibreOffice Calc で Python から帳票作成(シートのスタイル以外は) はコメントを受け付けていません

LibreOffice Calc を C# からアクセスする…が、できません

最終的には Python か VB あたりで動かしたいのですが、ちょっと寄り道。

Python コードを書くときに型チェックが出来なくて困っています…と言いますか、Python の場場合にはプログラミングをするときにプロパティ名とかメソッド名とかどうやって補完しているのでしょうか?

*.pyi でスタブを作る

LibreOffice の UNO API は戻り値が Any なのでコード補完が効きません。多分、戻り値が object なので、そういう仕様になっているのでしょうが、コードを書くときに面倒です。というか、どのメソッドを呼び出せるのか実行時にしかわかりません。
ひょっとしてメソッド名を全部覚えているのか? とも思わなくもないのですが、もっと敷居を下げておきたいです。自分のためにも。

おそらく、C++、Java で UNO API を使うためには大量のインターフェースを定義してあるはずです。

from typing import TYPE_CHECKING, Protocol, cast, Any

class XCell(Protocol):
    def getString(self) -> str: ...
    def setString(self, value: str) -> None: ...
    def getFormula(self) -> str: ...
    def setFormula(self, formula: str) -> None: ...
    def getValue(self) -> float: ...
    def setValue(self, value: float) -> None: ...

class XCellRange(Protocol):
    def getCellByPosition(self, column: int, row: int) -> XCell: ...
    def getCellRangeByPosition(self, startColumn: int, startRow: int, endColumn: int, endRow: int) -> 'XCellRange': ...
    def getCellRangesByName (self, aRangeName: str) -> tuple['XCellRange', ...]: ...
    def getRangeAddress(self) -> CellRangeAddress: ...

class XNameAccess(Protocol):
    def getByName(self, name: str) -> XComponent: ...
    def hasByName(self, name: str) -> bool: ...
    def getElementNames(self) -> tuple[str, ...]: ...
    def getEmbeddedObject(self) -> XChartDocument: ...

class XChartDocument(Protocol):
    def setDiagram(self, diagram: XDiagram) -> None: ...
    def createInstance(self, serviceName: str) -> Any: ...

class XDiagram (Protocol):
    pass

class XTableCharts(Protocol):
    def addNewByName(self, name: str, aRect: Any, aRanges: tuple[str, ...], bColumnHeaders: bool, bRowHeaders: bool) -> None: ...
    def hasByName(self, name: str) -> bool: ...
    def removeByName(self, name: str) -> None: ...
    def getByName(self, name: str) -> XNameAccess: ...
...

こんな感じで calc.pyi というスタブを作っておいて、本体のコードで参照させます。

import uno
import re
from typing import Any, TYPE_CHECKING, cast

# typings/calc.pyi は型チェック専用に読み込む(実行時はフォールバック)
if TYPE_CHECKING:
	from calc import XDesktop, XComponent, XSpreadsheet, XCellRange, Rectangle  # type: ignore
else:
	XDesktop = XComponent = XSpreadsheet = XCellRange = Rectangle = Any

"""
棒グラフを作成するスクリプト
"""


def connect_to_libreoffice() -> tuple[XDesktop, XComponent, XSpreadsheet]:
	"""LibreOffice に接続してアクティブな Calc シートを取得する"""
	local_ctx = uno.getComponentContext()
	resolver = local_ctx.ServiceManager.createInstanceWithContext(
		"com.sun.star.bridge.UnoUrlResolver", local_ctx)
	ctx = resolver.resolve(
		"uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
	smgr = ctx.ServiceManager
	desktop : XDesktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

	model = desktop.getCurrentComponent()
	if model is None:
		raise RuntimeError("LibreOffice Calc ドキュメントが開かれていません")

	sheet = model.getCurrentController().getActiveSheet()
	return desktop, model, sheet

if TYPE_CHECKING のところで、コーディング時と実行時の切り替えをしなといけないのが面倒なのですが、これで Any となっている型を無理矢理 XCellRange などの型に変えることができます。
本来は、cast 関数を使って正式にキャストをすればいいのですが、どうせ Any からのキャストでしかないので、変数の型だけ決めて、そこに無理やり押しこみます。

desktop : XDesktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

実行時の型チェックではなくて、あくまでコーディング時のコード補完のためなのでこれで十分です。

pyi のスタブはどう作るのか?

UNO API のクラスは膨大にあって、ちまちま手作業で変換するのは大変です。いや、そもそも使いたいのは Calc 関係だけなので、全部を変換するのは大袈裟だし、手間がかかります。
基本は AI エージェントにコードを作って貰うつもりなので、すべての型が必要なわけではありません。ちょっと手直しをするとか、数行のマクロコードを書くときにコード補完ができればよいのです。

C++ や Java のライブラリを利用すればいいのでは?

UNO API のインターフェースは、Python だけではありません。C++ や Java からも使うことができます。ドキュメントは非常に少ないですが、C# から、つまり .NET からのアクセスも可能となっています。

Copilot に聞くと unoidl.dll というのがあるそうです(実際にはありません。後述するように、cli_oootypes.dll 等のファイル名に変わっています)。これをうまく使えば自動生成ができるかもしれませんね。

SDK をインストールする

ここでは、他のドキュメントを見るために SDK をインストールしていますが、実行時には必要ありません。C# の場合は、適当な *.dll を実行ファイルと一緒のフォルダーに入れることになるので、客先の PC に SDK を入れなくてもよいです。

SDK and Sourcecode
https://www.libreoffice.org/download/download-libreoffice

これをインストールすると LibreOffice のマクロドキュメントなどがインストールされます。
フォルダー名は C:\Program Files\LibreOffice\sdk\ です。

IDL リファレンスは本家 https://api.libreoffice.org/ にアクセスすると重たくて仕方がないので、ローカル PC に入れて検索すると良いです。懐かしの doxygen による自動生成です。

C# でプロジェクト参照する

適当なプロジェクトを作って、C:\Program Files\LibreOffice\sdk\cli\ にある *.dll をプロジェクトから参照します。

主に cli_oootypes に各種のクラス定義が入っています。

これをうまくピックアップしながら *.pyi のスタブファイルを作っていけばよいわけです。

これは今後の課題。

おまけとして、C# でアクセスできるのか?

結論から言うと現状の 25.8.3 では動作しません。これは .NET 関係で作られているラッパーがおかしいのか、プログラムがおかしいのか不明ですが、UNO API サーバーに繋がりません。

using System;
using System.Diagnostics;
using uno;
using uno.util;
using unoidl.com.sun.star.bridge;
using unoidl.com.sun.star.container;
using unoidl.com.sun.star.frame;
using unoidl.com.sun.star.lang;
using unoidl.com.sun.star.sheet;
using unoidl.com.sun.star.table;
using unoidl.com.sun.star.text;
using unoidl.com.sun.star.uno;
using unoidl.com.sun.star.beans;

class Program
{
    static void Main()
    {
        try
        {
            Console.WriteLine("Starting...");
            string[] programPathCandidates =
            {
                @"C:\\Program Files\\LibreOffice\\program",
                @"C:\\Program Files (x86)\\LibreOffice\\program"
            };

            string? libreOfficeProgramPath = null;
            foreach (var p in programPathCandidates)
            {
                if (System.IO.Directory.Exists(p))
                {
                    libreOfficeProgramPath = p;
                    break;
                }
            }

            if (libreOfficeProgramPath == null)
            {
                Console.WriteLine("LibreOffice program folder not found. Please adjust path.");
                foreach (var p in programPathCandidates)
                {
                    Console.WriteLine($"Tried: {p}");
                }
                return;
            }

            Console.WriteLine($"UNO_PATH target: {libreOfficeProgramPath}");

            // Ensure UNO can locate LibreOffice binaries.
            Environment.SetEnvironmentVariable("UNO_PATH", libreOfficeProgramPath, EnvironmentVariableTarget.Process);
            var bootstrapIni = System.IO.Path.Combine(libreOfficeProgramPath, "fundamental.ini");
            Environment.SetEnvironmentVariable("URE_BOOTSTRAP", $"vnd.sun.star.pathname:{bootstrapIni}", EnvironmentVariableTarget.Process);

            string ureBin = System.IO.Path.Combine(libreOfficeProgramPath, "..\\URE\\bin");
            string newPathSegment = libreOfficeProgramPath + ";" + ureBin;

            string? currentPath = Environment.GetEnvironmentVariable("PATH") ?? string.Empty;
            if (!currentPath.Contains(libreOfficeProgramPath, StringComparison.OrdinalIgnoreCase) || !currentPath.Contains(ureBin, StringComparison.OrdinalIgnoreCase))
            {
                Environment.SetEnvironmentVariable("PATH", newPathSegment + ";" + currentPath, EnvironmentVariableTarget.Process);
            }

            Environment.SetEnvironmentVariable("UNO_SERVICES", System.IO.Path.Combine(libreOfficeProgramPath, "uno_services.rdb"), EnvironmentVariableTarget.Process);

            // Some environments need the working directory inside the LibreOffice program folder.
            Environment.CurrentDirectory = libreOfficeProgramPath;

            // Start (or reuse) LibreOffice headless with a socket connector.
            var sofficePath = System.IO.Path.Combine(libreOfficeProgramPath, "soffice.exe");
            var acceptArg = "--accept=socket,host=localhost,port=2002;urp;StarOffice.ServiceManager";

            if (!System.IO.File.Exists(sofficePath))
            {
                Console.WriteLine($"soffice.exe not found at {sofficePath}");
                return;
            }

            bool sofficeRunning = Process.GetProcessesByName("soffice.bin").Length > 0;
            if (!sofficeRunning)
            {
                Console.WriteLine("Starting soffice headless...");
                var psi = new ProcessStartInfo
                {
                    FileName = sofficePath,
                    Arguments = $"--headless --nologo --norestore --nodefault {acceptArg}",
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    WorkingDirectory = libreOfficeProgramPath
                };
                Process.Start(psi);
                // Give soffice time to open the socket.
                System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5));
            }

            Console.WriteLine("Creating initial UNO context...");
            XComponentContext localContext = Bootstrap.defaultBootstrap_InitialComponentContext();
            XMultiComponentFactory localSmgr = localContext.getServiceManager();
            XUnoUrlResolver resolver = (XUnoUrlResolver)localSmgr.createInstanceWithContext("com.sun.star.bridge.UnoUrlResolver", localContext);

            Console.WriteLine("Resolving remote context via socket...");
            XComponentContext remoteContext = (XComponentContext)resolver.resolve("uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext");

            XMultiComponentFactory serviceManager = remoteContext.getServiceManager();
            XDesktop? desktop = serviceManager.createInstanceWithContext("com.sun.star.frame.Desktop", remoteContext) as XDesktop;
            if (desktop == null)
            {
                Console.WriteLine("Failed to acquire LibreOffice desktop.");
                return;
            }

            Console.WriteLine("Desktop acquired");

            XComponentLoader? loader = desktop as XComponentLoader;
            if (loader == null)
            {
                Console.WriteLine("Failed to acquire component loader.");
                return;
            }

            Console.WriteLine("Component loader OK");

            // Reuse the active Calc document, or create a new one if none is open.
            Console.WriteLine("Checking current component...");
            XSpreadsheetDocument? calcDocument = desktop.getCurrentComponent() as XSpreadsheetDocument;
            if (calcDocument == null)
            {
                Console.WriteLine("No active Calc. Creating new...");
                var args = Array.Empty<PropertyValue>();
                var component = loader.loadComponentFromURL("private:factory/scalc", "_blank", 0, args);
                calcDocument = component as XSpreadsheetDocument;
            }

            if (calcDocument == null)
            {
                Console.WriteLine("No Calc document available.");
                return;
            }

            Console.WriteLine("Calc document ready");

            // Access the first sheet and write text to cell A1.
            XSpreadsheets sheets = calcDocument.getSheets();
            XIndexAccess sheetAccess = (XIndexAccess)sheets;
            Any sheetAny = (Any)sheetAccess.getByIndex(0);
            XSpreadsheet? sheet = sheetAny.Value as XSpreadsheet;
            if (sheet == null)
            {
                Console.WriteLine("Failed to access the first sheet.");
                return;
            }

            Console.WriteLine("Sheet acquired");

            XCell cell = sheet.getCellByPosition(0, 0);
            cell.setFormula("Hello by C#");

            Console.WriteLine("Value written to A1.");
        }
        catch (System.Exception ex)
        {
            Console.WriteLine($"Failed to write to Calc: {ex.Message}");
            Console.WriteLine(ex);
        }
    }
}

結果

PS H:\LibreOffice\net-ref> dotnet run
Starting...
UNO_PATH target: C:\\Program Files\\LibreOffice\\program
Creating initial UNO context...
Failed to write to Calc: External component has thrown an exception.
System.Runtime.InteropServices.SEHException (0x80004005): External component has thrown an exception.
   at cppu.defaultBootstrap_InitialComponentContext(Reference<com::sun::star::uno::XComponentContext>*)
   at uno.util.Bootstrap.defaultBootstrap_InitialComponentContext(String ini_file, IDictionaryEnumerator bootstrap_parameters)
   at uno.util.Bootstrap.defaultBootstrap_InitialComponentContext()
   at Program.Main() in H:\LibreOffice\net-ref\Program.cs:line 97
PS H:\LibreOffice\net-ref> 

実行すると、XComponentContext localContext = Bootstrap.defaultBootstrap_InitialComponentContext(); の位置で例外が発生していて致命的です。いわゆる LibreOffice がホストしているサーバーに繋がらず最初のインスタンスが生成できていません。Python からは繋げることができるので、

local_ctx = uno.getComponentContext()

まあ、 .NET 関係がおかしいのでしょう。

ひとまず、.NET 関係で動かすのは諦めて、素直に Python での実装を考えます。つまりは *.pyi のスタブをもう少し充実させます。

カテゴリー: 開発, LibreOffice | LibreOffice Calc を C# からアクセスする…が、できません はコメントを受け付けていません

LibreOffice Calc を外部から Python で操作する

Microsoft Office 365 が値上げをされたので、無料の LibreOffice に移行しましょう…ってのが流行ったがどうかわかりませんが、まあ、いままで Excel で作っていた資料を LibreOffice Calc でうまく作れるかどうかが問題ですよね。これは Google Workspace の Calc でも同じ問題を抱えるわけですが。

全体の操作感は別として(個人的には Excel が慣れているので、他の表計算ソフトを使うのはちょっと避けたいです)、いままで作っていた Excel VBA のマクロだとか、あらたな Calc でうまく移植できるのか? が問題になってきます。実は Excel VBA の開発環境もほぼ化石のままだし、LibreOffice Calc の Basic マクロの開発環境は化石みたいなものです。現在の vscode + copilot のような AI を使ったコード環境に比べると、ちょっとこのまま Excel VBA で開発のするのは避けたい…というのがプログラマの心情なのですが、俺にはこれしかないんだ、これが一番なのだという 97式自動小銃を持ち出す松本零士キャラならば、まあ、確かにそうですよね、とも言えなくもありません。

LibreOffice Calc は Excel VBA の互換環境なのか?

一見作れそうな感じなのですが、全然だめでした。全く互換はありません。

おなじみの Excel VBA のコードですが、これを LibreOffice Calc の Basic にコピペしても動きません。

Sub test()
    Dim sh As Worksheet
    Set sh = ActiveSheet
    sh.Cells(1, 1).Value = "増田智明"
End Sub

エラーになるので ChatGPT でコンバートすると以下のようになります。

Sub test
    Dim oDoc As Object
    Dim oSheet As Object
    Dim oCell As Object

    ' 現在のドキュメントを取得
    oDoc = ThisComponent

    ' アクティブシートを取得
    oSheet = oDoc.CurrentController.ActiveSheet

    ' A1セルを取得 (行・列は0始まり)
    oCell = oSheet.getCellByPosition(0, 0)

    ' 値を設定
    oCell.String = "増田智明"
End Sub

いや…これだと無理ですね。複雑な Excel VBA の移植は到底無理だし、簡単な関数も結構手間なような感じです。

あらたに Calc 用の Basic を覚えるのは馬鹿馬鹿しいので、新規一転して Python を使ってみましょう。

LibreOffice に Python エディタはついていない!!!

理由はよくわからないのですが、LibreOffice には Basic マクロのエディタは付いているのですが、Python マクロのエディタは付属していません。

「ツール」→「マクロ」→「マクロの編集」を開くと Basic マクロのエディタが開きます。

このエディタは文法は Basic だけど、Excel VBA とも違うし、さらに言うと Excel VBA エディタと同じぐらい化石なのでなんとも使い勝手が悪いです。「As」の後の補完候補もでないので、Excel VBA よりも使い勝手は悪いです…どうやってマクロプログラミングをするのでしょうか?

で、Basic のほうはおまけみたいなものらしく、結局のところ Basic じゃなくて Python で組む方が良さそうとのこと。

Python マクロを使う

実は LibreOffice Calc のPython マクロを使うのには2種類のやり方があります。

  • 内部マクロにして XSCRIPTCONTEXT モジュールを使って組む
  • 外部マクロにして UNO API 経由で組む

Excel VBA のように Excel ファイルに組み込まれた形なのは前者の XSCRIPTCONTEXT モジュールのほうになるのですが、どうも使い勝手が悪いので、ここでは UNO API 経由のほうを紹介します。

UNO API 経由のほうは、LibreOffice Calc がサーバーになって socket 経由で Python から Calc の内部を操作することができます。これは、Excel オブジェクトを C# で操作するような感じで使えるので、意外と使い勝手がよいです。特に外部から自動実行や AI 経由で何かさせたいときはこっちのほうが良さそうです。

UNO API 経由

Calc を起動しておく。

PowerShell で、以下のコマンドを実行しておく。ポート 2002 番で LibreOffice のサーバーを実行させます。

& "C:\Program Files\LibreOffice\program\soffice" --accept="socket,host=localhost,port=2002;urp;" --norestore --nologo

vscode で以下のコードを記述(これは Copilot に作って貰ってます)
コード名は calc001.py です。

# UNO接続の準備
local_ctx = uno.getComponentContext()
resolver = local_ctx.ServiceManager.createInstanceWithContext(
    "com.sun.star.bridge.UnoUrlResolver", local_ctx)

# LibreOfficeに接続
ctx = resolver.resolve(
    "uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")

smgr = ctx.ServiceManager
desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)

# 現在開いているCalcドキュメントを取得
doc = desktop.getCurrentComponent()
sheet = doc.Sheets[0]  # 1枚目のシート

# A1セルに文字列を入力
cell = sheet.getCellByPosition(0, 0)  # (列=0, 行=0) → A1
cell.setString("tomoaki masuda")

PowerShell で、以下を実行

& "C:\Program Files\LibreOffice\program\python" .\calc001.py

A1 セルに文字列「tomoaki masuda」が入る。

この方法が良いのは、

  • Calc は開いたままでよい
  • Python コードは vscode で書ける
  • コードが即反映される(Python.exe を動かすので当たり前)

ただし、コードの前提条件として、

  • import uno が必要
  • LibreOffice に接続するまでが少々手間
  • おそらくデバッグ実行はできないのでは?
  • Python は、LibreOffice 内蔵の “C:\Program Files\LibreOffice\program\python” を動かす必要あり。
  • シート付属ではないので、環境依存になりがち

という形で結構制限が多いです。Excel VBA のときのように、Excel VBA エディタを開きながらぽちぽちとコーディングをしたり、シートにダイアログやボタンを貼り付けたりすることはできませんが(これは別途後で調べます)

  • テキストを読み込んで、Calc にシートを作成して表形式で整える
  • 既存のシートを読み込んで、Python で計算した後に、シートに計算結果を入れる
  • シートのデータをもとに、グラフを作成して貼り付ける

のような定型作業には便利そうです。

import uno のコード補完を有効にする

python が特殊なものを参照しているので、import uno のコード補完が有効になりません。これはかなり不便なので、コード補完が有効になるように settings.json を追加します。

{
    "python.analysis.autoImportCompletions": true,
    "python.analysis.extraPaths": [
        "C:/Program Files/LibreOffice/program",
        "C:/Program Files/LibreOffice/program/python-core-3.11.13/lib"
    ]
}

Lib を参照している Python のバージョン部分「python-core-3.11.13」を開発環境で揃えないといけませんが、これで import uno のコード補完が有効になります。
ただし、もともと Any 型になっているものが多いのか、あまり役に立たないのですが、メソッド名とかクラス名からライブラリのコードにジャンプできるのはいいかもしれません。

Markdown の表を読み込んでシートに書き出す

もう少し複雑な例として Markdown 形式で書かれた表を読み取ってシートに貼り付ける Python マクロを書いてみましょう。これも最初のコメント部分だけ書いておいて、コードは Copilot に書いて貰っています。

"""
markdown のテーブルを読み取って、LibreOffice Calc のシートに書き出すスクリプト

使用方法:
1. LibreOffice Calc を開いた状態で実行
2. Markdownテーブルを含むファイルを指定

実行コマンド:
& "C:\Program Files\LibreOffice\program\python" .\calc002.py
"""

import uno
import re
from pathlib import Path


def parse_markdown_table(markdown_text: str) -> list[list[str]]:
    """
    Markdownテーブルをパースして2次元リストに変換する
    
    Args:
        markdown_text: Markdownテーブルを含むテキスト
    
    Returns:
        2次元リスト(各行のセルデータ)
    """
    lines = markdown_text.strip().split('\n')
    table_data = []
    
    for line in lines:
        # テーブル行かどうかを確認(|で始まる行)
        if not line.strip().startswith('|'):
            continue
        
        # 区切り行(|---|---|など)をスキップ
        if re.match(r'^\|[\s\-:]+\|$', line.strip().replace('|', '|').replace('-', '-')):
            if '---' in line or ':-' in line or '-:' in line:
                continue
        
        # セルを分割
        cells = line.split('|')
        # 最初と最後の空要素を除去
        cells = [cell.strip() for cell in cells[1:-1]]
        
        if cells:  # 空でない行のみ追加
            table_data.append(cells)
    
    return table_data


def connect_to_libreoffice():
    """
    LibreOffice に接続してデスクトップオブジェクトを取得する
    
    Returns:
        (desktop, model, sheet) のタプル
    """
    local_ctx = uno.getComponentContext()
    resolver = local_ctx.ServiceManager.createInstanceWithContext(
        "com.sun.star.bridge.UnoUrlResolver", local_ctx)
    # LibreOfficeに接続
    ctx = resolver.resolve(
        "uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
    smgr = ctx.ServiceManager
    desktop = smgr.createInstanceWithContext("com.sun.star.frame.Desktop", ctx)
    
    # 現在のドキュメントを取得
    model = desktop.getCurrentComponent()
    
    if model is None:
        raise Exception("LibreOffice Calc ドキュメントが開かれていません")
    
    # アクティブなシートを取得
    sheet = model.getCurrentController().getActiveSheet()
    
    return desktop, model, sheet


def write_to_calc(sheet, table_data: list[list[str]], start_row: int = 0, start_col: int = 0):
    """
    LibreOffice Calc のシートにデータを書き込む
    
    Args:
        sheet: Calcのシートオブジェクト
        table_data: 書き込むデータ(2次元リスト)
        start_row: 開始行(0始まり)
        start_col: 開始列(0始まり)
    """
    for row_idx, row_data in enumerate(table_data):
        for col_idx, cell_value in enumerate(row_data):
            cell = sheet.getCellByPosition(start_col + col_idx, start_row + row_idx)
            cell.setString(cell_value)


def main():
    """メイン処理"""
    # サンプルのMarkdownテーブル
    sample_markdown = """
| 名前 | 年齢 | 職業 |
|------|------|------|
| 田中 | 30 | エンジニア |
| 佐藤 | 25 | デザイナー |
| 鈴木 | 35 | マネージャー |
"""
    
    try:
        # Markdownテーブルをパース
        table_data = parse_markdown_table(sample_markdown)
        print(f"パースしたデータ: {table_data}")
        
        # LibreOffice に接続
        desktop, model, sheet = connect_to_libreoffice()
        print("LibreOffice Calc に接続しました")
        
        # データを書き込み
        write_to_calc(sheet, table_data)
        print("データの書き込みが完了しました")
        
    except Exception as e:
        print(f"エラーが発生しました: {e}")


if __name__ == "__main__":
    main()

コードを見ると、Markdown 形式の文字列から読み込んでいるので、なんちゃってツールではありますが、これで動作は確認できます。実は、そのままでは動かなかったので、connect_to_libreoffice 関数や、以下のアクティブシートのところだけ手作業で書き変えています。

    # アクティブなシートを取得
sheet = model.getCurrentController().getActiveSheet()

アクティブワークブックやアクティブシートは、現在開いているワークブックやシートに書き込むので結構頻繁に使います。

コマンドラインから実行

すると Calc のアクティブシートにデータ書き込まれます。

2列目に「—」が入ったままなのはご愛敬なのですが、CSV 形式からの貼り付けとか、別フォーマットからの貼り付けとかには使えそうです。表形式の場合は、ここから罫線を付けたり、タイトル行に色を付けたりするので、そのあたりも Python マクロにできれば一応便利になるかなと。

で、Excel & Excel VBA から LibreOffice & Python に移行できそうか?

結論から言うと、Excel VBA から Python への移行は敷居が高いです。Excel VBA には ActiveWorkbook とか Me とか、Visual Basic 風な便利なグローバルオブジェクトが多いんですよね。import uno がどこまでグローバルオブジェクトが作られているか分からないのですが、この部分がないと結構コーディングが手間です。

逆に言えば、AI エージェント使ったコーディングができれば Python コードでも特に問題がなさそうです。おそらく、Excel VBA ベースのグローバル変数を追加して、いろいろな前提条件んどを端折れるようにすれば業務的に使えるようになるとは思います。

何か内製できそうなツールを作ってみるといいんじゃないでしょうか?

  • 複数のアンケートシートからひとつの集計結果にまとめる
  • データベースの情報を収集して、Calc に週間レポートや月間レポートしてグラフ付きで作成する
  • シートに関数をつけて計算しておいて、更に集計してまとめるようなツール

このあたりが、社内でツールとして作れればよいかな、と。M365 の Python マクロってどのくらい使われているんでしょうね?

追記 2025/12/16

LibreOffice Calc で Python から帳票作成(シートのスタイル以外は) https://www.moonmile.net/blog/archives/11840
LibreOffice Calc で Python から帳票作成(テンプレートファイルの利用) https://www.moonmile.net/blog/archives/11849

追記 2026/01/05

LibreOffice Calc で使う Excel Like UNO を仮リリース https://www.moonmile.net/blog/archives/11916

カテゴリー: 開発, LibreOffice | LibreOffice Calc を外部から Python で操作する はコメントを受け付けていません

プログラム言語の型が何故前置が多かったのか?

結論から言うと、今となっては型の記述は後置のほうがやりやすいです。これは、型推論の事情から後置表現じゃないとうまくいきません。

例えば、C/C++ の場合、型を前に書きます。

int x, y;
int sum ;

逆に Pascal を始めとして、次のように後置になるものが最近の傾向です。

var x, y: Integer;
var sum: Integer;

一見すると、どちらでも変わらないような気がするのですが、ここに C++ のイテレータを書くと

varctor<int> vec = {1,2,3};
std::vector<int>::iterator it = vec.begin();

いや、これはもう無理です。まあ、実際こう書いていたわけですが、ちょっとインテリセンスとか何かの IDE の助けがないと難しいです。イテレータぐらいだから大丈夫だけど、C# のややこしい LINQ のポインタだとか諸々を加えると無理です。なので、型推論させて、C++ のほうは auto とか、C# の場合は var で受けるのが常です。

varctor<int> vec = {1,2,3};
auto it = vec.begin();

と型推論をさせます。ハッキリ言って、C/C++ でも後置型に変えてしまって、

auto x = 10 : int ;
auto y = 20 : int ;

だろうとは思います。実際のところ auto が使えるので、下記のようにキャストを使って無理矢理推論させることが可能です。あまり意味がありませんが。

auto x = (int)10 ;
auto y = (int)20 ;

C言語以前は、前置型であったのか?

C 言語以前が、前置型だったのか、とは思っていたのですが、これが違います。Ada が後置型で、なんと COBOL が後置型です。

– Fortran 1957 年  前置型
– COBOL 1959 年    後置型
– Pascal 1970 年代  後置型
– C 言語 1972 年   前置型
– Ada 1980 年代    後置型
– Java 1995 年     前置型

ここで、調べたときに「ああ、これ、前置型とか後置型とかはプログラム言語を作った人の趣味だわ…」と気づいたわけで、何か意図があって指定したわけではなさそうです。

ただ、Pascal が教育言語として出て来たときには、「C 言語のように前置きで型指定をするのではなく、教育的に理解しやすいように Pascal は型の指定が後置にしてある」という話があったので、何等かの理由があって、C 言語以前は前置きが多かったのではないか?と思っていたわけです。

これ、実際にコンパイラを作ろうとすると型の指定が前のほうにあったほうが楽で(これも人に依るとは思うのですが)、後にあるときはちょっと面倒なんですよね。あと、変数の前に var とか let とかあるのが無意味では?と思ったものです。まあ、Basic なんて Dim で定義したりするので、型はないのですが「変数であること」宣言みたいなものが var/let/Dim には存在します。

構文解析のときに前置型と後置型でメモリ使用量などに差異はでるのか?

ここからが本題で、例えばコンパイラを作ろうとしたときに何等かのメモリの制約などに違いがでるのか?という視点で見ていきます。今となっては、メモリが潤沢にあるのでどちらでも構わないのですが、歴史上「32kB 程度で動くことを想定」したときに差異がでていたのかもしれません。

ただし、これも結論を先に言えば、COBOL が後置型なので、仮説としても弱いです。

agnet.md を作成する

この手の実験をするのに、AI エージェントはとても役に立ちます。かつては、上記のことを証明しようと思ったら疑似コンパイラをちまちまとプログラミングして比較しないといけないところなのですが、バイブコーディングでかなりイイ線までいってくれます。

疑似的にコンパイラを作ればいいので、字句解析と構文解析だけ作ります。想定としては変数の型指定のところだけなので、変数定義と関数定義だけを例にとっています。もう少し話を広げれば構造体とかも含めてもいいかもしれません。あと、C 言語風のポインタ指定とかも入れるとよいでしょう。

プログラム言語は、C言語、Pascal、Fortran、COBOL、Ada、Java、Kotlin、Lisp を対象にしています。最初は、C言語、Pascal だけ作って後から少しずつ増やしていきました。

# コンパイル時の使用メモリ量をチェックする

プログラム言語には、型指定を前置と後置するものがある。

例えば、C言語やJavaは前置型指定であり、変数宣言時に型を指定する。

```c
int x = 10;
```

一方、Pascal や Kotlin などは後置型指定となる。

```pascal
var x: Integer;
x := 10;
```

プログラム言語の歴史上、前置型指定が主流であったが、後置型指定も徐々に普及してきている。
前置型指定は、コンパイルするときに型の指定が前置のほうがメモリ使用量が少なかったのではないか?と仮説を立てる。
これを検証する。

C 言語以前には、Ada が後置型であるので、Pascal 以前にも後置型があってもおかしくはない

```Ada
X : Integer := 10;
```

## 登場時期

- Fortran 1957 年  前置型
- COBOL 1959 年    後置型
- Pascal 1970 年代  後置型
- C 言語 1972 年   前置型
- Ada 1980 年代    後置型
- Java 1995 年     前置型

## 仮説

a. 前置型指定のほうがコンパイル時のメモリ使用量が少ない。このため、前置型が主流であり、メモリが増えるときに後置型が発生した
b. 前置型指定は慣習であり、メモリ使用量には影響しない。C 言語前後で前置型が作られたのは偶然である

a/b を検証する。

## 検証方法

1. 前置型指定言語と後置型指定言語で、同じアルゴリズムを実装する
2. コンパイル時のメモリ使用量を測定する

比較対象は、

- 構文木のメモリ使用量
- シンボルテーブルのメモリ使用量

利用メモリは、32KB 以下とする。

## 実装

- 字句解析の実装
- 構文解析の実装
- シンボルテーブルのメモリ容量を比較
- 構文解析時の構文木のメモリ容量を比較

## 文法

以下の文法を解析できるコードを作成する。

### C 言語(前置型指定)

```C
type_specifier: "int" | "float" | "char" ;
declaration: type_specifier identifier ";" ;
```

```C
int x, y;
int sum ;

x = 10 ;
y = 20 ;
sum = x + y ;

int func(int a, int b) {
    return a + b ;
}
```

### Pascal(後置型指定)

```pascal
type_specifier: "Integer" | "Real" | "Char" ;
declaration: "var" identifier ":" type_specifier ";" ;
```

```pascal
var x, y: Integer;
var sum: Integer;
x := 10 ;
y := 20 ;
sum := x + y ;

function func(a: Integer; b: Integer): Integer;
begin
    return a + b ;
end;
```

### Fortran(前置型指定)

```fortran
type_specifier: "INTEGER" | "REAL" | "CHARACTER" ;
declaration: type_specifier identifier_list ";" ;
```

```fortran
INTEGER x, y
INTEGER sum
x = 10
y = 20
sum = x + y

INTEGER FUNCTION func(a, b)
    INTEGER a, b
    func = a + b
END FUNCTION func
```
### COBOL(後置型指定)

```cobol
type_specifier: "PIC 9" | "PIC X" ;
declaration: "01" identifier "PIC" type_specifier "." ;
```

```cobol
IDENTIFICATION DIVISION.
PROGRAM-ID. SAMPLE.

DATA DIVISION.
WORKING-STORAGE SECTION.
01 X        PIC 9(4) VALUE 0.
01 Y        PIC 9(4) VALUE 0.
01 SUM      PIC 9(4) VALUE 0.
01 A        PIC 9(4) VALUE 0.
01 B        PIC 9(4) VALUE 0.
01 RESULT   PIC 9(4) VALUE 0.

PROCEDURE DIVISION.
MAIN-PARA.
    MOVE 10 TO X.
    MOVE 20 TO Y.
    ADD X TO Y GIVING SUM.

    MOVE 5 TO A.
    MOVE 7 TO B.
    PERFORM FUNC-PARA.

    DISPLAY "SUM = " SUM.
    DISPLAY "FUNC(A,B) = " RESULT.

    STOP RUN.

FUNC-PARA.
    ADD A TO B GIVING RESULT.
    EXIT.
```

### Ada(後置型指定)

```ada
type_specifier: "Integer" | "Float" | "Character" ;
declaration: identifier ":" type_specifier ";" ;
```

```ada
X : Integer;
Y : Integer;
SUM : Integer;
X := 10;
Y := 20;
SUM := X + Y;
function Func(A : Integer; B : Integer) return Integer is
begin
    return A + B;
end Func;
``` 

### Java (前置型指定)

```java
type_specifier: "int" | "float" | "char" ;
declaration: type_specifier identifier ";" ;
```

```java
int x, y;
int sum;
x = 10;
y = 20;
sum = x + y;
int func(int a, int b) {
    return a + b;
}
```

### Kotlin(後置型指定)

```kotlin
type_specifier: "Int" | "Float" | "Char" ;
declaration: "var" identifier ":" type_specifier ;
```

```kotlin
var x: Int
var y: Int
var sum: Int
x = 10
y = 20
sum = x + y
fun func(a: Int, b: Int): Int {
    return a + b
}
```

### Lisp(前置型指定)

```lisp
type_specifier: 'int | 'float | 'char ;
declaration: (declaim (type type_specifier identifier)) ;
```

```lisp
(declaim (type 'int x))
(declaim (type 'int y))
(declaim (type 'int sum))
(setf x 10)
(setf y 20)
(setf sum (+ x y))
(defun func (a b)
  (+ a b))
```

Plan モードで作成

vscode + copilot に Plan モードがやって来たので、先の agent.md(つまりは要件定義)から具体的な実装計画を立てて貰います。

実装時に足りなそうなところは、「Further Considerations」に追加されるので、この部分を補足していきます。

## Plan: 前置型/後置型コンパイラメモリ使用量比較ツールの実装

C言語(前置型指定)とPascal(後置型指定)の構文解析器を実装し、コンパイル時のシンボルテーブルとASTのメモリ使用量を比較して、仮説を検証します。現在のワークスペースには実装が存在しないため、すべてゼロから構築する必要があります。

### Steps
1. **トークン定義の実装** - `Token.cs` を作成し、両言語で必要なトークン種別(`int`, `var`, `:=`, `=` など)と `Token` クラスを定義する
2. **字句解析器の実装** - `Lexer.cs` を作成し、C言語用とPascal用の2つのLexerクラスを実装する(共通基底クラス + 派生クラス構成)
3. **ASTノード定義** - `Ast.cs` を作成し、`ProgramNode`, `DeclarationNode`, `FunctionNode`, `AssignmentNode`, `BinaryExpressionNode` などのノードクラスを定義する
4. **シンボルテーブルの実装** - `SymbolTable.cs` を作成し、変数名・型のマッピングとスコープ管理を実装する
5. **パーサーの実装** - `CParser.cs`(前置型)と `PascalParser.cs`(後置型)を作成し、それぞれの文法に基づく構文解析器を実装する
6. **メモリ測定機能の実装** - `MemoryMeasurement.cs` を作成し、GCとオブジェクトサイズを使ったメモリ使用量計測ロジックを実装し、`Program.cs` で比較実行と結果出力を行う

### Further Considerations
1. **メモリ測定手法の選択** - `GC.GetTotalMemory()` を使うか、各オブジェクトのサイズを手動計算するか? → 正確性を重視するなら手動計算を推奨

構文木のノード数、シンボル数のカウントを行い、メモリを計算する。
C# のメモリではなく、計測のために、構文木のノードとシンボルの容量を allocate するように実装する。

できるだけ小さなメモリ(32KB 以下)で動作するように注意する。


2. **Lexer の共通化レベル** - 完全に別クラスにするか、共通基底クラスを使うか? → 比較の公平性のため、できるだけ共通化を推奨

他の文法も試したいので、できるだけ共通化して。
完全に共通化できなくても、構文木の作成とシンボルテーブルの部分は共通化する。

3. **テストデータの規模** - agent.md のサンプルコードだけで十分か、より大きなテストケースを用意するか? → 32KB制約内で複数パターンのテストを推奨

テスト用の文法は、いくつかのパターンを用意して、32KB 以下を想定して実験する。


### メモリ計算の定義
- ASTノード: 基底部分 8byte + 子ノード参照 4byte × 子数
- シンボルエントリ: 識別子名(固定長 16byte)+ 型情報 2byte + スコープ情報 2byte = 20byte
- 測定対象: パース完了時点での構文木 + シンボルテーブルの合計サイズ、およびパース中の最大使用量

### allocate シミュレーション
- `MemoryAllocator` クラスを作成し、32KB のバッファを管理(ノードのカウント数、シンボル数から計算する)
- ノード/シンボル生成時に `Allocate(size)` を呼び出し、上限を越えたら警告を出す。実行は続行してよい。

比較するところはコンパイルするときのメモリ消費量なので、構文木のノードとシンボルテーブルの量を疑似的に計算します。コンパイラによっては、構文解析の部分が言語ごとに異なると思うので、それは別途計算しないといけないかもしれません。今回は、それはパスして、単にノードの数だけを概算しています。

プログラムを作る

vscode + copilot + Claude Opus 4.5 で作っています。最近 Codex が入ったらしいので、そっちを使ったほうが早いかもしれません。

生成コードは C# にしてあります。コード直すことはないのですが、私の場合は C# が直しやすいのはこれで。生成コードは Python にしても構いません。

ファイル名を見るとわかりますが、典型的なコンパイラのファイル名で返してくれます。プログラム言語ごとに Parser が違うだけで、そのほかは共通になっています。Token.cs が各言語でごちゃまぜになっていますが、研究ツールとしてはこれで十分です。

結果

PS H:\ai\check-mem-parser> dotnet run
======================================================================
前置型 vs 後置型 コンパイル時メモリ使用量比較
8言語比較: C, Fortran, Java, Lisp (前置型) vs Pascal, COBOL, Ada, Kotlin (後置型)
======================================================================

=== C言語(前置型) パース結果 ===
AST メモリ: 260 bytes
シンボルテーブル メモリ: 80 bytes
合計メモリ: 340 bytes (0.33 KB)
ノード数: 19
シンボル数: 6
パース中最大メモリ: 632 bytes
エラー数: 0
警告数: 0

=== Fortran(前置型) パース結果 ===
AST メモリ: 284 bytes
シンボルテーブル メモリ: 80 bytes
合計メモリ: 364 bytes (0.36 KB)
ノード数: 20
シンボル数: 6
パース中最大メモリ: 732 bytes
エラー数: 0
警告数: 0

=== Java(前置型) パース結果 ===
AST メモリ: 260 bytes
シンボルテーブル メモリ: 80 bytes
合計メモリ: 340 bytes (0.33 KB)
ノード数: 19
シンボル数: 6
パース中最大メモリ: 632 bytes
エラー数: 0
警告数: 0

=== Lisp(前置型) パース結果 ===
AST メモリ: 280 bytes
シンボルテーブル メモリ: 80 bytes
合計メモリ: 360 bytes (0.35 KB)
ノード数: 20
シンボル数: 6
パース中最大メモリ: 532 bytes
エラー数: 0
警告数: 0

=== Pascal(後置型) パース結果 ===
AST メモリ: 260 bytes
シンボルテーブル メモリ: 60 bytes
合計メモリ: 320 bytes (0.31 KB)
ノード数: 19
シンボル数: 6
パース中最大メモリ: 632 bytes
エラー数: 0
警告数: 0

=== COBOL(後置型) パース結果 ===
AST メモリ: 496 bytes
シンボルテーブル メモリ: 140 bytes
合計メモリ: 636 bytes (0.62 KB)
ノード数: 30
シンボル数: 8
パース中最大メモリ: 724 bytes
エラー数: 0
警告数: 0

=== Ada(後置型) パース結果 ===
AST メモリ: 280 bytes
シンボルテーブル メモリ: 60 bytes
合計メモリ: 340 bytes (0.33 KB)
ノード数: 20
シンボル数: 6
パース中最大メモリ: 648 bytes
エラー数: 0
警告数: 0

=== Kotlin(後置型) パース結果 ===
AST メモリ: 280 bytes
シンボルテーブル メモリ: 60 bytes
合計メモリ: 340 bytes (0.33 KB)
ノード数: 20
シンボル数: 6
パース中最大メモリ: 648 bytes
エラー数: 0
警告数: 0

========================================================================================================================
8言語比較結果
========================================================================================================================

項目                           C言語     Fortran        Java        Lisp      Pascal       COBOL         Ada      Kotlin
------------------------------------------------------------------------------------------------------------------------
AST (bytes)                  260         284         260         280         260         496         280         280
Symbol (bytes)                80          80          80          80          60         140          60          60
合計 (bytes)                   340         364         340         360         320         636         340         340
最大 (bytes)                   632         732         632         532         632         724         648         648
ノード数                          19          20          19          20          19          30          20          20
シンボル数                          6           6           6           6           6           8           6           6

======================================================================
仮説の検証(前置型 vs 後置型 の平均比較)
======================================================================

前置型(C, Fortran, Java, Lisp)平均パース中最大メモリ: 632.0 bytes
後置型(Pascal, COBOL, Ada, Kotlin)平均パース中最大メモリ: 663.0 bytes

結果: 後置型は前置型より平均 31.0 bytes 多くメモリを使用
→ 仮説 a を支持: 前置型の方がメモリ効率が良い可能性
======================================================================
仮説の検証(前置型 vs 後置型 の平均比較)
======================================================================

前置型(C, Fortran, Java, Lisp)平均パース中最大メモリ: 632.0 bytes
後置型(Pascal, COBOL, Ada, Kotlin)平均パース中最大メモリ: 663.0 bytes

======================================================================
仮説の検証(前置型 vs 後置型 の平均比較)
======================================================================

前置型(C, Fortran, Java, Lisp)平均パース中最大メモリ: 632.0 bytes
======================================================================
仮説の検証(前置型 vs 後置型 の平均比較)
======================================================================

======================================================================
仮説の検証(前置型 vs 後置型 の平均比較)
======================================================================
======================================================================
======================================================================
仮説の検証(前置型 vs 後置型 の平均比較)
======================================================================

前置型(C, Fortran, Java, Lisp)平均パース中最大メモリ: 632.0 bytes
後置型(Pascal, COBOL, Ada, Kotlin)平均パース中最大メモリ: 663.0 bytes

結果: 後置型は前置型より平均 31.0 bytes 多くメモリを使用
→ 仮説 a を支持: 前置型の方がメモリ効率が良い可能性

注: 後置型では識別子を一時的にバッファリングする必要がありますが、
    このサンプルコードの規模では顕著な差は見られません。
    より大規模なコード(多数の変数宣言)で差が出る可能性があります。
PS H:\ai\check-mem-parser>

結果は、前置型と後置型で平均を出して比較しているので、これはあまり意味がありません(AI エージェントが適当に提案したものなので)。個々のノード量を見るとわかりますが、さほど差がありません。COBOL だけ AST が多いのは、これだけ DISPLAY を呼び出して画面表示の関数を呼び出しているからですね。基本的にこれぐらいだと差が出ないはずだし、おそらく構文木のノードにしてもほとんど変わらない筈です。もうちょっと突飛な文法を付けると変化するとは思うのですが、今回は変数定義の前置/後置の違いを検証したかったので、実際にコンパイラを作ってもメモリ容量はさほど変わらない、という結果になります。

ここは発展として、

  • C言語のポインタ「*」の表現を各言語にいれたらどうなるのか?
  • 構造体やクラスの場合は違いがでるのか?
  • ひょっとしたら Lisp とか極端にノードが減ったりしないか?
  • Lua とかスクリプト言語はどうなるのか?

参考コード

https://github.com/moonmile/check-mem-parser

考察 2025/12/06

理解しやすいプログラム言語を考えるうえで、何を基準にするかというと、工学的にはなんらかの計測可能な測定基準が必要になると思うのです。もちろん、進化論的に偶発的に発生する問題(右利き、左利きとか)でもありますが、これを「変数の前置/後置」にして再考したのが上記の結果というわけです。

わたしの経験上、C言語、Fortran 時代が長いので後置型を長く使っているのと「COBOL が前置だった」という勘違いが大きいので、なんらかのメモリの制約があって前置ではなかったか、という仮説を持っていたわけですが…先に書いた通り Ada、COBOL という古い言語に関しても後置となっているので、それは否定できますね。

じゃあ、もう少し視野を広くして「人間にとって理解しやすいプログラム言語の文法とは何か?」を考えるときに、短期記憶の負荷が少ないプログラム言語が理解しやすいのではないか?という仮定に切り替えてみます。つまり、構文解析をするときに探索木が少なくて済むとか、一時的なシンボルのメモリが少なくてすむとか、という明確な測定基準を設定することができます。先の実験プログラムで言えば、ノード数とかシンボル数とかを比較します。そこで、比較したいのは一時的なメモリの最大値です。これは、人間で言えば短期記憶になります。

C 言語の書き方でよく言われるように、一時的な変数は近くに書くとか、スコープを制限して書くとかいう方法があります。関数を短めにするのもそれです。それを数値化するのが短期記憶容量であって一時的なシンボルや構文木ノードの数になります。

また、Rust や関数型プログラムにあるように、束縛変数(変更できない変数)も短期記憶の削減に寄与します。これは、変更されない安全性の確保にもつながりますが、同時に理解しやすいプログラム言語の測定やプログラムコードの書き方に応用できるかも、ってわけです。プログラムコードの複雑さについては密結合に関してや条件文のからみあいを計測する手法(サイクロマティック複雑度 https://emb-sw-eng.com/cyclomatic_complexity/) が既にあるのですが、もうちょっと違う視点でできるかなと。あと、プログラム言語の文法の設計あるいは言語拡張にも応用できるかもしれません。

カテゴリー: 開発 | プログラム言語の型が何故前置が多かったのか? はコメントを受け付けていません

国際ロボット展 2025 の感想文(主にロボットアーム系)

開催概要 | 2025国際ロボット展 https://irex.nikkan.co.jp/overview/ に行ってきたので、この感想文です。

みなさん、歩く方のロボットが目当てっぽいのですが、私の場合ロボットアームが目当てなのでちょっと視点がずれています…というか異なります。この手の産業展は写真不可が多いのですが、ロボット展は可なんですね。なので、写真付きです。
ちなみに、面倒だったのでセミナーのほうは行ってません。歩くロボットの展示はセミナーが多かったみたいなので、何らかの予約をしていくと良いです。

最初の探したのが川崎重工です。四足のコンセプトロボットではなくて、以前展示されていた ROS で動作している歩行ロボット(確かピックアップ利用目的)の開発がどうなったのかな、というところです。

どうやら、実演を見逃してしまったようで「Kaleido(カレイド)」という形で開発されています。中味のが ROS2 かどうかは分かりません。

「踊らない」ヒト型ロボで逆転狙う日本勢 川崎重工、最新版を公開 – 日本経済新聞 https://www.nikkei.com/article/DGXZQOUC02BVH0S5A201C2000000/

結構注目を集めていたのが、NACHi https://www.nachi-fujikoshi.co.jp/ の産業ロボットです。会場でも相当数 NACHi バッグを持っている人がいます。

これが凄いのは、複数のロボットアームが協調動作をしているのですが、相互にぶつからないように作業ができています。これ、自動車の塗装や溶接だと事前プログラムで衝突を回避するのですが、不二越ロボットはリアルタイム(MZS OS だったか)で相互干渉を避けることができます。これは木製の線路かたずけを行っているデモですが、他にもボールやビスの片づけとか4,5台のロボットアームが協調動作するところが見られます。

「皆さんご存じデジタルツイン」の展示です。仮想空間にロボットアームや機器を配置させて、競合を避ける手法のひとつです。従来だと、実際の機器で動作範囲を指定していたのですが、現在はこのような仮想空間を利用します。という形で「デジタルツイン」です。

実際にはロボットアーム自体の物理量(振り回しやトルクとか)があるので、その許容範囲を計算するのと、作業台の上の道具の位置が若干ことなるとか、そういう差異を吸収しないといけません。そのあたりは、具体的にロボットアームの動作プログラムをしている方がぽろぽろと X のタイムラインにもいるので探してください。企業秘密なのでなかなか見つかりませんが、趣味ロボットを作っている方を漁ると出会えたりします。つまりは、仕事でもロボットだし、趣味でもロボットなわけです。

ロボット展なので、いわゆる「ロボット」が多いのですが、こういうのもロボットです。商品のピックアップを画像認識とローラーで判別して送り出すことができます。

Amazon のように多種多様なピックアップの場合、棚移動ロボットとロボットのような人間によるピックアップの組み合わせがよいのですが、この展示のようにある程度かたちが揃っている場合はベルトコンベア方式でピックアップすると高速にできます。たぶん、ローラーで商品判別をするパターンの応用だと思うのですが、見どころは手間のローラー部分ではなくて、裏のほうで動いているボールがたくさんついてるローラーのほうです。可動部分が多くて連続動作はどうなのだろう? と思ったりもしますが、かなりスムーズに動けます。

ソフトウェアとして Unity が使われています、の例です。HMD 対応のソフトウェア開発もそうなのですが、画像関係では Unity が欠かせません。Unity 自体の利用はゲーム業界が多いとは思うのですが、こういうリアルタイム動作の UI によく使われています。

同じ画像関係として Unreal Engine があるのですが、この手のロボットや画像研究展では Unity が使われていることが多いです。多分 C# と C++ の違いかなと思います。Unreal Engine で C++ を使ってがりがりやって、他との通信部分を作るのは辛いでしょう。そのあたりは、C# で豊富なライブラリを使った方が有利、ということです。Unity 自体のブースもロボット展に出ています。

もう、見飽きてしまった位の7軸ロボットですが、ピックアップ&協調動作と言う形で広く使われています。

この丸みをおびたデザインは、実はドイツの Industry 4.0 が発祥になります。Industry 4.0 は工場の自動化と同時に、ロボットが人と協調するときの安全性も進めていました。

インダストリー4.0とは?日本の製造業への応用やドイツの狙いをわかりやすく解説! | NTTドコモビジネス 法人のお客さま https://www.ntt.com/business/services/xmanaged/lp/column/industry-4-0.html

IoT とか工場のセンサーとか、自動発注システムとかも Industry 4.0 の一部ですね。それ以前は無骨で指や腕を持って行かれそうなデザインだったわけですが、このような形で丸みを帯びたカーブになっています。これだと指を挟むことがありません。ちょうど、歯医者の支持アームのようなものです。

もちろん、完全無人化の場合は、人間への配慮が必要がないのでこのての形状じゃないものも多いです。自動車の溶接や塗装アームとは異なる分野になります。

ちなみに写真を取り忘れてしまったのですが、ロボットアームの作業を「共創」するパターンがあります。先の MZS OS の場合は一社だけの協調作業ですが、「共創」する場合は、多様な会社のロボットアームを協調動作させることが目的になります。いろいろな会社が、その会社特有のアームを持ち出して、協調的に作業をできるというスタイルです。今回でも、このスタイルを主張する会社が2社ほどありました。

他社のロボットアームが AI などを使った自動化に重きを置くのですが、人力の操作に重きを置いたのが「人機一体」です。いわゆる従来型の人が操作するロボットアームになりますが、重機や臨機応変な作業にはこれが有効になります。

おそらく、一番の売りは「フィードバックが来るようになった」です。HMD で遠隔操作ができると同時に、アームが何かに触ったときに、人間の操作アームのほうにフィードバックが入ります。これができないと、押したり引いたりの力加減を「視界」だけで行なわなければいけません。いわゆる触覚です。これは遠隔地の電板を開いてブレーカーを操作しているところです。これで高所や高圧線に近いところでも人間は安全に作業ができるということです。まあ、ロボットも結構な値段がするでしょうが、人間の命のほうが高いですからね。場合によってはロボットが犠牲になるのです。

ちなみに、人機一体の缶バッチが売ってます。この手の展覧会でノベルティグッズを売っているのは初めて見ました。買っておけばよかった。ちょっとスルーしてしまいました。

ワイヤー型のロボットアームがありました。このワイヤー型は7軸ロボットに押されてしまっていますが、実は非常に高速に動きます。

これ、ワイヤーの自重がロボットアーム型よりも軽いので、凄く素早く動くんですよね。慣性力が少ないのです。しかも、巻き取りのステッピングモーターだけで済ませられるというのが特徴です。おそらく、重さ制限はあるのですが、こんな形で液体ボトルのぴくアップができます。他にも、この展覧会では見なかったですが、食品業界で使われる三軸のアームがあります。これも凄い高速で動くのでお薦めです…何に薦めるかわかりませんが。

この手のロボット展ですが、以前は物体認識の画像研究のシンポジウムと組み合わせるのが必要だったのですが、いまだと普通に組み込まれています。画像認識(キーエンスの薬品識別とか)は別な分野でもあるのですが、ロボットアームについても必ず組み込まれるようになっています。特に、人との協調ロボットには必須の機能ですよね。

そんな訳で、金曜日、土曜日とまだ展示会があるので、都合をつけてビックサイトに行ってみるのもよいです。

カテゴリー: 開発 | 国際ロボット展 2025 の感想文(主にロボットアーム系) はコメントを受け付けていません

Siv3D でもバイブコーディング!!!

本記事は Siv3D – Qiita Advent Calendar 2025 – Qiita https://qiita.com/advent-calendar/2025/siv3d の 3日目の記事です。

ChatGPTが一般的に使われ始めて2年ほど経ち、今度は AI エージェントというのが今年の4月頃から流行りはじめました。ChatGPT を使って、プログラミングのコードを教えて教えて貰うのはいいのですが、いちいちコードをコピペしないとイケなかったり、そもそも大量のコードをを ChatGPT に貼り付けないと文脈をうまく読み取ってくれないという問題がありました。が、これを Visual Studio Code 上なので直接ファイルを弄ってくれるのが AI エージェントの役割です。ちょっと危険な気もする(まあ、実際のところ MCP あたりが暴走すると危険なんですが)けれども、うまく使いこなせればこその「道具」なので、その道具を使いこなしていきましょう、というのがこの記事の主旨です。

バイブコーディング(vibe coding)とは?

ChatGPT にちまちまと関数のコードを貼り付けていたのととは違って、生成 AI を使って一気にコードを作成してしまおうというのが「バイブコーディング」の主な特徴です。いわゆる、AI にコードを書かせてしまって、コードを書く楽しみが…とも言えるのですが、職業プログラマとしては、

  • テストツールなどのたいくつなツールは AI に書かせた方が早い
  • 使い捨てツール=治具などは、AI に書かせたほうが早い
  • 最初のひな形は、AI にやってもらってたたき台にすることができる

という形で、使い捨てであったり最初のアイデアを実現するツールにバイブコーディングを使うのが一番効果的です。その後で、実際に仕事に使う場合にはコードの検証やら正確性やらが必要になってくるので、AI コードそのままを使うことはできませんが、それでも私自身の場合は、

  • 複雑怪奇になってしまったコードの解析を AI に頼む
  • 長々と出てくるエラーコードの解析を AI に頼む
  • ちょっと古めのコードを AI に読んで貰って、修正点のアイデアを出して貰う

のようなことをやります。最近は AI 任せにするツールや手法が盛んではありますが、その一歩手前の「AIペアプロ」がお勧めです。まあ、慣れてきたら、設計書を書くなりして一晩ぐるぐる回すほうが効果的ではあるのですが、慣れないうちはちまちま設計用の Readme.md や Agents.md(ファイル名は実は何でも構いません)に書いておいて、AI にちょっと作って貰うというのを繰り返すのがよいでしょう。自分なりのコーディングスタイルを掴んでください。

C++ でバイブコーディングできるのか?

巷に公開されているものは React などの Node.js や TypeScript を使った Web 開発のものが多いでしょう。設計書ノウハウ(「仕様書駆動」や「sepc駆動」というやつです)も、そのあたりのものが多いです。Web 開発の場合、コードを修正した途端に UI が変更になるとか、npm を使うとかするパターンが多いので、果たして C++ のように間にコンパイルが挟む場合はどうなるのか? という不安はありますが、大丈夫です。C++ でも十分使えます。しかも、Siv3D のような独自なライブラリを使っている場合でも、最近の生成 AI はうまく扱ってくれます。おそらく、Siv3D のチュートリアルサイト https://siv3d.github.io/ja-jp/ があるのが大きいと思います。つまり、Web サイトにある情報をうまく学習しているのが、現在の AI エージェントのモデルになっているわけです。

また、AI エージェントによるプログラム言語のコンバートの能力も上がっています。ロジックに関しては、一般的なプログラム言語はかなり網羅されています。別件ではありますが、Python のコードを VB.NET のコードに変換することもできます。あるいは、Kotlin や Swift のコードに変換したりします。それぞれデータベースなどライブラリの使い方は異なるのですが、それもプログラム言語特有のもの、かつ、そのプログラム言語で使われているライブラリをうまく使って変換してくれます。

Siv3Dでバイブコーディングしてみる

まあ、ここまで長々と書いてようやく Siv3D のコーディングに辿り着くわけですが、あまりやることがないので、ちょっと長々と書いてしまっているだけです。

AI エージェントでは、Visual Studio Code + Claude Sonnet を使っています。GitHub Copilot ならば月10ドルで契約が可能です。学生ならば 0 円で利用ができます。他にも結構高めなものもありますが、夜中にぐるぐる回さない限りこれで十分です。Google の Gemini のほうは分からないのですが、どの AI エージェントを使ってもだいたい似た感じでいけます。好きなものを使ってください。

AI エージェントによっては、プロジェクトを自動作成するところからできるパターンも多いのですが、このように Visual Studio を使ってプロジェクトテンプレートを使ってひな形のプロジェクトだけ作っておくとよいです。ビルドも自動化できるのですが、結構面倒なので、AI ペアプロとしてビルドは人間のほうの担当にします。

仕事としてはファイルを分けたほうがいいのですが、今回は面倒なので Main.cpp に全てを突っ込みます。この初期状態でビルドができるところまで確認しておきます。

agents.md を作成する

実はファイル名は Agents.md でも Readme.md でも Claudes.md でもなんでも構いません。夜中に自動化しようとすると、ファイル名を決めたほうがうまく AI エージェントに伝わるのですが、AI ペアプロの場合には「agents.md に従ってコードを作成して」とプロンプトで伝えるだけで十分です。ファイル名を頼りにうまく探してくれます。

agents.md の中身は以下のようになっています。

# ウマ娘レース

## 概要

ウマ娘レースは、ウマ娘たちが競い合うレースイベントです。
プレイヤーは、ウマ娘のパラメータを調節してトップを目指します。

## ルール

- レースは、横一列に並んだ5人のウマ娘で行なわれる
- 各ウマ娘には、スピード、スタミナ、パワー、根性、賢さの5つのパラメータがある
- プレイヤーは、自分のウマ娘のパラメータを調整してレースに挑む
- レースは複数のターンで構成され、各ターンでウマ娘たちは前進する
- 最終的にゴールに最も早く到達したウマ娘が勝者となる


## パラメータの説明

- スピード: ウマ娘の基本的な速さを決定するパラメータ
- スタミナ: ウマ娘が長距離を走る際の持久力を決定するパラメータ
- パワー: ウマ娘の加速力や坂道での強さを決定するパラメータ
- 根性: ウマ娘が苦しい状況でも踏ん張る力を決定するパラメータ
- 賢さ: ウマ娘がレース中に適切な判断を下す能力を決定するパラメータ

パラメータは、合計 20 ポイントまで振り分けることができる。


## レースの進行

- 各ターンでウマ娘たちは、パラメータに基づいて前進距離が決定される

サイコロ 1から6までのサイコロを2個振る。

前進距離 =
スピード * サイコロA +
スタミナ * サイコロB +
パワー * (サイコロA + サイコロB) / 2 +
根性 * ランダム値(3から6) +
賢さ * ランダム値(1から3)

各ウマ娘は順番サイコロを振り、出た目の大きい順に前進距離を計算していく。
最初に 1000 メートルに到達したウマ娘が勝者となる。

## 画面構成

- レーストラック: ウマ娘たちが走るコースが表示される。横長の一列でよい。
- 自分のウマ娘のパラメータを入れる
- 他のウマ娘のパラメータはランダムに設定される
- スタートボタンを押すと、レースが開始される。1秒毎に順番サイコロを振る。

## 設計

// ウマ娘の構造体
struct UmaMusume {
name: String,
speed: u8,
stamina: u8,
power: u8,
guts: u8,
wisdom: u8,
position: u32,
};

// レースの構造体
struct Race {
uma_musumes: Vec<UmaMusume>,
track_length: u32
};

これも慣れるとここまで書けるのですが、慣れないうちはそんなに細かく書かなくてもいいです。はっきり言って、概要とルールだけ書いても、このウマ娘レースは作成されてしまいます。どうやら、この手のゲームロジック(ブロック崩しとかインベーダーゲームとか)は生成 AI の学習データにあるっぽくて、それらしい何処かで見たようなものをうまく出してくれます。まあ、端的に言えばパクりなのですが、ここでは目をつぶっておきましょう。私的範囲内の利用ということです。

AI にコーディングしてもらう。

プロンプトで「agents.md に従ってコーディングして」と頼むだけです。AI エージェントのほうもだんだん賢くなっているので、結構なスピードでコードを生成してくれます。

見てわかるのですが、

  • パラメータを入れるためのセットアップ画面
  • レース中の画面
  • 結果を表示する画面

の3つに分かれています。この要素は agents.md に入れていないので、つまりはこれはどこからからパクって来たものです。こんな風に典型的な画面の作りの場合には AI エージェントが一番効率的に動きます。逆に、この3つの画面を、うまくひとつの画面に落とし込むのはなかなか苦労します。独自画面は苦手なんですよね。

そのような場合は、あらかじめコードのほうに、1画面の配置をコーディングしてしまうとうまくいきます。agents.md にあれこれと画面構成を記述する方法も可能なのですが、AI エージェント自身が既存のコードを解析しながら、そこに付け加えるように進めていくのでコード自身で伝えるほうが効果的です。

逆に、うまくコードで伝わらないときは、agents.md を作り直して一からバイブコーディングをしてしてみてください。バイブコーディングの良いところは、うまくいかなかった時はすべてを捨ててやり直しができるところです。これが人間がコーディングをすると、手間がかかっていたり再利用を考えたりするところなのですが、バイブコーディングの場合はコーディングの作成時間が短い&人間の労力が少ないので、最初からやり直してしまったほうが早いです。同じ agents.md であっても、確率的に別な構造になるので、何度かやってうまくいったものを利用する手があります。

もっとも、数行のプロンプトだけでうまくいくまで AI に書いて貰うという学校の課題方式ではだめです。ちょっとずつ agents.md を書き変えて、うまくパターンをみつけるのが人間の仕事になります。

できあがった Main.cpp

しばらく待つと Main.cpp ができあがります。

# include <Siv3D.hpp> // Siv3D v0.6.16

// ウマ娘の構造体
struct UmaMusume
{
	String name;
	int32 speed;
	int32 stamina;
	int32 power;
	int32 guts;
	int32 wisdom;
	int32 position;
	Color color;

	// パラメータの合計を計算
	int32 totalParams() const
	{
		return speed + stamina + power + guts + wisdom;
	}

	// 前進距離を計算
	int32 calculateAdvance() const
	{
		int32 diceA = Random(1, 6);
		int32 diceB = Random(1, 6);
		int32 gutsRandom = Random(3, 6);
		int32 wisdomRandom = Random(1, 3);

		int32 advance = speed * diceA
			+ stamina * diceB
			+ power * (diceA + diceB) / 2
			+ guts * gutsRandom
			+ wisdom * wisdomRandom;

		return advance;
	}
};

// レースの構造体
struct Race
{
	Array<UmaMusume> umaMusumes;
	int32 trackLength = 1000;
	bool isRunning = false;
	bool isFinished = false;
	int32 winnerIndex = -1;
	Stopwatch turnTimer;

	// ランダムなパラメータを生成(合計20ポイント)
	static UmaMusume createRandomUma(const String& name, const Color& color)
	{
		UmaMusume uma;
		uma.name = name;
		uma.color = color;
		uma.position = 0;

		// ランダムに20ポイントを振り分け
		Array<int32> params = { 1, 1, 1, 1, 1 }; // 最低1ずつ
		int32 remaining = 15; // 残り15ポイント

		for (int32 i = 0; i < remaining; ++i)
		{
			int32 idx = Random(0, 4);
			params[idx]++;
		}

		uma.speed = params[0];
		uma.stamina = params[1];
		uma.power = params[2];
		uma.guts = params[3];
		uma.wisdom = params[4];

		return uma;
	}

	// レースを1ターン進める
	void advanceTurn()
	{
		if (isFinished) return;

		// 各ウマ娘の前進距離を計算
		Array<std::pair<int32, int32>> advances; // (前進距離, インデックス)
		for (size_t i = 0; i < umaMusumes.size(); ++i)
		{
			advances.push_back({ umaMusumes[i].calculateAdvance(), static_cast<int32>(i) });
		}

		// 前進距離の大きい順にソート
		std::sort(advances.begin(), advances.end(), [](const auto& a, const auto& b) {
			return a.first > b.first;
		});

		// 順番に前進
		for (const auto& [advance, idx] : advances)
		{
			umaMusumes[idx].position += advance;

			// ゴール判定
			if (umaMusumes[idx].position >= trackLength && !isFinished)
			{
				isFinished = true;
				winnerIndex = idx;
				isRunning = false;
			}
		}
	}
};

void Main()
{
	// ウィンドウサイズを設定
	Window::Resize(1200, 700);
	Scene::SetBackground(ColorF{ 0.3, 0.6, 0.3 });

	// フォントを作成
	const Font font{ FontMethod::MSDF, 48, Typeface::Bold };
	const Font smallFont{ FontMethod::MSDF, 32 };

	// ウマ娘の絵文字
	const Texture umaEmoji{ U"🏇"_emoji };

	// ウマ娘の名前と色
	const Array<String> umaNames = { U"プレイヤー", U"スペシャル", U"サイレンス", U"トウカイ", U"マチカネ" };
	const Array<Color> umaColors = { Palette::Red, Palette::Blue, Palette::Green, Palette::Orange, Palette::Purple };

	// レース
	Race race;

	// プレイヤーのパラメータ
	double playerSpeed = 4;
	double playerStamina = 4;
	double playerPower = 4;
	double playerGuts = 4;
	double playerWisdom = 4;

	// ゲーム状態
	enum class GameState { Setup, Racing, Result };
	GameState state = GameState::Setup;

	while (System::Update())
	{
		// タイトル表示
		font(U"🏇 ウマ娘レース 🏇").drawAt(48, Vec2{ 600, 40 }, Palette::White);

		if (state == GameState::Setup)
		{
			// パラメータ設定画面
			font(U"パラメータを設定してください").drawAt(32, Vec2{ 600, 100 }, Palette::Yellow);

			const int32 sliderX = 400;
			const int32 sliderWidth = 200;
			int32 y = 150;

			// パラメータスライダー
			SimpleGUI::Slider(U"スピード: {:.0f}"_fmt(playerSpeed), playerSpeed, 1.0, 10.0, Vec2{ sliderX, y }, 180, sliderWidth);
			y += 50;
			SimpleGUI::Slider(U"スタミナ: {:.0f}"_fmt(playerStamina), playerStamina, 1.0, 10.0, Vec2{ sliderX, y }, 180, sliderWidth);
			y += 50;
			SimpleGUI::Slider(U"パワー: {:.0f}"_fmt(playerPower), playerPower, 1.0, 10.0, Vec2{ sliderX, y }, 180, sliderWidth);
			y += 50;
			SimpleGUI::Slider(U"根性: {:.0f}"_fmt(playerGuts), playerGuts, 1.0, 10.0, Vec2{ sliderX, y }, 180, sliderWidth);
			y += 50;
			SimpleGUI::Slider(U"賢さ: {:.0f}"_fmt(playerWisdom), playerWisdom, 1.0, 10.0, Vec2{ sliderX, y }, 180, sliderWidth);

			// 合計ポイント表示
			int32 total = static_cast<int32>(playerSpeed) + static_cast<int32>(playerStamina)
				+ static_cast<int32>(playerPower) + static_cast<int32>(playerGuts) + static_cast<int32>(playerWisdom);

			Color totalColor = (total <= 20) ? Palette::Lime : Palette::Red;
			font(U"合計: {} / 20 ポイント"_fmt(total)).drawAt(28, Vec2{ 600, 430 }, totalColor);

			if (total > 20)
			{
				smallFont(U"※ 合計20ポイント以下にしてください").drawAt(24, Vec2{ 600, 470 }, Palette::Red);
			}

			// スタートボタン
			if (SimpleGUI::Button(U"レーススタート!", Vec2{ 500, 520 }, 200, (total <= 20)))
			{
				// レースを初期化(代入ではなくメンバごとに再初期化)
				race.umaMusumes.clear();
				race.trackLength = 1000;
				race.isRunning = false;
				race.isFinished = false;
				race.winnerIndex = -1;
				race.turnTimer.restart(); // タイマーをリセットして再スタート(既存インスタンスを再利用)

				// プレイヤーのウマ娘を作成
				UmaMusume player;
				player.name = umaNames[0];
				player.color = umaColors[0];
				player.speed = static_cast<int32>(playerSpeed);
				player.stamina = static_cast<int32>(playerStamina);
				player.power = static_cast<int32>(playerPower);
				player.guts = static_cast<int32>(playerGuts);
				player.wisdom = static_cast<int32>(playerWisdom);
				player.position = 0;
				race.umaMusumes.push_back(player);

				// 他のウマ娘をランダムに作成
				for (size_t i = 1; i < umaNames.size(); ++i)
				{
					race.umaMusumes.push_back(Race::createRandomUma(umaNames[i], umaColors[i]));
				}

				race.isRunning = true;
				race.turnTimer.restart();
				state = GameState::Racing;
			}

			// パラメータ説明
			y = 580;
			smallFont(U"スピード: 基本的な速さ | スタミナ: 持久力 | パワー: 加速力").draw(18, Vec2{ 200, y }, Palette::White);
			smallFont(U"根性: 踏ん張る力 | 賢さ: 判断力").draw(18, Vec2{ 200, y + 30 }, Palette::White);
		}
		else if (state == GameState::Racing)
		{
			// レース画面
			const int32 trackY = 200;
			const int32 trackHeight = 80;
			const int32 trackStartX = 100;
			const int32 trackWidth = 1000;

			// トラック背景を描画
			for (size_t i = 0; i < race.umaMusumes.size(); ++i)
			{
				int32 laneY = trackY + static_cast<int32>(i) * trackHeight;

				// レーン背景
				Rect{ trackStartX, laneY, trackWidth, trackHeight - 5 }
					.draw(ColorF{ 0.8, 0.6, 0.4 })
					.drawFrame(2, Palette::Brown);

				// ゴールライン
				Rect{ trackStartX + trackWidth - 10, laneY, 10, trackHeight - 5 }.draw(Palette::Red);
			}

			// 1秒ごとにターンを進める
			if (race.isRunning && race.turnTimer.ms() >= 1000)
			{
				race.advanceTurn();
				race.turnTimer.restart();
			}

			// ウマ娘を描画
			for (size_t i = 0; i < race.umaMusumes.size(); ++i)
			{
				const auto& uma = race.umaMusumes[i];
				int32 laneY = trackY + static_cast<int32>(i) * trackHeight;

				// 位置を計算(トラック幅に合わせてスケーリング)
				double progress = static_cast<double>(uma.position) / race.trackLength;
				progress = Min(progress, 1.0);
				int32 drawX = trackStartX + static_cast<int32>(progress * (trackWidth - 50));

				// ウマ娘の絵文字を描画
				umaEmoji.scaled(0.5).drawAt(drawX + 25, laneY + trackHeight / 2 - 2);

				// 名前と位置を表示
				smallFont(uma.name).draw(18, Vec2{ 10, laneY + 20 }, uma.color);
				smallFont(U"{}m"_fmt(uma.position)).draw(16, Vec2{ 10, laneY + 45 }, Palette::White);
			}

			// レース情報
			font(U"ゴール: {}m"_fmt(race.trackLength)).draw(24, Vec2{ 100, 620 }, Palette::White);

			// レース終了判定
			if (race.isFinished)
			{
				state = GameState::Result;
			}
		}
		else if (state == GameState::Result)
		{
			// 結果画面
			font(U"🎉 レース終了! 🎉").drawAt(48, Vec2{ 600, 150 }, Palette::Yellow);

			if (race.winnerIndex >= 0)
			{
				const auto& winner = race.umaMusumes[race.winnerIndex];
				font(U"優勝: {}"_fmt(winner.name)).drawAt(40, Vec2{ 600, 220 }, winner.color);

				if (race.winnerIndex == 0)
				{
					font(U"おめでとうございます!").drawAt(32, Vec2{ 600, 280 }, Palette::Lime);
				}
				else
				{
					font(U"残念...次は勝てるかも!").drawAt(32, Vec2{ 600, 280 }, Palette::Orange);
				}
			}

			// 順位表示
			Array<std::pair<int32, size_t>> ranking;
			for (size_t i = 0; i < race.umaMusumes.size(); ++i)
			{
				ranking.push_back({ race.umaMusumes[i].position, i });
			}
			std::sort(ranking.begin(), ranking.end(), [](const auto& a, const auto& b) {
				return a.first > b.first;
			});

			int32 y = 340;
			smallFont(U"【最終順位】").draw(24, Vec2{ 500, y }, Palette::White);
			y += 40;

			for (size_t rank = 0; rank < ranking.size(); ++rank)
			{
				const auto& uma = race.umaMusumes[ranking[rank].second];
				smallFont(U"{}位: {} ({}m)"_fmt(rank + 1, uma.name, uma.position))
					.draw(22, Vec2{ 480, y }, uma.color);
				y += 35;
			}

			// リトライボタン
			if (SimpleGUI::Button(U"もう一度プレイ", Vec2{ 500, 580 }, 200))
			{
				state = GameState::Setup;
			}
		}
	}
}

実は、生成されるコードはビルドが通るとは限りません。今後の AI エージェントでは改善されると思いますが、文法的にちょっと間違っていたり、関数の整合性が合わないところがちょこちょこ出てきます。

このコードでは既に修正してありますが、スタートボタンを押したときの「// レースを初期化(代入ではなくメンバごとに再初期化)」の部分で、Race 構造体の初期化でコンパイルエラーがでていました。

race = Race{} ;

Race 構造体にデフォルトコンストラクタがないので、これでは駄目です。

race.umaMusumes.clear();
race.trackLength = 1000;
race.isRunning = false;
race.isFinished = false;
race.winnerIndex = -1;
race.turnTimer.restart(); // タイマーをリセットして再スタート(既存インスタンスを再利用)

まあ、他にも修正点はあるかもしれませんが、ひとまずビルドして動かしてみましょう。

動作確認

動かすと、馬が後ろ向きに走ってしまっているとか、パラメータを保存できないとか、ケース結果が蓄積されないとか、いろいろ改善点があります。

これは agents.md に書き込んで「この部分を修正して」とプロンプトに入れてもよいし、自分でコードを書き直してもよいです。コードのコメントに修正案を書いて、「この部分を実装して」でも AI がコードを作ってくれます。

では、AI ペアプロで、よい Siv3D プログラミングライフを!

サンプルコード

moonmile/siv3d_uma_race https://github.com/moonmile/siv3d_uma_race

カテゴリー: 開発 | Siv3D でもバイブコーディング!!! はコメントを受け付けていません

エコーチェンばーちゃんの開発

7月頃だったか、新人研修が終わって AI エージェントがちょっと流行り始めた頃に1か月ほどあれこれと弄ってい時期があります。その頃は Claude Sonnet 4 を使って試していたのですが、いまだと Claude Opus 4.5 が使えるようになったので、もうちょっとマシになっているかなと思ってのお試しです。

この手のブラウザを使ったなんらかの Web サービスみたいなのは React で作っておくと楽です。私自身は React は得意じゃないのですが、AI エージェントが得意ならばそれでいいです。AI エージェントの得意分野のほうにあわせましょう、というスタイルですね。

これも、あらかじめ、React プロジェクトを作成しておきます。AI エージェントでプロジェクトから作成して貰ってもいいのですが、初期状態はバージョン関係か初期値のフォルダーの問題とかがあるので、手作業でやります。

npx create-react-app echo-cham-baachan

で、プロジェクトを作成しておいて、ここからスタートです。

Readme.md を作る

なかみは夏ごろに作ったものに少し手を入れています。書き方は機能から作ってもいいし、画面構成から考えてもいいです。本当は、いろいろと目的からスタートすればいいのですが、基本的に「エコーチェンバー現象」のシミュレーションツールというネタ機能なので、これだけで十分です。

# エコーチェンばぁちゃん

小中学生向けに、自分の意見が通る「エコーチェンバー現象」を疑似体験するツール。
反対意見がでてこないので、自分の意見が正しいと錯覚しやすくなる。

エコーチェンバー現象 - Wikipedia 
https://ja.wikipedia.org/wiki/%E3%82%A8%E3%82%B3%E3%83%BC%E3%83%81%E3%82%A7%E3%83%B3%E3%83%90%E3%83%BC%E7%8F%BE%E8%B1%A1

## 機能

- 自分の意見を入れると、5人のエコーチェンばぁちゃんが賛同してくれる。
- ばぁちゃんたちは、賛同の言葉をランダムに変化させる。
- 画面は、LINE のように吹き出しとばぁちゃんのアイコンが表示される。
- ばぁちゃんの性格は、いくつかのシステムプロンプトを用意して切り替えられる。

## 画面構成

- 入力エリア
  - 自分の意見を入力するテキストエリア
  - 送信ボタン
- チャットエリア
  - ばぁちゃんのアイコンと吹き出しで構成されるチャット表示エリア

## 技術スタック

- フロントエンド: React
- フロントエンドから OpenAI API へのリクエスト: Axios
- デプロイ: Vercel
- AI モデル: OpenAI GPT-5

## 環境変数 .env

- OPENAI_API_KEY: OpenAI API キー

Plan.prompt.md を作る

開発プランを vscode + copilot の Plan モードを使って作って貰います。

## Plan: エコーチェンばぁちゃん アプリ実装

README.mdに基づき、小中学生向けのエコーチェンバー現象疑似体験ツールを実装する計画です。Create React Appの初期状態から、LINEライクなチャットUIと5人のばぁちゃんAIを構築します。

### Steps

1. **依存関係のインストール** - `axios`を追加し、`package.json`を更新
2. **環境変数の設定** - `.env`ファイルを作成し、`REACT_APP_OPENAI_API_KEY`を定義
3. **UIコンポーネントの作成** - `src/components/`配下に`ChatContainer`、`MessageBubble`、`GrandmaAvatar`、`InputArea`を実装
4. **ばぁちゃんの設定ファイル作成** - `src/data/grandmas.js`に5人のばぁちゃん情報(名前、アイコン、システムプロンプト)を定義
5. **OpenAI APIサービスの実装** - `src/services/openai.js`でAPIリクエスト処理を作成
6. **メインApp.jsの実装** - `src/App.js`でチャット状態管理とコンポーネント統合
7. **LINEライクなスタイリング** - `src/App.css`で吹き出し・チャットUIのCSSを実装

### Further Considerations

1. **APIキーのセキュリティ** - フロントエンドから直接APIを呼ぶとキーが露出します。Vercel Serverless Functions (`/api/chat.js`) でラップする方式に変更しますか?

Vercel Serverless Functions を使う方式で。

2. **ばぁちゃんアイコン** - 5人分のアイコン画像はどこから用意しますか?(絵文字/生成AI/フリー素材)

ひとまず、絵文字で。

3. **性格の切り替え機能** - READMEに「システムプロンプトを用意して切り替えられる」とありますが、UIでの切り替え方法は?(ドロップダウン/タブ/設定画面)

設定が画面を作って。

足りない設定は「Further Considerations」に書かれているので、これを付けたします。いきなり Agent モードで作成すると、ばぁちゃんアイコンとか設定を想定で作ってしまうので、後からの修正が結構面倒です。なので、あらかじめ Plan を出して貰ってから、その後で Agent で進めるのがコツですね。

開発する

開発の頼み方はいろいろあるのですが、要は AI に通じればいいです。今回の場合は 30 分位で完了です。途中、いくつかフォルダーを作ったりするときに承認ボタンがでてきますが、これを押せば ok です。

実行する

まあ、質問をすると、なんでも肯定的に返してくれるので、こんな感じになります。

内部で使われているシステムプロンプトがこんな感じ。このシステムプロンプトも AI エージェント自身が作っているので、私は手をいれていません。他の性格を作りたいときは、この部分を直接書き変えてしまえばよいです。

// 性格タイプ別のシステムプロンプト
export const systemPrompts = {
  supportive: [
    `あなたは「ウメばぁちゃん」という優しいおばあちゃんです。
相手の意見には必ず賛同し、応援してください。
「そうそう」「その通りじゃ」「よく言った」などの言葉を使ってください。
返答は2〜3文程度で、温かみのある口調で話してください。
語尾は「〜じゃよ」「〜じゃね」「〜じゃのう」を使ってください。`,

    `あなたは「ハナばぁちゃん」という物知りなおばあちゃんです。
相手の意見には必ず賛同し、「さすがじゃ」「賢いのう」と褒めてください。
返答は2〜3文程度で、穏やかな口調で話してください。
語尾は「〜じゃよ」「〜じゃね」「〜じゃのう」を使ってください。`,

    `あなたは「トメばぁちゃん」という元気なおばあちゃんです。
相手の意見には必ず賛同し、「いいねぇ」「最高じゃ」と盛り上げてください。
返答は2〜3文程度で、明るい口調で話してください。
語尾は「〜じゃよ」「〜じゃね」「〜じゃのう」を使ってください。`,

    `あなたは「キヨばぁちゃん」という優しいおばあちゃんです。
相手の意見には必ず賛同し、「よく分かるよ」「うんうん」と共感してください。
返答は2〜3文程度で、包み込むような口調で話してください。
語尾は「〜じゃよ」「〜じゃね」「〜じゃのう」を使ってください。`,

    `あなたは「フミばぁちゃん」という知的なおばあちゃんです。
相手の意見には必ず賛同し、「なるほど」「深いのう」と感心してください。
返答は2〜3文程度で、落ち着いた口調で話してください。
語尾は「〜じゃよ」「〜じゃね」「〜じゃのう」を使ってください。`,
  ],

ChatGPT との会話でエコーチェンバー化するときは1対1なので、末尾のおべっかあたりを無視すればいいのですが、こんな風に5人の人格(ってほど分けられてはいないけど)にホめられると、ちょっといい気分になりそうなのが怖いですw

参考

GitHub https://github.com/moonmile/echo-cham-baachan

Vercel https://echo-cham-baachan.vercel.app で動作確認

設定から「陰謀論型」を選んだ時。

カテゴリー: 開発 | エコーチェンばーちゃんの開発 はコメントを受け付けていません

vscode + copilot の plan モードを実務の計画に応用できるか確認してみよう

別に Claude Code のプランモードが駄目という訳ではないのですが、IT 屋の範疇でいる “Plan” と、実務的な “Plan” とは大きな差があるので、そこだけ高速化したり自動化したりしても仕方がないんですよ。というのが本記事の主旨です。いわゆる、部分最適化に陥り勝ちになるので、じゃあ、全体を見渡した時に昨今の AI エージェントの Plan モードあるいは spec 駆動が、どのように実務に活用できるのか、という実験です。

プログラムを作るとか IT システムを組むとかいう範囲ではなくて、もう少し広い範囲で考えてみます。例えば、次のような要件での入札案件が出て来たとしましょう(実際に案件としてあるわけだし)。実際には入札までの設計とか見積もりとか人員の調達などが入るのですが、じゃあ、実務見積もりとしてどのくらい AI エージェントの機能が貢献できるのか? という実験をしてみます。これ、入口の部分は今書いている書籍に入ってはいるのですが、その続きはどうなるのかわからないので、結論はよくわからないところからスタートします。

Readme.md で目的を立てる

AI に資料を渡さないとスタートができないので、Readme.md にスタートとゴールの記述をします。

# 幼保小の架け橋プログラムに関する調査研究事業のまとめ

## 目的

「幼保小の架け橋プログラムに関する調査研究事業」の各都道府県の成果報告書をまとめる。
成果報告書があがっているが、フォーマットが統一されていないので、内容を整理する必要がある。
これを整理しつつ、目的のポイントを抑えて、参考となりそうな上位5件程度をピックアップしてまとめる。

## 進め方

1. 各都道府県の成果報告書を収集する。
2. ピックアップする内容、ポイントを抽出する。
3. ピックアップ項目に対して、各都道府県別の調査報告書から抜き出して、まとめる。
4. まとめた内容をレビューし、注目すべき上位5件ずつを選定する。
5. 最後に、ピックアップする項目単位で目次を作り、報告書を PDF にまとめる。

## 収集元

- 幼保小の架け橋プログラム:文部科学省 https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00002.htm
- 【幼保小の架け橋プログラム】中間成果報告会発表資料:文部科学省 https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00029.htm
- 【幼保小の架け橋プログラム】令和5年度成果報告書:文部科学省 https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00028.htm
- 【幼保小の架け橋プログラム】事業成果報告会(令和7年2月10日):文部科学省 https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00035.htm

## ピックアップ項目

- 全体的に、都道府県名、総人口、施設数を取りだす。
- 本プログラムの主旨に沿っているかチェックする
- 本プログラムの成果を確認する
- 本プログラムの次回の課題を確認する
- カリキュラム中の議事録やワークシートのリンク先をピックアップする

ピックアップ項目は、本プログラムの成果物をまとめた段階で、追加・修正する可能性がある。

いわゆる、入札案件の要件定義を自分たちで書き出すのと、見通してとしての計画を立てておきます。このあたりは、経験が必要なので IT 業界で2,3年では難しいかもしれませんが、逆に言えば非ITの実務業界としては、このあたりがスタート地点になりますよね。さらっと、A4 の用紙に1枚で書けるようになるといいです。

で、これはたたき台なので、その前身として AI を使ってもかまいません。ただし、AI の提案するプランはフルスペックなものが多いので、適度に端折らなければいけません。いわゆる、適度な予算で組めるような計画を立てないいけないのです。コツとしては、松竹梅で作っておいて、竹になるように工夫するわけですが、そのあたりは別のノウハウですね。

軽く「進め方」を書いてありますが、これは非 IT 業界の人でも可能です。

  • なんらかの形で「成果物報告書」を収集する(手作業でも可)
  • なんらかの形で「成果物から内容をピックアップする(手作業…というか、人が読むのも可)

というわけで、大抵は人手で頑張ります。基本 IT 屋さんも人手に頼るところもあるのですが、そこは IT 業界の強みとして、何らかのツールを使って高速化/省力化していきたいです。さらに言えば、AI エージェントなどの力を借りて、労力を下げたり、ひとよりも精度をアップさせたいわけです。そこが、非 IT の人との差別化になりますよね。IT に関してはプロなわけですから、道具を活用できるように少し頭を働かせてみようというわけです。

Plan.md を作成する

では、vscode + copilot を “plan” モードに切り替えて、データ収集とまとめのプランを立てて貰いましょう。

Plan モードって、手順を作るモードじゃなくて手順通りに進めるというモードなんですかね?このあたりが不明だけど、まあ、リンク先のファイルを読み込んで「19自治体の成果物」ってのは、たしかに正確ですね。

impliments とeditor があるので「editor」を選択しました。多分、impliments は即実行してしまうので危ういです。いったん editor で plan.prompt.md を確認しておきましょう。

# Plan: 幼保小の架け橋プログラム調査研究のまとめ

readme.md の目的を達成するため、19自治体の成果報告書(PDF形式)を収集・整理し、ピックアップ項目に基づいて上位5件を選定、最終レポートとしてPDFにまとめる計画です。

---

## Steps

1. **フォルダ構造の作成** - `data/`(年度別PDF保存用)、`analysis/`(比較分析用)、`output/`(最終成果物用)を readme.md と同階層に作成

2. **PDF一括ダウンロードスクリプトの作成** - 令和5年度・6年度成果報告書、中間報告会資料、事業成果報告会資料を19自治体分(計約70〜100ファイル)を自動取得するPowerShellまたはPythonスクリプトを作成

3. **基本情報収集シートの作成** - 19自治体(北海道、岐阜県、滋賀県、広島県、山口県、高知県、大館市、白石市、西会津町、川越市、横浜市、袋井市、掛川市、京都市、枚方市、箕面市、津和野町、高松市、竹田市)の総人口・施設数をExcel/CSVにまとめる

4. **ピックアップ項目の抽出と比較表作成** - 各報告書から「主旨との整合性」「成果」「次回課題」「カリキュラム・ワークシートのリンク」を抽出し、`analysis/summary.xlsx`に一覧化

5. **スコアリング基準に基づく上位5件の選定** - 先進性、エビデンスの質、再現可能性、地域特性の多様性を考慮して評価・ランキング

6. **最終レポートPDFの作成** - ピックアップ項目単位の目次構成で、選定した5自治体の詳細分析をまとめてPDF化

---

## Further Considerations

1. **PDFテキスト抽出ツールの選定** - 手作業で読み込むか、PyMuPDF/pdfplumberで自動抽出するか?(自動化推奨:19自治体×複数ファイル)

2. **評価基準の重み付け** - 「主旨との整合性」「成果の具体性」「課題の明確さ」のどれを優先するか?事前に決定が必要

3. **年度の扱い** - 令和5年度と6年度の両方を分析対象にするか、最新の6年度(最終年度)のみに絞るか?

---

## 対象自治体一覧(19自治体)

### 県レベル(6件)
| 自治体 |
|--------|
| 北海道 |
| 岐阜県 |
| 滋賀県 |
| 広島県 |
| 山口県 |
| 高知県 |

### 市町村レベル(13件)
| 自治体 | 所在県 |
|--------|--------|
| 大館市 | 秋田県 |
| 白石市 | 宮城県 |
| 西会津町 | 福島県 |
| 川越市 | 埼玉県 |
| 横浜市 | 神奈川県 |
| 袋井市 | 静岡県 |
| 掛川市 | 静岡県 |
| 京都市 | 京都府 |
| 枚方市 | 大阪府 |
| 箕面市 | 大阪府 |
| 津和野町 | 島根県 |
| 高松市 | 香川県 |
| 竹田市 | 大分県 |

---

## 推奨フォルダ構造

```
h:\ai-plan-junior-school-research\
├── readme.md
├── data\
│   ├── r5_reports\           # 令和5年度成果報告書
│   ├── r6_reports\           # 令和6年度成果報告書
│   ├── interim_reports\      # 中間報告会資料
│   └── final_reports\        # 事業成果報告会資料
├── analysis\
│   └── summary.xlsx          # 比較分析シート
└── output\
    └── final_report.pdf      # 最終成果物
```

---

## 収集元URL

- 幼保小の架け橋プログラム:https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00002.htm
- 中間成果報告会発表資料:https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00029.htm
- 令和5年度成果報告書:https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00028.htm
- 事業成果報告会(令和7年2月10日):https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00035.htm

フォルダーの構成とかを作ってくれますね。このあたり、非IT業界だと日本語で作るところですが、IT屋さんとしては Python をあたりを使って自動化したいところなので、このまま使います。多分、フォルダ構成を日本語に直しても AI エージェントのほうでうまくやってくれるはずです。

Steps が具体的な手順で、Further Considerations が設定しなければいけない要素ですね。対象自治体一覧はサイト https://www.mext.go.jp/a_menu/shotou/youchien/1258019_00035.htm をざっと見て確認しておいてください。うまく抽出できている模様です。これは文部科学省のサイトがうまく構造化されているということですね。場合によっては、このあたりが読み込めないサイトもあるでしょうから注意が必要です。たぶん、JavaScript を利用した CSR のサイトは難しいので、そのあたりは手作業が必要かもしれません。

Further Considerations に追加する

## Further Considerations

1. **PDFテキスト抽出ツールの選定** - 手作業で読み込むか、PyMuPDF/pdfplumberで自動抽出するか?(自動化推奨:19自治体×複数ファイル)

Python の PyMuPDF を使って自動収集する


2. **評価基準の重み付け** - 「主旨との整合性」「成果の具体性」「課題の明確さ」のどれを優先するか?事前に決定が必要

「成果の具体性」を最優先にして評価を行って。


3. **年度の扱い** - 令和5年度と6年度の両方を分析対象にするか、最新の6年度(最終年度)のみに絞るか?

令和5年度と6年度の両方を分析対象にして。

たぶん、それぞれの項目に設定値を書いておけばいいはずです。このあたりは、うまく AI エージェントが読み取ってくれます。

この後、Plan で動かすのか Agent で動かすのかが不明なのですが、Plan モードにしたままで、Plan.prompt.md を実行してもらいましょう。

ここでは「Start Implimentation」のボタンをクリック…できあがるのかな?

自動承認を有効にするか?

vscode + copilot で動かしたときに、Allow ボタンがでて、いちいちステップ実行の確認を取ります。これ、真夜中に動かしたいばあいは「自動承認」と言う形でやってしまうのがいいのですが、現時点では悪意ある MCP の防御ができなかったり、サイトの読み込み時に不備が発生しそうなので、人間がいちいち承認するほうがよいです。

何回か同じプロンプトを動かして、安全とわかっている場合にはいいのですが、最初に動かすプロンプトの場合は要注意かなと。

あと、このスクリプトのままだと Python の仮想環境を AI エージェントが作るので初期設定がちょっと面倒ですね。Python の仮想環境だけは、あらかじめ手作業で作っておいたほうがよさそうです。

ダウンロードスクリプトとか、もうベタに作ってくれます。

# -*- coding: utf-8 -*-
"""
幼保小の架け橋プログラム - 成果報告書PDFダウンロードスクリプト
文部科学省の成果報告書ページから19自治体分のPDFを一括ダウンロードします。
"""

import os
import requests
from pathlib import Path
from urllib.parse import urljoin
import time

# ベースURL
BASE_URL = "https://www.mext.go.jp"

# 保存先ディレクトリ
DATA_DIR = Path(r"h:\ai-plan-junior-school-research\data")

# 令和5年度成果報告書(2024年7月9日公開)
R5_REPORTS = {
    "北海道": {
        "成果報告書": "https://www.mext.go.jp/content/20240709-mxt_youji-000023526-1.pdf",
        "カリキュラム": "https://www.mext.go.jp/content/20240709-mxt_youji-000023526-2.pdf",
        "成果物": "https://www.mext.go.jp/content/20240709-mxt_youji-000023526-3.pdf",
    },
    "岐阜県": {
        "成果報告書": "https://www.mext.go.jp/content/20240709-mxt_youji-000023526-4.pdf",
        "カリキュラム": "https://www.mext.go.jp/content/20240709-mxt_youji-000023526-5.pdf",
    },
    "滋賀県": {
        "成果報告書": "https://www.mext.go.jp/content/20240709-mxt_youji-000023526-6.pdf",
        "カリキュラム": "https://www.mext.go.jp/content/20240709-mxt_youji-000023526-7.pdf",
    },
...

見ると分かるのですが、ファイル名がべた書きですよね。これはこれでいいのです。治具=使い捨てのツールとして、Python スクリプトが用いられているわけで、これをファイルから読み込むとか何かの設定から読み込むとかしなくて十分なのです。ダウンロードスクリプトなんて1回しか動かさないし、ファイル名なんて固定で良い訳です。

これがプログラマだったり、プログラムを外注したりするとあれこれと設計やら打合せが必要になるのですが、AI エージェントががりがりとベタで作ってくれるので、その分手間が省けます。逆に言えば、ちょっと修正したい場合(自治体を増やしたい場合とか)は、Plan.prompt.md に自治体とリンク先を追加して、もういちど AI エージェントを使って実行して貰えばいいのです。

このあたりの繰り返しが非常に高速になったのが AI エージェントの利点です。どこのブログだったかツイートだったか忘れましたが「ウォーターフォール開発のプロセスを 15 分で廻せるようになった」のが大きいです。いままでは、この部分をアジャイル開発だとか計画駆動とかで設計を練る、あるいは動作させてから考え直す、というループが必要だったのですが、AI エージェントがすべてをやり直してくれるので「高速にまわる計画駆動」ができるようなったのが大きいですね。

まあ、この部分はPDF のダウンロード&分析という比較的やりやすいところをターゲットにしているので AI エージェントが有効に働きますが、これが部品の品質点検&改良とか、食品加工での調味料の組み合わせ試験、とかになると AI エージェントだけでは回らないので工夫が必要です。そういうときは別な計画が必要になります。他にも、組み込み機器で実測とか、Android 機器の BLE 相性あわせ(これをやっている)とかは AI エージェントだけでは回りません。

分析の完了

さて、2時間ほど掛かりましたが分析レポートが作成されました。

このレポートの点数付けは analyze_reports.py の analyze_report 関数で計算しているので、単純な加点形式なのですが、ひとまず下調べとしてはよいでしょう。

def analyze_report(municipality: str) -> dict:
    """
    自治体の成果報告書を分析する
    
    Args:
        municipality: 自治体名
    
    Returns:
        分析結果の辞書
    """
    result = {
        "自治体名": municipality.replace("_", "(") + ")" if "_" in municipality else municipality,
        "成果報告書": "",
        "主旨との整合性": "",
        "主な成果": "",
        "次回の課題": "",
        "カリキュラム・ワークシート": "",
        "成果の具体性スコア": 0,
        "備考": ""
    }
    
    # 令和5年度成果報告書を読み込む
    r5_report_path = DATA_DIR / "r5_reports" / f"{municipality}_成果報告書.pdf"
    r5_text = ""
    if r5_report_path.exists():
        r5_text = extract_text_from_pdf(r5_report_path)
        result["成果報告書"] = "あり"
    else:
        result["成果報告書"] = "なし"
        result["備考"] = "令和5年度成果報告書が見つかりません"
    

本来ならば、ピックアップしたい用語などを含めて PDF を OpenAI API に流し込んで、AI にスコア判定をさせる方法がよいです。これをやるためには、plan.prompt.md を書き変えて、評価の重みづけをするツールを Python スクリプトを作るのがよいです。このあたりは、プログラマの領分になってしまうので、またの機会にしましょう。

スコア計算のツールを AI エージェントを使って作るればいいだけなのですが、プロンプトの作成と OpenAI の API キーコードが必要になってしまうので、ちょっと非 IT 業界の人には荷が重いかもしれません。このあたり、自動化をしないのであれば、ChatGPT に PDF を手作業で突っ込みながらも可能なので、要件自体は満たせます。

final_report.md を python の md-to-pdf を使って PDF ファイルに直したものです。スコアをどう見るかは、中身に確認していかないと駄目なのですが、下調べ段階としてはこれで十分でしょう。すべてを調ベルのは大変なので、優先度としてスコアの高いものから順にチェックしていけばいいのです。そこは AI の判断を信用するかどうかは別なのですが。

まあ、こんな感じで非 IT 業界でも vscode + copilot の AI エージェントを使って資料作成とか情報収集とかができるようになりますよ、という例です。特に、この手のツールをを外注したり自前で時間をかけて作る必要がなくなるのが良いかなと。Plan のほうは、自分で計画立てないと途中で頓挫しそうですが、少しは頼りになるでしょう。

参考先

https://github.com/moonmile/ai-plan-junior-school-research

追記 OpenAI API を使う

もう少し実用的な例を含めてみようと思って、OpenAI API を使ってそれぞれの報告書のスコアを出してみましょう。

Plan.prompt.md に新しい評価基準を追加します。

### 評価基準(成果の具体性スコア: 最大100点)

評価基準関数 analyze_report を OpenAI API を使って再実装する
- analyze_report_ai 関数とする


- 成果報告書の有無 20点
- 主旨との整合性 20点
  主要キーワードが上手く織り込まれているかをチェック
- 主な成果 20点
  成果の主張や具体的なデータ・事例の提示がされているかをチェック
- 次回の課題 20点
  問題点を明らかにして、次回以降の課題が具体的に締召されているかをチェック
- カリキュラム・ワークシートの有無 20点
  カリキュラムやワークシートのリンクが提供されているかをチェック

この内容を OpenAI のプロンプトと成果物の文書を一緒に渡して、評価結果を返して貰う事になります。以前は、システムプロンプトを作ったり、OpenAI API を呼び出す関数を作ったりと色々大変でしたが、いまでは AI エージェントが適切なコードを作ってくれます。

ここからはプログラマの領域に踏み込むので、非 IT 業界の方には少し辛いかもしれませんが、大丈夫です。概ねは AI エージェントがやってくれます。が、一発ではうまく動かない(大抵は JSON 形式が喰い違ってうまくいかない)のですが、これも AI エージェントが少しずつ直していってくれます。

このあたりが、AI エージェントが Excel VBA のように一般に広まるかどうかの肝ですね。いわゆる、バグ直しをしないといけないので、これは従来のプログラミングとほとんど変わりません。コードを書かない代わりに、不具合を見つけてうまく AI に指示を出すとか、自分が納得できるまで AI エージェントを働かせ続けることが必要になります。つまりは、根気がいるわけですが…ここはちょっと分かりません。私はプログラマなので慣れているのですが、一般の人はどう思うのか?

結果として、こんな風に Excel 形式で分析結果がでます。

それぞれのスコアもそれっぽく出ているので、プログラムの動きとしては問題なさそうです。

システムプロンプトに関しては、以下のようになっているので、この内容を書き変えれば別の評価基準になります。

# OpenAI クライアント(環境変数 OPENAI_API_KEY を自動的に使用)
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))

# 評価プロンプト
EVALUATION_PROMPT = """あなたは「幼保小の架け橋プログラム」の成果報告書を評価する専門家です。
以下の自治体の成果報告書を評価してください。

## 評価対象自治体
{municipality}

## 成果報告書の内容(抜粋)
{report_text}

## カリキュラム・成果物の有無
{materials_info}

## 評価基準(最大100点)

以下の5つの観点で各20点満点で評価してください:

### 1. 成果報告書の有無・充実度(20点)
- 成果報告書が存在するか
- 報告書の内容が充実しているか
- 構成が整理されているか

### 2. 主旨との整合性(20点)
以下の主要キーワードが適切に織り込まれているかをチェック:
- 架け橋期、5歳児、小学校1年生
- 連携、接続
- 主体的、対話的、深い学び
- 多様性、学びの基盤

### 3. 主な成果(20点)
- 成果の主張が明確か
- 具体的なデータや数値が提示されているか
- 実践事例が具体的に記載されているか
- エビデンスに基づいた成果報告になっているか

### 4. 次回の課題(20点)
- 問題点が明確に示されているか
- 次回以降の課題が具体的に提示されているか
- 改善に向けた方向性が示されているか

### 5. カリキュラム・ワークシートの有無(20点)
- カリキュラムが提供されているか
- ワークシートや成果物が提供されているか
- 他自治体でも活用可能な形式か

## 出力形式
以下のJSON形式で回答してください(コードブロックなしで純粋なJSONのみ):

{{"成果報告書スコア": 15, "成果報告書評価": "評価コメント", "主旨整合性スコア": 18, "主旨整合性評価": "評価コメント", "成果スコア": 16, "成果評価": "評価コメント", "課題スコア": 14, "課題評価": "評価コメント", "成果物スコア": 12, "成果物評価": "評価コメント", "総合スコア": 75, "総合評価": "総合的な評価コメント", "主な成果要約": "成果の要約(200文字以内)", "主な課題要約": "課題の要約(200文字以内)"}}
"""

いままで、このプロンプトを書くのが大変で「プロンプトエンジニアリング」と呼ばれていたものですが、こんな風にレポートを出したり要約を出す程度であれば、AI エージェントが作成してくれるプロンプトで構いません。それほど手を入れなくて済みます。

まあ、謎な画像AIのプロンプトならば別なのでしょうが、OpenAI API などを使って AI とやり取りするための JSON 形式や内容を変換するようなプロンプトをを作る場合には AI エージェント自身に作成してもらったほうがいいでしょう。

コード自体は、https://github.com/moonmile/ai-plan-junior-school-research/tree/openai のほうにブランチ openai にしておくので、そこからダウンロードしてください。

レポートを見ると、分析手法が書いてあります。成果の具体性スコアも変わっています。これれは、実際に報告書を読んで人間が判断する必要があります。

カテゴリー: 開発 | vscode + copilot の plan モードを実務の計画に応用できるか確認してみよう はコメントを受け付けていません

数学的な問題について画像AIによる嘘を避ける方法

この手の話は、DALL-E の頃から言われていて、何かと数学的な図を書かせようとするとどこからかのなんちゃって画像を持ってくるために変なことになります。たぶん、古い教科書のスキャン画像とかを学習データにいれてしまっていて、そこから引っ張ってきているだけです。そもそも、画像の生成 AI に関しては、「教師なし学習」での推論でしかなくて、数学や物理のような自然科学的な解答のある「教師あり学習」の結果を求めようとしても無駄です。このあたりは、漫画やイラストを描かせた場合にはなんとなくいいけれど、部分的に手の指がおかしかったり腕が三本あったりするのがそれです。このあたりの正確性≒正解と明確に分かるものは、将来的にAIエージェントによる自己チェック機能で避けることができると思います。

まあ、その自己チェックを入れてあげれば、この手の図も正確にできるようになると、という例が以下のものです。

現状の唯一の方法としては、先の「シュレディンガー方程式」については、直接画像AIを使うのではなく、いったん Python などを使って正確な式から正確な図を描くようにします。こうすると、当たり前ですが正確な図ができますね。これを、切り貼りする(素材として画像AIに渡してもいいでしょう)ことで、「数学的に正確な図」を私達は得ることができます。

当たり前といえば、当たり前なのですが、この手のなんちゃって画像AI全般に言えることで、プロンプトがどうという話ではなく、さきに書いた通り

・「教師なし学習」的な正解ではないもの、つまりは「創造性」みたいなものを求めるのか?
・「教師あり学習」的な数学や物理のように自然科学として正解があるものを求めるのか?

という違いです。先の X のポストから言えば、当然後者のほうではあるのですが、実は意図として「数学的には正しくはないけれど、なんとなく中世で扱っていた数学っぽい SF 的な図を作り出して」というプロンプトであれば、前者が求められるというわけです。

で、実験的に Python でコードを出力もらったらどうなのか? ということを試してみましょう。以下は「Claude Sonnet 4.5」を使って Python コードを出力したものです。画像は、matplotlib.pyplot を使って PNG 形式で得ることができます。

ランダム値

プロンプトで「ランダム値をプロットして」というと、これもなんちゃって画像が生成されます。実際にランダム値の正確な図が欲しいのであれば、Python コードで書けばいいだけです。さぼってはいけませんw

# 一様分布のグラフを描く
import numpy as np
import matplotlib.pyplot as plt

# 日本語フォントの設定
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Meiryo']
plt.rcParams['axes.unicode_minus'] = False

# 一様分布からランダム値を生成
np.random.seed(42)  # 再現性のため
n_samples = 10000

# 0から1の範囲で一様分布
uniform_values = np.random.uniform(0, 1, n_samples)

# グラフの作成
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

# 1. ヒストグラム
axes[0, 0].hist(uniform_values, bins=50, edgecolor='black', alpha=0.7)
axes[0, 0].set_title('一様分布のヒストグラム')
axes[0, 0].set_xlabel('値')
axes[0, 0].set_ylabel('度数')
axes[0, 0].grid(True, alpha=0.3)

# 2. 累積分布
axes[0, 1].hist(uniform_values, bins=50, cumulative=True, edgecolor='black', alpha=0.7)
axes[0, 1].set_title('累積分布')
axes[0, 1].set_xlabel('値')
axes[0, 1].set_ylabel('累積度数')
axes[0, 1].grid(True, alpha=0.3)

# 3. 散布図(サンプル順)
sample_indices = np.arange(min(500, n_samples))
axes[1, 0].scatter(sample_indices, uniform_values[:len(sample_indices)], alpha=0.5, s=10)
axes[1, 0].set_title('ランダム値の分布(最初の500サンプル)')
axes[1, 0].set_xlabel('サンプル番号')
axes[1, 0].set_ylabel('値')
axes[1, 0].grid(True, alpha=0.3)

# 4. 理論値との比較
sorted_values = np.sort(uniform_values)
theoretical = np.linspace(0, 1, n_samples)
axes[1, 1].plot(theoretical, sorted_values, 'b-', alpha=0.5, label='実測値')
axes[1, 1].plot([0, 1], [0, 1], 'r--', label='理論値(y=x)')
axes[1, 1].set_title('Q-Qプロット(理論値との比較)')
axes[1, 1].set_xlabel('理論分位点')
axes[1, 1].set_ylabel('実測分位点')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('一様分布.png', dpi=300, bbox_inches='tight')
plt.show()

print(f'サンプル数: {n_samples}')
print(f'平均値: {np.mean(uniform_values):.4f} (理論値: 0.5000)')
print(f'標準偏差: {np.std(uniform_values):.4f} (理論値: {1/np.sqrt(12):.4f})')
print(f'最小値: {np.min(uniform_values):.4f}')
print(f'最大値: {np.max(uniform_values):.4f}')

波動関数


波動関数をグラフにしたものです、波動関数自体は数学的な式なので、画像AIに頼るよりも Python などで正確に描いたほうが良いです。ここぐらいまでは、コードやグラフを眺めて、ほぼ出ているだろうというレベルでしょう。


# シュレディンガー方程式の「波動関数」と「確率密度」
# 波動関数の例を 2D グラフで
# 確率密度のグラフを 3D グラフで

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

# 日本語フォントの設定
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Meiryo']
plt.rcParams['axes.unicode_minus'] = False

# 1次元無限井戸型ポテンシャルの波動関数
def wave_function_1d(x, n, L):
    """
    1次元無限井戸型ポテンシャルの波動関数
    n: 量子数 (1, 2, 3, ...)
    L: 井戸の幅
    """
    return np.sqrt(2/L) * np.sin(n * np.pi * x / L)

# 2次元波動関数(例:水素原子の2p軌道)
def wave_function_2d(x, y):
    """
    簡易的な2次元波動関数の例
    """
    r = np.sqrt(x**2 + y**2)
    return r * np.exp(-r) * np.cos(np.arctan2(y, x))

# 確率密度関数(波動関数の絶対値の2乗)
def probability_density(psi):
    """
    確率密度 = |ψ|²
    """
    return np.abs(psi)**2

# ===== 1. 波動関数の2Dグラフ =====
fig = plt.figure(figsize=(16, 10))

# 1次元波動関数(複数の量子状態)
x = np.linspace(0, 1, 1000)
L = 1.0

ax1 = plt.subplot(2, 3, 1)
for n in [1, 2, 3, 4]:
    psi = wave_function_1d(x, n, L)
    ax1.plot(x, psi, label=f'n={n}')
ax1.set_xlabel('位置 x')
ax1.set_ylabel('波動関数 ψ(x)')
ax1.set_title('1次元無限井戸型ポテンシャルの波動関数')
ax1.legend()
ax1.grid(True, alpha=0.3)
ax1.axhline(y=0, color='k', linestyle='-', linewidth=0.5)

# 確率密度(1次元)
ax2 = plt.subplot(2, 3, 2)
for n in [1, 2, 3, 4]:
    psi = wave_function_1d(x, n, L)
    prob = probability_density(psi)
    ax2.plot(x, prob, label=f'n={n}')
ax2.set_xlabel('位置 x')
ax2.set_ylabel('確率密度 |ψ(x)|²')
ax2.set_title('確率密度(1次元)')
ax2.legend()
ax2.grid(True, alpha=0.3)

# 波動関数の実部と虚部(時間発展を含む例)
ax3 = plt.subplot(2, 3, 3)
t = 0
n = 2
psi = wave_function_1d(x, n, L)
E = n**2  # エネルギー固有値(簡略化)
psi_real = psi * np.cos(E * t)
psi_imag = psi * np.sin(E * t)
ax3.plot(x, psi_real, label='実部 Re(ψ)', color='blue')
ax3.plot(x, psi_imag, label='虚部 Im(ψ)', color='red')
ax3.plot(x, np.abs(psi), label='振幅 |ψ|', color='green', linestyle='--')
ax3.set_xlabel('位置 x')
ax3.set_ylabel('波動関数')
ax3.set_title(f'波動関数の実部・虚部(n={n}, t={t})')
ax3.legend()
ax3.grid(True, alpha=0.3)
ax3.axhline(y=0, color='k', linestyle='-', linewidth=0.5)

# ===== 2. 2次元確率密度の3Dグラフ =====

# 2次元グリッド
x_2d = np.linspace(-5, 5, 100)
y_2d = np.linspace(-5, 5, 100)
X, Y = np.meshgrid(x_2d, y_2d)

# 波動関数を計算
psi_2d = wave_function_2d(X, Y)
prob_2d = probability_density(psi_2d)

# 3Dプロット
ax4 = plt.subplot(2, 3, 4, projection='3d')
surf = ax4.plot_surface(X, Y, prob_2d, cmap='viridis', alpha=0.8)
ax4.set_xlabel('x')
ax4.set_ylabel('y')
ax4.set_zlabel('確率密度 |ψ|²')
ax4.set_title('2次元確率密度(3D表示)')
plt.colorbar(surf, ax=ax4, shrink=0.5)

# 2Dヒートマップ(上から見た図)
ax5 = plt.subplot(2, 3, 5)
contour = ax5.contourf(X, Y, prob_2d, levels=20, cmap='viridis')
ax5.set_xlabel('x')
ax5.set_ylabel('y')
ax5.set_title('確率密度(ヒートマップ)')
ax5.set_aspect('equal')
plt.colorbar(contour, ax=ax5)

# 等高線プロット
ax6 = plt.subplot(2, 3, 6)
contour_lines = ax6.contour(X, Y, prob_2d, levels=15, colors='black', linewidths=0.5)
ax6.contourf(X, Y, prob_2d, levels=20, cmap='plasma', alpha=0.7)
ax6.clabel(contour_lines, inline=True, fontsize=8)
ax6.set_xlabel('x')
ax6.set_ylabel('y')
ax6.set_title('確率密度(等高線)')
ax6.set_aspect('equal')

plt.tight_layout()
plt.savefig('波動関数と確率密度.png', dpi=300, bbox_inches='tight')
plt.show()

# 統計情報を出力
print("=" * 50)
print("1次元波動関数の統計(n=1の場合)")
print("=" * 50)
psi_1 = wave_function_1d(x, 1, L)
prob_1 = probability_density(psi_1)
print(f"波動関数の最大値: {np.max(np.abs(psi_1)):.4f}")
print(f"確率密度の積分(規格化確認): {np.trapz(prob_1, x):.4f}")
print(f"期待値 <x>: {np.trapz(x * prob_1, x):.4f}")

print("\n" + "=" * 50)
print("2次元確率密度の統計")
print("=" * 50)
total_prob = np.sum(prob_2d) * (x_2d[1] - x_2d[0]) * (y_2d[1] - y_2d[0])
print(f"確率密度の総和(近似): {total_prob:.4f}")
print(f"最大確率密度: {np.max(prob_2d):.6f}")
max_idx = np.unravel_index(np.argmax(prob_2d), prob_2d.shape)
print(f"最大確率密度の位置: (x={X[max_idx]:.2f}, y={Y[max_idx]:.2f})")

水素原子の電子軌道

実は、水素原子の軌道計算がちょっとアヤシイです。自分の場合は、原子力学科なのでこの軌道計算は最初の頃にやった筈なのですが(まあ、波動関数自体はいわゆる常識の範囲なので)、図の書き方が一般的に流通するものと違うのでなんとも言えないです。

ただし、電子軌道計算は、https://betterlate-thannever.github.io/Chemistry-2e/%E7%AC%AC6%E7%AB%A0-%E5%85%83%E7%B4%A0%E3%81%AE%E9%9B%BB%E5%AD%90%E6%A7%8B%E9%80%A0%E3%81%A8%E5%91%A8%E6%9C%9F%E7%9A%84%E6%80%A7%E8%B3%AA.html#%E9%87%8F%E5%AD%90%E8%AB%96%E3%81%AE%E7%99%BA%E5%B1%95 にあるように描かれるのが定番ではあるのですが、実際には電子雲の確率として示されるので、ちょっと旧来の書き方はいわゆる電子の粒のイメージが強くて、微妙なんですよね。確かに、私も「ダンベル型」になるという覚えがあるのですが、このイメージよりも単純な電子雲つまり数式そのものとしてはあくするようないい気がしています。まあ、どちらにせよ、模式図でしかないので、計算するとこうなるよという Python コードと図です。

# 水素原子の電子軌道の 3D グラフ
# s、p、f、d 軌道も追加

import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from scipy.special import sph_harm_y, genlaguerre, factorial
import matplotlib.colors as mcolors

# 日本語フォントの設定
plt.rcParams['font.sans-serif'] = ['MS Gothic', 'Yu Gothic', 'Meiryo']
plt.rcParams['axes.unicode_minus'] = False

# ボーア半径(原子単位)
a0 = 1.0

def radial_wave_function(r, n, l):
    """
    動径波動関数 R_nl(r)
    n: 主量子数
    l: 軌道角運動量量子数
    """
    rho = 2 * r / (n * a0)
    norm = np.sqrt((2 / (n * a0))**3 * factorial(n - l - 1) / (2 * n * factorial(n + l)))
    laguerre = genlaguerre(n - l - 1, 2 * l + 1)(rho)
    return norm * np.exp(-rho / 2) * rho**l * laguerre

def hydrogen_orbital(r, theta, phi, n, l, m):
    """
    水素原子の波動関数 ψ_nlm(r, θ, φ)
    n: 主量子数 (1, 2, 3, ...)
    l: 軌道角運動量量子数 (0, 1, ..., n-1)
    m: 磁気量子数 (-l, ..., 0, ..., l)
    """
    R_nl = radial_wave_function(r, n, l)
    Y_lm = sph_harm_y(l, m, theta, phi)
    return R_nl * Y_lm

def create_orbital_visualization(n, l, m, resolution=50, r_max=None):
    """
    軌道を3D可視化
    """
    if r_max is None:
        r_max = n**2 * a0 * 3  # 適切な範囲を設定
    
    # 球座標グリッド
    theta = np.linspace(0, np.pi, resolution)
    phi = np.linspace(0, 2*np.pi, resolution)
    THETA, PHI = np.meshgrid(theta, phi)
    
    # 確率密度の最大値を探すための半径
    r_values = np.linspace(0.1, r_max, 100)
    max_prob = 0
    optimal_r = r_max / 2
    
    for r_test in r_values:
        psi = hydrogen_orbital(r_test, np.pi/2, 0, n, l, m)
        prob = np.abs(psi)**2 * r_test**2
        if prob > max_prob:
            max_prob = prob
            optimal_r = r_test
    
    # 複数の半径で等値面をプロット
    r_surfaces = [optimal_r * factor for factor in [0.5, 0.8, 1.0]]
    
    return THETA, PHI, r_surfaces

# 各軌道の名前
orbital_names = {
    (1, 0, 0): '1s',
    (2, 0, 0): '2s',
    (2, 1, -1): '2p_y',
    (2, 1, 0): '2p_z',
    (2, 1, 1): '2p_x',
    (3, 0, 0): '3s',
    (3, 1, -1): '3p_y',
    (3, 1, 0): '3p_z',
    (3, 1, 1): '3p_x',
    (3, 2, -2): '3d_xy',
    (3, 2, -1): '3d_yz',
    (3, 2, 0): '3d_z²',
    (3, 2, 1): '3d_xz',
    (3, 2, 2): '3d_x²-y²',
    (4, 0, 0): '4s',
    (4, 1, 0): '4p_z',
    (4, 2, 0): '4d_z²',
    (4, 3, -3): '4f_y(3x²-y²)',
    (4, 3, -2): '4f_xyz',
    (4, 3, -1): '4f_yz²',
    (4, 3, 0): '4f_z³',
    (4, 3, 1): '4f_xz²',
    (4, 3, 2): '4f_z(x²-y²)',
    (4, 3, 3): '4f_x(x²-3y²)',
}

# 可視化する軌道(s, p, d, f 軌道を含む)
orbitals_to_plot = [
    (1, 0, 0),   # 1s
    (2, 1, 1),   # 2px
    (3, 2, 1),   # 3dxz
    (4, 3, 1),   # 4fxz²
]

fig = plt.figure(figsize=(20, 16))

for idx, (n, l, m) in enumerate(orbitals_to_plot, 1):
    ax = fig.add_subplot(3, 5, idx, projection='3d')
    
    THETA, PHI, r_surfaces = create_orbital_visualization(n, l, m, resolution=60)
    
    # 各半径で等値面をプロット
    for i, r in enumerate(r_surfaces):
        # 直交座標に変換
        X = r * np.sin(THETA) * np.cos(PHI)
        Y = r * np.sin(THETA) * np.sin(PHI)
        Z = r * np.cos(THETA)
        
        # 波動関数を計算
        psi = hydrogen_orbital(r, THETA, PHI, n, l, m)
        
        # 確率密度(実部の符号で色分け)
        prob = np.abs(psi)**2
        phase = np.angle(psi)
        colors = np.real(psi)
        
        # 正負で色分け(vmin < vcenter < vmax を保証)
        vmin, vmax = colors.min(), colors.max()
        if vmin >= 0:
            vmin = -1e-10
        if vmax <= 0:
            vmax = 1e-10
        norm = mcolors.TwoSlopeNorm(vmin=vmin, vcenter=0, vmax=vmax)
        
        surf = ax.plot_surface(X, Y, Z, facecolors=plt.cm.RdBu(norm(colors)),
                              alpha=0.7 - i*0.2, shade=True, 
                              linewidth=0, antialiased=True)
    
    # 軸設定
    orbital_name = orbital_names.get((n, l, m), f'{n},{l},{m}')
    ax.set_title(f'{orbital_name} (n={n}, l={l}, m={m})', fontsize=12, fontweight='bold')
    ax.set_xlabel('x (a₀)')
    ax.set_ylabel('y (a₀)')
    ax.set_zlabel('z (a₀)')
    
    # 範囲を統一
    max_range = n**2 * a0 * 2
    ax.set_xlim(-max_range, max_range)
    ax.set_ylim(-max_range, max_range)
    ax.set_zlim(-max_range, max_range)
    
    # 視点を調整
    ax.view_init(elev=20, azim=45)
    
    # グリッドを薄く
    ax.grid(True, alpha=0.2)

plt.tight_layout()
plt.savefig('水素原子の電子軌道.png', dpi=300, bbox_inches='tight')
plt.show()

# 軌道の特徴を出力
print("=" * 60)
print("水素原子の電子軌道の特徴")
print("=" * 60)

for n, l, m in orbitals_to_plot:
    orbital_name = orbital_names.get((n, l, m), f'{n},{l},{m}')
    
    # 動径波動関数の最大値の位置(最も確率の高い半径)
    r_values = np.linspace(0.01, n**2 * a0 * 3, 1000)
    radial_prob = [radial_wave_function(r, n, l)**2 * r**2 for r in r_values]
    max_idx = np.argmax(radial_prob)
    r_max_prob = r_values[max_idx]
    
    print(f"\n{orbital_name}軌道:")
    print(f"  主量子数 n = {n}")
    print(f"  角運動量量子数 l = {l} ({'s' if l==0 else 'p' if l==1 else 'd' if l==2 else 'f'}軌道)")
    print(f"  磁気量子数 m = {m}")
    print(f"  最大確率密度の半径: {r_max_prob:.3f} a₀")
    print(f"  エネルギー準位: E_{n} = -13.6/{n}² = {-13.6/n**2:.3f} eV")

print("\n" + "=" * 60)
print("凡例:")
print("  赤色: 波動関数が正")
print("  青色: 波動関数が負")
print("  透明度: 外側ほど薄く表示")
print("=" * 60)

元ツイ主が理系なのか文系なのかわからないのですが、、数学的・物理的に正確な図を求める場合には、画像AIに頼るのではなく、いったんプログラムコードで正確に描画するほうが正しい図が得られるよ、という老婆心であります。

カテゴリー: 開発 | 数学的な問題について画像AIによる嘘を避ける方法 はコメントを受け付けていません