NASA C3X Conjugate Heat Transfer: Internally Cooled Turbine Vane, Run 157¶

This case runs the internally cooled NASA C3X turbine vane of Hylton et al., NASA CR-168015 (1983) at experimental run 157. It is a conjugate heat-transfer validation: the transonic cascade flow and the heat conduction inside the 310 stainless-steel vane are solved together, and all ten internal coolant passages are driven by their individually measured mass flow rates and temperatures.

The study is a single steady, fully turbulent k-omega SST RANS solve at the measured run 157 inlet condition. The post-processing slices the vane surface at midspan, maps it onto the measured vane profile, and compares the normalized wall temperature, the normalized surface heat-transfer coefficient, and the surface static-to-total pressure ratio against the experiment, with the reported experimental uncertainty as error bars.

Run the notebook top to bottom in a single kernel: load the mesh, build the parameters from the measured conditions, submit, wait, then post-process the in-kernel case object into the three published figures.

Requirements¶

This notebook runs against the Flow360 Python API. Install it and configure your API key by following the installation & setup guide.

This notebook targets the latest Flow360 Python client. It fetches the case's root asset and reference data with flow360.examples.download_benchmark_assets, added in 25.10.4. Install the client (25.10.4 or newer) before running:

pip install "flow360>=25.10.4"

Beyond flow360, the notebook needs a few plotting, data, and mesh-handling packages:

pip install numpy pandas matplotlib pyvista

pyvista reads the surface output and takes the midspan slice. Run the cells top to bottom in a single kernel.

Imports¶

In [ ]:
import os
import tarfile
import tempfile
from functools import lru_cache
from pathlib import Path

import flow360 as fl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import pyvista as pv
from matplotlib.colors import to_rgba

from flow360.examples import download_benchmark_assets

Input data¶

Both the setup and the post-processing read from this case's reference data: the cascade test conditions, the measured coolant-passage flow rates, the coolant hole centers, the vane profile, and the run 157 measurements. Fetch them from the public benchmark bucket; this recreates a local ./ref_data/ directory.

In [ ]:
download_benchmark_assets("C3X_CHT", "ref_data")

REF_DIR = Path("ref_data")

Run 157 is test-condition code 4521. Its row carries the cascade inlet total pressure and total temperature, the inlet Mach number, and the measured wall-to-gas temperature ratio used to initialize the solid.

In [ ]:
EXPERIMENT_CODE = 4521
PSIA_TO_PA = 6894.757293168
GAMMA = 1.4
CHORD_CM = 14.493
HEIGHT_CM = 7.62
OUTLET_PRESSURE_PA = 254172
OUTLET_TAP_X_CM = 9.0246
EXPERIMENT_NORMALIZATION_TEMPERATURE_K = 811.0

conditions = pd.read_csv(REF_DIR / "C3X_test_conditions.csv")
condition = conditions.loc[conditions["Code"] == EXPERIMENT_CODE].iloc[0]
condition

The solver is given the static freestream state, so convert the measured total quantities with the isentropic relations at the inlet Mach number. The solid is initialized at the measured wall-to-gas ratio, expressed relative to that static temperature because the heat equation's initial condition is non-dimensional.

In [ ]:
total_pressure_pa = condition.PT1_psia * PSIA_TO_PA
static_temperature_k = condition.TT1_K / (1 + (GAMMA - 1) * condition.M1**2 / 2)
total_density = total_pressure_pa / (287.05 * condition.TT1_K)
static_density = total_density / (1 + (GAMMA - 1) * condition.M1**2 / 2) ** (1 / (GAMMA - 1))
initial_wall_temperature = (
    condition.Tw_Tg * EXPERIMENT_NORMALIZATION_TEMPERATURE_K / static_temperature_k
)

Load project¶

The root asset for this case is a pre-generated volume mesh that already contains both the fluid cascade passage and the solid vane, plus the ten coolant channels. Download it from the public benchmark bucket and start a fresh project from it, no private project id is needed. The mesh boundaries are addressed by name below (Domain/Inlet, Vane/Top1, Channel_3/Channel_Inlets, and so on).

In [ ]:
root_asset_files = download_benchmark_assets("C3X_CHT", "root_assets")
# The snapshot bundles the mesh with solver metadata and logs; select the
# volume mesh file (.cgns or .ugrid, possibly .zst-compressed) to load.
volume_mesh_file = next(
    f for f in root_asset_files if f.removesuffix(".zst").endswith((".cgns", ".ugrid"))
)
project = fl.Project.from_volume_mesh(volume_mesh_file, name="NASA C3X Vane")
volume_mesh = project.volume_mesh

Physics setup¶

Each of the ten coolant passages was instrumented separately, so each gets its own inlet and outlet pair: a total temperature and mass flow rate at the inlet, and the same mass flow rate at the outlet. coolant_models builds those twenty boundary conditions from Appendix A of the report.

In [ ]:
def coolant_models(run, volume_mesh):
    coolant = pd.read_csv(REF_DIR / "coolant_data_157.csv").loc[
        lambda data: data["run"] == run,
        ["hole_no", "average_temperature_degK", "coolant_flow_rate_kg_per_sec"],
    ]
    models = []
    for row in coolant.sort_values("hole_no").itertuples(index=False):
        channel = int(row.hole_no)
        mass_flow = row.coolant_flow_rate_kg_per_sec * fl.u.kg / fl.u.s
        models.extend(
            [
                fl.Inflow(
                    name=f"Channel {channel} Inlet",
                    total_temperature=row.average_temperature_degK * fl.u.K,
                    spec=fl.MassFlowRate(value=mass_flow),
                    surfaces=volume_mesh[f"Channel_{channel}/Channel_Inlets"],
                ),
                fl.Outflow(
                    name=f"Channel {channel} Outlet",
                    spec=fl.MassFlowRate(value=mass_flow),
                    surfaces=volume_mesh[f"Channel_{channel}/Channel_Outlets"],
                ),
            ]
        )
    return models

Outputs setup¶

Two probe sets monitor the solve: a spanwise line through each coolant channel, and a line across the outlet static-pressure tap station used to check that the exit Mach number has settled. The published comparison itself comes from the surface output on the fluid-solid interface, which carries temperature, static pressure, and the heat-transfer coefficient.

In [ ]:
def channel_probe_output():
    centers = pd.read_csv(
        REF_DIR / "c3x_hole_centers_radii_xy.csv",
        usecols=["hole_id", "x_cm", "y_cm"],
    ).sort_values("hole_id")
    probes = [
        fl.PointArray(
            name=f"Channel {int(row.hole_id)} testing",
            start=(row.x_cm, row.y_cm, HEIGHT_CM) * fl.u.cm,
            end=(row.x_cm, row.y_cm, 0.0) * fl.u.cm,
            number_of_points=10,
        )
        for row in centers.itertuples(index=False)
    ]
    return fl.ProbeOutput(
        name="Channels",
        entities=probes,
        output_fields=["primitiveVars", "Mach"],
    )


outlet_probe = fl.PointArray(
    name="Outflow Probes",
    start=(OUTLET_TAP_X_CM, -9.69687, 3.86) * fl.u.cm,
    end=(OUTLET_TAP_X_CM, 2.07613, 3.86) * fl.u.cm,
    number_of_points=10,
)

Simulation params¶

The full setup: a k-omega SST fluid solver, a Solid model that solves the heat equation inside the vane with 310 stainless-steel properties, the cascade inlet and outlet, slip walls on the endwalls, translational periodicity across the passage, and the ten coolant-passage conditions. The cascade runs at zero incidence, so the operating condition is built from the inlet Mach number with the static state derived above.

In [ ]:
with fl.SI_unit_system:
    params = fl.SimulationParams(
        models=[
            # Fluid solver
            fl.Fluid(
                turbulence_model_solver=fl.KOmegaSST(absolute_tolerance=1e-10),
                navier_stokes_solver=fl.NavierStokesSolver(absolute_tolerance=1e-12),
            ),
            # Conjugate heat-transfer solid
            fl.Solid(
                name="CHT",
                volumes=volume_mesh["Vane"],
                material=fl.SolidMaterial(
                    name="310SS",
                    density=7900 * fl.u.kg / fl.u.m**3,
                    specific_heat_capacity=502 * fl.u.m**2 / fl.u.s**2 / fl.u.K,
                    thermal_conductivity=19.1 * fl.u.kg / fl.u.s**3 * fl.u.m / fl.u.K,
                ),
                heat_equation_solver=fl.HeatEquationSolver(
                    linear_solver=fl.LinearSolver(
                        max_iterations=20,
                        absolute_tolerance=1e-10,
                    ),
                    equation_evaluation_frequency=10,
                ),
                initial_condition=fl.HeatEquationInitialCondition(
                    temperature=f"{initial_wall_temperature}"
                ),
            ),
            # Cascade boundary conditions
            fl.Inflow(
                name="Inflow",
                total_temperature=condition.TT1_K * fl.u.K,
                spec=fl.TotalPressure(value=total_pressure_pa * fl.u.Pa),
                surfaces=volume_mesh["Domain/Inlet"],
            ),
            fl.Outflow(
                spec=fl.Pressure(value=OUTLET_PRESSURE_PA * fl.u.Pa),
                surfaces=volume_mesh["Domain/Outlet"],
            ),
            fl.SlipWall(
                name="SlipWall",
                surfaces=[volume_mesh["*Top"], volume_mesh["*Bottom"]],
            ),
            fl.Periodic(
                surface_pairs=[volume_mesh["*Periodic*"]],
                spec=fl.Translational(),
            ),
            fl.Wall(surfaces=[volume_mesh["Vane/Top1"], volume_mesh["Vane/Bottom1"]]),
            # Ten measured coolant-passage conditions from Appendix A
            *coolant_models(int(condition.Run), volume_mesh),
        ],
        operating_condition=fl.AerospaceCondition.from_mach(
            mach=condition.M1,
            alpha=0 * fl.u.deg,
            thermal_state=fl.ThermalState(
                temperature=static_temperature_k * fl.u.K,
                density=static_density,
                material=fl.Air(),
            ),
        ),
        reference_geometry=fl.ReferenceGeometry(
            moment_center=(3.302, 8.128, HEIGHT_CM / 2) * fl.u.cm,
            moment_length=(CHORD_CM, CHORD_CM, CHORD_CM) * fl.u.cm,
            area=CHORD_CM * HEIGHT_CM * fl.u.cm**2,
        ),
        time_stepping=fl.Steady(
            max_steps=6000,
            CFL=fl.AdaptiveCFL(convergence_limiting_factor=0.9),
        ),
        outputs=[
            fl.SurfaceOutput(
                entities=volume_mesh["Domain/Interface_Vane"],
                output_fields=[
                    "T",
                    "pressure_pa",
                    "heatTransferCoefficientStaticTemperature",
                ],
                output_format="paraview",
            ),
            fl.ProbeOutput(
                name="Mach Convergence",
                probe_points=outlet_probe,
                output_fields=["Mach"],
            ),
            channel_probe_output(),
        ],
    )

Submit case¶

The benchmark is one case, tagged with its solver version and turbulence model.

In [ ]:
SOLVER_VERSION = os.environ.get("SOLVER_VERSION_OVERRIDE", "release-25.9")

case = project.run_case(
    name=f"C3X_CHT_run157_KOmegaSST_no_low_{SOLVER_VERSION}",
    params=params,
    solver_version=SOLVER_VERSION,
    tags=[SOLVER_VERSION, "KOmegaSST"],
)

Wait for completion¶

Block until the case finishes. This can take a while: it is a coupled fluid and solid steady solve of up to 6000 steps.

In [ ]:
case.wait()

Postprocessing¶

The published results are three figures, each plotting a quantity along the vane surface against the run 157 measurements:

  1. Normalized vane-wall temperature, T_w / 811 K.
  2. Normalized surface heat-transfer coefficient, h / 1135.
  3. Surface static-to-total pressure ratio, P_s / P_t.

All three read the same midspan surface slice from the in-kernel case object (no project is re-opened). The x-axis is the surface distance over arc length, signed so the pressure surface runs left of zero and the suction surface right of zero, with the leading edge at the center.

Setup¶

Styling colors, the normalization constants the experiment reports against, the axis limits of the published figures, and the fields read from the surface output.

In [ ]:
os.makedirs("results", exist_ok=True)

FLOW360_COLOR = "#00643c"   # Flow360 results
REFERENCE_COLOR = "black"   # experimental reference
FLOW360_COLORS = (FLOW360_COLOR, "#0072B2")

MIDSPAN_Z_CM = 3.81
MAX_SURFACE_DISTANCE_CM = 0.2
NORMALIZATION_TEMPERATURE_K = EXPERIMENT_NORMALIZATION_TEMPERATURE_K
NORMALIZATION_HEAT_TRANSFER_COEFFICIENT = 1135.0
FIELDS = ("T", "pressure_pa", "heatTransferCoefficientStaticTemperature")
THERMAL_FIELDS = {"T_over_811K", "h_over_1135"}
YLIMS = {
    "T_over_811K": (0.675, 0.855),
    "h_over_1135": (0.22, 1.15),
    "Ps_over_Pt": (0.485, 1.025),
}

Midspan surface slice¶

Download the surface output, read the vane interface patch, and cut it at the midspan plane. Cell-centered fields are moved to the points first so the slice interpolates them.

In [ ]:
def surface_slice(case):
    with tempfile.TemporaryDirectory() as folder:
        folder = Path(folder)
        archive_path = folder / "surfaces.tar.gz"
        case.results.surfaces.to_file(str(archive_path), overwrite=True)
        with tarfile.open(archive_path, "r:*") as archive:
            archive.extractall(folder)

        matches = list(folder.rglob("surface_Domain_Interface_Vane.pvtu"))
        if len(matches) != 1:
            raise FileNotFoundError(
                "Expected one surface_Domain_Interface_Vane.pvtu in the surface results."
            )

        mesh = pv.read(matches[0])
        if isinstance(mesh, pv.MultiBlock):
            mesh = pv.merge([block for block in mesh if block is not None], merge_points=False)
        if any(field in mesh.cell_data and field not in mesh.point_data for field in FIELDS):
            mesh = mesh.cell_data_to_point_data(pass_cell_data=True)
        missing = [field for field in FIELDS if field not in mesh.point_data]
        if missing:
            raise ValueError(f"Missing fields {missing} in {matches[0].name}.")

        section = mesh.slice(normal=(0, 0, 1), origin=(0, 0, MIDSPAN_Z_CM))
        if not section.n_points:
            raise ValueError(f"The z={MIDSPAN_Z_CM} cm surface slice is empty.")
        return pd.DataFrame(
            {
                "X": section.points[:, 0],
                "Y": section.points[:, 1],
                "Z": section.points[:, 2],
                **{field: np.asarray(section.point_data[field]).reshape(-1) for field in FIELDS},
            }
        )

Map the slice onto the measured vane profile¶

The measurements are reported against surface distance over arc length on the pressure and suction branches of the vane profile. Split the tabulated vane coordinates into those two branches (the longer one is the suction side), project every slice point onto the nearer branch, keep the points that land within 2 mm of it, and record the arc fraction and which surface they belong to.

In [ ]:
def arc_length(vertices):
    return np.linalg.norm(np.diff(vertices, axis=0), axis=1).sum()


@lru_cache(maxsize=1)
def reference_surfaces():
    points = pd.read_csv(REF_DIR / "C3X_vane_coordinates.csv").sort_values("position")
    split = np.flatnonzero(points["position"].to_numpy() == 30)[0]
    first = points.iloc[: split + 1][["x_cm", "y_cm"]].to_numpy()
    second = points.iloc[split + 1 :][["x_cm", "y_cm"]].to_numpy()[::-1]
    leading_edge = 0.5 * (first[0] + second[0])
    trailing_edge = 0.5 * (first[-1] + second[-1])
    branches = [
        np.vstack((leading_edge, first, trailing_edge)),
        np.vstack((leading_edge, second, trailing_edge)),
    ]
    branches.sort(key=arc_length, reverse=True)
    return {"suction": branches[0], "pressure": branches[1]}


def project_onto(points, vertices):
    starts = vertices[:-1]
    vectors = np.diff(vertices, axis=0)
    lengths = np.linalg.norm(vectors, axis=1)
    offsets = np.r_[0.0, np.cumsum(lengths[:-1])]
    relative = points[:, None, :] - starts[None, :, :]
    fractions = np.clip(
        np.einsum("nsi,si->ns", relative, vectors) / lengths**2,
        0.0,
        1.0,
    )
    projections = starts + fractions[:, :, None] * vectors
    distances = np.linalg.norm(points[:, None, :] - projections, axis=2)
    nearest = distances.argmin(axis=1)
    rows = np.arange(len(points))
    arc_fraction = (
        offsets[nearest] + fractions[rows, nearest] * lengths[nearest]
    ) / lengths.sum()
    return distances[rows, nearest], arc_fraction


def map_to_reference(points):
    xy = points[["X", "Y"]].to_numpy()
    projections = {
        surface: project_onto(xy, vertices)
        for surface, vertices in reference_surfaces().items()
    }
    suction_distance, suction_arc = projections["suction"]
    pressure_distance, pressure_arc = projections["pressure"]
    suction = suction_distance <= pressure_distance
    distance = np.where(suction, suction_distance, pressure_distance)
    mapped = points.loc[distance <= MAX_SURFACE_DISTANCE_CM].copy()
    kept = distance <= MAX_SURFACE_DISTANCE_CM
    mapped["surface"] = np.where(suction[kept], "suction", "pressure")
    mapped["surface_distance_over_arc_length"] = np.where(
        suction[kept], suction_arc[kept], pressure_arc[kept]
    )
    if mapped.empty:
        raise ValueError("No VTK slice points mapped to the vane reference geometry.")
    return mapped

Normalize to the reported quantities¶

The solver writes non-dimensional temperature and heat-transfer coefficient, so scale them by the case's own reference state before dividing by the experiment's normalization constants. Static pressure is already in Pa and is divided by the measured inlet total pressure bound in the setup above.

In [ ]:
def normalize(mapped, case):
    mapped = mapped.copy()
    reference_temperature = float(
        case.params.operating_condition.thermal_state.temperature.to("K").value
    )
    base_cp = case.params.base_velocity**2 / case.params.base_temperature
    h0 = float((case.params.base_density * case.params.base_velocity * base_cp).value)
    mapped["T_over_811K"] = (
        mapped["T"] * reference_temperature / NORMALIZATION_TEMPERATURE_K
    )
    mapped["h_over_1135"] = (
        mapped["heatTransferCoefficientStaticTemperature"]
        * h0
        / NORMALIZATION_HEAT_TRANSFER_COEFFICIENT
    )
    mapped["Ps_over_Pt"] = mapped["pressure_pa"] / total_pressure_pa
    return mapped


mapped = normalize(map_to_reference(surface_slice(case)), case)
cases = [("KOmegaSST", mapped)]

Experimental reference data¶

The thermal measurements are tabulated as one pass around the vane, from the pressure trailing edge to the suction trailing edge; the minimum axial position marks the leading edge, so split there to label the two surfaces. The pressure measurements are already labelled by surface. The uncertainty table gives a global percentage for temperature, an absolute value in kPa for pressure, and a per-surface, per-arc-band percentage for the heat-transfer coefficient.

In [ ]:
@lru_cache(maxsize=1)
def thermal_experiment():
    data = pd.read_csv(REF_DIR / "157_thermal.csv").rename(
        columns={
            "Surface distance over arc length": "surface_distance_over_arc_length",
            "Normalized temperature (Tw/811 K)": "T_over_811K",
            "Normalized heat transfer coefficient": "h_over_1135",
        }
    )
    split = data["Axial distance over axial chord"].astype(float).argmin()
    pressure = data.iloc[: split + 1].iloc[::-1].copy()
    pressure["surface"] = "pressure"
    suction = data.iloc[split:].copy()
    suction["surface"] = "suction"
    return pd.concat((pressure, suction), ignore_index=True)


@lru_cache(maxsize=1)
def pressure_experiment():
    return pd.read_csv(REF_DIR / "157_pressure.csv")


@lru_cache(maxsize=1)
def uncertainty():
    return pd.read_csv(REF_DIR / "C3X_experimental_uncertainty.csv")


def global_uncertainty(quantity):
    rows = uncertainty().loc[
        lambda data: (data["quantity"] == quantity) & (data["basis"] == "global")
    ]
    return float(rows["plus_minus_uncertainty"].iloc[0])


def heat_transfer_uncertainty(surface, arc_fraction):
    rows = (
        uncertainty()
        .loc[
            lambda data: (data["quantity"] == "h")
            & (data["basis"] == "surface_arc")
            & (data["surface"] == surface)
        ]
        .sort_values("arc_start_percent")
    )
    percent = 100.0 * arc_fraction
    match = rows.loc[
        lambda data: (data["arc_start_percent"] <= percent)
        & ((percent < data["arc_end_percent"]) | (data["arc_end_percent"] == 100))
    ]
    return float(match["plus_minus_uncertainty"].iloc[0])


def error_bars(data, field):
    if field == "T_over_811K":
        return data[field].abs() * global_uncertainty("temperature") / 100.0
    if field == "Ps_over_Pt":
        value = (global_uncertainty("pressure") * 1000.0) / total_pressure_pa
        return pd.Series(value, index=data.index)
    return pd.Series(
        [
            abs(value) * heat_transfer_uncertainty(surface, arc) / 100.0
            for surface, arc, value in zip(
                data["surface"], data["surface_distance_over_arc_length"], data[field]
            )
        ],
        index=data.index,
    )

The comparison figure¶

One builder draws all three published plots. The Flow360 curve is drawn per surface and, for the thermal quantities, clipped to the arc range the experiment actually covers; the measurements are drawn as open markers with their uncertainty. The pressure surface is mirrored onto the negative x-axis so both surfaces share the leading edge at zero.

In [ ]:
def signed_x(data):
    sign = -1.0 if data["surface"].iloc[0] == "pressure" else 1.0
    return sign * data["surface_distance_over_arc_length"]


def thermal_range(experiment, surface, field):
    if field not in THERMAL_FIELDS:
        return None
    values = experiment.loc[
        lambda data: (data["surface"] == surface)
        & data["surface_distance_over_arc_length"].notna()
        & data[field].notna(),
        "surface_distance_over_arc_length",
    ]
    return None if values.empty else (values.min(), values.max())


def make_plot(cases, experiment, field, label, suffix):
    fig, axis = plt.subplots(figsize=(7.5, 4.8))
    for index, (model, mapped) in enumerate(cases):
        for surface in ("suction", "pressure"):
            flow = mapped[mapped["surface"] == surface].sort_values(
                "surface_distance_over_arc_length"
            )
            limits = thermal_range(experiment, surface, field)
            if limits is not None:
                flow = flow[flow["surface_distance_over_arc_length"].between(*limits)]
            if flow.empty:
                continue
            axis.plot(
                signed_x(flow),
                flow[field],
                color=FLOW360_COLORS[index % len(FLOW360_COLORS)],
                linewidth=2.5,
                label=f"Flow360 RANS, {model}" if surface == "suction" else None,
            )

    for surface, marker in (("suction", "o"), ("pressure", "s")):
        reference = experiment[experiment["surface"] == surface].sort_values(
            "surface_distance_over_arc_length"
        )
        if reference.empty:
            continue
        axis.errorbar(
            signed_x(reference),
            reference[field],
            yerr=error_bars(reference, field),
            fmt=marker,
            linestyle="none",
            markersize=5,
            markerfacecolor="none",
            markeredgecolor=to_rgba(REFERENCE_COLOR, 0.5),
            ecolor=to_rgba(REFERENCE_COLOR, 0.5),
            capsize=2,
            label="Exp., L. D. Hylton et al." if surface == "suction" else None,
        )

    axis.axvline(0.0, color="0.35", linewidth=0.8, alpha=0.6)
    axis.set(
        xlim=(-1.0, 1.0),
        ylim=YLIMS[field],
        xlabel=r"Surface distance, $S/\mathrm{arc}$",
        ylabel=label,
    )
    ticks = np.linspace(-1, 1, 11)
    axis.set_xticks(ticks, [f"{abs(tick):.1f}" if tick else "0" for tick in ticks])
    axis.text(0.23, -0.14, "Pressure", transform=axis.transAxes, ha="center", va="top")
    axis.text(0.77, -0.14, "Suction", transform=axis.transAxes, ha="center", va="top")
    axis.grid(alpha=0.3)
    axis.legend()
    fig.tight_layout()
    fig.savefig(f"results/C3X_{suffix}.png", dpi=300)
    return fig

Normalized vane-wall temperature¶

The predicted outer-wall temperature, normalized by the 811 K reference the experiment reports against. This is the primary conjugate heat-transfer result: it reflects the balance between external heating and the ten internal coolant passages.

In [ ]:
make_plot(
    cases,
    thermal_experiment(),
    "T_over_811K",
    r"$T_w\,/ 811\ \mathrm{K}$",
    "temperature_ratio",
)
plt.show()

Normalized surface heat-transfer coefficient¶

The surface heat-transfer coefficient, normalized by 1135 W/m^2/K. The fully turbulent solve has no laminar-to-turbulent transition, which is the main source of the difference from the measurements near the leading edge, around S/arc ~ 0.1.

In [ ]:
make_plot(
    cases,
    thermal_experiment(),
    "h_over_1135",
    r"$h / 1135$",
    "heat_transfer_ratio",
)
plt.show()

Surface static-to-total pressure ratio¶

The surface static pressure normalized by the measured cascade inlet total pressure, the aerodynamic check that the passage is loaded as it was in the rig.

In [ ]:
make_plot(
    cases,
    pressure_experiment(),
    "Ps_over_Pt",
    r"$P_s / P_t$",
    "pressure_ratio",
)
plt.show()