Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions flow360/component/simulation/migration/BETDisk.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ def _parse_flow360_bet_disk_dict(
log.info("You can print and correct the value and unit of `Omega` afterwards if needed.")

updated_bet_dict["chord_ref"] = updated_bet_dict["chord_ref"] * mesh_unit

# Handle sectional_radiuses: convert scalar to list if needed (for backward compatibility)
if not isinstance(updated_bet_dict["sectional_radiuses"], list):
updated_bet_dict["sectional_radiuses"] = [updated_bet_dict["sectional_radiuses"]]
updated_bet_dict["sectional_radiuses"] = updated_bet_dict["sectional_radiuses"] * mesh_unit

if "blade_line_chord" in updated_bet_dict:
Expand Down
8 changes: 7 additions & 1 deletion flow360/component/simulation/unit_system.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,15 @@ def set_current(self, unit_system: UnitSystem):
def _encode_ndarray(x):
"""
encoder for ndarray

For scalar values (ndim==0), convert to float.
For arrays (ndim>0), preserve as tuple/list even if size==1,
since Array types should remain as collections.
"""
if x.shape == ():
if x.ndim == 0:
# This is a true scalar (e.g., LengthType, not LengthType.Array)
return float(x)
# This is an array (e.g., LengthType.Array), preserve as collection
return tuple(x.tolist())


Expand Down
38 changes: 38 additions & 0 deletions tests/simulation/converter/data/single_polar.xrotor
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
XROTOR VERSION: 7.54
single_polar_test
! Rho Vso Rmu Alt
1.00 342.00 0.17170E-04 2000.0
! Rad Vel Adv Rake
3.81 1.0 4.23765e-3 0.0000
! XI0 XIW
0.13592 0.0000
! Naero
1
! Xisection
0.5
! A0deg dCLdA CLmax CLmin
-6.5 4.00 1.2500 -0.0000
! dCLdAstall dCLstall Cmconst Mcrit
0.10000 0.10000 -0.10000 0.75000
! CDmin CLCDmin dCDdCL^2
0.075000E-01 0.00000 0.40000E-02
! REref REexp
0.30000E+06 -0.70000
!LVDuct LDuct LWind
T F F
! II Nblds
10 3
! r/R C/R Beta0deg Ubody
0.1 0.113428403 30.0 0
0.2 0.113428403 25.0 0
0.3 0.113428403 20.0 0
0.4 0.113428403 15.0 0
0.5 0.113428403 10.0 0
0.6 0.113428403 5.0 0
0.7 0.113428403 2.0 0
0.8 0.113428403 0.0 0
0.9 0.113428403 -2.0 0
1.0 0.113428403 -5.0 0
! URDuct
1.0000

167 changes: 167 additions & 0 deletions tests/simulation/converter/data/single_polar_flow360_bet_disk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
{
"rotationDirectionRule": "leftHand",
"centerOfRotation": [
0,
0,
0
],
"axisOfRotation": [
0,
0,
1
],
"numberOfBlades": 3,
"radius": 150,
"omega": 0.0046,
"chordRef": 14,
"thickness": 15,
"nLoadingNodes": 20,
"MachNumbers": [
0
],
"ReynoldsNumbers": [
1000000
],
"alphas": [
-180,
-170,
-160,
-150,
-140,
-130,
-120,
-110,
-100,
-90,
-80,
-70,
-60,
-50,
-40,
-30,
-20,
-10,
0,
10,
20,
30,
40,
50,
60,
70,
80,
90,
100,
110,
120,
130,
140,
150,
160,
170,
180
],
"sectionalRadiuses": 75.0,
"twists": [
{
"radius": 75.0,
"twist": 10.0
}
],
"chords": [
{
"radius": 75.0,
"chord": 14.0
}
],
"sectionalPolars": [
{
"liftCoeffs": [
[
[
0.0,
0.1,
0.2,
0.3,
0.4,
0.5,
0.6,
0.7,
0.8,
0.9,
1.0,
0.9,
0.8,
0.7,
0.6,
0.5,
0.4,
0.3,
0.2,
0.1,
0.0,
-0.1,
-0.2,
-0.3,
-0.4,
-0.5,
-0.6,
-0.7,
-0.8,
-0.9,
-1.0,
-0.9,
-0.8,
-0.7,
-0.6,
-0.5,
0.0
]
]
],
"dragCoeffs": [
[
[
0.01,
0.012,
0.015,
0.02,
0.025,
0.03,
0.035,
0.04,
0.045,
0.05,
0.055,
0.06,
0.065,
0.07,
0.075,
0.08,
0.085,
0.09,
0.095,
0.1,
0.105,
0.1,
0.095,
0.09,
0.085,
0.08,
0.075,
0.07,
0.065,
0.06,
0.055,
0.05,
0.045,
0.04,
0.035,
0.03,
0.01
]
]
]
}
]
}
71 changes: 71 additions & 0 deletions tests/simulation/converter/test_bet_disk_flow360_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,74 @@ def test_full_flow360_bet_convert():
mesh_unit=1 * u.cm,
freestream_temperature=288.15 * u.K,
)


def test_single_polar_flow360_bet_convert():
"""
Test conversion of a BETDisk with only one sectional radius and one polar.

In V1 Flow360 JSON, sectionalRadiuses can be a scalar value (e.g., 75.0)
when there's only one polar. This test verifies that:
1. The converter properly handles scalar sectionalRadiuses (converts to list)
2. Single-element arrays remain as collections during serialization
3. The resulting BETDisk passes validation

This addresses the issue where users got validation error:
"arg '0.0 m' needs to be a collection of values of any length"
"""
disk = read_single_v1_BETDisk(
file_path="./data/single_polar_flow360_bet_disk.json",
mesh_unit=1 * u.m,
freestream_temperature=288.15 * u.K,
bet_disk_name="SinglePolarDisk",
)
assert isinstance(disk, BETDisk)
assert len(disk.sectional_radiuses) == 1
assert len(disk.sectional_polars) == 1
# Verify that sectional_radiuses is an array, not a scalar
assert hasattr(disk.sectional_radiuses, "__len__")
assert disk.sectional_radiuses[0].value == 75.0
# Test that the disk can be serialized and validated successfully
disk_dict = disk.model_dump()
# Verify the model can be reconstructed
reconstructed_disk = BETDisk.model_validate(disk_dict)
assert isinstance(reconstructed_disk, BETDisk)


def test_xrotor_single_polar():
"""
Test that XROTOR converter with single polar works correctly.

This verifies that when using BETDisk.from_xrotor() with a file containing
only one aero section, the sectional_radiuses remains as an array (not scalar)
and can be properly serialized and validated.
"""
import flow360 as fl

with fl.SI_unit_system:
bet_cylinder = fl.Cylinder(
name="BET_cylinder", center=[0, 0, 0], axis=[0, 0, 1], outer_radius=3.81, height=15
)

bet_disk = fl.BETDisk.from_xrotor(
file=fl.XROTORFile(file_path="./data/single_polar.xrotor"),
rotation_direction_rule="leftHand",
omega=0.0046 * fl.u.deg / fl.u.s,
chord_ref=14 * fl.u.m,
n_loading_nodes=20,
entities=bet_cylinder,
angle_unit=fl.u.deg,
length_unit=fl.u.m,
)

assert isinstance(bet_disk, fl.BETDisk)
assert len(bet_disk.sectional_radiuses) == 1
assert len(bet_disk.sectional_polars) == 1
# Verify sectional_radiuses is an array
assert hasattr(bet_disk.sectional_radiuses, "__len__")
assert bet_disk.sectional_radiuses.ndim == 1

# Test serialization and validation
disk_dict = bet_disk.model_dump()
reconstructed_disk = fl.BETDisk.model_validate(disk_dict)
assert isinstance(reconstructed_disk, fl.BETDisk)