diff --git a/examples/dataset2/info_day1.json b/examples/dataset2/info_day1.json new file mode 100644 index 00000000..84cbfa5f --- /dev/null +++ b/examples/dataset2/info_day1.json @@ -0,0 +1,6 @@ +{ + "name": "Day 1 experiment", + "description": "This is the first day of the experiment.", + "experimenter": "John Doe", + "date": "2023-10-01" +} \ No newline at end of file diff --git a/examples/dataset2/info_day2.json b/examples/dataset2/info_day2.json new file mode 100644 index 00000000..842cbd89 --- /dev/null +++ b/examples/dataset2/info_day2.json @@ -0,0 +1,6 @@ +{ + "name": "Day 2 experiment", + "description": "This is the second day of the experiment.", + "experimenter": "John Doe", + "date": "2023-10-02" +} \ No newline at end of file diff --git a/examples/dataset2/info_day3.json b/examples/dataset2/info_day3.json new file mode 100644 index 00000000..891e4808 --- /dev/null +++ b/examples/dataset2/info_day3.json @@ -0,0 +1,6 @@ +{ + "name": "Day 3 experiment", + "description": "This is the third day of the experiment.", + "experimenter": "Mary Smith", + "date": "2023-10-05" +} \ No newline at end of file diff --git a/examples/dataset2/info_day4.json b/examples/dataset2/info_day4.json new file mode 100644 index 00000000..3778fc8a --- /dev/null +++ b/examples/dataset2/info_day4.json @@ -0,0 +1,6 @@ +{ + "name": "Day 3 experiment", + "description": "This is the fourth day of the experiment.", + "experimenter": "Mary Smith", + "date": "2023-10-06" +} \ No newline at end of file diff --git a/examples/dataset2/info_day5.json b/examples/dataset2/info_day5.json new file mode 100644 index 00000000..6cf194c2 --- /dev/null +++ b/examples/dataset2/info_day5.json @@ -0,0 +1,6 @@ +{ + "name": "Day 5 experiment", + "description": "This is the fifth day of the experiment.", + "experimenter": "Paul Brown", + "date": "2023-10-09" +} \ No newline at end of file diff --git a/notebooks/01_orcabridge_core_concepts copy.ipynb b/notebooks/01_orcabridge_core_concepts copy.ipynb index 48aae52d..590b9777 100644 --- a/notebooks/01_orcabridge_core_concepts copy.ipynb +++ b/notebooks/01_orcabridge_core_concepts copy.ipynb @@ -59,11 +59,6 @@ "* Different pipeline DAG defintion -- In `orcapod` the directed acyclic graph (DAG) for the `pipeline` should be defined using YAML file (or less frequently using API on `pipeline` struct in the Rust library). In `orcabridge` you will find that a `pipeline` DAG is defined dynamically through a series of application of `operation`. This is very much akin to how some DAG-based neural network library like TensorFlow defines a computation graph. While this works well for simple examples, it is rather difficult to track changes to the pipeline defined dynamically/programmatically using version control system. Since *how* you define the pipeline DAG is strictly speaking an orthogonal problem to the everything else that concerns the operation of the `pipeline`, no effort will be given to align the DAG definition in `orcabridge` and `orcapod`.\n", "* Limited usage of a `stream` -- Currently `orcabrdige` only support single producer single consumer (SCSP) `stream`x, whereas in `orcapod`, `stream` should support single producer multiple consumer (SPMC) paradigm. While the same stream can be used in multiple downstream operations, each iteration of the stream actually results in recomputations of the entire chain of pipeline leading up to that stream. This inefficiency can be ameliorated by `CacheStream` operation after particularly computationally expensive segment of the pipeline. Using storage-backed `FunctionPod` will also help ameliorate the cost of recomputation by retrieving memoized computation result instead of recomputing." ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [] } ], "metadata": { diff --git a/notebooks/02_orcabridge_basic_usage.ipynb b/notebooks/02_orcabridge_basic_usage.ipynb index d0e783ba..c90964fc 100644 --- a/notebooks/02_orcabridge_basic_usage.ipynb +++ b/notebooks/02_orcabridge_basic_usage.ipynb @@ -37,7 +37,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -53,16 +53,24 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[0m\u001b[01;32mday1.txt\u001b[0m* \u001b[01;32mday2.txt\u001b[0m* \u001b[01;32mday3.txt\u001b[0m* \u001b[01;32mday4.txt\u001b[0m*\n" + ] + } + ], "source": [ - "ls ../examples/dataset1" + "%ls ../examples/dataset1" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -79,9 +87,20 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Packet {'txt_file': PosixPath('../examples/dataset1/day1.txt')} with tag {'file_name': 'day1'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day2.txt')} with tag {'file_name': 'day2'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day3.txt')} with tag {'file_name': 'day3'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day4.txt')} with tag {'file_name': 'day4'}\n" + ] + } + ], "source": [ "for tag, packet in dataset1():\n", " print(f\"Packet {packet} with tag {tag}\")" @@ -89,9 +108,20 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Packet {'txt_file': PosixPath('../examples/dataset1/day1.txt')} with tag {'file_name': 'day1'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day2.txt')} with tag {'file_name': 'day2'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day3.txt')} with tag {'file_name': 'day3'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day4.txt')} with tag {'file_name': 'day4'}\n" + ] + } + ], "source": [ "# equivalent to above but more natural without the need to call `dataset1()`\n", "for tag, packet in dataset1:\n", @@ -107,7 +137,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -123,9 +153,20 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Packet {'data': PosixPath('../examples/dataset1/day1.txt')} with tag {'date': 'day1'}\n", + "Packet {'data': PosixPath('../examples/dataset1/day2.txt')} with tag {'date': 'day2'}\n", + "Packet {'data': PosixPath('../examples/dataset1/day3.txt')} with tag {'date': 'day3'}\n", + "Packet {'data': PosixPath('../examples/dataset1/day4.txt')} with tag {'date': 'day4'}\n" + ] + } + ], "source": [ "for tag, packet in dataset1_custom:\n", " print(f\"Packet {packet} with tag {tag}\")" @@ -154,9 +195,20 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Packet {'bin_data': PosixPath('../examples/dataset2/session_day1.bin')} with tag {'file_name': 'session_day1'}\n", + "Packet {'bin_data': PosixPath('../examples/dataset2/session_day3.bin')} with tag {'file_name': 'session_day3'}\n", + "Packet {'bin_data': PosixPath('../examples/dataset2/session_day4.bin')} with tag {'file_name': 'session_day4'}\n", + "Packet {'bin_data': PosixPath('../examples/dataset2/session_day5.bin')} with tag {'file_name': 'session_day5'}\n" + ] + } + ], "source": [ "dataset2 = GlobSource(\"bin_data\", \"../examples/dataset2\", \"*.bin\")\n", "\n", @@ -220,24 +272,41 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Once you have created `source` from which streams can be formed, you can alter the stream by applying various `mappers`. More precisely, a `mapper` can work on tags and/or packets." + "Once you have created a `source` from which streams can be formed, you can alter the stream by applying various `mappers`. More precisely, a `mapper` can work on tags and/or packets." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Map keys\n", - "Likey one of the most common mapper operation to be found in Orcapod pipeline is `MapKeys` mapper. As the name implies, it let's you alter the keys (argument names) found in the `packet`." + "### Map packets\n", + "Likely one of the most common mapper operation to be found in Orcapod pipeline is `MapPackets` mapper. As the name implies, it let's you alter the keys (argument names) found in the `packet`." ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "from orcabridge.mapper import MapKeys\n", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Before mapping:\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day1.txt')} with tag {'file_name': 'day1'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day2.txt')} with tag {'file_name': 'day2'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day3.txt')} with tag {'file_name': 'day3'}\n", + "Packet {'txt_file': PosixPath('../examples/dataset1/day4.txt')} with tag {'file_name': 'day4'}\n", + "After mapping:\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day1.txt')} with tag {'file_name': 'day1'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day2.txt')} with tag {'file_name': 'day2'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day3.txt')} with tag {'file_name': 'day3'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day4.txt')} with tag {'file_name': 'day4'}\n" + ] + } + ], + "source": [ + "from orcabridge.mapper import MapPackets\n", "\n", "print(\"Before mapping:\")\n", "for tag, packet in dataset1:\n", @@ -245,10 +314,10 @@ "\n", "\n", "# create a new stream mapping packet keys 'txt_file' to 'content'\n", - "key_mapper = MapKeys(key_map={\"txt_file\": \"content\"})\n", + "packet_mapper = MapPackets(key_map={\"txt_file\": \"content\"})\n", "\n", "print(\"After mapping:\")\n", - "for tag, packet in key_mapper(dataset1):\n", + "for tag, packet in packet_mapper(dataset1):\n", " print(f\"Mapped Packet {packet} with tag {tag}\")" ] }, @@ -256,7 +325,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "You'd notice that for each packet, the key `txt_file` was replaced with `content` without altering the pointed `path` or the associated tag. As the keys of the packets will be used as the name of arguments when invoking pods on a stream, we will see that `MapKeys` are commonly used to *map* the correct path to the argument." + "You'd notice that for each packet, the key `txt_file` was replaced with `content` without altering the pointed `path` or the associated tag. As the keys of the packets will be used as the name of arguments when invoking pods on a stream, we will see that `MapPackets` are commonly used to *map* the correct path to the argument." ] }, { @@ -269,13 +338,24 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'day': 'day1'} {'txt_file': PosixPath('../examples/dataset1/day1.txt')}\n", + "{'day': 'day2'} {'txt_file': PosixPath('../examples/dataset1/day2.txt')}\n", + "{'day': 'day3'} {'txt_file': PosixPath('../examples/dataset1/day3.txt')}\n", + "{'day': 'day4'} {'txt_file': PosixPath('../examples/dataset1/day4.txt')}\n" + ] + } + ], "source": [ "from orcabridge.mapper import MapTags\n", "\n", - "tag_mapper = MapTags(tag_map={\"file_name\": \"day\"})\n", + "tag_mapper = MapTags(key_map={\"file_name\": \"day\"})\n", "\n", "for tag, packet in tag_mapper(dataset1):\n", " print(tag, packet)" @@ -297,17 +377,28 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "key_mapper = MapKeys(key_map={\"txt_file\": \"content\"})\n", - "key_mapped_stream = key_mapper(dataset1)\n", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapped Packet {'content': PosixPath('../examples/dataset1/day1.txt')} with tag {'day': 'day1'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day2.txt')} with tag {'day': 'day2'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day3.txt')} with tag {'day': 'day3'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day4.txt')} with tag {'day': 'day4'}\n" + ] + } + ], + "source": [ + "packet_mapper = MapPackets(key_map={\"txt_file\": \"content\"})\n", + "key_mapped_stream = packet_mapper(dataset1)\n", "\n", - "tag_mapper = MapTags(tag_map={\"file_name\": \"day\"})\n", - "tag_and_key_mapped = tag_mapper(key_mapped_stream)\n", + "tag_mapper = MapTags(key_map={\"file_name\": \"day\"})\n", + "tag_and_packet_mapped = tag_mapper(key_mapped_stream)\n", "\n", - "for tag, packet in tag_and_key_mapped:\n", + "for tag, packet in tag_and_packet_mapped:\n", " print(f\"Mapped Packet {packet} with tag {tag}\")" ] }, @@ -327,13 +418,24 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mapped Packet {'content': PosixPath('../examples/dataset1/day1.txt')} with tag {'day': 'day1'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day2.txt')} with tag {'day': 'day2'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day3.txt')} with tag {'day': 'day3'}\n", + "Mapped Packet {'content': PosixPath('../examples/dataset1/day4.txt')} with tag {'day': 'day4'}\n" + ] + } + ], "source": [ "# totally valid, but difficult to read and thus not recommended\n", - "for tag, packet in MapTags(tag_map={\"file_name\": \"day\"})(\n", - " MapKeys(key_map={\"txt_file\": \"content\"})(dataset1)\n", + "for tag, packet in MapTags(key_map={\"file_name\": \"day\"})(\n", + " MapPackets(key_map={\"txt_file\": \"content\"})(dataset1)\n", "):\n", " print(f\"Mapped Packet {packet} with tag {tag}\")" ] @@ -358,9 +460,27 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset 1:\n", + "Tag: {'file_name': 'day1'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day1.txt')}\n", + "Tag: {'file_name': 'day2'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day2.txt')}\n", + "Tag: {'file_name': 'day3'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day3.txt')}\n", + "Tag: {'file_name': 'day4'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day4.txt')}\n", + "\n", + "Dataset 2:\n", + "Tag: {'file_name': 'session_day1'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day1.bin')}\n", + "Tag: {'file_name': 'session_day3'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day3.bin')}\n", + "Tag: {'file_name': 'session_day4'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day4.bin')}\n", + "Tag: {'file_name': 'session_day5'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day5.bin')}\n" + ] + } + ], "source": [ "# dataset 1\n", "print(\"Dataset 1:\")\n", @@ -382,7 +502,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 14, "metadata": {}, "outputs": [], "source": [ @@ -417,11 +537,34 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "dataset1_retagged = MapTags(tag_map={\"file_name\": \"day\"})(dataset1)\n", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "01 Tag: {'day': 'day1', 'file_name': 'session_day1'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day1.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day1.bin')}\n", + "02 Tag: {'day': 'day1', 'file_name': 'session_day3'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day1.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day3.bin')}\n", + "03 Tag: {'day': 'day1', 'file_name': 'session_day4'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day1.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day4.bin')}\n", + "04 Tag: {'day': 'day1', 'file_name': 'session_day5'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day1.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day5.bin')}\n", + "05 Tag: {'day': 'day2', 'file_name': 'session_day1'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day2.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day1.bin')}\n", + "06 Tag: {'day': 'day2', 'file_name': 'session_day3'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day2.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day3.bin')}\n", + "07 Tag: {'day': 'day2', 'file_name': 'session_day4'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day2.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day4.bin')}\n", + "08 Tag: {'day': 'day2', 'file_name': 'session_day5'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day2.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day5.bin')}\n", + "09 Tag: {'day': 'day3', 'file_name': 'session_day1'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day3.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day1.bin')}\n", + "10 Tag: {'day': 'day3', 'file_name': 'session_day3'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day3.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day3.bin')}\n", + "11 Tag: {'day': 'day3', 'file_name': 'session_day4'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day3.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day4.bin')}\n", + "12 Tag: {'day': 'day3', 'file_name': 'session_day5'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day3.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day5.bin')}\n", + "13 Tag: {'day': 'day4', 'file_name': 'session_day1'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day4.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day1.bin')}\n", + "14 Tag: {'day': 'day4', 'file_name': 'session_day3'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day4.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day3.bin')}\n", + "15 Tag: {'day': 'day4', 'file_name': 'session_day4'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day4.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day4.bin')}\n", + "16 Tag: {'day': 'day4', 'file_name': 'session_day5'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day4.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day5.bin')}\n" + ] + } + ], + "source": [ + "dataset1_retagged = MapTags(key_map={\"file_name\": \"day\"})(dataset1)\n", "\n", "for i, (tag, packet) in enumerate(join_op(dataset1_retagged, dataset2)):\n", " print(f\"{i+1:02d} Tag: {tag}, Packet: {packet}\")" @@ -456,9 +599,20 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'day': 'day1'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day1.bin')}\n", + "Tag: {'day': 'day3'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day3.bin')}\n", + "Tag: {'day': 'day4'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day4.bin')}\n", + "Tag: {'day': 'day5'}, Packet: {'bin_data': PosixPath('../examples/dataset2/session_day5.bin')}\n" + ] + } + ], "source": [ "from orcabridge.mapper import Transform\n", "\n", @@ -486,12 +640,22 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'day': 'day1'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day1.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day1.bin')}\n", + "Tag: {'day': 'day3'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day3.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day3.bin')}\n", + "Tag: {'day': 'day4'}, Packet: {'txt_file': PosixPath('../examples/dataset1/day4.txt'), 'bin_data': PosixPath('../examples/dataset2/session_day4.bin')}\n" + ] + } + ], "source": [ "# change filename to day for dataset1\n", - "tag_mapper = MapTags(tag_map={\"file_name\": \"day\"})\n", + "tag_mapper = MapTags(key_map={\"file_name\": \"day\"})\n", "retagged_dataset1 = tag_mapper(dataset1)\n", "\n", "join_op = Join()\n", @@ -551,7 +715,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -573,7 +737,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 19, "metadata": {}, "outputs": [], "source": [ @@ -592,9 +756,24 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "File ../examples/dataset1/day1.txt has 24 lines.\n", + "Tag: {'file_name': 'day1'}, Packet: {}\n", + "File ../examples/dataset1/day2.txt has 15 lines.\n", + "Tag: {'file_name': 'day2'}, Packet: {}\n", + "File ../examples/dataset1/day3.txt has 27 lines.\n", + "Tag: {'file_name': 'day3'}, Packet: {}\n", + "File ../examples/dataset1/day4.txt has 22 lines.\n", + "Tag: {'file_name': 'day4'}, Packet: {}\n" + ] + } + ], "source": [ "# apply the function pod on a stream\n", "processed_stream = function_pod(dataset1)\n", @@ -614,7 +793,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ @@ -648,14 +827,52 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing stats for file: ../examples/dataset2/session_day1.bin\n", + "[-1.08209134 -0.66806394 0.42870206 -0.09321731 -3.14078305 1.33520433\n", + " 1.11085152 1.31931842 -1.19915697 0.07701737 1.30020807 0.27541194\n", + " 0.84430062 0.18236837 -0.83039631 -1.66166191 0.8720775 -1.72170657\n", + " -0.01962253 -0.18050553 1.35478472 0.69928177 0.7314272 -0.06915687\n", + " -0.08364667 -0.45551653 0.70752188 1.02283734 -0.18612795 0.8767394\n", + " -1.542636 1.04685484 -2.1311672 -1.34874222 0.61977577 -0.33880262\n", + " 0.6624482 0.60257325 -3.04901544 -0.20685843 -0.08997232 0.88932232]\n", + "Tag: {'file_name': 'session_day1'}, Packet: {'stats': PosixPath('/tmp/tmph9gbd0fv/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day3.bin\n", + "[ 0.56114059 -1.34902274 1.0665563 0.71890802 0.65244834 1.04369548\n", + " 0.54872876 2.19365207 0.53864286 -1.44108823 -0.55651539 0.1603561\n", + " -0.93869224 0.64645323 -1.08815337 1.40972393 -0.14662931 1.34692375\n", + " 0.38400938 -1.23004316 1.34426647 -0.07620065 -0.91983972 0.23537101\n", + " 0.91515395 0.8064348 0.81470895 -1.04466683 -0.25893558 -1.46253167\n", + " 1.39972807 -0.13940519]\n", + "Tag: {'file_name': 'session_day3'}, Packet: {'stats': PosixPath('/tmp/tmptchkxdfb/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day4.bin\n", + "[ 0.70078854 1.18137906 -0.44361437 -0.389409 0.29719038 0.2523247\n", + " -0.97418716 0.49301127 0.07900351 -0.29965042 -0.25810762 -2.78777445\n", + " -1.24321702 0.13011593 1.07826637 -0.33177479 -0.78337033 -1.30075356\n", + " -0.15710138 0.51927589 0.08671884 0.02058063 0.20778149 -1.40382559\n", + " -0.69978105 -1.10525753 0.1945444 0.82623748 0.17467868]\n", + "Tag: {'file_name': 'session_day4'}, Packet: {'stats': PosixPath('/tmp/tmp76wnct82/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day5.bin\n", + "[ 1.9125739 -0.05252076 0.33347618 0.31627214 0.47141153 -0.71088615\n", + " -0.74745805 0.53959117 -0.14395142 -0.28713782 -0.29422236 -1.00231383\n", + " 0.69566576 -0.25895608 -0.9660761 -0.78504297 -1.91668262 0.89452296\n", + " -0.82748688 -0.19792482 0.07305616 0.36133414 1.7164791 0.64364619\n", + " -0.73146429 0.96324864 -1.05981222 -0.59502066 0.15084192]\n", + "Tag: {'file_name': 'session_day5'}, Packet: {'stats': PosixPath('/tmp/tmpl50i68wn/statistics.json')}\n" + ] + } + ], "source": [ "fp_stats = FunctionPod(compute_stats, output_keys=[\"stats\"])\n", "\n", "# change the key from 'bin_data' to 'bin_file', matching the function's input\n", - "mapped_dataset2 = MapKeys(key_map={\"bin_data\": \"bin_file\"})(dataset2)\n", + "mapped_dataset2 = MapPackets(key_map={\"bin_data\": \"bin_file\"})(dataset2)\n", "\n", "for tag, packet in fp_stats(mapped_dataset2):\n", " print(f\"Tag: {tag}, Packet: {packet}\")" @@ -670,9 +887,47 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing stats for file: ../examples/dataset2/session_day1.bin\n", + "[-1.08209134 -0.66806394 0.42870206 -0.09321731 -3.14078305 1.33520433\n", + " 1.11085152 1.31931842 -1.19915697 0.07701737 1.30020807 0.27541194\n", + " 0.84430062 0.18236837 -0.83039631 -1.66166191 0.8720775 -1.72170657\n", + " -0.01962253 -0.18050553 1.35478472 0.69928177 0.7314272 -0.06915687\n", + " -0.08364667 -0.45551653 0.70752188 1.02283734 -0.18612795 0.8767394\n", + " -1.542636 1.04685484 -2.1311672 -1.34874222 0.61977577 -0.33880262\n", + " 0.6624482 0.60257325 -3.04901544 -0.20685843 -0.08997232 0.88932232]\n", + "Tag: {'file_name': 'session_day1'}, Packet: {'stats': PosixPath('/tmp/tmp5ce8eqt6/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day3.bin\n", + "[ 0.56114059 -1.34902274 1.0665563 0.71890802 0.65244834 1.04369548\n", + " 0.54872876 2.19365207 0.53864286 -1.44108823 -0.55651539 0.1603561\n", + " -0.93869224 0.64645323 -1.08815337 1.40972393 -0.14662931 1.34692375\n", + " 0.38400938 -1.23004316 1.34426647 -0.07620065 -0.91983972 0.23537101\n", + " 0.91515395 0.8064348 0.81470895 -1.04466683 -0.25893558 -1.46253167\n", + " 1.39972807 -0.13940519]\n", + "Tag: {'file_name': 'session_day3'}, Packet: {'stats': PosixPath('/tmp/tmpo07l5i_w/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day4.bin\n", + "[ 0.70078854 1.18137906 -0.44361437 -0.389409 0.29719038 0.2523247\n", + " -0.97418716 0.49301127 0.07900351 -0.29965042 -0.25810762 -2.78777445\n", + " -1.24321702 0.13011593 1.07826637 -0.33177479 -0.78337033 -1.30075356\n", + " -0.15710138 0.51927589 0.08671884 0.02058063 0.20778149 -1.40382559\n", + " -0.69978105 -1.10525753 0.1945444 0.82623748 0.17467868]\n", + "Tag: {'file_name': 'session_day4'}, Packet: {'stats': PosixPath('/tmp/tmpum88t4vy/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day5.bin\n", + "[ 1.9125739 -0.05252076 0.33347618 0.31627214 0.47141153 -0.71088615\n", + " -0.74745805 0.53959117 -0.14395142 -0.28713782 -0.29422236 -1.00231383\n", + " 0.69566576 -0.25895608 -0.9660761 -0.78504297 -1.91668262 0.89452296\n", + " -0.82748688 -0.19792482 0.07305616 0.36133414 1.7164791 0.64364619\n", + " -0.73146429 0.96324864 -1.05981222 -0.59502066 0.15084192]\n", + "Tag: {'file_name': 'session_day5'}, Packet: {'stats': PosixPath('/tmp/tmppy2mzr9l/statistics.json')}\n" + ] + } + ], "source": [ "# everytime you run the following loop, new computations are performed and\n", "# saved in a different set of temporary files\n", @@ -703,16 +958,54 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Computing stats for file: ../examples/dataset2/session_day1.bin\n", + "[-1.08209134 -0.66806394 0.42870206 -0.09321731 -3.14078305 1.33520433\n", + " 1.11085152 1.31931842 -1.19915697 0.07701737 1.30020807 0.27541194\n", + " 0.84430062 0.18236837 -0.83039631 -1.66166191 0.8720775 -1.72170657\n", + " -0.01962253 -0.18050553 1.35478472 0.69928177 0.7314272 -0.06915687\n", + " -0.08364667 -0.45551653 0.70752188 1.02283734 -0.18612795 0.8767394\n", + " -1.542636 1.04685484 -2.1311672 -1.34874222 0.61977577 -0.33880262\n", + " 0.6624482 0.60257325 -3.04901544 -0.20685843 -0.08997232 0.88932232]\n", + "Tag: {'file_name': 'session_day1'}, Packet: {'stats': PosixPath('/tmp/tmpom7k61_b/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day3.bin\n", + "[ 0.56114059 -1.34902274 1.0665563 0.71890802 0.65244834 1.04369548\n", + " 0.54872876 2.19365207 0.53864286 -1.44108823 -0.55651539 0.1603561\n", + " -0.93869224 0.64645323 -1.08815337 1.40972393 -0.14662931 1.34692375\n", + " 0.38400938 -1.23004316 1.34426647 -0.07620065 -0.91983972 0.23537101\n", + " 0.91515395 0.8064348 0.81470895 -1.04466683 -0.25893558 -1.46253167\n", + " 1.39972807 -0.13940519]\n", + "Tag: {'file_name': 'session_day3'}, Packet: {'stats': PosixPath('/tmp/tmp1smo1jfe/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day4.bin\n", + "[ 0.70078854 1.18137906 -0.44361437 -0.389409 0.29719038 0.2523247\n", + " -0.97418716 0.49301127 0.07900351 -0.29965042 -0.25810762 -2.78777445\n", + " -1.24321702 0.13011593 1.07826637 -0.33177479 -0.78337033 -1.30075356\n", + " -0.15710138 0.51927589 0.08671884 0.02058063 0.20778149 -1.40382559\n", + " -0.69978105 -1.10525753 0.1945444 0.82623748 0.17467868]\n", + "Tag: {'file_name': 'session_day4'}, Packet: {'stats': PosixPath('/tmp/tmp__qhe8vk/statistics.json')}\n", + "Computing stats for file: ../examples/dataset2/session_day5.bin\n", + "[ 1.9125739 -0.05252076 0.33347618 0.31627214 0.47141153 -0.71088615\n", + " -0.74745805 0.53959117 -0.14395142 -0.28713782 -0.29422236 -1.00231383\n", + " 0.69566576 -0.25895608 -0.9660761 -0.78504297 -1.91668262 0.89452296\n", + " -0.82748688 -0.19792482 0.07305616 0.36133414 1.7164791 0.64364619\n", + " -0.73146429 0.96324864 -1.05981222 -0.59502066 0.15084192]\n", + "Tag: {'file_name': 'session_day5'}, Packet: {'stats': PosixPath('/tmp/tmpjhm16xjo/statistics.json')}\n" + ] + } + ], "source": [ "from orcabridge.mapper import CacheStream\n", "\n", "# create a cache stream operation\n", "cache_stream = CacheStream()\n", "# change the key from 'bin_data' to 'bin_file', matching the function's input\n", - "mapped_dataset2 = MapKeys(key_map={\"bin_data\": \"bin_file\"})(dataset2)\n", + "mapped_dataset2 = MapPackets(key_map={\"bin_data\": \"bin_file\"})(dataset2)\n", "stats_stream = fp_stats(mapped_dataset2)\n", "\n", "# now cache the stream\n", @@ -732,9 +1025,20 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 25, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'session_day1'}, Packet: {'stats': PosixPath('/tmp/tmpom7k61_b/statistics.json')}\n", + "Tag: {'file_name': 'session_day3'}, Packet: {'stats': PosixPath('/tmp/tmp1smo1jfe/statistics.json')}\n", + "Tag: {'file_name': 'session_day4'}, Packet: {'stats': PosixPath('/tmp/tmp__qhe8vk/statistics.json')}\n", + "Tag: {'file_name': 'session_day5'}, Packet: {'stats': PosixPath('/tmp/tmpjhm16xjo/statistics.json')}\n" + ] + } + ], "source": [ "for tag, packet in cached_stream:\n", " print(f\"Tag: {tag}, Packet: {packet}\")" @@ -760,35 +1064,57 @@ "source": [ "Although the simple `FunctionPod` worked as expected, it's lack of ability to store computation results significantly limits its utility. You certainly wouldn't want to be computing everything from scratch if it can be avoided.\n", "\n", - "This is where storage-backed function pods step in. As the name indicates, these are `FunctionPod` that has a stroage back-end that allows for the computation results to be **memoized**, such that if the same function call with identical inputs occur, the *memoized* result will be returned instead of computing it again.\n", + "The good news is that you can easily equip a function pod with an ability to store and retrieve previously stored packets. All you have to do is create an instance of `DataStore` and pass it in at the construction of the `FunctionPod`.\n", "\n", - "Let's take a look at a specific example of stroage-backed function pod, `FunctionpodWithDirStorage`, which as the name suggests, stores the computation results into a directory you specify. If you omit the directory specification, it will automatically create and store the result in the local directory `./pod_data`" + "Here we are going to configure and use `DirDataStore` where all `packets` and output `packet` contents are stored in a designated directory." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 26, "metadata": {}, "outputs": [], "source": [ - "from orcabridge.pod import FunctionPodWithDirStorage\n", + "from orcabridge.store import DirDataStore\n", "\n", + "data_store = DirDataStore(\"./pod_data\")" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "metadata": {}, + "outputs": [], + "source": [ "# use default storage directory of './pod_data'. You could specify a different directory by passing `store_dir` argument\n", - "fp_stats_stored = FunctionPodWithDirStorage(compute_stats, output_keys=[\"stats\"])" + "fp_stats_stored = FunctionPod(\n", + " compute_stats, output_keys=[\"stats\"], data_store=data_store\n", + ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Once created, stored `FunctionPod` can be used in an identical fashion to a regular `FunctionPod`" + "Now your `FunctionPod` is equipped with an ability to store and retrieve stored packets!" ] }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 28, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'session_day1'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/4f87e2b3-fe44-ea43-6eee-70f63e6d0e95/statistics.json'}\n", + "Tag: {'file_name': 'session_day3'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/eb5c269e-d64d-278a-abc1-afe50716d21a/statistics.json'}\n", + "Tag: {'file_name': 'session_day4'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/ba148557-207e-9a7e-6ee2-353dc861cefa/statistics.json'}\n", + "Tag: {'file_name': 'session_day5'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/8de9dd2c-fa49-bae2-9c60-d4e84cba136e/statistics.json'}\n" + ] + } + ], "source": [ "for tag, packet in fp_stats_stored(mapped_dataset2):\n", " print(f\"Tag: {tag}, Packet: {packet}\")" @@ -803,9 +1129,20 @@ }, { "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'session_day1'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/4f87e2b3-fe44-ea43-6eee-70f63e6d0e95/statistics.json'}\n", + "Tag: {'file_name': 'session_day3'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/eb5c269e-d64d-278a-abc1-afe50716d21a/statistics.json'}\n", + "Tag: {'file_name': 'session_day4'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/ba148557-207e-9a7e-6ee2-353dc861cefa/statistics.json'}\n", + "Tag: {'file_name': 'session_day5'}, Packet: {'stats': 'pod_data/compute_stats/eb526f4394837124/8de9dd2c-fa49-bae2-9c60-d4e84cba136e/statistics.json'}\n" + ] + } + ], "source": [ "for tag, packet in fp_stats_stored(mapped_dataset2):\n", " print(f\"Tag: {tag}, Packet: {packet}\")" diff --git a/notebooks/03_orcabridge_qol_features.ipynb b/notebooks/03_orcabridge_qol_features.ipynb new file mode 100644 index 00000000..338d3766 --- /dev/null +++ b/notebooks/03_orcabridge_qol_features.ipynb @@ -0,0 +1,486 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "58c56103", + "metadata": {}, + "source": [ + "# QoL Improving Features of `orcabridge`" + ] + }, + { + "cell_type": "markdown", + "id": "b16b1b64", + "metadata": {}, + "source": [ + "In the [previous notebook](./02-02-advanced-usage.ipynb), we explored the `orcabridge` package and learned how to build and execute a simple pipeline using concepts like `streams`, `operations` and `pods`." + ] + }, + { + "cell_type": "markdown", + "id": "90a30ec7", + "metadata": {}, + "source": [ + "For an example, we saw that we can define a function pod to wrap a function and to feed in a stream with the packet keys properly mapped into argument names expected by the pod:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f5339f87", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'day1'}, Packet: {'output_data': 'path/to/result/file'}\n", + "Tag: {'file_name': 'day2'}, Packet: {'output_data': 'path/to/result/file'}\n", + "Tag: {'file_name': 'day3'}, Packet: {'output_data': 'path/to/result/file'}\n", + "Tag: {'file_name': 'day4'}, Packet: {'output_data': 'path/to/result/file'}\n" + ] + } + ], + "source": [ + "from orcabridge.source import GlobSource\n", + "from orcabridge.pod import FunctionPod\n", + "from orcabridge.mapper import MapPackets\n", + "\n", + "source = GlobSource(\"text_file\", \"../examples/dataset1\", \"*.txt\")\n", + "\n", + "\n", + "def process_data(data):\n", + " # perform data processing on data\n", + " # return result file path\n", + " return \"path/to/result/file\"\n", + "\n", + "\n", + "fp_process = FunctionPod(process_data, [\"output_data\"])\n", + "\n", + "packet_mapper = MapPackets({\"text_file\": \"data\"}) # map packet key text_file to data\n", + "\n", + "# chain them together into a pipeline\n", + "mapped_stream = packet_mapper(source)\n", + "processed_data_stream = fp_process(mapped_stream)\n", + "\n", + "processed_data_stream.head() # see the first 5 packets" + ] + }, + { + "cell_type": "markdown", + "id": "d9b7f902", + "metadata": {}, + "source": [ + "While separately creating all `mapper` and `pods` and then chaining them helps to rigorously define the data pipeline, admittedly it can get quite verbose and cumbersome.\n", + "\n", + "Fortunately, `orcabrdige` has a number of quality-of-life (QoL) improving features that will help you much more quickly create and combine `operations` and `streams` to define your pipeline without losing the full expressivity. In this notebook, we will explore such QoL improvement features together." + ] + }, + { + "cell_type": "markdown", + "id": "78e8c206", + "metadata": {}, + "source": [ + "## `function_pod` decorator for simple `FunctionPod` creation" + ] + }, + { + "cell_type": "markdown", + "id": "f398fcfd", + "metadata": {}, + "source": [ + "We saw that we can use `FunctionPod` class to wrap an existing function and to associate `output_keys` to rigorously define a `FunctionPod` object that can be used to perform computations on streams of data.\n", + "\n", + "Often, you'd want to define a function intending to only use it as a `FunctionPod`. In that case, you can simplify the `FunctionPod` creation by decorating the function with the `function_pod` decorator:" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "9703d751", + "metadata": {}, + "outputs": [], + "source": [ + "from orcabridge.pod import function_pod\n", + "import json\n", + "import tempfile\n", + "from pathlib import Path\n", + "\n", + "json_source = GlobSource(\"json_file\", \"../examples/dataset2\", \"*.json\")\n", + "\n", + "\n", + "@function_pod([\"output_data\"])\n", + "def extract_name_from_json(json_file):\n", + " with open(json_file, \"r\") as f:\n", + " data = json.load(f)\n", + " output_data = {\"info\": \"\"}\n", + " if \"name\" in data:\n", + " output_data[\"info\"] = data[\"name\"]\n", + " output_path = Path(tempfile.mkdtemp()) / \"output.json\"\n", + " with open(output_path, \"w\") as f:\n", + " json.dump(output_data, f)\n", + " return output_path" + ] + }, + { + "cell_type": "markdown", + "id": "f3ffe3d6", + "metadata": {}, + "source": [ + "With the above code, the decorator takes the decorated function and creates a FunctionPod with the specified output arguments (\"output_data\" in this case).\n", + "\n", + "The name `extract_name_from_json` now holds the resulting `FunctionPod` that can be immeidately applied to a stream." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "1e2b5d86", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'info_day1'}, Packet: {'output_data': PosixPath('/tmp/tmpn0nn30b3/output.json')}\n", + "Tag: {'file_name': 'info_day2'}, Packet: {'output_data': PosixPath('/tmp/tmpqg13bjib/output.json')}\n", + "Tag: {'file_name': 'info_day3'}, Packet: {'output_data': PosixPath('/tmp/tmp1f_08m5t/output.json')}\n", + "Tag: {'file_name': 'info_day4'}, Packet: {'output_data': PosixPath('/tmp/tmpq3x8a298/output.json')}\n", + "Tag: {'file_name': 'info_day5'}, Packet: {'output_data': PosixPath('/tmp/tmp_cma7686/output.json')}\n" + ] + } + ], + "source": [ + "extract_name_from_json(json_source).head() # preview the first 5 packets" + ] + }, + { + "cell_type": "markdown", + "id": "1ca2aa60", + "metadata": {}, + "source": [ + "If you need to access the original function, it can be retrieved by accessing the `function` attribute" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "c5dc8a46", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{'info': 'Day 2 experiment'}\n" + ] + } + ], + "source": [ + "output_path = extract_name_from_json.function(\"../examples/dataset2/info_day2.json\")\n", + "\n", + "with open(output_path, \"r\") as f:\n", + " data = json.load(f)\n", + " print(data) # {'info': 'John Doe'}" + ] + }, + { + "cell_type": "markdown", + "id": "bea0880a", + "metadata": {}, + "source": [ + "## Mapping tags and packets with `>>` operator" + ] + }, + { + "cell_type": "markdown", + "id": "7a1e38e4", + "metadata": {}, + "source": [ + "As you chain multiple pods together forming a complex pipeline, you are bound to make frequent use of `MapPackets` to *rename* the output argument from one pod into another argunemt name for the next pod. We have already seen how this can be achieved by creating a specific instance of `MapPackets`, initializing the object with a dictionary defining the name mapping." + ] + }, + { + "cell_type": "markdown", + "id": "af660c4f", + "metadata": {}, + "source": [ + "Consider the following data source and function pod:" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "c30e1e4a", + "metadata": {}, + "outputs": [], + "source": [ + "json_files = GlobSource(\"json_file\", \"../examples/dataset2\", \"*.json\")\n", + "\n", + "\n", + "@function_pod([\"line_count\"])\n", + "def count_lines(text_file):\n", + " with open(text_file, \"r\") as f:\n", + " lines = f.readlines()\n", + " line_count = len(lines)\n", + " output_path = Path(tempfile.mkdtemp()) / \"line_count.json\"\n", + " with open(output_path, \"w\") as f:\n", + " json.dump({\"line_count\": line_count}, f)\n", + " return output_path" + ] + }, + { + "cell_type": "markdown", + "id": "6788ff05", + "metadata": {}, + "source": [ + "If I want to apply the function pod to count and save the number of lines present in the JSON files from the data source, I will have to create a `MapPackets` that renames the output argument `\"json_file\"` itno `\"text_file\"` expected by the `count_lines` function." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2d171b0e", + "metadata": {}, + "outputs": [], + "source": [ + "json_to_text = MapPackets(\n", + " {\"json_file\": \"text_file\"}\n", + ") # map packet key json_file to text_file" + ] + }, + { + "cell_type": "markdown", + "id": "c7994b9b", + "metadata": {}, + "source": [ + "Finally we can chain them together into a functional pipeline:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "d48fc20c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'info_day1'}, Packet: {'line_count': PosixPath('/tmp/tmpbrelyuro/line_count.json')}\n", + "Tag: {'file_name': 'info_day2'}, Packet: {'line_count': PosixPath('/tmp/tmp1mgotcqw/line_count.json')}\n", + "Tag: {'file_name': 'info_day3'}, Packet: {'line_count': PosixPath('/tmp/tmp8mjlafrx/line_count.json')}\n", + "Tag: {'file_name': 'info_day4'}, Packet: {'line_count': PosixPath('/tmp/tmpvs_r2obl/line_count.json')}\n", + "Tag: {'file_name': 'info_day5'}, Packet: {'line_count': PosixPath('/tmp/tmpha6qrjs2/line_count.json')}\n" + ] + } + ], + "source": [ + "line_info = count_lines(json_to_text(json_files))\n", + "\n", + "line_info.head() # preview the first 5 packets" + ] + }, + { + "cell_type": "markdown", + "id": "09d9da59", + "metadata": {}, + "source": [ + "This is all fine until you start having many more `Pods` and `streams` in your pipeline that needs to be connected together. Many of these connection would need the `MapPackets` `mapper` to be inserted for the function to work properly -- that could be a lot of `MapPackets` you have to create!" + ] + }, + { + "cell_type": "markdown", + "id": "063cc9e7", + "metadata": {}, + "source": [ + "Because `MapPackets` is such a common operation, `orcabridge` provides a very simple shortcut for creating a *mapped stream* from another stream using right shift (`>>`) operator." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "10437185", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'info_day1'}, Packet: {'text_file': PosixPath('../examples/dataset2/info_day1.json')}\n", + "Tag: {'file_name': 'info_day2'}, Packet: {'text_file': PosixPath('../examples/dataset2/info_day2.json')}\n", + "Tag: {'file_name': 'info_day3'}, Packet: {'text_file': PosixPath('../examples/dataset2/info_day3.json')}\n", + "Tag: {'file_name': 'info_day4'}, Packet: {'text_file': PosixPath('../examples/dataset2/info_day4.json')}\n", + "Tag: {'file_name': 'info_day5'}, Packet: {'text_file': PosixPath('../examples/dataset2/info_day5.json')}\n", + "Tag: {'file_name': 'info_day1'}, Packet: {'line_count': PosixPath('/tmp/tmp_o8blef4/line_count.json')}\n", + "Tag: {'file_name': 'info_day2'}, Packet: {'line_count': PosixPath('/tmp/tmp7s3mk_9p/line_count.json')}\n", + "Tag: {'file_name': 'info_day3'}, Packet: {'line_count': PosixPath('/tmp/tmpdv672rbb/line_count.json')}\n", + "Tag: {'file_name': 'info_day4'}, Packet: {'line_count': PosixPath('/tmp/tmpbvxkwo29/line_count.json')}\n", + "Tag: {'file_name': 'info_day5'}, Packet: {'line_count': PosixPath('/tmp/tmp26qs4pk8/line_count.json')}\n" + ] + } + ], + "source": [ + "mapped_stream = json_files >> {\"json_file\": \"text_file\"}\n", + "\n", + "mapped_stream.head()\n", + "\n", + "count_lines(mapped_stream).head()" + ] + }, + { + "cell_type": "markdown", + "id": "3cd02672", + "metadata": {}, + "source": [ + "That's it! Hopefully you'd agree that this is far more convenient than having to define your own `MapPackets` mapper! Using the `>>` operator, the whole pipeline would have looked like:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "9be9f86d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'info_day1'}, Packet: {'line_count': PosixPath('/tmp/tmpcscf2auv/line_count.json')}\n", + "Tag: {'file_name': 'info_day2'}, Packet: {'line_count': PosixPath('/tmp/tmpwtsrkvg4/line_count.json')}\n", + "Tag: {'file_name': 'info_day3'}, Packet: {'line_count': PosixPath('/tmp/tmpw4cj_kso/line_count.json')}\n", + "Tag: {'file_name': 'info_day4'}, Packet: {'line_count': PosixPath('/tmp/tmpyo6pc_fw/line_count.json')}\n", + "Tag: {'file_name': 'info_day5'}, Packet: {'line_count': PosixPath('/tmp/tmp3up8exm6/line_count.json')}\n" + ] + } + ], + "source": [ + "# preview the first 5 packets\n", + "count_lines(json_files >> {\"json_file\": \"text_file\"}).head()" + ] + }, + { + "cell_type": "markdown", + "id": "24a4d11e", + "metadata": {}, + "source": [ + "Not only is this simpler to type, we believe it actually makes the pipeline creation more intuitive and expressive of your intention!" + ] + }, + { + "cell_type": "markdown", + "id": "18ec64f3", + "metadata": {}, + "source": [ + "### Mapping tags and advanced mapping" + ] + }, + { + "cell_type": "markdown", + "id": "05e8ff25", + "metadata": {}, + "source": [ + "We just saw how the rightshift operator can be used to simplify the `MapPackets` operation creation. How about `MapTags`? We can get `MapTags` equivalent operation also by using the rightshift (`>>`) operator, but with the help of an additional function: `tag()`." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b0164e6e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'experiment_day': 'info_day1'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day1.json')}\n", + "Tag: {'experiment_day': 'info_day2'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day2.json')}\n", + "Tag: {'experiment_day': 'info_day3'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day3.json')}\n", + "Tag: {'experiment_day': 'info_day4'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day4.json')}\n", + "Tag: {'experiment_day': 'info_day5'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day5.json')}\n" + ] + } + ], + "source": [ + "from orcabridge.mapper import tag, packet\n", + "\n", + "(json_files >> tag({\"file_name\": \"experiment_day\"})).head()" + ] + }, + { + "cell_type": "markdown", + "id": "ac34eed4", + "metadata": {}, + "source": [ + "Now if you were to closely inspect `MapPackets` and `MapPackets`, you would know that it is capable of taking in some additional arguments such as `drop_unmapped`. Using `tag()` and `packet()` helper functions would let you specify those arguments as well while using the `>>` operator." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b366b19c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'info_day1'}, Packet: {}\n", + "Tag: {'file_name': 'info_day2'}, Packet: {}\n", + "Tag: {'file_name': 'info_day3'}, Packet: {}\n", + "Tag: {'file_name': 'info_day4'}, Packet: {}\n", + "Tag: {'file_name': 'info_day5'}, Packet: {}\n" + ] + } + ], + "source": [ + "# no packet key matches `data_file`: by default, this will lead to an empty packet\n", + "(json_files >> {\"data_file\": \"file_path\"}).head()" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "b3920086", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'info_day1'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day1.json')}\n", + "Tag: {'file_name': 'info_day2'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day2.json')}\n", + "Tag: {'file_name': 'info_day3'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day3.json')}\n", + "Tag: {'file_name': 'info_day4'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day4.json')}\n", + "Tag: {'file_name': 'info_day5'}, Packet: {'json_file': PosixPath('../examples/dataset2/info_day5.json')}\n" + ] + } + ], + "source": [ + "# you can preseve unmapped packet key by using `packet` function with `drop_unmapped=False`\n", + "(json_files >> packet({\"data_file\": \"file_path\"}, drop_unmapped=False)).head()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/04_orcabridge_tracker.ipynb b/notebooks/04_orcabridge_tracker.ipynb new file mode 100644 index 00000000..ff16fb1a --- /dev/null +++ b/notebooks/04_orcabridge_tracker.ipynb @@ -0,0 +1,442 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "d698bea9", + "metadata": {}, + "source": [ + "## Using Tracker" + ] + }, + { + "cell_type": "markdown", + "id": "5be82e8a", + "metadata": {}, + "source": [ + "In this notebook, I'll demonstrate using `Tracker` and related features of `orcabridge` to keep *track* of all `operations` you are using in a pipeline and to automatically construct computation graph based on it." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "f28a62af", + "metadata": {}, + "outputs": [], + "source": [ + "from orcabridge.tracker import Tracker\n", + "from orcabridge.source import GlobSource\n", + "from orcabridge.store import DirDataStore\n", + "from orcabridge.pod import function_pod\n", + "from orcabridge.mapper import tag, packet\n", + "import orcabridge.mapper as router\n", + "import networkx as nx\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "110970a8", + "metadata": {}, + "source": [ + "As before let's construct a simple data pipeline that reads files and counts the number of lines, saving that information into another file. We will then chain it with another function that converts JSON to YAML." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "56136d16", + "metadata": {}, + "outputs": [], + "source": [ + "data_source = GlobSource(\"data_file\", \"../examples/dataset1\", \"*.txt\")" + ] + }, + { + "cell_type": "markdown", + "id": "8137ce66", + "metadata": {}, + "source": [ + "We are also going to define a few functions to serve as function pods. We are using convenience function `function_pod` to decorate the function to immediately turn them into a `FunctionPod` as was covered [in previous notebook](./03_orcabridge_qol_features.ipynb)." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c32938d6", + "metadata": {}, + "outputs": [], + "source": [ + "import tempfile\n", + "from pathlib import Path\n", + "import json\n", + "import yaml\n", + "\n", + "# use default data store location of `./pod_data`\n", + "data_store = DirDataStore()\n", + "\n", + "\n", + "# function to count lines in a file and save it as JSON\n", + "@function_pod([\"stats\"], data_store=data_store)\n", + "def count_lines(file: str):\n", + " with open(file, \"r\") as f:\n", + " n = len(f.readlines())\n", + " tmp_dir = tempfile.mkdtemp()\n", + " data_file = Path(tmp_dir) / \"data.json\"\n", + " data = dict(lines=n)\n", + " with open(data_file, \"w\") as f:\n", + " json.dump(data, f)\n", + " return data_file\n", + "\n", + "\n", + "@function_pod([\"yaml_file\"], data_store=data_store)\n", + "def json_to_yaml(json_file: str):\n", + " with open(json_file, \"r\") as f:\n", + " data = json.load(f)\n", + " tmp_dir = tempfile.mkdtemp()\n", + " yaml_path = Path(tmp_dir) / Path(json_file).with_suffix(\".yaml\").name\n", + " with open(yaml_path, \"w\") as f:\n", + " yaml.dump(data, f)\n", + " return yaml_path\n", + "\n", + "\n", + "# function to extract keys from a JSON file and save them as a list in another JSON file\n", + "@function_pod([\"key_info\"], data_store=data_store)\n", + "def extract_keys(json_file: str):\n", + " with open(json_file, \"r\") as f:\n", + " data = json.load(f)\n", + " keys = list(data.keys())\n", + " tmp_dir = tempfile.mkdtemp()\n", + " keys_file = Path(tmp_dir) / \"keys.json\"\n", + " with open(keys_file, \"w\") as f:\n", + " json.dump(keys, f)\n", + " return keys_file" + ] + }, + { + "cell_type": "markdown", + "id": "787b26ca", + "metadata": {}, + "source": [ + "With all steps defined, let's chain them together to form a full pipeline. Note that we are using the `>>` operator to map packet keys on the fly to match expected arguments for each pod." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "3cdc6e7f", + "metadata": {}, + "outputs": [], + "source": [ + "line_info_json = count_lines(data_source >> {\"data_file\": \"file\"})\n", + "line_info_yaml = json_to_yaml(line_info_json >> {\"stats\": \"json_file\"})\n", + "line_info_keys = extract_keys(line_info_json >> {\"stats\": \"json_file\"})" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "71d18d81", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'day1'}, Packet: {'key_info': 'pod_data/extract_keys/cf978f9c23318c91/c4c3939c-aced-18ac-8a7c-330f07780bbd/keys.json'}\n", + "Tag: {'file_name': 'day2'}, Packet: {'key_info': 'pod_data/extract_keys/cf978f9c23318c91/61e78af0-346a-00f7-df3d-f2ec1693a84e/keys.json'}\n", + "Tag: {'file_name': 'day3'}, Packet: {'key_info': 'pod_data/extract_keys/cf978f9c23318c91/92fbcba4-a642-4105-8be2-c01ce9c3e12e/keys.json'}\n", + "Tag: {'file_name': 'day4'}, Packet: {'key_info': 'pod_data/extract_keys/cf978f9c23318c91/9e7da977-1ed3-03a2-ffd5-a3d626c286d8/keys.json'}\n" + ] + } + ], + "source": [ + "line_info_keys.head()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2950e1d5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Tag: {'file_name': 'day1'}, Packet: {'yaml_file': 'pod_data/json_to_yaml/0a2282eda2b641e6/c4c3939c-aced-18ac-8a7c-330f07780bbd/data.yaml'}\n", + "Tag: {'file_name': 'day2'}, Packet: {'yaml_file': 'pod_data/json_to_yaml/0a2282eda2b641e6/61e78af0-346a-00f7-df3d-f2ec1693a84e/data.yaml'}\n", + "Tag: {'file_name': 'day3'}, Packet: {'yaml_file': 'pod_data/json_to_yaml/0a2282eda2b641e6/92fbcba4-a642-4105-8be2-c01ce9c3e12e/data.yaml'}\n", + "Tag: {'file_name': 'day4'}, Packet: {'yaml_file': 'pod_data/json_to_yaml/0a2282eda2b641e6/9e7da977-1ed3-03a2-ffd5-a3d626c286d8/data.yaml'}\n" + ] + } + ], + "source": [ + "line_info_yaml.head()" + ] + }, + { + "cell_type": "markdown", + "id": "ff85672b", + "metadata": {}, + "source": [ + "We have now defined a nontrivial data pipeline that counts the number of lines in files and output that info to JSON. We then convert the JSON files into YAML files. On the other branch of the pipeline, we take the JSON files and extract a list of keys found in the JSON and save it as yet another JSON file." + ] + }, + { + "cell_type": "markdown", + "id": "1abdf7d6", + "metadata": {}, + "source": [ + "As your pipeline grows, it can get qutie tricky to keep track of exactly who connects to what and it can be easy to lose sight of exactly what's going on in the pipeline. This is exactly where `Tracker` comes in handy. As the name suggeests, a `Tracker` can keep track of all pipeline-defining computations you have run, and then generate helpful information such as graphs or plot the graph as an image for visualization." + ] + }, + { + "cell_type": "markdown", + "id": "27346fa5", + "metadata": {}, + "source": [ + "You may think that the sensible next step is to create and setup a `Tracker` to keep track of the pipeline. It turns out that `orcabridge` comes with a default tracker that has been tracking all of the pipeline-defining operation all this time, secretly without you knowing!\n", + "\n", + "Let's access and use the default tracker." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "2cdf8d30", + "metadata": {}, + "outputs": [], + "source": [ + "from orcabridge import DEFAULT_TRACKER" + ] + }, + { + "cell_type": "markdown", + "id": "19a999a7", + "metadata": {}, + "source": [ + "As we will see shortly, `DEFAULT_TRACKER` is just an ordinary instance of `Tracker` that has been instantiated and activated at the time of the first import of `orcabridge`. Consequently, it has been keeping track of all `operation` execution that you have performed thus far. Let's use its `draw_graph()` to display the computation graph it has detected." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "7a91ffaa", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/eywalker/workspace/orcabridge/src/orcabridge/tracker.py:92: UserWarning: This figure includes Axes that are not compatible with tight_layout, so results might be incorrect.\n", + " plt.tight_layout()\n" + ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "DEFAULT_TRACKER.draw_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "3b8fd7d0", + "metadata": {}, + "source": [ + "Does that match what you thought of the pipeline thus far? You might notice that while we used the convenience operator `>>` to map packet keys, the corresponding `MapPackets` mapper actually shows up in the graph. Remember that `>>` is just for convenience, making the creation of `MapPackets` and `MapTags` more accesible." + ] + }, + { + "cell_type": "markdown", + "id": "3c02d58c", + "metadata": {}, + "source": [ + "## Explicitly tracking computation pipeline with a tracker" + ] + }, + { + "cell_type": "markdown", + "id": "db480859", + "metadata": {}, + "source": [ + "While the `DEFAULT_TRACKER` can serve as a nice catch-all tracker, sometimes you'd want to explicitly control what gets tracked so that you can visualize specific section of the entire computation graph. \n", + "\n", + "To do so, you would want to create your own tracker, and define the computation using the tracker's context manager. Let's see an example." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "f678050e", + "metadata": {}, + "outputs": [], + "source": [ + "# instantiate a new tracker\n", + "tracker = Tracker()\n", + "\n", + "# start a context manager to track the pipeline\n", + "with tracker:\n", + " line_info_json = count_lines(data_source >> {\"data_file\": \"file\"})\n", + " line_info_keys = extract_keys(line_info_json >> {\"stats\": \"json_file\"})\n", + "\n", + "# perform the conversion outside of the tracker\n", + "line_info_yaml = json_to_yaml(line_info_json >> {\"stats\": \"json_file\"})" + ] + }, + { + "cell_type": "markdown", + "id": "f7d3484f", + "metadata": {}, + "source": [ + "In the example above, you would notice that only first half of the pipeline is chained together within the `tracker` context manager. Let's now see what the graph looks like." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "b3bf152b", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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RETUIAyURERERNQgDJRERERE1CAMlERERETUIAyURERERNQgDJRERERE1CAMlERERETUIAyURERERNQgDJRERERE1CAMlERERETUIAyURERERNQgDJRERERE1CAMlERERETUIAyURERERNQgDJRERERE1CAMlERERETUIAyURERERNQgDJRERERE1CAMlERERETUIAyURERERNQgWmcXQETUVPz+++8oKCiotOzMmTMAgPXr11da3qJFC3Tu3Pmy1UZE1JQpIiLOLoKIyNkyMzMRExNTr2327t2L6OjoS1QREZHrYJc3ERGAqKgoJCQkQFGUC66r0WiQkJCAqKioy1AZEVHTx0BJRARAURQ8//zzqEunjc1mw6xZs+oUPomImgN2eRMR/UlE0KlTJ6Snp8Nms1W7jkajQVxcHHbs2MFASUT0J7ZQEhH9ydFKWVOYBNg6SURUHbZQEhFVUFsrJVsniYiqxxZKIqIKamulZOskEVH12EJJRHSe6lop2TpJRFQztlASEZ2nulZKtk4SEdWMLZRERNVwtFLu2rULAJCQkMDWSSKiGrCFkoioGo5WSge2ThIR1YwtlERENRARBAQEAAByc3MZKImIasBASUR0HpsISixWlNsEx7KzIYqCkKAgaBQFWo0Cb50HNAyXREQqBkoiatZsIigoLUd+qQX55yzINVtQUGpBzbc2t48VaqHXIcCgg8lLB5NehxZ6LUMmETVbDJRE1CzlmstwIL8ERwvNsP35KagAqM8HYsX1NQoQajQg0t8b/l6ejVssEVETx0BJRM2G1SY4UmjG/rxinC0tr3eAvBDH/vz0WkT5+yDUaICHhq2WROT+GCiJyO1ZbYKMM0XYn1+Mctvl+8jTahRE+vsgNsCXwZKI3BoDJRG5tTPmMmzOyUexxeq0Gnx0HkgKNiHAwK5wInJPDJRE5JasNkH66UJk5hU3etd2fTmOH+3vg7iWRrZWEpHbYaAkIreTay7DJie3StaErZVE5I4YKInIrRwrNCMtOx+Ac1sla+Jom+wRYkJbo8GptRARNRYGSiJyG1n5Jdh64qyzy6izxCA/hPt5O7sMIqIG43d5E5FbcLUwCQBbj59F1tkSZ5dBRNRgDJRE5PKOFZpdLkw6bD1+FscKzc4ug4ioQRgoicil5ZrL1DGTriotOx+55jJnl0FEdNEYKInIZVltgk05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X71IRERERA3CQElEREREDcJASUREREQNwkBJRERERA3CQElEREREDcJASUREREQNwkBJRERERA3CQElEREREDcJASUREREQNwkBJRERERA3CQElEREREDcJASUREREQNwkBJRERERA3CQElEREREDcJASUREREQNwkBJRERERA3CQElEREREDcJASUREREQNwkBJRERERA3CQElEREREDaJ1dgFERE1FSUkJysrKKi2zWCwAgPz8/ErLPT094e3tfblKIyJq0hQREWcXQUTkbFlZWYiJiVED5IXodDrs3bsX4eHhl7YwIiIXwC5vIiIAISEhaNWqVZ3Xb926NUJCQi5hRUREroOBkogI9i7s6dOn13n96dOnw9PT8xJWRETkOtjlTUT0p7KyMrRv3x45OTmo6aNRURSEhITgwIEDDJRERH9iCyUR0Z8crZS1/Z0tImydJCI6D1soiYgqqK2Vkq2TRETVYwslEVEFtbVSsnWSiKh6bKEkIjpPda2UbJ0kIqoZWyiJiM5TXSslWyeJiGrGFkoiomo4Wimzs7MBAG3btmXrJBFRDdhCSURUjfPvS8nWSSKimrGFkoioBmVlZfDz8wMAnD17loGSiKgGWmcXQETU1NhEUGKxotymYPH3P0AUBUVWQHPOAq1GgbfOAxpFcXaZRERNBlsoiahZs4mgoLQc+aUW5J+zINdsQUGpBbZattEAaKHXIcCgg8lLB5NehxZ6LUMmETVbDJRE1CzlmstwIL8ERwvNsP35KagAqM8HYsX1NQoQajQg0t8b/l7sGiei5oWBkoiaDatNcKTQjP15xThbWl7vAHkhjv356bWI8vdBqNEADw1bLYnI/TFQEpHbs9oEGWeKsD+/GOW2y/eRp9UoiPT3QWyAL4MlEbk1BkoicmtnzGXYnJOPYovVaTX46DyQFGxCgIFd4UTknhgoicgtWW2C9NOFyMwrbvSu7fpyHD/a3wdxLY1srSQit8NASURuJ9dchk1ObpWsCVsricgdMVASkVs5VmhGWnY+AOe2StbE0TbZI8SEtkaDU2shImosDJRE5Day8kuw9cRZZ5dRZ4lBfgj383Z2GUREDcbv8iYit+BqYRIAth4/i6yzJc4ug4iowRgoicjlHSs0u1yYdNh6/CyOFZqdXQYRUYMwUBKRS8s1l6ljJl1VWnY+cs1lzi6DiOiiMVASkcuy2gSbcvKdXUaj2JSTD+tlvOk6EVFjYqAkIpeVfroQxRZrk5zNXR8CoNhiRfrpQmeXQkR0URgoicglnTGXITOv2NllNKrMvGJ2fRORS2KgJCKXY7UJNufkw92+b0YBu76JyDUxUBKRy8nILXKLru7zObq+M3KLnF0KEVG9MFASkUux2gT73ayr+3z784rZSklELoWBkohcytFCM8rdPGyV2wRHeW9KInIhDJRE5FL2uXnrpENzOU8icg8MlETkMnLNZThbWu7sMi6Ls6XlnPFNRC6DgZKIXMaB/BK3m9ldEwX28yUicgUMlETkEmxiH1fo3qMn/yKwjxe1SXM5YyJyZQyUROQSCkrL4eZzcaqwCVDYTLr4ici1MVASkUvIL7U4uwSnyGum501EroWBktzOjBkzoCgKwsPDnV3KRVu4cCEURYGiKMjKynJ2OU1C/jlLlfGT0+4ehmGxIXh98kRnlFStHz//yF7T3yeoyx66tgeGxYbg89fn1GtfCuznfamtXLkSnTp1gpeXFxRFwYwZM6p9H4WHh6s/r4/p06dDURT84x//aNzCa+Co0/H4+uuvq11v7dq1TeZ95qhj4cKF9d62utfvYo0ZM6bSc9eQfVHzwkBJdZKSklLpQ6YuH9aXWk2/3EJDQ5GcnIyuXbtekuNW/CXkeBiNRsTHx2PWrFkoLnbN271UDLGXW1ZWlnrstWvXVrtOrtnS5MdPllssWPLWXADA4LEPqMvbxyUgunMiAoOC67U/gf28LyWbzYY77rgDO3fuhNFoRHJyMkJDQxv1ffToo49Cr9dj3rx5OH36dCNUXTcRERFITk5GQEAAgL+u8ZquMWdKTk5GcnIyWrVqVa/tanr96soRIB0iIyORnJwMo9FYrzqItM4ugFyLp6dnlV8wjg/rpuK+++7Dfffdd1mOFRERgVatWuHw4cNIT0/H1KlTkZaWhm+//fayHN+ZysrK4OnpeVmOZRPBWRfo+t285ieczsnGFdGxCI+NV5f//T8LLnqfBaUW2ESguURBPzs7G/n5+QCAjz76CAMHDlR/1ljvo1atWmHAgAFYtmwZPvzwQzzxxBONst8LmTp1KsaMGXNZjtVQGzduvKjtanv9LsbUqVMxdepUpKSkYN26dQ3aFzUvbKGkegkODsbGjRsrPfr06VNjC9P5rYgVW/e++eYb9OnTBwaDAbGxsVi2bFmlY2VmZuLOO+9EUFAQPD09ERoaikmTJqnHOnToEADgueeeq9SyVl1XndVqxSuvvIK4uDjo9Xr4+flhwIABWL9+vbpOfWpzmDp1KjZu3IgjR44gOTkZAPDdd98hLy8PAJCbm4vx48ejXbt20Ol0aNOmDUaNGoXDhw9X2s/rr7+Otm3bwsfHB3fddRfOnj1b5VgV66v4HJ89exYTJkxAWFiY+jw9+eSTKCmx33Lm8OHDMJlMUBQFzz33HADg2LFj6rJp06ZhzJgxGDt2rLrP87u7HP9/+eWXceutt8LX1xcPPGBvgRs9ejSio6NhNBrh6emJsLAwPP744ygoKKhU/08//YT+/fvDz88PXl5eiI2NxUcffYSFCxeiffv26nr9+vWDoihISUlRl5XU8Xu7s7MO4N7eXTAsNgT/fOgeWMpKYSkrxWfzZmP8wF64/cowjO15Jd74xxMoyDsDANjx23oMiw3BsNgQZGcdUPe1/MP3MCw2BHcnxaKs9BzyTp3E3EnjcW/vLrj9ynDce01nTB89HFvW/U/d5pfvvwYAdO83oFJd53d5W61WfPTKP/Fw/6twR6f2GJ0ch78NuwFfv/emuk3pOTM+fu1FPHx9T3jp9QgICMAtt9yCnTt3qutUbFVes2YNEhMTYTAYkJiYWKeAsnDhQrRr1079/w033KB2u9Z16Eh2djbGjRuHkJAQeHp6IiIiAs8//zzKyytPJrr55psBAJ9++ukF66rJO++8g8LCwovevi6eeuopKIoCb29vrFq1CgCQmpqKQYMGwWQywcvLC4mJiVi8eLG6Te/evaEoCkaNGqUus1qtaN26NRRFwYsvvnjB457f5V2X17a21w8AfvnlFwwcOBB+fn7Q6/Xo2LEjZs+eDavV2tCniagqIaqDvn37CgAJCwur9ucHDx4U2HvoZM2aNerysLAwASDTp08XEZE1a9ao6+l0OomOjhaDwSAAxGg0ypkzZ0REJDMzU0wmkwAQDw8P6dixowQFBUnnzp0lOztbkpOTxdPTUwBI27ZtJTk5WZKTk0VEZPr06VVqvffee9XjRkVFSUBAgAAQrVYra9eurVdtFdd7//33RUSkvLxckpOT1eW5ubliNpslISFBPU5cXJx4eXkJAAkJCZGTJ0+KiMi3336rbteqVStp166d+Pj4qMsOHjxY5biO57i0tFS6dOkiAMTLy0s6deqkHuPaa68Vm80mIiIfffSRABBPT0/ZtWuX3HTTTQJAevToIRaLRWbOnCkRERHq/h3P5zvvvCMioi739PSUFi1aSEJCgtx///0iIuLn5yeBgYHSuXPnSvu47bbb1Of/iy++EEVRBIAYDAZJSEiQFi1ayIQJE2TZsmXqOQCQjh07SnJysjz88MPq9nnmMlmSkV3lEZ90tQCQlFtGyPz/pUnL4BABIEnXXi+f7ciSJRnZktj3OgEgGg8PCesQJ96+RgEgoVEx8sn2/bL4j2MSEm6v+9YHHquy7wEjRsmSjGxJHnCj/Xn29pGIuCulZXCIKIoiI8Y/qW5jatVaAMjfXn+vUp2tQkIFgLruuH/MrFRTUFh70eo8JT7panWbTj17CwBRFEViOnQQX19fASC+vr7yxx9/iIjI+++/rz5ver1eOnToIFqtVr3+LRZLre/rmp77ZcuWVfs+Ov/9fPr0aWnXrp36HunUqZN6/LFjx1Y61tatW9X3QlFRUY01HTt2TA4ePFjlsWHDBtHr9XL11VdLQUFBreflqNPx/nRwPF+O90/F99TBgwdl6tSpAkC8vb3lf//7n4iI/PLLL6LT6QSABAUFSYcOHdRtFi1aJCIiH3/8sXpt5+fnV9q3RqORI0eO1FqvyF/vMUfNdXlta3v91qxZo67v7+8v0dHR6nr33XefetzRo0dLdVHA8ZnveK2JLoSBkurE8eFS3UPk4gLlk08+KSIi33zzjbpsxYoVIiIyduxYNdj9+uuv6v62bt1a474dzv9FuG/fPjXMTJgwQURE8vPz1e379OlTr9oqrhcRESHJyckSHBysLhs8eLCIiCxYsEBdtnTpUhER2bJli2g0GgEg06ZNExGRa665RgBIZGSkFBYWSnl5uaSkpFQJlKmpqdKhQwfp0KGDpKamiojIwoUL1aC3d+9eERHZvn27uu2qVavU5+X2228XANKmTRsBID4+Puo2IpV/gZ3PsTw2NlZyc3NFxB6iHcer6JlnnlGDg9lsFhGR9u3bq+eYk5MjIvYwvGvXLhGp+fpxOFNSWmug7HJNigRdEW4Pk9cNlM93HpIlGdky84Ml6n5nfviVLMnIlnd/3iaef4buh2fNkSUZ2TJm8gwBIAFtguWL3Udkwa871NfphU++liUZ2XJFdKz9Gpr9H/X47/68Tf69fJ0syciWj7Zkqsea/dXKWgPljXfZr+/+w+9U1/loS6a89OVyWZKRLTMWfqnua+yU5+RMSakcOXJEDZX33HNPldds3rx5IiLy73//W13mCJ61qem5r0ugnDFjhnpNOf5A+vrrr9UgnJmZ+ddreOaMehzH616dxMRE0ev1NT40Gs0FQ2VNgfJ8Fd/L48ePV8Pk6tWr1XUc78UBAwaoAX3ixIn2P0pCQ0XEfi23atVKAMj8+fNFROTRRx+1v8b9+9dag0NtgbK217am169Pnz7q65eXlyciIhMmTFBfm/3799daDwMl1Re7vKlePD091cHjjsfFuvvuuwEAcXFx6rITJ04AsHcxAUDfvn3Rs2dP9ecXM0Fgy5YtkD9vDn3nnXcCAPz8/DBo0CAAwObNm+tVW0UHDhxAamoqCgoKEBcXh5kzZ6pdeps2bQIAeHt745ZbbgEAJCYmokOHDpWOu3v3bgDAwIED4evrCw8PD9x6661VjtWjRw9kZGQgIyMDPXr0AACkpaUBsI9njImJgaIo6NKli7pNxW7P+fPnIyQkRD2POXPmIDo6uoZnrXqjR4+Gv78/AMDDwwMAsGrVKiQkJMBgMEBRFLzwwgsAgPLycpw6dQqnTp3CwYMHAQBjx45FUFAQAPu1FB8fX81RqrrQzb23/7IWxw9nIbpTV0ya+za0Oh0AIHPnNnWdaXffimGxIbivT1eUnTtn//nvWwEA/YaOgKeXF3JP5GD7L2uRuuoH2Gw2BIW1R2yi/bl2dGO/PnkCxl/fE/988B6s+24JAlrbz6ek6K8ufoOPb631dksZAEVRsOrLT3B/n0RMu+c2LH5rLnz9TACA/bu2q+v2vnkobCIIDQ1F7969ATTsmm1MjuvvxIkTaveu41oXEfV9DAAtWrRQ/13dkA6HLVu24Ny5czU+Xn/9dfz222/48ccfG/Vc3njjDQD2Lvl+/fqpyx3n+NNPP0Gn00FRFMydOxcAcPToURw7dgyenp4YN24cAGDBggUQESxduhSA/T3TUBfz2jo+fxxd9cBfn38igi1btjS4LqKKOCmH6sUxhvJ8FWcJVhyfU9svDseHnFb712UoFwgOl0tda3v//febxKD/6iZLAVDDH2Afz1lxXOO+ffvqfZw2bdpU+v/HH3+MSZMmAbBfG+3atcPp06dx4IB9LGJjjdW60IQUL28fnCspxv7dO7D15/+hx3U3VFknunNilWWmlq0BAL5+JlwzaAhWf/U51nz1OUqK7OP0Uobcpq575xOTEZuYhO2/rMXhzD1I37wRW9atwu603/DMfz+Et89fs2LPldQ+079r7xTM/molNvywDIcy0nHwj13YnbYBa5d+gf+s3FDv8wec+34yGo2Vwo6Dt7e3+u+K117FcHm+hIQE9Y+smvTp0wc33FD1NW4IX19fFBUVYc6cORgwYAAMBkOln7dt27ba2dOOsaIPPfQQZs+ejU2bNuG9997DsWPHYDQaq/3jsL6a8mclkQNbKKlRtG7dWv333r17AdhbrhyzD+vL0fK5bt26Sq0cv//+u/pvxy+rC92mp1u3bmrg/eSTTwDYg+7y5csBAN27d7+oGi8kKSkJAFBSUqLeWmnr1q3Ys2dPpeM6Wul+/PFHFBcXw2q1qq0bFaWlpSE2NhaxsbFqq4njGFarFW+++aY6UWrt2rV4+umn1RYJq9WKu+++G0VFRejcuTMURcFrr71WaRZnxV/+NT2n599SyPHHhdFoxMGDB5Gamorrr7++0jqtWrVSJ90sXLgQJ0+eBABYLBakp6fX6dgXClRXDRiEvkNug81qxWtPPoLdab8BAKISuqjr3PrAo3jx82V48fNleOHjr3H7o0/humEj1Z8PHGlvSdq0+kfsTtsARVHQ9//+CpQZW9MQl3Q17n12Fp5b9CUemvkyACB9k/05MPj6wtTSfsuXU9lHa603a086WgQE4q4nJuMf//0ALy/5AQCQf/oUsg/uR2SFutcvWwqNouDo0aPqJLJLdc3Wl+P602q1+Oyzz9Tr76effsIjjzyCoUOHqus6JtFptdpKk7DOt27dOuTk5FR5bN26FV5eXujduzeWL18OHx+fRj2XDz74AEajEevXr8eIESPUoOg4x7CwMKxZs0Y9x8WLF2PKlCkICwsDYJ+A6Ai5EydOBAAMGzas0rV9OTnqXr58ufo57Og9URQF3bp1c0pd5L4YKKlRGAwGXH311QDssySvvfZaDBkyBBrNxV1i//jHP2AymWCxWNCrVy/Ex8ejbdu2lbqPYmNjAQDz5s1DUlJSpVnKFUVGRqrdUf/+978RHR2NiIgIHDp0CFqtVp353NhGjhyJhIQEAMDw4cMRHx+PXr16wWazISQkBI8++igAqC18+/btQ0REBCIiIrBhQ9VWqpKSEuzZswd79uxRZ3CPHDkSnTp1gtVqRVJSEhISEtChQweYTCbcdttt6i+Sf/3rX/jtt9/g7++PFStW4MEHH4TNZsPo0aPVliPH8wnYu9auuuoq/Prrr7WeY6dOnQAAhYWFau1ffPFFlfVeeuklKIqCffv2oX379ujUqRNatWqFt99+G4A9dAYGBgKwd+8lJyfj9ddfV7fXai7QQqcoeGTWK+jcsw/KSs/hxUfG4MDuHUhI7oku16TYaxg/Do/d2BsTbk7BPT1iMev+u3Dy2BF1F1FXdkFkfCeUW8pQbrEgrvtVaB361wzaj175J8ZcFY/x1/fE07cOxBvPPAkACOvQUV2nYzf7H0L7d+2otdwNK77Dgynd8WC/7nj61oF48v+uBQDoDQYEXRGGK6/qhU497d3bC1+cgau6dkZcXByKiorg6+uLKVOm1P58XCbjx49H27ZtkZeXhw4dOqBLly6IjIxEYGBgla5exx9BXbt2rTUMBgYGIigoqMqja9eumD9//iUJk466vvrqK+h0Oixbtgzjxo2DiGDmzJnQarXYsGEDgoOD0bVrV4SGhuKKK67Aa6+9VmkfDz/8MIC//ihqjO7ui/Xcc89Bq9Xi0KFDiIiIQExMjNpVf++99yIiIsJptZF7YqCkRrNw4UJ1jNfRo0fx5ptvVrqlRX1ERUUhLS0NI0eORGBgIDIzMwEA1113nbrOrFmzcNVVV0Gj0WDz5s2Vbqdyvv/+97+YPXs2OnbsiMOHD8NisaB///5YvXp1pdvTNCYvLy+sW7cOjzzyCIKCgrB3714YjUbcdddd+O2339QbGA8ZMgSvvfYagoKCUFhYiO7du2PWrFl1OoZer8e6devw+OOPo127dti7dy/y8vLQvXt3vPDCC2jTpg22bNmCmTNnArAH6uDgYMyePRvt27fHoUOH1GDbqVMnTJ06FW3atMHhw4eRmpqq3v6oJvfeey+efPJJtGzZEoWFhUhJSVGPVdHw4cOxcuVKXHvttdBqtdi7dy/atGmjtrQpioJ33nkHUVFRKCgoQFpamtqiBQDeOo8q35JzPq1Oh6fnvYuIuCtRUlSI5++/C9kH9+PvbyzA8EeeQHBYBE4ePYz8U6fQNiIatz08EVfEdKi0j4F3/hUA+t5yW6Wf9brx/xCZ0AklRYU4nJkBH6Mfeg0agide+etWP9fcdAsAYPOa6sf3KX/+gRXXPRldeveDzSY4nLkHIoIrr7oGz7z9MXxa+AEAJr+5ELc++Dhah16B/fsyodVqMWTIEGzYsKFS+HemVq1aYePGjRg7diwCAwOxe/dumM1m9O7du0rYctx6a+TIkdXtqk7GjBkDX9/ax6c2RP/+/bFgwQIoioIPP/wQEydORJ8+ffDzzz/jxhtvhKIoSE9Ph06nw7Bhw9Q/Bh0GDRqktliGhYWhb9++l6zWC0lJScGaNWswYMAAWK1WZGVlITY2Fi+99BLeeustp9VF7ksRDsQgIhewOuv0Jf8+773bt2DKHYPh5e2Nd3/eDkM9w0u5xYLx11+N0znZePWb/yGsQ0dYy8txT4+OOFdSjAefexnX3z7qwjuqwKTX4drwlvXapqk5deqUei/WrKwstTX6UgkPD1db5lq1aoWXX34Zffr0uaTHdLjhhhuwcuVKTJ06tdo/rpq6559/Ht9//z3S09NRWFiI6dOn8+sXqU44KYeImqyhQ4ciJycHgP3m5uesNvVnf3/9Pfi3blPTpvVydH8mvnzzNaRvto/XHTDi7nqHScDeSjrsoYn47/S/4dsFbyHpuoH4+r03ca6kGBoPDyQk97zwTipQAAQYdPWuoyJHQKjO1KlTcdNNNzVo/3Xx+uuvo7S0FE899dQlD5MVHThwAAcOHEBubu4lP9YLL7yAn3/+GT/++CO8vb0xfvx49Wc5OTmVxpNWFBwcXO2YaWfZv39/pXHrRHXFFkoiarIcLU3Vmb8qtdIYx4bYlboB00ffBi9vH3RL6Y/x/3wVei/DhTe8gM9fn4PF8+eidegVuP2xSegzuP4zfhOD/BDud/ETO8aMGYNFixZV+7OmcpcCd5CSkoL169cjMjISr732WqWgnpWVVeNEpLCwMGRlZV2mKokuHQZKInIJ+ecsWH3otLPLuOyuC2sJP6+GtVISEV1qnJRDRC6hhV6LC032djcaBTDqOTKJiJo+BkoicgkaRUGo0XDB2d7uQgEQajTU6abmRETOxkBJRC4jwuSN5jJGRwBE+jvnpthERPXFQElELiPA4Am/ZtIF7KfXwt/L09llEBHVCQMlEbmUKP/G/5aUpqi5nCcRuQcGSiJyKaFGw4W/itHFaTX28aJERK6CgZKIXIqHRkGkm7feRfr7wMPNQzMRuRcGSiJyObEBvvCpw/d7uxoFgI/OA7EBl+77qomILgUGSiJyOR4aBd2DTW4341sAJAWb2DpJRC6HgZKIXFKgwRPRbtb1He3vgwADZ3YTkethoCQilxXX0ugWXd+Oru64lkZnl0JEdFEYKInIZXloFCQFm5xdRqNgVzcRuTIGSiJyaQEGT/QIMTm7jAbpEWJiVzcRuTQGSiJyeW2NBiQG+Tm7jIuSGOSHtrznJBG5OAZKInIL4X7eLhcqE4P8EO7H7+smIteniIi73XmDiJqxY4VmpGXnA0CTvK2QY5RkjxATWyaJyG0wUBKR28k1l2FTTj6KLVZnl1KFj84DScEcM0lE7oWBkojcktUmSD9diMy8Yihwbmul4/jR/j6Ia2nkbG4icjsMlETk1s6Yy7DZya2VbJUkInfHQElEbs9qE2TkFmF/XjHKbZfvI0+rURDp74PYAF+2ShKRW2OgJKJmw2oTHC00Y19eMc6Wljd6V7hjfya9FpH+Pgg1GhgkiahZYKAkomYp11yGA/klOFpohqPRsr4Bs+L6GgUINRoQ6e8Nfy92bRNR88JASUTNmk0EhaXlyCu1IP+cBblmCwpKLbDVso0GQAu9DgEGHUxeOvjrdTDqtdAobI0kouaJgZKI6Dw2EZRYrCi3CWxif2gUBRpFgVajwFvnwfBIRFQBAyURERERNQi/epGIiIiIGoSBkoiIiIgahIGSiIiIiBqEgZKIiIiIGoSBkoiIiIgahIGSiIiIiBqEgZKIiIiIGoSBkoiIiIgahIGSiIiIiBqEgZKIiIiIGoSBkoiIiIgahIGSiIiIiBqEgZKIiIiIGoSBkoiIiIgahIGSiIiIiBqEgZKIiIiIGoSBkoiIiIgahIGSiIiIiBrk/wEVL5/ebONzNAAAAABJRU5ErkJggg==", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tracker.draw_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "73992089", + "metadata": {}, + "source": [ + "As you can see, the tracker only tracked one branch of the computation graph, leaving the other branch outside of the computation graph it has constructed and visualized." + ] + }, + { + "cell_type": "markdown", + "id": "8a02717c", + "metadata": {}, + "source": [ + "## Labeling operations" + ] + }, + { + "cell_type": "markdown", + "id": "74984946", + "metadata": {}, + "source": [ + "All operations accept `label` keyword argument when creating and also has property `.label` where you can change it at any time you like. If `label` attribute of an operation is not `None`, the value will be used instead to label the operation node in the graph. In fact, you can change the label after the pipeline construction, and the change will still be reflected the next time you draw the graph!" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b5ea2dde", + "metadata": {}, + "outputs": [], + "source": [ + "data_source.label = \"LocalDataStore\"\n", + "\n", + "count_lines.label = \"CountLines\"" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "cfc2e1c3", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tracker.draw_graph()" + ] + }, + { + "cell_type": "markdown", + "id": "7482ce7f", + "metadata": {}, + "source": [ + "You can always get the orginal label back by setting label to `None`:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "8d4664a1", + "metadata": {}, + "outputs": [], + "source": [ + "count_lines.label = None" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "1ac44e76", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "tracker.draw_graph()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/orcabridge/base.py b/src/orcabridge/base.py index 2bc3e3d0..67b4a36c 100644 --- a/src/orcabridge/base.py +++ b/src/orcabridge/base.py @@ -1,10 +1,19 @@ +from orcabridge.hashing import HashableMixin from .types import Tag, Packet -from typing import Optional, Tuple, List, Dict, Any, Collection, Callable, Iterator -from .utils.hash import hash_dict -import networkx as nx - - -class Operation: +from typing import ( + Optional, + Tuple, + List, + Any, + Collection, + Callable, + Iterator, +) +from collections.abc import Collection +from typing import Any, List, Tuple + + +class Operation(HashableMixin): """ Operation defines a generic operation that can be performed on a stream of data. It is a base class for all operations that can be performed on a collection of streams @@ -15,35 +24,39 @@ class Operation: information is stored as Invocation object and attached to the output stream. """ - def get_invocation(self, *streams: "SyncStream") -> str: + def __init__(self, label: Optional[str] = None, **kwargs) -> None: + super().__init__(**kwargs) + self._label = label + + @property + def label(self) -> str: """ - Given a list of streams to perform the operation on, define - the invocation ID that would uniquely identify this particular - invocation of the operation. This ID is used to track each distinct - invocation of the operation and its associated streams. - The default implementation is to use the hash of the streams - as the invocation ID. This is sensitive to the order of the streams. - - For operations that are not sensitive to the order of the streams, - this method should be overridden to provide a more appropriate - invocation ID. For example, a join operation may want to use the - hash of the joined streams instead of the individual stream hashes. + Overwrite this method to attain a custom label logic for the operation. """ - # default implementation where ID is stream order sensitive - invocation_id = ":".join([str(hash(s)) for s in streams]) - return Invocation(self, invocation_id, streams) + return self._label - def __call__(self, *streams: "SyncStream") -> "SyncStream": - # if any source is passed in as a stream, invoke it to extract stream first - from .source import Source + @label.setter + def label(self, value: str) -> None: + self._label = value + + def identity_structure(self, *streams: "SyncStream") -> Any: + # Default implementation of identity_structure for the operation only + # concerns the operation class and the streams if present. Subclasses of + # Operations should override this method to provide a more meaningful + # representation of the operation. + return (self.__class__.__name__, streams) - streams = [s() if isinstance(s, Source) else s for s in streams] + def __call__(self, *streams: "SyncStream") -> "SyncStream": + # trigger call on source if passed as stream + streams = [ + stream() if isinstance(stream, Source) else stream for stream in streams + ] output_stream = self.forward(*streams) # create an invocation instance - invocation = self.get_invocation(*streams) + invocation = Invocation(self, streams) # label the output_stream with the invocation information - output_stream.source = invocation + output_stream.invocation = invocation # delay import to avoid circular import from .tracker import Tracker @@ -61,7 +74,7 @@ def __repr__(self): def forward(self, *streams: "SyncStream") -> "SyncStream": ... -class Invocation: +class Invocation(HashableMixin): """ This class represents an invocation of an operation on a collection of streams. It contains the operation, the invocation ID, and the streams that were used @@ -73,59 +86,72 @@ class Invocation: def __init__( self, operation: Operation, - invocation_id: str, streams: Collection["SyncStream"], ) -> None: self.operation = operation - self.invocation_id = invocation_id self.streams = streams - def __repr__(self) -> str: - return f"Invocation({self.operation}, ID:{self.invocation_id})" + # @property + # def invocation_id(self) -> int: + # """ + # The invocation ID is a unique identifier for the invocation. + # It is used to track the invocation in the tracker. + # """ + # return hash(self) def __hash__(self) -> int: - # TODO: use a better hash function that is based on the operation, - # invocation_id and the streams (ideally invocation objects attached to the streams) return super().__hash__() + def __repr__(self) -> str: + return f"Invocation({self.operation}, ID:{hash(self)})" + + def identity_structure(self) -> int: + # default implementation is streams order sensitive. If an operation does + # not depend on the order of the streams, it should override this method + return self.operation.identity_structure(*self.streams) + def __eq__(self, other: Any) -> bool: if not isinstance(other, Invocation): return False - return ( - self.operation == other.operation - and self.invocation_id == other.invocation_id - ) + return hash(self) == hash(other) def __lt__(self, other: Any) -> bool: if not isinstance(other, Invocation): return NotImplemented if self.operation == other.operation: - return self.invocation_id < other.invocation_id - return self.operation < other.operation + return hash(self) < hash(other) + # otherwise, order by the operation + return hash(self.operation) < hash(other.operation) -class Stream: +class Stream(HashableMixin): """ A stream is a collection of tagged-packets that are generated by an operation. The stream is iterable and can be used to access the packets in the stream. - A stream has propery `source` that is an instance of Invocation that generated the stream. + A stream has propery `invocation` that is an instance of Invocation that generated the stream. This may be None if the stream is not generated by an operation. """ - def __init__(self): - self._source: Optional[Invocation] = None + def __init__(self, **kwargs) -> None: + super().__init__(**kwargs) + self._invocation: Optional[Invocation] = None + + def identity_structure(self) -> Any: + if self.invocation is not None: + return self.invocation.identity_structure() + return super().identity_structure() @property - def source(self) -> Optional[Invocation]: - return self._source + def invocation(self) -> Optional[Invocation]: + return self._invocation - @source.setter - def source(self, value: Invocation) -> None: + @invocation.setter + def invocation(self, value: Invocation) -> None: if not isinstance(value, Invocation): - raise TypeError("source must be an instance of Invocation") - self._source = value + raise TypeError("invocation field must be an instance of Invocation") + self._invocation = value def __iter__(self) -> Iterator[Tuple[Tag, Packet]]: raise NotImplementedError("Subclasses must implement __iter__ method") @@ -137,8 +163,16 @@ class SyncStream(Stream): will have to wait for the stream to finish before proceeding. """ + def content_hash(self) -> str: + if ( + self.invocation is not None + ): # and hasattr(self.invocation, "invocation_id"): + # use the invocation ID as the hash + return self.invocation.content_hash() + return super().content_hash() + def __hash__(self) -> int: - return super().__hash__() + return hash(self.content_hash()) def keys(self) -> Tuple[List[str], List[str]]: """ @@ -153,7 +187,7 @@ def keys(self) -> Tuple[List[str], List[str]]: tag, packet = next(iter(self)) return list(tag.keys()), list(packet.keys()) - def preview(self, n: int = 1) -> None: + def head(self, n: int = 5) -> None: """ Print the first n elements of the stream. This method is useful for previewing the stream @@ -172,3 +206,43 @@ def __len__(self) -> int: This method is not guaranteed to be efficient and should be used with caution. """ return sum(1 for _ in self) + + def __rshift__(self, transformer: Any) -> "SyncStream": + """ + Returns a new stream that is the result of applying the mapping to the stream. + The mapping is applied to each packet in the stream and the resulting packets + are returned in a new stream. + """ + from .mapper import MapPackets + + # TODO: extend to generic mapping + if isinstance(transformer, dict): + return MapPackets(transformer)(self) + elif isinstance(transformer, Callable): + return transformer(self) + + def __mul__(self, other: "SyncStream") -> "SyncStream": + """ + Returns a new stream that is the result joining with the other stream + """ + from .mapper import Join + + if not isinstance(other, SyncStream): + raise TypeError("other must be a SyncStream") + return Join()(self, other) + + +class Source(Operation, SyncStream): + """ + A base class for all sources in the system. A source can be seen as a special + type of Operation that takes no input and produces a stream of packets. + For convenience, the source itself is also a stream and thus can be used + as an input to other operations directly. + """ + + def __init__(self, label: Optional[str] = None, **kwargs) -> None: + super().__init__(label=label, **kwargs) + self._invocation = None + + def __iter__(self) -> Iterator[Tuple[Tag, Packet]]: + yield from self() diff --git a/src/orcabridge/hashing.py b/src/orcabridge/hashing.py new file mode 100644 index 00000000..2dc47dd9 --- /dev/null +++ b/src/orcabridge/hashing.py @@ -0,0 +1,242 @@ +# a function to hash a dictionary of key value pairs into uuid +from collections.abc import Collection, Mapping +import hashlib +import uuid +from uuid import UUID +from typing import Any, Dict, Optional, Union +import inspect +import json + +# arbitrary depth of nested dictionaries +T = Dict[str, Union[str, "T"]] + + +# TODO: implement proper recursive hashing +def hash_dict(d: T) -> UUID: + # Convert the dictionary to a string representation + dict_str = str(sorted(d.items())) + + # Create a hash of the string representation + hash_object = hashlib.md5(dict_str.encode()) + + # Convert the hash to a UUID + hash_uuid = uuid.UUID(hash_object.hexdigest()) + + return hash_uuid + + +import hashlib + + +def stable_hash(s): + """Create a stable hash that returns the same integer value across sessions.""" + # Convert input to bytes if it's not already + if not isinstance(s, bytes): + s = str(s).encode("utf-8") + + hash_hex = hashlib.sha256(s).hexdigest() + return int(hash_hex[:16], 16) + + +def function_content_hash(func): + """ + Compute a hash based on the function's source code, name, module, and closure variables. + """ + components = [] + + # Add function name + components.append(f"name:{func.__name__}") + + # Add module + components.append(f"module:{func.__module__}") + + # Get the function's source code + try: + source = inspect.getsource(func) + # Clean up the source code + source = source.strip() + components.append(f"source:{source}") + except (IOError, TypeError): + # If we can't get the source (e.g., built-in function), use the function's string representation + components.append(f"repr:{repr(func)}") + + # Add closure variables if any + if func.__closure__: + closure_values = [] + for cell in func.__closure__: + # Try to get a stable representation of the cell content + try: + # For simple immutable objects + if isinstance(cell.cell_contents, (int, float, str, bool, type(None))): + closure_values.append(repr(cell.cell_contents)) + # For other objects, we'll use their string representation + else: + closure_values.append(str(cell.cell_contents)) + except: + # If we can't get a stable representation, use the cell's id + closure_values.append(f"cell_id:{id(cell)}") + + components.append(f"closure:{','.join(closure_values)}") + + # Add function attributes that affect behavior + if hasattr(func, "__defaults__") and func.__defaults__: + defaults_str = ",".join(repr(d) for d in func.__defaults__) + components.append(f"defaults:{defaults_str}") + + if hasattr(func, "__kwdefaults__") and func.__kwdefaults__: + kwdefaults_str = ",".join( + f"{k}={repr(v)}" for k, v in func.__kwdefaults__.items() + ) + components.append(f"kwdefaults:{kwdefaults_str}") + + # Function's code object properties (excluding filename and line numbers) + code = func.__code__ + code_props = { + "co_argcount": code.co_argcount, + "co_posonlyargcount": getattr(code, "co_posonlyargcount", 0), # Python 3.8+ + "co_kwonlyargcount": code.co_kwonlyargcount, + "co_nlocals": code.co_nlocals, + "co_stacksize": code.co_stacksize, + "co_flags": code.co_flags, + "co_code": code.co_code, + "co_names": code.co_names, + "co_varnames": code.co_varnames, + } + components.append(f"code_properties:{repr(code_props)}") + + # Join all components and compute hash + combined = "\n".join(components) + return hashlib.sha256(combined.encode("utf-8")).hexdigest() + + +class HashableMixin: + """A mixin that provides content-based hashing functionality.""" + + def identity_structure(self) -> Any: + """ + Return a structure that represents the identity of this object. + By default, returns None to indicate that no custom structure is provided. + Subclasses should override this method to provide meaningful representations. + + Returns: + None to indicate no custom structure (use default hash) + """ + return None + + def content_hash(self, char_count: Optional[int] = 16) -> str: + """ + Generate a stable string hash based on the object's content. + + Returns: + str: A hexadecimal digest representing the object's content + """ + # Get the identity structure + structure = self.identity_structure() + + # If no custom structure is provided, use the superclass's hash + if structure is None: + # Convert the default hash to a stable string + return hashlib.sha256(str(super().__hash__()).encode()).hexdigest() + + # Generate a hash from the identity structure + return self._hash_structure(structure, char_count=char_count) + + def content_hash_int(self, hexdigits=16) -> int: + """ + Generate a stable integer hash based on the object's content. + + Returns: + int: An integer representing the object's content + """ + # pass in char_count=None to get the full hash + return int(self.content_hash(char_count=None)[:hexdigits], 16) + + def __hash__(self) -> int: + """ + Hash implementation that uses the identity structure if provided, + otherwise falls back to the superclass's hash method. + + Returns: + int: A hash value based on either content or identity + """ + # Get the identity structure + structure = self.identity_structure() + + # If no custom structure is provided, use the superclass's hash + if structure is None: + return super().__hash__() + + # Generate a hash and convert to integer + hash_hex = self._hash_structure(structure, char_count=None) + return int(hash_hex[:16], 16) + + def _hash_structure(self, structure: Any, char_count: Optional[int] = 16) -> str: + """ + Helper method to compute a hash string from a structure. + + Args: + structure: The structure to hash + + Returns: + str: A hexadecimal hash digest of the structure + """ + processed = self._process_structure(structure) + json_str = json.dumps(processed, sort_keys=True).encode() + return hashlib.sha256(json_str).hexdigest()[:char_count] + + def _process_structure(self, obj: Any) -> Any: + """ + Recursively process a structure to prepare it for hashing. + + Args: + obj: The object or structure to process + + Returns: + A processed version of the structure with HashableMixin objects replaced by their hashes + """ + # Handle None + if obj is None: + return "None" + + # If the object is a HashableMixin, use its content_hash + if isinstance(obj, HashableMixin): + # Don't call content_hash on self to avoid cycles + if obj is self: + # Use the superclass's hash for self + return str(super(HashableMixin, self).__hash__()) + return obj.content_hash() + + # Handle basic types + if isinstance(obj, (str, int, float, bool)): + return str(obj) + + # Handle named tuples (which are subclasses of tuple) + if hasattr(obj, "_fields") and isinstance(obj, tuple): + # For namedtuples, convert to dict and then process + return self._process_structure( + {field: value for field, value in zip(obj._fields, obj)} + ) + + # Handle mappings (dict-like objects) + if isinstance(obj, Mapping): + return { + str(k): self._process_structure(v) + for k, v in sorted(obj.items(), key=lambda x: str(x[0])) + } + + # Handle sets and frozensets specifically + if isinstance(obj, (set, frozenset)): + # Process each item first, then sort the processed results + processed_items = [self._process_structure(item) for item in obj] + return sorted(processed_items, key=str) + + # Handle collections (list-like objects) + if isinstance(obj, Collection): + return [self._process_structure(item) for item in obj] + + # For bytes and bytearray, convert to hex representation + if isinstance(obj, (bytes, bytearray)): + return obj.hex() + + # For other objects, just use their string representation + return str(obj) diff --git a/src/orcabridge/mapper.py b/src/orcabridge/mapper.py index 0e07573b..885ebf7c 100644 --- a/src/orcabridge/mapper.py +++ b/src/orcabridge/mapper.py @@ -2,7 +2,13 @@ from .stream import SyncStream, SyncStreamFromGenerator from .base import Operation -from .utils.stream_utils import join_tags, batch_tag, batch_packet +from .utils.stream_utils import ( + join_tags, + check_packet_compatibility, + batch_tag, + batch_packet, +) +from .hashing import function_content_hash, stable_hash from .types import Tag, Packet from typing import Iterator, Tuple @@ -15,6 +21,10 @@ class Mapper(Operation): class Join(Mapper): + def identity_structure(self, *streams): + # Join does not depend on the order of the streams -- convert it onto a set + return (self.__class__.__name__, set(streams)) + def forward(self, *streams: SyncStream) -> SyncStream: """ Joins two streams together based on their tags. @@ -29,6 +39,10 @@ def generator(): for left_tag, left_packet in left_stream: for right_tag, right_packet in right_stream: if (joined_tag := join_tags(left_tag, right_tag)) is not None: + if not check_packet_compatibility(left_packet, right_packet): + raise ValueError( + f"Packets are not compatible: {left_packet} and {right_packet}" + ) yield joined_tag, {**left_packet, **right_packet} return SyncStreamFromGenerator(generator) @@ -36,8 +50,11 @@ def generator(): def __repr__(self) -> str: return "Join()" + def __hash__(self) -> int: + return stable_hash(self.__class__.__name__) + -class MapKeys(Mapper): +class MapPackets(Mapper): """ A Mapper that maps the keys of the packet in the stream to new keys. The mapping is done using a dictionary that maps old keys to new keys. @@ -69,10 +86,18 @@ def generator(): return SyncStreamFromGenerator(generator) def __repr__(self) -> str: - return f"MapKeys({self.key_map})" + map_repr = ", ".join([f"{k} ⇒ {v}" for k, v in self.key_map.items()]) + return f"packets({map_repr})" + def identity_structure(self, *streams): + return ( + self.__class__.__name__, + tuple(sorted(self.key_map.items())), + self.drop_unmapped, + ) + tuple(streams) -class MapTags(Operation): + +class MapTags(Mapper): """ A Mapper that maps the tags of the packet in the stream to new tags. Packet remains unchanged. The mapping is done using a dictionary that maps old tags to new tags. @@ -80,9 +105,9 @@ class MapTags(Operation): drop_unmapped=False, in which case unmapped tags will be retained. """ - def __init__(self, tag_map: Dict[str, str], drop_unmapped: bool = True) -> None: + def __init__(self, key_map: Dict[str, str], drop_unmapped: bool = True) -> None: super().__init__() - self.tag_map = tag_map + self.key_map = key_map self.drop_unmapped = drop_unmapped def forward(self, *streams: SyncStream) -> SyncStream: @@ -91,18 +116,35 @@ def forward(self, *streams: SyncStream) -> SyncStream: stream = streams[0] - def generator(): + def generator() -> Iterator[Tuple[Tag, Packet]]: for tag, packet in stream: if self.drop_unmapped: - tag = {v: tag[k] for k, v in self.tag_map.items() if k in tag} + tag = {v: tag[k] for k, v in self.key_map.items() if k in tag} else: - tag = {self.tag_map.get(k, k): v for k, v in tag.items()} + tag = {self.key_map.get(k, k): v for k, v in tag.items()} yield tag, packet return SyncStreamFromGenerator(generator) def __repr__(self) -> str: - return f"MapTags({self.tag_map})" + map_repr = ", ".join([f"{k} ⇒ {v}" for k, v in self.key_map.items()]) + return f"tags({map_repr})" + + def __hash__(self) -> int: + return stable_hash( + ( + self.__class__.__name__, + tuple(sorted(self.key_map.items())), + self.drop_unmapped, + ) + ) + + def identity_structure(self, *streams): + return ( + self.__class__.__name__, + tuple(sorted(self.key_map.items())), + self.drop_unmapped, + ) + tuple(streams) class Filter(Mapper): @@ -122,7 +164,7 @@ def forward(self, *streams: SyncStream) -> SyncStream: stream = streams[0] - def generator(): + def generator() -> Iterator[Tuple[Tag, Packet]]: for tag, packet in stream: if self.predicate(tag, packet): yield tag, packet @@ -132,6 +174,12 @@ def generator(): def __repr__(self) -> str: return f"Filter({self.predicate})" + def identity_structure(self, *streams): + return ( + self.__class__.__name__, + function_content_hash(self.predicate), + ) + tuple(streams) + class Transform(Mapper): """ @@ -150,7 +198,7 @@ def forward(self, *streams: SyncStream) -> SyncStream: stream = streams[0] - def generator(): + def generator() -> Iterator[Tuple[Tag, Packet]]: for tag, packet in stream: yield self.transform(tag, packet) @@ -159,6 +207,12 @@ def generator(): def __repr__(self) -> str: return f"Transform({self.transform})" + def identity_structure(self, *streams): + return ( + self.__class__.__name__, + function_content_hash(self.transform), + ) + tuple(streams) + class Batch(Mapper): """ @@ -205,6 +259,14 @@ def generator() -> Iterator[Tuple[Tag, Packet]]: def __repr__(self) -> str: return f"Batch(size={self.batch_size}, drop_last={self.drop_last})" + def identity_structure(self, *streams): + return ( + self.__class__.__name__, + self.batch_size, + function_content_hash(self.tag_processor), + self.drop_last, + ) + tuple(streams) + class CacheStream(Mapper): """ @@ -246,3 +308,33 @@ def clear_cache(self) -> None: def __repr__(self) -> str: return f"CacheStream(active:{self.is_cached})" + + def identity_structure(self, *streams): + # treat every CacheStream as a different stream + return None + + +def tag( + mapping: Dict[str, str], drop_unmapped: bool = True +) -> Callable[[SyncStream], SyncStream]: + def transformer(stream: SyncStream) -> SyncStream: + """ + Transform the stream by renaming the keys in the tag. + The mapping is a dictionary that maps the old keys to the new keys. + """ + return MapTags(mapping, drop_unmapped=drop_unmapped)(stream) + + return transformer + + +def packet( + mapping: Dict[str, str], drop_unmapped: bool = True +) -> Callable[[SyncStream], SyncStream]: + def transformer(stream: SyncStream) -> SyncStream: + """ + Transform the stream by renaming the keys in the packet. + The mapping is a dictionary that maps the old keys to the new keys. + """ + return MapPackets(mapping, drop_unmapped=drop_unmapped)(stream) + + return transformer diff --git a/src/orcabridge/pod.py b/src/orcabridge/pod.py index d66da170..91c1ff4a 100644 --- a/src/orcabridge/pod.py +++ b/src/orcabridge/pod.py @@ -3,14 +3,59 @@ logger = logging.getLogger(__name__) from pathlib import Path -from typing import List, Optional, Tuple, Iterator, Iterable, Collection -from .utils.hash import hash_dict +from typing import List, Optional, Tuple, Iterator, Iterable, Collection, Literal, Any +from .hashing import function_content_hash, hash_dict, stable_hash +from .utils.name import get_function_signature from .base import Operation from .mapper import Join from .stream import SyncStream, SyncStreamFromGenerator from .types import Tag, Packet, PodFunction +from .store import DataStore, NoOpDataStore import json import shutil +import functools + + +def function_pod( + output_keys: Optional[Collection[str]] = None, + store_name: Optional[str] = None, + data_store: Optional[DataStore] = None, + function_hash_mode: Literal["signature", "content", "name", "custom"] = "name", + custom_hash: Optional[int] = None, + force_computation: bool = False, + skip_memoization: bool = False, +): + """ + Decorator that wraps a function in a FunctionPod instance. + + Args: + output_keys: Keys for the function output + force_computation: Whether to force computation + skip_memoization: Whether to skip memoization + + Returns: + FunctionPod instance wrapping the decorated function + """ + + def decorator(func): + # Create a FunctionPod instance with the function and parameters + pod = FunctionPod( + function=func, + output_keys=output_keys, + store_name=store_name, + data_store=data_store, + function_hash_mode=function_hash_mode, + custom_hash=custom_hash, + force_computation=force_computation, + skip_memoization=skip_memoization, + ) + + # Update the metadata to make the pod look more like the original function + functools.update_wrapper(pod, func) + + return pod + + return decorator class Pod(Operation): @@ -46,8 +91,6 @@ def generator() -> Iterator[Tuple[Tag, Packet]]: return SyncStreamFromGenerator(generator) - def __hash__(self) -> int: ... - def process(self, packet: Packet) -> Packet: ... @@ -59,50 +102,30 @@ def __init__( self, function: PodFunction, output_keys: Optional[Collection[str]] = None, - force_computation=False, - skip_memoization=False, + store_name=None, + data_store: Optional[DataStore] = None, + function_hash_mode: Literal["signature", "content", "name", "custom"] = "name", + custom_hash: Optional[int] = None, + label: Optional[str] = None, + force_computation: bool = False, + skip_memoization: bool = False, + **kwargs, ) -> None: - super().__init__() + super().__init__(label=label, **kwargs) self.function = function if output_keys is None: output_keys = [] self.output_keys = output_keys + self.store_name = self.function.__name__ if store_name is None else store_name + self.data_store = data_store if data_store is not None else NoOpDataStore() + self.function_hash_mode = function_hash_mode + self.custom_hash = custom_hash self.skip_memoization = skip_memoization self.force_computation = force_computation - def __hash__(self) -> int: - return hash((self.function, tuple(self.output_keys))) - - def process(self, packet: Packet) -> Packet: - memoized_packet = self.retrieve_memoized(packet) - if not self.force_computation and memoized_packet is not None: - return memoized_packet - - values = self.function(**packet) - if len(self.output_keys) == 0: - values = [] - elif len(self.output_keys) == 1: - values = [values] - elif isinstance(values, Iterable): - values = list(values) - elif len(self.output_keys) > 1: - raise ValueError( - "Values returned by function must be a pathlike or a sequence of pathlikes" - ) - - if len(values) != len(self.output_keys): - raise ValueError( - "Number of output keys does not match number of values returned by function" - ) - - output_packet: Packet = {k: v for k, v in zip(self.output_keys, values)} - - if not self.skip_memoization: - # output packet may be modified by the memoization process - # e.g. if the output is a file, the path may be changed - output_packet = self.memoize(packet, output_packet) - - return output_packet + def __repr__(self) -> str: + func_sig = get_function_signature(self.function) + return f"FunctionPod:{func_sig} ⇒ {self.output_keys}" def forward(self, *streams: SyncStream) -> SyncStream: # if multiple streams are provided, join them @@ -118,7 +141,9 @@ def forward(self, *streams: SyncStream) -> SyncStream: def generator() -> Iterator[Tuple[Tag, Packet]]: n_computed = 0 for tag, packet in stream: - memoized_packet = self.retrieve_memoized(packet) + memoized_packet = self.data_store.retrieve_memoized( + self.store_name, self.content_hash(), packet + ) if not self.force_computation and memoized_packet is not None: yield tag, memoized_packet continue @@ -144,7 +169,9 @@ def generator() -> Iterator[Tuple[Tag, Packet]]: if not self.skip_memoization: # output packet may be modified by the memoization process # e.g. if the output is a file, the path may be changed - output_packet = self.memoize(packet, output_packet) + output_packet = self.data_store.memoize( + self.store_name, self.content_hash(), packet, output_packet + ) n_computed += 1 logger.info(f"Computed item {n_computed}") @@ -152,112 +179,25 @@ def generator() -> Iterator[Tuple[Tag, Packet]]: return SyncStreamFromGenerator(generator) - def memoize( - self, packet: Packet, output_packet: Packet, overwrite: bool = False - ) -> Packet: - return output_packet - - def retrieve_memoized(self, packet: Packet) -> Optional[Packet]: - return None - - -class FunctionPodWithDirStorage(FunctionPod): - """ - A FunctionPod that stores the output in the specified directory. - The output is stored in a subdirectory named store_name, creating it if it doesn't exist. - If store_name is None, the function name is used as the directory name. - The output is stored in a file named based on the hash of the input packet. - """ - - def __init__( - self, - function: PodFunction, - output_keys: Optional[List[str]] = None, - store_dir="./pod_data", - store_name=None, - copy_files=True, - preserve_filename=True, - **kwargs, - ) -> None: - super().__init__(function, output_keys, **kwargs) - self.store_dir = Path(store_dir) - if store_name is None: - store_name = self.function.__name__ - self.store_name = store_name - self.data_dir = self.store_dir / self.store_name - # Create the data directory if it doesn't exist - self.data_dir.mkdir(parents=True, exist_ok=True) - self.copy_files = copy_files - self.preserve_filename = preserve_filename - - def memoize( - self, packet: Packet, output_packet: Packet, overwrite: bool = False - ) -> Packet: - packet_hash = hash_dict(packet) - output_dir = self.data_dir / f"{packet_hash}" - info_path = output_dir / "_info.json" - - if info_path.exists() and not overwrite: - logger.info( - f"Entry for packet {packet} already exists, and will not be overwritten" - ) - return False + def identity_structure(self, *streams) -> Any: + if self.function_hash_mode == "content": + function_hash = function_content_hash(self.function) + elif self.function_hash_mode == "signature": + function_hash = get_function_signature(self.function) + elif self.function_hash_mode == "name": + function_hash = self.store_name + elif self.function_hash_mode == "custom": + if self.custom_hash is None: + raise ValueError("Custom hash function not provided") + function_hash = self.custom_hash else: - output_dir.mkdir(parents=True, exist_ok=True) - if self.copy_files: - new_output_packet = {} - # copy the files to the output directory - for key, value in output_packet.items(): - if self.preserve_filename: - relative_output_path = Path(value).name - if (output_dir / relative_output_path).exists(): - raise ValueError( - f"File {relative_output_path} already exists in {output_path}" - ) - else: - # preserve the suffix of the original if present - relative_output_path = key + Path(value).suffix - - output_path = output_dir / relative_output_path - if output_path.exists() and not overwrite: - # TODO: handle case where it's a directory - raise ValueError( - f"File {relative_output_path} already exists in {output_path}" - ) - shutil.copy(value, output_path) - # register the key with the new path - new_output_packet[key] = str(relative_output_path) - output_packet = new_output_packet - # store the packet in a json file - with open(info_path, "w") as f: - json.dump(output_packet, f) - logger.info(f"Stored output for packet {packet} at {output_path}") - - # retrieve back the memoized packet and return - # TODO: consider if we want to return the original packet or the memoized one - output_packet = self.retrieve_memoized(packet) - if output_packet is None: - raise ValueError(f"Memoized packet {packet} not found after storing it") - - return output_packet - - def retrieve_memoized(self, packet: Packet) -> Optional[Packet]: - packet_hash = hash_dict(packet) - output_dir = self.data_dir / f"{packet_hash}" - info_path = output_dir / "_info.json" - - if info_path.exists(): - with open(info_path, "r") as f: - output_packet = json.load(f) - # update the paths to be absolute - for key, value in output_packet.items(): - output_packet[key] = str(output_dir / value) - logger.info(f"Retrieved output for packet {packet} from {info_path}") - return output_packet - else: - logger.info(f"No memoized output found for packet {packet}") - return None + raise ValueError( + f"Unknown function hash mode: {self.function_hash_mode}. " + "Must be one of 'content', 'signature', 'name', or 'custom'." + ) - def clear_store(self) -> None: - # delete the folder self.data_dir and its content - shutil.rmtree(self.data_dir) + return ( + self.__class__.__name__, + function_hash, + tuple(self.output_keys), + ) + tuple(streams) diff --git a/src/orcabridge/source.py b/src/orcabridge/source.py index 12b88ae2..ab836224 100644 --- a/src/orcabridge/source.py +++ b/src/orcabridge/source.py @@ -1,25 +1,10 @@ -from .base import Operation +from .base import Source from .stream import SyncStream, SyncStreamFromGenerator from .types import Tag, Packet -from typing import Iterator, Tuple, Optional, Callable +from typing import Iterator, Tuple, Optional, Callable, Any from os import PathLike from pathlib import Path - - -class Source(Operation, SyncStream): - """ - A base class for all sources in the system. A source can be seen as a special - type of Operation that takes no input and produces a stream of packets. - For convenience, the source itself is also a stream and thus can be used - as an input to other operations directly. - """ - - def __init__(self) -> None: - super().__init__() - self._source = self - - def __iter__(self) -> Iterator[Tuple[Tag, Packet]]: - yield from self() +from .hashing import function_content_hash, stable_hash class GlobSource(Source): @@ -51,20 +36,24 @@ class GlobSource(Source): ... lambda f: {'date': Path(f).stem[:8]}) """ + default_tag_function = lambda f: {"file_name": Path(f).stem} + def __init__( self, name: str, file_path: PathLike, pattern: str = "*", + label: Optional[str] = None, tag_function: Optional[Callable[[PathLike], Tag]] = None, + **kwargs, ) -> None: - super().__init__() + super().__init__(label=label, **kwargs) self.name = name self.file_path = file_path self.pattern = pattern if tag_function is None: # extract the file name without extension - tag_function = lambda file: {"file_name": Path(file).stem} + tag_function = self.__class__.default_tag_function self.tag_function = tag_function def forward(self) -> SyncStream: @@ -73,3 +62,15 @@ def generator() -> Iterator[Tuple[Tag, Packet]]: yield self.tag_function(file), {self.name: file} return SyncStreamFromGenerator(generator) + + def __repr__(self) -> str: + return f"GlobSource({str(Path(self.file_path) / self.pattern)}) ⇒ {self.name}" + + def identity_structure(self, *streams) -> Any: + return ( + self.__class__.__name__, + self.name, + str(self.file_path), + self.pattern, + function_content_hash(self.tag_function), + ) + tuple(streams) diff --git a/src/orcabridge/store.py b/src/orcabridge/store.py new file mode 100644 index 00000000..278d124b --- /dev/null +++ b/src/orcabridge/store.py @@ -0,0 +1,143 @@ +from .types import Tag, Packet +from typing import Optional, Collection +from pathlib import Path +from .hashing import hash_dict +import shutil +import logging +import json + +logger = logging.getLogger(__name__) + + +class DataStore: + def memoize( + self, + store_name: str, + content_hash: str, + packet: Packet, + output_packet: Packet, + overwrite: bool = False, + ) -> Packet: ... + + def retrieve_memoized( + self, store_name: str, content_hash: str, packet: Packet + ) -> Optional[Packet]: ... + + +class NoOpDataStore(DataStore): + """ + An empty data store that does not store anything. + This is useful for testing purposes or when no memoization is needed. + """ + + def memoize( + self, + store_name: str, + content_hash: str, + packet: Packet, + output_packet: Packet, + overwrite: bool = False, + ) -> Packet: + return output_packet + + def retrieve_memoized( + self, store_name: str, content_hash: str, packet: Packet + ) -> Optional[Packet]: + return None + + +class DirDataStore(DataStore): + def __init__( + self, + store_dir="./pod_data", + copy_files=True, + preserve_filename=True, + ) -> None: + self.store_dir = Path(store_dir) + # Create the data directory if it doesn't exist + self.store_dir.mkdir(parents=True, exist_ok=True) + self.copy_files = copy_files + self.preserve_filename = preserve_filename + + def memoize( + self, + store_name: str, + content_hash: int, + packet: Packet, + output_packet: Packet, + overwrite: bool = False, + ) -> Packet: + + packet_hash = hash_dict(packet) + output_dir = self.store_dir / store_name / content_hash / str(packet_hash) + info_path = output_dir / "_info.json" + + if info_path.exists() and not overwrite: + raise ValueError( + f"Entry for packet {packet} already exists, and will not be overwritten" + ) + else: + output_dir.mkdir(parents=True, exist_ok=True) + if self.copy_files: + new_output_packet = {} + # copy the files to the output directory + for key, value in output_packet.items(): + if self.preserve_filename: + relative_output_path = Path(value).name + if (output_dir / relative_output_path).exists(): + raise ValueError( + f"File {relative_output_path} already exists in {output_path}" + ) + else: + # preserve the suffix of the original if present + relative_output_path = key + Path(value).suffix + + output_path = output_dir / relative_output_path + if output_path.exists() and not overwrite: + # TODO: handle case where it's a directory + raise ValueError( + f"File {relative_output_path} already exists in {output_path}" + ) + shutil.copy(value, output_path) + # register the key with the new path + new_output_packet[key] = str(relative_output_path) + output_packet = new_output_packet + # store the packet in a json file + with open(info_path, "w") as f: + json.dump(output_packet, f) + logger.info(f"Stored output for packet {packet} at {output_path}") + + # retrieve back the memoized packet and return + # TODO: consider if we want to return the original packet or the memoized one + output_packet = self.retrieve_memoized(store_name, content_hash, packet) + if output_packet is None: + raise ValueError(f"Memoized packet {packet} not found after storing it") + + return output_packet + + def retrieve_memoized( + self, store_name: str, content_hash: str, packet: Packet + ) -> Optional[Packet]: + packet_hash = hash_dict(packet) + output_dir = self.store_dir / store_name / content_hash / str(packet_hash) + info_path = output_dir / "_info.json" + + if info_path.exists(): + with open(info_path, "r") as f: + output_packet = json.load(f) + # update the paths to be absolute + for key, value in output_packet.items(): + output_packet[key] = str(output_dir / value) + logger.info(f"Retrieved output for packet {packet} from {info_path}") + return output_packet + else: + logger.info(f"No memoized output found for packet {packet}") + return None + + def clear_store(self, store_name: str) -> None: + # delete the folder self.data_dir and its content + shutil.rmtree(self.store_dir / store_name) + + def clear_all_stores(self) -> None: + # delete the folder self.data_dir and its content + shutil.rmtree(self.store_dir) diff --git a/src/orcabridge/stream.py b/src/orcabridge/stream.py index 6c389bb2..a4c697a4 100644 --- a/src/orcabridge/stream.py +++ b/src/orcabridge/stream.py @@ -13,8 +13,9 @@ def __init__( generator_factory: Callable[[], Iterator[Tuple[Tag, Packet]]], tag_keys: Optional[List[str]] = None, packet_keys: Optional[List[str]] = None, + **kwargs, ) -> None: - super().__init__() + super().__init__(**kwargs) self.tag_keys = tag_keys self.packet_keys = packet_keys self.generator_factory = generator_factory diff --git a/src/orcabridge/tracker.py b/src/orcabridge/tracker.py index 6eedc63e..b612ba94 100644 --- a/src/orcabridge/tracker.py +++ b/src/orcabridge/tracker.py @@ -2,6 +2,7 @@ from typing import Dict, Collection, List import networkx as nx from .base import Operation, Invocation +import matplotlib.pyplot as plt class Tracker: @@ -14,7 +15,9 @@ def __init__(self) -> None: self.invocation_lut: Dict[Operation, Collection[Invocation]] = {} def record(self, invocation: Invocation) -> None: - self.invocation_lut.setdefault(invocation.operation, set()).add(invocation) + invocation_list = self.invocation_lut.setdefault(invocation.operation, []) + if invocation not in invocation_list: + invocation_list.append(invocation) def reset(self) -> Dict[Operation, Collection[Invocation]]: """ @@ -28,13 +31,16 @@ def generate_namemap(self) -> Dict[Invocation, str]: namemap = {} for operation, invocations in self.invocation_lut.items(): # if only one entry present, use the operation name alone - invocations = sorted(invocations) + if operation.label is not None: + node_label = operation.label + else: + node_label = str(operation) if len(invocations) == 1: - namemap[invocations[0]] = f"{operation}" + namemap[invocations[0]] = node_label continue # if multiple entries, use the operation name and index for idx, invocation in enumerate(invocations): - namemap[invocation] = f"{operation}_{idx}" + namemap[invocation] = f"{node_label}_{idx}" return namemap def activate(self) -> None: @@ -60,13 +66,31 @@ def generate_graph(self): for operation, invocations in self.invocation_lut.items(): for invocation in invocations: for upstream in invocation.streams: - # if upstream.source is not in the graph, add it - if upstream.source not in G: - G.add_node(upstream.source) - G.add_edge(upstream.source, invocation, stream=upstream) + # if upstream.invocation is not in the graph, add it + if upstream.invocation not in G: + G.add_node(upstream.invocation) + G.add_edge(upstream.invocation, invocation, stream=upstream) return G + def draw_graph(self): + G = self.generate_graph() + labels = self.generate_namemap() + + pos = nx.drawing.nx_agraph.graphviz_layout(G, prog="dot") + nx.draw( + G, + pos, + labels=labels, + node_size=2000, + node_color="lightblue", + with_labels=True, + font_size=10, + font_weight="bold", + arrowsize=20, + ) + plt.tight_layout() + def __enter__(self): self.activate() return self diff --git a/src/orcabridge/utils/hash.py b/src/orcabridge/utils/hash.py deleted file mode 100644 index 8cadd1c7..00000000 --- a/src/orcabridge/utils/hash.py +++ /dev/null @@ -1,22 +0,0 @@ -# a function to hash a dictionary of key value pairs into uuid -import hashlib -import uuid -from uuid import UUID -from typing import Dict, Union - -# arbitrary depth of nested dictionaries -T = Dict[str, Union[str, "T"]] - - -# TODO: implement proper recursive hashing -def hash_dict(d: T) -> UUID: - # Convert the dictionary to a string representation - dict_str = str(sorted(d.items())) - - # Create a hash of the string representation - hash_object = hashlib.md5(dict_str.encode()) - - # Convert the hash to a UUID - hash_uuid = uuid.UUID(hash_object.hexdigest()) - - return hash_uuid diff --git a/src/orcabridge/utils/name.py b/src/orcabridge/utils/name.py index 0d8fe76b..6a44380b 100644 --- a/src/orcabridge/utils/name.py +++ b/src/orcabridge/utils/name.py @@ -3,6 +3,10 @@ """ import re +import inspect +import pickle +import types +import ast def pascal_to_snake(name: str) -> str: @@ -24,3 +28,41 @@ def snake_to_pascal(name: str) -> str: # Split the string by underscores and capitalize each component components = name.split("_") return "".join(x.title() for x in components) + + +def get_function_signature(func): + """ + Returns a string representation of how the function arguments were defined. + Example output: f(a, b, c, d=0, **kwargs) + """ + sig = inspect.signature(func) + function_name = func.__name__ + + param_strings = [] + + for name, param in sig.parameters.items(): + # Handle different parameter kinds + if param.kind == param.POSITIONAL_ONLY: + formatted_param = f"{name}, /" + elif param.kind == param.POSITIONAL_OR_KEYWORD: + if param.default is param.empty: + formatted_param = name + else: + # Format the default value + default = repr(param.default) + formatted_param = f"{name}={default}" + elif param.kind == param.VAR_POSITIONAL: + formatted_param = f"*{name}" + elif param.kind == param.KEYWORD_ONLY: + if param.default is param.empty: + formatted_param = f"*, {name}" + else: + default = repr(param.default) + formatted_param = f"{name}={default}" + elif param.kind == param.VAR_KEYWORD: + formatted_param = f"**{name}" + + param_strings.append(formatted_param) + + params_str = ", ".join(param_strings) + return f"{function_name}({params_str})" diff --git a/src/orcabridge/utils/stream_utils.py b/src/orcabridge/utils/stream_utils.py index 7ae6b6ce..654a2562 100644 --- a/src/orcabridge/utils/stream_utils.py +++ b/src/orcabridge/utils/stream_utils.py @@ -24,6 +24,17 @@ def join_tags(tag1: Mapping[K, V], tag2: Mapping[K, V]) -> Optional[Mapping[K, V return joined_tag +def check_packet_compatibility(packet1: Packet, packet2: Packet) -> bool: + """ + Checks if two packets are compatible. If the packets have the same key, the value must be the same or False will be returned. + If the packets have different keys, they are compatible. + """ + for k in packet1.keys(): + if k in packet2 and packet1[k] != packet2[k]: + return False + return True + + def batch_tag(all_tags: Sequence[Tag]) -> Tag: """ Batches the tags together. Grouping values under the same key into a list.