Quickstart

This page shows a minimal end-to-end example: a single borehole with a Single U-tube configuration, one soil layer, and no inter-borehole thermal interference. It is the simplest possible pyCaRM simulation and a good starting point before moving to multi-borehole fields.

Imports

import numpy as np

from carm import (
    BoreholeGeometry,
    BoreholeMesh,
    BoreholeThermalProperties,
    SingleUtube,
)
from carm import EnvironmentalProperties, EnvironmentalTimeSeries
from carm import Fluid
from carm import GroundGeometry, GroundMesh
from carm import PhysicalModel
from carm import Simulation

Fluid

Define the thermal and physical properties of the heat carrier fluid:

fluid = Fluid(
    k_w=0.5687,       # thermal conductivity [W/(m·K)]
    rho_w=1000.14,    # density [kg/m³]
    cp_w=4207.4,      # specific heat [J/(kg·K)]
    ni_w=1.496e-6,    # kinematic viscosity [m²/s]
)

Borehole

Assemble the borehole geometry, mesh, thermal properties, and pipe configuration into a SingleUtube object:

bore_geom = BoreholeGeometry(Lbore=100, D0=0.15)
bore_mesh = BoreholeMesh(m_mesh=40)
bore_th_props = BoreholeThermalProperties(cp_0=1460, rho_0=1655, k0=1.8)

props_b = SingleUtube(
    geom=bore_geom,
    mesh=bore_mesh,
    thermalprops=bore_th_props,
    fluid=fluid,
    Rp0=0.25,
    RppB=0.72,
    pipe_spacing=0.0823,
    pipe_thick=0.003,
    Dpi=0.026,
    n_pipes=2,
)

Ground

Define the ground geometry and radial/axial mesh. stratification is a list of layers, each specified as (k, rho, cp, permeability):

ground_geom = GroundGeometry(D0=0.15, L=100, L_sup=1, L_inf=10, rn=10)
ground_mesh = GroundMesh(n_mesh=20, m_mesh=40, m_mesh_sup=4, m_mesh_inf=40)

stratification = [(1.8, 947.37, 1900, 111)]   # single homogeneous layer

Environmental boundary conditions

Time-varying surface boundary conditions are built from plain NumPy arrays via from_array(). Here a synthetic seasonal + daily profile stands in for real measured data:

dt      = 3600          # timestep [s]
n_steps = 276

hours = np.arange(n_steps) * dt / 3600.0
T_ext = 12.5 - 11.0 * np.cos(2 * np.pi * hours / 8760.0) \
              + 3.0 * np.sin(2 * np.pi * hours / 24.0)
daylight = np.clip(np.sin(2 * np.pi * (hours % 24.0 - 6.0) / 24.0), 0.0, None)
seasonal = 0.5 - 0.5 * np.cos(2 * np.pi * hours / 8760.0)
SolarRad = 900.0 * daylight * seasonal

env_input = EnvironmentalTimeSeries.from_array(Tm=13, T_ext=T_ext, SolarRad=SolarRad)
env_props = EnvironmentalProperties(
    R_ext=0.04,
    absorptance=0.7,
    eps=0.95,
    At=10,
    tau=0,
    tau_y=365 * 24 * 3600,
    tau_shift=210 * 24 * 3600,
)

Note

pyCaRM has no built-in Excel reader: from_array is the only way to build an EnvironmentalTimeSeries (or a FieldInput) from data. If your data lives in a spreadsheet, read it with pandas.read_excel and pass the resulting columns to from_array — see examples/Excel_input.py for the full recipe.

Physical model

Combine all components into a PhysicalModel. Tg is the undisturbed ground temperature [°C]:

model = PhysicalModel(
    ground_geom=ground_geom,
    ground_mesh=ground_mesh,
    borehole=props_b,
    fluid=fluid,
    Tg=13,
    stratification=stratification,
)

Simulation

Define the inlet fluid temperature Tf1 and mass flow rate mw_tot as arrays of shape (n_groups, n_steps). For a single borehole in parallel (the default), n_groups = 1:

Tf1    = np.full((1, n_steps), 2.0)      # inlet temperature [°C]
mw_tot = np.full((1, n_steps), 0.1657)   # mass flow rate [kg/s]

simulation = Simulation(
    model=model,
    envinput=env_input,
    envprops=env_props,
    timesteps=dt,
    n_steps=n_steps,
    mw_tot=mw_tot,
    Tf1=Tf1,
)

T_history = simulation.run()

T_history has shape (n_steps + 1, n_bhes, n_dof), where index 0 along the first axis is the initial condition. See Output for how to extract temperatures of interest from this array.

Note

run() does not write anything to disk unless asked: pass save_results=True to also dump a timestamped .npz archive to a results/ directory. See examples/SingleUtube.py for a runnable use of this option. When history_path is also passed (see Output), the archive stores that path instead of T_history.

Extracting the outlet temperature

As a quick check, extract and plot the outlet fluid temperature over time:

import matplotlib.pyplot as plt

m_mesh_sup = 4
n_mesh     = 20
m_mesh     = 40

nsup    = m_mesh_sup + 1
nground = n_mesh * m_mesh

Tfout = T_history[1:, 0, nsup + nground + (props_b.n_equations - 1)]
time  = np.arange(1, n_steps + 1) * dt / 3600   # [h]

plt.plot(time, Tfout)
plt.xlabel("Time [h]")
plt.ylabel("Outlet temperature [°C]")
plt.grid(True, linestyle="--", alpha=0.4)
plt.tight_layout()
plt.show()

Note

For multi-borehole fields in parallel or series, see the examples in the examples/ directory of the repository.