Skip to content

Commit 3c127a6

Browse files
npillardoujhuang2601herve-gross
authored
Add 1D cooling Thermo-Mechanical tutorial to documentation (#4123)
* Add 1D cooling tutorial to doc * wordsmithing the example rest * update wellbore thermal tutorials --------- Co-authored-by: Jian Huang <53012159+jhuang2601@users.noreply.github.com> Co-authored-by: Herve Gross <40979822+herve-gross@users.noreply.github.com>
1 parent 54cd9a5 commit 3c127a6

17 files changed

Lines changed: 718 additions & 13 deletions

File tree

inputFiles/thermoPoromechanics/ThermoDruckerPrager_1DCooling_fim_smoke.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
solidInternalEnergyModelName="rockInternalEnergy"
5555
/>
5656

57-
<DruckerPrager
57+
<!-- SPHINX_DRUCKERPRAGER_SOLID -->
58+
<DruckerPrager
5859
name="rockSolid"
5960
defaultDensity="2700"
6061
defaultBulkModulus="0.5e9"
@@ -63,7 +64,8 @@
6364
defaultFrictionAngle="15.27"
6465
defaultDilationAngle="0.0"
6566
defaultHardeningRate="0.0"
66-
defaultDrainedLinearTEC="3e-7"/>
67+
defaultDrainedLinearTEC="3e-7"/>
68+
<!-- SPHINX_DRUCKERPRAGER_SOLID_END -->
6769

6870
<BiotPorosity
6971
name="rockPorosity"

inputFiles/thermoPoromechanics/ThermoElastic_1DCooling_fim_smoke.xml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@
5454
solidInternalEnergyModelName="rockInternalEnergy"
5555
/>
5656

57-
<ElasticIsotropic
57+
<!-- SPHINX_ELASTIC_SOLID -->
58+
<ElasticIsotropic
5859
name="rockSolid"
5960
defaultDensity="2700"
6061
defaultBulkModulus="0.5e9"
6162
defaultShearModulus="0.3e9"
62-
defaultDrainedLinearTEC="3e-7"/>
63+
defaultDrainedLinearTEC="3e-7"/>
64+
<!-- SPHINX_ELASTIC_SOLID_END -->
6365

6466
<BiotPorosity
6567
name="rockPorosity"

inputFiles/thermoPoromechanics/ThermoMech_1DCooling_base.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
functionName="timeFunction"
5454
scale="1.0"/>
5555

56+
<!-- SPHINX_CONSTRAINTS -->
5657
<FieldSpecification
5758
name="xconstraint"
5859
fieldName="totalDisplacement"
@@ -73,14 +74,17 @@
7374
component="2"
7475
objectPath="nodeManager"
7576
setNames="{ zneg }"/>
77+
<!-- SPHINX_CONSTRAINTS_END -->
7678
</FieldSpecifications>
7779

7880
<Functions>
81+
<!-- SPHINX_COOLING_RAMP -->
7982
<TableFunction
8083
name="timeFunction"
8184
inputVarNames="{ time }"
8285
coordinates="{ 0, 1e-10, 100.0 }"
8386
values="{ 100.0, 100.0, 20.0 }"/>
87+
<!-- SPHINX_COOLING_RAMP_END -->
8488
</Functions>
8589

8690
<Tasks>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import math
2+
import numpy as np
3+
4+
class DPAnalyticalSolution:
5+
def __init__(self):
6+
self.bulkMod = 0.5e9 # Pa
7+
self.shearMod = 0.3e9 # Pa
8+
self.lambdaCoeff = self.bulkMod - 2*self.shearMod/3. # Pa
9+
self.cohesion = 5e3 # Pa
10+
self.frictionAngle = 15.27 # deg
11+
self.thermalExpansionCoeff = 3e-7
12+
self.phi = 6 * np.sin(np.deg2rad(self.frictionAngle)) / ( 3 - np.sin(np.deg2rad(self.frictionAngle)) )
13+
self.C = 6 * self.cohesion * np.cos(np.deg2rad(self.frictionAngle)) / ( 3 - np.sin(np.deg2rad(self.frictionAngle)) )
14+
15+
def compute_stress(self, deltaTemp):
16+
sigma_x = 0
17+
sigma_z = 0
18+
19+
epsMech_y = - self.thermalExpansionCoeff * deltaTemp
20+
21+
# We first assume all elastic deformation
22+
epsMech_x = -epsMech_y * self.lambdaCoeff / 2 / (self.lambdaCoeff + self.shearMod)
23+
epsMech_z = epsMech_x
24+
25+
sigma_y = (self.lambdaCoeff + 2*self.shearMod) * epsMech_y + 2 * self.lambdaCoeff * epsMech_x
26+
27+
P = sigma_y / 3.
28+
Q = sigma_y
29+
30+
yieldFunc = Q + self.phi * P - self.C
31+
32+
if yieldFunc <= 0.0:
33+
eps_x = epsMech_x + self.thermalExpansionCoeff * deltaTemp
34+
35+
return sigma_x, sigma_y, eps_x
36+
else:
37+
sigma_y = self.C / (1 + self.phi/3.0)
38+
epsMech_x = (sigma_y - (3*self.lambdaCoeff + 2*self.shearMod)*epsMech_y )/ (6*self.lambdaCoeff + 4*self.shearMod)
39+
multipler = -(sigma_y - (self.lambdaCoeff + 2*self.shearMod)*epsMech_y - 2*self.lambdaCoeff*epsMech_x)/ (4/3 * self.shearMod)
40+
eps_x = epsMech_x + self.thermalExpansionCoeff * deltaTemp
41+
42+
return sigma_x, sigma_y, eps_x
43+
44+
def compute_disp(self, deltaTemp):
45+
_, _, eps_x = self.compute_stress(deltaTemp)
46+
47+
disp_y = 0
48+
disp_x = eps_x*1
49+
50+
return disp_x, disp_y
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
.. _ExampleThermoMech1DCooling:
2+
3+
4+
####################################################
5+
Thermally Induced Failure under Confined Cooling
6+
####################################################
7+
8+
**Context**
9+
10+
11+
When a rock cools but cannot contract, it develops tensile stress. In a confined
12+
rock mass, this thermal stress combines with the existing in-situ stresses and
13+
may cause the rock to fail—even when pore pressure and external loading stay constant.
14+
This mechanism is responsible for thermal fracturing around cold-fluid injectors
15+
and caprock damage during CO\ :sub:`2` storage, among other phenomena.
16+
17+
This example simulates thermal contraction stresses using a one-dimensional
18+
thermo-mechanical problem. We compare two constitutive models subjected to the same cooling history:
19+
20+
- **Thermo-elastic rock** (``ElasticIsotropic``): The induced stress increases indefinitely as the rock continues cooling.
21+
22+
- **Thermo-plastic rock** (``DruckerPrager``): The induced stress is limited by the yield surface. Beyond a critical temperature drop, the rock fails and deforms plastically.
23+
24+
Both cases have closed-form solutions, making this example useful for verifying
25+
the thermo-mechanical coupling and the Drucker-Prager return mapping under thermal loading.
26+
27+
28+
**InputFile**
29+
30+
This example uses no external input files. Everything required is contained within three GEOS
31+
input files located at:
32+
33+
.. code-block:: console
34+
35+
inputFiles/thermoPoromechanics/ThermoMech_1DCooling_base.xml
36+
37+
.. code-block:: console
38+
39+
inputFiles/thermoPoromechanics/ThermoElastic_1DCooling_fim_smoke.xml
40+
41+
.. code-block:: console
42+
43+
inputFiles/thermoPoromechanics/ThermoDruckerPrager_1DCooling_fim_smoke.xml
44+
45+
---------------------------------------------------
46+
Description of the case
47+
---------------------------------------------------
48+
49+
We consider a seven-meter column discretized with 14 elements along the ``y`` direction, and a
50+
single element in the two other directions. The column is initially at a uniform temperature
51+
of 100 K, and is cooled down to 20 K following a linear ramp imposed over the whole domain.
52+
53+
The mechanical boundary conditions are the essential ingredient of the problem: the two ends
54+
of the column (``yneg`` and ``ypos``) are fixed along ``y``, so the axial strain is prevented
55+
(ε_yy = 0), while the `x` and `z` directions are only restrained on one face each and are
56+
therefore free to deform. The lateral faces being traction-free, σ_xx = σ_zz = 0, and the only
57+
non-zero stress component is σ_yy.
58+
59+
.. math::
60+
61+
\varepsilon_{yy} = 0, \qquad \sigma_{xx} = \sigma_{zz} = 0
62+
63+
.. _thermoMech1DCoolingSketchFig:
64+
.. figure:: xz_cross_section_uniaxial_stress.png
65+
:align: center
66+
:width: 500
67+
:figclass: align-center
68+
69+
Sketch of the confined column: both ends are fixed along ``y``, while ``x`` and ``z`` are
70+
restrained on a single face each and remain free to deform.
71+
72+
.. literalinclude:: ../../../../../../../inputFiles/thermoPoromechanics/ThermoMech_1DCooling_base.xml
73+
:language: xml
74+
:start-after: <!-- SPHINX_CONSTRAINTS -->
75+
:end-before: <!-- SPHINX_CONSTRAINTS_END -->
76+
77+
The cooling history is prescribed by a ``TableFunction`` applied to the temperature field.
78+
A short initial temperature plateau lets the mechanical equilibrium settle before the thermal
79+
loading starts.
80+
81+
.. literalinclude:: ../../../../../../../inputFiles/thermoPoromechanics/ThermoMech_1DCooling_base.xml
82+
:language: xml
83+
:start-after: <!-- SPHINX_COOLING_RAMP -->
84+
:end-before: <!-- SPHINX_COOLING_RAMP_END -->
85+
86+
The pore pressure is fixed to zero and the permeability is set to a negligible value
87+
(:math:`10^{-100}` m\ :sup:`2`), so that no fluid flow takes place: the stress evolution is
88+
entirely thermo-mechanical, like in the analytical solution.
89+
90+
------------------------------------------------------------------
91+
Constitutive models
92+
------------------------------------------------------------------
93+
94+
The two cases differ **only** by the solid model. The thermo-elastic case uses an
95+
``ElasticIsotropic`` solid, with a drained linear thermal expansion coefficient
96+
:math:`\alpha = 3 \times 10^{-7}` K\ :sup:`-1`:
97+
98+
.. literalinclude:: ../../../../../../../inputFiles/thermoPoromechanics/ThermoElastic_1DCooling_fim_smoke.xml
99+
:language: xml
100+
:start-after: <!-- SPHINX_ELASTIC_SOLID -->
101+
:end-before: <!-- SPHINX_ELASTIC_SOLID_END -->
102+
103+
The thermo-plastic case uses the same elastic properties and thermal expansion
104+
coefficient, and adds a Drucker-Prager yield surface:
105+
106+
.. literalinclude:: ../../../../../../../inputFiles/thermoPoromechanics/ThermoDruckerPrager_1DCooling_fim_smoke.xml
107+
:language: xml
108+
:start-after: <!-- SPHINX_DRUCKERPRAGER_SOLID -->
109+
:end-before: <!-- SPHINX_DRUCKERPRAGER_SOLID_END -->
110+
111+
------------------------------------------------------------------
112+
Analytical solution
113+
------------------------------------------------------------------
114+
115+
**Thermo-elastic response.** In this uniaxial stress state (σ_xx = σ_zz = 0), the axial strain is blocked (ε_yy = 0) and the thermo-elastic constitutive law reduces to
116+
117+
.. math::
118+
119+
\sigma_{yy} = -E \, \alpha \, \Delta T
120+
121+
with :math:`E = 9KG/(3K+G)` the Young modulus. A cooling :math:`\Delta T < 0` therefore
122+
produces a **tensile** stress that grows linearly with the temperature drop, without any
123+
bound.
124+
125+
**Onset of failure.** The Drucker-Prager yield function implemented in GEOS reads
126+
127+
.. math::
128+
129+
F = Q + b \, P - c
130+
131+
where :math:`P = \mathrm{tr}(\sigma)/3` is the mean stress and :math:`Q` the von Mises stress.
132+
The two coefficients are obtained from the friction angle :math:`\varphi` and the cohesion so
133+
that the cone passes through the triaxial compression corners of the Mohr-Coulomb surface:
134+
135+
.. math::
136+
137+
b = \frac{6 \sin \varphi}{3 - \sin \varphi}, \qquad
138+
c = \frac{6\, \mathrm{cohesion} \cos \varphi}{3 - \sin \varphi}
139+
140+
For the uniaxial stress state of this problem, :math:`Q = \sigma_{yy}` and
141+
:math:`P = \sigma_{yy}/3`, so the yield condition :math:`F = 0` gives a closed-form cap on the
142+
thermally induced stress, and the corresponding critical cooling:
143+
144+
.. math::
145+
146+
\sigma_{f} = \frac{c}{1 + b/3}, \qquad
147+
\Delta T_{f} = -\frac{\sigma_{f}}{E \, \alpha}
148+
149+
With the properties of this example, :math:`\sigma_{f} = 8868` Pa is reached after a cooling
150+
of only :math:`\Delta T_{f} = -39.4` K, that is, less than half of the imposed temperature
151+
drop. Beyond that point the rock deforms plastically and the stress stays on the yield
152+
surface.
153+
154+
**Lateral displacement.** Because the ``y`` direction is blocked while ``x`` and ``z`` are
155+
free, all the deformation shows up laterally, and this gives a second, kinematic check that is
156+
independent from the stress. In the elastic regime,
157+
158+
.. math::
159+
160+
\varepsilon_{xx} = \alpha \, \Delta T \left( 1 + \frac{\lambda}{2(\lambda + G)} \right)
161+
162+
Once the yield surface is reached, plastic flow adds lateral strain while the stress stays
163+
put: the elasto-plastic column keeps contracting **faster** than the elastic one. Both
164+
branches are implemented in ``AnalyticalSol.py``, which is the reference solution used below.
165+
166+
------------------------------------------------------------------
167+
Running the case and post-processing
168+
------------------------------------------------------------------
169+
170+
Both cases are run independently, each in its own directory:
171+
172+
.. code-block:: console
173+
174+
geosx -i ThermoElastic_1DCooling_fim_smoke.xml
175+
geosx -i ThermoDruckerPrager_1DCooling_fim_smoke.xml
176+
177+
Each run writes ``stressHistory.hdf5`` and ``displacementHistory.hdf5`` through the
178+
``TimeHistory`` outputs. Those files are **not** stored in the repository; the curves shown
179+
below are extracted once, locally, into a small CSV file with:
180+
181+
.. code-block:: console
182+
183+
python3 postprocess1DCooling.py -e <elastic_run_dir> -d <druckerPrager_run_dir>
184+
185+
The figure of this page is then generated at documentation build time from that CSV only.
186+
187+
------------------------------------------------------------------
188+
Results
189+
------------------------------------------------------------------
190+
191+
.. plot:: docs/sphinx/advancedExamples/validationStudies/thermoPoromechanics/1DCooling/plot1DCooling.py
192+
193+
The figure on the left shows the stress induced by the confined cooling. Up to
194+
:math:`-\Delta T \approx 39` K the two models are indistinguishable and follow the elastic
195+
line :math:`-E \alpha \Delta T` exactly. Past that threshold, the elastic rock keeps
196+
accumulating tensile stress and reaches 17.8 kPa at the end of the cooling, whereas the
197+
Drucker-Prager rock **yields** and its stress saturates at the analytical cap
198+
:math:`\sigma_{f}`, matching to machine precision.
199+
200+
The figure in the middle explains the mechanism in the invariant plane. Because
201+
:math:`\sigma_{xx} = \sigma_{zz} = 0`, the loading path is the straight line :math:`Q = 3P`,
202+
whatever the amount of cooling. The elastic path simply crosses the Drucker-Prager envelope
203+
and keeps going, which is physically inadmissible; the elasto-plastic path stops on the
204+
envelope and slides along it.
205+
206+
The figure on the right shows the kinematic counterpart. Up to the failure threshold, the two columns
207+
contract identically. Beyond it, the roles reverse with respect to the stress plot: the
208+
elasto-plastic column, whose stress is now frozen, contracts **more** than the elastic one,
209+
reaching :math:`-32.7` against :math:`-29.7` µm. Plastic flow converts what would have been
210+
additional stress into additional strain. GEOS matches the analytical displacement of both
211+
branches to machine precision.
212+
213+
The practical consequence is that **the safe amount of cooling is set by the strength of the
214+
rock, not by its stiffness alone**: an elastic-only analysis of a cold injection would
215+
over-predict the stress by a factor of two here, under-predict the deformation, and miss the
216+
failure entirely.
217+
218+
------------------------------------------------------------------
219+
To go further
220+
------------------------------------------------------------------
221+
222+
**Feedback on this example**
223+
224+
For any feedback on this example, please submit a `GitHub issue on the project's GitHub page <https://github.com/GEOS-DEV/GEOS/issues>`_.

0 commit comments

Comments
 (0)