Proposal for including porosity input for porespy.generators.cylinders
See original GitHub issueI 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!
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:
- Created 3 years ago
- Comments:8 (4 by maintainers)

Top Related StackOverflow Question
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?
I have streamlined the function. I’ll make a Jupyter notebook with the test above and submit a PR soon.