From 46fba6a0375cd01f6a4383f48d54402789b586f8 Mon Sep 17 00:00:00 2001 From: jgostick Date: Sun, 23 Jul 2023 14:16:42 +0900 Subject: [PATCH 1/3] updating the init files in a few places with docstrings --- openpnm/algorithms/__init__.py | 9 +++++++++ openpnm/core/__init__.py | 34 +--------------------------------- openpnm/network/__init__.py | 8 ++++++++ openpnm/phase/__init__.py | 8 ++++++++ 4 files changed, 26 insertions(+), 33 deletions(-) diff --git a/openpnm/algorithms/__init__.py b/openpnm/algorithms/__init__.py index c16bf87de6..01739b2dab 100644 --- a/openpnm/algorithms/__init__.py +++ b/openpnm/algorithms/__init__.py @@ -1,3 +1,12 @@ +r""" +Algorithms +========== + +This module contains various algorithms for performing transport simulations + + +""" + from ._algorithm import * from ._transport import * diff --git a/openpnm/core/__init__.py b/openpnm/core/__init__.py index f064ace70b..ca385e27ab 100644 --- a/openpnm/core/__init__.py +++ b/openpnm/core/__init__.py @@ -2,8 +2,7 @@ Main classes of OpenPNM ======================= -This module contains the main classes from which all other major objects -(Network, Geometry, Physics, Phase, and Algorithm) derive. +This module contains the main classes from which all other major objects derive. The Base class -------------- @@ -12,37 +11,6 @@ and throats, applying labels, and managing the stored data. All OpenPNM object inherit from ``Base`` so possess these methods. ----- - -``Base`` objects, Networks, Phase, Algorithms, are assigned to all locations -in the domain. The ``Subdomain`` class is a direct descendent of ``Base`` -which has the added ability to be assigned to a subset of the domain. Objects -that inherit from ``Subdomain`` are Geomery and Physics. - -Boss objects refer to the Full Domain object it is associated with. For -Geomery objects this is the Network, and for Physics objects this is the -Phase that was specified during instantiation. - -The associations between an object and it's boss are tracked using labels in -the boss. So a Geometry object named ``geom1`` will put labels 'pore.geom1' -and 'throat.geom1' into the Network dictionary, with ``True`` values indicating -where ``geom1`` applies. - -The ModelsMixin class ---------------------- - -`Mixins `_ are a useful feature of Python -that allow a few methods to be added to a class that needs them. In OpenPNM, -the ability to store and run 'pore-scale' models is not needed by some objects -(Network, Algorithms), but is essential to Geometry, Physics, and Phase -objects. - -In addition to these methods, the ``ModelsMixin`` also adds a ``models`` -attribute to each object. This is a dictionary that stores the pore-scale -models and their associated parameters. When ``regenerate_models`` is called -the function and all the given parameters are retrieved from this dictionary -and run. - """ from ._models import * diff --git a/openpnm/network/__init__.py b/openpnm/network/__init__.py index ab5365e06a..2090014e4d 100644 --- a/openpnm/network/__init__.py +++ b/openpnm/network/__init__.py @@ -1,3 +1,11 @@ +r""" +Network +======= + +Contains network generators and the basic Network class + +""" + from ._network import Network from ._cubic import Cubic from ._demo import Demo diff --git a/openpnm/phase/__init__.py b/openpnm/phase/__init__.py index 5e87026bf6..0d5e403345 100644 --- a/openpnm/phase/__init__.py +++ b/openpnm/phase/__init__.py @@ -1,3 +1,11 @@ +r""" +Phase +===== + +This module contains classes generating and handling thermophysical properties + +""" + def _fetch_chemical_props(a): temp = {} k = 1.380649e-23 # Boltzmann constant From 9648d718aaf07f7c616aafc9edb0dae0d2271f61 Mon Sep 17 00:00:00 2001 From: jgostick Date: Wed, 22 Nov 2023 17:23:47 +0900 Subject: [PATCH 2/3] adding a network extraction example --- .../applications/network_extraction.ipynb | 516 ++++++++++++++++++ 1 file changed, 516 insertions(+) create mode 100644 examples/applications/network_extraction.ipynb diff --git a/examples/applications/network_extraction.ipynb b/examples/applications/network_extraction.ipynb new file mode 100644 index 0000000000..1c2e221962 --- /dev/null +++ b/examples/applications/network_extraction.ipynb @@ -0,0 +1,516 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "b236b8d3", + "metadata": {}, + "source": [ + "# Import an Extracted Network and Predict Transport Properties\n", + "\n", + "This example illustrates the process of both extracting a pore network from an image (that has already been binarized), then opening this image with OpenPNM to perform some simulations. The results of the simulations are compared to the known transport properties of the image and good agreement is obtained. " + ] + }, + { + "cell_type": "code", + "execution_count": 28, + "id": "e13b67cf", + "metadata": {}, + "outputs": [], + "source": [ + "import openpnm as op\n", + "import porespy as ps\n", + "import numpy as np\n", + "import os\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "markdown", + "id": "5dceea09", + "metadata": {}, + "source": [ + "Load a small image of Berea sandstone:" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "e83f28be", + "metadata": {}, + "outputs": [], + "source": [ + "path = Path(os.getcwd(),\n", + " '../../tests/fixtures/berea_100_to_300.npz')\n", + "data = np.load(path.resolve())\n", + "im = data['im']" + ] + }, + { + "cell_type": "markdown", + "id": "429e869f", + "metadata": {}, + "source": [ + "Note meta data for this image, which we'll use to compare the network predictions too later:" + ] + }, + { + "cell_type": "code", + "execution_count": 30, + "id": "0000735d", + "metadata": {}, + "outputs": [], + "source": [ + "data = {\n", + " 'shape': {\n", + " 'x': im.shape[0],\n", + " 'y': im.shape[1],\n", + " 'z': im.shape[2],\n", + " },\n", + " 'resolution': 5.345e-6,\n", + " 'porosity': 19.6,\n", + " 'permeability': {\n", + " 'Kx': 1360,\n", + " 'Ky': 1304,\n", + " 'Kz': 1193,\n", + " 'Kave': 1286,\n", + " },\n", + " 'formation factor': {\n", + " 'Fx': 23.12,\n", + " 'Fy': 23.99,\n", + " 'Fz': 25.22,\n", + " 'Fave': 24.08,\n", + " },\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "cb2f0380", + "metadata": {}, + "source": [ + "Perform extraction using `snow2` from `PoreSpy`. " + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "id": "b2a3d3d4", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "0it [00:00, ?it/s]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Extracting pore and throat properties: 0%| | 0/1420 [00:00\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + " # Properties Valid Values\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + " 2 throat.conns 2510 / 2510\n", + " 3 pore.coords 1420 / 1420\n", + " 4 pore.region_label 1420 / 1420\n", + " 5 pore.phase 1420 / 1420\n", + " 6 throat.phases 2510 / 2510\n", + " 7 pore.region_volume 1420 / 1420\n", + " 8 pore.equivalent_diameter 1420 / 1420\n", + " 9 pore.local_peak 1420 / 1420\n", + " 10 pore.global_peak 1420 / 1420\n", + " 11 pore.geometric_centroid 1420 / 1420\n", + " 12 throat.global_peak 2510 / 2510\n", + " 13 pore.inscribed_diameter 1420 / 1420\n", + " 14 pore.extended_diameter 1420 / 1420\n", + " 15 throat.inscribed_diameter 2510 / 2510\n", + " 16 throat.total_length 2510 / 2510\n", + " 17 throat.direct_length 2510 / 2510\n", + " 18 throat.perimeter 2510 / 2510\n", + " 19 pore.volume 1420 / 1420\n", + " 20 pore.surface_area 1420 / 1420\n", + " 21 throat.cross_sectional_area 2510 / 2510\n", + " 22 throat.equivalent_diameter 2510 / 2510\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + " # Labels Assigned Locations\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + " 2 pore.all 1420\n", + " 3 throat.all 2510\n", + " 4 pore.boundary 180\n", + " 5 pore.xmin 103\n", + " 6 pore.xmax 77\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n" + ] + } + ], + "source": [ + "print(pn)" + ] + }, + { + "cell_type": "markdown", + "id": "dcd1152d", + "metadata": {}, + "source": [ + "When we update the SNOW algorithm to `snow2` we removed many of the opinionated decision that the original version made. For instance, `snow2` does not pick which diameter should be the *definitive* one, so this decision needs to be made by the user once they can the network into OpenPNM. This is illustrated below:" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "f1aa6c19", + "metadata": {}, + "outputs": [], + "source": [ + "pn['pore.diameter'] = pn['pore.equivalent_diameter']\n", + "pn['throat.diameter'] = pn['throat.inscribed_diameter']\n", + "pn['throat.spacing'] = pn['throat.total_length']" + ] + }, + { + "cell_type": "markdown", + "id": "6cc07911", + "metadata": {}, + "source": [ + "The user also needs to decide which 'shape' to assume for the pores and throats, which impacts how the transport conductance values are computed. Here we use the `pyramids_and_cuboid` models:" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "f32e2f79", + "metadata": {}, + "outputs": [], + "source": [ + "pn.add_model(propname='throat.hydraulic_size_factors',\n", + " model=op.models.geometry.hydraulic_size_factors.pyramids_and_cuboids)\n", + "pn.add_model(propname='throat.diffusive_size_factors',\n", + " model=op.models.geometry.diffusive_size_factors.pyramids_and_cuboids)\n", + "pn.regenerate_models()" + ] + }, + { + "cell_type": "markdown", + "id": "20104b10", + "metadata": {}, + "source": [ + "More information about the \"size factors\" can be found in [this example](https://openpnm.org/examples/reference/simulations/size_factors_and_transport_conductance.html)." + ] + }, + { + "cell_type": "markdown", + "id": "590326c1", + "metadata": {}, + "source": [ + "One issue that can happen in extracted networks is that some pores can be isolated from the remainder of the network. The problem with 'disconnected networks' is discussed in detail [here](https://openpnm.org/examples/reference/networks/managing_clustered_networks.html). The end result is that we need to first check the network health, then deal with any problems:" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "7868024f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + "Key Value\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + "headless_throats []\n", + "looped_throats []\n", + "isolated_pores [456, 574, 783, 1023, 1115, 1158, 1179]\n", + "disconnected_pores [227, 246, 247, 319, 362, 456, 574, 783, 1023, 1115, 1158, 1179, 1181, 1189, 1204, 1345, 1346, 1347]\n", + "duplicate_throats []\n", + "bidirectional_throats []\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n" + ] + } + ], + "source": [ + "h = op.utils.check_network_health(pn)\n", + "print(h)" + ] + }, + { + "cell_type": "markdown", + "id": "6247f065", + "metadata": {}, + "source": [ + "The above output shows there are both 'single' isolated pores as well as 'groups' of pores that are disconnected. These all need to be \"trimmed\". We can use the list provided by `h['disconnected_pores']` directly in the `trim` function:" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "e6975f8c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + "Key Value\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n", + "headless_throats []\n", + "looped_throats []\n", + "isolated_pores []\n", + "disconnected_pores []\n", + "duplicate_throats []\n", + "bidirectional_throats []\n", + "――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\n" + ] + } + ], + "source": [ + "op.topotools.trim(network=pn, pores=h['disconnected_pores'])\n", + "h = op.utils.check_network_health(pn)\n", + "print(h)" + ] + }, + { + "cell_type": "markdown", + "id": "bdb57f8c", + "metadata": {}, + "source": [ + "Now we are ready to perform some simulations, so let's create a phase object to compute the thermophysical properties and transport conductances:" + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "id": "ebe5084d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
[17:21:55] WARNING  throat.entry_pressure was not run since the following property is missing:       _models.py:480\n",
+       "                    'throat.surface_tension'                                                                       \n",
+       "
\n" + ], + "text/plain": [ + "\u001b[2;36m[17:21:55]\u001b[0m\u001b[2;36m \u001b[0m\u001b[31mWARNING \u001b[0m throat.entry_pressure was not run since the following property is missing: \u001b[2m_models.py\u001b[0m\u001b[2m:\u001b[0m\u001b[2m480\u001b[0m\n", + "\u001b[2;36m \u001b[0m \u001b[32m'throat.surface_tension'\u001b[0m \u001b[2m \u001b[0m\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gas = op.phase.Phase(network=pn)\n", + "gas['pore.diffusivity'] = 1.0\n", + "gas['pore.viscosity'] = 1.0\n", + "gas.add_model_collection(op.models.collections.physics.basic)\n", + "gas.regenerate_models()" + ] + }, + { + "cell_type": "markdown", + "id": "609a0594", + "metadata": {}, + "source": [ + "Finally we can do a Fickian diffusion simulation to find the formation factor:" + ] + }, + { + "cell_type": "code", + "execution_count": 43, + "id": "1943e69d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The Formation factor of the extracted network is 25.098142684548733\n", + "The compares to a value of 23.12 from DNS\n" + ] + } + ], + "source": [ + "fd = op.algorithms.FickianDiffusion(network=pn, phase=gas)\n", + "fd.set_value_BC(pores=pn.pores('xmin'), values=1.0)\n", + "fd.set_value_BC(pores=pn.pores('xmax'), values=0.0)\n", + "fd.run()\n", + "dC = 1.0\n", + "L = (data['shape']['x'] + 6)*data['resolution']\n", + "A = data['shape']['y']*data['shape']['z']*data['resolution']**2\n", + "Deff = fd.rate(pores=pn.pores('xmin'))*(L/A)/dC\n", + "F = 1/Deff\n", + "print(f\"The Formation factor of the extracted network is {F[0]}\")\n", + "print(f\"The compares to a value of {data['formation factor']['Fx']} from DNS\")\n", + "np.testing.assert_allclose(F, data['formation factor']['Fx'], rtol=0.09)" + ] + }, + { + "cell_type": "markdown", + "id": "89681eea", + "metadata": {}, + "source": [ + "And a Stokes flow simulation to find Permeability coefficient:" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "4a500b65", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Permeability coefficient is 1.3181940680110902 Darcy\n", + "The compares to a value of 1.36 from DNS\n" + ] + } + ], + "source": [ + "sf = op.algorithms.StokesFlow(network=pn, phase=gas)\n", + "sf.set_value_BC(pores=pn.pores('xmin'), values=1.0)\n", + "sf.set_value_BC(pores=pn.pores('xmax'), values=0.0)\n", + "sf.run()\n", + "dP = 1.0\n", + "L = (data['shape']['x'] + 6)*data['resolution']\n", + "A = data['shape']['y']*data['shape']['z']*data['resolution']**2\n", + "K = sf.rate(pores=pn.pores('xmin'))*(L/A)/dP*1e12\n", + "print(f'Permeability coefficient is {K[0]} Darcy')\n", + "print(f\"The compares to a value of {data['permeability']['Kx']/1000} from DNS\")\n", + "np.testing.assert_allclose(K, data['permeability']['Kx']/1000, rtol=0.05)" + ] + }, + { + "cell_type": "markdown", + "id": "b8597118", + "metadata": {}, + "source": [ + "Both of the above simulations agree quite well with the known values for this sample. This is not always the case because network extraction is not always perfect. One problem that can occur is that the pore sizes are much too small due to anisotropic materials. In other cases the pores are too large and overlap each other too much. Basically, the user really need to double, then triple check that their extraction is 'sane'. It is almost mandatory to compare the extraction to some known values as we have done above. It's also a good idea visualize the network in Paraview, as explained [here](https://openpnm.org/examples/tutorials/10_visualization_options.html), which can reveal problems. " + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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 +} From 95c7a7e184b4e572be47f9b199e019f881d0d4f1 Mon Sep 17 00:00:00 2001 From: jgostick Date: Wed, 22 Nov 2023 17:39:33 +0900 Subject: [PATCH 3/3] fixing a pep8 error --- openpnm/phase/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpnm/phase/__init__.py b/openpnm/phase/__init__.py index 0d5e403345..fee8d12be4 100644 --- a/openpnm/phase/__init__.py +++ b/openpnm/phase/__init__.py @@ -6,6 +6,7 @@ """ + def _fetch_chemical_props(a): temp = {} k = 1.380649e-23 # Boltzmann constant