Reading time: 10 min
12 /4/2019
Concrete is a four phase material comprising of cement, sand, aggregate and voids. In order to better represent the complexity of concrete material, a mesoscale modelling approach is normally considered which attempts to capture the dominant material structure whilst ignoring finer details. For example, there will be a limitation to the minimum particle size and packing of particles considered in the model due to computational reasons.
To begin considering heterogeneous concrete models for simulation, you would normally start with circles (2D) or spheres (3D) to represent the aggregate before moving on to ellipse and polygon type aggregates. In order to accurately represent concrete as a mesoscale model, the gradation of particles/sieve distribution needs to be considered and implemented in the generation algorithm. The sieve distribution would either come from your experiment or adapted from literature.
The distribution of particles or gradation is often determined by the fuller curve [1].
The area representation of particles A_agg in each sieve size [d_i, d_i+1}] is [2]
Where d_max, d_min is the maximum and minimum size of aggregates, P_agg is the area fraction of all aggregates with respect to the total area of concrete sample A.
The aggregates generally occupy 60-80% in volume for concrete [3]. For normal strength concrete, coarse aggregates usually represent 40-50% of concrete volume [1]. In this article, the approach by [3] is adopted. Aggregates larger than 2.36mm are modelled and smaller fine particles with cement is treated as mortar. Pore size range between 2-4mm diameter is considered here with a 2% total pore area. The sieve size distribution adapted from [3] is given in Table 1 and illustrated in Figure 1.
Table 1. Four segment gradation of aggregate size distribution (Wang et al, 2015)
Sieve Size (mm) | Total Percentage Passing (%) |
19.00 | 100 |
12.70 | 97 |
9.50 | 61 |
4.75 | 10 |
2.36 | 1.4 |
The common aggregate/pore generation procedure is to generate and place the aggregates and pores in a repeated manner until the target area is fully packed. I will explain the detailed procedure for circular aggregates alongside python code for implementation.
height = 0.1 #m
length = 0.1
d0 = 0.002
d1 = 0.004
pore_content = 0.02 #%
minbound = 0.005
def generateCirclePores(length, height, d0, d1, pore_content, minbound):
#length and height is the dimension of the generation area
#d0 is min pore diameter, d1 is max pore diameter
#pore content is pore % of total area
#minbound is the distance away from side boundaries
poreArea = pore_content*length*height
totalPoreArea = 0
poreXvalues = []
poreYvalues = []
poreRvalues = []
while totalPoreArea <= poreArea:
r = (d0+random.random()*(d1-d0))/2
boundary = minbound + r
x = boundary + random.random()*(length-2*boundary)
y = boundary + random.random()*(height-2*boundary)
if len(poreXvalues) == 0:
poreXvalues.append(x)
poreYvalues.append(y)
poreRvalues.append(r)
totalPoreArea = totalPoreArea + np.pi*r**2
else: #check conditions
distance = np.asarray([(x-x1)**2+(y-y1)**2 for x1,y1 in zip(poreXvalues,poreYvalues)])
minDistance = np.asarray([(2*(r+r1))**2 for r1 in poreRvalues])
if np.any(distance < minDistance):
continue
else:
poreXvalues.append(x)
poreYvalues.append(y)
poreRvalues.append(r)
totalPoreArea = totalPoreArea + np.pi*r**2
print("Total pore area is {} and error is {}%".format(totalPoreArea,(totalPoreArea-poreArea)*100/poreArea))
return poreXvalues, poreYvalues, poreRvalues
[poreX, poreY, poreR] = generateCirclesPores(length, height, d0, d1, pore_content, minbound)
aggVol = 0.3 #%
sieveMax = [0.019, 0.0127,0.0095,0.00475] #m
sieveMin = [0.0127,0.0095,0.00475,0.00236] #m
cumPass = [1, 0.97, 0.61, 0.10, 0.014]
itz = 0.0025
def generateCirclesWithPores(length, height, minbound, poreX,poreY,poreR, aggVol, sieveMin, sieveMax, cumPass, itz):
#length and height is the dimension of the generation area
#minbound is the distance away from side boundaries
#aggVol is volume/area of coarse aggregate in decimal i.e 0.5
#sieveMin is the min sieve sizes ordered from largest to smallest in list format
#sieveMax is the max sieve sizes ordered from largest to smallest in list format
#cumPass is the passing percentage in decimal for each sieve size from largest to smallest in list format
#itz is interfacial transition zone thickness in metres
area = length*height
Pmax = cumPass[0]
Pmin = cumPass[-1]
aggArea = [(cumPass[i]-cumPass[i+1])/(Pmax-Pmin)*aggVol*area for i in range(len(cumPass)) if i!=len(cumPass)-1]
#initalise lists
aggX = []
aggY = []
aggR = []
aggRitz = []
for sMin, sMax, gradeArea in zip(sieveMin, sieveMax, aggArea):
totalAggArea = 0
breakCount = 0
while totalAggArea < gradeArea: if breakCount >2**20:
break
r = (sMin+random.random()*(sMax-sMin))/2
r_itz = r+itz;
boundary = minbound + r_itz
x = boundary + random.random()*(length-2*boundary)
y = boundary + random.random()*(height-2*boundary)
#check not overlapping with pores
distance = np.asarray([(x-x1)**2+(y-y1)**2 for x1,y1 in zip(poreX,poreY)])
minDistance = np.asarray([(r_itz+r1)**2 for r1 in poreR])
if np.any(distance < minDistance):
breakCount = breakCount +1
continue
if len(aggX) ==0:
aggX.append(x)
aggY.append(y)
aggR.append(r)
aggRitz.append(r_itz)
totalAggArea = totalAggArea + np.pi*r**2
else:
#check not overlapping with existing aggregates
distance = np.asarray([(x-x1)**2+(y-y1)**2 for x1,y1 in zip(aggX,aggY)])
minDistance = np.asarray([(r_itz+r1)**2 for r1 in aggR])
if np.any(distance < minDistance):
breakCount = breakCount +1
continue
else:
aggX.append(x)
aggY.append(y)
aggR.append(r)
aggRitz.append(r_itz)
totalAggArea = totalAggArea + np.pi*r**2
print("Reached agg size: {}".format(sMax))
print("Agg Area: {}, error is {}% ".format(totalAggArea, (totalAggArea-gradeArea)*100/gradeArea))
return aggX, aggY, aggR, aggRitz
[aggX, aggY, aggR, aggRitz] = generateCirclesWithPores(length,height,minbound,poreX,poreY,poreR,aggVol,sieveMin, sieveMax, cumPass, itz)
To visual the generation of circular aggregates, the list of stored aggregate/pore values will be extracted and plotted in a chart.
def plotCircles(X,Y,R,xrange,yrange):
fig, ax = plt.subplots()
ax.set_xlim((0, xrange))
ax.set_ylim((0, yrange))
for x,y,r in zip(X,Y,R):
circles = plt.Circle([x,y],r,color='red', fill=False)
ax.add_artist(circles)
plt.show()
plotCircles(poreX,poreY,poreR,length,height)
plotCircles(aggX,aggY,aggR,length,height)
So there you have it, I have demonstrated a brute force algorithm for circular aggregate generation. I hope this introductory overview provides you with the foundation to conduct more advanced mesoscale modelling. Of course, there is a still a crucial aspect that is missing, which is conducting mesoscale modelling through finite element software. This aspect of mesoscale modelling is something I will discuss later.
[1] Wriggers, P., Moftah, S.O., 2006, Mesoscale models for concrete: homogenisation and damage behaviour, Finite Element Analysis and Design, 42 (7), 623-636
[2] Wang, Z., Kwan, A., Chan, H., 1999, Mesoscopic study of concrete I: generation of random aggregate structure and finite element mesh, Computers & Structures, 70 (5), 533-544
[3] Wang, X.F., Yang, Z.J., Yates, J.R., Jivkov, A.P., Zhang, Ch., 2015, Monte carlo simulations of mesoscale fracture modelling of concrete with random aggregates and pores, Construction and Building Materials, 75, 35-45
Submit a form to get to touch