stormbird-setup 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- stormbird_setup/__init__.py +11 -0
- stormbird_setup/actuator_line/__init__.py +15 -0
- stormbird_setup/actuator_line/actuator_line_builder.py +24 -0
- stormbird_setup/actuator_line/corrections.py +18 -0
- stormbird_setup/actuator_line/settings.py +29 -0
- stormbird_setup/base_model.py +50 -0
- stormbird_setup/circulation_corrections.py +138 -0
- stormbird_setup/controller.py +133 -0
- stormbird_setup/input_power.py +128 -0
- stormbird_setup/lifting_line/__init__.py +19 -0
- stormbird_setup/lifting_line/complete_sail_model.py +16 -0
- stormbird_setup/lifting_line/simulation_builder.py +135 -0
- stormbird_setup/lifting_line/solver.py +29 -0
- stormbird_setup/lifting_line/velocity_corrections.py +71 -0
- stormbird_setup/lifting_line/wake.py +160 -0
- stormbird_setup/line_force_model.py +52 -0
- stormbird_setup/py.typed +0 -0
- stormbird_setup/range.py +11 -0
- stormbird_setup/section_models.py +220 -0
- stormbird_setup/simplified_setup/__init__.py +5 -0
- stormbird_setup/simplified_setup/simple_sail_setup.py +136 -0
- stormbird_setup/simplified_setup/single_wing_simulation.py +125 -0
- stormbird_setup/spatial_vector.py +118 -0
- stormbird_setup/utils.py +23 -0
- stormbird_setup/wind/__init__.py +15 -0
- stormbird_setup/wind/gust_spectrums/__init__.py +16 -0
- stormbird_setup/wind/gust_spectrums/davenport.py +32 -0
- stormbird_setup/wind/gust_spectrums/discretized_spectrum.py +12 -0
- stormbird_setup/wind/gust_spectrums/froya.py +32 -0
- stormbird_setup/wind/gust_spectrums/gust_spectrum.py +37 -0
- stormbird_setup/wind/gust_spectrums/kaimal.py +37 -0
- stormbird_setup/wind/gust_spectrums/ochi_shin.py +38 -0
- stormbird_setup/wind/inflow_corrections.py +83 -0
- stormbird_setup/wind/velocity_variation/__init__.py +12 -0
- stormbird_setup/wind/velocity_variation/logarithmic_model.py +256 -0
- stormbird_setup/wind/velocity_variation/power_model.py +55 -0
- stormbird_setup/wind/wind_environment.py +26 -0
- stormbird_setup-0.1.0.dist-info/METADATA +44 -0
- stormbird_setup-0.1.0.dist-info/RECORD +42 -0
- stormbird_setup-0.1.0.dist-info/WHEEL +5 -0
- stormbird_setup-0.1.0.dist-info/licenses/LICENSE +674 -0
- stormbird_setup-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .spatial_vector import SpatialVector
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"SpatialVector"
|
|
11
|
+
]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .actuator_line_builder import ActuatorLineBuilder
|
|
8
|
+
from .corrections import LiftingLineCorrectionBuilder, EmpiricalCirculationCorrection
|
|
9
|
+
from .settings import Gaussian, ProjectionSettings, SamplingSettings, SolverSettings
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"ActuatorLineBuilder",
|
|
13
|
+
"LiftingLineCorrectionBuilder", "EmpiricalCirculationCorrection",
|
|
14
|
+
"Gaussian", "ProjectionSettings", "SamplingSettings", "SolverSettings"
|
|
15
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..base_model import StormbirdSetupBaseModel
|
|
8
|
+
from ..line_force_model import LineForceModelBuilder
|
|
9
|
+
|
|
10
|
+
from .settings import ProjectionSettings, SolverSettings, SamplingSettings
|
|
11
|
+
from .corrections import LiftingLineCorrectionBuilder, EmpiricalCirculationCorrection
|
|
12
|
+
|
|
13
|
+
from ..controller import ControllerBuilder
|
|
14
|
+
|
|
15
|
+
class ActuatorLineBuilder(StormbirdSetupBaseModel):
|
|
16
|
+
line_force_model: LineForceModelBuilder
|
|
17
|
+
projection_settings: ProjectionSettings = ProjectionSettings()
|
|
18
|
+
solver_settings: SolverSettings = SolverSettings()
|
|
19
|
+
sampling_settings: SamplingSettings = SamplingSettings()
|
|
20
|
+
write_iterations_full_result: int = 100
|
|
21
|
+
start_time: float = 0
|
|
22
|
+
controller: ControllerBuilder | None = None
|
|
23
|
+
lifting_line_correction: LiftingLineCorrectionBuilder | None = None
|
|
24
|
+
empirical_circulation_correction: EmpiricalCirculationCorrection | None = None
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..base_model import StormbirdSetupBaseModel
|
|
8
|
+
|
|
9
|
+
from ..lifting_line.wake import SymmetryCondition
|
|
10
|
+
|
|
11
|
+
class LiftingLineCorrectionBuilder(StormbirdSetupBaseModel):
|
|
12
|
+
wake_length_factor: float = 100.0
|
|
13
|
+
symmetry_condition: SymmetryCondition = SymmetryCondition.NoSymmetry
|
|
14
|
+
initialization_time: float | None = None
|
|
15
|
+
|
|
16
|
+
class EmpiricalCirculationCorrection(StormbirdSetupBaseModel):
|
|
17
|
+
exp_factor: float = 10.0
|
|
18
|
+
overall_correction: float = 1.0
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..base_model import StormbirdSetupBaseModel
|
|
8
|
+
|
|
9
|
+
class Gaussian(StormbirdSetupBaseModel):
|
|
10
|
+
chord_factor: float = 0.25
|
|
11
|
+
thickness_factor: float = 0.25
|
|
12
|
+
|
|
13
|
+
class ProjectionSettings(StormbirdSetupBaseModel):
|
|
14
|
+
projection_function: Gaussian = Gaussian()
|
|
15
|
+
realign_sectional_forces: bool = True
|
|
16
|
+
realign_to_local_velocity_at_each_cell: bool = False
|
|
17
|
+
project_viscous_lift: bool = False
|
|
18
|
+
project_sectional_drag: bool = False
|
|
19
|
+
|
|
20
|
+
class SamplingSettings(StormbirdSetupBaseModel):
|
|
21
|
+
use_point_sampling: bool = False
|
|
22
|
+
span_projection_factor: float = 0.5
|
|
23
|
+
neglect_span_projection: bool = False
|
|
24
|
+
extrapolate_end_velocities: bool = False
|
|
25
|
+
remove_span_velocity: bool = False
|
|
26
|
+
correction_factor: float = 1.0
|
|
27
|
+
|
|
28
|
+
class SolverSettings(StormbirdSetupBaseModel):
|
|
29
|
+
damping_factor: float = 0.1
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TypeVar, Type
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
|
11
|
+
|
|
12
|
+
T = TypeVar('T', bound='StormbirdSetupBaseModel')
|
|
13
|
+
|
|
14
|
+
class StormbirdSetupBaseModel(BaseModel):
|
|
15
|
+
'''
|
|
16
|
+
Base class for the classes that define the setup of stormbird simulations.
|
|
17
|
+
'''
|
|
18
|
+
model_config = ConfigDict(
|
|
19
|
+
frozen=False,
|
|
20
|
+
validate_assignment=True,
|
|
21
|
+
extra='forbid',
|
|
22
|
+
populate_by_name=True,
|
|
23
|
+
use_enum_values=False,
|
|
24
|
+
validate_default=True
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_json_string(cls: Type[T], json_string: str) -> T:
|
|
29
|
+
return cls.model_validate_json(json_string)
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_json_file(cls: Type[T], file_path: Path) -> T:
|
|
33
|
+
return cls.model_validate_json(file_path.read_text())
|
|
34
|
+
|
|
35
|
+
def to_json_string(self) -> str:
|
|
36
|
+
return self.model_dump_json(exclude_none=True, indent=4)
|
|
37
|
+
|
|
38
|
+
def to_json_file(self, file_path: Path | str) -> None:
|
|
39
|
+
|
|
40
|
+
if isinstance(file_path, str):
|
|
41
|
+
file_path_out = Path(file_path)
|
|
42
|
+
elif isinstance(file_path, Path):
|
|
43
|
+
file_path_out = file_path
|
|
44
|
+
else:
|
|
45
|
+
raise TypeError(f"Input must be of type Path or str. Right now it is {type(file_path)}")
|
|
46
|
+
|
|
47
|
+
file_path_out.write_text(self.to_json_string())
|
|
48
|
+
|
|
49
|
+
def to_dict(self) -> dict:
|
|
50
|
+
return self.model_dump(exclude_none=True, mode='json')
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base_model import StormbirdSetupBaseModel
|
|
8
|
+
|
|
9
|
+
from pydantic import model_serializer, model_validator
|
|
10
|
+
|
|
11
|
+
from enum import Enum
|
|
12
|
+
|
|
13
|
+
class WindowSize(Enum):
|
|
14
|
+
Five = "Five"
|
|
15
|
+
Seven = "Seven"
|
|
16
|
+
Nine = "Nine"
|
|
17
|
+
|
|
18
|
+
class GaussianSmoothingBuilder(StormbirdSetupBaseModel):
|
|
19
|
+
smoothing_length_factor: float = 0.1
|
|
20
|
+
|
|
21
|
+
class CubicPolynomialSmoothingBuilder(StormbirdSetupBaseModel):
|
|
22
|
+
window_size: WindowSize = WindowSize.Five
|
|
23
|
+
|
|
24
|
+
class CirculationSmoothingBuilder(StormbirdSetupBaseModel):
|
|
25
|
+
smoothing_type: GaussianSmoothingBuilder = GaussianSmoothingBuilder()
|
|
26
|
+
|
|
27
|
+
@model_serializer
|
|
28
|
+
def ser_model(self):
|
|
29
|
+
if isinstance(self.smoothing_type, GaussianSmoothingBuilder):
|
|
30
|
+
return {
|
|
31
|
+
"smoothing_type": {
|
|
32
|
+
"Gaussian":self.smoothing_type.model_dump()
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
else:
|
|
36
|
+
raise NotImplementedError("Only Gaussian smoothing is implemented")
|
|
37
|
+
|
|
38
|
+
@model_validator(mode='before')
|
|
39
|
+
@classmethod
|
|
40
|
+
def deserialize_from_rust_enum(cls, data):
|
|
41
|
+
if not isinstance(data, dict):
|
|
42
|
+
return data
|
|
43
|
+
|
|
44
|
+
if not data:
|
|
45
|
+
return data
|
|
46
|
+
|
|
47
|
+
# Check if smoothing_type needs to be unwrapped from Rust enum format
|
|
48
|
+
if 'smoothing_type' in data:
|
|
49
|
+
st = data['smoothing_type']
|
|
50
|
+
if isinstance(st, dict):
|
|
51
|
+
if 'Gaussian' in st:
|
|
52
|
+
return {'smoothing_type': GaussianSmoothingBuilder(**st['Gaussian'])}
|
|
53
|
+
elif 'CubicPolynomial' in st:
|
|
54
|
+
return {'smoothing_type': CubicPolynomialSmoothingBuilder(**st['CubicPolynomial'])}
|
|
55
|
+
|
|
56
|
+
return data
|
|
57
|
+
|
|
58
|
+
class PrescribedCirculationShape(StormbirdSetupBaseModel):
|
|
59
|
+
inner_power: float = 2.0
|
|
60
|
+
outer_power: float = 0.5
|
|
61
|
+
|
|
62
|
+
class PrescribedCirculation(StormbirdSetupBaseModel):
|
|
63
|
+
shape: PrescribedCirculationShape = PrescribedCirculationShape()
|
|
64
|
+
curve_fit_shape_parameters: bool = False
|
|
65
|
+
|
|
66
|
+
class CirculationCorrectionBuilder(StormbirdSetupBaseModel):
|
|
67
|
+
correction: CirculationSmoothingBuilder | PrescribedCirculation | None = None
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def new_gaussian_smoothing(
|
|
71
|
+
cls,
|
|
72
|
+
smoothing_length_factor: float = 0.1,
|
|
73
|
+
):
|
|
74
|
+
return cls(
|
|
75
|
+
correction = CirculationSmoothingBuilder(
|
|
76
|
+
smoothing_type = GaussianSmoothingBuilder(
|
|
77
|
+
smoothing_length_factor = smoothing_length_factor
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@classmethod
|
|
83
|
+
def new_prescribed_circulation(
|
|
84
|
+
cls,
|
|
85
|
+
inner_power: float = 2.0,
|
|
86
|
+
outer_power: float = 0.5,
|
|
87
|
+
curve_fit_shape_parameters: bool = False
|
|
88
|
+
):
|
|
89
|
+
return cls(
|
|
90
|
+
correction = PrescribedCirculation(
|
|
91
|
+
shape = PrescribedCirculationShape(
|
|
92
|
+
inner_power = inner_power,
|
|
93
|
+
outer_power = outer_power
|
|
94
|
+
),
|
|
95
|
+
curve_fit_shape_parameters = curve_fit_shape_parameters
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@model_validator(mode='before')
|
|
100
|
+
@classmethod
|
|
101
|
+
def deserialize_from_rust_enum(cls, data):
|
|
102
|
+
# Handle the "None" string case
|
|
103
|
+
if data == "None":
|
|
104
|
+
return {'correction': None}
|
|
105
|
+
|
|
106
|
+
if not isinstance(data, dict):
|
|
107
|
+
return data
|
|
108
|
+
|
|
109
|
+
if not data:
|
|
110
|
+
return data
|
|
111
|
+
|
|
112
|
+
# Already in Python/Pydantic form
|
|
113
|
+
if 'correction' in data:
|
|
114
|
+
return data
|
|
115
|
+
|
|
116
|
+
# Rust externally-tagged enum format
|
|
117
|
+
if 'Prescribed' in data:
|
|
118
|
+
return {'correction': PrescribedCirculation(**data['Prescribed'])}
|
|
119
|
+
elif 'Smoothing' in data:
|
|
120
|
+
# Use model_validate so it goes through CirculationSmoothingBuilder's validator
|
|
121
|
+
return {'correction': CirculationSmoothingBuilder.model_validate(data['Smoothing'])}
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError(f"Unknown circulation correction variant: {list(data.keys())}")
|
|
124
|
+
|
|
125
|
+
@model_serializer
|
|
126
|
+
def ser_model(self):
|
|
127
|
+
if self.correction is None:
|
|
128
|
+
return "None"
|
|
129
|
+
elif isinstance(self.correction, PrescribedCirculation):
|
|
130
|
+
return {
|
|
131
|
+
"Prescribed": self.correction.model_dump(exclude_none=True)
|
|
132
|
+
}
|
|
133
|
+
elif isinstance(self.correction, CirculationSmoothingBuilder):
|
|
134
|
+
return {
|
|
135
|
+
"Smoothing": self.correction.ser_model()
|
|
136
|
+
}
|
|
137
|
+
else:
|
|
138
|
+
raise ValueError("Invalid correction type")
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base_model import StormbirdSetupBaseModel
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from pydantic import field_serializer, Field
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
class InternalStateType(Enum):
|
|
16
|
+
Generic = "Generic"
|
|
17
|
+
SpinRatio = "SpinRatio"
|
|
18
|
+
|
|
19
|
+
class SpinRatioConversion(StormbirdSetupBaseModel):
|
|
20
|
+
diameter: float
|
|
21
|
+
max_rps: float
|
|
22
|
+
|
|
23
|
+
class ControllerSetPoints(StormbirdSetupBaseModel):
|
|
24
|
+
apparent_wind_directions_data: list[float]
|
|
25
|
+
angle_of_attack_data: list[float] | None = None
|
|
26
|
+
section_model_internal_state_data: list[float] | None = None
|
|
27
|
+
internal_state_type: InternalStateType = InternalStateType.Generic
|
|
28
|
+
internal_state_conversion: SpinRatioConversion | None = Field(default=None, exclude=True)
|
|
29
|
+
use_effective_angle_of_attack: bool = False
|
|
30
|
+
max_local_wing_angle_change_rate: float | None = None
|
|
31
|
+
max_internal_section_state_change_rate: float | None = None
|
|
32
|
+
|
|
33
|
+
@field_serializer('internal_state_type')
|
|
34
|
+
def serialize_internal_state_type(self, value: InternalStateType):
|
|
35
|
+
match value:
|
|
36
|
+
case InternalStateType.Generic:
|
|
37
|
+
return "Generic"
|
|
38
|
+
case InternalStateType.SpinRatio:
|
|
39
|
+
if self.internal_state_conversion is None:
|
|
40
|
+
raise ValueError("SpinRatioConversion must be provided for SpinRatio internal state type.")
|
|
41
|
+
return {
|
|
42
|
+
"SpinRatio": self.internal_state_conversion.model_dump()
|
|
43
|
+
}
|
|
44
|
+
case _:
|
|
45
|
+
raise ValueError("Unsupported internal state type:", value)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def new_default_wing_sail_single_element(cls, max_angle_deg: float = 15.0):
|
|
49
|
+
apparent_wind_directions_data = np.radians([-180, -15, -10, 10, 15, 180])
|
|
50
|
+
angle_of_attack_data = np.radians([-max_angle_deg, -max_angle_deg, 0.0, 0.0, max_angle_deg, max_angle_deg])
|
|
51
|
+
|
|
52
|
+
return ControllerSetPoints(
|
|
53
|
+
apparent_wind_directions_data = apparent_wind_directions_data.tolist(),
|
|
54
|
+
angle_of_attack_data = angle_of_attack_data.tolist()
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def new_default_wing_sail_two_element(cls, max_angle_deg: float = 12.0):
|
|
59
|
+
apparent_wind_directions_data = np.radians([-180, -15, -10, 10, 15, 180])
|
|
60
|
+
angle_of_attack_data = np.radians([-max_angle_deg, -max_angle_deg, 0.0, 0.0, max_angle_deg, max_angle_deg])
|
|
61
|
+
section_model_internal_state_data = np.radians([-30.0, -30.0, 0.0, 0.0, 30.0, 30.0])
|
|
62
|
+
|
|
63
|
+
return ControllerSetPoints(
|
|
64
|
+
apparent_wind_directions_data = apparent_wind_directions_data.tolist(),
|
|
65
|
+
angle_of_attack_data = angle_of_attack_data.tolist(),
|
|
66
|
+
section_model_internal_state_data = section_model_internal_state_data.tolist()
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@classmethod
|
|
70
|
+
def new_default_rotor_sail(cls, *, diameter: float, max_rps: float):
|
|
71
|
+
'''
|
|
72
|
+
Helper function to quickly set up a suitable controller for a rotor sail. Assumed to be
|
|
73
|
+
fairly general
|
|
74
|
+
'''
|
|
75
|
+
apparent_wind_directions_data = np.radians([-180, -40, -15, 15, 40, 180])
|
|
76
|
+
section_model_internal_state_data = [3.0, 3.0, 0.0, 0.0, -3.0, -3.0]
|
|
77
|
+
|
|
78
|
+
internal_state_type = InternalStateType.SpinRatio
|
|
79
|
+
internal_state_conversion = SpinRatioConversion(
|
|
80
|
+
diameter = diameter,
|
|
81
|
+
max_rps = max_rps
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return ControllerSetPoints(
|
|
85
|
+
apparent_wind_directions_data = apparent_wind_directions_data.tolist(),
|
|
86
|
+
section_model_internal_state_data = section_model_internal_state_data,
|
|
87
|
+
internal_state_type = internal_state_type,
|
|
88
|
+
internal_state_conversion = internal_state_conversion
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def new_default_suction_sail(cls, max_aoa_deg: float=30.0, max_ca: float = 0.3):
|
|
93
|
+
apparent_wind_directions_data = np.radians([-180, -15, -10, 10, 15, 180]).tolist()
|
|
94
|
+
|
|
95
|
+
angle_of_attack_data = np.radians([
|
|
96
|
+
-max_aoa_deg, -max_aoa_deg, 0.0,
|
|
97
|
+
0.0, max_aoa_deg, max_aoa_deg
|
|
98
|
+
]).tolist()
|
|
99
|
+
|
|
100
|
+
section_model_internal_state_data = [
|
|
101
|
+
-max_ca, -max_ca, 0.0,
|
|
102
|
+
0.0, max_ca, max_ca
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
return ControllerSetPoints(
|
|
106
|
+
apparent_wind_directions_data = apparent_wind_directions_data,
|
|
107
|
+
angle_of_attack_data = angle_of_attack_data,
|
|
108
|
+
section_model_internal_state_data = section_model_internal_state_data
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class MeasurementType(Enum):
|
|
113
|
+
Mean = "Mean"
|
|
114
|
+
Max = "Max"
|
|
115
|
+
Min = "Min"
|
|
116
|
+
|
|
117
|
+
class MeasurementSettings(StormbirdSetupBaseModel):
|
|
118
|
+
measurement_type: MeasurementType = MeasurementType.Mean
|
|
119
|
+
start_index: int = 1
|
|
120
|
+
end_offset: int = 1
|
|
121
|
+
|
|
122
|
+
class FlowMeasurementSettings(StormbirdSetupBaseModel):
|
|
123
|
+
angle_of_attack: MeasurementSettings = MeasurementSettings()
|
|
124
|
+
wind_direction: MeasurementSettings = MeasurementSettings()
|
|
125
|
+
wind_velocity: MeasurementSettings = MeasurementSettings()
|
|
126
|
+
|
|
127
|
+
class ControllerBuilder(StormbirdSetupBaseModel):
|
|
128
|
+
set_points: list[ControllerSetPoints]
|
|
129
|
+
flow_measurement_settings: FlowMeasurementSettings = FlowMeasurementSettings()
|
|
130
|
+
time_steps_between_updates: int = 1
|
|
131
|
+
start_time: float = 0.0
|
|
132
|
+
moving_average_window_size: int | None = None
|
|
133
|
+
use_input_velocity_for_apparent_wind_direction: bool = False
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base_model import StormbirdSetupBaseModel
|
|
8
|
+
|
|
9
|
+
from enum import Enum
|
|
10
|
+
|
|
11
|
+
from pydantic import model_serializer, model_validator
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
|
|
15
|
+
class InputPowerData(StormbirdSetupBaseModel):
|
|
16
|
+
section_models_internal_state_data: list[float]
|
|
17
|
+
input_power_coefficient_data: list[float]
|
|
18
|
+
|
|
19
|
+
class InputPowerDataType(Enum):
|
|
20
|
+
NoPower = "NoPower"
|
|
21
|
+
InternalStateAsPowerCoefficient = "InternalStateAsPowerCoefficient"
|
|
22
|
+
InterpolatePowerCoefficientFromInternalState = "InterpolatePowerCoefficientFromInternalState"
|
|
23
|
+
InterpolateFromInternalStateOnly = "InterpolateFromInternalStateOnly"
|
|
24
|
+
|
|
25
|
+
class InputPowerModel(StormbirdSetupBaseModel):
|
|
26
|
+
'''
|
|
27
|
+
Interface to the input power model
|
|
28
|
+
'''
|
|
29
|
+
input_power_type: InputPowerDataType = InputPowerDataType.NoPower
|
|
30
|
+
input_power_data: InputPowerData | None = None
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def new_from_internal_state_as_power_coefficient(cls) -> "InputPowerModel":
|
|
34
|
+
return cls(
|
|
35
|
+
input_power_type = InputPowerDataType.InternalStateAsPowerCoefficient
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@classmethod
|
|
39
|
+
def new_polynomial_rotor_sail_model(
|
|
40
|
+
cls,
|
|
41
|
+
max_power: float,
|
|
42
|
+
max_rps: float,
|
|
43
|
+
area: float,
|
|
44
|
+
poly_power: float = 2.5
|
|
45
|
+
) -> "InputPowerModel":
|
|
46
|
+
'''
|
|
47
|
+
Simple model for the power based on a polynomial relationship between the power and RPS.
|
|
48
|
+
|
|
49
|
+
The polynomial power is set to 2.5, which comes from data fitted to data from the SWOPP
|
|
50
|
+
project. However, the actual power is scaled based on the supplied values for max_power
|
|
51
|
+
and max_rps.
|
|
52
|
+
'''
|
|
53
|
+
|
|
54
|
+
section_models_internal_state_data = np.linspace(0, max_rps, 20)
|
|
55
|
+
|
|
56
|
+
factor = max_power / (max_rps**poly_power * area)
|
|
57
|
+
|
|
58
|
+
input_power_coefficient_data = factor * (section_models_internal_state_data**poly_power)
|
|
59
|
+
|
|
60
|
+
return cls(
|
|
61
|
+
input_power_type = InputPowerDataType.InterpolateFromInternalStateOnly,
|
|
62
|
+
input_power_data = InputPowerData(
|
|
63
|
+
section_models_internal_state_data = section_models_internal_state_data.tolist(),
|
|
64
|
+
input_power_coefficient_data = input_power_coefficient_data.tolist()
|
|
65
|
+
)
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
@model_validator(mode='before')
|
|
69
|
+
@classmethod
|
|
70
|
+
def deserialize_from_rust_enum(cls, data):
|
|
71
|
+
# Handle the "NoPower" string case (unit variant)
|
|
72
|
+
if data == "NoPower":
|
|
73
|
+
return {
|
|
74
|
+
'input_power_type': InputPowerDataType.NoPower,
|
|
75
|
+
'input_power_data': None
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if not isinstance(data, dict):
|
|
79
|
+
return data
|
|
80
|
+
|
|
81
|
+
# Empty dict means use defaults
|
|
82
|
+
if not data:
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
# Already in Python/Pydantic form
|
|
86
|
+
if 'input_power_type' in data or 'input_power_data' in data:
|
|
87
|
+
return data
|
|
88
|
+
|
|
89
|
+
# Rust externally-tagged enum format
|
|
90
|
+
if 'InterpolateFromInternalStateOnly' in data:
|
|
91
|
+
return {
|
|
92
|
+
'input_power_type': InputPowerDataType.InterpolateFromInternalStateOnly,
|
|
93
|
+
'input_power_data': InputPowerData(**data['InterpolateFromInternalStateOnly'])
|
|
94
|
+
}
|
|
95
|
+
elif 'InternalStateAsPowerCoefficient' in data:
|
|
96
|
+
return {
|
|
97
|
+
'input_power_type': InputPowerDataType.InternalStateAsPowerCoefficient,
|
|
98
|
+
'input_power_data': None
|
|
99
|
+
}
|
|
100
|
+
elif 'InterpolatePowerCoefficientFromInternalState' in data:
|
|
101
|
+
return {
|
|
102
|
+
'input_power_type': InputPowerDataType.InterpolatePowerCoefficientFromInternalState,
|
|
103
|
+
'input_power_data': InputPowerData(**data['InterpolatePowerCoefficientFromInternalState'])
|
|
104
|
+
}
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Unknown input power model variant: {list(data.keys())}")
|
|
107
|
+
|
|
108
|
+
@model_serializer
|
|
109
|
+
def ser_model(self) -> dict[str, object] | str:
|
|
110
|
+
match self.input_power_type:
|
|
111
|
+
case InputPowerDataType.NoPower | InputPowerDataType.InternalStateAsPowerCoefficient:
|
|
112
|
+
return self.input_power_type.value
|
|
113
|
+
case InputPowerDataType.InterpolateFromInternalStateOnly:
|
|
114
|
+
if self.input_power_data is None:
|
|
115
|
+
raise ValueError("input_power_data must be set for InterpolateFromInternalStateOnly")
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"InterpolateFromInternalStateOnly": self.input_power_data.model_dump()
|
|
119
|
+
}
|
|
120
|
+
case InputPowerDataType.InterpolatePowerCoefficientFromInternalState:
|
|
121
|
+
if self.input_power_data is None:
|
|
122
|
+
raise ValueError("input_power_data must be set for InterpolatePowerCoefficientFromInternalState")
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
"InterpolatePowerCoefficientFromInternalState": self.input_power_data.model_dump()
|
|
126
|
+
}
|
|
127
|
+
case _:
|
|
128
|
+
raise ValueError("Unknown input power type", self.input_power_type)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .solver import InducedVelocityCorrectionMethod, Linearized, SimpleIterative
|
|
8
|
+
from .wake import SymmetryCondition, ViscousCoreLength, QuasiSteadyWakeSettings, DynamicWakeBuilder, ViscousCoreLengthEvolution, FirstWakePointsDirection
|
|
9
|
+
from .simulation_builder import QuasiSteadySettings, DynamicSettings, SimulationBuilder
|
|
10
|
+
from .velocity_corrections import VelocityCorrections
|
|
11
|
+
from .complete_sail_model import CompleteSailModelBuilder
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"InducedVelocityCorrectionMethod", "Linearized", "SimpleIterative",
|
|
15
|
+
"SymmetryCondition", "ViscousCoreLength", "QuasiSteadyWakeSettings", "DynamicWakeBuilder", "ViscousCoreLengthEvolution", "FirstWakePointsDirection",
|
|
16
|
+
"QuasiSteadySettings", "DynamicSettings", "SimulationBuilder",
|
|
17
|
+
"VelocityCorrections",
|
|
18
|
+
"CompleteSailModelBuilder"
|
|
19
|
+
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright (C) 2024, NTNU
|
|
3
|
+
Author: Jarle Vinje Kramer <jarlekramer@gmail.com; jarle.a.kramer@ntnu.no>
|
|
4
|
+
License: GPL v3.0 (see separate file LICENSE or https://www.gnu.org/licenses/gpl-3.0.html)
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..base_model import StormbirdSetupBaseModel
|
|
8
|
+
|
|
9
|
+
from .simulation_builder import SimulationBuilder
|
|
10
|
+
from ..wind import WindEnvironment
|
|
11
|
+
from ..controller import ControllerBuilder
|
|
12
|
+
|
|
13
|
+
class CompleteSailModelBuilder(StormbirdSetupBaseModel):
|
|
14
|
+
lifting_line_simulation: SimulationBuilder
|
|
15
|
+
controller: ControllerBuilder
|
|
16
|
+
wind_environment: WindEnvironment = WindEnvironment()
|