<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 The Stick and Ball Geometry
The SpheresAndCylinders class contains an assortment of pore-scale models that generate geometrical information assuming the pores are spherical and throats are cylindrical. 
The SpheresAndCylinders is a perfect starting point for generating your own custom geometry.  In fact, it's likely that only the calculation of 'pore.diameter' would need to be changed.  By default the 'pore.diameter' values are drawn from a random distribution which is not very realistic.  Luckily, it's easy to update the model used to calculate diameter, and then propagate this change to all the dependent values (i.e. 'pore.volume'), as illustrated below.
Step1:  The spacing of the above network is in um for this example to make values easier to read, but in general you should always use SI
Now we can create a geometry object based on the SpheresAndCylinders
Step2:  As can be seen by printing it, there are quite a few geometrical properties already added to this object.  Defining these manually would have been a pain, so it's a good idea to start with this class then alter the few models that need it
Step3:  The pore size distribution on the SpheresAndCylinders is probably the more likely thing to change, since it is a random (i.e. uniform distribution) as shown below
Step4:  The models on the geo object can be seen by printing them
Step5:  In this tutorial we will change how pore sizes are calculated.  We can do this by assigning a new pore-scale model for 'pore.diameter'.  Let's use  Gaussian distribution
Step6:  This model is automatically run when it's assigned, so we can inspect the new pore diameter values
Step7:  The above distribution does not look very much like a Gaussian distribution.  This is because the 'pore.seed' values are truncated between 0.2 and 0.7
Step8:  We should change this to a wider range to capture more pores on the "tails", then call regenerate_models, which will not only regenerate the random numbers, but all the other properties that depend on it such as 'pore.diameter', 'pore.volume', and so on
<ASSISTANT_TASK:>
Python Code:
import openpnm as op
%config InlineBackend.figure_formats = ['svg']
import matplotlib.pyplot as plt
pn = op.network.Cubic(shape=[20, 20, 20], spacing=100)
Explanation: The Stick and Ball Geometry
The SpheresAndCylinders class contains an assortment of pore-scale models that generate geometrical information assuming the pores are spherical and throats are cylindrical. 
The SpheresAndCylinders is a perfect starting point for generating your own custom geometry.  In fact, it's likely that only the calculation of 'pore.diameter' would need to be changed.  By default the 'pore.diameter' values are drawn from a random distribution which is not very realistic.  Luckily, it's easy to update the model used to calculate diameter, and then propagate this change to all the dependent values (i.e. 'pore.volume'), as illustrated below.
End of explanation
geo = op.geometry.SpheresAndCylinders(network=pn, pores=pn.Ps, throats=pn.Ts)
Explanation: The spacing of the above network is in um for this example to make values easier to read, but in general you should always use SI
Now we can create a geometry object based on the SpheresAndCylinders:
End of explanation
print(geo)
Explanation: As can be seen by printing it, there are quite a few geometrical properties already added to this object.  Defining these manually would have been a pain, so it's a good idea to start with this class then alter the few models that need it:
End of explanation
fig = plt.hist(geo['pore.diameter'], bins=25, edgecolor='k')
Explanation: The pore size distribution on the SpheresAndCylinders is probably the more likely thing to change, since it is a random (i.e. uniform distribution) as shown below:
End of explanation
print(geo.models)
Explanation: The models on the geo object can be seen by printing them:
End of explanation
f = op.models.geometry.pore_size.normal
geo.add_model(propname='pore.diameter', 
              model=f,
              loc=50, scale=10)
Explanation: In this tutorial we will change how pore sizes are calculated.  We can do this by assigning a new pore-scale model for 'pore.diameter'.  Let's use  Gaussian distribution:
End of explanation
fig = plt.hist(geo['pore.diameter'], bins=25, edgecolor='k')
Explanation: This model is automatically run when it's assigned, so we can inspect the new pore diameter values:
End of explanation
print(geo.models['pore.seed'])
Explanation: The above distribution does not look very much like a Gaussian distribution.  This is because the 'pore.seed' values are truncated between 0.2 and 0.7:
End of explanation
geo.models['pore.seed']['num_range'] = [0.001, 0.999]
geo.regenerate_models()
fig = plt.hist(geo['pore.diameter'], bins=25, edgecolor='k')
Explanation: We should change this to a wider range to capture more pores on the "tails", then call regenerate_models, which will not only regenerate the random numbers, but all the other properties that depend on it such as 'pore.diameter', 'pore.volume', and so on:
End of explanation
<END_TASK>