マウスジェスチャーツール Crevice 3.2.184 をリリースしました

カッコいいロゴを頂いたのでテンション上がってそのまま18時間コーディングしたらメジャーバージョンが1つ上がっていました。

f:id:ruby-U:20171223133526p:plain

どうですか、超クールなのでは?? イエーイ🎉🎉

Crevice 3.2.184 リリースノート

1月にリリースしたCrevice 2.5 から Crevice 3 へメジャーバージョンが1つ上がり、いくつかの大きな変更が加わっています。

  • 新しいクールなアイコン! アプリケーションの外観をブラッシュアップしました。

  • ユーザースクリプトのキャッシュ機構を追加。Crevice 2.5 と比較して起動速度が最大で50倍高速になりました。

  • コマンドラインインターフェイスの追加。ユーザースクリプトの場所、タスクトレイアイコン非表示、プロセス優先度、詳細なログ出力などの設定を引数として指定できるようになりました。

特に頑張ったところは起動速度の高速化です。CreviceではユーザースクリプトC# Scriptを使っていて、Crevice 2.5 ではRoslynで毎回コンパイルしていたのですが、このリソースが無駄だし起動時間は遅くなるしいいことなしだったので、ユーザースクリプトコンパイルした後のアセンブリをうまいことキャッシュできないかなーと試行錯誤してたら偶然うまく行きました。雰囲気でやっている。

技術資料

以下、できたてホヤホヤのソースから抜粋して、Roslynでユルい C# Script をコンパイルして、アセンブリからそれを評価する方法を書いておきます。アセンブリに GetType して、 GetMethod して Invoke という資料はよく見かけますが、 C# Script 特有のトップレベルに変数がくるようなコードでうまくそれをやる方法が見当たらなかったので貴重な資料だと思います(ホントか?)

Crevice 2 の実装(ユルいC# Script にglobalsを与えて評価する)

ユーザースクリプトから Script を作ります。

private Script ParseScript(string userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Parsing UserScript...");
    stopwatch.Start();
    var script = CSharpScript.Create(
        userScript,
        ScriptOptions.Default
            .WithSourceResolver(ScriptSourceResolver.Default.WithBaseDirectory(UserDirectory))
            .WithMetadataResolver(ScriptMetadataResolver.Default.WithBaseDirectory(UserDirectory))
            .WithReferences("microlib")                   // microlib.dll
            .WithReferences("System")                     // System.dll
            .WithReferences("System.Core")                // System.Core.dll
            .WithReferences("Microsoft.CSharp")           // Microsoft.CSharp.dll
            .WithReferences(Assembly.GetEntryAssembly()), // CreviceApp.exe
                                                          // todo dynamic type
        globalsType: typeof(Core.UserScriptExecutionContext));
    stopwatch.Stop();
    Verbose.Print("UserScript parse finished. ({0})", stopwatch.Elapsed);
    return script;
}

そしてこの Script にコンテキスト ctx を globals として与えて評価します。ctxLeftButton, MiddleButton のような設定のためのトークン、 ジェスチャー定義に関する @when 関数や、Config のようなインスタンスTooltip, Balloon のようなユーティリティ関数などをメンバーとして持ち、それを globals として与えることで、ユーザースクリプト評価時のコンテキストとしています。

public class UserScriptExecutionContext
{
    public readonly DSL.Def.LeftButton   LeftButton   = DSL.Def.Constant.LeftButton;
    public readonly DSL.Def.MiddleButton MiddleButton = DSL.Def.Constant.MiddleButton;
// 略
    public Config.UserConfig Config
    {
      get { return Global.UserConfig; }
    }

    public DSL.WhenElement @when(DSL.Def.WhenFunc func)
    {
        return root.@when(func);
    }

    public void Tooltip(string text)
    {
        Tooltip(text, Global.UserConfig.UI.TooltipPositionBinding(WinAPI.Window.Window.GetPhysicalCursorPos()));
    }

    public void Balloon(string text)
    {
        Balloon(text, Global.UserConfig.UI.BalloonTimeout);
    }
// 略
}
private IEnumerable<Core.GestureDefinition> EvaluateUserScript(Script userScript, Core.UserScriptExecutionContext ctx)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Compiling UserScript...");
    stopwatch.Start();
    var diagnotstics = userScript.Compile();
    stopwatch.Stop();
    Verbose.Print("UserScript compilation finished. ({ 0})", stopwatch.Elapsed);
    foreach (var dg in diagnotstics.Select((v, i) => new { v, i }))
    {
        Verbose.Print("Diagnotstics[{0}]: {1}", dg.i, dg.v.ToString());
    }
    Verbose.Print("Evaluating UserScript...");
    stopwatch.Restart();
    userScript.RunAsync(ctx).Wait();
    stopwatch.Stop();
    Verbose.Print("UserScript evaluation finished. ({0})", stopwatch.Elapsed);
    return ctx.GetGestureDefinition();
}

評価後 ctx にはマウスジェスチャーの設定が保存されています。

Crevice 3 の実装(ユルい C# Script から生成したアセンブリをロードして globals を与えて評価する)

ユーザースクリプトから Script を作ります。

private Script ParseScript(string userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Parsing UserScript...");
    stopwatch.Start();
    var script = CSharpScript.Create(
        userScript,
        ScriptOptions.Default
            .WithSourceResolver(ScriptSourceResolver.Default.WithBaseDirectory(UserDirectory))
            .WithMetadataResolver(ScriptMetadataResolver.Default.WithBaseDirectory(UserDirectory))
            .WithReferences("microlib")                   // microlib.dll
            .WithReferences("System")                     // System.dll
            .WithReferences("System.Core")                // System.Core.dll
            .WithReferences("Microsoft.CSharp")           // Microsoft.CSharp.dll
            .WithReferences(Assembly.GetEntryAssembly()), // CreviceApp.exe
                                                          // todo dynamic type
        globalsType: typeof(Core.UserScriptExecutionContext));
    stopwatch.Stop();
    Verbose.Print("UserScript parse finished. ({0})", stopwatch.Elapsed);
    return script;
}

ここまでは以前の実装と同じです。次にアセンブリのバイナリを作ります。次回起動時にアプリケーションのバージョンやユーザースクリプトに変更がなければ、これをキャッシュとして使うことで起動時間を大幅に短縮することができます。

private UserScriptAssembly.Cache CompileUserScript(UserScriptAssembly usa, string userScriptCode, Script userScript)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Compiling UserScript...");
    stopwatch.Start();
    var compilation = userScript.GetCompilation();
    stopwatch.Stop();
    Verbose.Print("UserScript compilation finished. ({0})", stopwatch.Elapsed);

    var peStream = new MemoryStream();
    var pdbStream = new MemoryStream();
    Verbose.Print("Genarating UserScriptAssembly...");
    stopwatch.Restart();
    compilation.Emit(peStream, pdbStream);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly generation finished. ({0})", stopwatch.Elapsed);
    return usa.CreateCache(userScriptCode, peStream.GetBuffer(), pdbStream.GetBuffer());
}

キャッシュをロードして、以前と同様にコンテキスト ctx を globals として与えて評価します。アセンブリに GetType("Submission#0") して、 GetMethod("") して得られた関数を Invoke するのですが、この時に ctxnew object[] { new object[] { ctx, null } } のような形で2番めの引数として渡すのがポイントです。

private IEnumerable<Core.GestureDefinition> EvaluateUserScriptAssembly(UserScriptAssembly.Cache cache, Core.UserScriptExecutionContext ctx)
{
    var stopwatch = new Stopwatch();
    Verbose.Print("Loading UserScriptAssembly...");
    stopwatch.Start();
    var assembly = Assembly.Load(cache.pe, cache.pdb);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly loading finished. ({0})", stopwatch.Elapsed);
    Verbose.Print("Evaluating UserScriptAssembly...");
    stopwatch.Restart();
    var type = assembly.GetType("Submission#0");
    var factory = type.GetMethod("<Factory>");
    var parameters = new object[] { new object[] { ctx, null } };
    var result = factory.Invoke(null, parameters);
    stopwatch.Stop();
    Verbose.Print("UserScriptAssembly evaluation finished. ({0})", stopwatch.Elapsed);
    return ctx.GetGestureDefinition();
}

評価後 ctx にはマウスジェスチャーの設定が保存されています。

仕組み的に Crevice 2 と Crevice 3 でのユーザースクリプトの実行は全く同様の結果になる…はずなのですが、いまいち確信が持てないので失敗時にはフォールバックするようには実装しています。

private IEnumerable<Core.GestureDefinition> GetGestureDef(Core.UserScriptExecutionContext ctx)
{
    var userScriptCode = GetUserScriptCode();
    var userScript = ParseScript(userScriptCode);
    if (!Global.CLIOption.NoCache)
    {
        try
        {
            var cache = GetUserScriptAssemblyCache(userScriptCode, userScript);
            return EvaluateUserScriptAssembly(cache, ctx);
        }
        catch (Exception ex)
        {
            Verbose.Print("Error occured when UserScript conpilation and save and restoration; fallback to the --nocache mode and continume");
            Verbose.Print(ex.ToString());
        }
    }
    return EvaluateUserScript(userScript, ctx);
}

うまく動かないよーという方がもしいらっしゃいましたらご連絡ください。

DisplayPortディスプレイには妖精が住んでいる(Windowsのディスプレイ構成をプロファイルごとに切り替える)

以前からWindowsのディスプレイ周りには困惑していて、特にDisplayPort(以下DP)が絡んだ場合には、DPの電源オフ時にはデバイス自体がOS上でオフ扱いになるという仕様からほんとにもうどうしようもない挙動をします。ディスプレイがオフ扱いになる、あるいは復帰するタイミング、またはデフォルトのモニター解像度の存在などが渾然一体となり、休憩から帰った後にウィンドウが元の位置にあることは決して保証されません。そのサイズすらもです。

この問題の対処法はいろいろあります。とりあえず簡単に紹介します。

DPディスプレイの電源を切らずに運用する

シンプルに因果律から導き出されるアイディアです。しばらく放っておくとDPディスプレイはスタンバイ状態に入り、この状態への移行、この状態からの復帰では特に問題は発生しません。 この解決策は良さそうに見えますが、席を立った後、あなたのディスプレイがまだ光を放っていることに気づいた親切な同僚の存在について考察してみましょう。

プログラムから即座にディスプレイをスタンバイ状態に移行する方法を以下に紹介します。あなたのディスプレイのインジケーターがスリープ時にモンスターたちを惹きつけないことを祈ります。

gist9167e2b52ab8033414fd70357079070e

モニターの解像度に関するレジストリ値を変更する

モニターを省電力から復帰させると解像度やウィンドウのサイズが小さくなるのを解決する : プログ 塵の雨日記: DisplayPort接続時のディスプレイのオンオフによるウィンドウの再配置について PC用4Kモニタでウインドウが小さく左上に偏る問題をレジストリ編集で解決

多くの方の直った!という声が寄せられていますが、この方法はマルチディスプレイ環境でウィンドウが別ウインドウに移動してしまう問題については効果がありません。

DPケーブルをハックする

DisplayPortの切断を回避するアダプター - Qiita DisplayPort接続でモニターの電源を切ると認識が外れるのを何とかできる? - Aquablews_apps.dump()

私はこの方法を試していませんが、コストと互換性問題について納得できれば試す価値はあるでしょう。

ウィンドウ位置を管理するツールを使う

あるディスプレイにウィンドウを集めるもの、ウィンドウの状態を保存・復元するものなどいろいろありますが、多くのツールが、異なるDPIを含むマルチディスプレイ環境で正常に動かず、ひとまず断念しました。いいツールがあったら教えてください。

ディスプレイ構成をプロファイルごとに切り替える

試行錯誤の後、最終的にこの方法にたどり着きました。要するに、悪いDPディスプレイは能動的に取り外して、DPディスプレイがオフ扱いになるタイミングを制御しようというものです。前述のレジストリ値を使う方法と組み合わせて、この問題を多少改善することができます。

1. Display Changer IIでWindowsのディスプレイ構成を保存・復元する

Display Changer II « 12noon

例えば以下のコマンドでProfile1.xmlにディスプレイの設定を保存し、復元することができます。

dc2.exe -create="Profile1.xml"
dc2.exe -configure="Profile1.xml"

このxmlファイルはその時点でのWindowsのディスプレイの設定を保存したものなので、グラフィックドライバの更新などでデバイス名などに大きな変更が加わった場合はその都度作り直す必要があります。

2. コマンドをタスクとして登録する

dc2.exe -configure="Profile1.xml" のようなコマンドを管理者権限のタスクとして、タスクスケジューラに登録しておきます。こうすることで、通常権限のAutoHotKeyアプリケーションからコマンドを実行できます。 ここでは例として、SwitchDisplayProfile1, SwitchDisplayProfile2, SwitchDisplayProfile3の3つのプロファイルを作成しました。

3. AutoHotKeyからディスプレイのプロファイルを切り替える

AutoHotKeyスクリプトには前述のDisplayStandBy.exeと管理者権限タスクSwitchDisplayProfile1, SwitchDisplayProfile2, SwitchDisplayProfile3を起動するフック、Ctrl+Shift+Alt+F9, Ctrl+Shift+Alt+F10, Ctrl+Shift+Alt+F11, Ctrl+Shift+Alt+F12を記述しました。

gist869012765c79d16d2604292c890c5f54

Amazonの過去の売れ筋商品データ(月間)を取得する

Amazonの商品コードを大量に集める必要があり、いろいろ方法を調べていると、次のようなものを見つけた。なかなか有用だと思うので取得方法を纏めておく。

過去の売れ筋商品データ(月間)

f:id:ruby-U:20171111085507p:plain

対象年月と商品カテゴリを選択してボタンを押せばデータへのリンクが表示される。

f:id:ruby-U:20171111085512p:plain

データのURLは固定されているようで、認証なども必要ない。好きなように取ってくればよさそうだ。

中身は普通のTSVで、1行のヘッダあり。なお、これは2011-01の本カテゴリのデータ。懐かしい…。 f:id:ruby-U:20171111090205p:plain

2011-01から2017-09までのデータを集めたものをダウンロードできるようにしておくので、面倒な人はこちら(Google Drive)

なお、注意点として、カメラカテゴリの他にも一部データに抜けがある。DVDカテゴリの2011-11, 2011-12など。恐らく元データが存在しない。

以下のスクリプトで作成した。

gist.github.com

GPUが使えて仮想環境を認識するJupyter環境のDockerfileを書いた

Jupyterが立ち上げるだけでプログラミング環境ができあがる各種の便利なDockerイメージを出してくれているが、GPUが使えない。ので、無理やり使えるようにしてみた。 Dockerfileの継承元のFROMを書き換える必要があったので強引な感じになった。 以下ではcudaのDockerイメージを元にしているが、jupyterを元にcudaをインストールしたほうが完了までの時間が短いはず。

なお、TensorflowがGPUの使えるJupyter入りのDockerイメージをリリースしてくれている。なので、Jupyter本家に拘らなければこんなことをする必要はほぼない…が、まあせっかく書いたので。

適当に継ぎ接ぎしてDockerfileを作る

mkdir ~/Documents/cuda-based-jupyter-datascience-notebook
cd ~/Documents/cuda-based-jupyter-datascience-notebook

wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/base-notebook/Dockerfile -O base-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/minimal-notebook/Dockerfile -O minimal-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/scipy-notebook/Dockerfile -O scipy-notebook
wget https://raw.githubusercontent.com/jupyter/docker-stacks/master/datascience-notebook/Dockerfile -O datascience-notebook

cat << "EOF" > Dockerfile
# Merging nvidia/cuda and jupyter/datascience-notebook DockerFiles.
FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04

MAINTAINER rubyu

EOF
cat << "EOF" >> Dockerfile

# jupter/base-notebook
EOF
sed -e '1,8 s/^/# /g' base-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/minimal-notebook
EOF
sed -e '1,6 s/^/# /g' minimal-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/scipy-notebook
EOF
sed -e '1,5 s/^/# /g' scipy-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

# jupter/datascience-notebook
EOF
sed -e '1,5 s/^/# /g' datascience-notebook >> Dockerfile
cat << "EOF" >> Dockerfile

USER $NB_USER

RUN jupyter notebook --generate-config && \
    jupyter serverextension enable --py jupyterlab --sys-prefix && \
    pip install environment_kernels && \
    echo "c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager'" >> ~/.jupyter/jupyter_notebook_config.py
EOF

最後のRUNの部分がJupyter Labとconda createで作る仮想環境のための設定。

Dockerイメージ作成に必要なファイルをダウンロードしておく

wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/fix-permissions -O fix-permissions
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/jupyter_notebook_config.py -O jupyter_notebook_config.py
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start-notebook.sh -O start-notebook.sh
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start-singleuser.sh -O start-singleuser.sh
wget https://github.com/jupyter/docker-stacks/raw/master/base-notebook/start.sh -O start.sh
sudo chmod ugo+x fix-permissions
sudo chmod ugo+x jupyter_notebook_config.py
sudo chmod ugo+x start-notebook.sh
sudo chmod ugo+x start-singleuser.sh
sudo chmod ugo+x start.sh

実行できる権限がないとダメ。ここではwgetしているが、普通はgit cloneするのでハマることはないはず。

コンテナを立ち上げる

sudo nvidia-docker build -t rubyu/datascience-notebook-gpu:1.0 .

sudo nvidia-docker run -d --name notebook-gpu -p 8888:8888 --restart=always -v /storage/samba:/home/jovyan rubyu/datascience-notebook-gpu:1.0 start.sh jupyter lab --NotebookApp.token='XXXX'

これで8888ポートでJupyter Labが起動している。

本題には関係ない話ですが、Jupyterのhome(/home/jovyan)をまるごとホストのディレクトリに割り当てて、かつそれをSamba経由でアクセスできるようにするのが好きです。Windowsで処理する必要がある時にいちいち転送する必要がなくなるので。なんやかんやでGUIWindowsが使いやすい(個人の感想です)

アタッチしてconda createで仮想環境を作る

docker exec -it notebook-gpu /bin/bash

conda create -n your-new-envronment python=3.5 jupyter tensorflow-gpu && conda clean -tipsy

作成された仮想環境はJupyterから見えて、カーネルとして選択することができる。

Dockerfile

# Merging nvidia/cuda and jupyter/datascience-notebook DockerFiles.
FROM nvidia/cuda:8.0-cudnn6-devel-ubuntu16.04

MAINTAINER rubyu


# jupter/base-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# 
# # Ubuntu 16.04 (xenial) from 2017-07-23
# # https://github.com/docker-library/official-images/commit/0ea9b38b835ffb656c497783321632ec7f87b60c
# FROM ubuntu@sha256:84c334414e2bfdcae99509a6add166bbb4fa4041dc3fa6af08046a66fed3005f
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# Install all OS dependencies for notebook server that starts but lacks all
# features (e.g., download as all possible file formats)
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update && apt-get -yq dist-upgrade \
 && apt-get install -yq --no-install-recommends \
    wget \
    bzip2 \
    ca-certificates \
    sudo \
    locales \
    fonts-liberation \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

RUN echo "en_US.UTF-8 UTF-8" > /etc/locale.gen && \
    locale-gen

# Install Tini
RUN wget --quiet https://github.com/krallin/tini/releases/download/v0.10.0/tini && \
    echo "1361527f39190a7338a0b434bd8c88ff7233ce7b9a4876f3315c22fce7eca1b0 *tini" | sha256sum -c - && \
    mv tini /usr/local/bin/tini && \
    chmod +x /usr/local/bin/tini

# Configure environment
ENV CONDA_DIR=/opt/conda \
    SHELL=/bin/bash \
    NB_USER=jovyan \
    NB_UID=1000 \
    NB_GID=100 \
    LC_ALL=en_US.UTF-8 \
    LANG=en_US.UTF-8 \
    LANGUAGE=en_US.UTF-8
ENV PATH=$CONDA_DIR/bin:$PATH \
    HOME=/home/$NB_USER

ADD fix-permissions /usr/local/bin/fix-permissions
# Create jovyan user with UID=1000 and in the 'users' group
# and make sure these dirs are writable by the `users` group.
RUN useradd -m -s /bin/bash -N -u $NB_UID $NB_USER && \
    mkdir -p $CONDA_DIR && \
    chown $NB_USER:$NB_GID $CONDA_DIR && \
    fix-permissions $HOME && \
    fix-permissions $CONDA_DIR

USER $NB_USER

# Setup work directory for backward-compatibility
RUN mkdir /home/$NB_USER/work && \
    fix-permissions /home/$NB_USER

# Install conda as jovyan and check the md5 sum provided on the download site
ENV MINICONDA_VERSION 4.3.21
RUN cd /tmp && \
    wget --quiet https://repo.continuum.io/miniconda/Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh && \
    echo "c1c15d3baba15bf50293ae963abef853 *Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh" | md5sum -c - && \
    /bin/bash Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh -f -b -p $CONDA_DIR && \
    rm Miniconda3-${MINICONDA_VERSION}-Linux-x86_64.sh && \
    $CONDA_DIR/bin/conda config --system --prepend channels conda-forge && \
    $CONDA_DIR/bin/conda config --system --set auto_update_conda false && \
    $CONDA_DIR/bin/conda config --system --set show_channel_urls true && \
    $CONDA_DIR/bin/conda update --all --quiet --yes && \
    conda clean -tipsy && \
    fix-permissions $CONDA_DIR

# Install Jupyter Notebook and Hub
RUN conda install --quiet --yes \
    'notebook=5.2.*' \
    'jupyterhub=0.8.*' \
    'jupyterlab=0.28.*' \
    && conda clean -tipsy && \
    fix-permissions $CONDA_DIR

USER root

EXPOSE 8888
WORKDIR $HOME

# Configure container startup
ENTRYPOINT ["tini", "--"]
CMD ["start-notebook.sh"]

# Add local files as late as possible to avoid cache busting
COPY start.sh /usr/local/bin/
COPY start-notebook.sh /usr/local/bin/
COPY start-singleuser.sh /usr/local/bin/
COPY jupyter_notebook_config.py /etc/jupyter/
RUN fix-permissions /etc/jupyter/

# Switch back to jovyan to avoid accidental container runs as root
USER $NB_USER

# jupter/minimal-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# 
# FROM jupyter/base-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# Install all OS dependencies for fully functional notebook server
RUN apt-get update && apt-get install -yq --no-install-recommends \
    build-essential \
    emacs \
    git \
    inkscape \
    jed \
    libsm6 \
    libxext-dev \
    libxrender1 \
    lmodern \
    pandoc \
    python-dev \
    texlive-fonts-extra \
    texlive-fonts-recommended \
    texlive-generic-recommended \
    texlive-latex-base \
    texlive-latex-extra \
    texlive-xetex \
    vim \
    unzip \
    && apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Switch back to jovyan to avoid accidental container runs as root
USER $NB_USER

# jupter/scipy-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# FROM jupyter/minimal-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# libav-tools for matplotlib anim
RUN apt-get update && \
    apt-get install -y --no-install-recommends libav-tools && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

USER $NB_USER

# Install Python 3 packages
# Remove pyqt and qt pulled in for matplotlib since we're only ever going to
# use notebook-friendly backends in these images
RUN conda install --quiet --yes \
    'nomkl' \
    'ipywidgets=7.0*' \
    'pandas=0.19*' \
    'numexpr=2.6*' \
    'matplotlib=2.0*' \
    'scipy=0.19*' \
    'seaborn=0.7*' \
    'scikit-learn=0.18*' \
    'scikit-image=0.12*' \
    'sympy=1.0*' \
    'cython=0.25*' \
    'patsy=0.4*' \
    'statsmodels=0.8*' \
    'cloudpickle=0.2*' \
    'dill=0.2*' \
    'numba=0.31*' \
    'bokeh=0.12*' \
    'sqlalchemy=1.1*' \
    'hdf5=1.8.17' \
    'h5py=2.6*' \
    'vincent=0.4.*' \
    'beautifulsoup4=4.5.*' \
    'protobuf=3.*' \
    'xlrd'  && \
    conda remove --quiet --yes --force qt pyqt && \
    conda clean -tipsy && \
    # Activate ipywidgets extension in the environment that runs the notebook server
    jupyter nbextension enable --py widgetsnbextension --sys-prefix && \
    fix-permissions $CONDA_DIR

# Install facets which does not have a pip or conda package at the moment
RUN cd /tmp && \
    git clone https://github.com/PAIR-code/facets.git && \
    cd facets && \
    jupyter nbextension install facets-dist/ --sys-prefix && \
    rm -rf facets && \
    fix-permissions $CONDA_DIR

# Import matplotlib the first time to build the font cache.
ENV XDG_CACHE_HOME /home/$NB_USER/.cache/
RUN MPLBACKEND=Agg python -c "import matplotlib.pyplot" && \
    fix-permissions /home/$NB_USER

USER $NB_USER

# jupter/datascience-notebook
# # Copyright (c) Jupyter Development Team.
# # Distributed under the terms of the Modified BSD License.
# FROM jupyter/scipy-notebook
# 
# MAINTAINER Jupyter Project <jupyter@googlegroups.com>

USER root

# R pre-requisites
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
    fonts-dejavu \
    gfortran \
    gcc && apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Julia dependencies
# install Julia packages in /opt/julia instead of $HOME
ENV JULIA_PKGDIR=/opt/julia

RUN . /etc/os-release && \
    echo "deb http://ppa.launchpad.net/staticfloat/juliareleases/ubuntu $VERSION_CODENAME main" > /etc/apt/sources.list.d/julia.list && \
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 3D3D3ACC && \
    apt-get update && \
    apt-get install -y --no-install-recommends \
    julia && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    # Show Julia where conda libraries are \
    echo "push!(Libdl.DL_LOAD_PATH, \"$CONDA_DIR/lib\")" >> /usr/etc/julia/juliarc.jl && \
    # Create JULIA_PKGDIR \
    mkdir $JULIA_PKGDIR && \
    chown $NB_USER $JULIA_PKGDIR && \
    fix-permissions $JULIA_PKGDIR

USER $NB_USER

# R packages including IRKernel which gets installed globally.
RUN conda config --system --add channels r && \
    conda install --quiet --yes \
    'rpy2=2.8*' \
    'r-base=3.3.2' \
    'r-irkernel=0.7*' \
    'r-plyr=1.8*' \
    'r-devtools=1.12*' \
    'r-tidyverse=1.0*' \
    'r-shiny=0.14*' \
    'r-rmarkdown=1.2*' \
    'r-forecast=7.3*' \
    'r-rsqlite=1.1*' \
    'r-reshape2=1.4*' \
    'r-nycflights13=0.2*' \
    'r-caret=6.0*' \
    'r-rcurl=1.95*' \
    'r-crayon=1.3*' \
    'r-randomforest=4.6*' && \
    conda clean -tipsy && \
    fix-permissions $CONDA_DIR

# Add Julia packages
# Install IJulia as jovyan and then move the kernelspec out
# to the system share location. Avoids problems with runtime UID change not
# taking effect properly on the .local folder in the jovyan home dir.
RUN julia -e 'Pkg.init()' && \
    julia -e 'Pkg.update()' && \
    julia -e 'Pkg.add("HDF5")' && \
    julia -e 'Pkg.add("Gadfly")' && \
    julia -e 'Pkg.add("RDatasets")' && \
    julia -e 'Pkg.add("IJulia")' && \
    # Precompile Julia packages \
    julia -e 'using HDF5' && \
    julia -e 'using Gadfly' && \
    julia -e 'using RDatasets' && \
    julia -e 'using IJulia' && \
    # move kernelspec out of home \
    mv $HOME/.local/share/jupyter/kernels/julia* $CONDA_DIR/share/jupyter/kernels/ && \
    chmod -R go+rx $CONDA_DIR/share/jupyter && \
    rm -rf $HOME/.local && \
    fix-permissions $JULIA_PKGDIR $CONDA_DIR/share/jupyter


USER $NB_USER

RUN jupyter notebook --generate-config && \
    jupyter serverextension enable --py jupyterlab --sys-prefix && \
    pip install environment_kernels && \
    echo "c.NotebookApp.kernel_spec_manager_class = 'environment_kernels.EnvironmentKernelSpecManager'" >> ~/.jupyter/jupyter_notebook_config.py

怪しいダイス(プロットしてみる)

ダイスを振った記録があるとして、その一部で、極端に偏りのあるダイス(この例では奇遇の偏り)が使われているのではという疑念を持ったときに、どうして調べればいいか。

前回 怪しいダイス(hmmlearn) は、隠れマルコフモデルで調べてるんだけど…というお話だったので、hmmlearnを使って、怪しい痕跡が見えるなーということを確かめました。

ただ、思ったようには収束しない場合があり、もうちょっとわかりやすい判別方法がありそうなので、今回は単純に二項分布を使って調べてみます。

ただし、ここで注意点として、このダイスを振った記録は、不正でありつつ、かつ全体では偏りを調節してあるということです。(単に偏りがあるだけなら普通に検定を行えばいい)

やっていることは単純で、

  1. ダイスを振った記録について、ダイスのそれぞれの面ごとに、ある面が出現していない繋がりを求める。

  2. それぞれの繋がりについて、確率を、二項分布 Bi(n, p) n=繋がりの長さ, p=5/6、確率密度関数に x=n として求める。 要するに、ダイスのある面が出る確率は1/6なので、ダイスのある面が出ない確率は5/6になり、それがn回続く確率を求めます。

  3. 必要に応じて、それぞれの面について計算した値を、ダイスを振った記録の位置ごとに足したりする。

ランダムに生成したデータと怪しいデータで若干違いが見えるようになりました。

import itertools
import numpy as np


np.random.seed(1)

nf = 6
s = np.random.randint(0, nf, 200)
ns = len(s)
state = [0 for _ in range(ns)]
result = [[0 for _ in range(ns)] for _ in range(nf)]

i = 0
while(True):
    v = s[i]
    if i > 0:
        for k in range(nf):
            if k == v:
                d = i - state[k]
                score = ss.binom(d, (nf-1.)/nf).pmf(d)
                for j in range(state[k], i):
                    result[k][j] += np.log(score)
                state[k] = i
    i += 1
    if ns == i:
        for k in range(nf):
            d = i - state[k]
            score = ss.binom(d, (nf-1.)/nf).pmf(d)
            for j in range(state[k], i):
                result[k][j] += np.log(score)
        break

%matplotlib inline
import matplotlib.pyplot as plt
plt.title("logP of faces (random generated data)")
for res in result:
    plt.plot(res)

f:id:ruby-U:20171001182048p:plain

import itertools
import numpy as np


np.random.seed(1)

nf = 6
s = np.random.randint(0, nf, 200)
ns = len(s)
state = [0 for _ in range(ns)]
result = [0 for _ in range(ns)]

i = 0
while(True):
    v = s[i]
    if i > 0:
        for k in range(nf):
            if k == v:
                d = i - state[k]
                score = ss.binom(d, (nf-1.)/nf).pmf(d)
                for j in range(state[k], i):
                    result[j] += np.log(score)
                state[k] = i
    i += 1
    if ns == i:
        for k in range(nf):
            d = i - state[k]
            score = ss.binom(d, (nf-1.)/nf).pmf(d)
            for j in range(state[k], i):
                result[j] += np.log(score)
        break
        
%matplotlib inline
import matplotlib.pyplot as plt
plt.title("logP (random generated data)")
plt.plot(result)

f:id:ruby-U:20171001182105p:plain

import itertools
import numpy as np


np.random.seed(1)

nf = 6
s = np.random.randint(0, nf, 200)
ns = len(s)
state = [0 for _ in range(ns)]
result = [[0 for _ in range(ns)] for _ in range(nf)]

i = 0
while(True):
    v = s[i]
    if i > 0:
        for k in range(nf):
            if k == v:
                d = i - state[k]
                score = ss.binom(d, (nf-1.)/nf).pmf(d)
                for j in range(state[k], i):
                    result[k][j] += np.log(score)
                state[k] = i
    i += 1
    if ns == i:
        for k in range(nf):
            d = i - state[k]
            score = ss.binom(d, (nf-1.)/nf).pmf(d)
            for j in range(state[k], i):
                result[k][j] += np.log(score)
        break
        
import numpy as np
result = [np.array(result[0])+np.array(result[2])+np.array(result[4]), 
          np.array(result[1])+np.array(result[3])+np.array(result[5])]

%matplotlib inline
import matplotlib.pyplot as plt
plt.title("logP of odd and even (random generated data)")
for res in result:
    plt.plot(res)

f:id:ruby-U:20171001182117p:plain

import itertools
import numpy as np


np.random.seed(1)

nf = 6
x = "12442552156235244611513665441521334522422516131461324351224626256625324354313241416144163515363423135135313531535155116446122436632362242642626642654256461421553232365354361461451264546335353161532646"
s = [int(s)-1 for s in x]
ns = len(s)
state = [0 for _ in range(ns)]
result = [[0 for _ in range(ns)] for _ in range(nf)]

i = 0
while(True):
    v = s[i]
    if i > 0:
        for k in range(nf):
            if k == v:
                d = i - state[k]
                score = ss.binom(d, (nf-1.)/nf).pmf(d)
                for j in range(state[k], i):
                    result[k][j] += np.log(score)
                state[k] = i
    i += 1
    if ns == i:
        for k in range(nf):
            d = i - state[k]
            score = ss.binom(d, (nf-1.)/nf).pmf(d)
            for j in range(state[k], i):
                result[k][j] += np.log(score)
        break

%matplotlib inline
import matplotlib.pyplot as plt
plt.title("logP of faces (fishy data)")
for res in result:
    plt.plot(res)

f:id:ruby-U:20171001182132p:plain

import itertools
import numpy as np


np.random.seed(1)

nf = 6
x = "12442552156235244611513665441521334522422516131461324351224626256625324354313241416144163515363423135135313531535155116446122436632362242642626642654256461421553232365354361461451264546335353161532646"
s = [int(s)-1 for s in x]
ns = len(s)
state = [0 for _ in range(ns)]
result = [0 for _ in range(ns)]

i = 0
while(True):
    v = s[i]
    if i > 0:
        for k in range(nf):
            if k == v:
                d = i - state[k]
                score = ss.binom(d, (nf-1.)/nf).pmf(d)
                for j in range(state[k], i):
                    result[j] += np.log(score)
                state[k] = i
    i += 1
    if ns == i:
        for k in range(nf):
            d = i - state[k]
            score = ss.binom(d, (nf-1.)/nf).pmf(d)
            for j in range(state[k], i):
                result[j] += np.log(score)
        break

%matplotlib inline
import matplotlib.pyplot as plt
plt.title("logP (fishy data)")
plt.plot(result)

f:id:ruby-U:20171001182142p:plain

import itertools
import numpy as np


np.random.seed(1)

nf = 6
x = "12442552156235244611513665441521334522422516131461324351224626256625324354313241416144163515363423135135313531535155116446122436632362242642626642654256461421553232365354361461451264546335353161532646"
s = [int(s)-1 for s in x]
ns = len(s)
state = [0 for _ in range(ns)]
result = [[0 for _ in range(ns)] for _ in range(nf)]

i = 0
while(True):
    v = s[i]
    if i > 0:
        for k in range(nf):
            if k == v:
                d = i - state[k]
                score = ss.binom(d, (nf-1.)/nf).pmf(d)
                for j in range(state[k], i):
                    result[k][j] += np.log(score)
                state[k] = i
    i += 1
    if ns == i:
        for k in range(nf):
            d = i - state[k]
            score = ss.binom(d, (nf-1.)/nf).pmf(d)
            for j in range(state[k], i):
                result[k][j] += np.log(score)
        break
        
import numpy as np
result = [np.array(result[0])+np.array(result[2])+np.array(result[4]), 
          np.array(result[1])+np.array(result[3])+np.array(result[5])]

%matplotlib inline
import matplotlib.pyplot as plt
plt.title("logP of odd and even (fishy data)")
for res in result:
    plt.plot(res)

f:id:ruby-U:20171001182154p:plain

怪しいダイス(hmmlearn)

ダイスの出目を隠れマルコフモデルでふにふにすることについて質問されたので書いた。 とりあえずhmmlearnで簡単に。

gistのほうが見やすいかも DirtyDice.ipynb

!pip install hmmlearn
Collecting hmmlearn
  Downloading hmmlearn-0.2.0.tar.gz (107kB)
[K    100% |████████████████████████████████| 112kB 1.8MB/s ta 0:00:01
[?25hBuilding wheels for collected packages: hmmlearn
  Running setup.py bdist_wheel for hmmlearn ... [?25ldone
[?25h  Stored in directory: /home/jovyan/.cache/pip/wheels/de/6c/25/c5fc03ff58b509d7b9769a0d5e2f69fc3fe89feebd70b89b86
Successfully built hmmlearn
Installing collected packages: hmmlearn
Successfully installed hmmlearn-0.2.0
import math
import numpy as np
from hmmlearn import hmm


np.random.seed(1) # デバッグのためにシードを設定しておく
# まずマトモなダイスの出力を再現してみる
n_dice = 1 #ダイスの数は1で
n_dice_faces = 6 # 6面とする
model = hmm.MultinomialHMM(n_components=n_dice)
model.startprob_ = np.array([1.0]) # ダイスは1つなので、最初のダイスが選ばれる確率は1
model.transmat_ = np.array([[1.0]]) # ダイスは1つなので、最初のダイスから常に最初のダイスが選ばれる
model.emissionprob_ = np.array([[1/6, 1/6, 1/6, 1/6, 1/6, 1/6]]) # ダイスのそれぞれの面は等しい確率で選ばれる
# ダイスを100回振ったとして
n_try = 100
x, z = model.sample(n_try) 
x = x.reshape(1, -1)[0] # 見やすく変形しているだけ
# ダイスを振った記録
x
array([5, 4, 1, 2, 2, 1, 2, 4, 2, 4, 5, 1, 5, 4, 4, 4, 2, 0, 0, 1, 5, 3, 2,
       1, 1, 5, 0, 2, 1, 5, 3, 0, 2, 3, 3, 4, 3, 0, 5, 3, 5, 5, 3, 3, 0, 4,
       1, 2, 2, 5, 0, 3, 3, 3, 0, 0, 4, 2, 4, 1, 3, 1, 1, 2, 3, 4, 0, 5, 4,
       3, 4, 4, 5, 1, 2, 3, 4, 2, 2, 3, 0, 0, 0, 4, 4, 2, 2, 2, 0, 0, 0, 0,
       5, 5, 0, 3, 5, 4, 2, 1])
# 常に最初のダイスが選ばれる
z
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0])
# この試行結果からそれぞれの面の確率を求めると、
remodel = hmm.MultinomialHMM(n_components=n_dice, n_iter=100)
remodel.fit(np.array([x]).T)
remodel.emissionprob_
array([[ 0.165,  0.17 ,  0.17 ,  0.165,  0.165,  0.165]])
# さて、ここで、"怪しい" サイコロのログを調べてみるとする  
# Ref: http://www.fward.net/archives/1548
x = "12442552156235244611513665441521334522422516131461324351224626256625324354313241416144163515363423135135313531535155116446122436632362242642626642654256461421553232365354361461451264546335353161532646"
x = [int(s)-1 for s in x]

remodel = hmm.MultinomialHMM(n_components=n_dice, n_iter=100)
remodel.fit(np.array([x]).T)
remodel.emissionprob_ # ダイスの面の出現確率は揃っていて、不正を見抜くことができない
array([[ 0.165,  0.17 ,  0.17 ,  0.165,  0.165,  0.165]])
# 復数のダイスがあると仮定してみる

remodel = hmm.MultinomialHMM(n_components=2, n_iter=100) # 2個のダイスを想定する
remodel.fit(np.array([x]).T)
remodel.transmat_
array([[ 0.962044  ,  0.037956  ],
       [ 0.18593815,  0.81406185]])
remodel.emissionprob_
array([[  1.31447796e-01,   2.10098809e-01,   1.31451592e-01,
          2.03925627e-01,   1.27562672e-01,   1.95513504e-01],
       [  3.07222812e-01,   2.71009724e-05,   3.33400979e-01,
          4.49521672e-08,   3.23691279e-01,   3.56577840e-02]])
log_p, state_seq = remodel.decode(np.array([x]).T)
state_seq # 綺麗な結果
array([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1,
       0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0])
# ただし、事前知識がないのでfit()の結果は安定しない…
# 次のような結果によくなる
remodel.fit(np.array([x]).T)
log_p, state_seq = remodel.decode(np.array([x]).T)
state_seq
array([0, 1, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0,
       1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0,
       0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0,
       1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1,
       0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 0, 1,
       1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 0, 1,
       0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1, 0,
       1, 0, 1, 0, 1, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0,
       1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1])
# このようなときは、初期値を明示的に設定してみる

remodel = hmm.MultinomialHMM(n_components=2, n_iter=10000, init_params="st", params="ste")
remodel.emissionprob_ = np.array([[1/6, 1/6, 1/6, 1/6, 1/6, 1/6],
                                  [2/6,   0, 2/6,   0, 2/6,   0]]) # 奇数のみ
remodel.fit(np.array([x]).T)
log_p, state_seq = remodel.decode(np.array([x]).T)
state_seq
/opt/conda/lib/python3.6/site-packages/hmmlearn/hmm.py:405: RuntimeWarning: divide by zero encountered in log
  return np.log(self.emissionprob_)[:, np.concatenate(X)].T





array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
       1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])

参考:

あやしいサイコロと『隠れマルコフモデル』 | 株式会社フォワードネットワーク

API Reference — hmmlearn 0.2.1 documentation

Logicoolマウスの敏感なホイールをソフトウェアでどうにかする

小型かつ、標準でL, M, R, W系x4, X1, X2に割り当てがあるLogicool m546をポチったんですが、ホイールが敏感すぎて、美少女紙芝居ゲーで遊ぶのと、ホイールでのマウスジェスチャが辛かったので対策しました。ブラウジングにはいいんですが。もうちょっとホイールのカチカチ感が強いといいバランスかもしれません。

var VWManager = new VerticalWheelManager(
    400, // Duration in milliseconds for suppression of wheel events given after once it have emitted.
    6    // Max number of the wheel events which will be suppressed in the duration for suppression.
);
@do((ctx) =>
{
    if (VWManager.Up.Check())
    {
        // do something
    }
}

VerticalWheelManagerのコンストラクタに渡す値は「連続するホイールメッセージを無効にする期間(ミリ秒)」, 「その期間内でも一定数以上連続すれば有効にする」です。あとはVerticalWheelManager.Up.Check()VerticalWheelManager.Down.Check()を叩けば、truefalseの判定の結果が返ります。

以下、マウスジェスチャツール CreviceAppの設定ファイルサンプルです。M325, M545, M546あたりの軽いホイール。または高速スクロール対応機種にも便利かもしれません。

gist6de4bf1c5f7d936bc2df2873173dec65

このぐらいの処理をマウスジェスチャツール本体ではなく、設定側でどうにかできるというのはなかなか便利です。