{ "cells": [ { "cell_type": "markdown", "metadata": { "lines_to_next_cell": 0 }, "source": [ "# Flow Identification" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "/code/code/canopyHydrodynamics\n", "/code/code/canopyHydrodynamics/docs/source/examples/logging_config.yml\n" ] } ], "source": [ "# For the purposes of this tutorial, we will turn off logging \n", "import logging\n", "import os\n", "logger = logging.getLogger()\n", "logger.setLevel(logging.CRITICAL)\n", "\n", "# Determines where configuration file is located\n", "# # file contains directory info and model input settings\n", "config_file = os.environ[\n", " \"CANOPYHYDRO_CONFIG\"\n", "] = f\"{os.getcwd()}/canopyhydro_config.toml\"\n", "log_config = os.environ[\"CANOPYHYDRO_LOG_CONFIG\"] = f\"{os.getcwd()}/docs/source/examples/logging_config.yml\"\n", "print(os.getcwd())\n", "print(os.environ[\"CANOPYHYDRO_LOG_CONFIG\"])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Graph Models" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "CanoPyHydro estimates flow partitioning by enriching its cylinder data with a graph based model - CylinderCollection.digraph. \\\n", "This graph representation simulates the tree's watershed and is used in tandem with a traversal algorithm to predict which percipitation partition each cylinder belongs to" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
2024.10.14 21:01:20.326 |MainThread | INFO | CylinderCollection.py:291 - from_csv() | model - Processing <_io.TextIOWrapper name='./data/input/5_SmallTree.csv' mode='r' encoding='UTF-8'>\n",
"\n"
],
"text/plain": [
"2024.10.14 21:01:20.326 |MainThread | INFO | CylinderCollection.py:291 - from_csv() | model - Processing <_io.TextIOWrapper name='./data/input/5_SmallTree.csv' mode='r' encoding='UTF-8'>\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 21:01:20.361 |MainThread | INFO | CylinderCollection.py:320 - from_csv() | model - ./data/input/5_SmallTree.csv initialized with 517 cylinders\n",
"\n"
],
"text/plain": [
"2024.10.14 21:01:20.361 |MainThread | INFO | CylinderCollection.py:320 - from_csv() | model - ./data/input/5_SmallTree.csv initialized with 517 cylinders\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 21:01:20.363 |MainThread | INFO | CylinderCollection.py:335 - project_cylinders() | model - Projection into XY axis begun for file 5_SmallTree.csv\n",
"\n"
],
"text/plain": [
"2024.10.14 21:01:20.363 |MainThread | INFO | CylinderCollection.py:335 - project_cylinders() | model - Projection into XY axis begun for file 5_SmallTree.csv\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 21:01:22.259 |MainThread | INFO | CylinderCollection.py:344 - project_cylinders() | model - Projection into XY axis complete for file 5_SmallTree.csv\n",
"\n"
],
"text/plain": [
"2024.10.14 21:01:22.259 |MainThread | INFO | CylinderCollection.py:344 - project_cylinders() | model - Projection into XY axis complete for file 5_SmallTree.csv\n"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# How Graphs are initialized\n",
"\n",
"import os\n",
"\n",
"os.environ[\"CANOPYHYDRO_CONFIG\"] = \"./canopyhydro_config.toml\"\n",
"from src.canopyhydro.CylinderCollection import CylinderCollection\n",
"\n",
"# Initializing a CylinderCollection object\n",
"myCollection = CylinderCollection()\n",
"\n",
"# Converting a specified file to a CylinderCollection object\n",
"myCollection.from_csv(\"5_SmallTree.csv\")\n",
"\n",
"# Requesting an plot of the tree projected onto the XY plane (birds-eye view)\n",
"myCollection.project_cylinders(\"XY\")\n",
"\n",
"# creating the digraph model\n",
"myCollection.initialize_digraph_from()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"These graphs are instances of 'DiGraph' objects from the ['networkx' package](https://networkx.org/documentation/stable/reference/index.html). For more information regarding their capabilities, reffer to the afforementioned linked documentation."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For our purposes, you just need to know that the edges of these graphs correspond to the cylinders in a CylinderCollection. So, when the 'find_flow_components' function traverses a graph, it is akin to walking along the branches of the tree."
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[(1, 0, {'cylinder': Cylinder( cyl_id=1.0, x=[-0.299115 -0.638138], y=[2.537844 2.995146], z=[-0.552 -0.354452], radius=0.531411, length=0.602566, branch_order=0.0, branch_id=0.0, volume=0.534583, parent_id=0.0, reverse_branch_order=67.0, segment_id=0.0})]\n",
"Cylinder( cyl_id=1.0, x=[-0.299115 -0.638138], y=[2.537844 2.995146], z=[-0.552 -0.354452], radius=0.531411, length=0.602566, branch_order=0.0, branch_id=0.0, volume=0.534583, parent_id=0.0, reverse_branch_order=67.0, segment_id=0.0\n"
]
}
],
"source": [
"# See how cylinder 0 (the base of the tree) correlates\n",
"# to edge 0 in our graph\n",
"print(myCollection.graph.edges(1,data=True))\n",
"print(myCollection.cylinders[1])"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Each of the edges in the graph, also have a direction, depending on their angle in space. In particular, each edge is directed in the direction in which intercepted water is presumed to flow. \n",
"\n",
"During traversal, each edge may only be traversed in the direction it has been assigned. For example, every cylinder in the stem is oriented towards its base so, just as is the case with water, the only direction in which we can traverse from trunk edge to trunk edge is downward."
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"The following cylinders are accessable from cylinder 5: \n",
"[4]\n",
"The following cylinders are accessable from cylinder 4: \n",
"[3]\n",
"The following cylinders are accessable from cylinder 3: \n",
"[2]\n",
"The following cylinders are accessable from cylinder 2: \n",
"[1]\n",
"The following cylinders are accessable from cylinder 1: \n",
"[0]\n"
]
}
],
"source": [
"# Looking at the cylinders accessable from our stem cylinders (i.e. their neighbors)\n",
"print('The following cylinders are accessable from cylinder 5: ')\n",
"print([x for x in myCollection.graph.neighbors(5)])\n",
"print('The following cylinders are accessable from cylinder 4: ')\n",
"print([x for x in myCollection.graph.neighbors(4)])\n",
"print('The following cylinders are accessable from cylinder 3: ')\n",
"print([x for x in myCollection.graph.neighbors(3)])\n",
"print('The following cylinders are accessable from cylinder 2: ')\n",
"print([x for x in myCollection.graph.neighbors(2)])\n",
"print('The following cylinders are accessable from cylinder 1: ')\n",
"print([x for x in myCollection.graph.neighbors(1)])\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Finding Flows"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This traversal (indeed much of anything at all to do with these graphs), is handled behind the scenes.\n",
"\n",
"The code below shows how a user can call 'find_flow_components' to trigger the use of the collections graph and therefore identify the paths water takes - the flows - in the tree canopy"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"2024.10.14 21:04:19.312 |MainThread | INFO | CylinderCollection.py:291 - from_csv() | model - Processing <_io.TextIOWrapper name='./data/input/5_SmallTree.csv' mode='r' encoding='UTF-8'>\n",
"\n"
],
"text/plain": [
"2024.10.14 21:04:19.312 |MainThread | INFO | CylinderCollection.py:291 - from_csv() | model - Processing <_io.TextIOWrapper name='./data/input/5_SmallTree.csv' mode='r' encoding='UTF-8'>\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 21:04:19.379 |MainThread | INFO | CylinderCollection.py:320 - from_csv() | model - ./data/input/5_SmallTree.csv initialized with 517 cylinders\n",
"\n"
],
"text/plain": [
"2024.10.14 21:04:19.379 |MainThread | INFO | CylinderCollection.py:320 - from_csv() | model - ./data/input/5_SmallTree.csv initialized with 517 cylinders\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 21:04:19.383 |MainThread | INFO | CylinderCollection.py:335 - project_cylinders() | model - Projection into XY axis begun for file 5_SmallTree.csv\n",
"\n"
],
"text/plain": [
"2024.10.14 21:04:19.383 |MainThread | INFO | CylinderCollection.py:335 - project_cylinders() | model - Projection into XY axis begun for file 5_SmallTree.csv\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 21:04:21.355 |MainThread | INFO | CylinderCollection.py:344 - project_cylinders() | model - Projection into XY axis complete for file 5_SmallTree.csv\n",
"\n"
],
"text/plain": [
"2024.10.14 21:04:21.355 |MainThread | INFO | CylinderCollection.py:344 - project_cylinders() | model - Projection into XY axis complete for file 5_SmallTree.csv\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 21:04:21.687 |MainThread | INFO | CylinderCollection.py:769 - find_flow_components() | model - 5_SmallTree.csv found to have 70 drip components\n",
"\n"
],
"text/plain": [
"2024.10.14 21:04:21.687 |MainThread | INFO | CylinderCollection.py:769 - find_flow_components() | model - 5_SmallTree.csv found to have 70 drip components\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"reached_End of find flows\n",
"Flow(num_cylinders=216.0, projected_area=16.51706021532694, surface_area=19.495074988550606, angle_sum=180.03288665020426, volume=3.9062960000000007, sa_to_vol=83646.49652248439, drip_node_id=0.0, drip_node_loc=(-0.299115, 2.537844, -0.598273))\n",
"Flow(num_cylinders=2, projected_area=0.0005597565466076131, surface_area=0.001924052712727801, angle_sum=-0.8861661864942612, volume=2e-06, sa_to_vol=1924.052712727801, drip_node_id=148, drip_node_loc=(1.736771, 2.700067, 14.216883))\n",
"Flow(num_cylinders=4, projected_area=0.0012679984136685896, surface_area=0.0043875954238933096, angle_sum=-1.1103646559240672, volume=4.9999999999999996e-06, sa_to_vol=3681.4517891642977, drip_node_id=150, drip_node_loc=(1.476039, 2.744678, 14.221036))\n",
"Flow(num_cylinders=1, projected_area=0.00026175097799692134, surface_area=0.0008268043545717619, angle_sum=0.20259148846207123, volume=1e-06, sa_to_vol=826.8043545717619, drip_node_id=159, drip_node_loc=(1.298384, 2.459679, 14.389457))\n",
"Flow(num_cylinders=3, projected_area=0.0008824774277666259, surface_area=0.0032463019367339413, angle_sum=-1.7087376025874943, volume=3e-06, sa_to_vol=3246.3019367339416, drip_node_id=164, drip_node_loc=(1.047176, 2.391643, 14.266511))\n",
"Flow(num_cylinders=2, projected_area=0.0006600186476422296, surface_area=0.0023746199311056493, angle_sum=0.534393748750537, volume=3e-06, sa_to_vol=1719.5271769974715, drip_node_id=175, drip_node_loc=(1.730164, 2.302889, 14.604732))\n",
"Flow(num_cylinders=2, projected_area=0.0004876054032408554, surface_area=0.001917581031861406, angle_sum=1.1425108485836808, volume=3e-06, sa_to_vol=1179.8879529087187, drip_node_id=179, drip_node_loc=(1.652252, 2.280224, 14.47647))\n",
"Flow(num_cylinders=1, projected_area=0.00041651108754572896, surface_area=0.0014212408085207547, angle_sum=-0.4465420656788475, volume=2e-06, sa_to_vol=710.6204042603774, drip_node_id=183, drip_node_loc=(1.455036, 2.239273, 14.49977))\n",
"Flow(num_cylinders=2, projected_area=0.0012733736410329834, surface_area=0.004064592575214475, angle_sum=-0.3665923044681696, volume=4.9999999999999996e-06, sa_to_vol=1666.127955868079, drip_node_id=187, drip_node_loc=(1.917529, 2.347151, 14.212591))\n",
"Flow(num_cylinders=0, projected_area=0.0, surface_area=0.0, angle_sum=0.0, volume=0.0, sa_to_vol=0.0, drip_node_id=189, drip_node_loc=(1.984224, 2.47876, 14.194845))\n",
"Flow(num_cylinders=6, projected_area=0.0022330283517565724, surface_area=0.007333246943678708, angle_sum=2.127804515876493, volume=9.999999999999999e-06, sa_to_vol=4446.06831715825, drip_node_id=193, drip_node_loc=(1.583993, 2.563848, 13.968024))\n",
"Flow(num_cylinders=1, projected_area=0.00020343043979622444, surface_area=0.0006478435290600194, angle_sum=0.2846026246124941, volume=1e-06, sa_to_vol=647.8435290600194, drip_node_id=198, drip_node_loc=(1.284843, 2.628993, 14.019867))\n",
"Flow(num_cylinders=3, projected_area=0.0007860084747630203, surface_area=0.003617512524682111, angle_sum=-2.5833501908977685, volume=4.9999999999999996e-06, sa_to_vol=2085.8447343876755, drip_node_id=208, drip_node_loc=(1.325472, 2.856421, 14.043133))\n",
"Flow(num_cylinders=2, projected_area=0.0006471769977986668, surface_area=0.0020395847825635657, angle_sum=0.26090730746353386, volume=2e-06, sa_to_vol=2039.584782563566, drip_node_id=209, drip_node_loc=(1.435887, 2.797216, 14.208758))\n",
"Flow(num_cylinders=8, projected_area=0.0017034251569991014, surface_area=0.007463890074178239, angle_sum=5.354426428330182, volume=8.999999999999999e-06, sa_to_vol=6831.314685414667, drip_node_id=214, drip_node_loc=(1.178362, 2.670965, 14.017909))\n",
"Flow(num_cylinders=3, projected_area=0.0012809041391582444, surface_area=0.004173213141212342, angle_sum=0.3207042500076585, volume=6e-06, sa_to_vol=2086.606570606171, drip_node_id=219, drip_node_loc=(1.152153, 2.815179, 13.969726))\n",
"Flow(num_cylinders=2, projected_area=0.0008570559793830896, surface_area=0.0027383692365015432, angle_sum=-0.10344238545184348, volume=4e-06, sa_to_vol=1369.1846182507718, drip_node_id=222, drip_node_loc=(1.607588, 2.540239, 13.949144))\n",
"Flow(num_cylinders=0, projected_area=0.0, surface_area=0.0, angle_sum=0.0, volume=0.0, sa_to_vol=0.0, drip_node_id=227, drip_node_loc=(1.868195, 2.555224, 13.948772))\n",
"Flow(num_cylinders=18, projected_area=0.005313162793692856, surface_area=0.021339283807471944, angle_sum=10.274852917465788, volume=2.6e-05, sa_to_vol=14370.354273860652, drip_node_id=232, drip_node_loc=(1.920238, 2.162247, 13.938462))\n",
"Flow(num_cylinders=7, projected_area=0.0029795345832851253, surface_area=0.013586461456943049, angle_sum=4.695171005225782, volume=1.8e-05, sa_to_vol=5378.31106616918, drip_node_id=242, drip_node_loc=(2.047486, 1.966398, 14.326444))\n"
]
}
],
"source": [
"# Finding the flows in the canopy\n",
"\n",
"import os\n",
"\n",
"os.environ[\"CANOPYHYDRO_CONFIG\"] = \"./canopyhydro_config.toml\"\n",
"from canopyhydro.CylinderCollection import CylinderCollection\n",
"\n",
"# Initializing a CylinderCollection object\n",
"myCollection = CylinderCollection()\n",
"\n",
"# Converting a specified file to a CylinderCollection object\n",
"myCollection.from_csv(\"5_SmallTree.csv\")\n",
"\n",
"# Requesting an plot of the tree projected onto the XY plane (birds-eye view)\n",
"myCollection.project_cylinders(\"XY\")\n",
"\n",
"# creating the digraph model\n",
"myCollection.initialize_digraph_from()\n",
"\n",
"# Traversing the graph, determining the fate of the water\n",
"# intercepted by each cylinder\n",
"myCollection.find_flow_components()\n",
"\n",
"# For each of the possible destinations for said water,\n",
"# summing the volume, area, etc of the contributing cylinders \n",
"myCollection.calculate_flows()\n",
"\n",
"for i in range(20):\n",
" print(myCollection.flows[i])\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"To summarize the above:\n",
" - We create a cylinder collection to hold the cylinders as well as ~1000 cylinders\\\n",
" that belong to that collection (Each with a surface area, length, etc.),\n",
" - For each Cylinder, we calculate the properties of its 'XY' projection\n",
" - We initialize a graph (stored in myCollection.Graph) for traversal\n",
" - We travers the graph, and determine where water intercepted by cylinders ends up\n",
" - For each contiguous group of cylinders *(Called a 'flow')*, we sum the total surface area, volumne etc. of each \\\n",
" cylinder"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"For more information on how the statistics calculated by 'calculate_flows' are used, see [statistics](statistics.ipynb)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Figures"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"The usage of the 'watershed_boundary' function is coverd in the [Concave Hulls and Watersheds](watersheds.md) example doc, so we will not go too into depth on them here. \n",
"\n",
"Watershed boundaries can also be calculated for filtered sections of the tree in much of the same way. \\\n",
"This is useful for creating figures and, perhaps more importantly, for generating statistics regarding the ground area covered by a tree's canopy"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/html": [
"2024.10.14 18:32:37.020 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:37.020 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 18:32:37.140 |MainThread | INFO | CylinderCollection.py:403 - draw() | model - 517 cylinders matched criteria\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:37.140 |MainThread | INFO | CylinderCollection.py:403 - draw() | model - 517 cylinders matched criteria\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"overlay [2024.10.14 18:32:37.143 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:37.143 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 18:32:37.437 |MainThread | ERROR | geometry.py:591 - draw_cyls() | model - Overlay must be a Polygon, a list of Polygons or coordinate list\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:37.437 |MainThread | ERROR | geometry.py:591 - draw_cyls() | model - Overlay must be a Polygon, a list of Polygons or coordinate list\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 18:32:37.525 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:37.525 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 18:32:37.617 |MainThread | INFO | CylinderCollection.py:403 - draw() | model - 517 cylinders matched criteria\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:37.617 |MainThread | INFO | CylinderCollection.py:403 - draw() | model - 517 cylinders matched criteria\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"overlay [2024.10.14 18:32:37.621 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:37.621 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 18:32:38.002 |MainThread | ERROR | geometry.py:591 - draw_cyls() | model - Overlay must be a Polygon, a list of Polygons or coordinate list\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:38.002 |MainThread | ERROR | geometry.py:591 - draw_cyls() | model - Overlay must be a Polygon, a list of Polygons or coordinate list\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"2024.10.14 18:32:56.455 |MainThread | INFO | CylinderCollection.py:403 - draw() | model - 517 cylinders matched criteria\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:56.455 |MainThread | INFO | CylinderCollection.py:403 - draw() | model - 517 cylinders matched criteria\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/html": [
"2024.10.14 18:32:56.461 |MainThread | WARNING | CylinderCollection.py:433 - draw() | model - No drip point locations found, running set_drip_points\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:56.461 |MainThread | WARNING | CylinderCollection.py:433 - draw() | model - No drip point locations found, running set_drip_points\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"name": "stdout",
"output_type": "stream",
"text": [
"overlay [[[1.583993, 1.920238, 2.047486, 1.797333, 1.421225, 1.317245, 1.520497], [2.563848, 2.162247, 1.966398, 2.576308, 3.038762, 3.130179, 2.822484]]]\n"
]
},
{
"data": {
"text/html": [
"2024.10.14 18:32:56.468 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n",
"\n"
],
"text/plain": [
"2024.10.14 18:32:56.468 |MainThread | INFO | geometry.py:563 - draw_cyls() | model - Plotting cylinder collection\n"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"