2020年12月24日木曜日

git-crecord

はじめに


gitのコミット時に行単位でコミットする部分を選択できるツール git-crecord に4年ぶりにリリースタグが付けられていましたので、 設定方法をメモしておきます。

git-crecordには、

  • 部分選択コミット作業に特化
  • テキストユーザーインターフェイス(ncursesベースのCUI)
  • 操作方法が直感的なので覚えることはほぼなし(習得が簡単!)
という特徴があります。コミット以外の作業もテキストユーザーインターフェイスで行いたい場合は 多機能なtigのほうが便利でしょう。 なお、git-crecordはMercurialの拡張機能であるcrecord(※1)のgit移植版です。

設定


https://github.com/andrewshadura/git-crecord からgitでcloneします。このメモの作成時点では Python 3.6 以降が必要とのことです。

あとは、cloneしたディレクトリにあるgit-crecordがgitから見つけられるようPATHを通すだけです。 すでに、~/.local/bin/にパスが通っているのであれば、 そこにgit-crecordへのリンクを作成するのが最も簡単でしょう。 名前はgit-crecordのままである必要があります。

実行例


gitリポジトリでgit crecordを実行すると、
Select hunks to commit - [x]=selected **=collapsed  c: confirm     
q: abort  arrow keys: move/expand/collapse  space: deselect        
?: help                                                            
[x]    diff --git a/aaa.txt b/a.txt                                
       similarity index 100%                                       
       rename from aaa.txt                                         
       rename to a.txt                                             
                                                                   
___________________________________________________________________
[x]    diff --git a/b.txt b/b.txt                                  
       new file mode 100644                                        
                                                                   
   [x]     @@ -0,0 +1,1 @@                                         
      [x]  +bbb                                                    
___________________________________________________________________
[~]    diff --git a/c.txt b/c.txt                                  
       index 1e2c877..7bd5b45 100644                               
       2 hunks, 3 lines changed                                    
                                                                   
   [~]     @@ -1,3 +1,4 @@                                         
            ccc                                                    
      [x]  -ddd                                                    
      [ ]  +111                                                    
      [x]  +dDd                                                    
            eee                                                    
                                                                   
   [x]     @@ -3,0 +5,1 @@                                         
      [x]  +333                                                    
のような画面が表示されます。使い方はみたままです。 スペースキーでコミットする部分を行単位またはハンク単位、ファイル単位で選択できますので、選んだ後にcキーを押せばコミット処理が開始されます。

補足


(※1) Mercurialの設定ファイルに
[ui]
interface = curses
と書いておけば hg commit -i で使えます。

2020年12月23日水曜日

仮想ファイルシステムをユーザー権限でマウント

準備


libguestfs-toolsが必要ですので、apt等でインストールしておきます。

ファイルシステムの作成


マウントしてアクセスしたいファイルシステムを作成します。ここでは例として、ディレクトリ1個とファイルが2個あるファイルシステムを作ります。
$ mkdir fs
$ echo "abc" > fs/abc.txt
$ mkdir fs/sub
$ echo "cde" > fs/sub/cde.txt
$ tree
.
├── abc.txt
└── sub
    └── cde.txt

1 directory, 2 files
$ virt-make-fs -t ext4 -s +10M fs fs.img
これで、fs.imgにファイルシステムが作成されます。 -t ext4により、ext4でファイルシステムを作成することを指定しています。軽量なものが良ければ、ext2がよいそうです。 -s +10M により、空き容量を10MB付け加えたファイルシステムを作るよう指定しています。

ファイルシステムのマウント


ファイルシステムが作成できましたので、これをマウントしてみます。マウントポイントはmpとします。ルート権限は不要です。
$ mkdir mp
$ guestmount -a fs.img -m /dev/sda mp
$ tree mp
mp
├── abc.txt
├── lost+found
└── sub
    └── cde.txt

2 directories, 2 files
マウントできました。今回作成したイメージの場合、-mオプションは省略できません。

dfで見てもマウントできていることが確認できます。

$ df -h -t fuse
ファイルシス   サイズ  使用  残り 使用% マウント位置
/dev/fuse         11M  199K  9.1M    3% /home/xxx/yyy/mp
ファイルの書き込みもできますし、もともと用意していたファイルの表示もできます。
$ cd mp
$ echo "xyz" > xyz.txt
$ cat xyz.txt
xyz
$ cat abc.txt
abc
$ cat sub/cde.txt 
cde

ファイルシステムのアンマウント


アンマウントも簡単にできます。
$ guestunmount mp
$ tree mp
mp

0 directories, 0 files

まとめ


仮想ディスクの作成からマウントし、読み書きして、アンマウントするところまで簡単にできました。 この仕組みを使えば、何かデータを処理したい計算機にファイルシステムごとデータを移動したり、 結果を回収したり、バックアップしたりする作業がお手軽にできそうです。

2020年12月21日月曜日

Kubernetesでコンテナ間ssh接続を試してみる

はじめに


Kubernetes (k8s) のPOD間でssh接続できるように設定してみます。

minikubeのインストール


k8sの環境を作るのにminikubeを利用します。
https://minikube.sigs.k8s.io/docs/start/
を参考にインストールします。LinuxとWindows、macOSに対応しているようです。

ここでは、Windowsで試してみます。仮想マシンが必要ですのでVirtualBoxなどをインストールしておきましょう。

インストールが完了したら、クラスタを開始します。

Windowsなのでコマンドプロンプトを開いて、

minikube start
を実行します。

自動的に準備が始まります。最初にいろいろダウンロードしますが、大きいものは、

  • VMブートイメージ(minikube-v1.16.0) 212.6MiB
  • Kubernetes(v1.20.0) 491MiB
でした。 ダウンロードが完了すると、virtualbox上にVMを作成し始めます。今回実行した環境ではCPUs=2, Memory=6000MB, 20000MBで作成されました。

kubectlが見つからないと言われるので、メッセージに書いてあるとおり、

minikube kubectl --get pods -A
を実行すると、39.52MiBのダウンロードが始まり、完了するとkubectlコマンドを使えるようになります。 上記コマンド自体は
NAMESPACE     NAME                               READY   STATUS    RESTARTS   AGE
kube-system   coredns-74ff55c5b-srhzs            1/1     Running   0          2m35s
kube-system   etcd-minikube                      1/1     Running   0          2m50s
kube-system   kube-apiserver-minikube            1/1     Running   0          2m50s
kube-system   kube-controller-manager-minikube   1/1     Running   0          2m50s
kube-system   kube-proxy-s8wbb                   1/1     Running   0          2m35s
kube-system   kube-scheduler-minikube            1/1     Running   0          2m50s
kube-system   storage-provisioner                1/1     Running   1          2m50s
という出力をします(出力前にコマンドがなかったら自動でダウンロードされるということのようです)。

sshキーの作成


コンテナ内にアクセスできるユーザを制限するため、sshの公開鍵と秘密鍵を作成しておきます。 Linuxではssh-keygenコマンドで作成できます。Windows環境ならWSL1で動作しているLinuxで作成すれば良いでしょう。詳細は省略しますが、ここでは id_rsa_xyz というキーを作成したとして進めます。

Pod上で動作させるDockerイメージの作成


https://minikube.sigs.k8s.io/docs/handbook/pushing/によると、 minikubeにssh接続してDockerのイメージを作るのがもっとも簡単のようです。
minikube ssh
でminikubeのノード内(VirtualBox内の環境)に入ることができます。ただし、コマンドプロンプトから入るとエスケープシーケンスが処理できず、バックスペースで文字の削除すらできません。

WindowsではWSL1が利用できるので、そこから入ることで回避します(WSL1の準備方法は省略)。WSLのターミナルを立ち上げて(ここではwslttyを利用)、同じコマンドを実行すると、今度はエスケープシーケンスの処理が正しくできます。

どのユーザでログインしたのかは、

$ pwd
/home/docker
$ who
docker  pts/0    xxxxxx
で確認できます。接続先のユーザはdockerになっているようです。

c:\Usersが最初からマウントされていて、minikubeのノード内からは/c/Users/...でアクセスできますので、これを使えばホストであるWindowsとファイルのやりとりができます。

このディレクトリに先程作成したsshキーの公開鍵(ここではid_rsa_xyz.pub)をコピーしておきます。

次に、sshサーバを動作させるDockerイメージを作成します。今回はWindows上でsshsrv.dockerファイルを以下の内容で作成します。

FROM ubuntu:20.04
RUN apt-get update && apt-get install -y openssh-server
RUN mkdir /var/run/sshd
RUN useradd -m xyz && echo "xyz:xyz" | chpasswd
RUN mkdir -p /home/xyz/.ssh && chown xyz /home/xyz/.ssh && chmod 700 /home/xyz/.ssh
ADD ./id_rsa_xyz.pub /home/xyz/.ssh/authorized_keys
RUN chown xyz /home/xyz/.ssh/authorized_keys && chmod 600 /home/xyz/.ssh/authorized_keys
RUN bash -c "echo ForwardAgent yes" > ~/.ssh/config
CMD ["/usr/sbin/sshd", "-D"]
sshサーバの設定は
https://qiita.com/YumaInaura/items/1d5c18a9e55484ccad89
http://kotaroito.hatenablog.com/entry/2016/03/07/094423
を参考にしました。

なお、3行目の /var/run/sshd がないと

Missing privilege separation directory: /run/sshd
とメッセージが出てsshdを起動できません。

Dockerイメージを作成します。

docker build -f sshsrv.docker -t sshsrv:v1 .
sshdをコンテナ内で実行してみます。
docker run -d -p 222:22 --name sshsrv sshsrv:v1
-dはコンテナをバックグラウンドで動かすための引数です。付けわすれるとそのターミナルでは操作できなくなります(Ctrl+DもCtrl+Cも効かない )。--nameで名前を付けておくと、docker psしたときに名前が表示されます。

コンテナ外からsshできるか試してみます。Windowsからアクセスするわけではなく、minikubeのノード内からsshsrvコンテナにアクセスします。 アクセスには秘密鍵かつパーミッションが適切である必要があるので、minikubeのノード内に秘密鍵であるid_rsa_xyzをコピーしてパーミッションも設定しておきます。

cp /c/Users/path/to/id_rsa_xyz ~/.ssh/
chmod 600 ~/.ssh/id_rsa_xyz
ssh-agent bash
ssh-add ~/.ssh/id_rsa_xyz
ssh -p 222 xyz@localhost
とすると、動いているコンテナの中に入ることができます。

動作確認ができましたので、コンテナ内から抜けて、今のコンテナは停止しておきます。

docker kill sshsrv
docker rm sshsrv

あとでコンテナ間接続で使うので、.ssh/configも設定しておきます。

echo "ForwardAgent yes" > ~/.ssh/config
chmod 600 ~/.ssh/config
なお、できあがったイメージの中にsshを使わずに入るには、
docker run --rm -it sshsrv:v1 /bin/bash
を実行すれば中にはいれますが、こちらは今回の目的とは異なりますので使う必要はありません。

k8sで実行


sshサーバを動かすコンテナが準備できましたので、ここではk8s経由で複数のsshサーバを立ててみます。 今回は試しに動作させて見るだけですので、Deploymentを使わずReplicaSetを使ってみます。

まず、k8sに指示するためのsshsrv.yamlファイルを作成します。

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: sshsrvset
spec:
  selector:
    matchLabels:
      app: sshsrv-template-label
  replicas: 3
  template:
    metadata:
      labels:
        app: sshsrv-template-label
    spec:
      containers:
      - name: sshsrv-template
        image: sshsrv:v1
        ports:
        - containerPort: 22
spec.template.metadata.labels に記述しているapp: sshsrv-template-labelがコンテナのテンプレート名です。

spec.template.spec 以下に、コンテナの設定が記述されています。

spec.template.spec.containers.name がテンプレートそのものの名前で、k8sが立ち上げるコンテナの名前の一部になります。

spec.template.spec.containers.image は前節で作成したDockerイメージの名前で、ここで指定したDockerイメージからコンテナが作成されます。

spec.template.spec.containers.ports のcontainerPort:22はssh接続用のポート番号となります。コメントアウトしてもsshでアクセスできるのでなくても良いのかもしれません。

spec.selector.matchLabels の app: sshsrv-template-labelは spec.template.metadata.labels に記載した内容と同じにします。このラベルを使って、PODに適用するテンプレートを探しているようです。同じyamlファイル中のtemplateが1個だけなら原理的にはtemplateは一意に特定できるので、わざわざlabelを設定する必要はなさそうにも思うのですが、selectorとtemplateの両方を書かないと動作しないので仕方がありません。

spec/replicas の3は、3個同じコンテナを立ち上げることを指示しています。

さて、yamlファイルが準備できましたので、コマンドプロンプトから実行します(WSL1からだとパスが処理できずminikubeを実行できません)。

minikube kubectl -- apply -f sshsrv.yml
minikube sshでminikubeのノード内に入って、docker ps -aを実行すると、
$ docker ps -a
CONTAINER ID   IMAGE                  COMMAND                  CREATED         STATUS                     PORTS     NAMES
f14472bc0ace   66c4d55614da           "/usr/sbin/sshd -D"      3 minutes ago   Up 3 minutes                         k8s_sshsrv-template_sshsrvset-m8784_
default_48b8f9f7-0c48-4681-957e-96aad5d7ac57_0
8b9c796bc762   66c4d55614da           "/usr/sbin/sshd -D"      3 minutes ago   Up 3 minutes                         k8s_sshsrv-template_sshsrvset-t79pr_
default_72cb56bd-d4d8-42f5-8378-34ef7b6d9a1a_0
97849d1f2fbc   66c4d55614da           "/usr/sbin/sshd -D"      3 minutes ago   Up 3 minutes                         k8s_sshsrv-template_sshsrvset-wgqpr_
default_62c6fcee-26ac-4799-8e3d-59749ef8a3db_0
のような表示が得られます。3つ立ち上がっていることが確認できます。コマンドプロンプトに戻って
minikube kubectl -- get pods -o wide -l app=sshsrv-template-label
を実行すると、k8s上では
NAME              READY   STATUS    RESTARTS   AGE   IP           NODE       NOMINATED NODE   READINESS GATES
sshsrvset-m8784   1/1     Running   0          3m    172.17.0.5   minikube              
sshsrvset-t79pr   1/1     Running   0          3m    172.17.0.4   minikube              
sshsrvset-wgqpr   1/1     Running   0          3m    172.17.0.3   minikube              
のような名前でコンテナが立ち上がっていることがわかります。IPアドレスも別々に割り振られているので、 minikubeのノード内から
$ ssh xyz@172.17.0.5
のようにすると、コンテナ内に接続できます。

コンテナを止めるには、コマンドプロンプトから

minikube kubectl -- delete replicaset sshsrvset
を実行します。

minikubeのノード内から各コンテナにssh


前節のようにコンテナを立ち上げても、IPアドレスがわからない場合には接続できません。 そこで、ランダム接続にはなってしまいますが、Serviceを作ります。 Serviceをつくると各コンテナに直接接続せず、Serviceで作成したIPアドレスとポートにつなぎにいけば、コンテナのいずれかに接続することができます。

まず、sshd-service.yamlを作成します。

apiVersion: v1
kind: Service
metadata:
  name: sshd-service
spec:
  type: ClusterIP
  selector:
    app: sshsrv-template-label
  ports:
  - protocol: TCP
    port: 22
    targetPort: 22
重要な点は、spec.selectorに書くラベルはReplicaSetのspec.template.metadata.labelsに記載しているラベルということです。 ReplicaSetのmetadata.nameではありません。また、ReplicaSetのyamlにmetadata.labelsの項目を作ってそこにラベルを書いてもそこは検索対象になりません。 なぜ、template側を参照する設計になっているのかは謎です。高度すぎてまるで分かりません。

さて、コマンドプロンプトからServiceを立ち上げます。

minikube kubectl -- apply -f sshd-service.yaml
立ち上げ後、
minikube kubectl -- get service -o wide
を実行すると表示されるsshd-serviceの行のCLUSTER-IPをたたけば、ReplicaSetで立ち上げたコンテナのいずれかにランダム(?)で接続できます。
ssh xyz@xxx.xxx.xxx.xxx
xxx.xxx.xxx.xxxにはCLUSTER-IPに表示されたIPアドレスを入れます。

コンテナの中からさらに

ssh xyz@xxx.xxx.xxx.xxx
を実行すると、同様に接続できます。確証はありませんが、20回試した範囲では、接続先からは自分自身は除かれているようです。 ただし、2段の場合は自分自身に戻ってくることがあります。 コンテナA→コンテナB→コンテナAというパターンはあるが、コンテナA→コンテナAはなさそうということです。

ランダムではなく各ノードの負荷状況を考慮して接続するサーバを選びたいのですが、残念ながら方法がわかりませんでした。

コンテナ内からであれば、IPアドレスではなくドメイン名でアクセスすることもできます。

ssh xyz@sshd-service.default.svc.cluster.local
または、サービス名のみで
ssh xyz@sshd-service
でアクセスすることもできます。コンテナ内で/etc/resolv.confの中身を表示すれば、ドメイン名の解決のための 設定がどのようになっているのかが分かるかと思います。

Serviceの停止は、

minikube kubectl -- delete service sshd-service
で停止できます。

参考


https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.20/ がk8sのAPIリファレンスです。kindがたくさんありますが、各kindがどういう役割のものなのかは、ここには記載されていません。yamlに記載する各フィールドの説明はしっかり記載されていますので、各kindを理解してから読むようにしましょう。

各kindがどういうものなのかはは、https://kubernetes.io/ja/docs/home/の検索窓にkind名を入力して検索すれば説明ページがでてきますので、それを参考にしましょう。

DeploymentとStatefulSetとJobだけはAPIリファレンスに短い解説がありましたので引用します。

  • Deployments for stateless persistent apps (e.g. HTTP servers).
  • StatefulSets for stateful persistent apps (e.g. databases).
  • Jobs for run-to-completion apps (e.g. batch Jobs).
また、Pod, ReplicaSet, Deploymentについては https://blog.a-know.me/entry/2018/08/14/185324#Deploymentに解説記事があります。

kind名はたくさんありますし、一覧と概要説明がないと不便ですので、一部ではありますが一覧を作りました。 各説明文はkubernetes.io/ja/docsからの引用となります(kubernetes.ioのドキュメントのライセンス表記に従い、この一覧のみCC BY 4.0が適用されます)。

Kind説明
PodPod は、Kubernetesアプリケーションの基本的な実行単位です。これは、作成またはデプロイするKubernetesオブジェクトモデルの中で最小かつ最も単純な単位です。Podは、クラスターで実行されているプロセスを表します。
https://kubernetes.io/ja/docs/concepts/workloads/pods/pod-overview/
JobJobは1つ以上のPodを作成し、指定された数のPodが正常に終了することを保証します。 JobはPodの正常終了を追跡します。正常終了が指定された回数に達すると、そのタスク(つまりJob)は完了します。Jobを削除すると、そのJobが作成したPodがクリーンアップされます。簡単な例としては、1つのPodを確実に実行して完了させるために、1つのJobオブジェクトを作成することです。ノードのハードウェア障害やノードの再起動などにより最初のPodが失敗したり削除されたりした場合、Jobオブジェクトは新たなPodを立ち上げます。また、Jobを使用して複数のPodを並行して実行することもできます。
https://v1-18.docs.kubernetes.io/ja/docs/concepts/workloads/controllers/job/
ReplicaSetReplicaSetの目的は、どのような時でも安定したレプリカPodのセットを維持することです。これは、理想的なレプリカ数のPodが利用可能であることを保証するものとして使用されます。
https://kubernetes.io/ja/docs/concepts/workloads/controllers/replicaset/
DeploymentDeployment はPodとReplicaSetの宣言的なアップデート機能を提供します。Deploymentにおいて 理想的な状態 を記述すると、Deploymentコントローラーは指定された頻度で現在の状態を理想的な状態に変更します。Deploymentを定義することによって、新しいReplicaSetを作成したり、既存のDeploymentを削除して新しいDeploymentで全てのリソースを適用できます。
https://kubernetes.io/ja/docs/concepts/workloads/controllers/deployment/
https://qiita.com/tkusumi/items/01cd18c59b742eebdc6a
StatefulSetStatefulSetはステートフルなアプリケーションを管理するためのワークロードAPIです。StatefulSetはDeploymentとPodのセットのスケーリングを管理し、それらのPodの順序と一意性を保証します。 Deploymentのように、StatefulSetは指定したコンテナのspecに基づいてPodを管理します。Deploymentとは異なり、StatefulSetは各Podにおいて管理が大変な同一性を維持します。これらのPodは同一のspecから作成されますが、それらは交換可能ではなく、リスケジュール処理をまたいで維持される永続的な識別子を持ちます。ワークロードに永続性を持たせるためにストレージボリュームを使いたい場合は、解決策の1つとしてStatefulSetが利用できます。StatefulSet内の個々のPodは障害の影響を受けやすいですが、永続化したPodの識別子は既存のボリュームと障害によって置換された新しいPodの紐付けを簡単にします。
https://kubernetes.io/ja/docs/concepts/workloads/controllers/statefulset/
ServicePodの集合で実行されているアプリケーションをネットワークサービスとして公開する抽象的な方法です。Kubernetesでは、なじみのないサービスディスカバリーのメカニズムを使用するためにユーザーがアプリケーションの修正をする必要はありません。 KubernetesはPodにそれぞれのIPアドレス割り振りや、Podのセットに対する単一のDNS名を提供したり、それらのPodのセットに対する負荷分散が可能です。
https://kubernetes.io/ja/docs/concepts/services-networking/service/
SecretKubernetesのSecretはパスワード、OAuthトークン、SSHキーのような機密情報を保存し、管理できるようにします。 Secretに機密情報を保存することは、それらをPodの定義やコンテナイメージに直接記載するより、安全で柔軟です。
https://kubernetes.io/ja/docs/concepts/secret/configuration/
ConfigMapConfigMapは、機密性のないデータをキーと値のペアで保存するために使用されるAPIオブジェクトです。Podは、環境変数、コマンドライン引数、またはボリューム内の設定ファイルとしてConfigMapを使用できます。
https://kubernetes.io/ja/docs/concepts/configuration/configmap/
Ingressクラスター内のServiceに対する外部からのアクセス(主にHTTP)を管理するAPIオブジェクトです。Ingressは負荷分散、SSL終端、名前ベースの仮想ホスティングの機能を提供します。
https://kubernetes.io/ja/docs/concepts/services-networking/ingress/

2020年12月12日土曜日

VirtualBoxのゲストOSの時刻をNTPで同期させる

VirtualBoxでホストPCがサスペンドするとゲストOSの時刻がサスペンドしていた時間だけずれてしまうことがあります。

そこで、NTPを使って自動的に再設定されるようにしてみます。

ゲストOSが Debian 10 (buster) の場合は、次のようにすると自動設定されるようになります。 なお、ここで説明する方法では、ゲストOSをNTPサーバにはできません。

  1. ntpdやchronydが動作しているのであればアンインストールします。
  2. /usr/lib/systemd/system/systemd-timesyncd.service.d/disable-with-time-daemon.conf
    ConditionFileIsExecutable=!/usr/sbin/VBoxService
    
    の行をコメントアウトします(先頭に#を入れます)。
  3. systemctl daemon-reloadを実行して、設定を更新します。
  4. 最後に、systemd-timesyncd を
    systemctl start systemd-timesyncd
    で開始します。
  5. 動作確認は、systemctl status systemd-timesyncdtimedatectlでできます。
設定後、ホストPCをサスペンドさせ、しばらくしてから復帰させると、これまで通りゲストOSの時刻がずれますが、 しばらく待っていると自動的に正しい時刻に設定されます。

2020年9月26日土曜日

xtensor

C++でもnumpyっぽく使えるxtensorを試してみます。

インストール

まずは、インストール。次のスクリプトを実行します。

#!/bin/bash
git clone https://github.com/xtensor-stack/xtensor.git
git clone https://github.com/xtensor-stack/xtl.git
git clone https://github.com/xtensor-stack/xsimd.git
git clone https://github.com/xtensor-stack/xtensor-blas.git

INSTALL_PATH=$(pwd)/root
for x in xtl xsimd xtensor xtensor-blas
do
    echo "-------- $x ----------"
    cd $x
    mkdir -p build
    cd build
    cmake -DCMAKE_INSTALL_PREFIX=$INSTALL_PATH ..
    make install
    cd ../..
done

行列積の計算時間

行列積の計算時間を測定してみます。測定する処理を書いたC++のコードは次の通り。10000x10000の2つの行列の積を計算し、その後、平均値を計算します。これを5回繰り返します。なお、平均値は10000になります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include "xtensor.hpp"
#include "xtensor-blas/xlinalg.hpp"

void dot(void){
  xt::xarray<float> a = xt::ones<float>({10000, 10000});
  xt::xarray<float> b = xt::ones<float>({10000, 10000});
  for(int i=0; i<5; ++i){
    auto d = xt::linalg::dot(a, b);
    auto c = xt::average(d);
    std::cout << c << std::endl;
  }
}

int main(void){
  dot();
  return 0;
}
このファイルをdot.cppとして保存し、次のようなCMakeLists.txtを作成します。xtensor_pathの値は適切なものに書き換えてください。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
cmake_minimum_required(VERSION 3.10)
project(xtexample VERSION 1.0)
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED True)

add_definitions(-DHAVE_CBLAS=1)
find_package(BLAS REQUIRED)
find_package(LAPACK REQUIRED)

add_executable(xtexample dot.cpp)
set(XTENSOR_USE_XSIMD ON)
set(xtensor_path "/path/to/xtensor/root")
set(xtensor_DIR "${xtensor_path}/lib/cmake/xtensor")
set(xtl_DIR "${xtensor_path}/lib/cmake/xtl")
set(xsimd_DIR "${xtensor_path}/lib/cmake/xsimd")
find_package(xtensor REQUIRED)
target_include_directories(xtexample PUBLIC ${xtensor_INCLUDE_DIRS})
target_link_libraries(xtexample PUBLIC xtensor xtensor xtensor::optimize xtensor::use_xsimd
                      ${BLAS_LIBRARIES} ${LAPACK_LIBRARIES})
ビルドします。
$ mkdir build
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Release ..
$ make
処理時間を測定します。
$ for _ in {1..4}; do time ./xtexample; done
結果は次の通りです。4コア8スレッドのCPUで測定しています。
Trial #realusersys
10m43.805s5m17.723s0m23.991s
20m46.952s5m44.626s0m21.066s
30m49.060s5m59.719s0m22.163s
40m50.819s6m9.617s0m25.812s

Python numpyとの比較

比較のため、Pythonのnumpyで同じことをしてみます。測定コードは以下の通り。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import numpy as np

def dot():
    a = np.ones((10000, 10000), dtype=np.float32)
    b = np.ones((10000, 10000), dtype=np.float32)
    for i in range(5):
        d = np.dot(a, b)
        c = np.average(d)
        print(c)

dot()
C++の場合と同様に実行します。
for _ in {1..4}; do time python3 comp.py; done
結果は次の通り。
Trial #realusersys
10m44.816s5m29.361s0m17.726s
20m49.529s6m5.012s0m21.217s
30m49.188s5m58.570s0m24.681s
40m50.497s6m11.620s0m22.015s
C++と同じくらいの結果となりました。

まとめ

C++でnumpyのように行列計算を使いたい場合は、xtensorを使えばよさそうです。 処理速度に関しても、少なくとも行列積に関してはnumpyに比べて遅いわけではないようです。

2020年7月4日土曜日

カメラで撮影した紙の画像の濃淡除去

目的


皺があったり光を一様に当てられない環境で紙をカメラで撮影すると、明るい部分と暗い部分ができてしまいます。
画像を二値化したいとき、このような明るさの変化が二値化の障害になることがあります。 そこで、これを緩和する方法を試してみます。 全体的な明度差のある画像に対応できていないOCRを使うときの前処理のひとつとして使えるかもしれません。

手法


日本の特許(登録番号3909604)を使ってみます。

畳み込み演算でぼかした画像のピクセルで元のピクセルの値を割った値を元に画像を再構成することで、細かい構造を残しながら全体的な濃淡を除去します。

詳細は https://www.inpit.go.jp/blob/katsuyo/pdf/business/19t1-2.pdf または、特許公報を参照してください。

ソースコード


gray.pngを読み込んで処理したあと、mod.pngに書き込みます。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import numpy as np
import cv2

def flatten(img, kernel_size=77):
    # Make a kernel
    half = kernel_size//2
    k = np.array(list(range(1,half+1))+[half+1]+list(range(half, 0, -1)))
    ks = [k]
    for i in range(2, half+2, 1):
        ks.append(k*i)
    for i in range(half, 0, -1):
        ks.append(k*i)
    k = np.stack(ks)
    k = np.array(k, dtype=np.float32)/np.sum(k)

    # Calculate uneven coefficients (JPP3909604)
    ave = cv2.filter2D(img, -1, k).astype(np.float32)
    a = img.astype(np.float32)/ave

    # Make a flatten image
    a = (a-1)*768+255
    a[a>255] = 255
    a[a<0] = 0
    return a.astype(np.uint8)

if __name__ == '__main__':
    img = cv2.imread('gray.png')
    img = flatten(img)
    cv2.imwrite('mod.png', img)


処理例を以下に示します。

処理前の画像

処理後の画像
文字周りの明度はある程度一定になっていることがわかります。

2020年6月14日日曜日

よくある文書フォーマット

ただのメモです。よくあるのはこういう形式。灰色の文字は書いてはいけませんw。

20xx年x月x日
あいうえお様(受取人名)
かきくけこ(差出人名)

(件名)さしすせそについて

 平素よりほにゃららほにゃららお礼申し上げます。
 かきくけこは・・・ご案内いたします。


1.あああ

2.いいい

3.ううう

以上


2020年6月13日土曜日

世界各国のコレステロール値

世界各国でのコレステロール値の変化。詳細は[1]を参照。

non-HDL


1980年と2018年の、年齢標準化されたHDLでないコレステロールの平均値を表したものが下図([1]のFig.3)。赤いほど値が大きい。
a: 1980年女性, b: 2018年女性, c: 1980年男性, d: 2018年男性

同図のキャプションに、"One mmol/l is equivalent to 38.61 mg/dl." とあるので、カラーバー中央の黄色で3.5mmol/l x 38.61 ≃ 135.1 mg/dl。 日本は、女性の値が低下している一方、男性の値は色で見る限りでは変化していない。

HDL


1980年と2018年の、年齢標準化されたHDLコレステロールの平均値を表したものが下図([1]のExtended Data Fig. 6)。

カラーバーの濃緑色付近の1.4 mmol/l x 38.61 ≃ 54.1 mg/dlである。 「HDL-コレステロール値が低いと診断された方へ」によると、 HDLの基準値は1.036 mmol/l以上であるので、赤色は平均値が基準以下を表している。日本は男女とも上昇している。

引用元文献


[1] Taddei, C., Zhou, B., Bixby, H. et al. Repositioning of the global epicentre of non-optimal cholesterol. Nature 582, 73–77 (2020). https://doi.org/10.1038/s41586-020-2338-1 (Open Access, CC BY 4.0)

2020年4月19日日曜日

Emacsで入力補完 その2

はじめに


先日の記事ではEmacsのcompany-modeの設定をしましたが、 CまたはC++を使う場合のプロジェクト内のヘッダファイルを検索対象にするために、ひと手間必要でした。

ここではそれを自動化するための設定をしてみます。

設定


プロジェクトルートを見つけるため、最初にProjectileをインストールします。
M-x package-install [Enter] projectile [Enter]
次に、~/.emacs.d/init.elに以下の内容を記載します。
(require 'company)
(global-company-mode t)
(define-key global-map (kbd "C-f") 'company-complete)
(setq company-backends '((company-clang company-dabbrev-code company-gtags)))

(require 'projectile)
(define-key projectile-mode-map (kbd "C-c p") 'projectile-command-map)
(projectile-mode +1)

;; Return a project root if it is found else a current directory                                                                                                  
(defun pproot () (let ((d (projectile-project-root))) (unless d (setq d ".")) d))

;; Find all include paths and set them to clang include path                                                                                                    
(defun set-company-clang-include-path ()
  (interactive)
  (setq company-clang-arguments
        (mapcar (lambda (x) (concat "-I" x)) ;; Make clang options to set include path                                                                          
                (seq-uniq (mapcar (lambda (x) (replace-regexp-in-string "/[^\/]*$" "" x)) ;; Extract directories                                                
                                  (directory-files-recursively (pproot) ".*\.\\(h\\|hpp\\)$")))))) ;; Find all header files
(defun set-company-clang-include-path-if-empty ()
  (unless company-clang-arguments (set-company-clang-include-path)))
(add-hook 'c++-mode-hook 'set-company-clang-include-path-if-empty)
(add-hook 'c-mode-hook 'set-company-clang-include-path-if-empty)
プロジェクトルート以下の.h/.hppをすべて検索するため、大きめのプロジェクトでは、 c-modeまたはc++-modeでファイルを開くと最初は時間がかかるかもしれません。

company-clang-argumentsを更新する場合は、M-x set-company-clang-include-pathを実行します。

2020年4月12日日曜日

Emacsで入力補完

はじめに


EmacsにIntellisenseっぽいものを導入してみます。

準備


Packageを使う準備ができていない場合は、
https://melpa.org/#/getting-started
に従って準備します。

設定ができたらemacsを立ち上げて、 M-x package-list-packages を実行します。

パッケージの一覧が表示されますので、companyの行へ移動します。なければ r を押してください。リストが更新されます。

companyの行へ移動できたら i を押します。すると、その行に I が表示され、インストール準備が完了した状態になります。

最後に x を押すとインストールしてよいかの確認の後、インストールされます。

使い方


emacsを立ち上げて、 M-x global-company-mode を実行すれば有効になります。

C++のファイルを開いているときにcompany-modeを有効にしようとして、もし、

Company backend ’company-clang’ could not be initialized:
Company found no clang executable
というエラーがでた場合は、clangがインストールされていないということなので、aptなどでclangをインストールします。

この状態でも使えるのですが、emacsを起動するたびにglobal-company-modeを有効にしなければならないので、 ~/.emacs.d/init.el に次の内容を追加しておきます。

(global-company-mode t)
(global-set-key (kbd "C-f") 'company-complete)
このようにすると、Ctrl+f で自動補完が動作するようになります。 カーソル移動にCtrl+fを使う人は別のキーに割り当てましょう。

C++のプロジェクト内で定義した関数等の補完


C++では、global-company-modeを有効にしただけでは標準の関数等に対してしか補完は行われません。 今まさに作っている最中のプロジェクト内のファイルに対する補完が効かないということです。これは、補完のために呼び出されるclangが読み込むべきファイルのパスを知らないためです。

そこで、プロジェクトのルートに次のような内容を記載した .dir-locals.el という名前のファイルを作成します。

((nil . ((company-clang-arguments . ("-I/path/to/include/"
                                     "-I/path/to/2nd/include/")))))
これで、指定したパスのファイル内で定義されている関数等は補完の対象になります。サブディレクトリを再帰的に見てくれるわけではないので注意が必要です。

なお、このファイルを作成すると、emacs起動時に

The local variables list in /path/to/include/ contains values that may not be safe (*).
と聞かれるようになってしまいます。設定値に文字列が入っていると聞いてくるようです。許可すればいいだけですが、ちょっと手間ですね。

まとめ


使えることは使えるのですが、プロジェクトごとにファイルを準備しないといけないため、 常用するにはもうひと工夫必要になりそうです。

参考


company の説明
http://company-mode.github.io/
http://emacs.rubikitch.com/company/
https://qiita.com/syohex/items/8d21d7422f14e9b53b17

emacs設定いろいろ
http://tuhdo.github.io/c-ide.html

clangが見つからないと言われたら
https://emacs.stackexchange.com/questions/19306/how-do-i-get-company-mode-to-recognize-clang

.dir-locals.el の説明
https://www.emacswiki.org/emacs/DirectoryVariables

bashのキーボード・ショートカット一覧
https://ss64.com/bash/syntax-keyboard.html

2020年3月1日日曜日

For 〜ingとTo infinitiveの違い

すぐに忘れてしまうので、メモ。

For 〜ing : 機能か理由を表す場合

To inifinitive : 目的か意図を表す場合

例を含む詳細は以下のページで確認できます。
https://ell.stackexchange.com/questions/24553/how-can-i-decide-when-to-use-for-ing-or-to-infinitive-in-a-sentence
https://dictionary.cambridge.org/us/grammar/british-grammar/for-ing

2020年2月16日日曜日

インフルエンザの報告数の推移

報告数


新型コロナウイルスの話があふれていますが、毎年流行っているインフルエンザの報告数について調べてみました。

指定届出機関からのインフルエンザの週ごとの報告総数です。 データは https://www.niid.go.jp/niid/ja/data.htmlから入手できます。

今年は2018年や2019年に比べて3分の1くらいになっています。2019年12月頃までは2018年に比べて多かったにもかかわらず、年が明けると例年並みに減り(1週目の値)、そこから報告数が拡大していません。どうしてなのでしょうね。

死亡率


ついでに、インフルエンザによる死亡率についても調べてみました。 米国のデータでは、 https://www.ncc.go.jp/jp/ncce/division/safety_management/about/kansen/020/010/20181002160441.html から引用すると、
年齢10万人あたりの死亡者数(米国)
25歳-34歳0.1
65歳-74歳2.2
75歳-84歳8.7
85歳以上43.3
とのこと。

リンクされている元データをたどってみると、 https://www.cdc.gov/nchs/data/nvsr/nvsr68/nvsr68_09-508.pdf のTable 5がそれっぽいので、その値をみてみると、

年齢10万人あたりの死亡者数(米国) 2017年
25歳-34歳0.9
65歳-74歳29.6
75歳-84歳93.8
85歳以上375.3
何か桁が1つ違うような。どうしてだろう。

それはともかく、10万人あたりだと実感がわかないので、%にしてみると、

年齢死亡率(%) (米国) 2017年
25歳-34歳0.0009
65歳-74歳0.0296
75歳-84歳0.0938
85歳以上0.3753
となりました。

日本での死亡率は https://www.hosp.med.osaka-u.ac.jp/home/hp-infect/file/ictmon/ictmon162.pdf に掲載されている図3から数値をざっくり読み取ったところ、

年齢死亡率(%) (日本)
2009年8月3日〜2009年12月13日
30歳-39歳0.001
60歳-69歳0.016
70歳以上0.032

でした。年齢の区分が異なるのでなんともいえませんが、日米ともだいたい同じくらいなのかな。

インフルエンザとは?


インフルエンザの説明は
https://www.niid.go.jp/niid/ja/kansennohanashi/219-about-flu.html
あたりが参考になります。

2020年2月8日土曜日

C++のビルド済みバイナリをpipでインストール

目的

C++ と cmake で作った実行ファイルを、pipでインストールできる形式にします。 加えて、pythonのライブラリも添付します。

なお、PyPIへのアップロード・配布は考慮していません。

作り方

pythonの準備

skbuildを使うので、pip install scikit-build でインストールしておきます。

python 3.8.1 で動作確認しています。

pipでインストールするC++のプログラムの準備

練習用C++プログラムを準備します。既存の、C++ & cmakeでビルドするプログラムがあれば、ここはスキップします。

まず、練習用C++プログラムの本体です。

[aaa.cpp]

1
2
3
4
5
6
7
8
9
#include <iostream>

int main(int argc, char *argv[]){
  std::cout << "aaa bbb ccc" << std::endl;
  for(int i=0; i<argc; ++i){
    std::cout << argv[i] << std::endl;
  }
  return 0;
}
文字列を表示するだけのプログラムです。

これをビルドするCMakeLists.txtを作成します。

[CMakeLists.txt]

1
2
3
4
cmake_minimum_required(VERSION 3.11.0)
project(projAAA)
add_executable(aaa aaa.cpp)
install(TARGETS aaa RUNTIME DESTINATION .)
ここでは aaa という名前のコマンドを作成しています。C++やcmakeの説明は省略します。

setup.py

配布パッケージを作成するためのPythonスクリプト setup.py を作成します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from skbuild import setup
setup(name="bdist-template", # パッケージの名前。pipでインストール後、pip list で表示される名前
      version="0.0.0",
      author="Your NAME",
      author_email="your@name.com",
      packages=["bdist_example"],  # importするときに使う名前。nameと一致してなくても良い。
                                   # 同じ方がユーザーには親切かも。
      cmake_install_dir="bdist_example/bin", # 最初のディレクトリ名はpackagesで指定した名前と同じにする
      entry_points={
          "console_scripts": [
              "cmdaaa=bdist_example:python_function_aaa"
          ]
          # cmdaaa がpip install後にシェルから使えるコマンド名になる
          # =と:の間は、packagesで指定した名前と同じにする
          # :の右側は後ほど説明
      },
      url="http://yourname.abc",
      download_url="https://yourname.abc/download",
      description="Test package", # このパッケージの短い説明文
      long_description="Test package !!!", # このパッケージの詳細な説明文
      classifiers=[
          "Programming Language :: C++",
          'Programming Language :: Python'
      ],
      license="???" # ライセンス名を記載(GPLとかApacheとか)
)

__init__.py

pipでインストールする場合、C++でビルドした実行ファイルをそのまま実行するのではなく、Pythonを一旦経由するので、 そのためのスクリプトを準備します。

まず、 setup.py のpackagesで指定した名前(ここではbdist_example)のディレクトリを作成します。 そこに次のような内容を含む __init__.py を作成します。

[bdist_example/__init__.py]

1
2
3
4
5
6
7
8
import os
import subprocess
import sys

CMAKE_BIN_DIR = os.path.join(os.path.dirname(__file__), "bin")

def python_function_aaa():
    raise SystemExit(subprocess.call([os.path.join(CMAKE_BIN_DIR, "aaa")] + sys.argv[1:]))
"aaa"の部分を CMakeLists.txt で書いたプログラム名と一致させます。

python_function_aaa は setup.py の console_scripts の部分の : の右側部分に書いた名前と一致させます。

同梱するスクリプト

次のスクリプトも同梱するようにします。バイナリのみの配布であれば、ここはスキップします。

[bdist_example/aaalib.py]

1
2
def print_aaaaa():
    print("aaaaa")

.whlファイルの作成

ここまでそろうとパッケージを作ることができます。

$ python setup.py bdist_wheel
を実行すると、distディレクトリの中に.whlファイルが作成されます。

インストール

作成した.whlを次のようにしてインストールします。

$ pip install dist/bdist_template-0.0.0-cp38-cp38-linux_x86_64.whl 
Processing ./dist/bdist_template-0.0.0-cp38-cp38-linux_x86_64.whl
Installing collected packages: bdist-template
Successfully installed bdist-template-0.0.0

インストールされたことの確認は、

$ pip list
Package        Version
-------------- -------
bdist-template 0.0.0  
packaging      20.1   
pip            20.0.2 
pyparsing      2.4.6  
scikit-build   0.10.0 
setuptools     41.2.0 
six            1.14.0 
wheel          0.34.2 
でできます。bdist-templateがインストールされていることがわかります。

動作確認

先程作成したコマンドが呼び出せることを確認します。

$ cmdaaa x y z
aaa bbb ccc
/home/user/.pyenv/versions/3.8.1/envs/py38/lib/python3.8/site-packages/bdist_example/bin/aaa
x
y
z
C++でビルドしたコマンドそのものは aaa でしたが、setup.pyで書いたコマンド名 cmdaaa が有効になっていることが確認できました。

同梱したスクリプトをpythonから実行してみます。

>>> import bdist_example.aaalib
>>> bdist_example.aaalib.print_aaaaa()
aaaaa

おまけ1

データを同梱したい場合は、次のようなコードを setup.py のsetup関数に渡す引数に追加します。

setup(...
      include_package_data=True,
      package_data={
          "bdist_example": ["data/*.dat"],
      }
)
ここでは、bdist_example/data/*.dat にデータがあると仮定しています。

おまけ2

.whlのファイル名でインストールできるpythonのバージョンが制限されるので、

$ mv bdist_template-0.0.0-{cp38-cp38,py3-none}-linux_x86_64.whl
のようにリネームしておくと、Python3ならインストールできるようになります。

ファイル名での制限を外すと、動作しないバージョンのPythonにインストールできてしまうので、代わりに、

setup(...
      python_requires='~=3.6'
)
のように指定しておきます。バージョン指定の書き方は
https://packaging.python.org/guides/distributing-packages-using-setuptools/#python-requires
が参考になります。

まとめ

skbuild により、 setup.py と __init__.py を準備するだけで .whl ファイルを作ることができました。

参考文献

scikit-build (skbuild) のドキュメント
https://scikit-build.readthedocs.io/en/latest/usage.html#basic-usage

cmake-python-distributions のコード
https://github.com/scikit-build/cmake-python-distributions

プロジェクトのパッケージングと配布
https://python-packaging-user-guide-ja.readthedocs.io/ja/latest/distributing.html
https://packaging.python.org/guides/distributing-packages-using-setuptools/