@@ -291,6 +291,72 @@ def test_polyfit_cond_number_warn(self, tmp_path: pathlib.Path) -> None:
291291 assert hits [0 ].severity in ("WARN" , "ERROR" )
292292 assert "polynomial-fit design matrix" in hits [0 ].message
293293
294+ # --- platform CM offset (issue #95) ---------------------------------
295+ def _build_cm_offset_tower (self , cm_x : float , cm_y : float = 0.0 ) -> Tower :
296+ """A hub_conn=2 PlatformSupport tower with a full 6×6 inertia and
297+ a configurable horizontal CM offset. Yaw radius of gyration is
298+ √(I_yaw/m) = √(2.0e10 / 1.8e7) ≈ 33.3 m."""
299+ n = 5
300+ span = np .linspace (0.0 , 1.0 , n )
301+ sp = SectionProperties (
302+ title = "t" , n_secs = n ,
303+ span_loc = span , str_tw = np .zeros (n ), tw_iner = np .zeros (n ),
304+ mass_den = np .full (n , 100.0 ),
305+ flp_iner = np .full (n , 10.0 ), edge_iner = np .full (n , 10.0 ),
306+ flp_stff = np .full (n , 1.0e9 ), edge_stff = np .full (n , 1.0e9 ),
307+ tor_stff = np .full (n , 1.0e8 ), axial_stff = np .full (n , 1.0e10 ),
308+ cg_offst = np .zeros (n ), sc_offst = np .zeros (n ), tc_offst = np .zeros (n ),
309+ )
310+ m = 1.8e7
311+ i6 = np .zeros ((6 , 6 ))
312+ i6 [0 , 0 ] = i6 [1 , 1 ] = i6 [2 , 2 ] = m
313+ i6 [3 , 3 ] = i6 [4 , 4 ] = 1.3e10
314+ i6 [5 , 5 ] = 2.0e10 # I_yaw → r_g ≈ 33.3 m
315+ platform = PlatformSupport (
316+ draft = 10.0 , cm_pform = 0.0 , mass_pform = m , i_matrix = i6 , ref_msl = 0.0 ,
317+ hydro_M = np .eye (6 ) * 1.0e6 , hydro_K = np .eye (6 ) * 1.0e7 ,
318+ mooring_K = np .diag ([1.0e6 ] * 6 ),
319+ distr_m_z = np .array ([]), distr_m = np .array ([]),
320+ distr_k_z = np .array ([]), distr_k = np .array ([]), wires = None ,
321+ cm_pform_x = cm_x , cm_pform_y = cm_y ,
322+ )
323+ bmi = BMIFile (
324+ title = "t" , echo = False , beam_type = 2 , rot_rpm = 0.0 , rpm_mult = 1.0 ,
325+ radius = 90.0 , hub_rad = 0.0 , precone = 0.0 , bl_thp = 0.0 , hub_conn = 2 ,
326+ n_modes_print = 20 , tab_delim = True , mid_node_tw = False ,
327+ tip_mass = TipMassProps (
328+ mass = 100_000.0 , cm_offset = 0.0 , cm_axial = 0.0 ,
329+ ixx = 0.0 , iyy = 0.0 , izz = 0.0 , ixy = 0.0 , izx = 0.0 , iyz = 0.0 ,
330+ ),
331+ id_mat = 1 , sec_props_file = "" , scaling = ScalingFactors (),
332+ n_elements = 10 , el_loc = np .linspace (0.0 , 1.0 , 11 ),
333+ tow_support = 1 , support = platform , source_file = None ,
334+ )
335+ tower = Tower .__new__ (Tower )
336+ tower ._bmi = bmi
337+ tower ._sp = sp
338+ return tower
339+
340+ def test_large_horizontal_cm_offset_warns (self ) -> None :
341+ """A horizontal CM offset exceeding the platform's yaw radius of
342+ gyration (the issue #95 input error) flags WARN."""
343+ tower = self ._build_cm_offset_tower (cm_x = - 39.0 ) # > r_g ≈ 33.3 m
344+ hits = _filter (check_model (tower ), "cm_pform_x" )
345+ assert len (hits ) == 1 and hits [0 ].severity == "WARN"
346+ assert "radius of gyration" in hits [0 ].message
347+ assert "tower axis" in hits [0 ].message .lower ()
348+
349+ def test_small_horizontal_cm_offset_is_silent (self ) -> None :
350+ """A modest CM offset (well under the gyration radius) is a
351+ legitimate asymmetric floater and must not warn."""
352+ tower = self ._build_cm_offset_tower (cm_x = - 4.0 ) # < r_g ≈ 33.3 m
353+ assert _filter (check_model (tower ), "cm_pform_x" ) == []
354+
355+ def test_zero_cm_offset_is_silent (self ) -> None :
356+ """The axisymmetric / symmetric default (offset 0) never warns."""
357+ tower = self ._build_cm_offset_tower (cm_x = 0.0 , cm_y = 0.0 )
358+ assert _filter (check_model (tower ), "cm_pform_x" ) == []
359+
294360
295361# ---------------------------------------------------------------------------
296362# Auto-run integration on Tower.run / RotatingBlade.run
0 commit comments