Skip to content

Commit 1d1485b

Browse files
authored
[FXC-3159] Add TimeAverageForceDistributionOutput (#1623)
* [FXC-3159] Add TimeAverageForceDistributionOutput * Remove unnecessary code * Apply comments
1 parent 393a158 commit 1d1485b

File tree

6 files changed

+125
-4
lines changed

6 files changed

+125
-4
lines changed

flow360/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
SurfaceOutput,
143143
SurfaceProbeOutput,
144144
SurfaceSliceOutput,
145+
TimeAverageForceDistributionOutput,
145146
TimeAverageIsosurfaceOutput,
146147
TimeAverageProbeOutput,
147148
TimeAverageSliceOutput,
@@ -258,6 +259,7 @@
258259
"VolumeOutput",
259260
"TimeAverageVolumeOutput",
260261
"ForceDistributionOutput",
262+
"TimeAverageForceDistributionOutput",
261263
"SliceOutput",
262264
"TimeAverageSliceOutput",
263265
"IsosurfaceOutput",

flow360/component/simulation/outputs/outputs.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,6 +1314,37 @@ class ForceDistributionOutput(Flow360BaseModel):
13141314
)
13151315

13161316

1317+
class TimeAverageForceDistributionOutput(ForceDistributionOutput):
1318+
"""
1319+
:class:`TimeAverageForceDistributionOutput` class for time-averaged customized force and moment distribution output.
1320+
Axis-aligned components are output for force and moment coefficients at the end of the simulation.
1321+
1322+
Example
1323+
-------
1324+
1325+
Calculate the average value starting from the :math:`4^{th}` physical step.
1326+
1327+
>>> fl.TimeAverageForceDistributionOutput(
1328+
... name="spanwise",
1329+
... distribution_direction=[0.1, 0.9, 0.0],
1330+
... start_step=4,
1331+
... )
1332+
1333+
====
1334+
"""
1335+
1336+
name: str = pd.Field(
1337+
"Time average force distribution output",
1338+
description="Name of the `TimeAverageForceDistributionOutput`.",
1339+
)
1340+
start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field(
1341+
default=-1, description="Physical time step to start calculating averaging."
1342+
)
1343+
output_type: Literal["TimeAverageForceDistributionOutput"] = pd.Field(
1344+
"TimeAverageForceDistributionOutput", frozen=True
1345+
)
1346+
1347+
13171348
OutputTypes = Annotated[
13181349
Union[
13191350
SurfaceOutput,
@@ -1334,6 +1365,7 @@ class ForceDistributionOutput(Flow360BaseModel):
13341365
StreamlineOutput,
13351366
TimeAverageStreamlineOutput,
13361367
ForceDistributionOutput,
1368+
TimeAverageForceDistributionOutput,
13371369
],
13381370
pd.Field(discriminator="output_type"),
13391371
]
@@ -1346,6 +1378,7 @@ class ForceDistributionOutput(Flow360BaseModel):
13461378
TimeAverageProbeOutput,
13471379
TimeAverageSurfaceProbeOutput,
13481380
TimeAverageStreamlineOutput,
1381+
TimeAverageForceDistributionOutput,
13491382
)
13501383

13511384
MonitorOutputType = Annotated[

flow360/component/simulation/translator/solver_translator.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
SurfaceOutput,
7575
SurfaceProbeOutput,
7676
SurfaceSliceOutput,
77+
TimeAverageForceDistributionOutput,
7778
TimeAverageIsosurfaceOutput,
7879
TimeAverageProbeOutput,
7980
TimeAverageSliceOutput,
@@ -655,14 +656,27 @@ def translate_force_distribution_output(output_params: list):
655656
"""Translate force distribution output settings."""
656657
force_distribution_output = {}
657658
for output in output_params:
658-
if isinstance(output, ForceDistributionOutput):
659+
if is_exact_instance(output, ForceDistributionOutput):
659660
force_distribution_output[output.name] = {
660661
"direction": list(output.distribution_direction),
661662
"type": output.distribution_type,
662663
}
663664
return force_distribution_output
664665

665666

667+
def translate_time_averaged_force_distribution_output(output_params: list):
668+
"""Translate time-averaged force distribution output settings."""
669+
time_averaged_force_distribution_output = {}
670+
for output in output_params:
671+
if isinstance(output, TimeAverageForceDistributionOutput):
672+
time_averaged_force_distribution_output[output.name] = {
673+
"direction": list(output.distribution_direction),
674+
"type": output.distribution_type,
675+
"startAverageIntegrationStep": output.start_step,
676+
}
677+
return time_averaged_force_distribution_output
678+
679+
666680
def user_variable_to_udf(
667681
variable: UserVariable, input_params: SimulationParams
668682
): # pylint:disable=too-many-branches
@@ -1012,6 +1026,12 @@ def translate_output(input_params: SimulationParams, translated: dict):
10121026
if has_instance_in_list(outputs, ForceDistributionOutput):
10131027
translated["forceDistributionOutput"] = translate_force_distribution_output(outputs)
10141028

1029+
##:: Step10b: Get translated["timeAveragedForceDistributionOutput"]
1030+
if has_instance_in_list(outputs, TimeAverageForceDistributionOutput):
1031+
translated["timeAveragedForceDistributionOutput"] = (
1032+
translate_time_averaged_force_distribution_output(outputs)
1033+
)
1034+
10151035
##:: Step11: Sort all "output_fields" everywhere
10161036
# Recursively sort all "outputFields" lists in the translated dict
10171037
def _sort_output_fields_in_dict(d):

flow360/component/simulation/validation/validation_output.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
ProbeOutput,
1212
SurfaceIntegralOutput,
1313
SurfaceProbeOutput,
14+
TimeAverageForceDistributionOutput,
1415
)
1516
from flow360.component.simulation.time_stepping.time_stepping import Steady
1617
from flow360.component.simulation.user_code.core.types import Expression
@@ -64,6 +65,7 @@ def extract_literal_values(annotation):
6465
"AeroAcousticOutput",
6566
"StreamlineOutput",
6667
"ForceDistributionOutput",
68+
"TimeAverageForceDistributionOutput",
6769
):
6870
continue
6971
# Get allowed output fields items:
@@ -120,6 +122,7 @@ def _check_output_fields_valid_given_turbulence_model(params):
120122
"AeroAcousticOutput",
121123
"StreamlineOutput",
122124
"ForceDistributionOutput",
125+
"TimeAverageForceDistributionOutput",
123126
):
124127
continue
125128
for item in output.output_fields.items:
@@ -168,6 +171,7 @@ def _check_output_fields_valid_given_transition_model(params):
168171
"AeroAcousticOutput",
169172
"StreamlineOutput",
170173
"ForceDistributionOutput",
174+
"TimeAverageForceDistributionOutput",
171175
):
172176
continue
173177
for item in output.output_fields.items:
@@ -246,7 +250,7 @@ def _check_unique_force_distribution_output_names(params):
246250
active_names = set()
247251

248252
for output_index, output in enumerate(params.outputs):
249-
if isinstance(output, ForceDistributionOutput):
253+
if isinstance(output, (ForceDistributionOutput, TimeAverageForceDistributionOutput)):
250254
if output.name in active_names:
251255
raise ValueError(
252256
f"In `outputs`[{output_index}] {output.output_type}: "

tests/simulation/params/test_validators_output.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
SurfaceIntegralOutput,
2727
SurfaceOutput,
2828
SurfaceProbeOutput,
29+
TimeAverageForceDistributionOutput,
2930
TimeAverageSurfaceOutput,
3031
VolumeOutput,
3132
)
@@ -458,7 +459,7 @@ def test_duplicate_force_distribution_names():
458459
with pytest.raises(
459460
ValueError,
460461
match=re.escape(
461-
"`outputs`[1] ForceDistributionOutput: Output name test has already been used for a "
462+
"`outputs`[1] TimeAverageForceDistributionOutput: Output name test has already been used for a "
462463
"`ForceDistributionOutput`. Output names must be unique among all force distribution outputs."
463464
),
464465
):
@@ -469,14 +470,35 @@ def test_duplicate_force_distribution_names():
469470
name="test",
470471
distribution_direction=[1.0, 0.0, 0.0],
471472
),
472-
ForceDistributionOutput(
473+
TimeAverageForceDistributionOutput(
473474
name="test",
474475
distribution_direction=[0.0, 1.0, 0.0],
475476
),
476477
],
477478
)
478479

479480

481+
def test_time_averaged_force_distribution_output_requires_unsteady():
482+
with pytest.raises(
483+
ValueError,
484+
match=re.escape(
485+
"`TimeAverageForceDistributionOutput` can only be used in unsteady simulations."
486+
),
487+
):
488+
with imperial_unit_system:
489+
SimulationParams(
490+
models=[Fluid(turbulence_model_solver=NoneSolver())],
491+
outputs=[
492+
TimeAverageForceDistributionOutput(
493+
name="test",
494+
distribution_direction=[1.0, 0.0, 0.0],
495+
start_step=10,
496+
),
497+
],
498+
time_stepping=Steady(),
499+
)
500+
501+
480502
def test_duplicate_probe_entity_names(mock_validation_context):
481503

482504
# should have no error

tests/simulation/translator/test_output_translation.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
SurfaceOutput,
2929
SurfaceProbeOutput,
3030
SurfaceSliceOutput,
31+
TimeAverageForceDistributionOutput,
3132
TimeAverageIsosurfaceOutput,
3233
TimeAverageProbeOutput,
3334
TimeAverageSurfaceOutput,
@@ -1216,6 +1217,45 @@ def test_force_distribution_output():
12161217
assert compare_values(param_with_ref[1], translated["forceDistributionOutput"])
12171218

12181219

1220+
def test_time_averaged_force_distribution_output():
1221+
param_with_ref = (
1222+
[
1223+
TimeAverageForceDistributionOutput(
1224+
name="test_name",
1225+
distribution_direction=[0.1, 0.9, 0.0],
1226+
),
1227+
TimeAverageForceDistributionOutput(
1228+
name="test_name2",
1229+
distribution_direction=[1.0, 0.0, 0.0],
1230+
distribution_type="cumulative",
1231+
start_step=5,
1232+
),
1233+
],
1234+
{
1235+
"test_name": {
1236+
"direction": [0.11043152607484655, 0.9938837346736189, 0.0],
1237+
"type": "incremental",
1238+
"startAverageIntegrationStep": -1,
1239+
},
1240+
"test_name2": {
1241+
"direction": [1.0, 0.0, 0.0],
1242+
"type": "cumulative",
1243+
"startAverageIntegrationStep": 5,
1244+
},
1245+
},
1246+
)
1247+
1248+
with SI_unit_system:
1249+
param = SimulationParams(
1250+
outputs=param_with_ref[0], time_stepping=Unsteady(steps=1, step_size=0.1)
1251+
)
1252+
param = param._preprocess(mesh_unit=1.0 * u.m, exclude=["models"])
1253+
1254+
translated = {}
1255+
translated = translate_output(param, translated)
1256+
assert compare_values(param_with_ref[1], translated["timeAveragedForceDistributionOutput"])
1257+
1258+
12191259
def test_surface_slice_output(vel_in_km_per_hr):
12201260
param_with_ref = (
12211261
[

0 commit comments

Comments
 (0)