Master Duel のサンダードラゴン

15時間ほどプレイしました。

f:id:nnt339:20220124131532p:plain

デッキ選択の理由

最近のデッキはよくわからないし、最初の10パックで《雷電龍》が出たからです。カードが呼んでました。

一応【魔術師】と【鉄獣戦線】と【ドラグマ】は組むつもりで集めています。

構築について

《真竜皇V.F.D.》が生存しているので、それを意識した構築を考えています。 といってもお祈りガンマとワンチャンディノミスクスぐらいしかありませんが。 癖で《エフェクト・ヴェーラー》から生成してしまったけれど、《無限泡影》のほうが《真竜皇V.F.D.》的には良かったでしょう。

【エルドリッチ】もいるらしいので、それを考慮して《ツイン・ツイスター》です。 【電脳堺】の表無効にする永続魔法も割りたいから速攻魔法の《ツイツイ》です。

ゲームの特性としてサイドデッキがないので《アーティファクト・ロンギヌス》は完全に無視した構築です。 そのおかげで(個人的な感覚と比較して)ほとんど事故りません。 《太陽電池メン》入れようとしたらURでたまげました。

《増殖するG》はまだ生成できていないから入っていませんが、たぶん要らないのではと考えています。 あのカードはマッチで一回通ってワンキルされなければラッキーなレベルのカードなので。

構築難易度

メインのURは《黄金櫃》くらいなので簡単だと思います。

「SRが余ったな~」と感じ始めたらすぐにでも組めるでしょう。

デッキパワー

正直Tier1のデッキには負けても仕方がないレベルのデッキパワーです。 でも"オンラインデュエルあるある"のプレミのおかげで奇跡的に捲れることはります(自分もプレミしまくる)。 《超雷龍》下で《炎舞-天璣》発動したり、《鉄獣戦線ナーベル》落としたり、《閃刀空域-エリアゼロ》で閃刀カード捲れちゃったりで負け確がひっくり返りました。 今日は結界像+リボルトの上からマリガンとディノミスクスで勝ちました

最終的には「プレミをしない方が勝ち」って感じなので【サンダードラゴン】が好きなら使うべきだと思います。 《超雷龍》《雷神龍の召喚演出でテンション上がること間違いなしです。

プレイングについて

もはや「暗黙知」化されてしまって、書こうと思ったけれどあまり出てきません。

コンセプトは「引き込む」なので、結構マリガンを多めに入れている気がします。 《超雷龍》+《ディノミスクス》+《雷電龍》(墓地)+《雷劫龍》(手札)(先にあるほど優先順位が高い)を理想の先行展開と考えて構築しています。 欲張って《原始生命態ニビル》食らうのは論外です。

《原始生命態ニビル》を食らって良いのは

  • 展開しなきゃ負けが確定している場合(この場合ゲームエンドする気で展開する)
  • 相手のリソースが少なく(手札が残り二枚の場合など)、"デステニードロー"をされなければ負けない場合

の二つくらいです。 それ以外は戦闘と《雷神龍でアド差を広げつつ、毎ターン《雷龍融合》してリソースを回していればキル確定までもっていけます。

【サンダードラゴン】は打点が高いから、《雷神龍が残ってターンが返ってくるだけでニビル貫通のワンキル圏内です。

焦らずじっくり負け筋を潰していくのが大事。 マリガンのおかげで長期的な情報戦では絶対有利だから。

マスターデュエルについて

演出については、モダンなカードゲームの技法を取り入れていて高いユーザー体験を実現していると思います。 派手な演出はユーザーの興奮を高める一方で、紙でぺちぺちやっていたユーザーとしてはテンポが悪く感じる部分もあるのが残念です。 ただ召喚するだけの動作で溜めてからフィールドに叩きつけるのは少し冗長であると感じます。 長考があったデュエルとはいえ、たった4ターンのやり取りで15分もかかるのは良いユーザー体験とは言えないでしょう。 もっとサクサク進めたいユーザーのためのオプションの実装などが望まれますね。

チェーン確認のオプションも改善の余地があると思います。 伏せカードが悟られないようにするために、発動できるカードがなくても優先権の移ったタイミングでフィールドを確認できるようにしてほしいです。 あえて長考することで相手にブラフのプレッシャーを強く与えるというプレイングが存在するからです。 フィールドにモンスターが出た瞬間にタイムラグが発生したら《無限泡影》が透けるというのはカードゲームとして避けなければならないでしょう。

ガバリプレイ

「二人ともガバリすぎたからOKか」

動画内に解説をいれるか迷いましたが、一時停止して読むような詳しい解説を入れるのは動画的に良くないと思いやめました。

vs【鉄獣戦線】

《サンダー・ドラゴン》を2枚引いている上に《烈風の結界像》を出されて敗色濃厚ですが、運良く《封印の黄金櫃》も引いているためマリガンでワンチャン狙います。

マリガンが成功して《孤高除獣》と《闇の誘惑》《雷電龍》のセットを引くことができました。さらに《闇の誘惑》で《バージェストマ・ディノミスクス》も引けてワンチャン生まれました。《雷電龍》のサーチ先は先の展開が読めなかったので《雷獣龍》にしましたが、《鉄獣の抗戦》読みができていれば《雷源龍》のほうが良かったかもしれません。結果的にこの《雷獣龍》が次のターン活躍するため"運"が良かったです。

《灰流うらら》と《バージェストマ・ディノミスクス》はライフが残るように使い、ターンが返ってきたので「サンダー・ドラゴン」たちで蹂躙してほぼ勝ち確です。捲られてもライフがあればまた超雷雷神が成立するからですね。

しかし、操作をミスって《鉄獣戦線 キット》召喚時に発動権を得られませんでした。ここで相手にワンチャン生まれたのですが、相手が再度ミスってサレンダーしました。《超雷龍》を除外されていたら負けてました。

vs【鉄獣戦線】

ドライバーを引いているので最初からマリガン狙いで動かします。運良く《闇の誘惑》で《雷獣龍》が引けたのでマリガンできますね。 そこに《増殖するG》が飛んできました。ガンマチャンスですが、ここで致命的ミス。 誤ってマウスの右ボタンをクリックしてしまったため《増殖するG》が通ってしまいました。 マリガンにも失敗してもう滅茶苦茶です。

相手は【十二鉄獣】で《戦華盟将-双龍》と《鉄獣戦線 凶鳥のシュライグ》で更地にされてしまいますが、相手がチェーン順序をミスったため《鉄獣の抗戦》は防ぐことができました。 たぶん《天霆號アーゼウス》を置かれてたらブラフの《雷龍融合》も吹っ飛んでたんで負けてました。

返しに《雷龍融合》しますが当然《戦華盟将-双龍》でバウンスされます。ここで相手が《原始生命態ニビル》を持ってたことが判明したため、初ターンでガンマしていたら直撃していたかもしれません。負け確ですがブラフを置いて返します。

するとブラフにビビったのかミスったのか、そのまま殴れば勝ちな場面で《鉄獣戦線 ケラス》が出てきたため、《PSYフレームギア・γ》で叩き落して延命します。

ターンが返ってきたので墓地リソースを消費して《戦華盟将-双龍》を使わせます。コストに《鉄獣戦線 凶鳥のシュライグ》を選ばれてたら危なかったです。

トップで引いた《バージェストマ・ディノミスクス》で延命します。

トップが《雷電龍》で墓地には《雷龍融合》、相手のリソースは場の《鉄獣戦線 凶鳥のシュライグ》のみ。頭を回転させて《超雷龍》+3000超打点の成立を狙います。私が導き出した方法は動画の通り。 相手のリソースを枯らしたので《雷神龍で蓋をします。《天雷震龍》も添えれば安心ですね。

返し再び操作をミスって召喚時の発動権を逃します(オートにすれば良い話だが発動したくもないタイミングで《灰流うらら》等が反応して手札ばれするのを防ぎたい)。しかしながら、《雷神龍2発で構えていたため相手に反撃のチャンスはありませんでした。

vs【鉄獣戦線】

先行をとったので《超雷龍》を出そうとしたところ、《雷源龍》に《灰流うらら》を貰いました。ラッキーとは思いつつも、マリガンが弱くなったのでなしではないのかな?と思ったり。 ハンドリソースを投げ捨てて《天雷震龍》を出したのはどうせ次のターン出せる見込みがないからです。

盤面とリソースが心許ないので相手ターンにすぐマリガンしに行きます。 するとまた《灰流うらら》を投げてきて、「もったいない使い方するなぁ」と思いながら1枚しかマリガンできない事実に驚愕しました。 しかも墓地の雷族が枯れてしまう致命的なプレミ。 結果として《アクセスコード・トーカー》に蹂躙されてしまいます。

トップは《孤高除獣》で、これはわかりやすく《雷龍融合》のサーチ先は《雷獣龍》です。このターンの最優先事項は《アクセスコード・トーカー》の処理です。 2枚の伏せカードに気を配りながら慎重にプレイします。 一枚は《墓穴の指名者》とわかり、もう一枚は《鉄獣の抗戦》でした。 《セキュリティ・ドラゴン》が除外されてしまい、リンク2で《アクセスコード・トーカー》を処理できないかエクストラデッキを眺めましたが、無理でした。 そこで墓地と除外ゾーンを見るとまだ効果を使用していない雷鳥龍》が除外されているではありませんか。

さあサンダードラゴン検定2級レベルのスキルを実践してみましょう。

敢えてリンク召喚はせず、慎重に融合素材を吟味して《雷龍融合》で《雷神龍特殊召喚します。 そして、そのままバトルフェイズへ入り、最初に《孤高除獣》を自爆特攻します。 《孤高除獣》が戦闘で破壊されたので雷鳥龍》を回収します。 次に3200の高打点で《鉄獣戦線 凶鳥のシュライグ》を処理します。 もう少し状況が良ければ、《雷劫龍》による戦闘破壊が入って妨害を+1できたでしょう。

メインフェイズ2へ入り、先ほど回収した雷鳥龍》を発動し《雷神龍で《アクセスコード・トーカー》を処理します。 雷鳥龍》の効果で《天雷震龍》が場に戻り、盤石な盤面が完成しました。 長い間遊戯王をやっていなかったので《超雷龍》を出してから《墓穴の指名者》が次のターンまで効果を無効にすることを思い出しました(プチプレミ)。

相手にはまだ捲るチャンスはありましたが、ミスって《鉄獣戦線 ケラス》を出してしまったため、《雷神龍で撃ち落としてGGです。

ご案内

f:id:nnt339:20220127032827p:plain

よろしくお願いします。

PowerShellからBashへ

PowerShellに別れの挨拶をしたので。

結論

WSLを使おう。

背景

今までコマンドを打ち込むときはPowerShellを使用していた。OSはWindows10なので、OSとの親和性を考慮しての選択だった。

PowerShell7CUIが弱かったのを改善しようとマイクロソフトが力を入れて開発しているクロスプラットフォームで動作するシェルである。 力を入れているだけあってコマンドプロンプトよりも使い勝手がはるかに向上している。

Set-Location, Copy-Item, Remove-ItemといったPowerShellの関数のエイリアスとして、それぞれcd, cp, mvが登録されている。

  • echo
  • cd
  • cp
  • cat
  • mv
  • mkdir
  • pwd
  • rm
  • rmdir

etc...
ほとんどのUnixコマンドはエイリアスとして登録されている。

問題点

このように頑張ればPowerShellだけでターミナル環境を整えることは難しくない。

しかし、それでも私がPowerShellにさよならを言ったのはやはりLinuxとの整合性がないからである。

  • grepってPowerShellでどうやるの?
  • dateをフォーマットしたいんだけど・・・
  • makeを使いたい、sedを使いたい

こういった疑問をいちいちグーグルするのに疲れてしまった。

仮に調べてわかっても、Linux用に書いたMakefileなどはそのまま使えないのである。

代替案

Git Bash

Git for Windowsをインストールすれば付いてくるbashである。
機能は悪くないが、色使いが好みではなかった。

Cygwin

レガシーなイメージがあるけど、2021年に更新していた。

MinGW

gccmakeコマンドを使うために一応入っている。

Msys2

Msysの64bit版。 pacmanでパッケージ管理ができる。

WSL

試しにWSLを使ったら、これが意外に満足のいくものだったのでそれを紹介したいと思う。

WSLDocker for Windowsを使っていれば特に追加でインストールするものはないのも高評価である。

Ubuntu-20.04

ディストリビューションは何でもよいが、使い慣れているUbuntu-20.04ディストリビューションを使用する。

wsl --install -d Ubuntu-20.04

インストール後、ユーザー名とパスワードを登録すればすぐに使える。

オプショナル操作

wsl --set-version Ubuntu-20.04 2  # wsl から wsl2 へ
wsl --set-default Ubuntu-20.04     # デフォルトのディストリビューションとして設定

デフォルトに設定しておくとbashPowerShellからWSLに入れる。

Dockerを使う

欲張りなのでdockerコマンドも使いたい。

WSLでは自動的にWindows側のドライブを/mnt/にマウントしてくれるのでファイル共有も簡単である。 さらに、WSL側からWindowsの実行バイナリを実行することが出来る。

$ docker.exe version
Client:
 Cloud integration: v1.0.22
 Version:           20.10.11
 API version:       1.41
 Go version:        go1.16.10
 Git commit:        dea9396
 Built:             Thu Nov 18 00:42:51 2021
 OS/Arch:           windows/amd64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.11
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.16.9
  Git commit:       847da18
  Built:            Thu Nov 18 00:35:39 2021
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.4.12
  GitCommit:        7b11cfaabd73bb80907dd23182b9347b4245eb5d
 runc:
  Version:          1.0.2
  GitCommit:        v1.0.2-0-g52b36a2
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

PATHも自動的にホストから引き継いでくれる便利さ。

実体はどこにあるかというと、

$ which docker.exe
/mnt/c/Program Files/Docker/Docker/resources/bin/docker.exe

ということで、これのシンボリックリンクを作成することでホスト側のdockerを使うことが出来る。

$ sudo ln -s "/mnt/c/Program Files/Docker/Docker/resources/bin/docker.exe" /usr/bin/docker
$ docker version
# the same content here

WSL2でDocker開発環境を整える

なんか失敗した。。。 VSCodeとの連携が上手くいかない。。。

問題提起

Docker Desktop for Windows...いらなくね?」

Linuxマシンで開発を行っているとき、開発環境は全てDockerコンテナで整えていた。 コンテナやイメージ、ボリューム、ネットワークの管理は全てdockerコマンドから行っていて、特に不自由を感じなかった。 むしろLinuxの方がコンテナの起動が速くて快適。

Windows11のニューマシンを手に入れて、いざ開発環境を整えようと時に思ったのが冒頭である。 わざわざGit for WindowsとかDocker Desktop for Windowsとかmsys2とかをmsiでインストールするのは面倒だし、アップデート管理も大変である。

まあ色々言いたいことはあるが、とにかく開発する上で欲しいのはLinux環境。 でも手元にはWindowsマシン。 そこでWSL2を使おうと決意したのであった。

下準備

  • WSLの有効化
  • BIOS/OSレベルの仮想化の有効化

Windowsにある程度精通していればやり方はすぐわかる。Google it♡)

Ubuntuをインストール

wslコマンドの確認

$ wsl --help

入手できるディストリビューションを確認。

$  wsl --list --online
インストールできる有効なディストリビューションの一覧を次に示します。
'wsl --install -d <Distro>' を使用してインストールします。

NAME            FRIENDLY NAME
Ubuntu          Ubuntu
Debian          Debian GNU/Linux
kali-linux      Kali Linux Rolling
openSUSE-42     openSUSE Leap 42
SLES-12         SUSE Linux Enterprise Server v12
Ubuntu-16.04    Ubuntu 16.04 LTS
Ubuntu-18.04    Ubuntu 18.04 LTS
Ubuntu-20.04    Ubuntu 20.04 LTS

Ubuntu 20.04 LTSをインストール。

$ wsl --install -d Ubuntu-20.04

このディストリビューションDocker専用にするためusernamedockerにして進める。 パスワードを設定すればWSLが利用できるようになる。

いろいろ設定

面倒なのでsudoのパスワード要求を切っておく。

$ sudo visudo
- %sudo   ALL=(ALL:ALL) ALL
+ %sudo ALL=(ALL:ALL) NOPASSWD: ALL

全てのパッケージを更新する。

$ sudo apt update
$ sudo apt upgrade -y

デフォルトのユーザをdockerに。

$ sudo nano /etc/wsl.conf
+ [user]
+ default=docker

bash起動時にdockerdが動くようにしておく。

$ nano ~/.bashrc
+ sudo service docker start > /dev/null

Dockerのインストール

docs.docker.com (accessed on 2022/01/17)

必要があればdocker-composeも。

$ sudo apt install -y docker-compose

一度ディストリビューションを落としてから再度WSL内へ。

$ exit
$ wsl --terminate Ubuntu-20.04
$ wsl -d Ubuntu-20.04

dockerの確認。

$ docker --version
Docker version 20.10.12, build e91ed57
$ docker images # dockerdが起動していなければエラーが返る
REPOSITORY   TAG       IMAGE ID   CREATED   SIZE

新たなディストリビューションとしてインポート

作成したディストリビューションtarファイルとしてエクスポートする。

$ wsl --export Ubuntu-20.04 docker.tar

カレントディレクトリに仮想ディスクを作るようにインポートする。

$ wsl --import Docker . docker.tar --version 2
$ wsl -d Docker

問題がないようであればUbuntu-20.04を削除する。

$ wsl --unregister Ubuntu-20.04

これでWSL2上で開発する準備が整った。

ディスクを圧迫してきたらdiskpartで圧縮する。

$ diskpart
DISKPART> select vdisk path/to/ext4.vhdx
DISKPART> attach vdisk readonly
DISKPART> compact vdisk
DISKPART> detach vdisk
DISKPART> exit

オプショナル

ホストのsshWSLから叩けるようにする。

sudo mv /usr/bin/ssh /usr/bin/ssh.original # 元々のsshをリネーム
sudo ln -s `which ssh.exe` /usr/bin/ssh

ついでにWSLにもssh接続できるようにする。

sudo service ssh start

Ubuntu Mail(Gmail)

メール設定のメモ

$ sudo apt install -y mailutils

/etc/postfix/main.cfを編集

inet_interfaces = loopback-only
relayhost = [smtp.gmail.com]:587
smtp_use_tls = yes
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous

Gmailのパスワード設定 /etc/postfix/sasl_passwdを作成

[smtp.gmail.com]:587 <username>@gmail.com:<password>

データベース化

$ sudo postmap /etc/postfix/sasl_passwd

必要か要検証 /etc/postfix/transport

gmail.com  smtp:[smtp.gmail.com]:587
*          :
$ sudo postmap /etc/postfix/transport

再起動

$ sudo systemctl restart postfix

テスト

echo "testing" | mail -s "test" foo@gmail.com

ログ

/var/log/mail.log
/var/log/mail.err

RaspberryPiをオーバークロックしてベンチマーク

Raspberry PiオーバークロックしてUnix Benchmark取ってみた。

パフォーマンスはチューニングしたわけではなく、普段起動してあるサービスは起動したままターミナルを2つだけ開いてベンチマークを実行。 もう一つのターミナルではクロック速度と温度を監視するスクリプトを実行しておいた。

ベンチマーク環境

BYTE UNIX Benchmarks (Version 5.1.3)

   System: raspi: GNU/Linux
   OS: GNU/Linux -- 5.4.0-1047-raspi -- #52-Ubuntu SMP PREEMPT Wed Nov 24 08:16:38 UTC 2021
   Machine: aarch64 (aarch64)
   Room Temperature: around 10 ℃

オーバークロックの方法

下準備

ハードウェア状態を監視するためのツールをインストール

sudo apt install -y libraspberrypi-bin

CPUクロックの定格をチェック

クロックを監視

watch -n 1 sudo vcgencmd measure_clock arm

温度を監視

watch -n 1 sudo vcgencmd measure_temp

電圧を取得

sudo vcgencmd measure_volts

オーバークロック

電圧とクロックを上げる。 /bootをいじるので慎重に。

ブートの設定は/boot/firmwareにある。 READMEを読んだら作業に入る。

sudo nano /boot/firmware/usercfg.txt

以下を追加

over_voltage=2
arm_freq=1750

保存してリブートするとオーバークロックが実現する。

import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
pd.options.display.float_format = "{:.4g}".format
plt.rcParams["figure.figsize"] = (9, 6)
plt.style.use("seaborn-bright")
results = []
def load_data(filename) -> pd.DataFrame:
    df = pd.read_csv(filename)
    df["clock [MHz]"] = df["clock [Hz]"] / 1e6
    del df["clock [Hz]"]
    df["time"] = df["time"].apply(lambda x: datetime.fromisoformat(x))
    df["throttled"] = df["throttled"].apply(lambda x: x.strip()).apply(lambda x: int(x, base=16))
    df = df[["time", "temp [℃]", "clock [MHz]", "voltage [V]", "throttled"]]
    df.set_index("time", inplace=True)
    return df


def parse_benchmark_result(filename) -> pd.DataFrame:
    with open(filename) as f:
        lines = f.readlines()
    names = []
    scores = []
    for line in lines:
        if line.startswith(" "):
            continue
        split = line.split()
        if len(split) < 3:
            continue
        try:
            score = float(split[-1])
        except ValueError:
            continue
        names.append(" ".join(split[:-3]))
        scores.append(score)
    n = len(names) // 2
    names = names[:n]
    names[-1] = "System Benchmarks Index Score"
    return pd.DataFrame({"name": names, "1 core": scores[:n], "4 core": scores[n:]})    

デフォルト

コンフィグ

arm_freq=1500

ベンチマーク結果

results.append(parse_benchmark_result("0/results/raspi-2021-12-31-01"))
results[0]
name 1 core 4 core
0 Dhrystone 2 using register variables 1293 5018
1 Double-Precision Whetstone 487.4 1902
2 Execl Throughput 96.9 329.2
3 File Copy 1024 bufsize 2000 maxblocks 231.6 445.8
4 File Copy 256 bufsize 500 maxblocks 154 289.2
5 File Copy 4096 bufsize 8000 maxblocks 447.4 738.1
6 Pipe Throughput 112.6 436
7 Pipe-based Context Switching 70.2 288.8
8 Process Creation 204.1 473.6
9 Shell Scripts (1 concurrent) 387.4 1019
10 Shell Scripts (8 concurrent) 873.8 972.4
11 System Call Overhead 113.4 430.1
12 System Benchmarks Index Score 250.2 665.9

CPUの状態

df = load_data("0/data.csv")
df.describe()
temp [℃] clock [MHz] voltage [V] throttled
count 2879 2879 2879 2879
mean 40.39 1467 0.85 0
std 4.668 58.85 1.11e-16 0
min 32.1 1200 0.85 0
25% 36 1400 0.85 0
50% 40.4 1500 0.85 0
75% 44.8 1500 0.85 0
max 50.1 1500 0.85 0
df.plot(subplots=True, );

f:id:nnt339:20211231213355p:plain

オーバークロック 1750 MHz

コンフィグ

arm_freq=1750
over_voltage=2

ベンチマーク結果

results.append(parse_benchmark_result("1/results/raspi-2021-12-31-01"))
results[1]
name 1 core 4 core
0 Dhrystone 2 using register variables 1508 5878
1 Double-Precision Whetstone 569.3 2220
2 Execl Throughput 112.8 374.4
3 File Copy 1024 bufsize 2000 maxblocks 270.6 521.6
4 File Copy 256 bufsize 500 maxblocks 176.9 341.3
5 File Copy 4096 bufsize 8000 maxblocks 512.3 837
6 Pipe Throughput 132.2 509.8
7 Pipe-based Context Switching 82.2 340.5
8 Process Creation 222.6 506.4
9 Shell Scripts (1 concurrent) 439.1 1133
10 Shell Scripts (8 concurrent) 975.9 1082
11 System Call Overhead 132.1 503.3
12 System Benchmarks Index Score 288 763.9

CPUの状態

df = load_data("1/data.csv")
df.describe()
temp [℃] clock [MHz] voltage [V] throttled
count 2879 2879 2879 2879
mean 45.32 1717 0.9 0
std 6.688 63.8 1.11e-16 0
min 35.5 1200 0.9 0
25% 38.9 1700 0.9 0
50% 46.2 1750 0.9 0
75% 52.1 1750 0.9 0
max 58.4 1750 0.9 0
df.plot(subplots=True);

f:id:nnt339:20211231213432p:plain

オーバークロック 2000 MHz

コンフィグ

arm_freq=2000
over_voltage=4

ベンチマーク結果

results.append(parse_benchmark_result("2/results/raspi-2021-12-31-01"))
results[2]
name 1 core 4 core
0 Dhrystone 2 using register variables 1725 6722
1 Double-Precision Whetstone 650.8 2545
2 Execl Throughput 128.1 414.2
3 File Copy 1024 bufsize 2000 maxblocks 303.1 586.8
4 File Copy 256 bufsize 500 maxblocks 205.2 388.4
5 File Copy 4096 bufsize 8000 maxblocks 563 907
6 Pipe Throughput 150.5 582.5
7 Pipe-based Context Switching 93.4 391.2
8 Process Creation 236.6 527.5
9 Shell Scripts (1 concurrent) 464 1227
10 Shell Scripts (8 concurrent) 1056 1170
11 System Call Overhead 150.7 576.8
12 System Benchmarks Index Score 321.8 851.8

CPUの状態

df = load_data("2/data.csv")
df.describe()
temp [℃] clock [MHz] voltage [V] throttled
count 2898 2898 2898 2898
mean 48.67 1953 0.95 0
std 7.673 88.98 0 0
min 34 1400 0.95 0
25% 41.3 1900 0.95 0
50% 49.1 2000 0.95 0
75% 56.4 2000 0.95 0
max 65.2 2001 0.95 0
df.plot(subplots=True);

f:id:nnt339:20211231213506p:plain

オーバークロック 2000MHz (force turbo)

コンフィグ

over_voltage=4
arm_freq=2000
force_turbo=1

ベンチマーク結果

results.append(parse_benchmark_result("3/results/raspi-2022-01-02-01"))
results[3]
name 1 core 4 core
0 Dhrystone 2 using register variables 1725 6745
1 Double-Precision Whetstone 651 2550
2 Execl Throughput 159.8 493.6
3 File Copy 1024 bufsize 2000 maxblocks 304.8 582.3
4 File Copy 256 bufsize 500 maxblocks 204.2 387.6
5 File Copy 4096 bufsize 8000 maxblocks 568.7 907.2
6 Pipe Throughput 149.4 580.9
7 Pipe-based Context Switching 120.9 413.4
8 Process Creation 326.7 549.2
9 Shell Scripts (1 concurrent) 625 1321
10 Shell Scripts (8 concurrent) 1169 1262
11 System Call Overhead 150.4 575.9
12 System Benchmarks Index Score 355.8 881.5

CPUの状態

df = load_data("3/data.csv")
df.describe()
temp [℃] clock [MHz] voltage [V] throttled
count 2945 2945 2945 2945
mean 55.63 2000 0.95 0
std 7.799 0.02204 0 0
min 40.4 2000 0.95 0
25% 48.2 2000 0.95 0
50% 56.4 2000 0.95 0
75% 63.3 2000 0.95 0
max 72 2001 0.95 0
df.plot(subplots=True);

f:id:nnt339:20220107142736p:plain

オーバークロック 2000MHz (force turbo + 温度計測なし)

コンフィグ

over_voltage=4
arm_freq=2000
force_turbo=1

ベンチマーク結果

results.append(parse_benchmark_result("4/results/raspi-2022-01-02-01"))
results[4]
name 1 core 4 core
0 Dhrystone 2 using register variables 1726 6898
1 Double-Precision Whetstone 651.2 2606
2 Execl Throughput 169.2 536.8
3 File Copy 1024 bufsize 2000 maxblocks 306.7 603.1
4 File Copy 256 bufsize 500 maxblocks 204.5 397.1
5 File Copy 4096 bufsize 8000 maxblocks 587.3 964.1
6 Pipe Throughput 150.9 606.5
7 Pipe-based Context Switching 123.5 429
8 Process Creation 354.5 598.5
9 Shell Scripts (1 concurrent) 653.5 1422
10 Shell Scripts (8 concurrent) 1252 1356
11 System Call Overhead 146.5 574.3
12 System Benchmarks Index Score 364.8 923.4

ベンチマーク結果比較

df = pd.DataFrame(
    [list(result["4 core"]) for result in results],
    columns=results[0]["name"],
    index=["1500MHz", "1750MHz", "2000MHz", "2000MHz(force turbo)", "2000MHz (performance)"]
)
fig, axes = plt.subplots(len(df.columns), figsize=(9, 18)) 
fig.tight_layout(h_pad=2)
df.iloc[::-1, :].plot.barh(ax=axes, subplots=True, legend=False, sharex=False);

f:id:nnt339:20220107142658p:plain

考察

2000MHz, +0.1000Vでスコア850程度オーバークロック前と比べておよそ1.28倍の性能アップである。 throttledは発生していないので電圧はover_voltage=4で十分だと思われる。 2.0GHzまで問題なくオーバークロックできることがわかったが、負荷がかかると最大温度65℃まで上昇している。 冬場でこれだと夏場は70℃を超えることが予想されるため、冷却機能を強化するかクロックを落とす必要があると考えられる。

force turboして温度計測をやめたところスコアは920まで伸びた

RaspberryPi + Ubuntu20.04LTS + Desktop

先日メインで使用していたマシンがお亡くなりになったので、 Ubuntu20.04LTS ServerをインストールしていたRapsberryPi 4Bモデルに急遽デスクトップ環境を整えた。 その作業に使用したコマンド類を記す。 因みにOSはRaspberryPi Imagerで書き込んだ。

環境

  • Model: Raspberry Pi 4 Model B Rev 1.4
  • OS: Ubuntu 20.04.3 LTS (Focal Fossa) bullseye/sid
  • RAM: 8GB

GUIのインストール

sudo apt install -y ubuntu-desktop

派生版も使用することができる。

sudo apt install -y lubuntu-desktop
sudo apt install -y kubuntu-desktop
sudo apt install -y xubuntu-desktop

リブートするとGUIのログイン画面が迎えてくれる。

Bluetoothのセットアップ

sudo apt install -y pi-bluetooth
bluetoothctl
デバイスをスキャン
# scan on
デバイスを表示
# devices
ペアリング
# pair <mac address>

キーボードの設定

Google日本語入力オープンソース

sudo apt install -y ibus-mozc

Chromiumのインストール

ブラウザはセキュリティの観点から最新版を使いたいので、多少のバグ覚悟でベータ版をインストール。

sudo snap install chromium --beta

VSCodeのインストール

公式に従ってインストールする。

code.visualstudio.com (accessed on 2021/12/31)

鍵のダウンロードとリポジトリの追加

wget -qO- https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor > packages.microsoft.gpg
sudo install -o root -g root -m 644 packages.microsoft.gpg /etc/apt/trusted.gpg.d/
sudo sh -c 'echo "deb [arch=amd64,arm64,armhf signed-by=/etc/apt/trusted.gpg.d/packages.microsoft.gpg] https://packages.microsoft.com/repos/code stable main" > /etc/apt/sources.list.d/vscode.list'
rm -f packages.microsoft.gpg
sudo apt update
sudo apt install -y code

Dockerのインストール

curl -fsSL https://get.docker.com/ | bash

docs.docker.jp (accessed on 2022/1/7)

Gitはインストール済みなのでこれで一通りの開発環境が整った。

f:id:nnt339:20220101194506p:plain

オーバークロック

GUIやブラウザの動きがもっさりしていたのでオーバークロックしてパフォーマンスを向上させる。

下準備

ハードウェア状態を監視するためのツールをインストール

sudo apt install -y libraspberrypi-bin

CPUクロックの定格をチェック

クロックを監視

watch -n 1 sudo vcgencmd measure_clock arm

温度を監視

watch -n 1 sudo vcgencmd measure_temp

電圧を取得

sudo vcgencmd measure_volts

スロットルの有無を確認

sudo vcgencmd get_throttled

オーバークロック

電圧とクロックを上げる。 /bootをいじるので慎重に。

ブートの設定は/boot/firmwareにある。 READMEを読んだら作業に入る。

sudo nano /boot/firmware/usercfg.txt

以下を追加

over_voltage=2
arm_freq=1750

保存してリブートするとオーバークロックが実現する。

over_voltageは+1で0.0250V上がると思われるが文献は未調査。 同じモデルでも初期電圧が違うのか、over_voltageは同じでもコマンドで得た電圧値が異なる。

最大までオーバークロック

https://magpi.raspberrypi.com/articles/how-to-overclock-raspberry-pi-4によると、2147MHzでブート出来なくなるらしい(これはgpu_freqをいじっているせいかもしれないが)。 ブート出来なくなっても別のマシンでSDカードを読み込んで設定を戻すか、OSを再インストールすれば良い。 最悪ボードが壊れても1万円でオーバークロックの勉強が出来たと思えばまあ。

私の場合は普段使いする予定なので、マージンを持たせて1900MHz(voltage+3)で稼働中。 恐らく2000MHzでも安定動作すると思うが問題は排熱機構で、ヒートシンク+ファンが付いているとは言えそれほど排熱性能が良いわけではないのでオーバークロックしすぎると夏場に昇天する可能性がある。

メインマシンの修理が完了したら限界を調査してみたいと思う。

Python unittestのベストプラクティス

Pythonユニットテストについて、現状の結論(ベストプラクティス)を記したいと思う。

使用するフレームワーク

標準モジュールのunittestを使用する。 pytestという有名なライブラリも存在するが、ライブラリ管理の手間が減るため標準モジュールで行けるところはそれでいきたい。 unittestで対応できない場面に遭遇したら他のライブラリも検討してみようと思う。

ルール

Pythonは自由度が高く、型も含めて制約がほとんどないのが特徴だが、その自由度の高さゆえに自分でルールを設けなければコードから統一感が喪失してしまう。 統一性のないコードは人間の推測する能力(いまやコンピュータでもできるが。。。)を活かせないため、1か月後には解読不能なコードになってしまう。 それを避けるためにユニットテストについて、いくつかルールを設けた。

関数ごとにテストケースを作成する

Test_関数名という名前でテストケースクラスを設計する。

例えば、get_name()という関数のテストケースであればTest_get_nameのようにテストケースを実装する。

from unittest import TestCase

class Test_get_name:
    
    def test_XX(self) -> None:
        # test here

メソッドであればTestクラス名_メソッド名でテストケースを作成する。

from unittest import TestCase

class TestMyClass_method(TestCase):
    
    def test_XX(self) -> None:
        # test here

これはクラス名はパスカルケースというPythonの慣例に背く形となるが、そうであってもこのようにした理由がいくつかある。

チュートリアルコードなどではclass TestMyClass(TestCase):とクラスごとにテストケースとする例が多くみられるが、その設計はメソッドが多くなると破綻してしまう。 新しいテスト関数を追加するときも、メソッドの順番を無視すれば保守性が崩壊して、順番を意識して実装すればその順番を探すのに時間がかかってしまう。 そこでclass Testクラス名_メソッド名(TestCase):とすることで関数ごとにテストケースを管理するようにした。

ここで、class TestMyClassGetName(TestCase):のように全てパスカルケースでクラスを定義してしまうと、どこまでがクラス名でどこからメソッド名かわからなくなってしまう。 そこで_でクラス名とメソッド名を区切ることにして、それに合わせてメソッド名もスネークケースのままで記すことにした。 統一性を持たせるためにただの関数の場合もclass Test_関数名(TestCase):となったのである。

プロパティのテストはTestCaseクラス名_propertyで行う

@propertyデコレータのついた関数のテストはTestCaseクラス名_property(TestCase)で行う。 一つのプロパティに対するテストは戻り値のチェックだけなので、それらをまとめて一つのテストケースにする。

アサーションは"結果"に対して行う

テスト関数の中でアサーションは戻り値や送出された例外など、関数呼び出しの"結果"に対して行う。 関数内部のif分岐やロジックに対してはアサーションを行わない。

関数内部のif文についてもテストを書けばコードはより堅牢になるが、実装への依存度が高くなるのでテストしない。

クリーンアップ処理はテスト関数から切り離す

副作用のある関数(ファイルダンプなど)のクリーンアップ処理をテスト関数内に書くと、どこがテストの本質なのか見えにくくなってしまう。 クリーンアップが必要なテストはテストケースとしてまとめてunittest.TestCase.tearDown()でクリーンアップ処理を行う。

チュートリアル

以上のルールを踏まえて、tutorial.pyモジュールのユニットテストtest_tutorial.pyを実装してみる。

tutorial.py

from __future__ import annotations
from math import lcm
from typing import Union


def add_positive_integers(x: int, y: int) -> int:
    if x <= 0:
        raise ValueError('`x` must be a positive number.')
    if y <= 0:
        raise ValueError('`y` must be a positive number.')
    return x + y



class Rational:
    _numerator: int
    _denominator: int

    def __init__(self, numerator: int, denominator: int) -> None:
        if denominator == 0:
            raise ValueError('`denominator` must be non-zero.')

        self._numerator = numerator
        self._denominator = denominator


    @property
    def numerator(self) -> None:
        return self._numerator

    
    @property
    def denominator(self) -> None:
        return self._denominator

    
    def __add__(self, other: Union[int, Rational]) -> Rational:
        if isinstance(other, Rational):
            denominator = lcm(self.denominator, other.denominator)
            n = self.numerator * denominator // self.denominator
            m = other.numerator * denominator // other.denominator
            numerator = n + m
            return Rational(numerator, denominator)
        elif isinstance(other, int):
            return self + Rational(other, 1)
        else:
            return NotImplemented

tests/test_tutorial.py

from unittest import TestCase
import tutorial

class Test_add_positive_integers(TestCase):

    def test_two_positives(self) -> None:
        x, y = 1, 2
        actual = tutorial.add_positive_integers(x, y)
        expect = x + y
        self.assertEqual(actual, expect)


    def test_zero(self) -> None:
        with self.assertRaises(ValueError):
            tutorial.add_positive_integers(0, 1)
        with self.assertRaises(ValueError):
            tutorial.add_positive_integers(1, 0)

    
    def test_negative(self) -> None:
        with self.assertRaises(ValueError):
            tutorial.add_positive_integers(-1, 1)
        with self.assertRaises(ValueError):
            tutorial.add_positive_integers(1, -1)
    


class TestRational___init__(TestCase):

    def test_numeator(self) -> None:
        numerator = 1
        denominator = 2
        r = tutorial.Rational(numerator, denominator)
        self.assertEqual(r._numerator, numerator)


    def test_denominator(self) -> None:
        numerator = 1
        denominator = 2
        r = tutorial.Rational(numerator, denominator)
        self.assertEqual(r._denominator, denominator)
    

    def test_denominator_is_zero(self) -> None:
        numerator = 1
        denominator = 0
        with self.assertRaises(ValueError):
            tutorial.Rational(numerator, denominator)



class TestRational_property(TestCase):

    def setUp(self) -> None:
        self.r = tutorial.Rational(1, 2)

    
    def test_numerator(self) -> None:
        self.assertEqual(self.r.numerator, 1)
    

    def test_denominator(self) -> None:
        self.assertEqual(self.r.denominator, 2)



class TestRational___add__(TestCase):

    def test_add_rational(self) -> None:
        r1 = tutorial.Rational(1, 2)
        r2 = tutorial.Rational(2, 3)
        actual = r1 + r2
        expect = tutorial.Rational(7, 6)
        self.assertEqual(actual.numerator, expect.numerator)
        self.assertEqual(actual.denominator, expect.denominator)

    
    def test_add_integer(self) -> None:
        r1 = tutorial.Rational(1, 2)
        actual = r1 + 2
        expect = tutorial.Rational(5, 2)
        self.assertEqual(actual.numerator, expect.numerator)
        self.assertEqual(actual.denominator, expect.denominator)


    def test_add_None(self) -> None:
        r1 = tutorial.Rational(1, 2)
        with self.assertRaises(TypeError):
            r1 + None

実行

coverageを通して実行する

$ pip install coverage
$ coverage run -m unittest tests.test_tutorial
...........
----------------------------------------------------------------------
Ran 11 tests in 0.003s
$ coverage report
Name                     Stmts   Miss  Cover
--------------------------------------------
tests\__init__.py            1      0   100%
tests\test_tutorial.py      59      0   100%
tutorial.py                 33      0   100%
--------------------------------------------
TOTAL                       93      0   100%