波物語クラスターのまとめ

波物語について

愛知県常滑市で「NAMIMONOGATARI2021」(以降、波物語)という野外音楽フェスが 2021 年 8 月 29 日に開催されました。報道によれば、緊急事態宣言下で 7392 人の参加者があり、会場での酒販・飲酒、一部参加者のマスク非着用が認められ、また参加者同士の距離を十分に取らない身体接触や出演者も煽る形での声出しが行われました。

陽性者とクラスタ

愛知県等の発表資料によると、波物語に関係する新型コロナウイルスの陽性者は、9 月 19 日現在で 47 名です。この内訳は次の通りです。

  • 愛知県がクラスターとして認定し発表した「イベントクラスター(10L)」:27 名
  • 愛知県と名古屋市が実施した、無症状(自己申告)の参加者に対する無料 PCR 検査(検査キットの郵送)
    • 愛知県実施分(351 検査):4 名
    • 名古屋市実施分(307 検査):4 名
  • 愛知県外陽性事例
  • 岡崎市在住の参加者から拡大した事例:2 名
  • 上記の合計 = 27 + 4 + 4 + 3 + 3 + 1 + 2 + 1 + 2 = 47 名

このうち、クラスター(10L)の 27 名、および岡崎市 10 代男性から同居家族(岡崎市 40 代女性、同 10 代男性)への拡大を 1 つの経路図として図示したものが、次の図です。

f:id:oxon:20210919082843p:plain
波物語クラスター(10L)の感染経路図

ここで注意したいのが、名古屋市管轄や愛知県管轄の陽性事例は接触経路が 8 月から非公表になったということです。そのため、岡崎市豊川市豊橋市豊田市以外に居住する事例の場合、接触経路は不明です。つまり、仮に 2 次感染があったとしても、それが明らかになるのはこの図では岡崎市事例と豊橋市事例のみになります。

この図では岡崎市豊橋市在住者は 3 名だけですので、大雑把には 3 分の 1 の確率で(保健所の追跡可能な)2 次感染が発生すると言えます。単純計算で 27 名中 9 名が 2 次感染をさらなる 2 名に起こしていたとすると、2 次感染の人数は 18 名程度と推測できます。

また、愛知県は単にクラスター 10L とだけ公表しておりその中でどのような接触経路が存在したかまでは公開していないため、先頭の稲沢市 20 代女性が全員に感染させた、いわゆるスーパースプレッダーであったという意味でもありません。感染日と、発症・受診・検査の時期は人によって異なります。あくまでこの図は陽性判明の時系列を表しているにすぎません。

この感染経路図の作成方法については、愛知県・岐阜県の感染経路図を可視化した際に書いた記事を参照してください。(この頃はまだ 329 事例でしたが、結局愛知だけで 10 万事例を超えたのにまだ継続しています。)
oxon.hatenablog.com
oxon.hatenablog.com

愛知県が通常「クラスター」として認定し公表するのは、次の条件を満たしている場合だと考えられます。

  1. 同一の場所で発生していること(家庭での拡大など、2 次感染は含まない)
  2. 感染者同士の接触経路が追えていること(濃厚接触が確定もしくは、職場の同僚など接触した可能性が高い)
  3. 合計で 10 名以上であること

※ ただし、豊橋市の高校でかなりの生徒に感染が広がった事例は、同一時間、同一場所ではないということでクラスター認定されなかった。
※ また、保健所の基準で濃厚接触ではなく接触が疑わしい程度だと、同じ職場で 10 名を超えていてもクラスター認定されない場合がある(例えば愛知県内の最初のデルタ株事例は職場感染を含め合計 26 名までの拡大が追跡されたが、職場内での接触経路は公開されずクラスター認定もなかった)。

しかし、この波物語クラスターは本当に愛知県がこれまで採用してきた「クラスター」の定義に合致するかは怪しいところです。27 人もいて、接触を辿れる同一のグループ行動を音楽フェス内でしていたとは考えにくいと思います。実際、愛知県外の事例は大規模な人数ではなく、またこの 27 人の居住地もあまりにバラバラです。

9 月 2 日の稲沢市の 20 代男性は稲沢市の消防士であることが報道から分かっており、この方は友人 2 人と参加したと発表されています。したがって、この 27 人はいくつかのグループに分割するのが自然であり(会場で同じ日に感染したものの、互いに接触していない複数のグループに分かれる)、愛知県がこれまで「クラスター」と呼んでいた形態とは異なるのではないかと思います。

また時系列を追ってみると分かりますが、稲沢市在住者 4 名が先頭に固まっていること、9 月 10 日の東海市事例は当初、経路不明(塗り潰し)として公表されていたこと、後半は名古屋市在住者が多数を占めることなどから、この「クラスター」と愛知県が呼んでいるものは、会場内で同時多発したいくつかの小規模クラスターなのではないかと自分は推測しますれます。またもし普段から行動をともにしている友人同士だった場合、たまたま波物語の開催時期に重なっただけで、その前からもしくは事後に友人同士で感染していた(した)可能性もあります。

リスク評価

2 次感染は無視したとして、7392 人の参加者のうち判明しているだけで 45 名の陽性事例が 14 日間にわたり発生したことになります。愛知県の 10〜20 代人口は約 150 万人です。また 9 月 1〜14 日の期間における愛知県内 10〜20 代の陽性事例は 6777 事例(10 代 2456、20 代 4321)です。したがって、これまた非常に単純な計算をすると (45/7392) / (6777/1500000) = 1.35 倍の確率で波物語は陽性事例が発生しやすい状況であったということになります。クラスターとして認定されていなかったり、参加した旨を申告していない感染者も実際にはいるでしょうから、多めに見積もって約 2 倍だと考えましょう。

これを多いと見るか、それとも大したことないと考えるかは、ちょっと難しいところです。(完全に偏見と、自分自身のクラブに行ったりしていた 20 年前の経験ですが)そもそも波物語の客層は新型コロナウイルスに感染しやすい交友関係を持っていた可能性があります。つまり、波物語に参加しなくても、遅かれ早かれどうせ友達同士で感染していたであろう人たちが、波物語をきっかけに感染した可能性があります。

例えば名古屋大学の学生数は約 15000 人ですが、このうち 9/1〜14 の期間で大学から公表された感染者数は 4 名です。上述の愛知県内 150 万人の 10〜20 代の感染事例 6777 と比較すると、10 分の 1 程度の発生頻度でしかなく、新型コロナに感染しやすい層としにくい層がいるのは明らかです。

波物語だけを敵視しなくても、4 人程度の若者同士の会食は愛知県内で 2000 回よりは桁で多く発生していると思われ、そのような場は県内全体の平均的な感染確率よりも高い環境であり、第 5 波の感染拡大に圧倒的に寄与しているはずです。

もちろん野外音楽フェスでもライブでも感染対策をするに越したことはありません。しかし「自粛の要請」という意味の分からない日本語を行政が振りかざし、特定のフェスにだけ愛知県知事が検証委員会を作って槍玉にあげるのは、「やっている感」を出したり怨嗟の吐口にするだけであって、日本全体、愛知県全体の感染拡大防止としては効果が薄いのではないかと思います。

この大雑把な計算で野外音楽フェスのリスク評価をするのは難しいですが、愛知県内全体の感染に比べて特に問題視するようなことではない、というのが自分の印象です。もっと定量的な評価は愛知県や国がしっかり行う必要があると思います。(感染拡大自体を自分は問題視しているので、波物語が感染防止対策を徹底しなかったのは問題視されるべきだが、波物語だけを攻撃するのは違うのではないか、ということです。)

計算間違いや、より定量的な評価方法などがあれば、ご指摘ください。

子供(10 歳未満)の感染経路はどうなっているか、20000 件超の感染経路図から考える

背景

4 月から開始した愛知・岐阜における新型コロナ感染経路の可視化は、既に報告件数 20000 件超を扱うようになりました。

oxon.hatenablog.com

このうち会食や職場など様々な感染経路が存在しますが、10 歳未満の子供の新型コロナの感染経路の実態はどうなっているのか。より個人的な観点からは、我が家の子供達が小学校や保育園で感染してくるか、また家庭内感染へと広がりうるのか。そういうことを実際の感染事例から概観してみたいと思います。

※1 この記事に書く内容は、あくまで愛知県、名古屋市豊田市岡崎市豊橋市岐阜県岐阜市の公開データに基づきます。そのため、症例の詳細や保健所や当事者しか知らない感染経路の本当のところは分かりません。
※2 これは医療関係者でも感染症の専門家でもなんでもない、素人の blog 記事です。
※3 10 歳未満に限っているのは、10 代を含めると(公開データが年代別になっており)18 歳以上の行動パターンと区別が困難になるためです。

国民の多くに新型コロナ対策が浸透し、それでもなお第 3 波が愛知県で広まり始めたのは 2020 年 10 月中旬のことです。このうち、11 月 1 日以降の陽性報告で陽性者の年齢が「10 歳未満」「0 歳」「1 歳未満」となっている事例と、それら事例に接触者・濃厚接触者として関連づけられている事例のみで、全ての感染経路図を作成しました。

かなり巨大な PDF として GitHub に公開しています。「2021-01-08(愛知、10 歳未満の子供を含むクラスターのみを表示)」というリンクがそれです。
github.com

PDF への直リンクはこちらです。
https://github.com/akira-okumura/COVID-19/raw/master/PDF/Aichi2021-01-08_kids.pdf

保育施設におけるクラスター事例

さてこのうち、保育施設で発生した大きいクラスターは 2 件のみです。小学校では自分の知る限り報道に出る規模のクラスターは発生していません。

クラスター 3E

1 つ目は愛知県がクラスター 3E と呼んでいる名古屋市の保育施設で発生したもの。これは職員と思われる 20〜40 代の方々と、施設利用者である 10 歳未満の子供たちで構成されます。ここで、報告順が左から並んでいるからといって、先頭の方がウイルスを持ち込んだかのように解釈しないでください。感染日と発症日と陽性確定日は人によって前後するためです。また線が繋がっているからといって接触している、感染させた、とは限りません。多数の線が出ているからといって「スーパースプレッダー」というわけでもありません。

f:id:oxon:20210109164425p:plain
名古屋市保育施設(クラスター 3E)

このうち、施設職員と母親(と思われる)に繋がっている子供は 9 名中 1 名のみですが存在します。すなわち、もし施設内でまず感染が広がり、その後この子供が家庭内感染を起こしたとすると、感染をある場所から他の場所へ広げていることになります。逆に家庭内で先に感染が起き、その後施設内でクラスターを発生させた可能性もありますが、その場合もやはり、10 歳未満の子供が他の場所へ拡大させる役割を持つことが分かります。

この図からは少なくとも感染の上流がどちらであったかは判断つきません。しかし 10 歳未満の子供(おそらく 6 歳以下)が感染を他の場所へ移動することは間違いありません。

クラスター 3G

2 つ目は同様に名古屋市内の保育施設もしくは学校のクラスター 3 Gです。これも 3E と同様、20〜60 代の職員と思われる方々と子供たちで構成されます。ここでも 9 名の子供のうち 1 名は家庭内感染と繋がっているため、他の場所へと感染を拡大させる場合があることが分かります。

f:id:oxon:20210109164607p:plain
名古屋市保育施設・学校(クラスター 3G)

クラスター 3G と 3E から分かること、分からないこと

クラスター 3E と 3G の事例の子供たちは家族構成が比較的似通っている(30〜40 代の両親もしくは片親、兄弟姉妹など)と考えられるため、このような家族構成の子供 18 事例中、ウイルスを他の集団に移動させたのは 2 事例だと言えます。ただし、他の家庭でも感染は起きていたのに、無症状かつ PCR 検査にかからなかった可能性は排除できません。

それぞれの施設内でどのように感染が広まったかは明らかになっていないため、子供達が遊ぶときに互いにベタベタ触ったのか、それとも子供や職員の飛沫感染が起きたのかは分かりません。しかし、子供から家庭感染への拡大が起きにくいことを考えると、子供から職員へ移したという可能性は低いのではないかと思います。親子の接触に比べると、子供と職員の接触は薄いためです。

大垣日大高校

ここで、10 歳未満ではありませんが、飲み会による感染を起こさないと思われる高校生のクラスター事例も見てみましょう。

f:id:oxon:20210109171359p:plain
大垣日大高校クラスタ

学校内でどのように感染が広がったかは報道にありませんが、高校生と思われる 10 代の 28 名のうち、家庭内感染は 4 件のみです。小さい子供に比べると親子の接触は減っていると思われますが(経験談)、3G・3E の 18 事例中の 2 事例と同程度の、28 事例中の 4 事例です。

ただし、このクラスターは全部で 45 名いるはずなのですが、岐阜県の公表データからは 38 名しか経路図として接続できることができませんでした。7 事例が家庭内感染もしくは他の生徒の事例として隠れているかもしれません。

子供から子供へ移したと思われる事例

こちらの事例は、おそらく最初の 4 名は 3 世代の家庭内感染(濃厚接触)です。最後の 10 歳未満の 2 名は、このうち真ん中の 10 歳未満の 2 名との「関連からの検査」として公表されています。「関連からの検査」という書き方は、その濃厚接触ではないが、その発生した集団内になんらかの形で属していた場合に使われる表現です。同居家族であれば「接触」や「濃厚接触者」として書かれる場合が多いはずです。

f:id:oxon:20210109163744p:plain
子供から子供へ移したと思われる事例

つまり、これら 4 名の 10 歳未満の子供たちは同じ集団に属しており、濃厚接触かどうかは追跡調査で判明しなかったものの、集団内で感染が起きたということです。その集団に大人がいるはずですが、大人への感染は確認されなかったと言えます。したがって、単純には 10 歳未満の子供同士で感染させ合う可能性があるということです。(インフルエンザでも学級閉鎖が起きるので当たり前ですが)

子供から複数の大人へ移したと思われる事例

こちらの事例では、中央の縦 1 列に 10 歳未満の子供が 3 名おり、これは先頭から繋がる何らかの集団(保育施設など)と思われます。

f:id:oxon:20210109164232p:plain
子供から複数の大人へ移したと思われる事例

このうち 1 番下の女の子はこの左側の集団を含め 4 つの異なる大人へと繋がっています。つまり、4 つの集団のうちどれが感染の上流かは分かりませんが、子供を媒介として複数の集団へ感染を拡大させる場合もあるということです。

子供を 2 人経由して移したと思われる事例

こちらの例では、大人から子供に感染し、それが他の子供へ感染し、さらに大人へ移すという事例です。もし子供から大人は移りにくい、子供同士は移りにくいというのが事実だとしても、それらの低い確率を掛け合わせた事例というのも、これだけ感染者が増えてくると存在するということです。ただし稀です。

f:id:oxon:20210109174831p:plain
子供を 2 人経由して移したと思われる事例

子供の感染の大部分を占める経路不明からの家庭内感染

多くの子供の感染事例のうち、頻繁に目にするのが感染経路不明の親(父親が多い)から家庭内感染をしたと思われる事例です。特にこれらの家庭(と思われる)を取り上げた理由はありませんが、典型例です。f:id:oxon:20210109175824p:plain

f:id:oxon:20210109175351p:plainf:id:oxon:20210109175431p:plainf:id:oxon:20210109175555p:plainf:id:oxon:20210109175632p:plainf:id:oxon:20210109175710p:plain

父親が外からもらってきたのか、母親がもらってきたのかは発症日と感染日に時間差があるため、これらの図では分かりません。ただし、傾向として父親が先に陽性確定する事例が多いということです。

大きなクラスターの末端に子供がくる例

現在、愛知県内、岐阜県内では様々なクラスターが発生しており、当然そのようなクラスターに含まれる大人の中には、小さい子供を持つ方たちも多くいます。そのような場合、クラスターの末端に子供がくるというのもたまに見かけます。

f:id:oxon:20210109180120p:plain
大きなクラスターの末端に子供がくる例

まとめ

当たり前ではありますが、子供から子供へも感染させる、子供から大人へも感染させる、そして大人から子供へ家庭内感染させたと思われる事例が圧倒的に多いということが確認できました。ここで取り上げた事例以外にも多数の経路が載っていますので、興味のある方は全体 PDF を眺めてみてください。

github.com

https://github.com/akira-okumura/COVID-19/raw/master/PDF/Aichi2021-01-08_kids.pdf

我が家の考え方としては、次の通りです。

  • 親は家庭外で会食をしない、職場でも気を付ける
  • 保育園も小学校も通わせないわけにもいかないので通わせる
  • 子供がもらってきたら諦めるが、その発生確率は高くはない
  • 子供は屋外で遊ばせ、他の家庭への訪問は避けてもらう

年末年始の帰省による、大都市圏からの新型コロナ感染者の流入実態(愛知県の場合)

2020 年の 4 月から愛知・岐阜の新型コロナの報告事例観察を継続しています。
oxon.hatenablog.com

さて、年末年始の帰省による人の移動で新型コロナが地方に広められてしまうのではないかという心配がありました。(発掘できなかったのですが)年末年始に診療にあたる医師が帰省してきた感染者の診察を何件かしているという Twitter の投稿も見かけました。

また実際、2021/1/5 の愛知県の発表では「東京などから年末年始に帰省して県内の実家で発症した例も複数含まれる」ということで、それが全国的な感染拡大にどのような影響を与えるのか心配なところです。
www.tokai-tv.com

f:id:oxon:20210110162127p:plain

そこで、2020 年 11 月以降の愛知県内の陽性報告事例のうち、住居地が愛知県・岐阜県三重県以外の事例のみを抽出したのが上の図です。これを見ると、10〜30 代の大都市圏(東京都 + 千葉県 + 神奈川県、大阪府京都府など)に居住する若者が愛知県内で発症する事例が、12/29 以降に突然、頻発するようになったのが分かります。年末年始の帰省によって、大都市圏から地方へ(この場合は愛知県ですが)新型コロナウイルスが拡散されるのが改めて確認されました。

No 発表日 年代・性別 住居地 接触状況
6353 11月3日 30代男性 東京都 10月31日まで東京都に滞在
8509 11月21日 40代男性 福井県
13553 12月18日 50代男性 千葉県
13564 12月18日 40代男性 沖縄県
13759 12月18日 30代男性 東京都
13795 12月19日 40代男性 千葉県
13963 12月19日 20代男性 兵庫県
14215 12月21日 40代男性 大阪府 No.12325,12324と接触
14565 12月23日 50代男性 福岡県
15285 12月26日 30代男性 石川県 フィリピン
15511 12月27日 50代男性 東京都
15907 12月29日 20代男性 神奈川県
16043 12月29日 50代男性 京都府
16100 12月30日 30代男性 福岡県 No.15711と接触
16389 12月31日 60代男性 東京都
16422 12月31日 20代男性 東京都
16618 1月1日 20代男性 東京都
16722 1月1日 20代女性 東京都
16724 1月1日 10代男性 京都府 京都府事例と接触
16766 1月1日 20代男性 東京都
16825 1月2日 20代男性 大阪府
16831 1月2日 20代女性 東京都
16834 1月2日 30代女性 東京都 東京都事例と接触
16874 1月2日 30代男性 東京都
16957 1月3日 20代男性 茨城県 No.16295と接触
17112 1月3日 30代女性 東京都
17137 1月4日 10代男性 北海道 北海道事例と接触
17138 1月4日 20代男性 大阪府 大阪府事例と接触
17321 1月5日 20代女性 東京都
17337 1月5日 20代男性 大阪府
17383 1月5日 20代男性 群馬県
17402 1月5日 20代男性 東京都
17404 1月5日 20代女性 東京都
17458 1月5日 20代男性 千葉県 千葉県事例と接触
17521 1月5日 20代男性 東京都
17768 1月6日 10代男性 福井県 No.17216と接触
17802 1月6日 20代女性 京都府
17804 1月6日 60代男性 静岡県
17886 1月6日 20代男性 京都府
17900 1月6日 30代男性 東京都
17952 1月7日 20代女性 東京都
18415 1月8日 20代男性 奈良県
18642 1月8日 20代女性 東京都

少なくとも 1 月 8 日(発症日ではなく公表日)の時点でこのような事例が愛知県内だけで 30 件発生しています*1。愛知県人口が 750 万人で、多くの帰省を受け入れる県・道の人口がおよそ 10 倍の 7500 万人と見積もると、全国で同様の事例が 200〜300 件は発生しているのではないかと推測されます。多くの場合、帰省中に家族や旧友と飲食をしていると考えられるので、この 200 件に少なくない数がさらなる感染者数として上乗せされると推測されます。

このうちの 9 事例では、帰省中の愛知県内での接触により感染拡大が起きている可能性があります。

*1:ただし、そのうち 16110 と 16957 の 2 件は、帰省中に愛知県内で感染したと思われる。

Feynman Diagram が TeX Live 2019/2020 でちょん切れないようにする

TeX Live 2018 + Mojave の環境までは LaTeXiT で Feynman diagram を描くときは阪大の山中さんのページを参考にしていましたが、Catalina + TeX Live 2019 にしたら図がちょん切れるようになった(bounding box がおかしくなった)ので、解決する方法の覚え書きです。

osksn2.hep.sci.osaka-u.ac.jp

詳細は StackExchange にも質問で書きました。

tex.stackexchange.com

新しいやり方

  • latex + dvipdf は使わない(そもそも、dvipdf だと図が真っ白になる)
  • dvipdfm にすると図がちょん切れるので、これも使わない
  • LaTeXiT の preamble に \usepackage{feynmp} だけでなく \DeclareGraphicsRule{*}{mps}{*}{} も追加する
  • mpost の実行後、pdflatex で PDF を出力させる

f:id:oxon:20200712130650p:plain

f:id:oxon:20200712130810p:plain

古いやり方だと \usepackage{feynmp} を追加するだけでしたが、これだけだと mpost コマンドの生成する拡張子なしの PS ファイルを pdflatex が正しく取り扱えません。そのため、古いやり方では latexdvipdf を使って DVI を経由させていたのだと思います。

\DeclareGraphicsRule{*}{mps}{*}{} も追加することで、拡張子のないファイルを pdflatex が扱えるようになるので、TeX Live 2019 や 2020 で現れる dvipdf もしくは dvipdfm の問題(仕様?)を回避することができます。

古いやり方

TeX Live 2018 まで問題なかった山中さんのやり方で dvipdfdvipdfm にすると絵は出てきますが、下がちょん切れていますね。

f:id:oxon:20200712130930p:plain

f:id:oxon:20200712131020p:plain

brew/pip3 install ipython の違い

Homebrew を使って IPython を導入する際、brew install を使うやり方と、pip3 install をやり方の主に 2 つの方法があると思います。前者でやると IPython 起動時に PYTHONPATH を勝手に書き換えてしまうことが分かり、少しはまりました。そもそも ipython コマンドというのは bash script だったり、Python script だったり、環境によって違うのだということを知りました。

macOS 10.15.5 です。

Homebrew 環境の構築

これは書くまでもありませんが、https://brew.sh/の説明通りに次のコマンドを実行します。

$ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"

brew install ipython の場合(非推奨)

さて、IPython を入れたいのだから brew install ipython と単純にやると次のようになります。

$ brew install ipython
$ rehash
$ which python
/usr/bin/python
$ which python2
/usr/bin/python2
$ which python3
/usr/bin/python3
$ which ipython
/usr/local/bin/ipython
$ which pip 
pip not found
$ which pip3
/usr/bin/pip3

つまり、macOS 標準の Python をデフォルトとして維持したまま ipython コマンドだけ入ります(実際には他にも色々と入りますが)。pip3 も入りません。

そして ipython の中身は次の bash script です。PYTHONPATH を勝手に書き換えてしまいます。

$ cat `which ipython`
#!/bin/bash
PYTHONPATH="/usr/local/Cellar/ipython/7.15.0/libexec/lib/python3.8/site-packages:/usr/local/Cellar/ipython/7.15.0/libexec/vendor/lib/python3.8/site-packages" exec "/usr/local/Cellar/ipython/7.15.0/libexec/bin/ipython" "$@"

これで PyROOT を使おうと思っても PYTHONPATH から ROOTSYS/lib が消えてしまい、おかしいおかしいと悩んでしまいました。これは bug じゃないかと思いますが、Homebrew の issue report をどこに上げるのかよく分からなかったので、放置しています。

brew install python を先にする場合(非推奨)

$ brew install python
$ rehash 
$ which python 
/usr/bin/python
$ which python2
/usr/bin/python2
$ which python3
/usr/local/bin/python3
$ which pip 
pip not found
$ which pip3
/usr/local/bin/pip3

python3 コマンドが /usr/local/bin に入り、pythonpython2 は OS 標準です。この場合だと、pip3 も入りました。

$ brew install ipython
$ rehash
$ which ipython
/usr/local/bin/ipython

ipython の中身は同じです。

$ cat `which ipython`
#!/bin/bash
PYTHONPATH="/usr/local/Cellar/ipython/7.15.0/libexec/lib/python3.8/site-packages:/usr/local/Cellar/ipython/7.15.0/libexec/vendor/lib/python3.8/site-packages" exec "/usr/local/Cellar/ipython/7.15.0/libexec/bin/ipython" "$@"

pip3 install の場合(推奨)

$ brew install python
$ rehash
$ pip3 install ipython
$ rehash

brew install python の後に rehash を忘れないようにしましょう。そうしないと、/usr/bin/pip3 が使われてしまいます。

cat `which ipython`
#!/usr/local/opt/python/bin/python3.7
# -*- coding: utf-8 -*-
import re
import sys
from IPython import start_ipython
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(start_ipython())
$ cat `which ipython3`
#!/usr/local/opt/python/bin/python3.7
# -*- coding: utf-8 -*-
import re
import sys
from IPython import start_ipython
if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
    sys.exit(start_ipython())

はい、こっちだと PYTHONPATH を勝手に書き換えませんし、bash ではなく Python です。こちらのやり方が推奨です。

Conda の場合

これは conda create で新しい環境を作ったときに入った ipython コマンドの中身です。また中身が違いますが、ほとんど一緒です。

#!/Users/oxon/anaconda3/envs/sstcam_root/bin/python

# -*- coding: utf-8 -*-
import re
import sys

from IPython import start_ipython

if __name__ == '__main__':
    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
    sys.exit(start_ipython())

macOS Catalina で PyVISA/PySerial を使う、に加えて初心者向けの丁寧な解説

macOS Catalina で PyVISA と PySerial を使うときの覚え書きです。PyVISA のほうは普通にやると、はまります。詳しい人向けの結論を先に書くと、NI-GPIB を先に入れないと NI-VISA が入らない、です。

環境

  • macOS Catalina 10.15.5
  • Python 3.7.6(brew install python で)
  • PyVISA 1.10.1(pip3 install pyvisa で)
  • PySerial 3.4(pip3 install pyserial で)
  • NI-GPIB 19.5
  • NI-VISA Runtime 19.0.0

経緯

以前にこういう記事を書いたのですが、かなり情報が古くなってしまいました。当時は 32 bit と 64 bit が混在している時代で、Python も 32 bit だったり、National Instruments も Mac 用のドライバの配布が遅かったりと、苦労していました。 oxon.hatenablog.com Mojave から Catalina 環境に移行するにあたり、少し PyVISA ではまったので、記録に残しておきます。

うちの学生向けに書くので、比較的丁寧です。

VISA とは

VISA(Virtual Instrument Software Architecture) というのは、いろいろな実験室の計測装置を共通のソフトウェアで簡単に使えるようにしましょうという規格です。「査証」やクレジットカードの VISA とは関係がありません。

en.wikipedia.org

実験室にある計測装置の背面を見てみると、電源ケーブル以外に色々なケーブルを接続できることに気がつきます。古い機種だと RS-232C と呼ばれる規格の端子がついています(シリアルポートとも呼ばれます)。これは 9 本の剥き出しのピン(オスの場合)だったり、9 本のピンが差し込めるようになっているメスのポートだったりします。昔は計算機側にもこのポートが 2 つずつ付いていることが多かったのですが、今どきは付いていません。しかし実験室では、特にデータ転送速度を重視しない機器(低圧電源や回転ステージなど)ではいまだに使われることが多いです。

ja.wikipedia.org

少し時代が経ち、オシロスコープのようにデータ量の多い装置では GPIB(IEEE の規格は IEEE 488)というケーブルの規格が使われるようになりました。最近はあまり見かけませんし、元々 GPIB 搭載の可能な機種でもオプション扱いのものが多かったと思うので、20〜30 年前くらいの高いデジタルオシロスコープなんかに付いていたりします(安いものにもついていたと思いますが、30 年前の安いものは既に廃棄処分されていると思います)。見た目がゴツくて格好いいやつです。写真はリンク先を参照。

ja.wikipedia.org

最近だと、USB 端子がつく機種も増えました。むしろ新しい機種で USB すら付いていない製品やそんなものを販売する会社は、手を出さないほうがいいんじゃないかという気がします。USB 1.1(最大 12 Mbps)は低速の用途にも良いですし、USB 2.0(最大 480 Mbps)ならオシロスコープのようなデータ量の多い装置にも十分耐えられます。USB 3.0(最大 5 Gbps)以降を使用している機種は、2020 年現在、まだ多くないと思います。

ja.wikipedia.org

オシロスコープのように通信量が大きい装置は、USB ではなく Ethernet ポートを備えたものもあります。機種によって 100 Mbps だったり 1 Gbps だったり 10 Gbps だったり、様々です。自分が実験で使う場合は USB 2 より Ethernet のほうが転送速度が早いため Ethernet を使いますが、この記事で説明するにはネットワークの設定の説明が面倒なのと、実験室ごとに設定がいろいろ変わるので、Ethernet の説明は省き USB 中心で説明します。

ja.wikipedia.org

さて、VISA の話に戻ると、こういう異なるケーブルや通信プロトコルごとにソフトウェアを書き換えるのは面倒なので、一括で同じように扱えるようにしようというのが、VISA です。例えば何か計算機からコマンドを周辺機器に送るとき、もしくは周辺機器から計算機にデータが送られてくるとき、VISA 経由でやれば、どのケーブルやプロトコルを使用しているのかはほとんど気にしなくて良くなります。

PyVISA とは

PyVISA は、Python から VISA に対応した周辺機器とやり取りするための機能を提供してくれます。macOS Catalina に Homebrew で Python 3 を導入している場合、次のように pip3 コマンドで PyVISA をインストールします。

$ pip3 install pyvisa

後述する NI-GPIB や NI-VISA のインストールされた環境では、この PyVISA を使うことによって周辺機器と簡単に通信することができるようになります。次の例は、IPython から Tektronix の MSO4102B というオシロスコープに接続してシリアルナンバーなどの情報を得ているところです。

$ ipython
In [1]: import visa                                                                     
In [2]: rm = visa.ResourceManager()                                                     
In [3]: rm.list_resources()                                                             
Out[3]: ('USB0::0x0699::0x041B::C022756::INSTR',)
In [4]: usb = rm.open_resource(rm.list_resources()[0])                                  
In [5]: usb.query('*IDN?')                                                              
Out[5]: 'TEKTRONIX,MSO4102B-L,C022756,CF:91.1CT FV:v2.90 \n'

RS-232C での通信の場合は、ボーレート(baud rate)やデリミター(delimiter)などを気に掛ける必要がありましたが、VISA なら勝手にうまいことやってくれます。

デバイスドライバの部分は、PyVISA 自身はやっていません。macOS の場合、PyVISA は National Instruments(NI)の NI-VISA というデバイスドライバーをバックエンドとして使用しています。

PyVISA を macOS Catalina に導入する手順

1. NI-GPIB(NI-488.2)を入れる

PyVISA を使うには NI-VISA が必要で、NI-VISA を Catalina に入れるには NI-GPIB を先に入れる必要があります。リンク先から、Supported OS に MacOS、Version に 19.5 を選択してディスクイメージ(拡張子 .dmg)をダウンロードします。

www.ni.com

NI-488.2 19.5.dmg を開くと NI-488.2 19.5.pkg が現れるので、これを開き、あとは指示に従ってインストールします。これは GPIB を Mac で使用するための NI 社製のデバイスドライバーです。再起動が必要です。

macOS 10.13 と 10.14 しか対応していないとなっていますが、10.15 で動作します。

もしも .pkg を開けないとダイアログが出てきた場合、右クリックから開くようにすると次のダイアログが出るので、Open を選択してください。

f:id:oxon:20200609155806p:plain

2. NI-VISA Runtime を入れる

次に、NI-VISA を入れます。同様に Mac 版の 19.0 を入れます。この際、Included Editions は Runtime にすれば十分です。Full でも良いですが、PyVISA だけの使用であれば不要です。 www.ni.com

NI-VISA_Runtime_19.0.0.dmg を開くと NI-VISA_Runtime_19.0.0.pkg が現れるので、これも同様にインストールしてください。再起動は多分必要ありません。

3. PyVISA を入れる

Homebrew 環境の場合、pip から入れてください。Python 3 であれば pip3 です。

$ pip3 install pyvisa  

4. インストールの確認

PyVISA とともに pyvisa-info というコマンドがインストールされるので、これが /Library/Frameworks/visa.framework/visa にある NI-VISA を自動的に検出できていれば成功です。

$ pyvisa-info      

Machine Details:
   Platform ID:    Darwin-19.5.0-x86_64-i386-64bit
   Processor:      i386

Python:
   Implementation: CPython
   Executable:     /usr/local/opt/python/bin/python3.7
   Version:        3.7.6
   Compiler:       Clang 11.0.0 (clang-1100.0.33.16)
   Bits:           64bit
   Build:          Dec 30 2019 19:38:26 (#default)
   Unicode:        UCS4

PyVISA Version: 1.10.1

Backends:
   ni:
      Version: 1.10.1 (bundled with PyVISA)
      #1: /Library/Frameworks/visa.framework/visa:
         found by: auto
         bitness: 64
         Vendor: National Instruments
         Impl. Version: 19922944
         Spec. Version: 5244928

もし abort したというエラーが出る場合、System Preferences の Security & Privacy で NI 製のソフトウェアがブロックされていないか確認してください。もしこの画像のようにブロックされている場合、鍵を外して Allow を押して、許可してやりましょう。 f:id:oxon:20200616145248p:plain

5. 動作確認

先述したコマンド例の繰り返しになりますが、VISA に対応しているはずの適当な実験装置を USB ケーブルで Mac に接続し、次のように真似して *IDN? コマンドを送信してみましょう。普通の装置は後述の SCPI というコマンド体系に準拠しているはずで、ほとんどの装置は *IDN? コマンドに対して機種情報を返します。

$ ipython
In [1]: import visa                                                                     
In [2]: rm = visa.ResourceManager()                                                     
In [3]: rm.list_resources()                                                             
Out[3]: ('USB0::0x0699::0x041B::C022756::INSTR',)
In [4]: usb = rm.open_resource(rm.list_resources()[0])                                  
In [5]: usb.query('*IDN?')                                                              
Out[5]: 'TEKTRONIX,MSO4102B-L,C022756,CF:91.1CT FV:v2.90 \n'

これで、Tektronix の MSO4102B-L という機種でシリアルナンバー(S/N)C022756 のものが繋がっているというのが分かります。ファームウェアのバージョンがおそらく 2.90 なのだと思います。

SCPI

さて、周辺機器の接続情報だけ得ても面白くないので、*IDN? 以外の SCPI について調べましょう。Wikipedia の記事にもあるように、SCPI は実験室で使うような測定装置とやり取りするための標準的なコマンドの文法の規格です。メーカーが異なっても、例えば Tektronix でも Keithley でも岩通でも、SCPI で制御できる装置は似たようなコマンドで動作させることができます。

en.wikipedia.org

例えば上述の Tektronix のオシロスコープの電圧表示範囲を変更してみましょう。文法を調べるには、例えば「MSO4102 programmer's manual」などで Google 検索すると PDF が出てくるはずです。次のページが出てきたので、PDF をここから入手しましょう。

jp.tek.com

マニュアルを読むと、このような説明が出てきます。

CH<x>? Returns vertical parameters for the specified channel

それでは、実際にやってみましょう。ここで注意するのは、<x> の部分は適切な値に置き換えよという意味です。? が最後についているのは、問い合わせのコマンドということです。つまり、返り値があります。

In [6]: usb.query('CH1?')                                                       
Out[6]: '0;10.0000;"No probe detected";"";1.0000;"V";50.0000;"Other";0.0E+0;0.0E+0;1.0000E+9;DC;0.0E+0;0.0E+0;0;-2.9600;1.0000;"V";50.0000;""\n'

この読み方はマニュアルを参照するとして、例えば縦軸の分割幅は 1.0000 V であり、オフセットが -2.9600 V だということが分かります。カップリングは DC で終端は 50.0000 Ω です。

次に、電圧のオフセットを変更してみましょう。マニュアルによると、文法は CH<x>:OFFSet <NR3> および CH<x>:OFFSet? です。ここで小文字は省略可能という意味です。また <NR3>2.0E-3 などの指数表示を意味します。(<NR1> は整数値、<NR2> は小数です。)

In [15]: usb.write('CH1:OFFS 1.23E-1')                                                                                                                          
Out[15]: (18, <StatusCode.success: 0>)
In [16]: usb.query('CH1:OFFS?')                                                                                                                                 
Out[16]: '123.0000E-3\n'
In [17]: voffset_ch1 = float(usb.query('CH1:OFFS?')[:-1])                                                                                                       
In [18]: voffset_ch1
Out[19]: 0.123
In [20]: usb.write('CH1:OFFS %.2E' % voffset_ch1)

動作確認時を除いて、実際にプログラム中に書くときは省略せずに全て書くことをお勧めします。コマンドの可読性が増すからです。

次に波形を取得してみましょう。簡単のため、データフォーマットは ASCII にします。

In [31]: usb.write('WFMOutpre:ENCdg ASCii')
In [32]: usb.write('DATA:SOURCE CH1')
In [33]: data = usb.query('WAVFRM?')
In [34]: data[:250]                                                                                                                                             
Out[34]: '2;16;ASCII;RI;MSB;"Ch1, DC coupling, 1.000V/div, 4.000ms/div, 1000000 points, Sample mode";1000000;Y;LINEAR;"s";40.0000E-9;-20.0000E-3;0;"V";156.2500E-6;29.6960E+3;123.0000E-3;TIME;ANALOG;0.0E+0;0.0E+0;0.0E+0;27648,27904,27392,27648,27648,27904,27648'

あとは、マニュアルを読んでこのデータを波形情報に戻してみましょう。

PySerial

USB 端子を持っていても、VISA に対応していない機器があります。その多くは USB 端子の後ろに USB to Serial converter と呼ばれる、USB とシリアル通信(RS-232C)を変換するチップが搭載されています。このような機器は PyVISA ではなく PySerial を使って制御します。

また、USB 端子ではなく RS-232C 端子しかない機器もあるでしょう。これには、USB-RS232C の変換ケーブルを使用します。変換チップには様々なメーカーのものが存在しますが、FTDI 社製のものが一般的です。また FTDI 製であれば macOS でも Windows でも Linux でもデバイスドライバーのインストールなしに動作しますので、例えば Buffalo のこのケーブルは確実に動作します。

Mac に FTDI 製のチップを持つ周辺機器、もしくは FTDI 製チップの使われている変換ケーブルで RS-232C の機器を接続したときは、/dev/ 以下にデバイスファイルが作成されます。

例えば Keithley 社製のある機器の場合、FTDI のチップが使われているため次のように表示されます。表示されるデバイスファイル名はチップのシリアルナンバーにしたがって異なるものになります(Mac の場合)。

$ system_profiler SPUSBDataType
(略)
    USB 3.0 Hi-Speed Bus:

      Host Controller Location: Built-in USB
      Host Controller Driver: AppleUSBXHCI
      PCI Device ID: 0x1e31 
      PCI Revision ID: 0x0004 
      PCI Vendor ID: 0x8086 
      Bus Number: 0x14 

        USB HS SERIAL CONVERTER:

          Product ID: 0x6001
          Vendor ID: 0x0403  (Future Technology Devices International Limited)
          Version: 4.00
          Serial Number: FT123456
          Speed: Up to 12 Mb/sec
          Manufacturer: FTDI
          Location ID: 0x14100000 / 1
          Current Available (mA): 500
          Current Required (mA): 44
$ ls /dev/tty.usbserial*
/dev/tty.usbserial-FT123456

PySerial を入れると、同様に SCPI のコマンドで制御することができるようになります。

import serial
keithley = serial.Serial(port="/dev/tty.usbserial-FT123456",
                         baudrate=57600,timeout=1,writeTimeout=1)

keithley.write('*RST\n')
keithley.write(':SENS:FUNC "VOLT"\n')
keithley.write(':SOUR:FUNC VOLT\n')
keithley.write(':OUTP ON\n')

ここで、RS-232C の装置の場合は baudrate という値の設定をその機種ごとに揃えて必要があります(今の例の場合、57600)。装置のマニュアルに必ず書いてあるはずですので、読むようにしましょう。また使用する改行コードも機種ごとに異なるため、今の場合は \n を使用していますが、これもマニュアルで確認してください。

Anaconda で ROOT 入れたり

Anaconda 環境に ROOT を入れるときのメモ。1 年前に Mojave でやったときとちょこちょこ変わっているので、2020 年 6 月現在の以下の環境を想定。

  • macOS 10.15.4
  • Xcode 11.5
  • CMake 3.17.1(conda ではなく Homebrew から)
  • Anaconda3-2020.02-MacOSX-x86_64.sh

まず、Mac 用の Anaconda を落としてくる。 https://repo.anaconda.com/archive/Anaconda3-2020.02-MacOSX-x86_64.sh

適当に答えて進める。

$  chmod +x Downloads/Anaconda3-2020.02-MacOSX-x86_64.sh
$ ./Downloads/Anaconda3-2020.02-MacOSX-x86_64.sh

自動的に .zshrc に Anaconda の設定が書き込まれるのが好きじゃないので、自分の場合は関数で括って好きなときにAnaconda 環境に入るようにしている。

conda_init(){
# >>> conda initialize >>>
# !! Contents within this block are managed by 'conda init' !!
__conda_setup="$('/Users/oxon/anaconda3/bin/conda' 'shell.zsh' 'hook' 2> /dev/null)"
if [ $? -eq 0 ]; then
    eval "$__conda_setup"
else
    if [ -f "/Users/oxon/anaconda3/etc/profile.d/conda.sh" ]; then
        . "/Users/oxon/anaconda3/etc/profile.d/conda.sh"
    else
        export PATH="/Users/oxon/anaconda3/bin:$PATH"
    fi
fi
unset __conda_setup
# <<< conda initialize <<<
}

4.8.3 に update する。

$ conda_init
(base) $ conda update -n base -c defaults conda

好きな環境、例えば sstcam_root という環境を作る。このとき、-c conda-forge で channel を指定する必要あり。ROOT はここにあるので。

(base) $ conda create -n sstcam_root root -c conda-forge
(base) $ conda activate sstcam_root
(sstcam_root) $ which root.exe 
/Users/oxon/anaconda3/envs/sstcam_root/bin/root.exe
(sstcam_root) $ root
root [0] .q

以前は MacOSX10.9.sdk とかを落としてくる必要があったのだけど、今は要らなくなった。なので、Anaconda 側で Xcode についてくる SDK を勝手に環境変数に設定してくれる。これは base のときは空っぽで、次のように ROOT 環境のほうだけ。10.15 が指定されている。

$ conda_init 
(base) $ echo $CONDA_BUILD_SYSROOT

(base) $ conda activate sstcam_root 
(sstcam_root) $ echo $CONDA_BUILD_SYSROOT 
/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
(sstcam_root) $ ll $CONDA_BUILD_SYSROOT
lrwxr-xr-x  1 root  wheel  15 Mar 25 11:24 /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk -> MacOSX10.15.sdk

これで、ACLiC も問題なく動く。ACLiC を動かすためにも、以前は SDK を自分で用意する必要があった。

(sstcam_root) $ root
root [0] .x aho.C+
Info in <TMacOSXSystem::ACLiC>: creating shared library /Users/oxon/./aho_C.so

Anaconda で入れるものではない library を CMake で build するとき、引数を追加する必要がある。

docs.conda.io ここを読むと、次のようにやる必要があるらしいのだけど(これは clang が Anaconda 環境のものが使用されてしまうため)、

(sstcam_root)$ cmake ../source -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX -DCMAKE_OSX_SYSROOT=$CONDA_BUILD_SYSROOT

実際には次のコマンドでちゃんと動いた。CMake のバージョンによるのかもしれない。

(sstcam_root)$ cmake ../source -DCMAKE_INSTALL_PREFIX=$CONDA_PREFIX

SYSROOTROOT は ROOT と関係ない。

この環境で例えば他に Python 関係のものを追加したい場合、conda-forge から入れるようにする必要がある。

(sstcam_root) $ conda config --env --add channels conda-forge
(sstcam_root) $ conda install scipy astropy matplotlib tqdm pandas numba cython scikit-learn