TeXで起案する公文書・公用文書(kianマクロ)

技術メモ

技術(主にIT技術)について,気が付いたことなどを記録したメモです。

ネットで検索しても出てこないような少し偏った話題を公開するようにしています。

目次

FFmpegをShell Scriptで使ったら予想外の結果になり,はまった話

複数の動画を一気に編集しようとして,FFmpegShell Scriptで使ったら予想外の結果になり,はまった話です。

次のようなShell Scriptを作成し,"a.mp4","b.mp4"及び"c.mp4"を用意して,実行します。


#!/bin/sh

printf %b 'a.mp4\nb.mp4\nc.mp4\n' | \
while read i; do
    j="`basename $i .mp4`.avi"
    ffmpeg -i $i $j
done

実行したところ,"a.avi"は作成されますが,"b.avi"と"c.avi"は作成されません。

原因は,FFmpegが標準入力を読み込むためだそうです。

ちなみに,リダイレクトで標準入力を指定してやれば,ちゃんと動くようになります。


#!/bin/sh

printf %b 'a.mp4\nb.mp4\nc.mp4\n' | \
while read i; do
    j="`basename $i .mp4`.avi"
    ffmpeg -i $i $j < /dev/null
done

このような変換ソフトで,実行中に標準入力を読み込むというのは,あまり例がないため,何が悪いのかをなかなか理解できず,相当の時間を要しました。

TeXの条件分岐で予想外の結果になり,はまった話

今回は,TeXの条件分岐で予想外の結果になり,はまった話です。

TeXは組版ソフトウェアで,数式がきれいに作成できるので,学術論文などの作成に使われています。私は,業務で使う文書は原則として全てTeXで書いています。ただし,生のTeX(plain TeX)は使いにくいので,ほとんどの方がそうするように,t私もマクロ集を加えたLaTeXを使っています。表計算が必要な場合も,テキストファイルで入力データを作成し,これをPythonのスクリプトで計算し,計算結果をLaTeXで処理して文書にしています。当サイトで公開している"kianマクロ"は,LaTeXで公文書や公用文書を作るためのマクロ集です。

問題点を理解していただくための簡単な例として,次のような内容のファイル"test1.tex"を作成します。


% test1.tex
% 予想外の結果になる
\documentclass{jsarticle}

\newcount\a\a=1
\newcount\b\b=0
\newcount\c\c=0

\begin{document}

\ifnum\a=1\b=1\else\b=2\fi%  (式1)\a=1なので,\b=1になる
\ifnum\b=1\c=1\else\c=2\fi%  (式2)\b=1なので,\c=1になる
c=\the\c%                    よって,「c=1」が表示されるはず

\end{document}

次のコマンドで"test1.pdf"を作成します。


platex test1.tex
dvipdfmx test1.dvi

できた"test1.pdf"の内容を確認すると,なぜか,次のようになっています。


c=2

この現象に遭遇したのは,もっと複雑なマクロの中だったので,どこに原因があるのかが分からず,かなりの時間をかけて悩みました。

ネットの情報を参照しつつ,上記のような単純化したファイルで試して,やっと,TeXの仕様であることに気が付きました。

TeXは先読みすることがあり,(式1)を読んだときに,次の(式2)まで読み込んでしまうそうです。すなわち,\b=0の状態で(式2)を読み込んでしまうため,\c=2になってしまうそうです。

これを防止するためには,(式1)の後ろの%を外すか,\relax又は{}を入れて,先読みが及ばないようにすることです。ただし,前者については,変な空白が入る場合があり,止めておいた方が無難です。

次のコードであれば,ちゃんとc=1になります。


% test2.tex
% 予想通りの結果になる
\documentclass{jsarticle}

\newcount\a\a=1
\newcount\b\b=0
\newcount\c\c=0

\begin{document}

\ifnum\a=1\b=1\else\b=2\fi\relax%  (式1)\a=1なので,\b=1になる
\ifnum\b=1\c=1\else\c=2\fi%        (式2)\b=1なので,\c=1になる
c=\the\c%                          よって,「c=1」が表示されるはず

\end{document}

同様の問題は,次のようなコードでも起きます。


% test3.tex
% 予想外の結果になる
\documentclass{jsarticle}

\newcount\a\a=0
\newcount\b\b=0

\begin{document}

\advance\a by 1%             (式3)0に1を足して,\a=1になる
\ifnum\a=0\b=0\else\b=1\fi%  (式4)\a=1なので,\b=1になる
b=\the\b%                    よって,「b=1」が表示されるはず

\end{document}

できた"test3.pdf"の内容を確認すると,なぜか,次のようになっています。


b=0

これも,(式3)の後に\relax又は{}を入れれば,正しい結果が得られます。

なお,以上はTeXの書式ですが,これをLaTeXの書式にすれば,\relax又は{}を入れなくても,正しい結果が得られます。


% test4.tex
% 予想通りの結果になる
\documentclass{jsarticle}

\newcounter{a}\setcounter{a}{1}
\newcounter{b}\setcounter{b}{0}
\newcounter{c}\setcounter{c}{0}

\begin{document}

\ifnum\value{a}=1\setcounter{b}{1}\else\setcounter{b}{2}\fi% (式1)\a=1なので,\b=1になる
\ifnum\value{b}=1\setcounter{c}{1}\else\setcounter{c}{2}\fi% (式2)\b=1なので,\c=1になる
c=\arabic{c}%                                                よって,「c=1」が表示されるはず

\end{document}

LaTeXのマクロはLaTeXで書くべきだと教わったことがあったような気がしますが,こういうことかと納得しました。

ハノイの塔方式からデータのバックアップ方式を一般化する話

以前の回でハノイの塔方式のデータのバックアップが公比1/2の等比級数に理論的背景を持っていることをご説明しました。

実際に,2年間,ハノイの塔方式でバックアップを取ってみます。

1年目01月01日
Aフォルダにデータをコピー
1年目01月02日
Bフォルダにデータをコピー
1年目01月03日
Aフォルダにデータをコピー
1年目01月04日
Cフォルダにデータをコピー
1年目01月05日
Aフォルダにデータをコピー
1年目01月06日
Bフォルダにデータをコピー
1年目01月07日
Aフォルダにデータをコピー
1年目01月08日
Dフォルダにデータをコピー
1年目01月09日
Aフォルダにデータをコピー
1年目01月10日
Bフォルダにデータをコピー
1年目01月11日
Aフォルダにデータをコピー
1年目01月12日
Cフォルダにデータをコピー
1年目01月13日
Aフォルダにデータをコピー
 ⋮
 ⋮
2年目12月28日
Aフォルダにデータをコピー
2年目12月29日
Dフォルダにデータをコピー
2年目12月30日
Aフォルダにデータをコピー
2年目12月31日
Bフォルダにデータをコピー

最終的に残っているバックアップは,次のとおりです。

1年目09月13日(256日目)
Iフォルダ
2年目05月27日(512日目)
Jフォルダ
2年目10月02日(640日目)
Hフォルダ
2年目11月03日(672日目)
Fフォルダ
2年目12月05日(704日目)
Gフォルダ
2年目12月21日(720日目)
Eフォルダ
2年目12月25日(724日目)
Cフォルダ
2年目12月29日(728日目)
Dフォルダ
2年目12月30日(729日目)
Aフォルダ
2年目12月31日(730日目)
Bフォルダ

これを見ると,1年目のバックアップが1つしか残っていませんし,2年目の12月のバックアップの割合が60%もあり,最近のデータしか残っていません。これでは,気が付かないうちにデータが損傷していたような場合に,対処が困難だと思われます。

そこで,ハノイの塔方式を一般化して,もう少し古いデータも残すことができないかを考えます。

まずはおさらいですが,ハノイの塔方式は,公比1/2の等比級数が1に収束することを利用し,バックアップ回数全体を1として,Aフォルダに1/2を,Bフォルダに1/4を,Cフォルダに1/8を…を割り当てていました。


1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128 + 1/256 + 1/512 + 1/512 = 1

この例では,等比級数を使って,バックアップの方式を決めましたが,実は,合計が1になるように分数の和を作れば,等比級数でなくてもバックアップの方式を作ることができます。

例えば,次の分数の和を考えます。


1/2 + 1/3 + 1/6
  = 3/6 + 2/6 + 1/6
  = 1

そして,"1/2"をAフォルダ,"1/3"をBフォルダ,"1/6"をCフォルダに対応させ,Aフォルダは2日に1回,Bフォルダは3日に1回,Cフォルダは6日に1回,バックアップを取ります。この分数の和は,通分すると分母が6になりますので,6回のバックアップを1セットになります。

具体的には,次のようになります。

1日目
Aフォルダにデータをコピー
2日目
Bフォルダにデータをコピー
3日目
Aフォルダにデータをコピー
4日目
Bフォルダにデータをコピー
5日目
Aフォルダにデータをコピー
6日目
Cフォルダにデータをコピー
(以下,繰返し)

では,次の分数の和はどうでしょうか。


2/3 + 2/6
  = 4/6 + 2/6
  = 1

これは次のように考えます。

1つの分数につき,分子の数だけフォルダを用意します。具体的には,"2/3"をA1フォルダとA2フォルダに,"2/6"をB1フォルダとB2フォルダに対応させ,A1フォルダとA2フォルダは3日に1回,B1フォルダとB2フォルダは6日に1回,バックアップを取ります。この分数の和も,通分すると分母が6になりますので,6回のバックアップを1セットになります。

具体的には,次のようになります。

1日目
A1フォルダにデータをコピー
2日目
A2フォルダにデータをコピー
3日目
B1フォルダにデータをコピー
4日目
A1フォルダにデータをコピー
5日目
A2フォルダにデータをコピー
6日目
B2フォルダにデータをコピー
(以下,繰返し)

1つの分数につき,分子の数だけフォルダを用意するので,"1/2"と"2/4"は別物だと考えます。"1/2"は,Aフォルダの1つだけを用意し,2日に1回,バックアップを取ります。"2/4"は,A1フォルダとA2フォルダの2つを用意し,それぞれ4日に1回,バックアップを取ります。

そうすると,次のような分数の和も考えられます。


1/1.5 + 2/6
  = 4/6 + 2/6
  = 1

"1/1.5"は,Aフォルダの1つだけを用意し,3日に2回,バックアップを取ります。そのため,次のようになります。

1日目
Aフォルダにデータをコピー
2日目
Aフォルダにデータをコピー
3日目
B1フォルダにデータをコピー
4日目
Aフォルダにデータをコピー
5日目
Aフォルダにデータをコピー
6日目
B2フォルダにデータをコピー
(以下,繰返し)

ここまで,分数の和からバックアップのバックアップの方式を作る方法をご説明しました。

この方法を用いて,様々な分数の和を検討しましたので,いくつかご紹介したいと思います。

まずは,真っ先に思い付いたのが,公比1/3の等比級数に用いたバックアップ方式です。


2/3 + 2/9 + 2/27 + 2/81 + 2/243 + 2/729 + 1/729 = 1

公比1/3の無限等比級数は1/2に収束しますので,全体を2倍し,分子が2になっています。


1/3 + 1/9 + 1/27 + 1/81 + 1/243 + 1/729 + … = 1/2

具体的には,次のようにバックアップを取ることになります。

1月01日
A1フォルダにデータをコピー
1月02日
A2フォルダにデータをコピー
1月03日
B1フォルダにデータをコピー
1月04日
A1フォルダにデータをコピー
1月05日
A2フォルダにデータをコピー
1月06日
B2フォルダにデータをコピー
1月07日
A1フォルダにデータをコピー
1月08日
A2フォルダにデータをコピー
1月09日
C1フォルダにデータをコピー
1月10日
A1フォルダにデータをコピー
1月11日
A2フォルダにデータをコピー
1月12日
B1フォルダにデータをコピー
1月13日
A1フォルダにデータをコピー
 ⋮
 ⋮

最終的に残っているバックアップは,次のとおりです。

1年目08月31日(243日目)
F1フォルダ
2年目05月01日(486日目)
F2フォルダ
2年目07月21日(567日目)
E1フォルダ
2年目10月10日(648日目)
E2フォルダ
2年目11月06日(675日目)
D1フォルダ
2年目12月03日(702日目)
D2フォルダ
2年目12月12日(711日目)
C1フォルダ
2年目12月21日(720日目)
C2フォルダ
2年目12月24日(723日目)
B1フォルダ
2年目12月27日(726日目)
B2フォルダ
2年目12月29日(728日目)
A2フォルダ
2年目12月30日(729日目)
G1フォルダ
2年目12月31日(730日目)
A1フォルダ

公比1/2の等比級数方式(ハノイの塔方式)と比べて,あまり状況が改善していません。

等比級数方式は,後ろの項になるに従い(フォルダ名がAからZに向かうに従い),バックアップの間隔が指数関数的に増加するので,古いデータが残りにくいのだと思われます。

そこで,公比に相当する比率を変化させてみます。


1/2 + 2/(2×3) + 3/(2×3×4) + 4/(2×3×4×5) + 5/(2×3×4×5×6) + 1/(2×3×4×5×6) = 1

公比に相当する比率が徐々に増加していますので,等比級数方式よりも状況が悪化するように思われるかもしれませんが,そうではありません。例えば,2項目の"2/(2×3)"は,バックアップの間隔が1項目"1/2"に比べて3倍になっていますが,2つのフォルダでバックアップを残しますので,バックアップの間隔はおおよそ2/3倍にしかなっていません。

具体的には,次のようにバックアップを取ることになります。

1月01日
A1フォルダにデータをコピー
1月02日
B1フォルダにデータをコピー
1月03日
A1フォルダにデータをコピー
1月04日
B2フォルダにデータをコピー
1月05日
A1フォルダにデータをコピー
1月06日
C1フォルダにデータをコピー
1月07日
A1フォルダにデータをコピー
1月08日
B1フォルダにデータをコピー
1月09日
A1フォルダにデータをコピー
1月10日
B2フォルダにデータをコピー
1月11日
A1フォルダにデータをコピー
1月12日
C2フォルダにデータをコピー
1月13日
A1フォルダにデータをコピー
 ⋮
 ⋮

最終的に残っているバックアップは,次のとおりです。

1年目04月30日(120日目)
E1フォルダ
1年目08月28日(240日目)
E2フォルダ
1年目12月26日(360日目)
E3フォルダ
2年目04月25日(480日目)
E4フォルダ
2年目08月23日(600日目)
E5フォルダ
2年目09月16日(624日目)
D1フォルダ
2年目10月10日(648日目)
D2フォルダ
2年目11月03日(672日目)
D3フォルダ
2年目11月27日(696日目)
D4フォルダ
2年目12月09日(708日目)
C2フォルダ
2年目12月15日(714日目)
C3フォルダ
2年目12月21日(720日目)
F1フォルダ
2年目12月27日(726日目)
C1フォルダ
2年目12月29日(728日目)
B1フォルダ
2年目12月30日(729日目)
A1フォルダ
2年目12月31日(730日目)
B2フォルダ

フォルダの個数が16個まで増えてしまいましたが,1年目のバックアップが3つ残っていますし,2年目の12月のバックアップの割合も約44%に押さえることができており,ハノイの塔方式に比べてだいぶ改善したように思います。

そこで,比率をもっと大きく変化させてみたらどうかと思い,次の分数の和を試してみました。


1/2 + 3/(2×4) + 7/(2×4×8) + 15/(2×4×8×16) + 1/(2×4×8×16) = 1

最終的に残っているバックアップは,次のとおりです。

1年目03月05日(064日目)
D1フォルダ
1年目05月08日(128日目)
D2フォルダ
1年目07月11日(192日目)
D3フォルダ
1年目09月13日(256日目)
D4フォルダ
1年目11月16日(320日目)
D5フォルダ
2年目01月19日(384日目)
D6フォルダ
2年目03月24日(448日目)
D7フォルダ
2年目05月27日(512日目)
D8フォルダ
2年目07月30日(576日目)
D9フォルダ
2年目10月02日(640日目)
DAフォルダ
2年目11月03日(672日目)
C4フォルダ
2年目11月11日(680日目)
C5フォルダ
2年目11月19日(688日目)
C6フォルダ
2年目11月27日(696日目)
C7フォルダ
2年目12月05日(704日目)
DBフォルダ
2年目12月13日(712日目)
C1フォルダ
2年目12月21日(720日目)
C2フォルダ
2年目12月25日(724日目)
B2フォルダ
2年目12月27日(726日目)
B3フォルダ
2年目12月29日(728日目)
C3フォルダ
2年目12月30日(729日目)
A1フォルダ
2年目12月31日(730日目)
B1フォルダ

1年目のバックアップが5つに増え,2年目の12月のバックアップの割合も約32%まで減りましたが,フォルダの個数が22個まで増えてしまいました。

最後に,暦を基準にした次の分数の和を紹介して,終わります。


6/7 + 3/(7×4) + 2/(7×4×3) + 3/(7×4×3×4) + 2/(7×4×3×4×) + 1/(7×4×3×4×) = 1

1項目が週に,2項目が月に,3項目が四季に,4項目が年に対応しています。ただし,このバックアップは48週周期(4×3×4週周期)で回るため,1年間で約1か月(4週間と1日)のずれが生じます。

最終的に残っているバックアップは,次のとおりです。

1年目12月02日(336日目)
E1フォルダ
2年目02月24日(420日目)
D1フォルダ
2年目05月19日(504日目)
D2フォルダ
2年目08月11日(588日目)
D3フォルダ
2年目11月03日(672日目)
E2フォルダ
2年目12月01日(700日目)
C1フォルダ
2年目12月08日(707日目)
B1フォルダ
2年目12月15日(714日目)
B2フォルダ
2年目12月22日(721日目)
B3フォルダ
2年目12月25日(724日目)
A3フォルダ
2年目12月26日(725日目)
A4フォルダ
2年目12月27日(726日目)
A5フォルダ
2年目12月28日(727日目)
A6フォルダ
2年目12月29日(728日目)
C2フォルダ
2年目12月30日(729日目)
A1フォルダ
2年目12月31日(730日目)
A2フォルダ

フォルダの個数が16個まで増えた一方で,1年目のバックアップが1つしか残っておらず,しかもそれは12月のものですし,2年目の12月のバックアップの割合が約69%もあり,当初の目的に反する結果になってしまいました。

ただし,1項目が週を基準にしていますので,1週間分のバックアップが常に残っているというメリットはあります。

「モンテカルロ法で次元の呪いを体験する」という記事を実際に確かめた話

前回に引き続き,高次元空間のお話しです。

ネットで「モンテカルロ法で次元の呪いを体験する」という記事(http://prunus1350.hatenablog.com/entry/2015/01/19/193002)を読みました。

高次元になると,超立方体内部の体積に比べ超球面内部の体積が極端に小さくなるため,モンテカルロ法で円周率を求めることができなくなるというのが,この記事の結論です。

面白そうなので,超立方体内部の体積に対する超球面内部の体積の比を求めるプログラムを,さっそくPythonで実装してみました。


#!/usr/bin/python3
import sys
import random

dim = 2
if(len(sys.argv) == 2):
    dim = sys.argv[1]

times = 100000000

in_sphere = 0
for i in range(times):
    sq_length = 0
    for d in range(int(dim)):
        x = (random.random())
        sq_length += x*x
    if(sq_length <= 1):
        in_sphere += 1
print(str(in_sphere) + '/' + str(times))

これを"supervolume.py"という名前で保存し,"chmod +x supervolume.py"を実行して実行権限を付与します。

まずは,試しに2次元で実行してみます。2次元での実行は"./supervolume.py"とするだけです。

ちょっと時間がかかりましたが,結果は"78538501/100000000"と表示されました。これは,半径1の円の4分の1の面積に相当するので,これを4倍して,円周率は3.14154004と出ました。正確な値は3.14159265…ですから,まあまあの値だと思います。

作成したプログラムがまあまあ信頼できることが分かったので,さっそく記事で試していた最高の次元である15次元で試してみます。15次元での実行は"./supervolume.py 15"とするだけです。

試してみると,1回目が"1171/100000000",2回目が"1230/100000000",3回目が"1192/100000000",4回目が"1192/100000000",5回目が"1139/100000000"でした。当たり前のことですが,乱数の自乗を次元の数だけ足すので,次元が高くなるに連れて超球面内部に当たる確率が下がっていきます。

確かに,2次元の場合に比べて15次元の場合は,1万分の1以下に減少しています。

そこで,この結果を用いて,円周率を計算してみます。超球の体積から,理論的にはπ7×7!/15!=.00001164…になるそうなので,これから逆算します。

その結果は,2回目の"1230/100000000"を用いた場合は3.16641431…,5回目の"1139/100000000"も用いた場合は3.13183570…になりました。1億回も試したためか,思ったほど結果は悪くはないようです。

そこで,さらに次元を上げて,20次元で試してみたところ,1回目が"4/100000000",2回目が"1/100000000",3回目が"3/100000000",4回目が"2/100000000",5回目が"3/100000000"でした。確かにここまでばらつきが大きいと,まともに円周率は求められないと思われます。

確かに,モンテカルロ法を使って高次元空間の超球面内部の体積を求め,円周率を求めるのは難しいようです。

「高次元空間中の正規分布は超球面状に分布する」という記事を理解した話

ネットで「高次元空間中の正規分布は超球面状に分布する」という記事(https://qiita.com/ae14watanabe/items/ef5689d40a0fbee957ea)を読みました。

正規分布は中央に山があり山から離れるほど値が小さくなるので,高次元空間になった場合でも,直感的に超球面の内部に分布しそうですが,実際には超球面の表面に分布するというのが,この記事の結論です。

そこで,なぜそうなるのかを,ちょっと考えてみることにします。

次の正規分布(標準正規分布)で2次元を考えます。


f(x) = 1/√(2π)×exp(-x2/2)

2次元では次のようになります。


f(x,y) = 1/(2π)×exp(-(x2+y2)/2)

これを極座標表示(r,θ)にすると,次のようになります。。


f(r,θ) = 1/(2π)×exp(-r2/2)

θ方向に1周積分します。


f(r) = ∫0 f(r,θ) rdθ = ∫0 1/(2π)×exp(-r2/2) rdθ = r×exp(-r2/2)

これがr一定の確率密度です。

これの増減を見るために,rで微分します。


f'(r) = (1-r2)×exp(-r2/2)

増減表は次のようになります。

r01
f'(r)1+0-0
f(r)01/√e0

確かに,f(r)は,原点付近ではなく,r=1で最大値を取ることが分かります。

もっと高次元を考えます。正規分布を使うと計算が難しいので,次のような簡単な分布を考えます。


x=-1になる確率: 1/4
x= 0になる確率: 1/2
x= 1になる確率: 1/4

同じような分布を2つ持ってきて,(x,y)を考えます。

全部で9通りの結果が考えられますが,そのうち原点からの距離r0となる場合は(0,0)の1通りでその確率は1/2×1/2=1/41となる場合は(-1,0), (1,0), (0,-1), (0,1)の4通りでその確率は1/2×1/4=1/8√2となる場合は(-1,-1), (1,-1), (-1,1), (1,1)の4通りでその確率は1/4×1/4=1/16です。

したがって,次のようになります。


【2次元の場合】
r=  0になる確率: 1/4
r=  1になる確率: 4/8  = 1/2
r=√2になる確率: 4/16 = 1/4

次に,同じような分布を3つ持ってきて,(x,y,z)を考えます。


【3次元の場合】
r=  0になる確率:  1/8
r=  1になる確率:  6/16 = 3/8
r=√2になる確率: 12/32 = 3/8
r=√3になる確率:  8/64 = 1/8

確かに,2次元と3次元の場合ですら,すでに原点からの距離が真ん中付近の確率が最大になっています。具体的には,2次元の場合はr=1の円周上が,3次元の場合はr=1r=√2の球面上が最大になっています。簡単に確かめられますが,正規分布の場合でもそうなります。

これを一般的に求めると,次のようになります。


【n次元の場合】
r=√iになる確率: (2i×nCi)×1/2n-i×1/4i = nCi×1/2n = n!/{(n-i)!×i!}×1/2n

ここで,2N次元の場合を考えてみます。


【2N次元の場合】
r=0になる確率:      1/22N
      ⋮
  (N-1個)
      ⋮
r=√Nになる確率:    2N!/(N!×N!)×1/22N
      ⋮
  (N-1個)
      ⋮
r=√(2N)になる確率: 1/22N

スターリングの近似により,十分大きなnに対してn!≃√(2πn)×(n/e)nが成り立つので,これを用いて整理すると次のようになります。


【2N次元の場合】
r=0になる確率:      1/22N
      ⋮
  (N-1個)
      ⋮
r=√Nになる確率:    2N!/(N!×N!)×1/22N ≃ 1/√(πN)
      ⋮
  (N-1個)
      ⋮
r=√(2N)になる確率: 1/22N

r=0r=√(2N)になる確率は指数関数で急速に減衰するのに対し,r=√Nになる確率は無理関数でゆっくりと減衰します。

考える確率の数(半径rの種類の数)が2N+1個で,次元に比例して(N倍で)増加するため,1つの確率(r=√iになる確率)は,平均的には次元に反比例して(1/N倍で)減少することを考えれば,むしろr=√Nの超球面付近に確率が集中していっているといえます。

ただし,注意が必要です。次元を固定して考えた場合,個々の点の確率,すなわち(x1,x2,x3,…)となる確率については,原点(0,0,0,…)が最大で,原点から離れるにつれて減衰します。これは完全に直感と合致します。原点付近に濃い霧がかかっており,周辺に行くに従い晴れ上がっていくイメージです。

ところが,次元が高くなるにつれて,原点から離れるにつれて点の数が指数関数で急速に増大します。

原点から離れるにつれて減衰する個々の点の確率に,原点から離れるにつれて急速に増加する点の数を掛けあわせた結果,原点付近でも最遠部でもなく,両者の間の超球面(n次元空間中のrが一定の点の集合)の確率が最大になります。そして,その結果を無理矢理に平面に落とし込むと,ネットの記事のようなドーナツ型になります。

最後に,高次元になるにつれて確率の分布の幅が狭くなり,超球面付近のみに確率が集中する理由について考えます。同じ分布で高次元にするということは,同じ試行を繰り返すことに等しいため,偶然によるばらつきが小さくなっていきます。例えば,サイコロを6回振って,1と6が3回ずつ出ることは考えられますが,サイコロを6万回振って,1と6が3万回ずつ出ることはちょっと考えられません。サイコロを6万回振れば,全ての目がおよそ1万回くらいずつ出るはずです。このように,高次元になるにしたがい,偶然によるばらつきが小さくなり,超球面付近に確率が集中していくのです。

実は,このようなことは日常的に起こっています。例えば,プロ野球の試合です。1打席の平均打率は2割5分くらいなので,次のようになります。


1打席で安打でない確率: 3/4
1打席で安打を打つ確率: 1/4

2打席の場合と3打席の場合は,次のようになります。


【2打席の場合】
0安打の確率: 9/16
1安打の確率: 6/16
2安打の確率: 1/16


【3打席の場合】
0安打の確率: 27/64
1安打の確率: 27/64
2安打の確率:  9/64
3安打の確率:  1/64

これを一般的に求めると,次のようになります。


【n打席の場合】
i安打の確率: nCi×(3/4)n-i×(1/4)i

出塁すると打席の数が変わってしまいますが,仮に1試合を27打席だと仮定して計算してみます。四球や犠打を考慮に入れていませんので,非常に簡略化したシンプルなモデルでの計算です。


【1試合(27打席と仮定)の場合】
 0安打の確率:  0.04233% ← ノーヒットトーラン
 1安打の確率:  0.38098%
 2安打の確率:  1.65089%
 3安打の確率:  4.58581%
 4安打の確率:  9.17162%
 5安打の確率: 14.06316%
 6安打の確率: 17.18830%
 7安打の確率: 17.18830%
 8安打の確率: 14.32358%
 9安打の確率: 10.07956%
10安打の確率:  6.04773%
11安打の確率:  3.11550%
12安打の確率:  1.38467%
      ⋮

確かに,個々の打席は安打でない確率の方が高いです。しかし,試合全体を見れば,6から7安打の試合が最頻値で,4安打から9安打の試合の確率で8割を超えています。0安打,すなわちノーヒットノーラン(ただし,四球や死球を考えていないので,完全試合と区別できていません。)は,0.04233%,2362試合に1試合という低確率です。適当な計算でしたが,まあまあ感覚に合致しているのではないでしょうか?

「高次元空間中の正規分布は超球面状に分布する」というショッキングなタイトルにビックリしましたが,よくよく考えてみれば自然な結論であることが理解できました。ネットの記事に感謝です。

ハノイの塔方式のデータのバックアップが公比1/2の等比級数に理論的背景を持っている話

コンピューターを使っていると,うっかりファイルを消去してしまうことがあります。また,何らかの原因でファイルが壊れてしまい,アプリケーションソフトから開けなくなってしまうことがあります。さらに,HDD(ハードディスクドライブ)SSD(ソリッドステートドライブ)などの補助記憶装置が故障してしまい,データが取り出せなくなることもあります。

そこで,重要なデータを扱う際は,そのようなトラブルに備えて,日頃からバックアップを取っておく必要があります。

バックアップは,たくさん取っておくことに越したことはありません。可能であれば,毎日,バックアップを取って,そのデータをすべて残しておくべきです。

しかし,バックアップを保存するメディア(HDDやSSDなど)の容量にも限りがあります。

そこで,限られたメディア容量で,要領よくバックアップを取ることを考えざるを得ません。

例えば,AからJまでの10個のフォルダを作って,次のようにバックアップを取ります。

1日目
Aフォルダにデータをコピー
2日目
Bフォルダにデータをコピー
3日目
Cフォルダにデータをコピー
4日目
Dフォルダにデータをコピー
5日目
Eフォルダにデータをコピー
6日目
Fフォルダにデータをコピー
7日目
Gフォルダにデータをコピー
8日目
Hフォルダにデータをコピー
9日目
Iフォルダにデータをコピー
10日目
Jフォルダにデータをコピー
11日目
Aフォルダにデータをコピー
12日目
Bフォルダにデータをコピー
13日目
Cフォルダにデータをコピー
 ⋮
 ⋮

もちろん,11日目以降は,フォルダ内の古いデータを消してから,データをコピーするようにします。

この場合,メディアの容量は,バックアップを取る必要のあるデータのサイズの約10倍も必要になります。

それでも,これだと10日前までのデータしか残りません。ファイルをうっかり削除して,気が付かないまま2週間を経過してしまった場合には,バックアップが残っていません。

そこで,毎日データを取るのではなく,月に1回にしてみます。

1か月目
Aフォルダにデータをコピー
2か月目
Bフォルダにデータをコピー
3か月目
Cフォルダにデータをコピー
4か月目
Dフォルダにデータをコピー
5か月目
Eフォルダにデータをコピー
6か月目
Fフォルダにデータをコピー
7か月目
Gフォルダにデータをコピー
8か月目
Hフォルダにデータをコピー
9か月目
Iフォルダにデータをコピー
10か月目
Jフォルダにデータをコピー
11か月目
Aフォルダにデータをコピー
12か月目
Bフォルダにデータをコピー
13か月目
Cフォルダにデータをコピー
 ⋮
 ⋮

この場合,10か月前のデータは残りますが,3日前に作成したファイルをうっかり削除した場合には,バックアップが残っていない可能性が大きいです。

そこで,両者のバランスを考えて,限られたデータ容量の中で,昔のデータのバックアップも少し残しながら,最近のデータのバックアップを小まめに残すのが理想的です。バックアップは時間が経つにつれて価値が下がっていくので,この考え方は非常に合理的です。

具体的には,Aフォルダは非常に小まめに,Bフォルダはまあまあ小まめに,Cフォルダはちょっぴり小まめに…,Iフォルダはまあまあ希に,Jフォルダは非常に希にバックアップを取ります。

そのために,1/2の有限等比級数を考えます。


1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128 + 1/256 + 1/512 + 1/512
    = 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128 + 1/256 + 1/256
    = 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/128 + 1/128
    = 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 + 1/64
    = 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/32
    = 1/2 + 1/4 + 1/8 + 1/16 + 1/16
    = 1/2 + 1/4 + 1/8 + 1/8
    = 1/2 + 1/4 + 1/4
    = 1/2 + 1/2
    = 1

そして,"1/2"をAフォルダに,"1/4"をBフォルダに,"1/8"をCフォルダに…,"1/512"をIフォルダに,最後の"1/512"をJフォルダに対応させて考えてみます。すなわち,Aフォルダは2日に1回,Bフォルダは4日に1回,Cフォルダは8日に1回…,Iフォルダは512日に1回,Jフォルダは1024×2=512日に1回,バックアップを取ることを考えてみます。

1日目
Aフォルダにデータをコピー
2日目
Bフォルダにデータをコピー
3日目
Aフォルダにデータをコピー
4日目
Cフォルダにデータをコピー
5日目
Aフォルダにデータをコピー
6日目
Bフォルダにデータをコピー
7日目
Aフォルダにデータをコピー
8日目
Dフォルダにデータをコピー
9日目
Aフォルダにデータをコピー
10日目
Bフォルダにデータをコピー
11日目
Aフォルダにデータをコピー
12日目
Cフォルダにデータをコピー
13日目
Aフォルダにデータをコピー
 ⋮
 ⋮

こうすると,最も新しいデータは1日前のデータ,次に新しいデータは2日前のデータになります。その後は3ないし4日前のデータ,5ないし8日前のデータ,9ないし16日前のデータとなり,最も古いデータは255ないし512日前のデータになります。

すなわち,新しいデータはたくさん,古いデータはちょっとだけ,バックアップとして残すことができるのです。

このアイデアを思い付いたときには,素晴らしいアイデアを思い付いてしまったと興奮して,実装するShell Scriptを一気に書き上げました。

ところで,話は少し変わりますが,ハノイの塔というパズルがあります。小さな円盤を大きいものから重ねて置き,小さな円盤の上に大きな円盤が乗らないようにしながら,円盤を移動させるパズルです。その名前は,「アジアのある寺院に64枚の円盤の塔があって,昼夜を通して僧侶が円盤を移し替えており,全ての円盤の移替えが終わったときに,世界は崩壊し終焉を迎える。」という物騒な伝説に由来するそうです(ただし,最低でも5845億年はかかるそうで,しばらくは世界の終焉はなさそうです。)。下に4枚のハノイの塔の例を載せますが,詳しい移動のさせ方はWikipedia等をご覧ください。

(ハノイの塔)

10段のハノイの塔を考え,一番小さい円盤をA円盤,次に小さい円盤をB円盤,その次に小さい円盤をC円盤…,一番大きい円盤をJ円盤とします。また,一番左の杭をア杭,真中の杭をイ杭,一番右の杭をウ杭とし,最初はア杭にすべての円盤があります。そうすると,動かし方は次のようになります。

1回目
A円盤にイ杭に移動
2回目
B円盤にウ杭に移動
3回目
A円盤にウ杭に移動
4回目
C円盤にイ杭に移動
5回目
A円盤にア杭に移動
6回目
B円盤にイ杭に移動
7回目
A円盤にイ杭に移動
8回目
D円盤にウ杭に移動
9回目
A円盤にウ杭に移動
10回目
B円盤にア杭に移動
11回目
A円盤にア杭に移動
12回目
C円盤にウ杭に移動
13回目
A円盤にイ杭に移動
 ⋮
 ⋮

どこの杭に移動させるのかは置いておいて,移動させる円盤のみに注目します。

そうすると,移動させる円盤の順番は「A→B→A→C→A→B→A→D→A→B→A→C→A」になっています。

この順番,実は,先ほどのバックアップのフォルダの順番と完全に一致しています。

そこで,先ほどのバックアップ方式は「ハノイの塔方式」と呼ばれているそうです。

私が思い付いたと思ったバックアップ方式は,とっくの昔に誰かが考えていたもので,車輪の再発明に過ぎなかったというのが,この話のオチです。本当にがっかりしました…。

実は,ハノイの塔方式という名前自体は以前から知っていましたし,1/2の等比級数を利用するという理論的背景をちゃんと理解した今は趣のある名前だと思いますが,当時は円盤を動かす順番でバックアップ取る変な方式という程度の認識しかありませんでした。

ハノイの塔方式のバックアップに関する記事は,ネット上にわずかしかありません。これは,趣のある名前からちょっとややこしい理論的背景に直感的に結びつきにくいことが原因ではないかと思います。しかし,ハノイの塔方式は非常に優れたアイデアですので,その理論的背景も含めて,多くの方に広く知っていただきたく,この記事を執筆いたしました。

また,後日,改めてハノイの塔方式でバックアップを取るパッケージアプリを探しましたが,Linux上では見付けることができませんでした。もしかして需要があるのではないかと思い,作成したShell Scriptを"bera(倍良)"という名前で公開しました。

追記

端末上でCtrl-;を使える理由の話

前回,端末上でCtrl-;(Ctrl(Contorl)を押しながら";"を押す。)を使えない話をしました。

確かに,通常の設定ではCtrl-;を使うことは出来ません。

しかし,";"キーはホームポジションにあり,Ctrl-;は非常に打ちやすいキー操作ですから,これを有効に使わない手はありません。

そこで,発想を転換して,以前の回で紹介した方法を使います。Ctrl-;に対応する制御文字が存在しないのであれば,Ctrl-;で別の制御文字を入力するように設定し,その制御文字に命令を割り当ててやればよいのです。

例えば,"^_"という制御文字をCtrl-;に割り当てます。なぜ,"^_"なのかの理由ですが,本来,"^_"を入力するためには,Ctrl-_(Ctrlを押しながら"_"を押す。)というキー操作をしなければなりませんが,"_"自体がShiftを押しながら"\"を押す必要があるため,Ctrl-_を入力するためには,CtrlとShiftを押しながら"\"キーを押すという複雑な操作をしなければなりません。そこで,使いにくいCtrl-_で入力される"^_"を,Ctrl-;に割り当ててやることにより,使いやすくしてやろうというわけです。

具体的には,Xtermであれば,ホームディレクトリーにある".Xresources"に次のように記載して,再起動すれば,Ctrl-;で"^_"を入力させることができます。


! ~/.Xresources
! Ctrl-;を"^_"に割り当てる設定
XTerm*VT100.translations: #override \
  Ctrl<Key>;: string("0x1F")

これだけでは,Ctrl-;を使うことはできません。端末上のアプリケーションの設定で,"^_"という制御文字に,機能を割り当ててやる必要があります。

例えば,端末上のシェルBashを使っている場合,"^_"にコマンドやファイルの補完を割り当てるには,ホームディレクトリーにある".bashrc"に次のように記載して,Bashを立ち上げます。


# ~/.bashrc
# "^_"を補完に割り当てる設定
bind '"\C-_": complete'

".Xresources"と".bashrc"をセットで設定することにより,Ctrl-;でコマンドやファイルの補完ができるようになりました。

ちなみに,割り当てることができる機能(組込みコマンド)は,次のコマンドで確認できます。なお,"bash>"はコマンドプロンプトですので,入力する必要はありません。


bash> help

また,端末上のシェルにZshを使っている場合,"^_"にコマンドやファイル補完を割り当てるには,ホームディレクトリーにある".zshrc"に次のように記載して,Zshを立ち上げます。


# ~/.zshrc
# "^_"を補完に割り当てる設定
bindkey '\C-_' complete-word

".Xresources"と".zshrc"をセットで設定することにより,Ctrl-;でコマンドやファイルの補完ができるようになりました。

ちなみに,割り当てることができる機能(widget)は,次のコマンドで確認できます。なお,"zsh>"はコマンドプロンプトですので,入力する必要はありません。


zsh> zle -al

端末上でCtrl-;を使えない理由の話

以前の回で,Xtermなどの端末上でCtrl-g(Ctrl(Control)を押しながら"g"を押す。)で割込信号を送る話をしました。

ところが,同じ方法で設定しようとしても,Ctrl-;(Ctrlを押しながら";"を押す。)で割込信号を送ることはできません。";"キーはホームポジションにあり,これを使えると非常に便利なのに,使えないのです。今回はその理由の話です。

一般的なコンピューター内部では,データは1byteという単位で扱われています。1byteとは,8桁の2進数(8bit)で,0から255までです。このうち,後半の128番から255番は,画像のデータを記録したり日本語の文字を表したりするのに使われています。前半の0番から127番までが,アルファベットを表したりコンピューターを制御したりするのに使われ,制御文字と呼びます。

今回問題になるのは,前半のアルファベットを表したりコンピューターを制御したりする部分ですので,前半の0番から127番を一覧表にしてみました。

  キーコード  キーコード   キーコード  キーコード
0^@0000000 64@1000000 43+0101011 107k1101011
1^A0000001 65A1000001 44'0101100 108l1101100
45-0101101 109m1101101
8^H0001000 72H1001000 46.0101110 110n1101110
47/0101111 111o1101111
26^Z0011010 90Z1011010 4800110000 112p1110000
27^[0011011 91[1011011 4910110001 113q1110001
28^\0011100 92\1011100 5020110010 114r1110010
29^]0011101 93]1011101 5130110011 115s1110011
30^^0011110 94^1011110 5240110100 116t1110100
31^_0011111 95_1011111 5350110101 117u1110101
32 0100000 96`1100000 5460110110 118v1110110
33!0100001 97a1100001 5570110111 119w1110111
34"0100010 98b1100010 5680111000 120x1111000
35#0100011 99c1100011 5790111001 121y1111001
36$0100100 100d1100100 58:0111010 122z1111010
37%0100101 101e1100101 59;0111011 123{1111011
38&0100110 102f1100110 60>0111100 124|1111100
39'0100111 103g1100111 61=0111101 125}1111101
40(0101000 104h1101000 62<0111110 126~1111110
41)0101001 105i1101001 63?0111111 127^?1111111
42*0101010 106j1101010 ※ 32番はスペース(半角スペース)です。

通常の文字として入力できるのは,32番のスペースから126番のチルダまでの95文字で,その中に小文字・大文字のアルファベット,数字,記号が含まれています。アルファベットの大文字が65番という中途半端な番号から始まり,大文字と小文字の間が少し番号が飛んでいます。なぜこんなにややこしい順番になっているかについては,"A","a"及び"1"を切りの良い数字(コードの下4桁がいずれも0001)に配置した結果だそうです。

残りの0番から31番までと127番までの33文字は,制御文字です(薄紫色の部分です。)。制御文字というのは,ベルを鳴らしたり,改行したりする制御のための特殊な文字です。例えば,8番の"^H"には1文字戻るという制御,すなわちBackSpaceが割り当てられています。そのため,端末上でBackSpaceキーを押すと,"^H"が入力されて,直前の1文字が消えます。他にも,9番の"^I"にはTabが割り当てられています。もっとも,このような例は例外的で,多くの制御文字は元々の意味とは無関係の機能を担っており,BackSpaceやTabのように専用のキーも存在しません。

いよいよ本題に入ります。

制御文字を入れるために考えられたのが,Ctrlキーです。専用のキーがなくても,Ctrlを押しながら同時にキーを押すことで入力するわけです。

Ctrlキーは,元々,コードの7桁目の0と1を入れ替えるためのキーでした。例えば,Ctrlを押しながらHを押すと,"H"のコード1001000の7桁目の1が0になってコード0001000になるため,結果として"^H"(すなわちBackSpace)が入力され,直前の1文字が消える仕組みになっていました(もしそうであるならば,単独で"h"キーを押すと"h",Shiftを押しながら"h"キーを押すと"H"なのだから,Ctrl-Shift-hで"^H"ではないかと思うのですが,その辺りは適当に設定されているようです。)。

2進数の7桁目は64の位ですから,Ctrlは,63番以下の文字と一緒に押すと64番ほど多い文字が,64番以上の文字と一緒に押すと64番ほど少ない文字が入力されることになります。上記の表は,分かりやすいように,64番違いの文字を隣同士に配置してあります。Ctrlを押すと,左から1番目と2番目が入れ替わり,3番目と4番目が入れ替わります,

そこで,改めて表を見ると,制御文字は最初の31文字と最後の1文字しかありません。そのため,Ctrlを押すことにより,制御文字を入力できる文字は限られてしまっています。例えば,35番の"#"のコードは0100011ですが,Ctrlにより7桁目の0を1にすると,99番の"c"のコード1100011になってしまい,制御文字を入力できません。

このように,Ctrlと同時押しで制御文字を入力できるのは,「@,AからZ,[,\,],^,_,?」の32文字しかないため,残りの文字は,Ctrlと同時押しをしたとしても,別の通常の文字になるだけで,意味がありません。59番の";"についても,Ctrlと同時押しをしたとしても,123番の"{"にしかなりません(もっとも,Ctrl-;を押しても"{"が入力されるわけではありませんので,その辺りは適当に設定されているようです。)。Ctrl-;にはこれに対応する制御文字が存在しないのです。

端末上でCtrl-;が使えないのは,このような理由です。

なお,コンピューターの進歩に伴い,Ctrlも元々の意味を失っており,Ctrl-h(Ctrlを押しながら"h"を押す。)を押しても必ずしもBackspaceにはなるとは限りません。Windowsでは,Ctrl-hに文字列を置換するという機能が割り当てられているそうです。

背景色が白でも黒でも目立つ鮮やかな色をたくさん見付けてEmacsなどをカラフルにする話

今回は少し長いお話です。

普段,あらゆる文書をEmacsで書いています。Emacsで文書を書いてTeXで処理して整形したり,Emacsで数値を入力してPythonのScriptで表計算をしたりしています。TeXやPythonの方が,小回りが利いて,商用ソフトよりも使い勝手が良いからです。

Emacsをお使いの方は,背景色を黒にしている方も多いと思います。その方が目に優しいからです。また,pLaTeXやPythonのキーワードに色を付けて,見やすくしている方も多いと思います。だいたい,こんな感じでしょうか。

(背景色黒のEmacsの前)

もっとも,背景色を黒にしていると,日光が当たったときに,非常に見えにくくなります。そこで,ノートパソコンを持ち歩いて使う場合などでは,背景色を白にした方が見やすいと思います。Emacsの背景色を白にしてみると,こんな感じになります。

(背景色白のEmacs前)

薄い色のキーワードが目立たなくなりました。このように,キーワードの色は,背景色によって,目立ったり,目立たなくなったりします。

そこで,当初は,背景色が濃い色の場合と薄い色の場合について,それぞれキーワードの色を調合して設定していました。少しずつ色を変化させて見やすいかどうかを確認してみるという作業を繰り返すのですが,やっとできたと思った色が別の色と似ていて区別が難しかったりして,手間のかかる作業の割に納得のいく色はなかなか見つかりませんでした。

あるとき,「計算により論理的に色を調合することができないだろうか?」と思い始めました。そして,「どうせ計算するならば背景色が白でも黒でも目立つ色にして,背景色による設定を統合させたい!」と思うようになりました。以下の文章は,その試行錯誤をした結果をまとめたメモです。

一般的に色はRGBという方式で表すことが多いと思います。これは,赤(Red),緑(Green),青(Blue)の各色について,0から255までの数値を指定することで,色を表すものです(光の三原色は,赤黄青ではなく,赤緑青です。)。


黒:(  0,   0,   0)
赤:(255,   0,   0)
緑:(  0, 255,   0)
青:(  0,   0, 255)
白:(255, 255, 255)

この表現方式では,色は3次元直交座標系の立方体で表されることになります。

(RGBの直交座標系)

立方体の表面を着色すると,次のようになります。

(RGBの立方体)

当初は,この表現形式を使い,数値を変化させて,色を作っていたのですが,なかなかうまくいきませんでした。この形式は,直感的でわかりやすいのですが,色が本来持っている性質を引き出せていないためです。

立方体を,真っ黒(0, 0, 0)から真っ白(255, 255, 255)方向(これを「BW方向」と呼ぶことにします。)に眺めると,次のように見えます(分かりやすいように,G軸とB軸を入れ替えていますが,本質的な問題ではないので,気にしないでください。)。

(RGBの六角形)

上の2つの図を眺めると,次のことがわかります。

BW方向に行くに従い,色が明るくなります。BW方向の軸(六角形の中心)から遠くに行くほど,色が鮮やかになります。BW方向の軸(六角形の中心)を中心に回転すると,色が「赤→黄→緑→シアン→青→マゼンダ→赤」と変わります。

これらを考慮し,発想を変えて,直交座標系(R,G,B)から,BW方向を高さにした円筒座標系(L,S,θ)に変換します。

(LSθの円筒座標系)

変換式は次のようになります。


L = ( √2 R + √2 G + √2 B ) / √6
x = S cosθ = ( 2 R - G - B ) / √6
y = S sinθ = ( √3 G - √3 B ) / √6

Lは明度(lightness)に由来する数値で(正確な定義は違いますが,ここでは「明度」と呼ぶことにします。),大きいほど明るくなります(イメージは色の明るさです。)。

(明度の帯)

S彩度(saturation)に由来する数値で(正確な定義は違いますが,ここでは「彩度」と呼ぶことにします。),大きいほど色が鮮やかになります(イメージは色の濁り具合です。)。

(彩度の帯)

θ色相(hue)に由来する角度で,この角度が0°から360°まで変わることで,色が変わり「赤→黄→緑→シアン→青→マゼンダ→赤」の順で一周します(一言で言えば色合いです。)。この角度が同じ色は,すべて同じ色なのだけれども,明度や彩度によって見え方が違うと考えると,理解しやすいと思います。なお,角度をイメージしやすいようにθにしましたが,一般的にはHで表します。

(色相の帯)

この3つのパラメーターを色の三属性と呼ぶそうです。この3つのパラメーターに沿うように,位相幾何(トポロジー)の発想で,色相の回転を完全な円形に変形して,色空間の立方体を円錐や円柱に変形したりすることもあります。

この3つのパラメーターを使い,明度を白と黒のちょうど中間に固定して,色を調合すれば,うまくいきそうです。

ところが,これも,うまくいきませんでした。同じ明度の色でも,人間には,青は暗く見え,緑は明るく見えるため,背景色が黒の場合に青は見えにくく,背景色が白の場合に緑が見えにくくなってしまうためです。

そこで,明度Lの代わりに,輝度Yを用います。輝度は,明るさを人間の見え方に合わせて補正したもので,次の式で表されます。


Y = 0.2126 R + 0.7152 G + 0.0722 B

その結果,使う式は次の3つになります。


Y = 0.2126 R + 0.7152 G + 0.0722 B
x = S cosθ = ( 2 R - G - B ) / √6
y = S sinθ = ( √3 G - √3 B ) / √6

これを解きます。手作業でも解けなくはないですが,ちょっと面倒くさいので,PCに解かせます。


> maxima -q

(%i1) solve([ Y=0.2126*r+0.7152*g+0.0722*b, x=(2*r-g-b)/sqrt(6), y=(g-b)/sqrt(2)],[r,g,b]);

rat: replaced -0.0722 by -361/5000 = -0.0722

rat: replaced -0.7152 by -447/625 = -0.7152

rat: replaced -0.2126 by -1063/5000 = -0.2126
            3937 sqrt(6) x - 3215 sqrt(2) y + 10000 Y
(%o1) [[r = -----------------------------------------,
                              10000
    (- 1063 sqrt(6) x) + 1785 sqrt(2) y + 10000 Y
g = ---------------------------------------------,
                        10000
    (- 1063 sqrt(6) x) - 8215 sqrt(2) y + 10000 Y
b = ---------------------------------------------]]
                        10000

すなわち,こうなります。


R = Y + ( 3937 √6 x - 3215 √2 y) / 10000
G = Y + (-1063 √6 x + 1785 √2 y) / 10000
B = Y + (-1063 √6 x - 8215 √2 y) / 10000

いよいよxyを捨てて,完全に円筒座標表示にします。


R = Y + S ( 3937 √6 cosθ - 3215 √2 sinθ) / 10000
G = Y + S (-1063 √6 cosθ + 1785 √2 sinθ) / 10000
B = Y + S (-1063 √6 cosθ - 8215 √2 sinθ) / 10000

今回の目的は,①背景色が白でも黒でも目立つ②鮮やかな色を③たくさん見付けることですが,これらの性質を次のように考えます。

①背景色が白でも黒でも目立つ色というのは,黒が輝度0,白が輝度1であることに注目し,輝度Yが0.5の色と考えます。

②鮮やかな色というのは,一定の色相の中で,彩度Sが一番高い色だと考えます。

③たくさん見付けるというのは,色相の角度θについて,360度を多数に分割することにより行います。

これをPythonで実装したものが,次のコードです(コピペして実行できます。)。


#!/usr/bin/python3
# calccolors.py

import math

# 色をいくつ探すか
DIVISION = 12
# 輝度
LUMINANCE = 0.5

# 次の数式で,輝度,彩度及び色相から,RGB(小数表示)を計算する。
# solve([ Y=0.2126*r+0.7152*g+0.0722*b, x=(2*r-g-b)/sqrt(6), y=(g-b)/sqrt(2)],[r,g,b]);
# r = (+3937*sqrt(6)*x -3215*sqrt(2)*y +10000*Y)/10000
# g = (-1063*sqrt(6)*x +1785*sqrt(2)*y +10000*Y)/10000
# b = (-1063*sqrt(6)*x -8215*sqrt(2)*y +10000*Y)/10000
def calc_rgb_ratio(ang, lum, sat):
    # 極座標表示(sat, ang)を直行座標(x, y)に変換
    x = sat*math.cos(2.0*math.pi*ang/360)
    y = sat*math.sin(2.0*math.pi*ang/360)
    # RGB(小数表示)を求める
    r_ratio = ((+ 3937*math.sqrt(6)*x - 3215*math.sqrt(2)*y) / 10000) + lum
    g_ratio = ((- 1063*math.sqrt(6)*x + 1785*math.sqrt(2)*y) / 10000) + lum
    b_ratio = ((- 1063*math.sqrt(6)*x - 8215*math.sqrt(2)*y) / 10000) + lum
    # 結果を返す
    return [r_ratio, g_ratio, b_ratio]

# 最初は彩度を大きく取り,RGB(小数表示)を計算する。
# RGB(小数表示)が0から1の範囲からはみ出しているかを確認する。
# はみ出した場合は,彩度を若干下げて,再度RGB(小数表示)を計算する。
# これを繰り返し,RGB(小数表示)がすべて0から1の範囲に収まる場合を見付ける。
# 見付かったら,その彩度とRGB(小数表示)を返す。
def get_rgb_and_sat(ang, lum):
    for i in range(100000, 0, -1):
        # 彩度を計算
        sat = math.sqrt(3)/2*i/100000
        # RGB(0〜1)を求める
        rgb_ratio = calc_rgb_ratio(ang, lum, sat)
        if((rgb_ratio[0] >= 0) and (rgb_ratio[0] <= 1)):
            if((rgb_ratio[1] >= 0) and (rgb_ratio[1] <= 1)):
                if((rgb_ratio[2] >= 0) and (rgb_ratio[2] <= 1)):
                    # RGB(0〜1)がいずれも0から1に収まっていれば結果を返す
                    return sat, rgb_ratio
    # 適切な色が見付からない場合
    return -1.0, [-1, -1, -1]

# 見付けた色を表示する。
def print_color(ang, lum, sat, rgb):
    # 輝度
    y = str(lum)
    # 色相(度数法)
    h = "{:03d}".format(int(ang))
    # 彩度(小数点以下3桁)
    s = "{0:.3f}".format(round(sat, 3))
    # 色の小数表示(0〜1)
    r_f = "{0:.3f}".format(round(rgb[0], 3))
    g_f = "{0:.3f}".format(round(rgb[1], 3))
    b_f = "{0:.3f}".format(round(rgb[2], 3))
    # 色の24ビット10進法表示(000〜256)
    r_d = "{:03d}".format(round(rgb[0]*255))
    g_d = "{:03d}".format(round(rgb[1]*255))
    b_d = "{:03d}".format(round(rgb[2]*255))
    # 色24ビット16進法表示(00〜FF)
    r_h = "{:02X}".format(round(rgb[0]*255))
    g_h = "{:02X}".format(round(rgb[1]*255))
    b_h = "{:02X}".format(round(rgb[2]*255))
    # 出力
    print('# Y=' + y + ', θ=' + h + ', S=' + s + '\n' +
          ' f=(' + r_f + ',' + g_f + ',' + b_f + ')' +
          ' d=(' + r_d + ',' + g_d + ',' + b_d + ')' +
          ' h=(' + r_h + ',' + g_h + ',' + b_h + ')')

# メインのループ
for n in range(DIVISION):
    # 色相を度数法で計算
    ang = float(n)*360/DIVISION
    # 彩度とRGB(0〜1)を求める
    sat, rgb = get_rgb_and_sat(ang, LUMINANCE)
    # 出力
    print_color(ang, LUMINANCE, sat, rgb)

実行結果です。"f"は小数表示,"d"は24ビット10進法表示,"h"は24ビット16進法表示です。


> ./calccolors.py
# Y=0.5, θ=000, S=0.518
 f=(1.000,0.365,0.365) d=(255,093,093) h=(FF,5D,5D)
# Y=0.5, θ=030, S=0.620
 f=(0.877,0.438,0.000) d=(224,112,000) h=(E0,70,00)
# Y=0.5, θ=060, S=0.440
 f=(0.539,0.539,0.000) d=(137,137,000) h=(89,89,00)
# Y=0.5, θ=090, S=0.430
 f=(0.304,0.609,0.000) d=(078,155,000) h=(4E,9B,00)
# Y=0.5, θ=120, S=0.571
 f=(0.000,0.699,0.000) d=(000,178,000) h=(00,B2,00)
# Y=0.5, θ=150, S=0.471
 f=(0.000,0.666,0.333) d=(000,170,085) h=(00,AA,55)
# Y=0.5, θ=180, S=0.518
 f=(0.000,0.635,0.635) d=(000,162,162) h=(00,A2,A2)
# Y=0.5, θ=210, S=0.620
 f=(0.123,0.562,1.000) d=(031,143,255) h=(1F,8F,FF)
# Y=0.5, θ=240, S=0.440
 f=(0.461,0.461,1.000) d=(118,118,255) h=(76,76,FF)
# Y=0.5, θ=270, S=0.430
 f=(0.696,0.391,1.000) d=(177,100,255) h=(B1,64,FF)
# Y=0.5, θ=300, S=0.571
 f=(1.000,0.301,1.000) d=(255,077,255) h=(FF,4D,FF)
# Y=0.5, θ=330, S=0.471
 f=(1.000,0.334,0.667) d=(255,085,170) h=(FF,55,AA)

この結果を帯にしてみました。

(色相の帯(カラー))

試しにグレースケールに変換してみます。

(色相の帯(グレースケール))

グレースケールは輝度を基準にして変換するので,すべての色が同じグレーになっています。

Emacsの設定は次のようになります。これを読み込むと,"000"などのキーワードに色が付きます(目立ちやすいようにボールドにしています。)。


(define-minor-mode angle-face-mode
  "" nil " a" nil
  (font-lock-add-keywords nil angle-terms)
  )
(defvar angle-terms
  '(("000" . angle-face-000)
    ("030" . angle-face-030)
    ("060" . angle-face-060)
    ("090" . angle-face-090)
    ("120" . angle-face-120)
    ("150" . angle-face-150)
    ("180" . angle-face-180)
    ("210" . angle-face-210)
    ("240" . angle-face-240)
    ("270" . angle-face-270)
    ("300" . angle-face-300)
    ("330" . angle-face-330)
    ))
(defface angle-face-000 '((t :foreground "rgb:FF/5D/5D" :bold t)) nil)
(defface angle-face-030 '((t :foreground "rgb:E0/70/00" :bold t)) nil)
(defface angle-face-060 '((t :foreground "rgb:89/89/00" :bold t)) nil)
(defface angle-face-090 '((t :foreground "rgb:4E/9B/00" :bold t)) nil)
(defface angle-face-120 '((t :foreground "rgb:00/B2/00" :bold t)) nil)
(defface angle-face-150 '((t :foreground "rgb:00/AA/55" :bold t)) nil)
(defface angle-face-180 '((t :foreground "rgb:00/A2/A2" :bold t)) nil)
(defface angle-face-210 '((t :foreground "rgb:1F/8F/FF" :bold t)) nil)
(defface angle-face-240 '((t :foreground "rgb:76/76/FF" :bold t)) nil)
(defface angle-face-270 '((t :foreground "rgb:B1/64/FF" :bold t)) nil)
(defface angle-face-300 '((t :foreground "rgb:FF/4D/FF" :bold t)) nil)
(defface angle-face-330 '((t :foreground "rgb:FF/55/AA" :bold t)) nil)
(defvar angle-face-000 'angle-face-000)
(defvar angle-face-030 'angle-face-030)
(defvar angle-face-060 'angle-face-060)
(defvar angle-face-090 'angle-face-090)
(defvar angle-face-120 'angle-face-120)
(defvar angle-face-150 'angle-face-150)
(defvar angle-face-180 'angle-face-180)
(defvar angle-face-210 'angle-face-210)
(defvar angle-face-240 'angle-face-240)
(defvar angle-face-270 'angle-face-270)
(defvar angle-face-300 'angle-face-300)
(defvar angle-face-330 'angle-face-330)
(angle-face-mode)

実際に試すとこうなります。

(背景色黒のEmacsのテスト) (背景色白のEmacsのテスト)

冒頭と同じ文章で試してみました。一部のキーワードを,あえてボールドにしておりませんが,ボールドにすると,もっと目立つようになると思います。

(背景色黒のEmacsの後) (背景色白のEmacs後)

後日談ですが,キーワードをもっと目立たせたい欲望に負けてしまい,現在は,輝度が0.6の色のセットと0.4の色のセットを作り,前者を背景色が濃い色の場合に,後者を背景色が薄い色の場合に使っております(普段,背景色を"Dark Green"(輝度0.28)にしており,輝度が0.5だとキーワードが目立ちにくいのも,設定を分けた理由の1つです。)。

また,色の数も12色では少し足りないため(もう少し黄色系の色を欲しいです。),色相をさらに分割して24色で使っています。

背景色が濃い色の場合と薄い色の場合の設定を統合するという目的には失敗しましたが,色を計算により論理的に調合するという目的は達成できたと思います。

使っているEmacs Lispを"kaki(牡蠣)"という名前で公開しました。

(kaki(牡蠣)の使用例)

追記

Kinput2で日本語入力終了キーを割り当てられた話

先日は,Kinput2で日本語入力終了キーを割り当てられない話をしました。

恥ずかしながら,あれからわずか数十日で解決いたしましたので,ご報告いたします。

失敗の原因は,開始にX Window Systemのリソースを使うため,終了もX Window Systemのリソースを使うものだと勝手に思い込んでいたことです。

Kinput2はX Window System上のアプリケーションであるため,その開始はX Window Systemのリソースを使う必要があります。しかし,一度開始してしまえば終了するのはKinput2の自由ですから,その終了はX Window Systemのリソースを使う必要はありません。すなわち,Kinput2の側で終了の設定をしてやればよいのです。

この設定は"/etc/kinput2"にある"ccdef.kinput2"又は"ccdef.kinput2.egg"で行います。私はEggユーザーですので,今回は"ccdef.kinput2.egg"に,元々設定されているShift-space(Shiftを押しながらspaceを押す。)に加えて"半角/全角"キーを日本語入力終了キーに追加する設定してみます。なお,Eggが何かについては,前回を参照してください。

"ccdef.kinput2.egg"に次のように記載して再起動すれば,Kinput2で"半角/全角"キーを日本語入力終了キーに割り当てることができます。


# /etc/kinput2/ccdef.kinput2.egg
# 日本語入力終了キーを,Shift-spaceと"半角/全角"キーに割り当てた設定
# この設定で解決
...
mode All        "?"
        ...
        ""      shift-space     ""      end-conversion goto Hiragana
        ""      Zenkaku_Hankaku ""      end-conversion goto Hiragana
        ...
endmode

最初に挑戦して失敗した1998年頃から20年以上の時を経て,あまりにも簡単に解決してしまいました。

Kinput2の開発者の皆様,誤解を招く記事を書いてしまい,申し訳ございませんでした。お詫び申し上げます。

ちなみに,ホームディレクトリーにある".Xresources"に次のように記載して再起動すれば,Kinput2で"半角/全角"キーを日本語入力開始キーに割り当てることができます。


! ~/.Xresources
! 日本語入力開始キーを,Shift-spaceと"半角/全角"キーに割り当てる設定
! この設定で解決
Kinput2*ConversionStartKeys: \
  Shift<Key>space \n\
  <Key>Zenkaku_Hankaku

Kinput2でMode_switchを使う話

前回は,Mode_switchの利点を解説し,Ctrl(Control)よりも活用されるべきと説明しました。

前回の設定方法("xmodmap -e 'keycode 41 = f F Right Right'"を実行)により,ほとんどのアプリではmod-f(Mode_switchを押しながら"f"を押す。)でカーソルが右に移動するようになります。しかし,ウィンドウマネージャなどのシステムに近いアプリには,前回の設定方法は効きません。X Window System上の日本語入力アプリであるKinput2も,前回の設定方法が効かないアプリの1つです。今回は,Kinput2でMode_switchを使って,mod-fでカーソルが右に移動するように設定する方法を説明します。

Kinput2のキー設定は"/etc/kinput2"にある"ccdef.kinput2"又は"ccdef.kinput2.egg"で行います。私はEggユーザーですので,今回は"ccdef.kinput2.egg"で設定してみます。ちなみに,Eggというのは,MuleEmacsの多言語拡張版のことで,本家Emacsに統合されたため,開発は終了しています。)の日本語システムで,「"た"くさん,"ま"たせて,"ご"めんなさい」の略である"たまご"を英語化したものです。

まず,"ccdef.kinput2.egg"をEmacsやViなどのエディターで開きます。設定を見るだけであれば,一般ユーザー権限で良いですが,編集する場合は,スーパーユーザー権限(root)で開く必要があります。

少し眺めてみると,モードごとにキー設定が分かれていることが分かります。今回はすべてのモード共通で設定しますので,"mode All"以下を設定します。"mode All"はファイルの最後にありますので,移動します。次のような感じになっています。


# /etc/kinput2/ccdef.kinput2.egg
...
mode All        "?"
        ""      ' '             ""      convert-next-or-move-top-or-sendback
        ""      '^\\'           ""      end-conversion goto Hiragana
        ""      '^@'            ""      convert-or-fix1
        ""      '^A'            ""      move-top
        ""      '^B'            ""      backward
        ""      '^C'            ""      clear-or-cancel
        ...

よく見てみると,Ctrl-f(Ctrlを押しながら"f"を押す。)の設定があり,次のようになっています。


        ""      '^F'            ""      forward

1番目の項目は前に入力されている文字,2番目の項目はキーの名前,3番目の項目は入力される文字,4番目の項目は発生する効果です。Ctrl-fの行について説明すると,1番目の項目は,入力されている文字に依存せずにカーソルが右に移動して欲しいので,空になっています。2番目の項目は,Ctrl-fのキャレット記法(caret notation)である"^F"になっています。3番目の項目は,入力される文字は不要ですので,空になっています。4番目の項目は,カーソルの右移動である"forward"になっています。

今回は,2番目の項目をmod-fになるように設定すれば良さそうです。その設定の仕方ですが,"ccdef.kinput2.egg"の下の方をよく見ると,参考になりそうな行があり,次のようになっています。


        ""      mod1-i          ""      shrink-s

xmodmap -pmを実行すると,Mode_switchはmod5に分類されているので,mod5-fとすれば良さそうです。


> xmodmap -pm

xmodmap:  up to 4 keys per modifier, (keycodes in parentheses):

shift       Shift_L (0x32)
lock        Zenkaku_Hankaku (0x64)
control     Control_L (0x17),  Control_R (0x69)
mod1        Alt_L (0x40),  Meta_L (0xcd)
mod2        Num_Lock (0x4d)
mod3      
mod4        Super_L (0x66),  Super_R (0x86),  Super_L (0xce),  Hyper_L (0xcf)
mod5        ISO_Level3_Shift (0x5c),  Mode_switch (0xcb)

そこで,mod5-fforwardに設定してみます。

# /etc/kinput2/ccdef.kinput2.egg
# 失敗例
...
mode All        "?"
        ...
        ""      mod5-f          ""      forward
        ...
endmode

これで良さそうに思えます。ところが,これだけでは動きません。そこで,"ccdef.kinput2.egg"の上の方をよく見ると,"mode Hiragana"に次のような行があります。


        "n"     '^['            "ん"    add-modifier-mod1

これによれば,mod5についても,"add-modifier-mod5"の設定が必要に思えます。これに気が付くまで,相当の時間を要しました。そこで,次のように設定してみます。


# /etc/kinput2/ccdef.kinput2.egg
# 失敗例
...
mode All        "?"
        ...
        ""      Mode_switch     ""      add-modifier-mod5
        ""      mod5-f          ""      forward
        ...
endmode

これで,mod-fでカーソルが右に移動するようになりました。しかし,これだと,カーソルが右に移動するたびに,Mode_switchを押し直さなければなりません。カーソルを複数回右に移動する場合は,Mode_switchを押したまま,"f"を連打したり又は押し続けたりするのが普通です。これを実現するためには,カーソルが移動した後も,mod5が残るようにしなければなりません。そこで,4番目の項目に"add-modifier-mod5"を付け加えます。紆余曲折しましたが,結論として,次のように設定すれば,Kinput2でMode_switchを使えます。


# /etc/kinput2/ccdef.kinput2.egg
# 成功例
...
mode All        "?"
        ...
        ""      Mode_switch     ""      add-modifier-mod5
        ""      mod5-f          ""      forward add-modifier-mod5
        ...
endmode

これで,普通にmod-fでカーソルが右に移動するようになりました。ちょっと動作が不安定な感じもありますが,十分に使えます。

Mode_switchはCtrl(Control)よりも便利なのでキーを割り当ててもっと活用すべき話

Mode_switchはUNIX系のOS(MacLinuxなど)で用意されている修飾キー(modifier key)です。修飾キーとは,ShiftやCtrl(Control)のように,他のキーと一緒に押すことにより,キー入力を変えるキーのことをいいます。Mode_switchは,ShiftやCtrlと違い,標準でキーが割り当てられておらず,ほとんど活用されていないようです。しかし,非常に便利なキーで,もっと活用されるべきですので,以下で説明します。

もしUNIX系OSをお持ちでXmodmapを実行できる環境にあるならば,端末上(terminal上)で"xmodmap -pke | grep ' f '"と実行してみてください。下記はその実行例ですが,おそらく"keycode 41 = f F f F"というような結果が返ってくると思います。なお,">"はコマンドプロンプトですので,入力する必要はありません。


> xmodmap -pke | grep ' f '
keycode  41 = f F f F

左辺の"41"という数字はキーの番号です。"a"キーには"38","s"キーには"39"というように,キーごとに数字が割り当てられています。

右辺の4つの文字は次の4つを表しています。

  1. キーを単独で押した場合に送られるキー入力
  2. キーをShiftと一緒に押した場合に送られるキー入力
  3. キーをMode_switchと一緒に押した場合に送られるキー入力
  4. キーをMode_switch及びShiftと一緒に押した場合に送られるキー入力

すなわち,"f"キーを,単独で押した場合は"f"が,Shiftと一緒に押した場合は"F"が,Mode_switchと一緒に押した場合は"f"が,Mode_switch及びShiftと一緒に押した場合は"F"が入力されることになります。このようなMode_switchの性質から,Mode_switchは第二のShiftであるともいえます(本来は"ä"のようなアクセント記号付きの文字を入力するためのキーだそうです。)。もっとも,この設定のままだと,Mode_switchを押しても押さなくても入力結果が同じであるため,Mode_switchには意味がありません。

そこで,"keycode 41 = f F Right Right"となるように設定してみます。この設定は,次のように実行すればできます("~/.Xmodmap"に設定を書いてログイン時に自動で設定させたり,ファイルに設定を書いておいて"xmodmap [filename]"を実行しても設定したりすることもできます。)。


> xmodmap -e 'keycode  41 = f F Right Right'

ここに書かれている"Right"は矢印キーの右キーを意味します。すなわち,Mode_switchを押しながら"f"キーを押すと(これを「Mod-f」と書くことにします。),矢印キーの右キーを押した場合と同じ効果を得ることができます。すなわち,BashEmacsなどのアプリでは,Ctrl-f(Ctrlを押しながら"f"を押す。)で,カーソルが右に移動しますが,これと同じことを実現できます。

これだけであれば,わざわざMod-fを使わなくても,Ctrl-fで十分だと思われるかもしれません。しかし,Mod-fには,Ctrl-fにはない利点があります。それは,全てのアプリで機能を統一することができることです。

Ctrl-fの場合,BashやEmacsではカーソルが右に移動する機能が割り当てられていますが(「forward」の頭文字を取ったものと思います。),FirefoxChromeなどでは検索機能が割り当てられています(「find」の頭文字を取ったものと思います。)。同じキー操作にもかかわらず,アプリによって機能がバラバラで,統一できていないのです(FirefoxやChromeでもCtrl-fをカーソルが右に移動する機能に割り当てようとしたことがありますが,うまくいきませんでした。)。他方,Mod-fの場合,これに矢印キーの右キーそのものを割り当てるため,どのアプリでもほぼ同じ機能(カーソルを右に移動させる機能)が割り当てられることになります。

さらに,Mode_switchを使えば,マウスの移動やクリックもキーボードから行えるようにできます。すなわち,Ctrl-fでカーソルを右に移動させる感覚で,例えばMod-gでマウスを右に移動させるようにできます。

Mode_switchはCtrlの代用となり得るキーであり,しかも機能を統一できる点でCtrlよりも便利といえます。Mode_switchはもっと活用されるべきです。

"akauni(赤海胆)"ではMode_switchをCaps_Lockキーに割り当てて,様々なキー設定をしています。

追記

Kinput2で日本語入力終了キーを割り当てられない話

※ この件は,解決しております。解決方法はこちらをご覧ください。

日本語入力は主にFreeWnnを使っています。Emacsでの日本語入力の際に,日本語変換と同時に辞書登録ができて,非常に便利だからです。

FreeWnnは主にEmacsでの日本語入力で活躍するアプリケーションですが,X Window System上のアプリケーション(メーラーやブラウザー)で日本語入力をする際は,Kinput2を使うことになります。

その設定に関し,何年かに一度,過去の失敗を忘れて,日本語入力終了キーを"半角/全角"キーに割り当てようとすることがあり,最近もそれをやりました。

ちなみに,日本語入力開始キーを"半角/全角"キーに割り当てるのは,ホームディレクトリーにある".Xresources"に次のように記載し,再起動すればできます。


! ~/.Xresources
! 日本語入力開始キーを,Shift-spaceと"半角/全角"キーに割り当てる設定
! この設定は有効
Kinput2*ConversionStartKeys: \
  Shift<Key>space \n\
  <Key>Zenkaku_Hankaku

このコードでは,Shift-space(Shiftを押しながらspaceを押す。)と"半角/全角"キーを,日本語入力開始キーに割り当ています。

ところが,".Xresources"に次のように記載しても,特殊な環境でない限り,日本語入力終了キーを"半角/全角"キーに割り当てることはできません。


! ~/.Xresources
! 日本語入力終了キーを,Shift-spaceと"半角/全角"キーに割り当てようとする設定
! この設定は無効
Kinput2*ConversionEndKeys: \
  Shift<Key>space \n\
  <Key>Zenkaku_Hankaku

Kinput2には,日本語入力終了キーを割り当てるリソースが存在しておらず,ソースコードを書き換えない限り,割り当てることはできません。"Kinput2*ConversionEndKeys"は存在するものの,特殊な環境のためのリソースのようです。Kinput2の日本語入力終了キーは,最初から用意されているShift-spaceを使うしかないようです。

最初にこれに挑戦して失敗したのは1998年頃だったと思いますが(PC-9821FreeBSD 2.2.6をインストールして使っていました。),恥ずかしながら,数年に一度,過去の失敗をすっかり忘れて,挑戦と失敗を繰り返しています。

Kinput2は完全に過去の遺物になっており,使っているという方はもうおられないかもしれませんが,二度と同じ失敗を繰り返さぬように,このメモを残しておきます。

もし簡単に設定できる方法をご存知の方がおられましたら,ご連絡をお待ちしております(連絡先)。

後日談ですが,解決しました。Kinput2には,日本語入力終了キーを割り当てるリソースが存在していないことは間違っていなかったのですが,全く別の方法で解決できました。解決方法はこちらです。

追記

端末上で"F1"で割込信号を送る話

前回は,端末上でCtrl-g(Ctrl(Control)を押しながら"g"を押す。)で割込信号を送る方法を説明しました。

しかし,この方法で設定できるのはCtrl-gなどに限られ,"F1"などのファンクションキーを割り当てることはできません。

これを"F1"に割り当てるのに試行錯誤し,解決法を思いつくまでに約1年もかかりました。いきなり結論ですが,端末がXtermの場合,発想を転換して,ホームディレクトリーにある".Xresources"に次のように記載して,再起動すればできます。


! ~/.Xresources
! "F1"をCtrl-cに割り当てる設定
XTerm*VT100.translations: #override \
  <Key>F1: string("0x03")

上のコード中の"0x03"はキーコードを16進数で表記したもので(最初の2文字の"0x"が16進数であることを示しています。),Ctrl-cを意味しています(正確にはキャレット記法で制御文字の"^c"を意味しています)。一方,"0x43"で"C"を意味します。"0x03"は2進数で"0000011","0x43"は2進数で"1000011"です。Ctrlは7ビット目(2進数の7桁目)を反転させる("0"を"1"に,"1"を"0"にする。)キーなので,"1000011"が"0000011"となるため,Ctrlを押しながら"c"を押すと,Ctrl-cという文字(割込信号)が端末に送られるのです。

もちろん,次のように実行しても構いません。なお,">"はコマンドプロンプトですので,入力する必要はありません。


> echo 'XTerm*VT100.translations: #override <Key>F1: string("0x03")' | xrdb -merge

この設定は,Xterm上では"F1"をCtrl-cと解釈するものです(簡単に言えば,割込信号を"F1"に設定できないので,発想を転換して,"F1"を割込信号であるCtrl-cに変えているのです。)。そのため,この設定をすると,"F1"はCtrl-cと解釈され,"F1"自体を入力することができなくなります。Xterm上で動くアプリ(Emacs,W3m,Bcなど)で,"F1"をショートカットに設定していた場合,そのショートカットが使えなくなるので,注意が必要です。

あと,当たり前ですが,前回の方法でCtrl-gで割込信号を送るようにしていた場合は,上の設定では動かないので,".Xresources"に次のように記載する必要があります("0x07"でCtrl-gを意味します)。


! ~/.Xresources
! "F1"をCtrl-gに割り当てる設定
XTerm*VT100.translations: #override \
  <Key>F1: string("0x07")

端末上でCtrl-gで割込信号を送る話

パソコンは,基本的にXtermを用い,CUI(Character User Interface)で使っています。CUIの方が,マウスを多用するGUI(Graphical User Interface)よりも効率がよく,履歴を確認できたり,ルーティーンワークをマクロ(Shell Script)化でできたりして,便利だからです。なお,Xtermは端末(terminal)エミュレーターの一つです。端末エミュレーターは単純に端末と呼ばれることが多いですが,UNIX系のOS(MacLinuxなど)でよく使う表現です。コマンドを打ち込むウィンドウのことで,WindowsでいうところのDos窓コマンドプロンプトに相当するものになります。

端末では,割込信号を送るのに(その上で起動中のアプリを強制終了させるのに),通常,Ctrl-c(Ctrl(Control)を押しながら"c"を押す。)を使います。

一方,文書を作成するエディターにはEmacsを用いていますが,Emacsで起動中の操作を強制終了させるのは,通常,Ctrl-g(Ctrlを押しながら"g"を押す。)であり,同じような機能であるにもかかわらず,キー操作が一致しません。

そこで,前者をCtrl-gに設定すべく試行錯誤したのですが,結論として,端末上で次を実行すればできます。なお,">"はコマンドプロンプトですので,入力する必要はありません。


> stty intr ^g

"intr"は,"interrupt"の略で,割込みを意味しています。また,"^"は慣習的にCtrlを意味しており,"^g"でCtrl-gとなりますが(厳密にはCtrl-gと"^g"は別の意味で,Ctrl-gというキー操作で"^g"という制御文字が入力されるという関係にあります。なお,"^g"は"^"と"g"の2文字ではなく,"^"がへんで"g"がつくりのようなもので,これで1文字と理解します。),このような記法をキャレット記法(caret notation)と言います。

ちなみに,端末上で次を実行すれば,現在の設定を確認できます。


> stty -a