question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Proposal for including porosity input for porespy.generators.cylinders

See original GitHub issue

I the porespy docs, there appears to be a desire for a cylinders method that takes a target porosity as an input.

ncylinders (scalar) – The number of cylinders to add to the domain. Adjust this value to control the final porosity, which is not easily specified since cylinders overlap and intersect different fractions of the domain.

I’ve implemented a proposed function for doing this. In general, the approach is to roughly estimate the number of fibers that would be needed to reach the target porosity, test this estimate by probing with a small fraction of that, improving the estimate, and iterating.

One relationship that this algorithm leans on to make a good estimate might be new to this group. This is the fact that, when inserting any potentially overlapping objects randomly into a volume v_total (which has units of pixels and is equal to dimx x dimy x dimz, for example), such that the total volume of objects added to the volume is v_added (and includes any volume that was inserted but overlapped with already occupied space), the resulting porosity will be equal to exp(-v_added/v_total). This might be more broadly useful in other methods, and I can share the derivation of this relationship if there is interest.

Good stuff:

  • It seems pretty robust, catches edge cases, and warns when it might not give a good result
  • It generally takes 4-5 times longer than a call to generators.cylinders, assuming you knew how many fibers to add to get to to the target porosity (which of course, you don’t)

Bad stuff:

  • It doesn’t take a tolerance or iteration parameter. It could be integrated, but it does a pretty good job. It might be good to put a caveat like there is the polydisperse spheres generator.

If there’s interest in this being integrated into porespy, I can submit a pull request. Let me know!

Anyway, here’s the method. This one is verbose so you can see what it’s doing, and includes a test case at the bottom. Test it out and let me know what you think!

@vasudevanv

import porespy as ps
import numpy as np
import time
import math
import warnings

def porocylinders(fun_dims, fun_cylrad,fun_porosity,  fun_phi_max, fun_theta_max, fun_length):
    # crudely estimate fiber volume
    vol_total = float(fun_dims[0]*fun_dims[1]*fun_dims[2])
    dim_ave = (fun_dims[0]*fun_dims[1]*fun_dims[2])**(1./3)
    # crudely estimate fiber length as cube root of product of dims if it isn't given as an input
    if fun_length==None:
        length_est = dim_ave
    else:
        length_est = fun_length
    
    # fiber volume estimate
    single_fiber_vol_est = length_est*3.1415*fun_cylrad*fun_cylrad
    # used derived relationship to calculate the volume of fiber that needs to be added. 
    # this is reasonably exact
    n_pixels_to_add = -math.log(fun_porosity)*vol_total
    # here's the crude estimate of the number of fibers that need to be added
    # note: even if this is very wrong, we will catch after the first pass and correct
    n_fiber_est = n_pixels_to_add/single_fiber_vol_est
    print('first n_fiber estimate = '+ str(n_fiber_est))

    first_pass_n = int(.2*n_fiber_est)
    warnings.simplefilter('always', UserWarning)
    # if it expect very few fibers, such that the first pass wouldn't have a single fiber, warn, increase probe, and continue
    if first_pass_n == 0:
        first_pass_n = int(math.ceil(.5*n_fiber_est))
        warnings.warn('Very few fibers needed, target porosity may not be reached.')

    tempim = ps.generators.cylinders(fun_dims,fun_cylrad,first_pass_n,fun_phi_max,fun_theta_max, fun_length)
    first_pass_porosity = ps.metrics.porosity(tempim)
    # calculate fiber volume added in the first pass (again, this is fairly exact)
    first_pass_added_volume = -math.log(first_pass_porosity)*vol_total
    # actual volume added per fiber in first pass
    first_pass_fiber_volume = first_pass_added_volume/first_pass_n
    # a better estimate of the number of fibers to add
    second_pass_n_est = n_pixels_to_add/first_pass_fiber_volume
    print('second n_fiber estimate = '+ str(second_pass_n_est))
    
    #catch if you overshot the number of fibers with initial estimate
    if first_pass_n > 0.9*second_pass_n_est:
        print('   overshot n_fibers with first estimate, correcting...')
        first_pass_n = math.ceil(0.2*second_pass_n_est)
        tempim = ps.generators.cylinders(fun_dims,fun_cylrad,first_pass_n,fun_phi_max,fun_theta_max, fun_length)
        first_pass_porosity = ps.metrics.porosity(tempim)
        first_pass_added_volume = -math.log(first_pass_porosity)*vol_total
        first_pass_fiber_volume = first_pass_added_volume/first_pass_n
        second_pass_n_est = n_pixels_to_add/first_pass_fiber_volume
        print('corrected second n_fiber estimate = '+ str(second_pass_n_est))

    second_pass_n = math.ceil(int(0.7*(second_pass_n_est)-first_pass_n))
    tempim = tempim & ps.generators.cylinders(fun_dims,fun_cylrad,second_pass_n,fun_phi_max,fun_theta_max, fun_length)
    second_pass_porosity = ps.metrics.porosity(tempim)
    second_pass_added_volume = -math.log(second_pass_porosity)*vol_total
    second_pass_fiber_volume = second_pass_added_volume/(first_pass_n+second_pass_n)

    third_pass_n_est = math.ceil(n_pixels_to_add/second_pass_fiber_volume)
    print('final n_fiber estimate = '+ str(third_pass_n_est))

    third_pass_n = third_pass_n_est-first_pass_n-second_pass_n
    if third_pass_n<1:
        third_pass_n = 0
    
    total_added_n_fiber = first_pass_n + second_pass_n + third_pass_n
    print('total fibers added = ' + str(total_added_n_fiber) + '('+ str(first_pass_n)+ '+' + str(second_pass_n)+'+' + str(third_pass_n)+ ')')
    if third_pass_n>0:
        tempim = tempim & ps.generators.cylinders(fun_dims,fun_cylrad,third_pass_n,fun_phi_max,fun_theta_max, fun_length)
    return total_added_n_fiber,tempim
#
test_poro_cylinders = True

if test_poro_cylinders:
    goal_poros = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9, 0.95, 0.99]
    for goal_poro in goal_poros:
        poro_start_time = time.time()
        dims = [200,200,200]
        rcyl = 5
        totaln, porotest = porocylinders(dims, rcyl,goal_poro,45,45,None)
        poro_duration = time.time()-poro_start_time
        poroout = ps.metrics.porosity(porotest)
        print('porogoal was ' + str(goal_poro))
        print('out_poro was ' + str(poroout))

        compare_start = time.time()
        compare = ps.generators.cylinders(dims, rcyl, goal_poro,0,90)
        compare_duration = time.time()-compare_start
        print('poro method took  '+ str(poro_duration))
        print('pscyl method took '+ str(compare_duration))
        print(str(poro_duration/compare_duration)+ ' times slower.\n')


Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
jgostickcommented, May 20, 2020

I think slowing the function down by 5x is probably not a good idea 😦 Maybe we could put a little ‘example’ of how to get the correct porosity in the doc string?

kw = {'shape' : [200, 200, 200],
      'radius' : 8,
      'ncylinders' : 10,
      'phi_max' : 0,
      'theta_max' : 90,
      'length' : None}
porosity = 0.8
im = np.ones(shape=kw['shape'], dtype=bool)
while im.sum()/im.size > porosity:
    im *= ps.generators.cylinders(**kw)
print("Desired porosity reached:", im.sum()/im.size)
plt.imshow(ps.visualization.sem(im, direction='Z'))
0reactions
1minus1commented, May 28, 2020

I have streamlined the function. I’ll make a Jupyter notebook with the test above and submit a PR soon.

Read more comments on GitHub >

github_iconTop Results From Across the Web

cylinders — PoreSpy documentation
Controls how many iterations the function uses to match the requested porosity. If ncylinders is given instead of porosity , then maxiter is...
Read more >
(PDF) PoreSpy: A Python Toolkit for Quantitative Analysis of ...
A pore network modeling (PNM) framework for the simulation of transport of charged species, such as ions, in porous media is presented. It ......
Read more >
Releases · PMEAL/porespy - GitHub
A set of tools for characterizing and analying 3D images of porous materials ... Cylinders generator now also accepts porosity (previously only number...
Read more >
Accurately predicting transport properties of porous fibrous ...
Traditionally, these tools have included empirical models, ... generated using an open-source python package (PoreSpy, cylinder generator), ...
Read more >
porespy Changelog - pyup.io
Cylinders generator now also accepts porosity (previously only number of cylinders) \enh - Refactored make\_contiguous to use less memory and by 5x faster...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found