├── requirements ├── rtd_requirements.txt ├── conda_requirements.txt ├── pypi_requirements.txt └── test_requirements.txt ├── setup.cfg ├── docs ├── user_guide.rst ├── examples │ ├── index.rst │ ├── sierpinski_carpet.rst │ ├── tau.rst │ ├── 3d_blobs.rst │ └── open_space.rst ├── index.rst ├── Makefile ├── make.bat ├── conf.py └── getting_started.rst ├── .gitignore ├── pytrax ├── __init__.py └── __RandomWalk__.py ├── pytest.ini ├── codecov.yml ├── test ├── test_package.py └── unit │ └── test_RandomWalk.py ├── .travis.yml ├── LICENSE ├── setup.py ├── README.rst └── examples.py /requirements/rtd_requirements.txt: -------------------------------------------------------------------------------- 1 | numpydoc 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | license_file = LICENSE 3 | -------------------------------------------------------------------------------- /requirements/conda_requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | scipy 3 | matplotlib 4 | scikit-image 5 | numba 6 | -------------------------------------------------------------------------------- /requirements/pypi_requirements.txt: -------------------------------------------------------------------------------- 1 | imageio 2 | gitpython 3 | tqdm 4 | array_split 5 | pyevtk 6 | -------------------------------------------------------------------------------- /docs/user_guide.rst: -------------------------------------------------------------------------------- 1 | .. _user_guide: 2 | 3 | ========== 4 | User Guide 5 | ========== 6 | 7 | .. automodule:: pytrax 8 | -------------------------------------------------------------------------------- /requirements/test_requirements.txt: -------------------------------------------------------------------------------- 1 | codecov 2 | coverage 3 | pep8 4 | pytest 5 | pytest-cache 6 | pytest-cov 7 | pytest-pep8 8 | gitpython 9 | porespy 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Compiled python modules. 2 | *.pyc 3 | 4 | # Setuptools distribution folder. 5 | /dist/ 6 | build/ 7 | 8 | # Python egg metadata, regenrated from source files in setuptools 9 | /*.egg-info 10 | .cache/ 11 | .coverage 12 | -------------------------------------------------------------------------------- /pytrax/__init__.py: -------------------------------------------------------------------------------- 1 | r''' 2 | ======= 3 | pytrax 4 | ======= 5 | 6 | pytrax is a pure python implementation of a random walk code with parallel 7 | processing capabilities. The purpose of the package is to enable the quick and 8 | simple estimation of the toruosity tensor of an image. 9 | ''' 10 | from .__RandomWalk__ import RandomWalk 11 | __version__ = "0.1.2" 12 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | python_files = *.py 3 | python_classes = *Test 4 | python_functions = test_* 5 | pep8maxlinelength = 85 6 | pep8ignore = 7 | E402 # Module level import not at top of file 8 | E226 # Space around arithmetic operators 9 | 10 | addopts = 11 | --doctest-modules 12 | --ignore=setup.py 13 | 14 | norecursedirs = 15 | .git 16 | archive 17 | bin 18 | build 19 | dist 20 | locals 21 | -------------------------------------------------------------------------------- /docs/examples/index.rst: -------------------------------------------------------------------------------- 1 | 2 | ############################################################################### 3 | **Examples** 4 | ############################################################################### 5 | 6 | The following 4 examples demostrate the use of pytrax for different types of image: 7 | 8 | .. toctree:: 9 | :maxdepth: 3 10 | 11 | open_space.rst 12 | tau.rst 13 | sierpinski_carpet.rst 14 | 3d_blobs.rst 15 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | branch: master 3 | 4 | coverage: 5 | precision: 2 6 | round: down 7 | range: "50...100" 8 | 9 | status: 10 | project: 11 | default: 12 | target: auto 13 | threshold: null 14 | branches: null 15 | 16 | patch: 17 | default: 18 | target: auto 19 | branches: null 20 | 21 | comment: 22 | layout: "header, diff, changes, sunburst, uncovered" 23 | branches: null 24 | behavior: default 25 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. pytrax documentation master file, created by 2 | sphinx-quickstart on Sat Jan 13 02:44:38 2018. 3 | 4 | Welcome to pytrax's documentation! 5 | ================================== 6 | 7 | .. toctree:: 8 | :maxdepth: 3 9 | 10 | getting_started.rst 11 | user_guide.rst 12 | examples/index.rst 13 | 14 | 15 | ================== 16 | Function Reference 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /test/test_package.py: -------------------------------------------------------------------------------- 1 | import pytrax as pt 2 | import git 3 | 4 | 5 | class PackageTest(): 6 | def setup_class(self): 7 | pass 8 | 9 | def test_version_number_and_git_tag_agree(self): 10 | repo = git.Repo(search_parent_directories=True) 11 | tag = repo.git.describe("--tags") 12 | tag = tag.strip('vV') # Remove 'v' or 'V' from tag if present 13 | tag = tag.split('-')[0] # Remove hash from tag number if present 14 | assert pt.__version__ == tag 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = pytrax 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: python 4 | 5 | python: 6 | - "3.6" 7 | 8 | before_install: 9 | - export DISPLAY=:99.0 10 | - sh -e /etc/init.d/xvfb start 11 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 12 | - bash miniconda.sh -b -p $HOME/miniconda 13 | - export PATH="$HOME/miniconda/bin:$PATH" 14 | 15 | install: 16 | - conda install --yes python=$TRAVIS_PYTHON_VERSION --file requirements/conda_requirements.txt 17 | - pip install -r requirements/pypi_requirements.txt 18 | - pip install -r requirements/test_requirements.txt 19 | - python setup.py install 20 | 21 | script: pytest --pep8 --cov=./ 22 | 23 | notifications: 24 | email: false 25 | 26 | after_success: 27 | - codecov 28 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | set SPHINXPROJ=pytrax 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 PMEAL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from distutils.util import convert_path 4 | 5 | sys.path.append(os.getcwd()) 6 | 7 | try: 8 | from setuptools import setup 9 | except ImportError: 10 | from distutils.core import setup 11 | 12 | main_ = {} 13 | ver_path = convert_path('pytrax/__init__.py') 14 | with open(ver_path) as f: 15 | for line in f: 16 | if line.startswith('__version__'): 17 | exec(line, main_) 18 | 19 | setup( 20 | name='pytrax', 21 | description='A random walk for estimating the toruosity tensor of images', 22 | version=main_['__version__'], 23 | classifiers=['Development Status :: 3 - Alpha', 24 | 'License :: OSI Approved :: MIT License', 25 | 'Programming Language :: Python', 26 | 'Topic :: Scientific/Engineering', 27 | 'Topic :: Scientific/Engineering :: Physics'], 28 | packages=['pytrax'], 29 | install_requires=['numpy', 30 | 'scipy', 31 | 'matplotlib', 32 | 'tqdm'], 33 | author='Tom Tranter', 34 | author_email='t.g.tranter@gmail.com', 35 | url='https://pytrax.readthedocs.io/en/latest/', 36 | project_urls={ 37 | 'Documentation': 'https://pytrax.readthedocs.io/en/latest/', 38 | 'Source': 'https://github.com/PMEAL/pytrax', 39 | 'Tracker': 'https://github.com/PMEAL/pytrax/issues', 40 | }, 41 | ) 42 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | 2 | .. image:: https://badge.fury.io/py/pytrax.svg 3 | :target: https://pypi.python.org/pypi/pytrax 4 | 5 | .. image:: https://travis-ci.org/PMEAL/pytrax.svg?branch=master 6 | :target: https://travis-ci.org/PMEAL/pytrax 7 | 8 | .. image:: https://codecov.io/gh/PMEAL/pytrax/branch/master/graph/badge.svg 9 | :target: https://codecov.io/gh/PMEAL/pytrax 10 | 11 | .. image:: https://readthedocs.org/projects/pytrax/badge/?version=latest 12 | :target: http://pytrax.readthedocs.org/ 13 | 14 | ############################################################################### 15 | Overview of pytrax 16 | ############################################################################### 17 | 18 | *pytrax* is an implementation of a random walk to calculate the tortuosity tensor of images. 19 | 20 | =============================================================================== 21 | Example Usage 22 | =============================================================================== 23 | 24 | The following code block illustrates how to use pytrax to perform a random walk simulation in open space, view the results and plot the mean square displacement to get the tortuosity: 25 | 26 | .. code-block:: python 27 | 28 | >>> import pytrax as pt 29 | >>> import numpy as np 30 | >>> image = np.ones([3, 3]) 31 | >>> rw = pt.RandomWalk(image) 32 | >>> rw.run(1000, 1000) 33 | >>> rw.plot_walk_2d() 34 | >>> rw.calc_msd() 35 | >>> rw.plot_msd() 36 | -------------------------------------------------------------------------------- /test/unit/test_RandomWalk.py: -------------------------------------------------------------------------------- 1 | import porespy as ps 2 | import pytrax as pt 3 | import scipy as sp 4 | import os 5 | import matplotlib.pyplot as plt 6 | 7 | 8 | class SimulationTest(): 9 | def setup_class(self): 10 | self.l = 100 11 | self.im = ps.generators.overlapping_spheres(shape=[self.l, self.l], 12 | radius=5, 13 | porosity=0.55) 14 | self.blobs = ps.generators.blobs([self.l, self.l, self.l]) 15 | self.rw = pt.RandomWalk(self.blobs) 16 | self.blobs_2d = ps.generators.blobs([self.l, self.l]).astype(int) 17 | self.rw_2d = pt.RandomWalk(self.blobs_2d, seed=True) 18 | 19 | def test_random_walk(self): 20 | self.rw.run(nt=1000, nw=100, stride=1) 21 | assert sp.shape(self.rw.real_coords) == (1000, 100, 3) 22 | 23 | def test_plot_msd(self): 24 | self.rw.plot_msd() 25 | pass 26 | 27 | def test_random_walk_2d(self): 28 | self.rw_2d.run(nt=1000, nw=100, same_start=True, stride=100) 29 | assert sp.shape(self.rw_2d.real_coords) == (10, 100, 2) 30 | 31 | def test_plot_walk_2d(self): 32 | self.rw_2d.plot_walk_2d(data='w') 33 | assert hasattr(self.rw_2d, 'im_big') 34 | assert sp.sum(self.rw_2d.im_big > self.rw_2d.nw) == 0 35 | plt.close('all') 36 | 37 | def test_export(self): 38 | cwd = os.getcwd() 39 | self.rw_2d.export_walk(sub='temp', image=self.rw_2d.im, sample=10) 40 | subdir = os.path.join(cwd, 'temp') 41 | assert os.path.exists(subdir) 42 | file_list = os.listdir(subdir) 43 | # 10 coordinate files based on the stride and number of steps + image 44 | assert len(file_list) == 11 45 | # Delete all files and folder 46 | for file in file_list: 47 | fp = os.path.join(subdir, file) 48 | os.remove(fp) 49 | os.rmdir(subdir) 50 | 51 | def test_seed(self): 52 | # rw_2d was initialized with seed = True, this should mean running it 53 | # repeatedly produces the same movements 54 | self.rw_2d.run(nt=1000, nw=100, same_start=True) 55 | temp_coords = self.rw_2d.real_coords.copy() 56 | self.rw_2d.run(nt=1000, nw=100, same_start=True) 57 | assert sp.allclose(self.rw_2d.real_coords, temp_coords) 58 | 59 | def test_axial_density_plot(self): 60 | self.rw.axial_density_plot(time=0, axis=0) 61 | plt.close('all') 62 | 63 | def test_rw_analytics(self): 64 | w_list = [100] 65 | t_list = [100] 66 | self.rw_2d.run_analytics(w_list, t_list, fname='test.csv', num_proc=1) 67 | cwd = os.getcwd() 68 | fpath = os.path.join(cwd, 'test.csv') 69 | assert os.path.exists(fpath) 70 | os.remove(fpath) 71 | plt.close('all') 72 | 73 | if __name__ == '__main__': 74 | t = SimulationTest() 75 | t.setup_class() 76 | t.test_random_walk() 77 | t.test_plot_msd() 78 | t.test_random_walk_2d() 79 | t.test_plot_walk_2d() 80 | t.test_export() 81 | t.test_seed() 82 | t.test_axial_density_plot() 83 | t.test_rw_analytics() 84 | -------------------------------------------------------------------------------- /docs/examples/sierpinski_carpet.rst: -------------------------------------------------------------------------------- 1 | .. _sierpinski_carpet: 2 | 3 | 4 | ############################################################################### 5 | Example 3: The Sierpinksi Carpet 6 | ############################################################################### 7 | 8 | This example will demonstrate the principle of calculating the tortuosity from a porous image with lower porosity. 9 | 10 | .. contents:: Topics Covered in this Tutorial 11 | 12 | **Learning Objectives** 13 | 14 | #. Generate a fractal image with self-similarity at different length scales 15 | #. Run the RandomWalk for a fractal image 16 | #. Produce some visualization 17 | 18 | =============================================================================== 19 | Instantiating the RandomWalk class 20 | =============================================================================== 21 | 22 | Assuming that you are now familiar with how to import and instantiate the simulation objects we now define a function to produce the Sierpinski carpet: 23 | 24 | .. code-block:: python 25 | 26 | >>> def tileandblank(image, n): 27 | >>> if n > 0: 28 | >>> n -= 1 29 | >>> shape = np.asarray(np.shape(image)) 30 | >>> image = np.tile(image, (3, 3)) 31 | >>> image[shape[0]:2*shape[0], shape[1]:2*shape[1]] = 0 32 | >>> image = tileandblank(image, n) 33 | >>> return image 34 | >>> im = np.ones([1, 1], dtype=int) 35 | >>> im = tileandblank(im, 4) 36 | 37 | =============================================================================== 38 | Running and ploting the walk 39 | =============================================================================== 40 | 41 | We're now ready to instantiate and run the walk, this time lets test the power of the program and run it for 1 million walkers using 10 parallel processes (please make sure you have that many avaialble): 42 | 43 | .. code-block:: python 44 | 45 | >>> rw = pt.RandomWalk(im) 46 | >>> rw.run(nt=2500, nw=1e6, same_start=False, stride=5, num_proc=10) 47 | >>> rw.plot_walk_2d() 48 | 49 | The simulation should take no longer than a minute when running on a single process and should produce a plot like this: 50 | 51 | .. image:: https://i.imgur.com/SnzeDNv.png 52 | :align: center 53 | 54 | Like the open space example the pattern is radial because the image is isotropic. We can see the reflected domains with the large black squares at the center. This time the walkers have escaped the original domain and some have travelled entirely through the next set of neighboring reflected domains and reached a third relflection. This signifies that the domain has been probed effectively over time. The MSD plot is as follows: 55 | 56 | .. code-block:: python 57 | 58 | >>> rw.plot_msd() 59 | 60 | .. image:: https://imgur.com/6QPCXYq.png 61 | :align: center 62 | 63 | The ``plot_msd`` function shows that mean square displacement and axial displacement are all the same and increase linearly with time. However, unlike the open space example the slope of the curve is less than one. This is because the walkers are impeded by the solid objects and it takes a longer time to go around them than in open space. The toruosity is calculated as the reciprocal of the MSD slope and is equal in both directions and very straight signifying that we have chosen an adequate number of walkers and steps. 64 | -------------------------------------------------------------------------------- /docs/examples/tau.rst: -------------------------------------------------------------------------------- 1 | .. _tau: 2 | 3 | 4 | ############################################################################### 5 | Example 2: The Tortuosity of Tau 6 | ############################################################################### 7 | 8 | This example shows the code working on a pseudo-porous media and explains some of the things to be careful of. 9 | 10 | .. contents:: Topics Covered in this Tutorial 11 | 12 | **Learning Objectives** 13 | 14 | #. Practice the code on a real image 15 | #. Explain the significance of the number of walkers and steps. 16 | #. Visualize the domain reflection 17 | 18 | =============================================================================== 19 | Obtaining the image 20 | =============================================================================== 21 | 22 | As with the previous example, the first thing to do is to import the packages that we are going to use including some packages for importing the image we will use: 23 | 24 | .. code-block:: python 25 | 26 | >>> import pytrax as ps 27 | >>> import numpy as np 28 | >>> import urllib.request as ur 29 | >>> from io import BytesIO 30 | >>> import matplotlib.pyplot as plt 31 | >>> from PIL import Image 32 | 33 | Next we are going to grab an image using its URL and make some modifications to prepare it for pytrax: 34 | 35 | .. code-block:: python 36 | 37 | >>> url = 'https://i.imgur.com/nrEJRDf.png' 38 | >>> file = BytesIO(ur.urlopen(url).read()) 39 | >>> im = np.asarray(Image.open(file))[:, :, 3] == 0 40 | >>> im = im.astype(int) 41 | >>> im = np.pad(im, pad_width=50, mode='constant', constant_values=1) 42 | 43 | The image is a .png and has 4 layers: r, g, b and alpha. We use the alpha layer which sets the contrast and make the image binary by setting the zero-valued pixels to ``True`` then converting to a type ``int``. Finally we pad the image with additional pore space which is designated as 1. 44 | 45 | .. code-block:: python 46 | 47 | >>> rw = pt.RandomWalk(image=ima, seed=False 48 | 49 | =============================================================================== 50 | Running and ploting the walk 51 | =============================================================================== 52 | 53 | We're now ready to run the walk setting the number of walkers to be 1,000 and the number of steps to be 20,000: 54 | 55 | .. code-block:: python 56 | 57 | >>> rw.run(nt=20000, nw=1000) 58 | >>> rw.plot_walk_2d() 59 | 60 | The following 2D plot is produced: 61 | 62 | .. image:: https://i.imgur.com/mSQZVku.png 63 | :align: center 64 | 65 | This time it is clear how the domain reflections happen with the original in the center surrounded by reflections of the Tau symbol along the two principal axes. Even though the number of steps taken by the walkers seems quite large, the number of pixels in the image is also large and the length of the average walk is no larger than the original domain. Let's plot the MSD to get a little more information: 66 | 67 | .. code-block:: python 68 | 69 | >>> rw.plot_msd() 70 | 71 | .. image:: https://i.imgur.com/B3L8dqc.png 72 | :align: center 73 | 74 | The ``plot_msd`` function shows that mean square displacement and axial displacement are increasing over time but not in such a linear fashion as the previous example. There also appears to be some anisotropy as the 0th axis has a different tortuosity to the 1st axis. The MSD plot shows that we should run the simulation again for longer as the length of the walk is about the same length as the average pore size. It really needs to be at least 4 or 5 times longer so that the effect of hitting pore walls is evenly distributed among the walkers over time. The number of walkers should also be increased as we can see that the MSD is not particularly smooth and creating a larger ensemble average will give better results. Try running the same simulation again but increasing both ``nt`` and ``nw`` by a factor of 4. 75 | -------------------------------------------------------------------------------- /examples.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Dec 7 23:40:31 2017 4 | Run the RandomWalk Examples 5 | @author: Tom Tranter 6 | """ 7 | try: 8 | import porespy as ps 9 | except ImportError: 10 | print('PoreSpy must be installed for the examples. ' + 11 | 'Use pip or conda to install it') 12 | raise 13 | 14 | import pytrax as pt 15 | import time 16 | import numpy as np 17 | import matplotlib.pyplot as plt 18 | import urllib.request as ur 19 | from io import BytesIO 20 | from PIL import Image 21 | 22 | save_figures = True 23 | global_stride = 1 24 | plt.close('all') 25 | if __name__ == '__main__': 26 | # Change number to run different example of include many in list 27 | for image_run in [0, 1, 2]: 28 | if image_run == 0: 29 | # Open space 30 | im = np.ones([3, 3], dtype=np.int) 31 | fname = 'open_' 32 | num_t = 10000 33 | num_w = 10000 34 | stride = 10 35 | elif image_run == 1: 36 | # Load tau test image 37 | url = 'https://i.imgur.com/nrEJRDf.png' 38 | file = BytesIO(ur.urlopen(url).read()) 39 | im = np.asarray(Image.open(file))[:, :, 3] == 0 40 | im = im.astype(np.int) 41 | im = np.pad(im, pad_width=50, mode='constant', constant_values=1) 42 | fname = 'tau_' 43 | # Number of time steps and walkers 44 | num_t = 20000 45 | num_w = 1000 46 | stride = 20 47 | elif image_run == 2: 48 | # Generate a Sierpinski carpet by tiling an image and blanking the 49 | # Middle tile recursively 50 | def tileandblank(image, n): 51 | if n > 0: 52 | n -= 1 53 | shape = np.asarray(np.shape(image)) 54 | image = np.tile(image, (3, 3)) 55 | image[shape[0]:2*shape[0], shape[1]:2*shape[1]] = 0 56 | image = tileandblank(image, n) 57 | return image 58 | 59 | im = np.ones([1, 1], dtype=np.int) 60 | im = tileandblank(im, 4) 61 | fname = 'sierpinski_' 62 | # Number of time steps and walkers 63 | num_t = 2500 64 | num_w = 100000 65 | stride = 5 66 | elif image_run == 3: 67 | # Make an anisotropic image of 3D blobs 68 | im = ps.generators.blobs(shape=[300, 300, 300], porosity=0.5, 69 | blobiness=[1, 2, 5]).astype(np.int) 70 | fname = 'blobs_' 71 | # Number of time steps and walkers 72 | num_t = 10000 73 | num_w = 10000 74 | stride = 100 75 | elif image_run == 4: 76 | im = ps.generators.cylinders([300, 300, 300], 77 | radius=10, 78 | ncylinders=100, 79 | phi_max=90, 80 | theta_max=90).astype(np.int) 81 | fname = 'random_cylinders_' 82 | num_t = 10000 83 | num_w = 10000 84 | stride = 10 85 | elif image_run == 5: 86 | im = ps.generators.cylinders([300, 300, 300], 87 | radius=10, 88 | ncylinders=100, 89 | phi_max=0, 90 | theta_max=0).astype(np.int) 91 | fname = 'aligned_cylinders_' 92 | num_t = 10000 93 | num_w = 10000 94 | stride = 10 95 | print('Running Example: '+fname.strip('_')) 96 | # Override all strides 97 | if global_stride is not None: 98 | stride = global_stride 99 | # Track time of simulation 100 | st = time.time() 101 | rw = pt.RandomWalk(im, seed=False) 102 | rw.run(num_t, num_w, same_start=False, stride=stride, num_proc=8) 103 | print('run time', time.time()-st) 104 | rw.calc_msd() 105 | # Plot mean square displacement 106 | rw.plot_msd() 107 | dpi = 600 108 | if save_figures: 109 | rw._save_fig(fname+'msd.png', dpi=dpi) 110 | if rw.dim == 2: 111 | # Plot the longest walk 112 | rw.plot_walk_2d(w_id=np.argmax(rw.sq_disp[-1, :]), data='t') 113 | 114 | if save_figures: 115 | rw._save_fig(fname+'longest.png', dpi=dpi) 116 | # Plot all the walks 117 | rw.plot_walk_2d(check_solid=True, data='t') 118 | if save_figures: 119 | rw._save_fig(fname+'all.png', dpi=dpi) 120 | else: 121 | if save_figures: 122 | # export to paraview 123 | rw.export_walk(image=rw.im, sample=1) 124 | -------------------------------------------------------------------------------- /docs/examples/3d_blobs.rst: -------------------------------------------------------------------------------- 1 | .. _3d_blobs: 2 | 3 | 4 | ############################################################################### 5 | Example 4: Anisotropic 3D Blobs 6 | ############################################################################### 7 | 8 | This example will demonstrate the principle of calculating the tortuosity from a 3D porous image with anisotropy. 9 | 10 | .. contents:: Topics Covered in this Tutorial 11 | 12 | **Learning Objectives** 13 | 14 | #. Generate an anisotropic 3D imgage with the porespy package 15 | #. Run the RandomWalk for the image showing the anisotropic tortuosity 16 | #. Export the results and visualize with Paraview 17 | 18 | =============================================================================== 19 | Generating the Image with porespy 20 | =============================================================================== 21 | 22 | In this example we generate a 3D anisotropic image with another PMEAL package called `porespy `_ which can be installed with ``pip``. The image will be 300 voxels cubed, have a porosity of 0.5 and the blobs will be stretched in each principle direction by a different factor or [1, 2, 5]: 23 | 24 | .. code-block:: python 25 | 26 | >>> import porespy as ps 27 | >>> im = ps.generators.blobs(shape=[300], porosity=0.5, blobiness=[1, 2, 5]).astype(int) 28 | 29 | =============================================================================== 30 | Running and exporting the walk with Paraview 31 | =============================================================================== 32 | 33 | We're now ready to instantiate and run the walk: 34 | 35 | .. code-block:: python 36 | 37 | >>> rw = pt.RandomWalk(im) 38 | >>> rw.run(nt=1e4, nw=1e4, same_start=False, stride=100, num_proc=10) 39 | >>> rw.plot_msd() 40 | 41 | The simulation should take no longer than 45 seconds when running on a single process and should produce an MSD plot like this: 42 | 43 | .. image:: https://imgur.com/cnAbJ29.png 44 | :align: center 45 | 46 | Unlike the previous examples, the MSD plot clearly shows that the axial square displacement is different along the different axes and this produces a tortuosity that approximately scales with the blobiness of the image. The image is three-dimensional and so we cannot use the 2D plotting function to visualize the walks, instead we make use of the export function to produce a set of files that can be read with Paraview: 47 | 48 | .. code-block:: python 49 | 50 | >>> rw.export_walk(image=rw.im, sample=1) 51 | 52 | This arguments ``image`` sets the image to be exported to be the original domain, optionally we could leave the argument as ``None`` in which case only the walker coordinated would be exported or we could set it to ``rw.im_big`` to export the domain encompassing all the walks. Caution should be exercised when using this function as larger domains produce very large files. The second argument ``sample`` tells the function to down-sample the coordinate data by this factor. We have already set a stride to only record every 100 steps which is useful for speeding up calculating the MSD and so the sample is left as the default of 1. The export function also accepts a ``path``, ``sub`` and ``prefix`` argument which lets you specify where to save the data and what to name the subfolder at this path location and also a prefix for the filenames to be saved. By default the current working directory is used as the path, ``data`` is used for the subdirectory and ``rw_`` is used as a prefix. After running the function, which takes a few seconds to complete, inspect your current working directory which should contain the exported data. There should be 101 files in the data folder: A small file containing the coordinates of each walker at each recorded time step with extention ``.vtu`` and larger file named ``rw_image.vti``. To load and view these files in Paraview take the following steps: 53 | 54 | #. Open the ``rw_image.vti`` file and press Apply 55 | #. Change Representation to Surface and under Coloring change the variable from ``Solid Color`` to ``image_data`` 56 | #. Apply a Threshold filter to this object (this may take a little while to process) and set the Maximum of the threshold to be 0 (again processing of this step takes some time) 57 | #. Now you can rotate the image and inspect only the solid portions in 3D 58 | #. Open the ``.vtu`` as a group and click Apply and a bunch of white dots should appear on the screen displaying the walker starting locations. 59 | #. Pressing the green play button will now animate the walkers. 60 | #. If you desire to see the paths taken by each walker saved on the screen then select the coords object and open the filters menu then select ``TemporalParticlesToPathlines``. Change the ``Mask Points`` property to 1, ``Max Track Length`` to exceed the total number of steps and ``Max Step Distance`` to exceed the stride. 61 | #. To produce animations adjust settings in the Animations view. 62 | 63 | The following images can be produced: 64 | 65 | 66 | .. image:: https://imgur.com/682ofAo.png 67 | :align: center 68 | 69 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # pytrax documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Jan 13 02:44:38 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.mathjax'] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'pytrax' 50 | copyright = '2018, PMEAL' 51 | author = 'PMEAL' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '0.1.0' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '0.1.0' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ---------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | html_theme = 'sphinx_rtd_theme' 87 | 88 | # Theme options are theme-specific and customize the look and feel of a theme 89 | # further. For a list of options available for each theme, see the 90 | # documentation. 91 | # 92 | # html_theme_options = {} 93 | 94 | # Add any paths that contain custom static files (such as style sheets) here, 95 | # relative to this directory. They are copied after the builtin static files, 96 | # so a file named "default.css" will overwrite the builtin "default.css". 97 | html_static_path = ['_static'] 98 | 99 | 100 | # -- Options for HTMLHelp output ------------------------------------------ 101 | 102 | # Output file base name for HTML help builder. 103 | htmlhelp_basename = 'pytraxdoc' 104 | 105 | 106 | # -- Options for LaTeX output --------------------------------------------- 107 | 108 | latex_elements = { 109 | # The paper size ('letterpaper' or 'a4paper'). 110 | # 111 | # 'papersize': 'letterpaper', 112 | 113 | # The font size ('10pt', '11pt' or '12pt'). 114 | # 115 | # 'pointsize': '10pt', 116 | 117 | # Additional stuff for the LaTeX preamble. 118 | # 119 | # 'preamble': '', 120 | 121 | # Latex figure (float) alignment 122 | # 123 | # 'figure_align': 'htbp', 124 | } 125 | 126 | # Grouping the document tree into LaTeX files. List of tuples 127 | # (source start file, target name, title, 128 | # author, documentclass [howto, manual, or own class]). 129 | latex_documents = [ 130 | (master_doc, 'pytrax.tex', 'pytrax Documentation', 131 | 'PMEAL', 'manual'), 132 | ] 133 | 134 | 135 | # -- Options for manual page output --------------------------------------- 136 | 137 | # One entry per manual page. List of tuples 138 | # (source start file, name, description, authors, manual section). 139 | man_pages = [ 140 | (master_doc, 'pytrax', 'pytrax Documentation', 141 | [author], 1) 142 | ] 143 | 144 | 145 | # -- Options for Texinfo output ------------------------------------------- 146 | 147 | # Grouping the document tree into Texinfo files. List of tuples 148 | # (source start file, target name, title, author, 149 | # dir menu entry, description, category) 150 | texinfo_documents = [ 151 | (master_doc, 'pytrax', 'pytrax Documentation', 152 | author, 'pytrax', 'One line description of project.', 153 | 'Miscellaneous'), 154 | ] 155 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | .. _getting_started: 2 | 3 | =============== 4 | Getting Started 5 | =============== 6 | 7 | ------------ 8 | Requirements 9 | ------------ 10 | 11 | **Software:** pytrax only relies on a few core python modules including Numpy and matplotlib, among others. These packages can be difficult to install from source, so it's highly recommended to download the Anaconda Python Distrubution install for your platform, which will install all of these packages for you (and many more!). Once this is done, you can then run the installation of pytrax as described in the next section. 12 | 13 | **Hardware:** Although there are no technical requirements, it must be noted that working with large images (>500**3) requires a substantial computer, with perhaps 16 or 32 GB of RAM. You can work on small images using normal computers to develop work flows, then size up to a larger computer for application on larger images. 14 | 15 | ------------ 16 | Installation 17 | ------------ 18 | 19 | pytrax is available on the Python Package Index (PyPI) and can be installed with the usual ``pip`` command as follows: 20 | 21 | .. code-block:: none 22 | 23 | pip install pytrax 24 | 25 | When installing in this way, the source code is stored somewhere deep within the Python installation folder, so it's not convenient to play with or alter the code. If you wish to customize the code, then it might be better to download the source code from github into a personal directory (e.g C:\\pytrax) then install as follows: 26 | 27 | 28 | .. code-block:: none 29 | 30 | pip install -e C:\pytrax 31 | 32 | The '-e' argument means that the package is 'editable' so any changes you make to the code will be available the next time that pytrax is imported. 33 | 34 | ----------- 35 | Basic Usage 36 | ----------- 37 | 38 | To use pytrax simply import it at the Python prompt: 39 | 40 | .. code-block:: python 41 | 42 | >>> import pytrax as pt 43 | 44 | At the moment the package is very lightweight and it is expected that users have their own images to analyze. For testing purposes we make use of another one of our packages called PoreSpy. As well as having lots of image analysis tools for porous media, PoreSpy also contains an image ``generators`` module to produce a sample image as follows: 45 | 46 | .. code-block:: python 47 | 48 | >>> import porespy as ps 49 | >>> image = ps.generators.blobs(shape=[100, 100]) 50 | 51 | Running the random walk simulation to estimate the tortuosity tensor is then completed with a few extra commands: 52 | 53 | .. code-block:: python 54 | 55 | >>> rw = pt.RandomWalk(image) 56 | >>> rw.run(nt=1000, nw=1000, same_start=False, stride=1, num_proc=None) 57 | 58 | Here the RandomWalk class is instantiated with the image that we generated and run with some parameters: ``nt`` is the number of time steps, ``nw`` is the number of walkers, ``same_start`` sets the walkers to have the same starting position in the image and is ``False`` (by default), ``stride`` is the number of steps between successive saves for calculations and output and ``num_proc`` is the number of parallel processors to use (defaulting to half the number available). 59 | 60 | ---------------- 61 | Plotting Results 62 | ---------------- 63 | 64 | pytrax has some built in plotting functionality to plot the coordinates of the walkers and also the mean square displacement vs. time which can be viewed with the following commands: 65 | 66 | .. code-block:: python 67 | 68 | >>> rw.plot_walk_2d(check_solid=True, data='t') 69 | >>> rw.plot_msd() 70 | 71 | The first plotting function plots the image and the walker steps and is colored by time step, changing the ``data`` argument to be ``w`` changes the color to walker index. The ``check_solid`` argument checks that the solid voxels in the image are not walked upon which is useful when changes to the code are made as a quick sense check. The walkers are free to leave the original image providing that there is a porous pathway at the edges. When this happens they are treated to be travelling in a reflected domain and the plotting function also displays this. The second plotting function shows the mean and axial square displacement and applies linear regression to fit a straight line with intercept through zero. The gradient of the slope is inversely proportional to the tortuosity of the image in that direction. This follows the definition of tortuosity being the ratio of diffusivity in open space to diffusivity in the porous media. 72 | 73 | ----------------- 74 | Exporting Results 75 | ----------------- 76 | 77 | For 3D images the ``plot_walk_2d`` function can be used to view a slice of the walk and image, however, for better visualization it is recommended to use the export function and view the results in Paraview. A tutorial on how to do this is provided but the following function will export the image and walker data: 78 | 79 | .. code-block:: python 80 | 81 | >>> rw.export_walk(image=None, path=None, sub='data', prefix='rw_', sample=1) 82 | 83 | The ``image`` argument optionally lets you export the original image or the larger reflected image which are both stored on the rw object as ``rw.im`` and ``rw.im_big``, respectively. Leaving the argument as ``None`` will not export any image. ``path`` is the directory to save the data and when set to ``None`` will default to the current working directory, ``sub`` creates a subfolder under the path directory to save the data in and defaults to ``data``, ``prefix`` gives all the data a prefix and defaults to ``rw_`` and finally ``sample`` is a down-sampling factor which in addition to the stride function in the run command will only output walker coordinates for time steps that are multiples of this number. 84 | -------------------------------------------------------------------------------- /docs/examples/open_space.rst: -------------------------------------------------------------------------------- 1 | .. _open_space: 2 | 3 | 4 | ############################################################################### 5 | Example 1: A Random Walk in Open Space 6 | ############################################################################### 7 | 8 | This example is the simplest use of pytrax but also illustrates an underlying theory of diffusion which is that the mean square displacement of diffusing particles should grow linearly with time. 9 | 10 | .. contents:: Topics Covered in this Tutorial 11 | 12 | **Learning Objectives** 13 | 14 | #. Introduce the main class in the pytrax package, RandomWalk 15 | #. Run the RandomWalk for an image devoid of solid features to demonstrate the principles of the package 16 | #. Produce some visualization 17 | 18 | 19 | .. hint:: Python and Numpy Tutorials 20 | 21 | * pytrax is written in Python. One of the best guides to learning Python is the set of Tutorials available on the `official Python website `_). The web is literally overrun with excellent Python tutorials owing to the popularity and importance of the language. The official Python website also provides `an long list of resources `_ 22 | 23 | * For information on using Numpy, Scipy and generally doing scientific computing in Python checkout the `Scipy lecture notes `_. The Scipy website also offers as solid introduction to `using Numpy arrays `_. 24 | 25 | * The `Stackoverflow `_ website is an incredible resource for all computing related questions, including simple usage of Python, Scipy and Numpy functions. 26 | 27 | * For users more familiar with Matlab, there is a `Matlab-Numpy cheat sheet `_ that explains how to translate familiar Matlab commands to Numpy. 28 | 29 | =============================================================================== 30 | Instantiating the RandomWalk class 31 | =============================================================================== 32 | 33 | The first thing to do is to import the packages that we are going to use. Start by importing pytrax and the Numpy package: 34 | 35 | .. code-block:: python 36 | 37 | >>> import pytrax as pt 38 | >>> import numpy as np 39 | 40 | Next, in order to instaintiate the RandomWalk class from the pytrax package we first need to make an binary image where 1 denotes open space available to walk on and 0 denotes a solid obstacle. In this example we are going set our walkers to explore open space and so we can build an image only containing ones. The size of the image doesn't matter, which will be explained later but for demostration purposes we will make the image two-dimensional: 41 | 42 | .. code-block:: python 43 | 44 | >>> image = np.ones(shape=[3, 3], dtype=int) 45 | >>> rw = pt.RandomWalk(image=image, seed=False) 46 | 47 | We now have a RandomWalk object instantiated with the handle ``rw``. 48 | 49 | * The ``image`` argument sets the domain of the random walk and is stored on the object for all future simulations. 50 | 51 | * The ``seed`` argument controls whether the random number generators in the class are seeded which means that they will always behave the same when running the walk multiple times with the same parameters. This is useful for debugging but under all other circumstances as it results in only semi-random walks. 52 | 53 | =============================================================================== 54 | Running and ploting the walk 55 | =============================================================================== 56 | 57 | We're now ready to run the walk: 58 | 59 | .. code-block:: python 60 | 61 | >>> rw.run(nt=1000, nw=1000, same_start=False, stride=1, num_proc=1) 62 | >>> rw.plot_walk_2d() 63 | 64 | * ``nt`` is the number of steps that each walker will take 65 | 66 | * ``nw`` is the number of walkers to run concurrently 67 | 68 | * ``same_start`` is a boolean controlling whether the walkers all start at the same spot in the image. By default this is False and this will result in the walkers being started randomly at different locations. 69 | 70 | * ``stride`` is a reporting variable and does not affect the length of the strides taken by each walker (which are always one voxel at a time), but controls how many steps are saved for plotting and export. 71 | 72 | * ``num_proc`` sets the number of parallel processors to run. By default half the number available will be used and the walkers will be divided into batches and run in parallel. 73 | 74 | Each walk is completley independent of any other which sounds strange as Brownian motion is intended to simulate particle-particle interactions. However, we are not simulating this directly but encompassing the behaviour by randomly changing the direction of the steps taken on an individual walker basis. The second line should produce a plot showing all the walkers colored by timestep, like the one below: 75 | 76 | .. image:: https://imgur.com/74bYtJb.png 77 | :align: center 78 | 79 | The appearance of the plot tells us a few things about the process. The circular shape and uniform color shows that the walkers are evenly distributed and have walked in each direction in approximately equal proportions. To display this information more clearly we can plot the mean square displacement (MSD) over time: 80 | 81 | .. code-block:: python 82 | 83 | >>> rw.plot_msd() 84 | 85 | .. image:: https://imgur.com/2YjL0C9.png 86 | :align: center 87 | 88 | The ``plot_msd`` function shows that mean square displacement and axial displacement are all the same and increase linearly with time. A neat explanation of why this is can be found in this paper http://rsif.royalsocietypublishing.org/cgi/doi/10.1098/rsif.2008.0014 which derives the probability debnsity function for the location of a walker after time ``t`` as: 89 | 90 | ..math:: 91 | 92 | p(x,t) = \frac{1}{\sqrt{4\piDt}exp\left(\frac{-x^2}{4Dt}\right) 93 | 94 | Which is the fundamental solution to the diffusion equation and so walker positions follow a Gaussian distribution which spreads out and has the property that MSD increases linearly with time. pytrax makes use of this property to calculate the toruosity of the image domain by using the definition that tortuosity is the ratio of diffusion in a porous space compared with that in open space. This simply translates to the reciprocal of the slope of the MSD which is unity for open space, as shown by this example. As a result of plotting the MSD we have some extra data on the RandomWalk object and we can use it to find the walker that travelled the furthest: 95 | 96 | .. code-block:: python 97 | 98 | >>> rw.plot_walk_2d(w_id=np.argmax(rw.sq_disp[-1, :]), data='t') 99 | 100 | .. image:: https://imgur.com/WwRFWGJ.png 101 | :align: center 102 | 103 | The attribute ``rw.sq_disp`` is the square displacement for all walkers at all stride steps which is all steps for this example. Indexing ``-1`` takes the last row and indexing ``:`` takes the whole row, the numpy function ``argmax`` returns the index of the largest value and this integer value is used for the ``w_id`` argument of the plotting function which stands for walker index. 104 | 105 | =============================================================================== 106 | A note on the image boundaries 107 | =============================================================================== 108 | 109 | As mentioned previously, the size of the image we used to instantiate the RandomWalk class for this example did not matter. This is because the walkers are allowed to leave the domain if there is a path in open space allowing them to do so. The image is treated as a representative sample of some larger medium and if the walkers were not allowed to leave the original domain their MSD's would eventually plateau and this would not be represtative of the general diffusive behaviour. The plotting function is actually showing an array of real and reflected domains with the original at the center, although this is hard to see with this example as there are no solid features and so the reflected images are identical to the original. We will discuss more on this later. 110 | -------------------------------------------------------------------------------- /pytrax/__RandomWalk__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | @author: Tom Tranter, Matt Lam, Matt Kok, Jeff Gostick 4 | PMEAL lab, University of Waterloo, Ontario, Canada 5 | 6 | Random Walker Code 7 | """ 8 | 9 | import numpy as np 10 | import matplotlib 11 | import matplotlib.pyplot as plt 12 | from tqdm import tqdm 13 | import os 14 | import time 15 | import csv 16 | from concurrent.futures import ProcessPoolExecutor 17 | import gc 18 | from pyevtk.hl import imageToVTK, pointsToVTK 19 | cmap = matplotlib.cm.viridis 20 | 21 | 22 | class RandomWalk(): 23 | r''' 24 | The RandomWalk class implements a simple vectorized version of a random 25 | walker. The image that is analyzed can be 2 or 3 dimensional and the run 26 | method can take an arbitrary number of steps and walkers. 27 | Walker starting positions can be set to the same point or to different ones 28 | chosen at random. 29 | The image is duplicated and flipped a number of times for visualization as 30 | this represents the real path the walker would have taken if it had not 31 | been confined to the bounds of the image. 32 | The mean square displacement is calculated and the gradient of the msd 33 | when plotted over time is equal to 1/tau, the tortuosity factor. 34 | The image data and walker co-ordinates can be exported for visualization 35 | in paraview. 36 | A simple 2d slice can also be viewed directly using matplotlib. 37 | Currently walkers do not travel along diagonals. 38 | Running walkers in parallel by setting num_proc is possible to speed up 39 | calculations as wach walker path is completely independent of the others. 40 | ''' 41 | 42 | def __init__(self, image, seed=False): 43 | r''' 44 | Get image info and make a bigger periodically flipped image for viz 45 | 46 | Parameters 47 | ---------- 48 | image: ndarray of int 49 | 2D or 3D image with 1 denoting pore space and 0 denoting solid 50 | 51 | seed: bool 52 | Determines whether to seed the random number generators so that 53 | Simulation is repeatable. Warning - This results in only semi- 54 | random walks so should only be used for debugging 55 | 56 | Examples 57 | -------- 58 | 59 | Creating a RandomWalk object: 60 | 61 | >>> import porespy as ps 62 | >>> import pytrax as pt 63 | >>> im = ps.generators.blobs([100, 100]) 64 | >>> rw = pt.RandomWalk(im) 65 | >>> rw.run(nt=1000, nw=100) 66 | ''' 67 | self.im = image 68 | self.shape = np.array(np.shape(self.im)) 69 | self.dim = len(self.shape) 70 | self.solid_value = 0 71 | self.seed = seed 72 | self._get_wall_map(self.im) 73 | self.data = {} 74 | 75 | def _rand_start(self, image, num=1): 76 | r''' 77 | Get a number of start points in the pore space of the image 78 | 79 | Parameters 80 | ---------- 81 | image: ndarray of int 82 | 2D or 3D image with 1 denoting pore space and 0 denoting solid 83 | num: int 84 | number of unique starting points to return 85 | ''' 86 | inds = np.argwhere(image != self.solid_value) 87 | if self.seed: 88 | np.random.seed(1) 89 | try: 90 | choice = np.random.choice(np.arange(0, len(inds), 1), 91 | num, 92 | replace=False) 93 | except ValueError: 94 | choice = np.random.choice(np.arange(0, len(inds), 1), 95 | num, 96 | replace=True) 97 | return inds[choice] 98 | 99 | def _get_wall_map(self, image): 100 | r''' 101 | Function savesc a wall map and movement vectors. 102 | This is referred to later when random walker moves in a particular 103 | direction to detect where the walls are. 104 | 105 | Parameters 106 | ---------- 107 | image: ndarray of int 108 | 2D or 3D image with 1 denoting pore space and 0 denoting solid 109 | ''' 110 | # Make boolean map where solid is True 111 | solid = image.copy() == self.solid_value 112 | solid = solid.astype(bool) 113 | moves = [] 114 | for axis in range(self.dim): 115 | ax_list = [] 116 | for direction in [-1, 1]: 117 | # Store the direction of the step in an array for later use 118 | step = np.arange(0, self.dim, 1, dtype=int) == axis 119 | step = step.astype(int) * direction 120 | ax_list.append(step) 121 | moves.append(ax_list) 122 | # Save inverse of the solid wall map for fluid map 123 | self.wall_map = ~solid 124 | self.moves = np.asarray(moves) 125 | 126 | def check_wall(self, walkers, move): 127 | r''' 128 | The walkers are an array of coordinates of the image, 129 | the wall map is a boolean map of the image rolled in each direction. 130 | directions is an array referring to the movement up or down an axis 131 | and is used to increment the walker coordinates if a wall is not met 132 | 133 | Parameters 134 | ---------- 135 | walkers: ndarray of int and shape [nw, dim] 136 | the current coordinates of the walkers 137 | move: ndarray of int and shape [nw, dim] 138 | the vector of the next move to be made by the walker 139 | inds: array of int and shape [nw] 140 | the index of the wall map corresponding to the move vector 141 | ''' 142 | next_move = walkers + move 143 | if self.dim == 2: 144 | move_ok = self.wall_map[next_move[:, 0], 145 | next_move[:, 1]] 146 | elif self.dim == 3: 147 | move_ok = self.wall_map[next_move[:, 0], 148 | next_move[:, 1], 149 | next_move[:, 2]] 150 | return ~move_ok 151 | 152 | def check_edge(self, walkers, axis, move, real): 153 | r''' 154 | Check to see if next move passes out of the domain 155 | If so, zero walker move and update the real velocity direction. 156 | Walker has remained stationary in the small image but tranisioned 157 | between real and reflected domains. 158 | Parameters 159 | ---------- 160 | walkers: ndarray of int and shape [nw, dim] 161 | the current coordinates of the walkers 162 | move: ndarray of int and shape [nw, dim] 163 | the vector of the next move to be made by the walker 164 | inds: array of int and shape [nw] 165 | the index of the wall map corresponding to the move vector 166 | ''' 167 | next_move = walkers + move 168 | move_real = move.copy() 169 | # Divide walkers into two groups, those moving postive and negative 170 | # Check lower edge 171 | axis = axis.flatten() 172 | w_id = np.arange(len(walkers)) 173 | shift = np.zeros_like(axis) 174 | # Check lower edge 175 | l_hit = next_move[w_id, axis] < 0 176 | shift[l_hit] = -1 177 | # Check upper edge 178 | u_hit = next_move[w_id, axis] >= self.shape[axis] 179 | shift[u_hit] = 1 180 | # Combine again and update arrays 181 | hit = np.logical_or(l_hit, u_hit) 182 | 183 | if np.any(hit): 184 | ax = axis[hit] 185 | real[hit, ax] *= -1 186 | # walker in the original image stays stationary 187 | move[hit, ax] = 0 188 | # walker in the real image passes through an interface between 189 | # original and flipped along the axis of travel 190 | # the transition step is reversed as it the reality of travel 191 | # both cancel to make the real walker follow the initial move 192 | move_real[hit, ax] *= -1 193 | 194 | return move, move_real, real 195 | 196 | def _get_starts(self, same_start=False): 197 | r''' 198 | Start walkers in the pore space at random location 199 | same_start starts all the walkers at the same spot if True and at 200 | different ones if False 201 | Parameters 202 | ---------- 203 | same_start: bool 204 | determines whether to start all the walkers at the same coordinate 205 | ''' 206 | if not same_start: 207 | walkers = self._rand_start(self.im, num=self.nw) 208 | else: 209 | w = self._rand_start(self.im, num=1).flatten() 210 | walkers = np.tile(w, (self.nw, 1)) 211 | return walkers 212 | 213 | def _run_walk(self, walkers): 214 | r''' 215 | Run the walk in self contained way to enable parallel processing for 216 | batches of walkers 217 | ''' 218 | # Number of walkers in this batch 219 | nw = len(walkers) 220 | walkers = np.asarray(walkers) 221 | wr = walkers.copy() 222 | # Array to keep track of whether the walker is travelling in a real 223 | # or reflected image in each axis 224 | real = np.ones_like(walkers) 225 | # real_coords = np.ndarray([self.nt, nw, self.dim], dtype=int) 226 | real_coords = [] 227 | for t in range(self.nt): 228 | # Random velocity update 229 | # Randomly select an axis to move along for each walker 230 | if self.seed: 231 | np.random.seed(self.seeds[t]) 232 | ax = np.random.randint(0, self.dim, nw) 233 | # Randomly select a direction positive = 1, negative = 0 index 234 | if self.seed: 235 | np.random.seed(self.seeds[-t]) 236 | pn = np.random.randint(0, 2, nw) 237 | # Get the movement 238 | m = self.moves[ax, pn] 239 | # Reflected velocity (if edge is hit) 240 | m, mr, real = self.check_edge(walkers, ax, m, real) 241 | # Check for wall hits and zero both movements 242 | # Cancel moves that hit walls - effectively walker travels half way 243 | # across, hits a wall, bounces back and results in net zero move 244 | wall_hit = self.check_wall(walkers, m) 245 | if np.any(wall_hit): 246 | m[wall_hit] = 0 247 | mr[wall_hit] = 0 248 | # Reflected velocity in real direction 249 | wr += mr*real 250 | walkers += m 251 | if t % self.stride == 0: 252 | real_coords.append(wr.copy()) 253 | return real_coords 254 | 255 | def run(self, nt=1000, nw=1, same_start=False, stride=1, num_proc=1): 256 | r''' 257 | Main run loop over nt timesteps and nw walkers. 258 | same_start starts all the walkers at the same spot if True and at 259 | different ones if False. 260 | 261 | Parameters 262 | ---------- 263 | nt: int (default = 1000) 264 | the number of timesteps to run the simulation for 265 | nw: int (default = 1) 266 | he vector of the next move to be made by the walker 267 | same_start: bool 268 | determines whether to start all the walkers at the same coordinate 269 | stride: int 270 | save coordinate data every stride number of timesteps 271 | num_proc: int (default 1, None - uses half available) 272 | number of concurrent processes to start running. Please make sure 273 | that the run method is c a __main__ method when using 274 | multiprocessing. 275 | ''' 276 | self.nt = int(nt) 277 | self.nw = int(nw) 278 | self.stride = stride 279 | record_t = int(self.nt/stride) 280 | # Get starts 281 | walkers = self._get_starts(same_start) 282 | if self.seed: 283 | # Generate a seed for each timestep 284 | np.random.seed(1) 285 | self.seeds = np.random.randint(0, self.nw, self.nt) 286 | real_coords = np.ndarray([record_t, self.nw, self.dim], dtype=int) 287 | # Default to run in parallel with half the number of available procs 288 | if num_proc is None: 289 | num_proc = int(os.cpu_count()/2) 290 | if num_proc > 1 and self.nw >= num_proc: 291 | # Run in parallel over multiple CPUs 292 | batches = self._chunk_walkers(walkers, num_proc) 293 | pool = ProcessPoolExecutor(max_workers=num_proc) 294 | mapped_coords = list(pool.map(self._run_walk, batches)) 295 | pool.shutdown() 296 | del pool 297 | # Put coords back together 298 | si = 0 299 | for mc in mapped_coords: 300 | mnw = np.shape(mc)[1] 301 | real_coords[:, si: si + mnw, :] = mc.copy() 302 | si = si + mnw 303 | else: 304 | # Run in serial 305 | real_coords = np.asarray(self._run_walk(walkers.tolist())) 306 | 307 | self.real_coords = real_coords 308 | 309 | def _chunk_walkers(self, walkers, num_chunks): 310 | r''' 311 | Helper function to divide the walkers into batches for pool-processing 312 | ''' 313 | num_walkers = len(walkers) 314 | n = int(np.floor(num_walkers / num_chunks)) 315 | l = walkers.tolist() 316 | chunks = [l[i:i + n] for i in range(0, len(l), n)] 317 | return chunks 318 | 319 | def calc_msd(self): 320 | r''' 321 | Calculate the mean square displacement 322 | ''' 323 | disp = self.real_coords[:, :, :] - self.real_coords[0, :, :] 324 | self.axial_sq_disp = disp**2 325 | self.sq_disp = np.sum(disp**2, axis=2) 326 | self.msd = np.mean(self.sq_disp, axis=1) 327 | self.axial_msd = np.mean(self.axial_sq_disp, axis=1) 328 | 329 | def _add_linear_plot(self, x, y, descriptor=None, color='k'): 330 | r''' 331 | Helper method to add a line to the msd plot 332 | ''' 333 | a, res, _, _ = np.linalg.lstsq(x, y, rcond=-1) 334 | tau = 1/a[0] 335 | SStot = np.sum((y - y.mean())**2) 336 | rsq = 1 - (np.sum(res)/SStot) 337 | label = ('Tau: ' + str(np.around(tau, 3)) + 338 | ', R^2: ' + str(np.around(rsq, 3))) 339 | print(label) 340 | plt.plot(x, a[0]*x, color+'--', label=label) 341 | self.data[descriptor + '_tau'] = tau 342 | self.data[descriptor + '_rsq'] = rsq 343 | 344 | def plot_msd(self): 345 | r''' 346 | Plot the mean square displacement for all walkers vs timestep 347 | And include a least squares regression fit. 348 | ''' 349 | self.calc_msd() 350 | self.data = {} 351 | fig, ax = plt.subplots(figsize=[6, 6]) 352 | ax.set(aspect=1, xlim=(0, self.nt), ylim=(0, self.nt)) 353 | x = np.arange(0, self.nt, self.stride)[:, np.newaxis] 354 | plt.plot(x, self.msd, 'k-', label='msd') 355 | print('#'*30) 356 | print('Square Displacement:') 357 | self._add_linear_plot(x, self.msd, 'Mean', color='k') 358 | colors = ['r', 'g', 'b'] 359 | for ax in range(self.dim): 360 | print('Axis ' + str(ax) + ' Square Displacement Data:') 361 | data = self.axial_msd[:, ax]*self.dim 362 | plt.plot(x, data, colors[ax]+'-', label='asd '+str(ax)) 363 | self._add_linear_plot(x, data, 'axis_'+str(ax), colors[ax]) 364 | plt.legend() 365 | 366 | def _check_big_bounds(self): 367 | r''' 368 | Helper function to check the maximum displacement and return the number 369 | of image copies needed to build a big image large enough to display all 370 | the walks 371 | ''' 372 | max_disp = np.max(np.max(np.abs(self.real_coords), axis=0), axis=0)+1 373 | num_domains = np.ceil(max_disp / self.shape) 374 | num_copies = int(np.max(num_domains))-1 375 | return num_copies 376 | 377 | def export_walk(self, image=None, path=None, sub='data', prefix='rw_', 378 | sample=1): 379 | r''' 380 | Export big image to vti and walker coords to vtu 381 | 382 | Parameters 383 | ---------- 384 | image: ndarray of int size (Default is None) 385 | Can be used to export verisons of the image 386 | path: string (default = None) 387 | the filepath to save the data, defaults to current working dir 388 | prefix: string (default = 'rw_) 389 | a string prefix for all the data 390 | sample: int (default = 1) 391 | used to down-sample the number of walkers to export by this factor 392 | ''' 393 | if path is None: 394 | path = os.getcwd() 395 | if sub is not None: 396 | subdir = os.path.join(path, sub) 397 | # if it doesn't exist, make it 398 | if not os.path.exists(subdir): 399 | os.makedirs(subdir) 400 | path = subdir 401 | if image is not None: 402 | if len(np.shape(image)) == 2: 403 | image = image[:, :, np.newaxis] 404 | im_fp = os.path.join(path, prefix+'image') 405 | imageToVTK(im_fp, cellData={'image_data': image}) 406 | # number of zeros to fill the file index 407 | zf = np.int(np.ceil(np.log10(self.nt*10))) 408 | w_id = np.arange(0, self.nw, sample) 409 | nw = len(w_id) 410 | time_data = np.ascontiguousarray(np.zeros(nw, dtype=int)) 411 | if self.dim == 2: 412 | z_coords = np.ascontiguousarray(np.ones(nw, dtype=int)) 413 | coords = self.real_coords 414 | for t in range(np.shape(coords)[0]): 415 | st = self.stride*t 416 | time_data.fill(st) 417 | x_coords = np.ascontiguousarray(coords[t, w_id, 0]) 418 | y_coords = np.ascontiguousarray(coords[t, w_id, 1]) 419 | if self.dim == 3: 420 | z_coords = np.ascontiguousarray(coords[t, w_id, 2]) 421 | wc_fp = os.path.join(path, prefix+'coords_'+str(st).zfill(zf)) 422 | pointsToVTK(path=wc_fp, 423 | x=x_coords, 424 | y=y_coords, 425 | z=z_coords, 426 | data={'time': time_data}) 427 | 428 | def _build_big_image(self, num_copies=0): 429 | r''' 430 | Build the big image by flipping and stacking along each axis a number 431 | of times on both sides of the image to keep the original in the center 432 | 433 | Parameters 434 | ---------- 435 | num_copies: int 436 | the number of times to copy the image along each axis 437 | ''' 438 | big_im = self.im.copy() 439 | func = [np.vstack, np.hstack, np.dstack] 440 | temp_im = self.im.copy() 441 | for ax in tqdm(range(self.dim), desc='building big image'): 442 | flip_im = np.flip(temp_im, ax) 443 | for c in range(num_copies): 444 | if c % 2 == 0: 445 | # Place one flipped copy either side 446 | big_im = func[ax]((big_im, flip_im)) 447 | big_im = func[ax]((flip_im, big_im)) 448 | else: 449 | # Place one original copy either side 450 | big_im = func[ax]((big_im, temp_im)) 451 | big_im = func[ax]((temp_im, big_im)) 452 | # Update image to copy for next axis 453 | temp_im = big_im.copy() 454 | return big_im 455 | 456 | def _fill_im_big(self, w_id=None, t_id=None, data='t'): 457 | r''' 458 | Fill up a copy of the big image with walker data. 459 | Move untrodden pore space to index -1 and solid to -2 460 | 461 | Parameters 462 | ---------- 463 | w_id: array of int of max length num_walkers (default = None) 464 | the indices of the walkers to plot. If None then all are shown 465 | t_id: array of int of max_length num_timesteps/stride (default = None) 466 | the indices of the timesteps to plot. If None then all are shown 467 | data: string (options are 't' or 'w', other) 468 | t fills image with timestep, w fills image with walker index, 469 | any other value places a 1 signifiying that a walker is at this 470 | coordinate. 471 | ''' 472 | offset = self._check_big_bounds() 473 | if not hasattr(self, 'im_big'): 474 | self.im_big = self._build_big_image(offset) 475 | big_im = self.im_big.copy().astype(int) 476 | big_im -= 2 477 | # Number of stored timesteps, walkers dimensions 478 | [nst, nsw, nd] = np.shape(self.real_coords) 479 | if w_id is None: 480 | w_id = np.arange(0, nsw, 1, dtype=int) 481 | else: 482 | w_id = np.array([w_id]) 483 | if t_id is None: 484 | t_id = np.arange(0, nst, 1, dtype=int) 485 | else: 486 | t_id = np.array([t_id]) 487 | indices = np.indices(np.shape(self.real_coords)) 488 | coords = self.real_coords + offset*self.shape 489 | if data == 't': 490 | # Get timestep indices 491 | d = indices[0, :, w_id, 0].T 492 | elif data == 'w': 493 | # Get walker indices 494 | d = indices[1, :, w_id, 0].T 495 | else: 496 | # Fill with 1 where there is a walker 497 | d = np.ones_like(indices[0, :, w_id, 0].T, dtype=int) 498 | if self.dim == 3: 499 | big_im[coords[:, w_id, 0][t_id], 500 | coords[:, w_id, 1][t_id], 501 | coords[:, w_id, 2][t_id]] = d[t_id] 502 | else: 503 | big_im[coords[:, w_id, 0][t_id], 504 | coords[:, w_id, 1][t_id]] = d[t_id] 505 | 506 | return big_im 507 | 508 | def _save_fig(self, figname='test.png', dpi=600): 509 | r''' 510 | Wrapper for saving figure in journal format 511 | ''' 512 | plt.figaspect(1) 513 | plt.savefig(fname=figname, dpi=dpi, facecolor='w', edgecolor='w', 514 | format='png', bbox_inches='tight', pad_inches=0.0) 515 | 516 | def plot_walk_2d(self, w_id=None, data='t', check_solid=False): 517 | r''' 518 | Plot the walker paths in a big image that shows real and reflected 519 | domains. The starts are temporarily shifted and then put back 520 | 521 | Parameters 522 | ---------- 523 | w_id: array of int and any length (default = None) 524 | the indices of the walkers to plot. If None then all are shown 525 | data: string (options are 't' or 'w') 526 | t fills image with timestep, w fills image with walker index 527 | 528 | ''' 529 | if self.dim == 3: 530 | print('Method is not implemented for 3d images') 531 | print('Please use export for visualizing 3d walks in paraview') 532 | else: 533 | offset = self._check_big_bounds() 534 | if not hasattr(self, 'im_big'): 535 | self.im_big = self._build_big_image(offset) 536 | sb = np.sum(self.im_big == self.solid_value) 537 | big_im = self._fill_im_big(w_id=w_id, data=data).astype(int) 538 | sa = np.sum(big_im == self.solid_value - 2) 539 | fig, ax = plt.subplots(figsize=[6, 6]) 540 | ax.set(aspect=1) 541 | solid = big_im == self.solid_value-2 542 | solid = solid.astype(float) 543 | solid[np.where(solid == 0)] = np.nan 544 | porous = big_im == self.solid_value-1 545 | porous = porous.astype(float) 546 | porous[np.where(porous == 0)] = np.nan 547 | plt.imshow(big_im, cmap=cmap) 548 | # Make Solid Black 549 | plt.imshow(solid, cmap='binary', vmin=0, vmax=1) 550 | # Make Untouched Porous White 551 | plt.imshow(porous, cmap='gist_gray', vmin=0, vmax=1) 552 | if check_solid: 553 | print('Solid pixel match?', sb == sa, sb, sa) 554 | 555 | def axial_density_plot(self, time=None, axis=None, bins=None): 556 | r''' 557 | Plot the walker density summed along an axis at a given time 558 | 559 | Parameters 560 | ---------- 561 | time: int 562 | the index in stride time. If the run method was set with a stride 563 | of 10 then putting time=2 will visualize the 20th timestep as this 564 | is the second stored time-step (after 0) 565 | axis: int - Used only for 3D walks 566 | The axis over which to sum, produces a slice in the plane normal to 567 | this axis. 568 | 569 | ''' 570 | copies = self._check_big_bounds() 571 | im_bins = (copies + 1)*np.asarray(self.shape) 572 | t_coords = self.real_coords[time, :, :] 573 | if self.dim == 3: 574 | if axis is None: 575 | axis = 2 576 | axes = np.arange(0, self.dim, 1) 577 | [a, b] = axes[axes != axis] 578 | [na, nb] = im_bins[axes != axis] 579 | else: 580 | [a, b] = [0, 1] 581 | [na, nb] = im_bins 582 | a_coords = t_coords[:, a] 583 | b_coords = t_coords[:, b] 584 | fig, ax = plt.subplots(figsize=[6, 6]) 585 | ax.set(aspect=1) 586 | if bins is None: 587 | bins = max([na, nb]) 588 | plt.hist2d(a_coords, b_coords, bins=bins, cmin=1, cmap=cmap) 589 | plt.colorbar() 590 | 591 | def run_analytics(self, ws, ts, fname='analytics.csv', num_proc=None): 592 | r''' 593 | Run run a number of times saving info 594 | Warning - this method may take some time to complete! 595 | 596 | Parameters 597 | ---------- 598 | ws: list 599 | list of number of walkers to run 600 | ts: int (default = 4) 601 | list of number of walkers to run 602 | fname: string 603 | file name - must end '.csv' 604 | ''' 605 | with open(fname, 'w', newline='\n') as f: 606 | self.data['sim_nw'] = 0 607 | self.data['sim_nt'] = 0 608 | self.data['sim_time'] = 0 609 | w = csv.DictWriter(f, self.data) 610 | header_written = False 611 | for nw in ws: 612 | for nt in ts: 613 | print('Running Analytics for:') 614 | print('Number of Walkers: ' + str(nw)) 615 | print('Number of Timesteps: ' + str(nt)) 616 | start_time = time.time() 617 | self.run(nt, nw, same_start=False, stride=10, 618 | num_proc=num_proc) 619 | sim_time = time.time() - start_time 620 | print('Completed in: ' + str(sim_time)) 621 | self.plot_msd() 622 | plt.title('Walkers: ' + str(nw) + ' Steps: ' + str(nt)) 623 | self.data['sim_nw'] = nw 624 | self.data['sim_nt'] = nt 625 | self.data['sim_time'] = sim_time 626 | w = csv.DictWriter(f, self.data) 627 | if not header_written: 628 | w.writeheader() 629 | header_written = True 630 | w.writerow(self.data) 631 | gc.collect() 632 | --------------------------------------------------------------------------------