Repository: ross-rotordynamics/ross
Pull request #366 (2019-11-01 18:50:17)

Improvements to api_report.py - Add test_api_report.py
- Add Stability level 1 screening criteria;
- Modifies the code to storage the cross-coupling range for each rotor stage (or impeller). This is useful to distinguish the cross-coupling evaluation for different `rotor_types` (between bearings, overhung...);
- Add `.summary()` method;
- Add summary for stability level 1


Patch:
diff --git a/ross/api_report.py b/ross/api_report.py
index b5a73e08..880041e8 100644
--- a/ross/api_report.py
+++ b/ross/api_report.py
@@ -9,9 +9,12 @@ import ross as rs
 
 import bokeh.palettes as bp
 from bokeh.plotting import figure
-from bokeh.layouts import gridplot
-from bokeh.models import(
-        ColumnDataSource, HoverTool, Span, Label, LinearAxis, Range1d, DataRange1d
+from bokeh.layouts import gridplot, widgetbox
+from bokeh.models.widgets import (
+    DataTable, NumberFormatter, TableColumn, Panel, Tabs
+)
+from bokeh.models import (
+    ColumnDataSource, HoverTool, Span, Label, LinearAxis, Range1d, DataRange1d,
 )
 
 # set bokeh palette of colors
@@ -20,13 +23,13 @@ bokeh_colors = bp.RdGy[11]
 
 class Report:
     def __init__(
-            self,
-            rotor,
-            minspeed,
-            maxspeed,
-            machine_type="compressor",
-            speed_units="rpm",
-            tag="Rotor"
+        self,
+        rotor,
+        minspeed,
+        maxspeed,
+        machine_type="compressor",
+        speed_units="rpm",
+        tag=None,
     ):
         """Report according to standard analysis.
 
@@ -51,7 +54,7 @@ class Report:
             Default is "rpm".
         tag: str
             String to name the rotor model
-            Default is "Rotor"
+            Default is the Rotor.tag attribute
 
         Attributes
         ----------
@@ -134,7 +137,22 @@ class Report:
         if machine_type not in machine_options:
             machine_type = "compressor"
         self.machine_type = machine_type
-        self.tag = tag
+
+        if tag is None:
+            self.tag = rotor.tag
+        else:
+            self.tag = tag
+
+        # list of attributes
+        self.Q0 = None
+        self.Qa = None
+        self.log_dec_a = None
+        self.CSR = None
+        self.Qratio = None
+        self.crit_speed = None
+        self.MCS = None
+        self.RHO_gas = None
+        self.condition = None
 
     @classmethod
     def from_saved_rotors(cls, path, minspeed, maxspeed, speed_units="rpm"):
@@ -675,9 +693,7 @@ class Report:
 
         return node_min, node_max
 
-    def stability_level_1(
-        self, D, H, HP, N, RHOd, RHOs, steps=21, unit="m"
-    ):
+    def stability_level_1(self, D, H, HP, oper_speed, RHO_ratio, RHOs, RHOd, unit="m"):
         """Stability analysis level 1.
 
         This analysis consider a anticipated cross coupling QA based on
@@ -689,34 +705,56 @@ class Report:
 
         Parameters
         ----------
-        D : list
+        D: list
             Impeller diameter, m (in.),
             Blade pitch diameter, m (in.),
-        H : list
+        H: list
             Minimum diffuser width per impeller, m (in.),
             Effective blade height, m (in.),
-        HP : list
+        HP: list
             Rated power per stage/impeller, W (HP),
-        N : float
+        oper_speed: float
             Operating speed, rpm,
-        RHOd : float
-            Discharge gas density per impeller, kg/m3 (lbm/in.3),
-        RHOs : float
-            Suction gas density per impeller, kg/m3 (lbm/in.3).
-        steps : int
-            Number of steps in the cross coupled stiffness range.
-            Default is 21.
-        unit : str
+        RHO_ratio: list
+            Density ratio between the discharge gas density and the suction
+            gas density per impeller (RHO_discharge / RHO_suction),
+            kg/m3 (lbm/in.3),
+        RHOs: float
+            Suction gas density in the first stage, kg/m3 (lbm/in.3).
+        RHOd: float
+            Discharge gas density in the last stage, kg/m3 (lbm/in.3),
+        unit: str
             Adopted unit system.
             Default is "m"
 
         Return
         ------
+        fig1: bokeh.figure
+            Applied Cross-Coupled Stiffness vs. Log Decrement plot;
+        fig2: bokeh.figure
+            CSR vs. Mean Gas Density plot.
+        condition: str
+            not required: Stability Level 1 satisfies the analysis;
+            required: Stability Level 2 is required.
 
         Example
         -------
-
+        >>> rotor = rotor_example()
+        >>> report = Report(rotor=rotor,
+        ...                 minspeed=400,
+        ...                 maxspeed=1000,
+        ...                 speed_units="rad/s")
+        >>> stability = report.stability_level_1(D=[0.35, 0.35],
+        ...                          H=[0.08, 0.08],
+        ...                          HP=[10000, 10000],
+        ...                          RHO_ratio=[1.11, 1.14],
+        ...                          RHOd=30.45,
+        ...                          RHOs=37.65,
+        ...                          oper_speed=1000.0)
+        >>> report.Qa
+        23022.32142857143
         """
+        steps = 11
         if unit == "m":
             C = 9.55
         elif unit == "in":
@@ -727,98 +765,151 @@ class Report:
         if len(D) != len(H):
             raise Exception("length of D must be the same of H")
 
-        # Qa - Anticipated cross-coupling - API 684 - SP6.8.5.6
+        Qa = 0.0
+        cross_coupled_array = np.array([])
+        # Qa - Anticipated cross-coupling for compressors - API 684 - SP6.8.5.6
         if self.machine_type == "compressor":
             Bc = 3.0
             Dc, Hc = D, H
-            Qa = 0.0
-            Qa_list = []
-            for i in range(len(self.rotor.disk_elements)):
-                qi = (HP[i] * Bc * C * RHOd) / (Dc[i] * Hc[i] * N * RHOs)
-                Qa_list.append(qi)
-                Qa += qi
-
-        # Qa - Anticipated cross-coupling - API 684 - SP6.8.5.6
+            for i, disk in enumerate(self.rotor.disk_elements):
+                if disk.n in self.disk_nodes:
+                    qi = HP[i] * Bc * C * RHO_ratio[i] / (Dc[i] * Hc[i] * oper_speed)
+                    Qi = np.linspace(0, 10 * qi, steps)
+                    cross_coupled_array = np.append(cross_coupled_array, Qi)
+                    Qa += qi
+
+        # Qa - Anticipated cross-coupling for turbines - API 684 - SP6.8.5.6
         if self.machine_type == "turbine" or self.machine_type == "axial_flow":
             Bt = 1.5
             Dt, Ht = D, H
-            Qa = 0.0
-            Qa_list = []
-            for i in range(len(self.rotor.disk_elements)):
-                qi = (HP[i] * Bt * C) / (Dt[i] * Ht[i] * N)
-                Qa_list.append(qi)
-                Qa += qi
-
-        cross_coupled_stiff = np.linspace(0, 10 * Qa, steps)
-
-        log_dec = np.zeros(len(cross_coupled_stiff))
-
-        for i, Q in enumerate(cross_coupled_stiff):
-            bearings = [copy(b) for b in self.rotor.bearing_seal_elements]
+            for i, disk in enumerate(self.rotor.disk_elements):
+                if disk.n in self.disk_nodes:
+                    qi = (HP[i] * Bt * C) / (Dt[i] * Ht[i] * oper_speed)
+                    Qi = np.linspace(0, 10 * qi, steps)
+                    cross_coupled_array = np.append(cross_coupled_array, Qi)
+                    Qa += qi
+
+        # Defining cross-coupling range to 10*Qa - API 684 - SP6.8.5.8
+        Qi = np.linspace(0, 10 * Qa, steps)
+        cross_coupled_array = np.append(cross_coupled_array, Qi)
+        cross_coupled_array = cross_coupled_array.reshape(
+                [len(self.disk_nodes) + 1, steps]
+        ).T
+
+        log_dec = np.zeros(len(cross_coupled_array))
+
+        # remove disks and seals from the rotor model
+        bearing_list = [
+            copy(b)
+            for b in self.rotor.bearing_seal_elements
+            if b.__class__.__name__ != "SealElement"
+        ]
+
+        # Applying cross-coupling on rotor mid-span
+        if self.rotor_type == "between_bearings":
+            for i, Q in enumerate(cross_coupled_array[:, -1]):
+                bearings = [copy(b) for b in bearing_list]
 
-            if self.rotor_type == "between_bearings":
                 # cross-coupling introduced at the rotor mid-span
                 n = np.round(np.mean(self.rotor.nodes))
                 cross_coupling = BearingElement(n=int(n), kxx=0, cxx=0, kxy=Q, kyx=-Q)
                 bearings.append(cross_coupling)
 
-            else:
+                aux_rotor = Rotor(
+                    shaft_elements=self.rotor.shaft_elements,
+                    disk_elements=[],
+                    bearing_seal_elements=bearings,
+                    rated_w=self.rotor.rated_w,
+                )
+                modal = aux_rotor.run_modal(speed=oper_speed)
+                non_backward = modal.whirl_direction() != "Backward"
+                log_dec[i] = modal.log_dec[non_backward][0]
+
+        # Applying cross-coupling for each disk - API 684 - SP6.8.5.9
+        else:
+            for i, Q in enumerate(cross_coupled_array[:, :-1]):
+                bearings = [copy(b) for b in bearing_list]
                 # cross-coupling introduced at overhung disks
-                for n in self.disk_nodes:
-                    cross_coupling = BearingElement(n=int(n), kxx=0, cxx=0, kxy=Q, kyx=-Q)
+                for n, q in zip(self.disk_nodes, Q):
+                    cross_coupling = BearingElement(
+                        n=n, kxx=0, cxx=0, kxy=q, kyx=-q
+                    )
                     bearings.append(cross_coupling)
 
-            aux_rotor = Rotor(
-                self.rotor.shaft_elements,
-                self.rotor.disk_elements,
-                bearings,
-                self.rotor.rated_w,
+                aux_rotor = Rotor(
+                    shaft_elements=self.rotor.shaft_elements,
+                    disk_elements=[],
+                    bearing_seal_elements=bearings,
+                    rated_w=self.rotor.rated_w,
+                )
+                modal = aux_rotor.run_modal(speed=oper_speed)
+                non_backward = modal.whirl_direction() != "Backward"
+                log_dec[i] = modal.log_dec[non_backward][0]
+
+        # verifies if log dec is greater than zero to begin extrapolation
+        cross_coupled_Qa = cross_coupled_array[:, -1]
+        if log_dec[-1] >= 0:
+            g = interp1d(
+                cross_coupled_Qa, log_dec, fill_value="extrapolate", kind="linear"
             )
-            non_backward = aux_rotor.whirl_direction() != "Backward"
-            log_dec[i] = aux_rotor.log_dec[non_backward][0]
+            stiff = cross_coupled_Qa[-1] * (1 + 1 / (len(cross_coupled_Qa)))
+            while g(stiff) > 0:
+                log_dec = np.append(log_dec, g(stiff))
+                cross_coupled_Qa = np.append(cross_coupled_Qa, stiff)
+                stiff += cross_coupled_Qa[-1] / (len(cross_coupled_Qa))
+            Q0 = cross_coupled_Qa[-1]
 
-        g = interp1d(cross_coupled_stiff, log_dec, fill_value="extrapolate", kind="quadratic")
-        stiff = cross_coupled_stiff[-1] * (1 + 1 / (len(cross_coupled_stiff)))
+        else:
+            idx = min(range(len(log_dec)), key=lambda i: abs(log_dec[i]))
+            Q0 = cross_coupled_Qa[idx]
 
-        while g(stiff) > 0:
-            log_dec = np.append(log_dec, g(stiff))
-            cross_coupled_stiff = np.append(cross_coupled_stiff, stiff)
-            stiff += cross_coupled_stiff[-1] / (len(cross_coupled_stiff))
-        Q0 = cross_coupled_stiff[-1]
+        # Find value for log_dec corresponding to Qa
+        log_dec_a = log_dec[np.where(cross_coupled_Qa == Qa)][0]
 
         # CSR - Critical Speed Ratio
-        CSR = self.maxspeed / self.rotor.run_modal(speed=self.maxspeed).wn[0]
+        crit_speed = self.rotor.run_modal(speed=self.maxspeed).wn[0]
+        CSR = self.maxspeed / crit_speed
 
         # RHO_mean - Average gas density
         RHO_mean = (RHOd + RHOs) / 2
-        RHO = np.linspace(0, RHO_mean * 5, 201)
-        Qc = np.piecewise(
-                RHO,
-                [RHO <= 16.53, RHO > 16.53, RHO == 60, RHO > 60],
-                [2.5, lambda RHO: (-0.0115 * RHO + 2.69), 2.0, 0.0],
+        RHO = np.linspace(0, RHO_mean * 5, 501)
+
+        # CSR_boundary - function to define the CSR boundaries
+        CSR_boundary = np.piecewise(
+            RHO,
+            [RHO <= 16.53, RHO > 16.53, RHO == 60, RHO > 60],
+            [2.5, lambda RHO: (-0.0115 * RHO + 2.69), 2.0, 0.0],
         )
 
-        # Plot area
+        # Plotting area
         fig1 = figure(
             tools="pan, box_zoom, wheel_zoom, reset, save",
             title="Applied Cross-Coupled Stiffness vs. Log Decrement - (API 684 - SP 6.8.5.10)",
+            width=640,
+            height=480,
             x_axis_label="Applied Cross-Coupled Stiffness, Q (N/m)",
             y_axis_label="Log Dec",
-            x_range=(0, max(cross_coupled_stiff)),
+            x_range=(0, max(cross_coupled_Qa)),
             y_range=DataRange1d(start=0),
         )
         fig1.title.text_font_size = "11pt"
         fig1.xaxis.axis_label_text_font_size = "11pt"
         fig1.yaxis.axis_label_text_font_size = "11pt"
 
-        fig1.line(cross_coupled_stiff, log_dec, line_width=3, line_color=bokeh_colors[0])
+        fig1.line(
+            cross_coupled_Qa, log_dec, line_width=3, line_color=bokeh_colors[0]
+        )
+        fig1.circle(Qa, log_dec_a, size=8, fill_color=bokeh_colors[0])
         fig1.add_layout(
-            Span(
-                location=Qa,
-                dimension="height",
-                line_color="black",
-                line_dash="dashed",
-                line_width=3,
+            Label(
+                x=Qa,
+                y=log_dec_a,
+                text="Qa",
+                text_font_style="bold",
+                text_font_size="12pt",
+                text_baseline="middle",
+                text_align="left",
+                y_offset=10
             )
         )
 
@@ -834,23 +925,25 @@ class Report:
         fig2 = figure(
             tools="pan, box_zoom, wheel_zoom, reset, save",
             title="CSR vs. Mean Gas Density - (API 684 - SP 6.8.5.10)",
+            width=640,
+            height=480,
             x_axis_label=x_label1,
             y_axis_label="MCSR",
             y_range=DataRange1d(start=0),
-            x_range=DataRange1d(start=0)
+            x_range=DataRange1d(start=0),
         )
         fig2.title.text_font_size = "11pt"
         fig2.xaxis.axis_label_text_font_size = "11pt"
         fig2.yaxis.axis_label_text_font_size = "11pt"
-        fig2.extra_x_ranges = {"x_range2": Range1d(f*min(RHO), f*max(RHO))}
+        fig2.extra_x_ranges = {"x_range2": Range1d(f * min(RHO), f * max(RHO))}
 
         fig2.add_layout(
             LinearAxis(
-                    x_range_name="x_range2",
-                    axis_label=x_label2,
-                    axis_label_text_font_size="11pt"
+                x_range_name="x_range2",
+                axis_label=x_label2,
+                axis_label_text_font_size="11pt",
             ),
-            place="below"
+            place="below",
         )
         fig2.add_layout(
             Label(
@@ -871,6 +964,7 @@ class Report:
                     x=x,
                     y=y,
                     text=txt,
+                    text_alpha=0.4,
                     text_font_style="bold",
                     text_font_size="12pt",
                     text_baseline="middle",
@@ -878,12 +972,44 @@ class Report:
                 )
             )
 
-        fig2.line(x=RHO, y=Qc, line_width=3, line_color=bokeh_colors[0])
+        fig2.line(x=RHO, y=CSR_boundary, line_width=3, line_color=bokeh_colors[0])
         fig2.circle(x=RHO_mean, y=CSR, size=8, color=bokeh_colors[0])
 
-        plot = gridplot([[fig1, fig2]])
-        
-        return plot
+        # Level 1 screening criteria - API 684 - SP6.8.5.10
+        idx = min(range(len(RHO)), key=lambda i: abs(RHO[i] - RHO_mean))
+
+        if self.machine_type == "compressor":
+            if Q0 / Qa < 2.0:
+                condition = "required"
+
+            if log_dec_a < 0.1:
+                condition = "required"
+
+            if 2.0 < Q0 / Qa < 10.0 and CSR > CSR_boundary[idx]:
+                condition = "required"
+
+            else:
+                condition = "not required"
+
+        if self.machine_type == "turbine" or self.machine_type == "axial flow":
+            if log_dec_a < 0.1:
+                condition = "required"
+
+            else:
+                condition = "not required"
+
+        # updating attributes
+        self.Q0 = Q0
+        self.Qa = Qa
+        self.log_dec_a = log_dec_a
+        self.CSR = CSR
+        self.Qratio = Q0 / Qa
+        self.crit_speed = crit_speed
+        self.MCS = self.maxspeed
+        self.RHO_gas = RHO_mean
+        self.condition = condition
+
+        return fig1, fig2, condition
 
     def stability_level_2(self):
         """Stability analysis level 2.
@@ -900,3 +1026,74 @@ class Report:
         (Check what we need to calculate the applied cross coupling and list
         them as parameters)
         """
+        pass
+
+    def summary(self):
+        """Report summary
+
+        This method will create tables to be presented in the report
+
+        Parameters
+        ----------
+
+        Returns
+        -------
+        tabs : bokeh WidgetBox
+            Bokeh WidgetBox with the summary table plot
+        """
+        machine_data = dict(
+            tags=[self.tag],
+            machine_type=[self.machine_type],
+            Q0=[self.Q0],
+            Qa=[self.Qa],
+            log_dec_a=[self.log_dec_a],
+            Qratio=[self.Qratio],
+            crti_speed=[self.crit_speed],
+            MCS=[self.MCS],
+            CSR=[self.CSR],
+            RHO_gas=[self.RHO_gas],
+        )
+        machine_source = ColumnDataSource(machine_data)
+
+        machine_titles = [
+            "Rotor Tag",
+            "Machine Type",
+            "$Q_0$ (N/m)",
+            "$Q_A$ (N/m)",
+            "",
+            "$Q_0 / Q_A$",
+            "1st Critical Spped (RPM)",
+            "MCS",
+            "CSR",
+            "Gas Density (kg/m³)",
+        ]
+
+        machine_formatters = [
+            None,
+            None,
+            NumberFormatter(format="0.000"),
+            NumberFormatter(format="0.000"),
+            NumberFormatter(format="0.000"),
+            NumberFormatter(format="0.000"),
+            NumberFormatter(format="0.000"),
+            NumberFormatter(format="0.000"),
+            NumberFormatter(format="0.000"),
+            NumberFormatter(format="0.000"),
+        ]
+
+        machine_columns = [
+            TableColumn(field=str(field), title=title, formatter=form)
+            for field, title, form in zip(
+                machine_data.keys(), machine_titles, machine_formatters
+            )
+        ]
+
+        machine_data_table = DataTable(
+            source=machine_source, columns=machine_columns, width=1600
+        )
+        rotor_table = widgetbox(machine_data_table)
+        tab1 = Panel(child=rotor_table, title="Stability Level 1")
+
+        tabs = Tabs(tabs=[tab1])
+
+        return tabs


Test patch:
diff --git a/ross/tests/test_api_report.py b/ross/tests/test_api_report.py
new file mode 100644
index 00000000..01bfa6ac
--- /dev/null
+++ b/ross/tests/test_api_report.py
@@ -0,0 +1,193 @@
+import numpy as np
+import pytest
+from numpy.testing import assert_almost_equal, assert_allclose
+
+from ross.api_report import Report
+from ross.bearing_seal_element import BearingElement
+from ross.disk_element import DiskElement
+from ross.materials import steel
+from ross.rotor_assembly import Rotor
+from ross.shaft_element import ShaftElement
+
+
+@pytest.fixture
+def report0():
+    # rotor type: between bearings
+    i_d = 0.0
+    o_d = 0.05
+    n = 6
+    L = [0.25 for _ in range(n)]
+
+    shaft_elem = [
+        ShaftElement(
+            l, i_d, o_d, steel, shear_effects=True, rotary_inertia=True, gyroscopic=True
+        )
+        for l in L
+    ]
+
+    disk0 = DiskElement.from_geometry(
+        n=2, material=steel, width=0.07, i_d=0.05, o_d=0.28
+    )
+    disk1 = DiskElement.from_geometry(
+        n=4, material=steel, width=0.07, i_d=0.05, o_d=0.35
+    )
+
+    stfx = 1e6
+    stfy = 0.8e6
+    bearing0 = BearingElement(0, kxx=stfx, kyy=stfy, cxx=0, scale_factor=0.5)
+    bearing1 = BearingElement(6, kxx=stfx, kyy=stfy, cxx=0, scale_factor=0.5)
+
+    rotor = Rotor(shaft_elem, [disk0, disk1], [bearing0, bearing1])
+
+    minspeed = 400.0
+    maxspeed = 1000.0
+    machine_type = "compressor"
+    units = "rad/s"
+
+    return Report(rotor, minspeed, maxspeed, machine_type, units)
+
+
+@pytest.fixture
+def report1():
+    # rotor type: single overhung
+    i_d = 0.0
+    o_d = 0.05
+    n = 6
+    L = [0.25 for _ in range(n)]
+
+    shaft_elem = [
+        ShaftElement(
+            l, i_d, o_d, steel, shear_effects=True, rotary_inertia=True, gyroscopic=True
+        )
+        for l in L
+    ]
+
+    disk0 = DiskElement.from_geometry(
+        n=0, material=steel, width=0.07, i_d=0.05, o_d=0.28
+    )
+
+    stfx = 1e6
+    stfy = 0.8e6
+    bearing0 = BearingElement(2, kxx=stfx, kyy=stfy, cxx=0, scale_factor=0.5)
+    bearing1 = BearingElement(6, kxx=stfx, kyy=stfy, cxx=0, scale_factor=0.5)
+
+    rotor = Rotor(shaft_elem, [disk0], [bearing0, bearing1])
+
+    minspeed = 400.0
+    maxspeed = 1000.0
+    machine_type = "turbine"
+    units = "rad/s"
+
+    return Report(rotor, minspeed, maxspeed, machine_type, units)
+
+
+@pytest.fixture
+def report2():
+    # rotor type: single double overhung
+    i_d = 0.0
+    o_d = 0.05
+    n = 6
+    L = [0.25 for _ in range(n)]
+
+    shaft_elem = [
+        ShaftElement(
+            l, i_d, o_d, steel, shear_effects=True, rotary_inertia=True, gyroscopic=True
+        )
+        for l in L
+    ]
+
+    disk0 = DiskElement.from_geometry(
+        n=0, material=steel, width=0.07, i_d=0.05, o_d=0.28
+    )
+    disk1 = DiskElement.from_geometry(
+        n=6, material=steel, width=0.07, i_d=0.05, o_d=0.35
+    )
+
+    stfx = 1e6
+    stfy = 0.8e6
+    bearing0 = BearingElement(2, kxx=stfx, kyy=stfy, cxx=0, scale_factor=0.5)
+    bearing1 = BearingElement(4, kxx=stfx, kyy=stfy, cxx=0, scale_factor=0.5)
+
+    rotor = Rotor(shaft_elem, [disk0, disk1], [bearing0, bearing1])
+
+    minspeed = 3820.0
+    maxspeed = 9550.0
+    machine_type = "pump"
+    units = "rpm"
+
+    return Report(rotor, minspeed, maxspeed, machine_type, units)
+
+
+def test_initial_attributes(report0, report1, report2):
+    assert report0.rotor_type == 'between_bearings'
+    assert report0.disk_nodes == [2, 4]
+    assert report0.machine_type == 'compressor'
+    assert report0.tag == 'Rotor 0'
+    assert_allclose(report0.maxspeed, 1000.0, atol=1e-2)
+    assert_allclose(report0.minspeed, 400.0, atol=1e-2)
+    assert report1.rotor_type == 'single_overhung_l'
+    assert report1.disk_nodes == [0]
+    assert report1.machine_type == 'turbine'
+    assert report1.tag == 'Rotor 0'
+    assert_allclose(report1.maxspeed, 1000.0, atol=1e-2)
+    assert_allclose(report1.minspeed, 400.0, atol=1e-2)
+    assert report2.rotor_type == 'double_overhung'
+    assert report2.disk_nodes == [0, 6]
+    assert report2.machine_type == 'compressor'
+    assert report2.tag == 'Rotor 0'
+    assert_allclose(report2.maxspeed, 1000.0736613927509, atol=1e-8)
+    assert_allclose(report2.minspeed, 400.0294645571003, atol=1e-8)
+
+
+def test_report_static_forces(report0, report1, report2):
+    F_0 = report0.static_forces()
+    F_1 = report1.static_forces()
+    F_2 = report2.static_forces()
+    assert_allclose(F_0[0], 50.40534339, atol=1e-6)
+    assert_allclose(F_0[1], 56.7174833, atol=1e-6)
+    assert_allclose(F_1[0], 66.13980523, atol=1e-6)
+    assert_allclose(F_1[1], -10.54402692, atol=1e-6)
+    assert_allclose(F_2[0], 25.15678377, atol=1e-6)
+    assert_allclose(F_2[1], 81.96604293, atol=1e-6)
+
+
+def test_unbalance_forces(report0, report1, report2):
+    Uforce_00 = report0.unbalance_forces(mode=0)
+    assert_allclose(Uforce_00, [71.23351373], atol=1e-6)
+
+    Uforce_02 = report0.unbalance_forces(mode=2)
+    assert_allclose(Uforce_02, [33.51806362, 37.71545011], atol=1e-6)
+
+    Uforce_10 = report1.unbalance_forces(mode=0)
+    assert_allclose(Uforce_10, [26.769833052956542], atol=1e-6)
+
+    Uforce_12 = report1.unbalance_forces(mode=2)
+    assert_allclose(Uforce_12, [26.769833052956542], atol=1e-6)
+
+    Uforce_20 = report2.unbalance_forces(mode=0)
+    assert_allclose(Uforce_20, np.array([26.7678613, 39.35850396]), atol=1e-6)
+
+    Uforce_22 = report2.unbalance_forces(mode=2)
+    assert_allclose(Uforce_22, np.array([26.7678613, 39.35850396]), atol=1e-6)
+
+
+def test_report_mode_shape(report0, report1, report2):
+    n1, n2 = report0.mode_shape(mode=0)
+    nodes = [int(node) for sub_nodes in [n1, n2] for node in sub_nodes]
+    assert nodes == [3]
+
+    n1, n2 = report1.mode_shape(mode=0)
+    nodes = [int(node) for sub_nodes in [n1, n2] for node in sub_nodes]
+    assert nodes == [0]
+
+    n1, n2 = report1.mode_shape(mode=3)
+    nodes = [int(node) for sub_nodes in [n1, n2] for node in sub_nodes]
+    assert nodes == [0]
+
+    n1, n2 = report2.mode_shape(mode=0)
+    nodes = [int(node) for sub_nodes in [n1, n2] for node in sub_nodes]
+    assert nodes == [0, 6]
+
+    n1, n2 = report2.mode_shape(mode=3)
+    nodes = [int(node) for sub_nodes in [n1, n2] for node in sub_nodes]
+    assert nodes == [0, 6]