Source code for buffalo_panel.config.internal.schema

"""Dataclasses for the structured Buffalo Panel case schema."""

from __future__ import annotations

from dataclasses import dataclass, field, fields, is_dataclass
from types import NoneType, UnionType
from typing import (
    Any,
    Literal,
    Union,
    cast,
    get_args,
    get_origin,
    get_type_hints,
)

from buffalo_core.schema import (
    SchemaChoice,
    SchemaCondition,
    SchemaField,
)
from buffalo_core.schema import (
    schema_field_metadata as _core_schema_field_metadata,
)
from buffalo_wings.airfoil import AirfoilDefinitionSpec, Point2DSpec

type LengthUnit = Literal["m", "ft"]
type TimeUnit = Literal["s", "min"]
type AngleUnit = Literal["deg", "rad"]
type SolverFormulation = Literal["hess_smith", "lumped_vortex"]
type AirfoilSpacing = Literal["uniform", "cosine"]
type BodyDiscretization = Literal["thick_body", "thin_body"]
type SurfaceQuantity = Literal["velocity", "cp"]
type IntegratedQuantity = Literal[
    "cl_pressure",
    "cl_circulation",
    "cd_pressure",
    "cm_pressure",
]
type FieldQuantity = Literal["velocity", "potential", "stream_function"]
type SchemaFieldMetadata = dict[str, object]
type SchemaConditionMetadata = dict[str, object]

_PAIR_ARG_COUNT = 2


[docs] @dataclass(frozen=True, slots=True) class SchemaFieldTree: """Tree node describing one schema field.""" name: str """Field name on the parent dataclass.""" type_name: str """Display-friendly type name for the field.""" optional: bool """Whether the field accepts ``None``.""" docstring: str | None """Short field documentation text, when available.""" metadata: SchemaFieldMetadata """Structured machine-readable field metadata.""" object_schema: SchemaClassTree | None = None """Nested dataclass schema when the field value is a dataclass object.""" item_type_name: str | None = None """Display-friendly item type name for list, tuple, or mapping values.""" item_schema: SchemaClassTree | None = None """Nested dataclass schema for list, tuple, or mapping items when known."""
[docs] @dataclass(frozen=True, slots=True) class SchemaClassTree: """Tree node describing one schema dataclass.""" class_name: str """Dataclass name.""" docstring: str | None """Top-level class documentation string.""" fields: tuple[SchemaFieldTree, ...] """Ordered field descriptions."""
schema_field_metadata = _core_schema_field_metadata def _unwrap_optional(target_type: Any) -> tuple[object, bool]: """Return the non-optional type plus whether ``None`` is allowed.""" origin = get_origin(target_type) if origin is UnionType or origin is Union: args = tuple( arg for arg in get_args(target_type) if arg is not NoneType ) if len(args) + 1 == len(get_args(target_type)): if len(args) == 1: return args[0], True return target_type, True return target_type, False def _type_name(target_type: Any) -> str: """Return a display-friendly type name.""" origin = get_origin(target_type) if origin is None: if isinstance(target_type, type): return target_type.__name__ return str(target_type) args = get_args(target_type) origin_name = getattr(origin, "__name__", str(origin)) if args: return f"{origin_name}[{', '.join(_type_name(arg) for arg in args)}]" return origin_name def _field_docstring(metadata: SchemaFieldMetadata) -> str | None: """Return the best available short doc text for a field.""" short_help = metadata.get("short_help") notes = metadata.get("notes") if isinstance(short_help, str) and isinstance(notes, str): return f"{short_help} {notes}" if isinstance(short_help, str): return short_help if isinstance(notes, str): return notes return None def _item_schema_for_type( target_type: Any, ) -> tuple[str | None, SchemaClassTree | None]: """Return item type display name and nested schema when available.""" origin = get_origin(target_type) args = get_args(target_type) if origin is list and args: item_type = args[0] return _type_name(item_type), ( schema_tree(cast(type[object], item_type)) if isinstance(item_type, type) and is_dataclass(item_type) else None ) if origin is tuple and args: item_type = ( args[0] if len(args) == _PAIR_ARG_COUNT and args[1] is Ellipsis else None ) if item_type is not None: return _type_name(item_type), ( schema_tree(cast(type[object], item_type)) if isinstance(item_type, type) and is_dataclass(item_type) else None ) return "mixed", None if origin is dict and len(args) == _PAIR_ARG_COUNT: item_type = args[1] return _type_name(item_type), ( schema_tree(cast(type[object], item_type)) if isinstance(item_type, type) and is_dataclass(item_type) else None ) return None, None
[docs] def schema_tree(cls: type[object]) -> SchemaClassTree: """Return a nested tree description for one schema dataclass. The returned object gives GUI and editor code one machine-readable structure containing: - ordered field descriptions - nested dataclass children - optionality - display-friendly type names - field-level documentation text - the structured field metadata used for validation and presentation """ if not is_dataclass(cls): raise TypeError("cls must be a dataclass type.") type_hints = get_type_hints(cls) field_trees: list[SchemaFieldTree] = [] for field_info in fields(cls): annotated_type = type_hints[field_info.name] value_type, optional = _unwrap_optional(annotated_type) resolved_value_type = value_type metadata = dict(field_info.metadata) object_schema = ( schema_tree(cast(type[object], resolved_value_type)) if isinstance(resolved_value_type, type) and is_dataclass(resolved_value_type) else None ) item_type_name, item_schema = _item_schema_for_type(resolved_value_type) field_trees.append( SchemaFieldTree( name=field_info.name, type_name=_type_name(resolved_value_type), optional=optional, docstring=_field_docstring(metadata), metadata=metadata, object_schema=object_schema, item_type_name=item_type_name, item_schema=item_schema, ) ) return SchemaClassTree( class_name=cls.__name__, docstring=cls.__doc__.strip() if cls.__doc__ is not None else None, fields=tuple(field_trees), )
[docs] @dataclass(slots=True) class UnitsSpec: """Unit declarations used by serialized panel case files.""" length: LengthUnit = field( default="m", metadata=SchemaField( value_kind="enum", required=False, label="Length Unit", order=10, short_help="Unit used by geometric and reference-length inputs.", default="m", choices=( SchemaChoice(value="m", label="m"), SchemaChoice(value="ft", label="ft"), ), ).to_metadata(), ) """Length unit used by geometric input and reference metadata.""" time: TimeUnit = field( default="s", metadata=SchemaField( value_kind="enum", required=False, label="Time Unit", order=20, short_help="Unit used with the legnth unit for velocity inputs.", default="s", choices=( SchemaChoice(value="s", label="s"), SchemaChoice(value="min", label="min"), ), ).to_metadata(), ) """Time unit used with the length unit for velocity metadata.""" angle: AngleUnit = field( default="deg", metadata=SchemaField( value_kind="enum", required=False, label="Angle Unit", order=30, short_help="Unit used by freestream and placement angles.", default="deg", choices=( SchemaChoice(value="deg", label="deg"), SchemaChoice(value="rad", label="rad"), ), ).to_metadata(), ) """Angle unit used by freestream and placement metadata."""
[docs] @dataclass(slots=True) class CaseMetadataSpec: """Human-readable metadata for one panel case.""" name: str = field( metadata=SchemaField( value_kind="string", required=True, label="Case Name", order=10, short_help="Unique identifier used for reports and outputs.", format_hint="non-empty case identifier", ).to_metadata() ) """Unique case name used in reports and output paths.""" description: str | None = field( default=None, metadata=SchemaField( value_kind="string", required=False, label="Description", order=20, short_help="Optional free-form summary of the case.", notes="Optional free-form case description.", advanced=True, ).to_metadata(), ) """Optional free-form case description."""
[docs] @dataclass(slots=True) class SolverSpec: """Solver selection and backend controls.""" formulation: SolverFormulation = field( default="hess_smith", metadata=SchemaField( value_kind="enum", required=False, label="Formulation", order=10, short_help="Potential-flow formulation used by the solve path.", default="hess_smith", choices=( SchemaChoice(value="hess_smith", label="Hess-Smith"), SchemaChoice( value="lumped_vortex", label="Lumped Vortex", ), ), ).to_metadata(), ) """Potential-flow formulation requested by the case.""" backend: str = field( default="python", metadata=SchemaField( value_kind="enum", required=False, label="Backend", order=20, short_help="Registered kernel backend label.", default="python", format_hint="registered backend label", choices=(SchemaChoice(value="python", label="Python"),), advanced=True, shown_when=( SchemaCondition( path="$.solver.formulation", operator="in", value=("hess_smith", "lumped_vortex"), message=( "Backend selection is currently defined per solver " "formulation." ), ), ), ).to_metadata(), ) """Kernel backend selected from the kernel registry."""
[docs] @dataclass(slots=True) class FreestreamSpec: """Freestream state for the case.""" speed: float = field( metadata=SchemaField( value_kind="float", required=True, label="Speed", order=10, short_help="Freestream speed magnitude.", minimum=0.0, ).to_metadata() ) """Freestream speed magnitude.""" alpha: float = field( metadata=SchemaField( value_kind="float", required=True, label="Angle of Attack", order=20, short_help=( "Freestream angle of attack in the configured angle unit." ), notes="Angle unit follows UnitsSpec.angle.", ).to_metadata() ) """Freestream angle of attack in ``units.angle``.""" mach: float | None = field( default=None, metadata=SchemaField( value_kind="float", required=False, label="Mach Number", order=30, short_help="Freestream Mach number for future compressibility use.", minimum=0.0, advanced=True, ).to_metadata(), ) """Freestream Mach number.""" reynolds: float | None = field( default=None, metadata=SchemaField( value_kind="float", required=False, label="Reynolds Number", order=40, short_help="Freestream Reynolds number for future viscous models.", exclusive_minimum=0.0, advanced=True, ).to_metadata(), ) """Freestream Reynolds number."""
[docs] @dataclass(slots=True) class AirfoilSamplingSpec: """Buffalo Wings airfoil sampling controls.""" num_points_per_surface: int = field( metadata=SchemaField( value_kind="int", required=True, label="Points Per Surface", order=10, short_help=( "Number of sample points requested on each airfoil surface." ), minimum=2, ).to_metadata() ) """Number of samples requested on each airfoil surface.""" spacing: AirfoilSpacing = field( default="cosine", metadata=SchemaField( value_kind="enum", required=False, label="Spacing", order=20, short_help="Spacing rule used when sampling the airfoil surfaces.", default="cosine", choices=("uniform", "cosine"), ).to_metadata(), ) """Surface sampling spacing rule."""
[docs] @dataclass(slots=True) class AirfoilPlacement2DSpec: """Rigid placement and scale applied to a Buffalo Wings airfoil boundary.""" scale: float = field( default=1.0, metadata=SchemaField( value_kind="float", required=False, label="Scale", order=10, short_help=( "Uniform scale applied to the unit-chord airfoil coordinates." ), default=1.0, exclusive_minimum=0.0, ).to_metadata(), ) """Uniform scale applied to the unit-chord Buffalo Wings coordinates.""" rotation: float = field( default=0.0, metadata=SchemaField( value_kind="float", required=False, label="Rotation", order=20, short_help="Rigid-body rotation applied after scaling.", default=0.0, notes="Angle unit follows UnitsSpec.angle.", advanced=True, ).to_metadata(), ) """Rotation angle applied after scaling.""" rotation_point: Point2DSpec = field( default=(0.0, 0.0), metadata=SchemaField( value_kind="tuple", required=False, label="Rotation Point", order=30, short_help="Body-frame point about which rigid rotation occurs.", default=(0.0, 0.0), tuple_length=2, item_kind="float", advanced=True, ).to_metadata(), ) """Body-frame point about which rotation is applied.""" translation: Point2DSpec = field( default=(0.0, 0.0), metadata=SchemaField( value_kind="tuple", required=False, label="Translation", order=40, short_help="Global x and y translation applied after rotation.", default=(0.0, 0.0), tuple_length=2, item_kind="float", ).to_metadata(), ) """Global translation applied after scaling and rotation."""
[docs] @dataclass(slots=True) class ReferenceSpec: """Reference quantities used to nondimensionalize integrated results.""" speed: float | None = field( default=None, metadata=SchemaField( value_kind="float", required=False, label="Reference Speed", order=10, short_help="Reference speed used for integrated coefficients.", exclusive_minimum=0.0, notes="If omitted, the runtime currently uses freestream.speed.", advanced=True, shown_when=( SchemaCondition( path="$.post.integrated.quantities", operator="non_empty", message=( "Reference speed matters when integrated " "coefficients are requested." ), ), ), ).to_metadata(), ) """Reference speed for integrated coefficient normalization.""" length: float | None = field( default=None, metadata=SchemaField( value_kind="float", required=False, label="Reference Length", order=20, short_help="Reference length used for integrated coefficients.", exclusive_minimum=0.0, notes=( "If omitted, the runtime currently uses body placement.scale." ), advanced=True, shown_when=( SchemaCondition( path="$.post.integrated.quantities", operator="non_empty", message=( "Reference length matters when integrated " "coefficients are requested." ), ), ), ).to_metadata(), ) """Reference length for force and moment coefficients.""" moment_point: Point2DSpec = field( default=(0.25, 0.0), metadata=SchemaField( value_kind="tuple", required=False, label="Moment Point", order=30, short_help=( "Global x and y point used for pitching-moment reference." ), default=(0.25, 0.0), tuple_length=2, item_kind="float", shown_when=( SchemaCondition( path="$.post.integrated.quantities", operator="non_empty", message=( "Moment reference is only needed when integrated " "coefficients are requested." ), ), ), ).to_metadata(), ) """Moment reference point in global coordinates."""
[docs] @dataclass(slots=True) class AirfoilBody2DSpec: """Two-dimensional body generated from an embedded Buffalo Wings airfoil.""" id: str = field( metadata=SchemaField( value_kind="string", required=True, label="Body ID", order=10, short_help="Unique identifier for this body within the case.", format_hint="unique body identifier", ).to_metadata() ) """Unique body identifier within the panel case.""" airfoil: AirfoilDefinitionSpec = field( metadata=SchemaField( value_kind="object", required=True, label="Airfoil", order=20, short_help=( "Embedded Buffalo Wings airfoil definition for this body." ), ).to_metadata() ) """Embedded Buffalo Wings airfoil definition for this body.""" sampling: AirfoilSamplingSpec = field( metadata=SchemaField( value_kind="object", required=True, label="Sampling", order=30, short_help="Buffalo Wings airfoil sampling settings.", ).to_metadata() ) """Buffalo Wings airfoil sampling controls.""" discretization: BodyDiscretization = field( default="thick_body", metadata=SchemaField( value_kind="enum", required=False, label="Discretization", order=35, short_help=( "Choose whether this body is panelized as a thick boundary " "or a thin camber line." ), default="thick_body", choices=( SchemaChoice( value="thick_body", label="Thick Body", ), SchemaChoice( value="thin_body", label="Thin Body", ), ), ).to_metadata(), ) """Discretization model used to convert the sampled airfoil into panels.""" placement: AirfoilPlacement2DSpec = field( default_factory=AirfoilPlacement2DSpec, metadata=SchemaField( value_kind="object", required=False, label="Placement", order=40, short_help=( "Scale, rotation, and translation applied to the sampled " "airfoil." ), ).to_metadata(), ) """Placement applied to sampled airfoil coordinates."""
[docs] @dataclass(slots=True) class GeometrySpec: """Geometry block for all bodies in one panel case.""" bodies: list[AirfoilBody2DSpec] = field( default_factory=list, metadata=SchemaField( value_kind="list", required=False, label="Bodies", order=10, short_help="Ordered list of bodies included in the case geometry.", min_items=1, item_kind="AirfoilBody2DSpec", collection_add_label="Add Body", collection_item_label_field="id", collection_item_creation_hint=( "Create a new airfoil body with an embedded airfoil definition." ), ).to_metadata(), ) """Ordered body definitions included in the case."""
[docs] @dataclass(slots=True) class SurfacePostSpec: """Surface post-processing requests.""" quantities: list[SurfaceQuantity] = field( default_factory=list, metadata=SchemaField( value_kind="list", required=False, label="Surface Quantities", order=10, short_help="Surface quantities requested from post-processing.", min_items=0, item_kind="enum", choices=( SchemaChoice( value="velocity", label="Surface Velocity", ), SchemaChoice(value="cp", label="Cp"), SchemaChoice( value="panel_lift_coefficient", label="Panel Lift Coefficient", ), ), ).to_metadata(), ) """Requested surface quantities."""
[docs] @dataclass(slots=True) class IntegratedPostSpec: """Integrated post-processing requests.""" quantities: list[IntegratedQuantity] = field( default_factory=list, metadata=SchemaField( value_kind="list", required=False, label="Integrated Quantities", order=10, short_help=( "Integrated aerodynamic coefficients requested from " "post-processing." ), min_items=0, item_kind="enum", choices=( SchemaChoice(value="cl_pressure", label="CL Pressure"), SchemaChoice( value="cl_circulation", label="CL Circulation", ), SchemaChoice(value="cd_pressure", label="CD Pressure"), SchemaChoice(value="cm_pressure", label="CM Pressure"), ), ).to_metadata(), ) """Requested integrated quantities."""
[docs] @dataclass(slots=True) class FieldGridSpec: """Structured Cartesian field grid request.""" x: tuple[float, float, int] = field( metadata=SchemaField( value_kind="tuple", required=True, label="X Grid", order=10, short_help=( "Cartesian x-grid definition as minimum, maximum, and count." ), tuple_length=3, format_hint="(minimum, maximum, count)", notes="count should be a positive integer.", shown_when=( SchemaCondition( path="$.post.field.quantities", operator="non_empty", message=( "Field grid settings matter when field outputs are " "requested." ), ), ), ).to_metadata() ) """X-grid definition as ``(minimum, maximum, count)``.""" y: tuple[float, float, int] = field( metadata=SchemaField( value_kind="tuple", required=True, label="Y Grid", order=20, short_help=( "Cartesian y-grid definition as minimum, maximum, and count." ), tuple_length=3, format_hint="(minimum, maximum, count)", notes="count should be a positive integer.", shown_when=( SchemaCondition( path="$.post.field.quantities", operator="non_empty", message=( "Field grid settings matter when field outputs are " "requested." ), ), ), ).to_metadata() ) """Y-grid definition as ``(minimum, maximum, count)``.""" mask_inside_bodies: bool = field( default=True, metadata=SchemaField( value_kind="bool", required=False, label="Mask Inside Bodies", order=30, short_help=( "Whether field points inside closed bodies should be masked." ), default=True, advanced=True, shown_when=( SchemaCondition( path="$.post.field.quantities", operator="non_empty", message=( "Body masking only affects requested field-grid " "outputs." ), ), ), ).to_metadata(), ) """Whether field points inside closed bodies should be masked."""
[docs] @dataclass(slots=True) class FieldPostSpec: """Field post-processing requests.""" grid: FieldGridSpec = field( metadata=SchemaField( value_kind="object", required=True, label="Grid", order=10, short_help=( "Structured Cartesian grid definition for field evaluation." ), shown_when=( SchemaCondition( path="$.post.field.quantities", operator="non_empty", message=( "Grid definition is only relevant when field " "outputs are requested." ), ), ), ).to_metadata() ) """Cartesian grid on which field quantities should be evaluated.""" quantities: list[FieldQuantity] = field( default_factory=list, metadata=SchemaField( value_kind="list", required=False, label="Field Quantities", order=20, short_help="Field quantities requested on the configured grid.", min_items=0, item_kind="enum", choices=( "velocity", "potential", SchemaChoice( value="stream_function", label="Stream Function", ), ), ).to_metadata(), ) """Requested field quantities."""
[docs] @dataclass(slots=True) class PostProcessingSpec: """Optional post-processing requests for a case.""" surface: SurfacePostSpec | None = field( default=None, metadata=SchemaField( value_kind="object", required=False, label="Surface Post", order=10, short_help="Requested surface post-processing outputs.", ).to_metadata(), ) """Surface quantity requests.""" integrated: IntegratedPostSpec | None = field( default=None, metadata=SchemaField( value_kind="object", required=False, label="Integrated Post", order=20, short_help="Requested integrated post-processing outputs.", ).to_metadata(), ) """Integrated quantity requests.""" field: FieldPostSpec | None = field( default=None, metadata=SchemaField( value_kind="object", required=False, label="Field Post", order=30, short_help="Requested field-grid post-processing outputs.", advanced=True, ).to_metadata(), ) """Field quantity requests."""
[docs] @dataclass(slots=True) class PanelCaseSpec: """Top-level Buffalo Panel case schema.""" schema_version: int = field( metadata=SchemaField( value_kind="int", required=True, label="Schema Version", order=10, short_help="Version of the serialized panel-case schema.", choices=(1,), minimum=1, notes="Only schema version 1 is currently supported.", ).to_metadata() ) """Schema version number for the serialized panel case.""" units: UnitsSpec = field( metadata=SchemaField( value_kind="object", required=True, label="Units", order=20, short_help="Top-level unit declarations used throughout the case.", ).to_metadata() ) """Unit declarations used by the serialized case.""" case: CaseMetadataSpec = field( metadata=SchemaField( value_kind="object", required=True, label="Case Metadata", order=30, short_help="Human-readable name and description for the case.", ).to_metadata() ) """Human-readable case metadata.""" solver: SolverSpec = field( metadata=SchemaField( value_kind="object", required=True, label="Solver", order=40, short_help="Solver formulation and backend settings.", ).to_metadata() ) """Solver and backend selection.""" freestream: FreestreamSpec = field( metadata=SchemaField( value_kind="object", required=True, label="Freestream", order=50, short_help="Freestream speed and angle-of-attack settings.", ).to_metadata() ) """Freestream state.""" geometry: GeometrySpec = field( metadata=SchemaField( value_kind="object", required=True, label="Geometry", order=60, short_help="Embedded body geometry, airfoil, and placement data.", ).to_metadata() ) """Geometry definitions for the case.""" reference: ReferenceSpec = field( default_factory=ReferenceSpec, metadata=SchemaField( value_kind="object", required=False, label="Reference", order=70, short_help="Reference quantities used for integrated coefficients.", advanced=True, ).to_metadata(), ) """Reference quantities for integrated coefficient normalization.""" post: PostProcessingSpec | None = field( default=None, metadata=SchemaField( value_kind="object", required=False, label="Post-Processing", order=80, short_help=( "Optional surface, integrated, and field output requests." ), advanced=True, ).to_metadata(), ) """Optional post-processing requests."""