"""Plain-data export helpers for sampled airfoil payloads."""
from __future__ import annotations
import json
from os import PathLike
from pathlib import Path
from typing import Literal, cast
import numpy as np
from buffalo_core.diagnostics import (
Diagnostic,
DiagnosticLocation,
DiagnosticReport,
)
from buffalo_core.numeric import as_float_scalar
from buffalo_core.typing import FloatArray, FloatInput
from numpy.typing import NDArray
from .base import Airfoil
from .sampling import (
AirfoilSurfaceSamples,
SurfaceSamples,
sample_airfoil,
sample_airfoil_at_arc_length,
sample_airfoil_at_xi,
)
[docs]
def surface_samples_to_dict(
samples: SurfaceSamples,
*,
include_parameters: bool = True,
include_derivatives: bool = True,
) -> dict[str, object]:
"""
Convert one sampled airfoil surface to plain Python data.
Parameters
----------
samples : SurfaceSamples
Sampled one-surface payload produced by the public sampling API.
include_parameters : bool, default=True
Whether to include the sampled ``xi``, surface-local arc length,
and native curve parameter vectors.
include_derivatives : bool, default=True
Whether to include slope, finite-slope flags, and curvature.
Returns
-------
dict[str, object]
Export-friendly mapping containing only plain Python scalars,
booleans, lists, and nested lists.
"""
payload: dict[str, object] = {
"coordinates": _float_matrix_to_list(samples.coordinates),
"surface_length": float(as_float_scalar(samples.surface_length)),
}
if include_parameters:
payload["xi"] = _float_vector_to_list(samples.xi)
payload["arc_length"] = _float_vector_to_list(samples.arc_length)
payload["curve_u"] = _float_vector_to_list(samples.curve_u)
if include_derivatives:
payload["slope"] = _float_vector_to_list(samples.slope)
payload["slope_is_finite"] = _bool_vector_to_list(
samples.slope_is_finite
)
payload["curvature"] = _float_vector_to_list(samples.curvature)
return payload
[docs]
def airfoil_surface_samples_to_dict(
samples: AirfoilSurfaceSamples,
*,
include_parameters: bool = True,
include_derivatives: bool = True,
include_diagnostics: bool = True,
) -> dict[str, object]:
"""
Convert a full two-surface sampling payload to plain Python data.
Parameters
----------
samples : AirfoilSurfaceSamples
Full downstream surface-sampling payload produced by the public
sampling API.
include_parameters : bool, default=True
Whether to include sampled ``xi``, surface-local arc length, and
native curve parameter vectors inside the lower and upper surface
payloads.
include_derivatives : bool, default=True
Whether to include slope, finite-slope flags, and curvature
inside the lower and upper surface payloads.
include_diagnostics : bool, default=True
Whether to include serialized structured diagnostics.
Returns
-------
dict[str, object]
Export-friendly mapping containing both sampled surfaces, point
metadata, chord length, and optional serialized diagnostics.
"""
payload: dict[str, object] = {
"lower": surface_samples_to_dict(
samples.lower,
include_parameters=include_parameters,
include_derivatives=include_derivatives,
),
"upper": surface_samples_to_dict(
samples.upper,
include_parameters=include_parameters,
include_derivatives=include_derivatives,
),
"leading_edge": _float_vector_to_list(samples.leading_edge),
"trailing_edge_upper": _float_vector_to_list(
samples.trailing_edge_upper
),
"trailing_edge_lower": _float_vector_to_list(
samples.trailing_edge_lower
),
"trailing_edge_midpoint": _float_vector_to_list(
samples.trailing_edge_midpoint
),
"chord_length": float(as_float_scalar(samples.chord_length)),
}
if include_diagnostics:
payload["diagnostics"] = _diagnostic_report_to_dict(samples.diagnostics)
return payload
[docs]
def airfoil_surface_samples_to_json(
samples: AirfoilSurfaceSamples,
*,
include_parameters: bool = True,
include_derivatives: bool = True,
include_diagnostics: bool = True,
indent: int = 2,
sort_keys: bool = True,
) -> str:
"""
Convert a full sampled airfoil payload to a JSON string.
Parameters
----------
samples : AirfoilSurfaceSamples
Full downstream surface-sampling payload produced by the public
sampling API.
include_parameters : bool, default=True
Whether to include sampled ``xi``, surface-local arc length, and
native curve parameter vectors inside the lower and upper surface
payloads.
include_derivatives : bool, default=True
Whether to include slope, finite-slope flags, and curvature
inside the lower and upper surface payloads.
include_diagnostics : bool, default=True
Whether to include serialized structured diagnostics.
indent : int, default=2
Indentation level passed to ``json.dumps``.
sort_keys : bool, default=True
Whether to sort JSON mapping keys deterministically.
Returns
-------
str
JSON serialization of the exported airfoil sampling payload.
"""
return json.dumps(
airfoil_surface_samples_to_dict(
samples,
include_parameters=include_parameters,
include_derivatives=include_derivatives,
include_diagnostics=include_diagnostics,
),
indent=indent,
sort_keys=sort_keys,
)
[docs]
def write_airfoil_surface_samples(
samples: AirfoilSurfaceSamples,
*,
path: str | PathLike[str],
file_format: Literal["json", "selig", "lednicer"],
title: str = "Buffalo Wings Airfoil",
include_parameters: bool = True,
include_derivatives: bool = True,
include_diagnostics: bool = True,
indent: int = 2,
sort_keys: bool = True,
) -> Path:
"""
Write sampled airfoil surfaces to a file format.
Parameters
----------
samples : AirfoilSurfaceSamples
Full downstream surface-sampling payload produced by the public
sampling API.
path : str | os.PathLike[str]
Destination file path for the exported output.
file_format : {"json", "selig", "lednicer"}
File format to emit.
title : str, default="Buffalo Wings Airfoil"
Title line written as the first non-ignored line of text-based
coordinate files.
include_parameters : bool, default=True
Whether to include sampled ``xi``, surface-local arc length, and
native curve parameter vectors when writing JSON.
include_derivatives : bool, default=True
Whether to include slope, finite-slope flags, and curvature when
writing JSON.
include_diagnostics : bool, default=True
Whether to include serialized structured diagnostics when writing
JSON.
indent : int, default=2
Indentation level passed to ``json.dumps`` when writing JSON.
sort_keys : bool, default=True
Whether to sort JSON mapping keys deterministically when writing
JSON.
Returns
-------
pathlib.Path
Resolved output path after writing the file.
"""
output_path = Path(path)
if file_format == "json":
output_path.write_text(
airfoil_surface_samples_to_json(
samples,
include_parameters=include_parameters,
include_derivatives=include_derivatives,
include_diagnostics=include_diagnostics,
indent=indent,
sort_keys=sort_keys,
),
encoding="utf-8",
)
return output_path
if file_format == "selig":
contents = _airfoil_surface_samples_to_selig(samples, title=title)
else:
contents = _airfoil_surface_samples_to_lednicer(samples, title=title)
output_path.write_text(contents, encoding="utf-8")
return output_path
[docs]
def write_sampled_airfoil(
airfoil: Airfoil,
*,
path: str | PathLike[str],
num_points_per_surface: int,
spacing: Literal["uniform", "cosine"] = "cosine",
file_format: Literal["json", "selig", "lednicer"],
title: str = "Buffalo Wings Airfoil",
include_parameters: bool = True,
include_derivatives: bool = True,
include_diagnostics: bool = True,
indent: int = 2,
sort_keys: bool = True,
) -> Path:
"""
Sample an airfoil and write the result to a file.
Parameters
----------
airfoil : Airfoil
Airfoil to sample.
path : str | os.PathLike[str]
Destination file path for the exported output.
num_points_per_surface : int
Number of sample points to generate on each surface.
spacing : {"uniform", "cosine"}, default="cosine"
Spacing rule used to distribute surface-local sample parameters.
file_format : {"json", "selig", "lednicer"}
File format to emit.
title : str, default="Buffalo Wings Airfoil"
Title line written as the first non-ignored line of text-based
coordinate files.
include_parameters : bool, default=True
Whether to include sampled ``xi``, surface-local arc length, and
native curve parameter vectors when writing JSON.
include_derivatives : bool, default=True
Whether to include slope, finite-slope flags, and curvature when
writing JSON.
include_diagnostics : bool, default=True
Whether to include serialized structured diagnostics when writing
JSON.
indent : int, default=2
Indentation level passed to ``json.dumps`` when writing JSON.
sort_keys : bool, default=True
Whether to sort JSON mapping keys deterministically when writing
JSON.
Returns
-------
pathlib.Path
Resolved output path after writing the file.
"""
return write_airfoil_surface_samples(
sample_airfoil(
airfoil,
num_points_per_surface=num_points_per_surface,
spacing=spacing,
),
path=path,
file_format=file_format,
title=title,
include_parameters=include_parameters,
include_derivatives=include_derivatives,
include_diagnostics=include_diagnostics,
indent=indent,
sort_keys=sort_keys,
)
[docs]
def write_sampled_airfoil_at_xi(
airfoil: Airfoil,
*,
path: str | PathLike[str],
lower: FloatInput,
upper: FloatInput,
file_format: Literal["json", "selig", "lednicer"],
title: str = "Buffalo Wings Airfoil",
include_parameters: bool = True,
include_derivatives: bool = True,
include_diagnostics: bool = True,
indent: int = 2,
sort_keys: bool = True,
) -> Path:
"""
Write a file from explicit ``xi`` surface samples.
Parameters
----------
airfoil : Airfoil
Airfoil to sample.
path : str | os.PathLike[str]
Destination file path for the exported output.
lower : FloatInput
Lower-surface ``xi`` values in ``[0, 1]`` ordered from leading edge
to trailing edge.
upper : FloatInput
Upper-surface ``xi`` values in ``[0, 1]`` ordered from leading edge
to trailing edge.
file_format : {"json", "selig", "lednicer"}
File format to emit.
title : str, default="Buffalo Wings Airfoil"
Title line written as the first non-ignored line of text-based
coordinate files.
include_parameters : bool, default=True
Whether to include sampled ``xi``, surface-local arc length, and
native curve parameter vectors when writing JSON.
include_derivatives : bool, default=True
Whether to include slope, finite-slope flags, and curvature when
writing JSON.
include_diagnostics : bool, default=True
Whether to include serialized structured diagnostics when writing
JSON.
indent : int, default=2
Indentation level passed to ``json.dumps`` when writing JSON.
sort_keys : bool, default=True
Whether to sort JSON mapping keys deterministically when writing
JSON.
Returns
-------
pathlib.Path
Resolved output path after writing the file.
"""
return write_airfoil_surface_samples(
sample_airfoil_at_xi(airfoil, lower=lower, upper=upper),
path=path,
file_format=file_format,
title=title,
include_parameters=include_parameters,
include_derivatives=include_derivatives,
include_diagnostics=include_diagnostics,
indent=indent,
sort_keys=sort_keys,
)
[docs]
def write_sampled_airfoil_at_arc_length(
airfoil: Airfoil,
*,
path: str | PathLike[str],
lower: FloatInput,
upper: FloatInput,
file_format: Literal["json", "selig", "lednicer"],
title: str = "Buffalo Wings Airfoil",
include_parameters: bool = True,
include_derivatives: bool = True,
include_diagnostics: bool = True,
indent: int = 2,
sort_keys: bool = True,
) -> Path:
"""
Write a file from explicit arc-length surface samples.
Parameters
----------
airfoil : Airfoil
Airfoil to sample.
path : str | os.PathLike[str]
Destination file path for the exported output.
lower : FloatInput
Lower-surface arc lengths ordered from leading edge to trailing
edge.
upper : FloatInput
Upper-surface arc lengths ordered from leading edge to trailing
edge.
file_format : {"json", "selig", "lednicer"}
File format to emit.
title : str, default="Buffalo Wings Airfoil"
Title line written as the first non-ignored line of text-based
coordinate files.
include_parameters : bool, default=True
Whether to include sampled ``xi``, surface-local arc length, and
native curve parameter vectors when writing JSON.
include_derivatives : bool, default=True
Whether to include slope, finite-slope flags, and curvature when
writing JSON.
include_diagnostics : bool, default=True
Whether to include serialized structured diagnostics when writing
JSON.
indent : int, default=2
Indentation level passed to ``json.dumps`` when writing JSON.
sort_keys : bool, default=True
Whether to sort JSON mapping keys deterministically when writing
JSON.
Returns
-------
pathlib.Path
Resolved output path after writing the file.
"""
return write_airfoil_surface_samples(
sample_airfoil_at_arc_length(airfoil, lower=lower, upper=upper),
path=path,
file_format=file_format,
title=title,
include_parameters=include_parameters,
include_derivatives=include_derivatives,
include_diagnostics=include_diagnostics,
indent=indent,
sort_keys=sort_keys,
)
def _float_vector_to_list(values: FloatArray) -> list[float]:
"""
Convert a one-dimensional float array to Python floats.
Parameters
----------
values : numpy.ndarray
One-dimensional float array to convert.
Returns
-------
list[float]
Converted Python float values.
"""
return cast(list[float], values.tolist())
def _float_matrix_to_list(values: FloatArray) -> list[list[float]]:
"""
Convert a two-dimensional float array to nested Python floats.
Parameters
----------
values : numpy.ndarray
Two-dimensional float array to convert.
Returns
-------
list[list[float]]
Converted nested Python float values.
"""
return cast(list[list[float]], values.tolist())
def _bool_vector_to_list(values: NDArray[np.bool_]) -> list[bool]:
"""
Convert a one-dimensional boolean array to Python booleans.
Parameters
----------
values : numpy.ndarray
One-dimensional boolean array to convert.
Returns
-------
list[bool]
Converted Python boolean values.
"""
return cast(list[bool], values.tolist())
def _diagnostic_location_to_dict(
location: DiagnosticLocation | None,
) -> dict[str, object] | None:
"""
Convert optional diagnostic location metadata to plain data.
Parameters
----------
location : DiagnosticLocation | None
Optional diagnostic location metadata.
Returns
-------
dict[str, object] | None
Plain-data diagnostic location mapping, or ``None`` when the
diagnostic has no attached location.
"""
if location is None:
return None
return {
"object_path": location.object_path,
"field_path": location.field_path,
"geometry_region": location.geometry_region,
}
def _diagnostic_to_dict(diagnostic: Diagnostic) -> dict[str, object]:
"""
Convert one structured diagnostic to plain Python data.
Parameters
----------
diagnostic : Diagnostic
Structured diagnostic to serialize.
Returns
-------
dict[str, object]
Plain-data diagnostic mapping.
"""
return {
"severity": diagnostic.severity.value,
"code": diagnostic.code,
"message": diagnostic.message,
"location": _diagnostic_location_to_dict(diagnostic.location),
}
def _diagnostic_report_to_dict(
report: DiagnosticReport,
) -> dict[str, object]:
"""
Convert a diagnostic report to plain Python data.
Parameters
----------
report : DiagnosticReport
Structured diagnostic report to serialize.
Returns
-------
dict[str, object]
Plain-data report containing diagnostic entries and summary
severity flags.
"""
return {
"entries": [_diagnostic_to_dict(entry) for entry in report.entries],
"has_errors": report.has_errors,
"has_warnings": report.has_warnings,
"has_infos": report.has_infos,
}
def _airfoil_surface_samples_to_selig(
samples: AirfoilSurfaceSamples,
*,
title: str,
) -> str:
"""
Convert sampled lower and upper surfaces to Selig text.
Parameters
----------
samples : AirfoilSurfaceSamples
Full downstream surface-sampling payload.
title : str
Title line written as the first non-ignored line of the file.
Returns
-------
str
Selig-format coordinate text.
"""
upper_coordinates = np.flip(samples.upper.coordinates, axis=0)
lower_coordinates = samples.lower.coordinates[1:]
all_coordinates = np.vstack((upper_coordinates, lower_coordinates))
lines = [title]
lines.extend(_format_coordinates(all_coordinates))
return "\n".join(lines) + "\n"
def _airfoil_surface_samples_to_lednicer(
samples: AirfoilSurfaceSamples,
*,
title: str,
) -> str:
"""
Convert sampled lower and upper surfaces to Lednicer text.
Parameters
----------
samples : AirfoilSurfaceSamples
Full downstream surface-sampling payload.
title : str
Title line written as the first non-ignored line of the file.
Returns
-------
str
Lednicer-format coordinate text.
"""
upper_coordinates = samples.upper.coordinates
lower_coordinates = samples.lower.coordinates
lines = [
title,
f"{upper_coordinates.shape[0]} {lower_coordinates.shape[0]}",
*_format_coordinates(upper_coordinates),
*_format_coordinates(lower_coordinates),
]
return "\n".join(lines) + "\n"
def _format_coordinates(values: FloatArray) -> list[str]:
"""
Format coordinate rows for text-based airfoil export.
Parameters
----------
values : FloatArray
``(n, 2)`` array of sampled coordinates.
Returns
-------
list[str]
Coordinate rows formatted as whitespace-separated ``x y`` pairs.
"""
return [f"{row[0]:.16g} {row[1]:.16g}" for row in values]