Edited at

Conformance test #1 について (実行コマンド生成編)


全体の構成



  • 前振り編


    • #1 でテストされる入力ファイルおよび期待される出力の説明



  • 実行コマンド生成編 (いまここ)


    • #1 で実行されるコマンドを CWL ファイルから生成する方法の解説




  • Docker & 出力パラメータ編


    • 実行コマンド生成編では取り扱わなかった、Docker 上でコマンドを動かす場合の解説および、出力パラメータの補足方法の解説



また、ダイジェスト版の説明スライド(英語)もあります。


注意!

この記事は CWL に対応したワークフローエンジンを実装する人向けの記事です!

CWL を書いて利用する一般の方は「中の人は大変そうだなぁ」と思いつつ、お茶でも飲みながら軽い気持ちで読んでいただければ幸いです。

実装する人は心を強く持ちましょう。


つづき

昨日の記事では「僕の考えた最強のワークフローエンジン」の最初の(そしてかなり大きな)壁である conformance test #1 の入力パラメータや出力例などを簡単に示しました。


次回は、このテストの突破に必要な仕様の解説を行います。


と書きましたが、分量が増えたので、今回は DockerRequirement と出力パラメータを適用する前の段階まで解説を行います。


早速解説

4.1 節に CWL での実行コマンド生成について記述があります。

以下が概要です。各番号は 4.1 節での手順に対応しています。



  1. arguments から CommandLineBinding オブジェクトを収集する。


  2. inputs から CommandLineBinding オブジェクトを収集し、入力オブジェクトをそれぞれ関連付ける。


  3. inputs から収集した CommandLineBinding にソート用のキーを対応付ける

  4. いい感じにソートする。

  5. CommandLineBinding オブジェクトに対して、引数生成ルールを適用する。

  6. 先頭に baseCommand を挿入して完成!


1. arguments から CommandLineBinding オブジェクトを収集する (いきなり仕様にない: #806)

4.1 節 の 1 は以下になります。


Collect CommandLineBinding objects from arguments. Assign a sorting key [position, i] where position is CommandLineBinding.position and i is the index in the arguments list.


CommandLineBinding は、5.1.2 節 で定義されたフィールドを持つオブジェクトのことです。今回の例では、最後の要素がそれに当たります。

じゃあ最初の2つはなんなんだ…?と思って仕様を確認すると、arguments の要素として文字列も来る場合があると記述がありますが、argumentsCommandLineBinding 以外が来る場合の処理についてはどこにも記述がありません


v1.0/bwa-mem-tool.cwl

arguments:

- bwa
- mem
- valueFrom: $(runtime.cores)
position: 1
prefix: -t

CWL project leader の Michael R. Crusoe 氏に確認すると、以下のような CommandLineBinding と置き換えても実装上は問題ないとのことでした。


同値なarguments

arguments:

- valueFrom: bwa
- valueFrom: mem
- valueFrom: $(runtime.cores)
position: 1
prefix: -t

これについては Issue #806 としてすでに登録されているので、v1.1 では明文化されるはずです。以下では "bwa""mem" を、上記の同値な CommandLineBindingオブジェクトとして取り扱います。

次に、各要素とソート用のキーを割り当てます。

5.1.2 節position の項を見ると、


The sorting key. Default position is 0.


とあることから、以下のようにキーと CommandLineBinding オブジェクトを関連付けられます。

キー([position, i])

CommandLineBinding オブジェクト

[0, 0]
"bwa"

[0, 1]
"mem"

[1, 2]
{ "valueFrom": ...}


2. inputs から CommandLineBinding オブジェクトを収集し、入力オブジェクトをそれぞれ関連付ける。

以下が 4.1 節 の 2 です。


Collect CommandLineBinding objects from the inputs schema and associate them with values from the input object. Where the input type is a record, array, or map, recursively walk the schema and input object, collecting nested CommandLineBinding objects and associating them with values from the input object.


inputBinding 以下が、inputsCommandLineBinding オブジェクトです。

また bwa-mem-job.json が入力オブジェクトです。

まずは入力オブジェクトを見ていきましょう。


default パラメータ

入力オブジェクトを見ると、フィールドが4つあることがわかります。


v1.0/bwa-mem-job.json

{

"reference": ...,
"reads": ...,
"min_std_max_min": ...,
"minimum_seed_length": ...
}

それぞれ CWL ファイルの inputs の各フィールドが対応しているはずなのですが…


v1.0/bwa-mem-tool.cwl

inputs:

- id: reference
...
- id: reads
...
- id: minimum_seed_length
...
- id: min_std_max_min
...
- id: args.py # <------------ !?
...

あぁん…?


何が起きてるの

入力パラメータ args.py を詳しく見ると、以下のようになっています。


v1.0/bwa-mem-tool.cwl

inputs:

...
- id: args.py
type: File
default:
class: File
location: args.py
...

CommandLineTool の 5.0 節には以下の記述があります。


If an input parameter is missing from the input object, it must be assigned a value of null (or the value of default for that parameter, if provided) ...


そのため default フィールドで指定された args.py ファイル (厳密には v1.0/bwa-mem-tool.cwl と同一ディレクトリにある args.py) が代わりに args.py パラメータの値として使用されます。パラメータ名とファイル名が同じなのでややこしい…


無視される入力パラメータのフィールド (仕様にない!: #808, #811)

さて、今度は reference パラメータを見てみましょう。


v1.0/bwa-mem-job.json

{

"reference": {
"class": "File",
"location": "chr20.fa",
"size": 123,
"checksum": "sha1$hash"
},
...
}

"size": 123とあるから中身があるファイルなのね」と思い、実際に中身を確認すると…

$ wc -c v1.0/chr20.fa

0

中身は空です。それによく見たら checksum も有効な sha1 ハッシュ値ではありません。

これをどう扱うべきか仕様を確認すると…

どこにも記述がない


  • 本来的には: 仕様にどうなるべきか(あるいは動作が実装依存だと)記述があるべき

  • 現状: 仕様に一切記述がないが、このテストはパスする意図で書かれている。おそらく入力パラメータの File オブジェクトの sizechecksum 等のパラメータは無視していいのだろう…


    • CWL project leader の Michael R. Crusoe 氏に確認したら、clarify すべきという Issue と Pull Request が立ちました(#808, #811)。

    • v1.1 では明文化されるはずです。仕様が公開されていると、Issue などで現状どうなっているか確認できるので便利ですね!



今回は、入力オブジェクトの sizechecksum が間違っていても受け付けてくれるゆるい処理系を前提にして話を進めます1


3. inputs から収集した CommandLineBinding にソート用のキーを対応付ける

以下が4.1 節 の 3 項です。


Create a sorting key by taking the value of the position field at each level leading to each leaf binding object. If position is not specified, it is not added to the sorting key. For bindings on arrays and maps, the sorting key must include the array index or map key following the position. If and only if two bindings have the same sort key, the tie must be broken using the ordering of the field or parameter name immediately containing the leaf binding.


つまり、

- 基本は position (省略時は 0)

- position が同じ場合にはフィールド名

この条件に従ってキーを対応付けたものが以下になります。

キー
フィールド名

[2, "reference"]
reference

[3, "reads"]
reads

[1, "minimum_seed_length"]
minimum_seed_length

[1, "min_std_max_min"]
min_std_max_min

[-1, "args.py"]
args.py


4. いい感じにソートする。

argumentsinputs での CommandLineBinding オブジェクトをソート用キーと一緒にまとめたものが以下の表です。

キー

CommandLineBinding オブジェクト

[0, 0]
"bwa"

[0, 1]
"mem"

[1, 2]
{ "valueFrom": ...}

[2, "reference"]

referenceinputBinding

[3, "reads"]

readsinputBinding

[1, "minimum_seed_length"]

minimum_seed_lengthinputBinding

[1, "min_std_max_min"]

min_std_max_mininputBinding

[-1, "args.py"]

args.pyinputBinding

4.1 節 の 4 の


Sort elements using the assigned sorting keys. Numeric entries sort before strings.


に従いソートすることで、以下の表が得られます。

キー

CommandLineBinding オブジェクト

[-1, "args.py"]

args.pyinputBinding

[0, 0]
"bwa"

[0, 1]
"mem"

[1, 2]
{ "valueFrom": ...}

[1, "min_std_max_min"]

min_std_max_mininputBinding

[1, "minimum_seed_length"]

minimum_seed_lengthinputBinding

[2, "reference"]

referenceinputBinding

[3, "reads"]

readsinputBinding


5. 各 CommandLineBinding オブジェクトに対して、引数生成ルールを適用する。

あとは4.1 節 の 5 にあるように、


In the sorted order, apply the rules defined in CommandLineBinding to convert bindings to actual command line elements.


を実行します。

5 中の rules は、5.1.2 節を参照してください。

以下では各 CommandLineBinding オブジェクトを、コマンドライン引数に変換していきます。


inputsargs.py


v1.0/bwa-mem-tool.cwl

inputs:

...
- id: args.py
type: File
default:
class: File
location: args.py
inputBinding:
position: -1
...

仕様には以下のようにあるため、単に path フィールドの値を使えばいいようです。


File: Add prefix and the value of File.path to the command line.


しかし与えられたパラメータには location フィールドしかありません。

ここでFile オブジェクトの仕様 (5.1.5 節)を確認すると、path については次のように書かれています。


Alternately to location, implementations must also accept the path property on File, which must be a filesystem path available on the same host ...


今回は location もローカルのファイルパスを指しているため、単に File.location の値(つまり "args.py")に変換できます。


arguments 内の "bwa""mem"

仕様には、


`string: Add prefix and the string to the command line.


とあるため、それぞれ "bwa""mem" に変換できます。


arguments 内のややこしそうな CommandLineBindingオブジェクト


v1.0/bwa-mem-tool.cwl

arguments:

...
- valueFrom: $(runtime.cores)
position: 1
prefix: -t

5.1.2 節 を見ると、


For binding objects listed in CommandLineTool.arguments, the term "value" refers to the effective value after evaluating valueFrom.


とあるので、まず valueFrom を評価する必要があります。



  • $(...) については、3.4 節 および3.5 節に記載されています。これは Expression と呼ばれるもので、実行時のパラメータ (入力オブジェクトや出力ディレクトリ)などに評価される場合(3.4 節)と、JavaScript のコードとして評価される場合(3.5 節)があります。今回は InlineJavascriptRequirement は使われていないので、3.4 節のみを参照すればいいです。


  • runtime が取りうるフィールドについては 4.2 節を参照すればよいです。以下を見ると、$(runtime.cores) を CPU コア数に評価すれば良いことがわかります。


    • 本文を見ると、See ResourceRequirement for details on how to describe the hardware resources required by a tool. とも書いてあるため、今回は ResourceRequirement の項も見る必要があります。




runtime.cores: number of CPU cores reserved for the tool process




  • 5.12 節ResourceRequirement についての記述があります。今回は coresMin: 2 と指定があるり、また 5.12 節に If "min" is specified but "max" is not, then "max" == "min" と記述があるため、最終的に $(runtime.cores)2 に評価されることがあります


v1.0/bwa-mem-tool.cwl

hints:

- class: ResourceRequirement
coresMin: 2


  • 注目すべきは hints です。3.3 節 を見ると、A hint is similar to a requirement; however, it is not an error if an implementation cannot satisfy all hints. とあります。つまり実行エンジンは、この ResourceRequirement を無視して、$(runtime.cores) で 2 以外の値を返しても仕様上は問題ありません。今回は $(runtime.cores) が 2 を返す場合を考えます。

5.1.2 節 に数値の変換規則が


number: Add prefix and decimal representation to command line.


とあるので、最終的に ["-t", 2] に変換できます2


inputsmin_std_max_min


v1.0/bwa-mem-tool.cwl

inputs:

...
- id: min_std_max_min
type: { type: array, items: int }
inputBinding:
position: 1
prefix: -I
itemSeparator: ","
...


v1.0/bwa-mem-job.json

{

...
"min_std_max_min": [
1,
2,
3,
4
],
...
}

仕様には以下のようにあります。["-I", "1,2,3,4"] に変換すればよいです。


number: Add prefix and decimal representation to command line.

...

array: If itemSeparator is specified, add prefix and the join the array into a single string with itemSeparator separating the items. ...



minimum_seed_length


v1.0/bwa-mem-tool.cwl

inputs:

...
- id: minimum_seed_length
type: int
inputBinding: { position: 1, prefix: -m }
...


v1.0/bwa-mem-job.json

{

...
"minimum_seed_length": 3
}

仕様には以下のようにあります。["-m", 3] に変換すればよいです。


number: Add prefix and decimal representation to command line.



inputsreference


v1.0/bwa-mem-tool.cwl

inputs:

- id: reference
type: File
inputBinding: { position: 2 }


v1.0/bwa-mem-job.json

{

"reference": {
"class": "File",
"location": "chr20.fa",
...
},
...
}

仕様には以下のように書いてあるので、"chr20.fa"に変換できます。


File: Add prefix and the value of File.path to the command line.



inputsreads


v1.0/bwa-mem-tool.cwl

inputs:

...
- id: reads
type:
type: array
items: File
inputBinding: { position: 3 }
...


v1.0/bwa-mem-job.json

{

...
"reads": [
{
"class": "File",
"location": "example_human_Illumina.pe_1.fastq"
},
{
"class": "File",
"location": "example_human_Illumina.pe_2.fastq"
}
],
...
}

仕様には以下のようにあります。今回は itemSeparatorprefix も指定されていないので、単に ["example_human_Illumina.pe_1.fastq", "example_human_Illumina.pe_2.fastq"] を返せばいいです。


File: Add prefix and the value of File.path to the command line.

...

array: If itemSeparator is specified, ... Otherwise first add prefix, then recursively process individual elements.


最後に変換したものをまとめると、以下のようになります。

  - "args.py"

- "bwa"
- "mem"
- ["-t", 2]
- ["-I", "1,2,3,4"]
- ["-m", 3]
- "chr20.fa"
- ["example_human_Illumina.pe_1.fastq", "example_human_Illumina.pe_2.fastq"]


6. 先頭に baseCommand を挿入して完成!


Insert elements from baseCommand at the beginning of the command line.



v1.0/bwa-mem-tool.cwl

baseCommand: python


先程変換したものに追加して、実行コマンドの完成です!

$ python args.py bwa mem -t 2 -I 1,2,3,4 -m 3 chr20.fa example_human_Illumina.pe_1.fastq example_human_Illumina.pe_2.fastq


標準出力の捕捉

与えられたツール定義に stdoutstderr フィールドがある場合、コマンドの標準出力や標準エラー出力をファイルとしてリダイレクトする必要があります (5 節)。


v1.0/bwa-mem-tool.cwl

stdout: output.sam


今回の場合、標準出力の結果が output.sam にリダイレクトされます。

また、その時の出力ディレクトリは $(runtime.outdir) で指定されたディレクトリです(cwltool の場合、デフォルトは $PWD で、--outdir でも設定できます)。

例えば、出力ディレクトリが $PWD の場合、リダイレクトも含めた最終的な実行コマンドは以下になります。

$ python args.py bwa mem -t 2 -I 1,2,3,4 -m 3 chr20.fa example_human_Illumina.pe_1.fastq example_human_Illumina.pe_2.fastq > $PWD/output.sam


動くか確認してみる

$ pwd

/Users/tom-tan/common-workflow-language
$ cd v1.0/1.0
$ python args.py bwa mem -t 2 -I 1,2,3,4 -m 3 chr20.fa example_human_Illumina.pe_1.fastq example_human_Illumina.pe_2.fastq > $PWD/output.sam
$ ls output.sam
output.sam

うまく動いてるようですね!


これで全部?

実はまだすべきことがあります。

例えば、

$ pwd

/Users/tom-tan/common-workflow-language/v1.0/v1.0
$ git status
...
Untracked files:
...
cwl.output.json
...
$ cat cwl.output.json
{"args": ["bwa", "mem", "-t", "2", "-I", "1,2,3,4", "-m", "3", "chr20.fa", "example_human_Illumina.pe_1.fastq", "example_human_Illumina.pe_2.fastq"]}

先程のコマンドは output.sam 以外にもファイルを出力していますが、これは一体どう扱えばいいのでしょうか?

また、実行コマンド生成に使っていないパーツが残っているようです。


v1.0/bwa-mem-tool.cwl

...

hints:
...
- class: DockerRequirement
dockerPull: python:2-slim
...
outputs:
- id: sam
type: ["null", File]
outputBinding: { glob: output.sam }
- id: args
type:
type: array
items: string

具体的には、

- コマンドを Docker コンテナで動かすときに注意すべきことは? (実装者視点)

- 出力オブジェクトのキャプチャーどうするの

- 頑張って解説した出力オブジェクトのキャプチャルールを台無しにする cwl.output.json


つづく

次回で全部解説できるはず!





  1. この場合の挙動について仕様には記載がないため、この不届きな入力に対して permanentFailure で落ちる場合も、それとなく無視して実行される場合も仕様に従った動作と言える可能性があります。皆さんは(動作が処理系依存になりうる)このような入力ファイルは用意しないようにしましょうね! 



  2. が、先程も述べたように $(runtime.cores) は2以外にも解釈されうるので、["-t", 3]["-t", 4] でも仕様上は間違っていません。しかし run_tesh.sh コマンドは $(runtime.cores)2 にならないとテストが通らないため、実は conformance test #1 は特定の処理系に依存したテストになってしまっています(報告済み)。皆さんも環境や処理系に依存したテストは書かないように気をつけましょうね!