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.
Files changed (42) hide show
  1. stormbird_setup/__init__.py +11 -0
  2. stormbird_setup/actuator_line/__init__.py +15 -0
  3. stormbird_setup/actuator_line/actuator_line_builder.py +24 -0
  4. stormbird_setup/actuator_line/corrections.py +18 -0
  5. stormbird_setup/actuator_line/settings.py +29 -0
  6. stormbird_setup/base_model.py +50 -0
  7. stormbird_setup/circulation_corrections.py +138 -0
  8. stormbird_setup/controller.py +133 -0
  9. stormbird_setup/input_power.py +128 -0
  10. stormbird_setup/lifting_line/__init__.py +19 -0
  11. stormbird_setup/lifting_line/complete_sail_model.py +16 -0
  12. stormbird_setup/lifting_line/simulation_builder.py +135 -0
  13. stormbird_setup/lifting_line/solver.py +29 -0
  14. stormbird_setup/lifting_line/velocity_corrections.py +71 -0
  15. stormbird_setup/lifting_line/wake.py +160 -0
  16. stormbird_setup/line_force_model.py +52 -0
  17. stormbird_setup/py.typed +0 -0
  18. stormbird_setup/range.py +11 -0
  19. stormbird_setup/section_models.py +220 -0
  20. stormbird_setup/simplified_setup/__init__.py +5 -0
  21. stormbird_setup/simplified_setup/simple_sail_setup.py +136 -0
  22. stormbird_setup/simplified_setup/single_wing_simulation.py +125 -0
  23. stormbird_setup/spatial_vector.py +118 -0
  24. stormbird_setup/utils.py +23 -0
  25. stormbird_setup/wind/__init__.py +15 -0
  26. stormbird_setup/wind/gust_spectrums/__init__.py +16 -0
  27. stormbird_setup/wind/gust_spectrums/davenport.py +32 -0
  28. stormbird_setup/wind/gust_spectrums/discretized_spectrum.py +12 -0
  29. stormbird_setup/wind/gust_spectrums/froya.py +32 -0
  30. stormbird_setup/wind/gust_spectrums/gust_spectrum.py +37 -0
  31. stormbird_setup/wind/gust_spectrums/kaimal.py +37 -0
  32. stormbird_setup/wind/gust_spectrums/ochi_shin.py +38 -0
  33. stormbird_setup/wind/inflow_corrections.py +83 -0
  34. stormbird_setup/wind/velocity_variation/__init__.py +12 -0
  35. stormbird_setup/wind/velocity_variation/logarithmic_model.py +256 -0
  36. stormbird_setup/wind/velocity_variation/power_model.py +55 -0
  37. stormbird_setup/wind/wind_environment.py +26 -0
  38. stormbird_setup-0.1.0.dist-info/METADATA +44 -0
  39. stormbird_setup-0.1.0.dist-info/RECORD +42 -0
  40. stormbird_setup-0.1.0.dist-info/WHEEL +5 -0
  41. stormbird_setup-0.1.0.dist-info/licenses/LICENSE +674 -0
  42. 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()