Repository: ImperialCollegeLondon/sharpy (language: python)

Issue:
Control surface deflection sign convention
Hi all,

This is related to #192 but really is a topic that needs addressing on its own as it is something that we may want to change, although, if we do, will imply backwards changes to all models with symmetric control surfaces.

## TLDR

Control surfaces deflections are not well defined according to a sign convention, which may pose problems in the future for antisymmetric control surfaces (ailerons) and may suppose now a difference in the linear versus nonlinear implementation of the control surfaces. We either change behaviour or continue as is, knowing how it works (a bug becomes a feature if you leave it long enough).

## Background

Control surfaces are implemented in the [aerogrid.py](https://github.com/ImperialCollegeLondon/sharpy/blob/master/sharpy/aero/models/aerogrid.py) file as part of the grid assembly process. 

The UVLM grid is assembled in "strips", where a strip is a chord wise section. These strips are concatenated in a span wise manner resulting in surfaces. 

A strip is generated here: https://github.com/ImperialCollegeLondon/sharpy/blob/41b6cb699fe33167d66cbe0c5ede1bdfaaeb50c4/sharpy/aero/models/aerogrid.py#L482

The whole surface is generated here:
https://github.com/ImperialCollegeLondon/sharpy/blob/41b6cb699fe33167d66cbe0c5ede1bdfaaeb50c4/sharpy/aero/models/aerogrid.py#L205

In the process of generating the strip, control surfaces are done first, in the local B frame by rotating the relevant chord wise section about `xb = [1, 0, 0]`. Then, the section is rotated according to any desired twist or sweep.

## Current process

These strips are concatenated sequentially in increasing node order. However, any rotation on the strip to assemble the surface is performed AFTER the control surface is implemented. Therefore, the control surface is rotated about an axis different from the material frame.

Take, for example the following configuration, where the arrow indicates the local xb, which is aligned with the beam and positive in the increasing node number direction
```
4 -<- 3 -<- 0 ->- 1 ->- 2 
```
The grid strips are generated sequentially for each element. Since the control surfaces are implemented always rotating about the local `[1, 0, 0]` and these are appended, the resulting rotation axis of each CS upon assembly looks like:
```
4 ->- 3 ->- 0 ->- 1 -> 2
```
which is NOT coincident with xb.

## Current behaviour

The current implementation is that, for symmetric control surfaces with a single control surface input (i.e. same `control_surface_id`, control surfaces are deflected in the same direction on both wings, as if they were elevators. Most, if not all, work done in SHARPy has been focused on longitudinal problems so I understand that this is simple as a single input is needed to define an elevator.

### Issues
* The sign convention is not well defined.
* This is not easily implemented in the linear solver, where no strips are generated, and a more clear convention is required.

## Options
1. Leave as is. Backwards compatible and as long as the user is informed on how this works for the nonlinear problem it should be ok. A different convention will be required in the linear solver which may need control surfaces to be defined separately (different `control_surface_id`) as there the convention will be the local `xb`. How would antisymmetric control surfaces (ailerons) be implemented with the current implementation and a single input is yet to be determined.
2. Change the behaviour, this can be done by moving where the control surface is implemented in https://github.com/ImperialCollegeLondon/sharpy/blob/41b6cb699fe33167d66cbe0c5ede1bdfaaeb50c4/sharpy/aero/models/aerogrid.py#L482
moving the `#control surface implementation` after `#transformation to beam and beam prime`.  The result is 
![image](https://user-images.githubusercontent.com/35684407/165939685-b98e229e-39b7-4429-a72a-0f71e435539b.png)
If this is done, this will affect all previous models as effectively all previously implemented control surfaces will deflect anti-symmetrically. The current behaviour (i.e. elevator-like) could be retained by modifying the models `xb` as 
```
3 ->- 4 ->- 0 ->- 1 -> 2
```
The advantage of this being that the sign convention is clearly defined, and that the same approach is retained in the linear solver.
3. A final option would be to leave as is, introducing some kind of "linkage" between control surfaces. This could mean a -1 gain from one to another to simulate ailerons in the current nonlinear implementation.

## Please comment :)
A bit of a longwinded explanation but I think that should be tackled as the code goes forward. Probably the easiest short term option is 1) (and I'll see what to do for the linear solver in relation to #192) but we may want to look towards option 2) for future releases.
Support loading/saving state-spaces and gains to h5 files
Little feature to save and load state-spaces and gains to h5 files. This saves as well the output/states/input properties, allowing them to be retrieved as `StateSpace` or `Gain` objects when loaded from file
Update AsymptoticStability and FrequencyResponse post-processors
Updates to these two post-processors, including
- Analysing a target system (i.e. aeroelastic, aerodynamic or structural)
- Saving eigenvalues to `.h5` file or as text (`.dat`)
- Improving handling of scaled systems and the use of reduced frequency
- Improved eigenvector plotting utils 
Flap inputs in state-space model not working in certain wing configurations
Issue in linear control surfaces depending on the geometrical alignment of the wing, due to a bug in the linearisation process that, mistakenly, used the wrong indices in `for_delta` variable. 

The original intent was to mimic the behaviour of the nonlinear control surfaces where, for a control surface installed on right and left wings with the same `control_surface_id`, the deflection was symmetric despite an `x_b` axis in opposite directions, which would thereby violate the "right hand rule" for simplicity in the definition of the inputs. This led to the incorrect use of the `for_delta` variable that, depending on the wing alignment, could lead to a matrix mapping the displacements of the control surface to the lattice grid full of zeros.

Fix in progress...

### Originally raised in https://github.com/ImperialCollegeLondon/sharpy/discussions/190

<div type='discussions-op-text'>

<sup>Originally posted by **PiH2000** April 26, 2022</sup>
Hello,

I am trying to extract a state-space model for a wing with a flap at the tip.
However, the 2 columns in the B and D matrices corresponding to the flap inputs (deflection & velocity), and only them, are full of zeros. I have tried many different things, such as changing the surface from static to dynamic, increasing its size etc. but I keep getting the same issue.

The code that I am using is based on the FlyingWing class template. I have the attached the solver settings file. I am using a M=5, N=52 discretisation and the assembled aeroelastic system, in modal coordinates, is of the form:

Aeroelastic system assembled:
Aerodynamic states: 5460
Structural states: 10
Total states: 5470
Inputs: 17
Outputs: 15

Is there any setting in particular that would allow the flap to be modelled in the state-space system? Am I missing something?

Thank you for your help.
Pierre

[settings_M5N52u20.solver.txt](https://github.com/ImperialCollegeLondon/sharpy/files/8565257/settings_M5N52u20.solver.txt)
</div>


Discussion / hints:

Fix patch:
diff --git a/docs/source/includes/linear/assembler/lineargustassembler/LeadingEdge.rst b/docs/source/includes/linear/assembler/lineargustassembler/LeadingEdge.rst
new file mode 100644
index 000000000..3a880ccf5
--- /dev/null
+++ b/docs/source/includes/linear/assembler/lineargustassembler/LeadingEdge.rst
@@ -0,0 +1,5 @@
+LeadingEdge
+-----------
+
+.. autoclass:: sharpy.linear.assembler.lineargustassembler.LeadingEdge
+	:members:
\ No newline at end of file
diff --git a/docs/source/includes/linear/assembler/lineargustassembler/LinearGustGenerator.rst b/docs/source/includes/linear/assembler/lineargustassembler/LinearGustGenerator.rst
deleted file mode 100644
index 83cdd4ca0..000000000
--- a/docs/source/includes/linear/assembler/lineargustassembler/LinearGustGenerator.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-LinearGustGenerator
--------------------
-
-.. autoclass:: sharpy.linear.assembler.lineargustassembler.LinearGustGenerator
-	:members:
\ No newline at end of file
diff --git a/docs/source/includes/linear/assembler/lineargustassembler/MultiLeadingEdge.rst b/docs/source/includes/linear/assembler/lineargustassembler/MultiLeadingEdge.rst
new file mode 100644
index 000000000..a45d964fa
--- /dev/null
+++ b/docs/source/includes/linear/assembler/lineargustassembler/MultiLeadingEdge.rst
@@ -0,0 +1,5 @@
+MultiLeadingEdge
+----------------
+
+.. autoclass:: sharpy.linear.assembler.lineargustassembler.MultiLeadingEdge
+	:members:
\ No newline at end of file
diff --git a/docs/source/includes/linear/assembler/lineargustassembler/campbell.rst b/docs/source/includes/linear/assembler/lineargustassembler/campbell.rst
new file mode 100644
index 000000000..a102d794e
--- /dev/null
+++ b/docs/source/includes/linear/assembler/lineargustassembler/campbell.rst
@@ -0,0 +1,4 @@
+campbell
+--------
+
+.. automodule:: sharpy.linear.assembler.lineargustassembler.campbell
\ No newline at end of file
diff --git a/docs/source/includes/linear/assembler/lineargustassembler/gust_from_string.rst b/docs/source/includes/linear/assembler/lineargustassembler/gust_from_string.rst
new file mode 100644
index 000000000..0366dce9e
--- /dev/null
+++ b/docs/source/includes/linear/assembler/lineargustassembler/gust_from_string.rst
@@ -0,0 +1,4 @@
+gust_from_string
+----------------
+
+.. automodule:: sharpy.linear.assembler.lineargustassembler.gust_from_string
\ No newline at end of file
diff --git a/docs/source/includes/linear/assembler/lineargustassembler/index.rst b/docs/source/includes/linear/assembler/lineargustassembler/index.rst
index c6a0113b3..bd3718f47 100644
--- a/docs/source/includes/linear/assembler/lineargustassembler/index.rst
+++ b/docs/source/includes/linear/assembler/lineargustassembler/index.rst
@@ -1,4 +1,8 @@
 .. toctree::
 	:glob:
 
-	./LinearGustGenerator
+	./LeadingEdge
+	./MultiLeadingEdge
+	./campbell
+	./gust_from_string
+	./spanwise_interpolation
diff --git a/docs/source/includes/linear/assembler/lineargustassembler/spanwise_interpolation.rst b/docs/source/includes/linear/assembler/lineargustassembler/spanwise_interpolation.rst
new file mode 100644
index 000000000..cc206310f
--- /dev/null
+++ b/docs/source/includes/linear/assembler/lineargustassembler/spanwise_interpolation.rst
@@ -0,0 +1,4 @@
+spanwise_interpolation
+----------------------
+
+.. automodule:: sharpy.linear.assembler.lineargustassembler.spanwise_interpolation
\ No newline at end of file
diff --git a/docs/source/includes/linear/src/libss/StateSpace.rst b/docs/source/includes/linear/src/libss/StateSpace.rst
new file mode 100644
index 000000000..dc2571338
--- /dev/null
+++ b/docs/source/includes/linear/src/libss/StateSpace.rst
@@ -0,0 +1,5 @@
+StateSpace
+----------
+
+.. autoclass:: sharpy.linear.src.libss.StateSpace
+	:members:
\ No newline at end of file
diff --git a/docs/source/includes/linear/src/libss/index.rst b/docs/source/includes/linear/src/libss/index.rst
index cfd72d46b..5f05d2cb1 100644
--- a/docs/source/includes/linear/src/libss/index.rst
+++ b/docs/source/includes/linear/src/libss/index.rst
@@ -11,7 +11,7 @@ the sparse arrays types defined in libsparse.
 The module includes:
 
 Classes:
-- ss: provides a class to build DLTI/LTI systems with full and/or sparse
+- StateSpace: provides a class to build DLTI/LTI systems with full and/or sparse
 	matrices and wraps many of the methods in these library. Methods include:
 	- freqresp: wraps the freqresp function
 	- addGain: adds gains in input/output. This is not a wrapper of addGain, as
@@ -61,6 +61,7 @@ to do:
 	./SSconv
 	./SSderivative
 	./SSintegr
+	./StateSpace
 	./addGain
 	./adjust_phase
 	./build_SS_poly
@@ -76,10 +77,10 @@ to do:
 	./parallel
 	./project
 	./random_ss
+	./retain_inout_channels
 	./scale_SS
 	./series
 	./simulate
-	./ss
 	./ss_block
 	./ss_to_scipy
 	./sum_ss
diff --git a/docs/source/includes/linear/src/libss/retain_inout_channels.rst b/docs/source/includes/linear/src/libss/retain_inout_channels.rst
new file mode 100644
index 000000000..67cd41d84
--- /dev/null
+++ b/docs/source/includes/linear/src/libss/retain_inout_channels.rst
@@ -0,0 +1,4 @@
+retain_inout_channels
+---------------------
+
+.. automodule:: sharpy.linear.src.libss.retain_inout_channels
\ No newline at end of file
diff --git a/docs/source/includes/linear/src/libss/ss.rst b/docs/source/includes/linear/src/libss/ss.rst
deleted file mode 100644
index 6fbe89b77..000000000
--- a/docs/source/includes/linear/src/libss/ss.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-ss
---
-
-.. autoclass:: sharpy.linear.src.libss.ss
-	:members:
\ No newline at end of file
diff --git a/docs/source/includes/postprocs/StabilityDerivatives.rst b/docs/source/includes/postprocs/StabilityDerivatives.rst
new file mode 100644
index 000000000..a8bf30a08
--- /dev/null
+++ b/docs/source/includes/postprocs/StabilityDerivatives.rst
@@ -0,0 +1,7 @@
+StabilityDerivatives
+--------------------
+
+
+
+.. autoclass:: sharpy.postproc.stabilityderivatives.StabilityDerivatives
+	:members:
\ No newline at end of file
diff --git a/docs/source/includes/structure/utils/modalutils/index.rst b/docs/source/includes/structure/utils/modalutils/index.rst
index 2d63a80e9..611a05a70 100644
--- a/docs/source/includes/structure/utils/modalutils/index.rst
+++ b/docs/source/includes/structure/utils/modalutils/index.rst
@@ -6,6 +6,8 @@
 	./free_modes_principal_axes
 	./get_mode_zeta
 	./mode_sign_convention
+	./modes_to_cg_ref
+	./principal_axes_inertia
 	./scale_mass_normalised_modes
 	./scale_mode
 	./write_modes_vtk
diff --git a/docs/source/includes/structure/utils/modalutils/modes_to_cg_ref.rst b/docs/source/includes/structure/utils/modalutils/modes_to_cg_ref.rst
new file mode 100644
index 000000000..6694ae30b
--- /dev/null
+++ b/docs/source/includes/structure/utils/modalutils/modes_to_cg_ref.rst
@@ -0,0 +1,4 @@
+modes_to_cg_ref
+---------------
+
+.. automodule:: sharpy.structure.utils.modalutils.modes_to_cg_ref
\ No newline at end of file
diff --git a/docs/source/includes/structure/utils/modalutils/principal_axes_inertia.rst b/docs/source/includes/structure/utils/modalutils/principal_axes_inertia.rst
new file mode 100644
index 000000000..e2828c8f0
--- /dev/null
+++ b/docs/source/includes/structure/utils/modalutils/principal_axes_inertia.rst
@@ -0,0 +1,4 @@
+principal_axes_inertia
+----------------------
+
+.. automodule:: sharpy.structure.utils.modalutils.principal_axes_inertia
\ No newline at end of file
diff --git a/sharpy/aero/utils/utils.py b/sharpy/aero/utils/utils.py
index 204b6300c..c1f842884 100644
--- a/sharpy/aero/utils/utils.py
+++ b/sharpy/aero/utils/utils.py
@@ -121,3 +121,55 @@ def span_chord(i_node_surf, zeta):
     dir_chord = algebra.unit_vector(dir_chord)
 
     return dir_span, span, dir_chord, chord
+
+
+def find_aerodynamic_solver(settings):
+    """
+    Retrieves the name and settings of the first aerodynamic solver used in the solution ``flow``.
+
+    Args:
+        settings (dict): SHARPy settings (usually found in ``data.settings`` )
+
+    Returns:
+        tuple: Aerodynamic solver name and solver settings
+    """
+    flow = settings['SHARPy']['flow']
+    # Look for the aerodynamic solver
+    if 'StaticUvlm' in flow:
+        aero_solver_name = 'StaticUvlm'
+        aero_solver_settings = settings['StaticUvlm']
+    elif 'StaticCoupled' in flow:
+        aero_solver_name = settings['StaticCoupled']['aero_solver']
+        aero_solver_settings = settings['StaticCoupled']['aero_solver_settings']
+    elif 'StaticCoupledRBM' in flow:
+        aero_solver_name = settings['StaticCoupledRBM']['aero_solver']
+        aero_solver_settings = settings['StaticCoupledRBM']['aero_solver_settings']
+    elif 'DynamicCoupled' in flow:
+        aero_solver_name = settings['DynamicCoupled']['aero_solver']
+        aero_solver_settings = settings['DynamicCoupled']['aero_solver_settings']
+    elif 'StepUvlm' in flow:
+        aero_solver_name = 'StepUvlm'
+        aero_solver_settings = settings['StepUvlm']
+    else:
+        raise KeyError("ERROR: aerodynamic solver not found")
+
+    return aero_solver_name, aero_solver_settings
+
+
+def find_velocity_generator(settings):
+    """
+    Retrieves the name and settings of the fluid velocity generator in the first aerodynamic solver used in the
+    solution ``flow``.
+
+    Args:
+        settings (dict): SHARPy settings (usually found in ``data.settings`` )
+
+    Returns:
+        tuple: velocity generator name and velocity generator settings
+    """
+    aero_solver_name, aero_solver_settings = find_aerodynamic_solver(settings)
+
+    vel_gen_name = aero_solver_settings['velocity_field_generator']
+    vel_gen_settings = aero_solver_settings['velocity_field_input']
+
+    return vel_gen_name, vel_gen_settings
diff --git a/sharpy/cases/hangar/horten_wing.py b/sharpy/cases/hangar/horten_wing.py
index 9f3611078..3a8fbe7e0 100644
--- a/sharpy/cases/hangar/horten_wing.py
+++ b/sharpy/cases/hangar/horten_wing.py
@@ -270,7 +270,6 @@ def dynamic_control_surface(self, *delta):
             np.savetxt(self.case_route + '/' + self.case_name + '.input.txt', cs)
             i += 1
 
-
     def planform_area(self):
         S_fus = 0.5 * (self.c_fuselage + self.c_root) * self.fuselage_width
         S_wing = 0.5 * (self.c_root + self.c_root*self.taper_ratio) * self.span / 2
@@ -1299,7 +1298,7 @@ def set_default_config_dict(self, route=None, case_name=None):
                              'rigid_body_modes': True,
                              'write_modes_vtk': 'on',
                              'print_matrices': 'on',
-                             'write_data': 'on',
+                             'save_data': 'on',
                              'continuous_eigenvalues': 'off',
                              'dt': dt,
                              'plot_eigenvalues': False}
diff --git a/sharpy/cases/hangar/richards_wing.py b/sharpy/cases/hangar/richards_wing.py
index 49da29392..41eada3d2 100644
--- a/sharpy/cases/hangar/richards_wing.py
+++ b/sharpy/cases/hangar/richards_wing.py
@@ -135,13 +135,13 @@ def update_mass_stiffness(self, sigma=1., sigma_mass=1., payload=0):
             # Create full cross section
             c_bar = self.c_root * ((1-alpha[i_elem]) + self.taper_ratio * alpha[i_elem])
             x_section = WingCrossSection(c_bar)
-            print(i_elem)
-            print('Section Mass: %.2f ' %x_section.mass)
-            print('Linear Mass: %.2f' % (mu_0 * (1-alpha[i_elem]) + mu_0 * self.taper_ratio * alpha[i_elem]))
-            print('Section Ixx: %.4f' % x_section.ixx)
-            print('Section Iyy: %.4f' % x_section.iyy)
-            print('Section Izz: %.4f' % x_section.izz)
-            print('Linear Ixx: %.2f' % (j_xx * (1-alpha[i_elem]) + j_xx * self.taper_ratio * alpha[i_elem]))
+            # print(i_elem)
+            # print('Section Mass: %.2f ' %x_section.mass)
+            # print('Linear Mass: %.2f' % (mu_0 * (1-alpha[i_elem]) + mu_0 * self.taper_ratio * alpha[i_elem]))
+            # print('Section Ixx: %.4f' % x_section.ixx)
+            # print('Section Iyy: %.4f' % x_section.iyy)
+            # print('Section Izz: %.4f' % x_section.izz)
+            # print('Linear Ixx: %.2f' % (j_xx * (1-alpha[i_elem]) + j_xx * self.taper_ratio * alpha[i_elem]))
             # base_mass[i_elem, :, :] = mass_root_right*(1-alpha[i_elem]) + mass_tip_right*alpha[i_elem]
             # base_mass[i_elem + self.n_elem_wing + self.n_elem_fuselage - 1] = mass_root_left*(1-alpha[i_elem]) + mass_tip_left*alpha[i_elem]
 
diff --git a/sharpy/cases/hangar/swept_flying_wing.py b/sharpy/cases/hangar/swept_flying_wing.py
index 2e2b57a19..c95c880b4 100644
--- a/sharpy/cases/hangar/swept_flying_wing.py
+++ b/sharpy/cases/hangar/swept_flying_wing.py
@@ -1095,7 +1095,7 @@ def set_default_config_dict(self, route=None, case_name=None):
                              'rigid_body_modes': True,
                              'write_modes_vtk': 'on',
                              'print_matrices': 'on',
-                             'write_data': 'on',
+                             'save_data': 'on',
                              'continuous_eigenvalues': 'off',
                              'dt': dt,
                              'plot_eigenvalues': False}
diff --git a/sharpy/cases/templates/Ttail.py b/sharpy/cases/templates/Ttail.py
index d2b5be010..7a5643cb1 100644
--- a/sharpy/cases/templates/Ttail.py
+++ b/sharpy/cases/templates/Ttail.py
@@ -559,7 +559,6 @@ def set_default_config_dict(self):
 
         config['Modal'] = {'NumLambda': 60,
                            'print_matrices': 'off',
-                           'keep_linear_matrices': 'on',
                            'write_modes_vtk': True,
                            'use_undamped_modes': True}
 
diff --git a/sharpy/cases/templates/flying_wings.py b/sharpy/cases/templates/flying_wings.py
index 01b006119..3b3cd03b7 100644
--- a/sharpy/cases/templates/flying_wings.py
+++ b/sharpy/cases/templates/flying_wings.py
@@ -600,8 +600,7 @@ def set_default_config_dict(self):
         config['Modal'] = {'NumLambda': 20,
                            'rigid_body_modes': 'off',
                            'print_matrices': 'off',
-                           'keep_linear_matrices': 'on',
-                           'write_dat': 'off',
+                           'save_data': 'off',
                            'continuous_eigenvalues': 'off',
                            'dt': 0,
                            'plot_eigenvalues': False,
diff --git a/sharpy/cases/templates/template_wt.py b/sharpy/cases/templates/template_wt.py
index 25beef137..e36687d17 100644
--- a/sharpy/cases/templates/template_wt.py
+++ b/sharpy/cases/templates/template_wt.py
@@ -4,7 +4,7 @@
 Functions needed to generate a wind turbines
 
 Notes:
-    To load this library: import cases.templates.template_wt as template_wt
+    To load this library: import sharpy.cases.templates.template_wt as template_wt
 """
 
 
diff --git a/sharpy/linear/assembler/lincontrolsurfacedeflector.py b/sharpy/linear/assembler/lincontrolsurfacedeflector.py
index 169a37c9a..11dbb84fd 100644
--- a/sharpy/linear/assembler/lincontrolsurfacedeflector.py
+++ b/sharpy/linear/assembler/lincontrolsurfacedeflector.py
@@ -4,12 +4,23 @@
 import sharpy.linear.utils.ss_interface as ss_interface
 import numpy as np
 import sharpy.utils.algebra as algebra
+import sharpy.linear.src.libsparse as libsp
+import scipy.sparse as sp
+import sharpy.linear.src.libss as libss
 
 
 @ss_interface.linear_system
 class LinControlSurfaceDeflector(object):
     """
-    Subsystem that deflects control surfaces for use with linear state space systems
+    Subsystem that deflects control surfaces for use with linear state space systems.
+
+    Note:
+        The control surface sign convention is different to the convention in the nonlinear solver. See
+        https://www.github.com/imperialcollegelondon/sharpy/issues/193 for more details.
+
+    In the linear implementation, the control surface deflection sign convention follows the :math:`x_B` vector,
+    thus, in order to have symmetric control surface deflections, additional inputs may be required compared to the
+    implementation in the nonlinear solver.
 
     The current version supports only deflections. Future work will include standalone state-space systems to model
     physical actuators.
@@ -31,11 +42,11 @@ def __init__(self):
         self.tsaero0 = None
         self.tsstruct0 = None
 
+        self.gain_cs = None  # type: np.ndarray # input gain to the UVLM
+
+        self.print_info = False  # used for debugging purposes
+
     def initialise(self, data, linuvlm):
-        # Tasks:
-        # 1 - Generic information
-        #   * How many control surfaces (number of actual inputs)
-        #   * How many uvlm surfaces
         self.n_control_surfaces = data.aero.n_control_surfaces
 
         self.linuvlm = linuvlm
@@ -93,7 +104,7 @@ def generate(self):
                 if global_node in structure.connectivities[i_elem, :]:
                     i_local_node = np.where(structure.connectivities[i_elem, :] == global_node)[0][0]
 
-                    for_delta = structure.frame_of_reference_delta[i_elem, :, 0]
+                    for_delta = structure.frame_of_reference_delta[i_elem, i_local_node, :]
 
                     # CRV to transform from G to B frame
                     psi = tsstruct0.psi[i_elem, i_local_node]
@@ -165,27 +176,80 @@ def generate(self):
                                     zeta_node = zeta0[i_vertex]  # Gframe
                                     chord_vec = (zeta_node - zeta_hinge)
 
+                                    if self.print_info:
+                                        print(f'i_node = {global_node}')
+                                        print(f'i_node_chord = {i_node_chord}')
+                                        print(f'zeta_node = {zeta_node}')
+                                        print(f'chord_vec = {chord_vec}')
+                                        print(f'zeta_hinge = {zeta_hinge}')
+                                        print(f'zeta_next_hinge = {zeta_next_hinge}')
+                                        print(f'hinge_axis = {hinge_axis}')
+
+                                    # Flap displacement
                                     Kdisp[i_vertex, i_control_surface] = \
-                                        Cgb.dot(der_R_arbitrary_axis_times_v(Cbg.dot(hinge_axis),
-                                                                             0,
-                                                                             -for_delta * Cbg.dot(chord_vec))) * for_delta * -1
+                                        der_R_arbitrary_axis_times_v(hinge_axis, 0, chord_vec)
 
                                     # Flap velocity
-                                    Kvel[i_vertex, i_control_surface] = -algebra.skew(-for_delta * chord_vec).dot(
-                                        hinge_axis) * for_delta * -1
+                                    Kvel[i_vertex, i_control_surface] = -algebra.skew(chord_vec).dot(
+                                        hinge_axis)
+
+                                    if self.print_info:
+                                        print('Matrix entries:')
+                                        print('Kdisp:')
+                                        print(Kdisp[i_vertex, i_control_surface])
+                                        print('Kvel:')
+                                        print(Kvel[i_vertex, i_control_surface])
 
                         else:
                             with_control_surface = False
                             hinge_axis = None  # Reset for next control surface
 
-        # >>>> Merge control surfaces 0 and 1
-        # Kdisp[:, 0] -= Kdisp[:, 1]
-        # Kvel[:, 0] -= Kvel[:, 1]
-
         self.Kzeta_delta = Kdisp
         self.Kdzeta_ddelta = Kvel
+
         return Kdisp, Kvel
 
+    def apply(self, ss):
+        """
+        Applies the control surface deflection to the UVLM state space
+
+        Args:
+            ss (libss.StateSpace): UVLM state space
+
+        Returns:
+            libss.StateSpace: UVLM state-space with control surfaces and control surface deflection rate as inputs
+        """
+
+        Kzeta_delta, Kdzeta_ddelta = self.generate()
+        n_zeta, n_ctrl_sfc = Kzeta_delta.shape
+
+        if type(ss.A) is libsp.csc_matrix:
+            gain_cs = sp.eye(ss.inputs, ss.inputs + 2 * self.n_control_surfaces,
+                             format='lil')
+            gain_cs[:n_zeta, ss.inputs: ss.inputs + n_ctrl_sfc] = Kzeta_delta
+            gain_cs[n_zeta: 2*n_zeta, ss.inputs + n_ctrl_sfc: ss.inputs + 2 * n_ctrl_sfc] = Kdzeta_ddelta
+            gain_cs = libsp.csc_matrix(gain_cs)
+        else:
+            gain_cs = np.eye(ss.inputs, ss.inputs + 2 * self.n_control_surfaces)
+            gain_cs[:n_zeta, ss.inputs: ss.inputs + n_ctrl_sfc] = Kzeta_delta
+            gain_cs[n_zeta: 2*n_zeta, ss.inputs + n_ctrl_sfc: ss.inputs + 2 * n_ctrl_sfc] = Kdzeta_ddelta
+
+        control_surface_gain = libss.Gain(gain_cs)
+        in_vars = ss.input_variables.copy()
+        in_vars.append('control_surface_deflection', size=n_ctrl_sfc)
+        in_vars.append('dot_control_surface_deflection', size=n_ctrl_sfc)
+        control_surface_gain.input_variables = in_vars
+        control_surface_gain.output_variables = ss_interface.LinearVector.transform(ss.input_variables,
+                                                                                    to_type=ss_interface.OutputVariable)
+
+        self.gain_cs = control_surface_gain
+
+        return ss
+    # def generator():
+    # future feature idea: instead of defining the inputs for the time domain simulations as the whole input vector
+    # etc, we could add a generate() method to these systems that can be called from the LinDynamicSim to apply
+    # the gust and generate the correct input vector.
+
 
 def der_Cx_by_v(delta, v):
     sd = np.sin(delta)
diff --git a/sharpy/linear/assembler/linearaeroelastic.py b/sharpy/linear/assembler/linearaeroelastic.py
index 370ba2e75..752654cd1 100644
--- a/sharpy/linear/assembler/linearaeroelastic.py
+++ b/sharpy/linear/assembler/linearaeroelastic.py
@@ -8,6 +8,8 @@
 import sharpy.utils.cout_utils as cout
 import sharpy.utils.algebra as algebra
 import sharpy.utils.generator_interface as gi
+from sharpy.linear.utils.ss_interface import LinearVector, InputVariable, StateVariable, OutputVariable
+import sharpy.aero.utils.utils as aero_utils
 
 
 @ss_interface.linear_system
@@ -101,30 +103,8 @@ def initialise(self, data):
         self.uvlm = ss_interface.initialise_system('LinearUVLM')
         self.uvlm.initialise(data, custom_settings=self.settings['aero_settings'])
 
-        # Look for the aerodynamic solver
-        if 'StaticUvlm' in data.settings:
-            aero_solver_name = 'StaticUvlm'
-            aero_solver_settings = data.settings['StaticUvlm']
-        elif 'StaticCoupled' in data.settings:
-            aero_solver_name = data.settings['StaticCoupled']['aero_solver']
-            aero_solver_settings = data.settings['StaticCoupled']['aero_solver_settings']
-        elif 'StaticCoupledRBM' in data.settings:
-            aero_solver_name = data.settings['StaticCoupledRBM']['aero_solver']
-            aero_solver_settings = data.settings['StaticCoupledRBM']['aero_solver_settings']
-        elif 'DynamicCoupled' in data.settings:
-            aero_solver_name = data.settings['DynamicCoupled']['aero_solver']
-            aero_solver_settings = data.settings['DynamicCoupled']['aero_solver_settings']
-        elif 'StepUvlm' in data.settings:
-            aero_solver_name = 'StepUvlm'
-            aero_solver_settings = data.settings['StepUvlm']
-        else:
-            raise RuntimeError("ERROR: aerodynamic solver not found")
-
-        # Velocity generator
-        vel_gen_name = aero_solver_settings['velocity_field_generator']
-        vel_gen_settings = aero_solver_settings['velocity_field_input']
-
         # Get the minimum parameters needed to define the wake
+        vel_gen_name, vel_gen_settings = aero_utils.find_velocity_generator(data.settings)
         vel_gen_type = gi.generator_from_string(vel_gen_name)
         vel_gen = vel_gen_type()
         vel_gen.initialise(vel_gen_settings) 
@@ -198,6 +178,12 @@ def assemble(self):
         if not self.load_uvlm_from_file:
             # Projecting the UVLM inputs and outputs onto the structural degrees of freedom
             Ksa = self.Kforces[:beam.sys.num_dof, :]  # maps aerodynamic grid forces to nodal forces
+            gain_ksa = libss.Gain(Ksa)
+            gain_ksa.input_variables = LinearVector.transform(uvlm.ss.output_variables, to_type=InputVariable)
+            if beam.sys.Kin is not None:
+                gain_ksa.output_variables = LinearVector.transform(beam.sys.Kin.input_variables, to_type=OutputVariable)
+            else:
+                gain_ksa.output_variables = LinearVector.transform(beam.ss.input_variables, to_type=OutputVariable)
 
             # Map the nodal displacement and velocities onto the grid displacements and velocities
             Kas = np.zeros((uvlm.ss.inputs, 2*beam.sys.num_dof + (uvlm.ss.inputs - 2*self.Kdisp.shape[0])))
@@ -208,12 +194,23 @@ def assemble(self):
             # Retain other inputs
             Kas[2*self.Kdisp.shape[0]:, 2*beam.sys.num_dof:] = np.eye(uvlm.ss.inputs - 2 * self.Kdisp.shape[0])
 
+            gain_kas = libss.Gain(Kas)
+            gain_kas.output_variables = LinearVector.transform(uvlm.ss.input_variables, to_type=OutputVariable)
+            if beam.sys.Kout is not None:
+                kas_in_vars = LinearVector.transform(beam.sys.Kout.output_variables, to_type=InputVariable)
+            else:
+                kas_in_vars = LinearVector.transform(beam.ss.output_variables, to_type=InputVariable)
+            for variable in uvlm.ss.input_variables:
+                if variable.name not in ['zeta', 'zeta_dot']:
+                    kas_in_vars.append(variable)
+            gain_kas.input_variables = kas_in_vars
+
             # Scaling
             if uvlm.scaled:
                 Kas /= uvlm.sys.ScalingFacts['length']
 
-            uvlm.ss.addGain(Ksa, where='out')
-            uvlm.ss.addGain(Kas, where='in')
+            uvlm.connect_output(gain_ksa)
+            uvlm.connect_input(gain_kas)
 
             # Stiffenning and damping terms within the uvlm
             Dmod = np.zeros_like(uvlm.ss.D)
@@ -227,8 +224,8 @@ def assemble(self):
                     Dmod /= uvlm.sys.ScalingFacts['force']
                 uvlm.ss.D += Dmod
 
-            self.couplings['Ksa'] = Ksa
-            self.couplings['Kas'] = Kas
+            self.couplings['Ksa'] = gain_ksa
+            self.couplings['Kas'] = gain_kas
 
             if self.settings['beam_settings']['modal_projection'] is True and \
                     self.settings['beam_settings']['inout_coords'] == 'modes':
@@ -237,10 +234,29 @@ def assemble(self):
                 in_mode_matrix = np.zeros((uvlm.ss.inputs, beam.ss.outputs + (uvlm.ss.inputs - 2*beam.sys.num_dof)))
                 in_mode_matrix[:2*beam.sys.num_dof, :2*beam.sys.num_modes] = sclalg.block_diag(phi, phi)
                 in_mode_matrix[2*beam.sys.num_dof:, 2*beam.sys.num_modes:] = np.eye(uvlm.ss.inputs - 2*beam.sys.num_dof)
+
+                in_mode_gain = libss.Gain(in_mode_matrix)
+                in_mode_inputs = LinearVector.transform(beam.ss.output_variables, to_type=InputVariable)
+                LinearVector.check_same_vectors(in_mode_inputs, beam.ss.output_variables)
+                for variable in uvlm.ss.input_variables.copy():
+                    if variable.name not in ['eta', 'eta_dot', 'beta_bar', 'beta']:
+                        in_mode_inputs.append(variable)
+
+                in_mode_gain.input_variables = in_mode_inputs
+
+                in_mode_gain.output_variables = LinearVector.transform(uvlm.ss.input_variables, to_type=OutputVariable)
+
                 out_mode_matrix = phi.T
+                out_mode_gain = libss.Gain(out_mode_matrix,
+                                           input_vars=LinearVector.transform(uvlm.ss.output_variables,
+                                                                             to_type=InputVariable),
+                                           output_vars=LinearVector.transform(beam.ss.input_variables,
+                                                                              to_type=OutputVariable))
 
-                uvlm.ss.addGain(in_mode_matrix, where='in')
-                uvlm.ss.addGain(out_mode_matrix, where='out')
+                uvlm.connect_input(in_mode_gain)
+                uvlm.connect_output(out_mode_gain)
+                self.couplings['in_mode_gain'] = in_mode_gain
+                self.couplings['out_mode_gain'] = out_mode_gain
 
             # Reduce uvlm projected onto structural coordinates
             if uvlm.rom:
@@ -254,18 +270,22 @@ def assemble(self):
             uvlm.ss = self.load_uvlm(self.settings['uvlm_filename'])
 
         # Coupling matrices
-        Tas = np.eye(uvlm.ss.inputs, beam.ss.outputs)
-        Tsa = np.eye(beam.ss.inputs, uvlm.ss.outputs)
+        Tas = libss.Gain(np.eye(uvlm.ss.inputs, beam.ss.outputs),
+                         input_vars=LinearVector.transform(beam.ss.output_variables, to_type=InputVariable),
+                         output_vars=LinearVector.transform(uvlm.ss.input_variables, to_type=OutputVariable))
+        Tsa = libss.Gain(np.eye(beam.ss.inputs, uvlm.ss.outputs),
+                         input_vars=LinearVector.transform(uvlm.ss.output_variables, to_type=InputVariable),
+                         output_vars=LinearVector.transform(beam.ss.input_variables, to_type=OutputVariable))
 
         # Scale coupling matrices
         if uvlm.scaled:
-            Tsa *= uvlm.sys.ScalingFacts['force'] * uvlm.sys.ScalingFacts['time'] ** 2
+            Tsa.value *= uvlm.sys.ScalingFacts['force'] * uvlm.sys.ScalingFacts['time'] ** 2
             if rigid_dof > 0:
-                Tas[:flex_nodes + 6, :flex_nodes + 6] /= uvlm.sys.ScalingFacts['length']
-                Tas[total_dof: total_dof + flex_nodes + 6] /= uvlm.sys.ScalingFacts['length']
+                Tas.value[:flex_nodes + 6, :flex_nodes + 6] /= uvlm.sys.ScalingFacts['length']
+                Tas.value[total_dof: total_dof + flex_nodes + 6] /= uvlm.sys.ScalingFacts['length']
             else:
                 if not self.settings['beam_settings']['modal_projection']:
-                    Tas /= uvlm.sys.ScalingFacts['length']
+                    Tas.value /= uvlm.sys.ScalingFacts['length']
 
         ss = libss.couple(ss01=uvlm.ss, ss02=beam.ss, K12=Tas, K21=Tsa)
 
@@ -303,14 +323,19 @@ def update(self, u_infty):
               u_infty (float): New reference velocity
 
         Returns:
-            sharpy.linear.src.libss.ss: Updated aeroelastic state-space system
+            sharpy.linear.src.libss.StateSpace: Updated aeroelastic state-space system
 
         """
         t_ref = self.uvlm.sys.ScalingFacts['length'] / u_infty
 
         self.beam.sys.update_matrices_time_scale(t_ref)
         self.beam.sys.assemble()
-        self.beam.ss = self.beam.sys.SSdisc
+        if self.beam.sys.SSdisc is not None:
+            self.beam.ss = self.beam.sys.SSdisc
+        elif self.beam.sys.SScont is not None:
+            self.beam.ss = self.beam.sys.SScont
+        else:
+            raise AttributeError('Could not find either a continuous or discrete system in Beam')
 
         self.ss = libss.couple(ss01=self.uvlm.ss, ss02=self.beam.ss,
                                K12=self.couplings['Tas'], K21=self.couplings['Tsa'])
@@ -325,7 +350,7 @@ def runrom_rbm(self, uvlm):
         else:
             orient_dof = 4
         # Input side
-        if self.settings['beam_settings']['modal_projection'] == True and \
+        if self.settings['beam_settings']['modal_projection'] is True and \
                 self.settings['beam_settings']['inout_coords'] == 'modes':
             rem_int_modes = np.zeros((ss.inputs, ss.inputs - rig_nodes))
             rem_int_modes[rig_nodes:, :] = np.eye(ss.inputs - rig_nodes)
@@ -342,7 +367,22 @@ def runrom_rbm(self, uvlm):
                 rem_quat_out[j, i] = 1
                 j += 1
 
+            in_vars = ss.input_variables.copy()
+            in_vars.modify('q', size=in_vars.get_variable_from_name('q').size - rig_nodes)
+            remove_integro_inputs = libss.Gain(rem_int_modes,
+                                               input_vars=in_vars,
+                                               output_vars=libss.LinearVector.transform(ss.input_variables,
+                                                                                        to_type=OutputVariable))
+
+            out_vars = ss.output_variables.copy()
+            out_vars.modify('Q', size=out_vars.get_variable_from_name('Q').size-orient_dof)
+            remove_quaternion_out = libss.Gain(rem_quat_out,
+                                               input_vars=libss.LinearVector.transform(ss.output_variables,
+                                                                                       to_type=InputVariable),
+                                               output_vars=out_vars)
+
         else:
+            # TODO: THESE NEED DOING
             rem_int_modes = np.zeros((ss.inputs, ss.inputs - rig_nodes))
             rem_int_modes[:self.beam.sys.num_dof_flex, :self.beam.sys.num_dof_flex] = \
                 np.eye(self.beam.sys.num_dof_flex)
@@ -352,13 +392,15 @@ def runrom_rbm(self, uvlm):
             rem_quat_out = np.zeros((ss.outputs-orient_dof, ss.outputs))
             rem_quat_out[:, :-orient_dof] = np.eye(ss.outputs-orient_dof)
 
-        ss.addGain(rem_int_modes, where='in')
-        ss.addGain(rem_quat_out, where='out')
+        ss.addGain(remove_integro_inputs, where='in')
+        ss.addGain(remove_quaternion_out, where='out')
         for k, rom in uvlm.rom.items():
             uvlm.ss = rom.run(uvlm.ss)
 
-        uvlm.ss.addGain(rem_int_modes.T, where='in')
-        uvlm.ss.addGain(rem_quat_out.T, where='out')
+        add_integro_inputs = remove_integro_inputs.transpose()
+        add_quaternion_outputs = remove_quaternion_out.transpose()
+        uvlm.ss.addGain(add_integro_inputs, where='in')
+        uvlm.ss.addGain(add_quaternion_outputs, where='out')
 
     def get_gebm2uvlm_gains(self, data):
         r"""
@@ -892,17 +934,55 @@ def get_gebm2uvlm_gains(self, data):
         self.Crr = Crr
 
     def to_nodal_coordinates(self):
+        """
+        Transforms the outputs of the system to nodal coordinates if they were previously expressed in modal space
+        """
 
         is_modal = self.beam.sys.modal
 
         if is_modal:
-            phi = self.beam.sys.U
-
-            # these would include control surface deflections, thrust etc.
-            non_structural_inputs = self.uvlm.ss.inputs - self.beam.ss.outputs
-
-            input_gain = sclalg.block_diag(phi.T, phi.T, np.eye(non_structural_inputs), phi.T)
-            output_gain = sclalg.block_diag(phi, phi, phi)
+            beam_kin = self.beam.sys.Kin    # N to Q
+            beam_kout = self.beam.sys.Kout  # q to eta
+
+            aug_in_gain = sclalg.block_diag(self.couplings['in_mode_gain'].value.T, beam_kin.value)
+            input_gain = libss.Gain(aug_in_gain,
+                                    input_vars=LinearVector.merge(
+                                        LinearVector.transform(
+                                            self.couplings['in_mode_gain'].output_variables, to_type=InputVariable),
+                                        beam_kin.input_variables),
+                                    output_vars=LinearVector.merge(
+                                        LinearVector.transform(
+                                            self.couplings['in_mode_gain'].input_variables, to_type=OutputVariable),
+                                        beam_kin.output_variables)
+                                    )
+            try:
+                acceleration_gain = self.beam.sys.acceleration_modal_gain
+            except AttributeError:
+                aug_out_gain = sclalg.block_diag(self.couplings['out_mode_gain'].value.T, beam_kout.value)
+                output_gain = libss.Gain(aug_out_gain,
+                                         input_vars=LinearVector.merge(
+                                             LinearVector.transform(
+                                                 self.couplings['out_mode_gain'].output_variables, to_type=InputVariable),
+                                             beam_kout.input_variables),
+                                         output_vars=LinearVector.merge(
+                                             LinearVector.transform(
+                                                 self.couplings['out_mode_gain'].input_variables, to_type=OutputVariable),
+                                             beam_kout.output_variables)
+                                         )
+            else:
+                aug_out_gain = sclalg.block_diag(self.couplings['out_mode_gain'].value.T, beam_kout.value,
+                                                 acceleration_gain.value)
+                output_gain = libss.Gain(aug_out_gain,
+                                         input_vars=LinearVector.merge(
+                                             LinearVector.transform(
+                                                 self.couplings['out_mode_gain'].output_variables, to_type=InputVariable),
+                                             LinearVector.merge(beam_kout.input_variables, acceleration_gain.input_variables),
+                                         ),
+                                         output_vars=LinearVector.merge(
+                                             LinearVector.transform(
+                                                 self.couplings['out_mode_gain'].input_variables, to_type=OutputVariable),
+                                             LinearVector.merge(beam_kout.output_variables, acceleration_gain.output_variables))
+                                         )
 
             self.ss.addGain(input_gain, where='in')
             self.ss.addGain(output_gain, where='out')
@@ -914,4 +994,5 @@ def load_uvlm(filename):
         read_data = h5.readh5(filename).ss
         # uvlm_ss_read = read_data.linear.linear_system.uvlm.ss
         uvlm_ss_read = read_data
-        return libss.ss(uvlm_ss_read.A, uvlm_ss_read.B, uvlm_ss_read.C, uvlm_ss_read.D, dt=uvlm_ss_read.dt)
+        return libss.StateSpace(uvlm_ss_read.A, uvlm_ss_read.B, uvlm_ss_read.C, uvlm_ss_read.D, dt=uvlm_ss_read.dt)
+
diff --git a/sharpy/linear/assembler/linearbeam.py b/sharpy/linear/assembler/linearbeam.py
index 8d88ea412..e98d8efb6 100644
--- a/sharpy/linear/assembler/linearbeam.py
+++ b/sharpy/linear/assembler/linearbeam.py
@@ -2,6 +2,7 @@
 Linear State Beam Element Class
 
 """
+import h5py
 
 from sharpy.linear.utils.ss_interface import BaseElement, linear_system, LinearVector
 import sharpy.linear.src.lingebm as lingebm
@@ -9,6 +10,8 @@
 import sharpy.utils.settings as settings
 import sharpy.utils.algebra as algebra
 import sharpy.structure.utils.modalutils as modalutils
+from sharpy.linear.utils.ss_interface import VectorVariable, OutputVariable, InputVariable
+import sharpy.linear.src.libss as libss
 
 
 @linear_system
@@ -21,7 +24,18 @@ class LinearBeam(BaseElement):
 
     State-space models can be defined in continuous or discrete time (dt
     required). Modal projection, either on the damped or undamped modal shapes,
-    is also avaiable.
+    is also available.
+
+    The beam state space has information on the states which will depend on whether the system is modal or expressed
+    in physical coordinates.
+
+    If ``modal`` the state variables will be ``q`` and ``q_dot`` representing the modal displacements and the
+    time derivatives.
+
+    If ``nodal`` and free-flying, the state variables will be ``eta`` for the flexible degrees of freedom (displacements
+    and CRVs for each node (dim6)), ``V`` representing the linear velocities at the A frame (dim3), ``W`` representing
+    the angular velocities at the ``A`` frame (dim3), and ``orient`` representing the orientation variable of the
+    ``A`` frame with respect to ``G``
 
     Notes on the settings:
 
@@ -101,7 +115,7 @@ class LinearBeam(BaseElement):
     settings_types['newmark_damp'] = 'float'
     settings_description['newmark_damp'] = 'Newmark damping value. For systems assembled using ``newmark``'
 
-    settings_default['use_euler'] = False
+    settings_default['use_euler'] = True
     settings_types['use_euler'] = 'bool'
     settings_description['use_euler'] = 'Use euler angles for rigid body parametrisation'
 
@@ -115,13 +129,19 @@ class LinearBeam(BaseElement):
 
     settings_types['remove_dofs'] = 'list(str)'
     settings_default['remove_dofs'] = []
-    settings_description['remove_dofs'] = 'Remove desired degrees of freedom'
+    settings_description['remove_dofs'] = 'Remove desired degrees of freedom (flexible DOFs, ' \
+                                          'linear velocities, rotational velocities, orientation)'
     settings_options['remove_dofs'] = ['eta', 'V', 'W', 'orient']
 
     settings_types['remove_sym_modes'] = 'bool'
     settings_default['remove_sym_modes'] = False
     settings_description['remove_sym_modes'] = 'Remove symmetric modes if wing is clamped'
 
+    settings_types['remove_rigid_states'] = 'bool'
+    settings_default['remove_rigid_states'] = False
+    settings_description['remove_rigid_states'] = '(For Stability Derivatives) - Remove RIGID STATES from SS leaving' \
+                                                  ' input/output channels unchanged'
+
     settings_table = settings.SettingsTable()
     __doc__ += settings_table.generate(settings_types, settings_default, settings_description, settings_options)
 
@@ -147,30 +167,36 @@ def initialise(self, data, custom_settings=None):
         settings.to_custom_types(self.settings, self.settings_types, self.settings_default,
                                  self.settings_options, no_ctype=True)
 
-        self.settings['rigid_modes_cg'] = data.settings['Modal']['rigid_modes_cg']  # use the same value as in Modal solver
+        self.settings['rigid_modes_ppal_axes'] = data.settings['Modal']['rigid_modes_ppal_axes']  # use the same value as in Modal solver
         beam = lingebm.FlexDynamic(data.linear.tsstruct0, data.structure, self.settings)
         self.sys = beam
         self.tsstruct0 = data.linear.tsstruct0
 
-        # State variables
+        # State variables - for the purposes of dof removal PRIOR to first order system assembly
         num_dof_flex = self.sys.structure.num_dof.value
         num_dof_rig = self.sys.Mstr.shape[0] - num_dof_flex
-        state_db = {'eta': [0, num_dof_flex],
-                  'V_bar': [num_dof_flex, num_dof_flex + 3],
-                  'W_bar': [num_dof_flex + 3, num_dof_flex + 6],
-                  'orient_bar': [num_dof_flex + 6, num_dof_flex + num_dof_rig],
-                  'dot_eta': [num_dof_flex + num_dof_rig, 2 * num_dof_flex + num_dof_rig],
-                  'V': [2 * num_dof_flex + num_dof_rig, 2 * num_dof_flex + num_dof_rig + 3],
-                  'W': [2 * num_dof_flex + num_dof_rig + 3, 2 * num_dof_flex + num_dof_rig + 6],
-                  'orient': [2 * num_dof_flex + num_dof_rig + 6, 2 * num_dof_flex + 2 * num_dof_rig]}
-        self.state_variables = LinearVector(state_db, self.sys_id)
+
+        # Variables described in class docstring. If modified, remember to change docstring
+        if num_dof_rig == 0:
+            state_variable_list = [
+                VectorVariable('eta', size=num_dof_flex, index=0),
+            ]
+        else:
+            state_variable_list = [
+                VectorVariable('eta', size=num_dof_flex, index=0),  # flexible dofs
+                VectorVariable('V', size=3, index=1),  # translational velocities
+                VectorVariable('W', size=3, index=2),  # angular velocities
+                VectorVariable('orient', size=num_dof_rig - 6, index=3),  # orientation
+            ]
+
+        self.state_variables = LinearVector(state_variable_list)
 
         if num_dof_rig == 0:
             self.clamped = True
 
         self.linearisation_vectors['eta'] = self.tsstruct0.q
         self.linearisation_vectors['eta_dot'] = self.tsstruct0.dqdt
-        self.linearisation_vectors['forces_struct'] = self.tsstruct0.steady_applied_forces.reshape(-1, order='C')
+        self.linearisation_vectors['forces_struct'] = self.tsstruct0.steady_applied_forces.reshape(-1, order='C') # B frame
 
     def assemble(self, t_ref=None):
         """
@@ -210,63 +236,70 @@ def assemble(self, t_ref=None):
         elif self.sys.SScont:
             self.ss = self.sys.SScont
 
+        if self.settings['remove_rigid_states']:
+            # Temporary - should be incorporated into StabilityDerivatives and the coupled system reassembled
+            self.remove_rigid_states()
+
         return self.ss
 
+    def remove_rigid_states(self):
+        if self.sys.clamped:
+            return
+        num_rig_dof = 9
+        if self.sys.modal:
+
+            self.ss.A = self.ss.A[num_rig_dof:, num_rig_dof:]
+            self.ss.B = self.ss.B[num_rig_dof:, :]
+            self.ss.C = self.ss.C[:, num_rig_dof:]
+            self.ss.state_variables.modify('q', size=self.ss.state_variables[0].size - num_rig_dof)
+            self.ss.state_variables.update_locations()
+
+            retain_state = np.zeros((self.ss.states - 9, self.ss.states))
+            retain_state[:self.ss.state_variables[0].size, :self.ss.state_variables[0].size] = \
+                np.eye(self.ss.state_variables[0].size)
+            retain_state[self.ss.state_variables[0].size:, self.ss.state_variables[0].size + 9:] = \
+                np.eye(self.ss.state_variables[0].size)
+            self.ss.A = retain_state.dot(self.ss.A.dot(retain_state.T))
+            self.ss.B = retain_state.dot(self.ss.B)
+            self.ss.C = self.ss.C.dot(retain_state.T)
+            self.ss.state_variables.modify('q_dot', size=self.ss.state_variables[1].size - 9)
+            self.ss.state_variables.update_locations()
+
+        else:
+            retain_state = np.zeros((self.ss.states - 2 * num_rig_dof, self.ss.states))
+            eta_size = self.ss.state_variables[0].size
+            retain_state[:eta_size, :eta_size] = np.eye(eta_size)
+            retain_state[eta_size: 2 * eta_size, eta_size + 9: 2 * eta_size + 9] = np.eye(eta_size)
+
+            self.ss.A = retain_state.dot(self.ss.A.dot(retain_state.T))
+            self.ss.B = retain_state.dot(self.ss.B)
+            self.ss.C = self.ss.C.dot(retain_state.T)
+            self.ss.state_variables.remove('beta_bar')
+            self.ss.state_variables.remove('beta')
+            self.ss.state_variables.update_locations()
+
     def x0(self):
         x = np.concatenate((self.tsstruct0.q, self.tsstruct0.dqdt))
         return x
 
     def trim_nodes(self, trim_list=list):
+        """
+        Removes degrees of freedom from the second order system.
+
+        Args:
+            trim_list (list): List of degrees of freedom to remove ``eta``, ``V``, ``W`` or ``orient``
+
+        """
 
         num_dof_flex = self.sys.structure.num_dof.value
-        num_dof_rig = self.sys.Mstr.shape[0] - num_dof_flex
 
-        # Dictionary containing DOFs and corresponding equations
-        dof_db = {'eta': [0, num_dof_flex, 1],
-                  'V': [num_dof_flex, num_dof_flex + 3, 2],
-                  'W': [num_dof_flex + 3, num_dof_flex + 6, 3],
-                  'orient': [num_dof_flex + 6, num_dof_flex + num_dof_rig, 4],
-                  'yaw': [num_dof_flex + 8, num_dof_flex + num_dof_rig, 1]}
-
-        # -----------------------------------------------------------------------
-        # Better to place in a function available to all elements since it will equally apply
-        # Therefore, the dof_db should be a class attribute
-        # Take away alongside the vector variable class
-
-        # All variables
-        vec_db = dict()
-        for item in dof_db:
-            vector_var = VectorVariable(item, dof_db[item], 'LinearBeam')
-            vec_db[item] = vector_var
-
-        used_vars_db = vec_db.copy()
-
-        # Variables to remove
-        removed_dofs = 0
-        removed_db = dict()
-        for item in trim_list:
-            removed_db[item] = vec_db[item]
-            removed_dofs += vec_db[item].size
-            del used_vars_db[item]
-
-        # Update variables position
-        for rem_item in removed_db:
-            for item in used_vars_db:
-                if used_vars_db[item].rows_loc[0] < removed_db[rem_item].first_pos:
-                    continue
-                else:
-                    # Update order and position
-                    used_vars_db[item].first_pos -= removed_db[rem_item].size
-                    used_vars_db[item].end_pos -= removed_db[rem_item].size
-
-        self.state_variables = used_vars_db
-        # TODO: input and output variables
-        ### ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+        n_dofs = self.state_variables.size
+        self.state_variables.remove(*trim_list)
+        removed_dofs = n_dofs - self.state_variables.size
+        trim_matrix = np.zeros((n_dofs, n_dofs - removed_dofs))
 
-        # Map dofs to equations
-        trim_matrix = np.zeros((num_dof_rig+num_dof_flex, num_dof_flex+num_dof_rig-removed_dofs))
-        for item in used_vars_db:
-            trim_matrix[used_vars_db[item].rows_loc, used_vars_db[item].cols_loc] = 1
+        for variable in self.state_variables:
+            trim_matrix[variable.rows_loc, variable.cols_loc] = 1
 
         # Update matrices
         self.sys.Mstr = trim_matrix.T.dot(self.sys.Mstr.dot(trim_matrix))
@@ -392,15 +425,17 @@ def unpack_ss_vector(self, x_n, u_n, struct_tstep):
         # Missing the forces
         # dqddt = self.sys.Minv.dot(-self.sys.Cstr.dot(dqdt) - self.sys.Kstr.dot(q))
 
-        for i_node in vdof[vdof >= 0]:
-            pos[i_node + 1, :] = q[6*i_node: 6*i_node + 3]
-            pos_dot[i_node + 1, :] = dqdt[6*i_node + 0: 6*i_node + 3]
-
+        # for i_node in vdof[vdof >= 0]:
+        #     pos[i_node + 1, :] = q[6*i_node: 6*i_node + 3]
+        #     pos_dot[i_node + 1, :] = dqdt[6*i_node + 0: 6*i_node + 3]
+        #
         # TODO: CRV of clamped node and double check that the CRV takes this form
-        for i_elem in range(struct_tstep.num_elem):
-            for i_node in range(struct_tstep.num_node_elem):
-                psi[i_elem, i_node, :] = np.linalg.inv(algebra.crv2tan(struct_tstep.psi[i_elem, i_node]).T).dot(q[i_node + 3: i_node + 6])
-                psi_dot[i_elem, i_node, :] = dqdt[i_node + 3: i_node + 6]
+        # for i_elem in range(struct_tstep.num_elem):
+        #     for i_node in range(struct_tstep.num_node_elem):
+        #         psi[i_elem, i_node, :] = np.linalg.inv(algebra.crv2tan(struct_tstep.psi[i_elem, i_node]).T).dot(q[i_node + 3: i_node + 6])
+        #         psi_dot[i_elem, i_node, :] = dqdt[i_node + 3: i_node + 6]
+
+        pos, psi, pos_dot, psi_dot = self.unpack_flex_dof(q, dqdt)
 
         if not clamped:
             for_vel = dqdt[-rig_dof: -rig_dof + 6]
@@ -413,9 +448,18 @@ def unpack_ss_vector(self, x_n, u_n, struct_tstep):
             for_acc = dqddt[-rig_dof:-rig_dof + 6]
 
         if u_n is not None:
+            cba_m = algebra.get_transformation_matrix('ba')
             for i_node in vdof[vdof >= 0]:
-                steady_applied_forces[i_node+1] = u_n[6*i_node: 6*i_node + 6]
-            steady_applied_forces[0] = u_n[-10:-4] - np.sum(steady_applied_forces[1:, :], 0)
+                i_elem = self.sys.structure.node_master_elem[i_node, 0]
+                i_local_node = self.sys.structure.node_master_elem[i_node, 1]
+                cba = cba_m(struct_tstep.psi[i_elem, i_local_node])
+                steady_applied_forces[i_node+1, :3] = cba.dot(u_n[6*i_node: 6*i_node + 3])
+                steady_applied_forces[i_node+1, 3:] = cba.dot(u_n[6*i_node + 3: 6*i_node + 6])
+            i_elem = self.sys.structure.node_master_elem[0, 0]
+            i_local_node = self.sys.structure.node_master_elem[0, 1]
+            cba = cba_m(struct_tstep.psi[i_elem, i_local_node])
+            steady_applied_forces[0, :3] = cba.dot(u_n[-10:-7]) - np.sum(steady_applied_forces[1:, :3], 0)
+            steady_applied_forces[0, 3:] = cba.dot(u_n[-7:-4]) - np.sum(steady_applied_forces[1:, 3:], 0)
 
         # gravity forces - careful - debug
         C_grav = np.zeros((q.shape[0], q.shape[0]))
@@ -455,28 +499,106 @@ def unpack_ss_vector(self, x_n, u_n, struct_tstep):
 
         return current_time_step
 
+    def unpack_flex_dof(self, eta, eta_dot=None):
+        """
+        Unpacks a vector of structural displacements and velocities into a SHARPy familiar
+        form of pos, psi and their time derivatives
 
-class VectorVariable(object):
+        Args:
+            eta (np.array): Vector of structural displacements
+            eta_dot (np.array (Optional): Vector of structural velocities
 
-    def __init__(self, name, pos_list, var_system):
+        Returns:
+            tuple: Containing ``pos``, ``psi``, ``pos_dot``, ``psi_dot`` if ``eta_dot`` is provided, else
+              only the displacements are returned
+        """
+        vdof = self.sys.structure.vdof
+        if np.max(np.abs(eta.imag)) > 0:
+            dtype=complex
+        else:
+            dtype=float
+        pos = np.zeros_like(self.tsstruct0.pos, dtype=dtype)
+        psi = np.zeros_like(self.tsstruct0.psi, dtype=dtype)
+        pos_dot = np.zeros_like(self.tsstruct0.pos_dot, dtype=dtype)
+        psi_dot = np.zeros_like(self.tsstruct0.psi_dot, dtype=dtype)
 
-        self.name = name
-        self.var_system = var_system
+        return_vels = True
+        if eta_dot is None:
+            return_vels = False
+            eta_dot = np.zeros_like(eta)
 
-        self.first_pos = pos_list[0]
-        self.end_pos = pos_list[1]
-        self.rows_loc = np.arange(self.first_pos, self.end_pos, dtype=int) # Original location, should not update
+        for i_node in vdof[vdof >= 0]:
+            pos[i_node + 1, :] = eta[6*i_node: 6*i_node + 3]
+            pos_dot[i_node + 1, :] = eta_dot[6*i_node + 0: 6*i_node + 3]
 
+        # TODO: CRV of clamped node and double check that the CRV takes this form
+        for i_elem in range(self.tsstruct0.num_elem):
+            for i_node in range(self.tsstruct0.num_node_elem):
+                psi[i_elem, i_node, :] = np.linalg.inv(
+                    algebra.crv2tan(self.tsstruct0.psi[i_elem, i_node]).T).dot(eta[i_node + 3: i_node + 6])
+                psi_dot[i_elem, i_node, :] = eta_dot[i_node + 3: i_node + 6]
+
+        if return_vels:
+            return pos, psi, pos_dot, psi_dot
+        else:
+            return pos, psi
 
-    # add methods to reorganise into SHARPy method?
+    def recover_accelerations(self, full_ss):
+        """
+        For a system with displacement and velocity outputs (``full_ss``), recover the accelerations and append them
+        as new output channels.
 
-    @property
-    def cols_loc(self):
-        return np.arange(self.first_pos, self.end_pos, dtype=int)
+        This function produces an output gain that should then be connected in series to the desired system
 
-    @property
-    def size(self):
-        return self.end_pos - self.first_pos
+        Args:
+            full_ss (libss.StateSpace): State space for which to provide output gain to recover accelerations
 
-if __name__=='__main__':
-    print(LinearBeam.__doc__)
+        Returns:
+            libss.Gain: Gain adding the accelerations as new output channels
+        """
+        n_in = full_ss.outputs
+        n_out = full_ss.outputs + self.ss.states // 2
+
+        acc_gain = np.zeros((n_out, n_in))
+
+        input_variables = LinearVector.transform(full_ss.output_variables, to_type=InputVariable)
+
+        output_variables = full_ss.output_variables.copy()
+        acceleration_variables = []
+        for var in self.ss.output_variables[self.ss.output_variables.num_variables//2:]:
+            new_var = var.copy()
+            new_var.name += '_dot'
+            output_variables.append(new_var)
+            acceleration_variables.append(new_var)
+
+        acc_gain[:n_in, :n_in] = np.eye(n_in)
+        acc_gain[-self.ss.states//2:, :self.ss.inputs] = self.ss.B[self.ss.states//2:]
+        acc_gain[-self.ss.states//2:, self.ss.inputs:] = self.ss.A[self.ss.states//2:, :]
+
+        acceleration_recovery = libss.Gain(acc_gain,
+                                           input_vars=input_variables,
+                                           output_vars=output_variables)
+
+        if self.sys.modal:
+            output_variables = []
+            for var in self.sys.Kout.output_variables[self.sys.Kout.output_variables.num_variables//2:]:
+                new_var = var.copy()
+                new_var.name += '_dot'
+                output_variables.append(new_var)
+            modal_gain = libss.Gain(self.sys.U,
+                                    input_vars=LinearVector.transform(LinearVector(acceleration_variables),
+                                                                      to_type=InputVariable),
+                                    output_vars=LinearVector(output_variables))
+            self.sys.acceleration_modal_gain = modal_gain
+
+        return acceleration_recovery
+
+    def save_reduced_order_bases(self, file_name):
+        gain = libss.Gain(self.sys.U)
+        gain.save(file_name)
+
+    def save_structural_matrices(self, file_name):
+        with h5py.File(file_name, 'w') as f:
+            f.create_dataset('m', data=self.sys.Mstr)
+            f.create_dataset('c', data=self.sys.Cstr)
+            f.create_dataset('k', data=self.sys.Kstr)
diff --git a/sharpy/linear/assembler/lineargustassembler.py b/sharpy/linear/assembler/lineargustassembler.py
index cfb0f34f2..3a02b4838 100644
--- a/sharpy/linear/assembler/lineargustassembler.py
+++ b/sharpy/linear/assembler/lineargustassembler.py
@@ -1,58 +1,554 @@
 import sharpy.linear.utils.ss_interface as ss_interface
 import numpy as np
-import sharpy.utils.algebra as algebra
-import scipy.sparse as scsp
 import sharpy.linear.src.libss as libss
+import scipy.signal as scsig
+import sharpy.utils.settings as settings
+import sharpy.utils.cout_utils as cout
+from abc import ABCMeta, abstractmethod
 
-@ss_interface.linear_system
-class LinearGustGenerator(object):
-    """Reduces the entire gust field input to a user-defined set of more comprehensive inputs
+dict_of_linear_gusts = {}
+
+
+def linear_gust(arg):
+    global dict_of_linear_gusts
+    try:
+        arg.gust_id
+    except AttributeError:
+        raise AttributeError('Class defined as gust has no gust_id attribute')
+    dict_of_linear_gusts[arg.gust_id] = arg
+    return arg
+
+
+def gust_from_string(gust_name):
     """
-    sys_id = 'LinearGustGenerator'
+    Returns an instance of the ``gust_name`` class
+
+    Args:
+        gust_name (str): Name of gust
+
+    Returns:
+        LinearGust: instance of linear gust
+    """
+    return dict_of_linear_gusts[gust_name]()
+
+
+class LinearGust(metaclass=ABCMeta):
+    # Base class from which to develop the desired gusts
+    settings_types = {}
+    settings_default = {}
+    settings_description = {}
+
+    print_info = True  # for debugging
 
     def __init__(self):
-        self.aero = None
-        self.ss_gust = None
-        self.Kout = None
+        self.aero = None  #: aerogrid
+        self.tsaero0 = None  # timestep info at the linearisation point
+
+        self.dt = None  # type: float
+        self.remove_predictor = None  # type: bool
+        self.Kzeta = None  # type: int  # total number of lattice vertices
+        self.KKzeta = None  # type: list  # list of number of lattice vertices in each surface
+
+        self.state_to_uext = None  #type: np.array # Gust system C matrix (for unpacking into timestep_info)
+        self.uin_to_uext = None  #type: np.array # Gust system D matrix (for unpacking into timestep_info)
+        self.gust_ss = None  #type: libss.StateSpace # containing the gust state-space system
 
-    def initialise(self, aero):
+        self.settings = None
+
+        self.u_ext_direction = None  # np.ndarray Unit external velocity vector in G frame
+        self.u_inf = None  # float Free stream velocity magnitude
+
+    def initialise(self, aero, linuvlm, tsaero0, u_ext, custom_settings=None):
+        """
+        Initialise gust class
+
+        Args:
+            aero (sharpy.aero.models.Aerogrid):
+            linuvlm (sharpy.linear.src.linuvlm.Dynamic) :
+            tsaero0 (sharpy.utils.datstructures.AeroTimestepInfo) : time step at reference state
+            u_ext (np.ndarray): free-stream velocity
+            custom_settings (dict):
+        """
         self.aero = aero
+        self.tsaero0 = tsaero0
+        self.dt = linuvlm.dt
+        self.remove_predictor = linuvlm.remove_predictor
+        self.KKzeta = linuvlm.MS.KKzeta
+        self.Kzeta = sum(self.KKzeta)
+
+        if custom_settings is not None:
+            self.settings = custom_settings
+            settings.to_custom_types(self.settings, self.settings_types, self.settings_default)
+
+        # find free stream velocity
+        self.u_inf = np.linalg.norm(u_ext)
+        self.u_ext_direction = u_ext / self.u_inf
+
+    def get_x_max(self):
+        """
+        Returns the maximum and minimum and minimum coordinates of the UVLM lattice projected onto the free-stream
+        velocity vector
+
+        Returns:
+            tuple: minimum and maximum coordinates of lattice projected onto velocity vector
+        """
+        max_chord_surf = []
+        min_chord_surf = []
+        for zeta in self.tsaero0.zeta:
+            zeta_proj = np.zeros_like(zeta)
+            _, m, n = zeta.shape
+            for i_m in range(m):
+                for i_n in range(n):
+                    zeta_proj[:, i_m, i_n] = zeta[:, i_m, i_n].dot(self.u_ext_direction) * self.u_ext_direction
+            max_chord_surf.append(np.max(zeta_proj))
+            min_chord_surf.append(np.min(zeta_proj))
+        return min(min_chord_surf), max(max_chord_surf)
+
+    @abstractmethod
+    def assemble(self):
+        """
+        Assemble gust system according to specific gust requirements in inherited classes
+
+        Returns:
+            libss.StateSpace
+        """
+        return libss.StateSpace(0, 0, 0, 0)  # placeholder for state space
+
+    def apply(self, ssuvlm):
+        r"""
+        Couples in series the gust system assembled by the LinearGust with the Linear UVLM.
+
+        It generates an augmented gust system which feeds through the other UVLM inputs
+        (grid displacements and velocities). The resulting augmented gust state space takes the form
+        of:
+
+        .. math::
+            \boldsymbol{B}_{aug} = \begin{pmatrix} \boldsymbol{0}_{K_\zeta} & \boldsymbol{0}_{K_\zeta}
+            & \boldsymbol{B}_{gust} \end{pmatrix}
+
+        .. math::
+            \boldsymbol{C}_{aug} = \begin{pmatrix} \boldsymbol{0}_{K_\zeta} \\ \boldsymbol{0}_{K_\zeta} \\
+            \boldsymbol{C}_{gust} \end{pmatrix}
+
+        .. math::
+            \boldsymbol{D}_{aug} = \begin{pmatrix} \boldsymbol{I}_{K_\zeta} \\ \ &  \boldsymbol{I}_{K_\zeta} \\
+            \boldsymbol{0}_{K_\zeta} \end{pmatrix}
+
+        where :math:`K_\zeta` is 3 times the number of vertices in the UVLM lattice and the size of the input
+        sets displacements and velocities, as well as external velocity inputs.
+
+        Therefore, the inputs to the resulting coupled system become
+
+        .. math:: \boldsymbol{u} = \begin{bmatrix} \boldsymbol{\zeta} & \boldsymbol{\dot{\zeta}} & \boldsymbol{u}_{gust}
+
+        where the size of :math:`\boldsymbol{u}_{gust} will depend on the chosen gust assembly scheme.
+
+        Args:
+            ssuvlm (libss.StateSpace): State space object of the linear UVLM.
+
+        Returns:
+            libss.StateSpace: Coupled gust system with Linear UVLM.
+        """
+        ssgust = self.assemble()  # libss.StateSpace()
+        #
+        # Feed through UVLM inputs
+        b_aug = np.zeros((ssgust.states, ssuvlm.inputs - ssgust.outputs + ssgust.inputs))
+        c_aug = np.zeros((ssuvlm.inputs, ssgust.states))
+        d_aug = np.zeros((ssuvlm.inputs, b_aug.shape[1]))
+        b_aug[:, -ssgust.inputs:] = ssgust.B
+        c_aug[-ssgust.outputs:, :] = ssgust.C
+        d_aug[:-ssgust.outputs, :-ssgust.inputs] = np.eye(ssuvlm.inputs - ssgust.outputs)
+
+        self.gust_ss = libss.StateSpace(ssgust.A, b_aug, c_aug, d_aug, dt=ssgust.dt)
+        input_variables = ssuvlm.input_variables.copy()
+        input_variables.remove('u_gust')
+        self.gust_ss.input_variables = \
+            ss_interface.LinearVector.merge(input_variables, ssgust.input_variables)
+        self.gust_ss.state_variables = ssgust.state_variables.copy()
+        self.gust_ss.output_variables = ss_interface.LinearVector.transform(ssuvlm.input_variables,
+                                                                            to_type=ss_interface.OutputVariable)
+        ss = libss.series(self.gust_ss, ssuvlm)
+
+        return ss
+
+    def assemble_gust_statespace(self, a_i, b_i, c_i, d_i):
+        """
+        Assembles the individual gust system in discrete time and removes the predictor term if that is specified
+        in the linear UVLM settings.
+
+        Args:
+            a_i (np.array): Gust A matrix
+            b_i (np.array): Gust B matrix
+            c_i (np.array): Gust C matrix
+            d_i (np.array): Gust D matrix
+
+        Returns:
+            libss.StateSpace: StateSpace object of the individual gust system
+        """
+        if self.remove_predictor:
+            a_mod, b_mod, c_mod, d_mod = libss.SSconv(a_i, None, b_i, c_i, d_i)
+            gustss = libss.StateSpace(a_mod, b_mod, c_mod, d_mod, dt=self.dt)
+            self.state_to_uext = c_mod
+            self.uin_to_uext = d_mod
+        else:
+            gustss = libss.StateSpace(a_i, b_i, c_i, d_i,
+                                      dt=self.dt)
+            self.state_to_uext = c_i
+            self.uin_to_uext = d_i
+        gustss.input_variables = ss_interface.LinearVector(
+            [ss_interface.InputVariable('u_gust', size=1, index=0)])
+        gustss.state_variables = ss_interface.LinearVector(
+            [ss_interface.StateVariable('gust', size=gustss.states, index=0)])
+        gustss.output_variables = ss_interface.LinearVector(
+            [ss_interface.OutputVariable('u_gust', size=gustss.outputs, index=0)])
+
+        return gustss
+
+    def discretise_domain(self):
+        """
+        Generates a "gust-station" domain, aligned with the free-stream velocity equispaced in
+        :math:`\Delta t U_\infty` increments.
+
+        Returns:
+            tuple: domain and number of elements in the domain
+        """
+        x_min, x_max = self.get_x_max()  # G frame, min and max of panels x coordinates
+
+        N = int(np.ceil((x_max - x_min) / self.dt / self.u_inf))
+        x_domain = np.linspace(x_min, x_max, N)
+
+        return x_domain, N
+
+
+@linear_gust
+class LeadingEdge(LinearGust):
+    """
+    Reduces the gust input to a single input for the vertical component of the gust at the leading edge.
+
+    This is vertical velocity is then convected downstream with the free stream velocity. The gust is uniform in span.
+
+    """
+    gust_id = 'LeadingEdge'
+
+    def assemble(self):
+        """
+        Assembles the gust state space system, creating the (A, B, C and D) matrices that convect the single gust input
+        at the leading edge downstream and uniformly across the span
+        """
+        Kzeta = self.Kzeta  # total number of lattice vertices
+
+        # Convection system: needed for as many inputs (since it carries their time histories)
+        x_domain, N = self.discretise_domain()
+
+        # State Equation
+        # for each input...
+        a_i = np.zeros((N, N))
+        a_i[1:, :-1] = np.eye(N-1)
+        b_i = np.zeros((N, 1))
+        b_i[0, 0] = 1
+
+        c_i = np.zeros((3 * Kzeta, N))
+        for i_surf in range(self.aero.n_surf):
+
+            M_surf, N_surf = self.aero.aero_dimensions[i_surf]
+            Kzeta_start = 3 * sum(self.KKzeta[:i_surf])  # number of coordinates up to current surface
+            shape_zeta = (3, M_surf + 1, N_surf + 1)
+
+            if self.print_info:
+                cout.cout_wrap(f'Aero surface {i_surf}')
+                cout.cout_wrap(f'Gust monitoring station domain:\n{x_domain}', 1)
+
+            for i_node_span in range(N_surf + 1):
+                for i_node_chord in range(M_surf + 1):
+                    i_vertex = [Kzeta_start + np.ravel_multi_index((i_axis, i_node_chord, i_node_span),
+                                                                   shape_zeta) for i_axis in range(3)]
+                    zeta = self.tsaero0.zeta[i_surf][:, i_node_chord, i_node_span]
+                    x_vertex = zeta.dot(self.u_ext_direction)
+                    interpolation_weights, column_indices = linear_interpolation_weights(x_vertex, x_domain)
+                    c_i[i_vertex, column_indices[0]] = np.array([0, 0, interpolation_weights[0]])
+                    c_i[i_vertex, column_indices[1]] = np.array([0, 0, interpolation_weights[1]])
+
+                    if self.print_info:
+                        cout.cout_wrap(f'Vertex info: i_n = {i_node_span}\ti_m = {i_node_chord}')
+                        cout.cout_wrap(f'\tCoordinate: {zeta}', 1)
+                        cout.cout_wrap(f'\tProjected coordinate: {x_vertex}', 1)
+                        cout.cout_wrap(f'\tInterpolation weights: {interpolation_weights}', 2)
+                        cout.cout_wrap(f'\tC matrix column indices: {column_indices}', 2)
+                        cout.cout_wrap(f'\ti_vertex: {i_vertex}', 2)
+
+
+
+        d_i = np.zeros((c_i.shape[0], b_i.shape[1]))
 
-    def generate(self, linuvlm, aero):
+        gustss = self.assemble_gust_statespace(a_i, b_i, c_i, d_i)
 
-        if aero is None:
-            aero = self.aero
+        return gustss
 
-        Kzeta = linuvlm.Kzeta
-        M = linuvlm.MS.MM[0]
 
-        # Create state-space to convect gust downstream
-        A_gust = np.zeros((M+1, M+1))
-        A_gust[1:, :-1] = np.eye(M, M)
+@linear_gust
+class MultiLeadingEdge(LinearGust):
+    """
+    Gust input channels defined at user-defined spanwise locations. Linearly interpolated in between
+    the spanwise input positions.
+
+    """
+
+    gust_id = 'MultiLeadingEdge'
+
+    settings_types = {}
+    settings_default = {}
+    settings_description = {}
+
+    settings_types['span_location'] = 'list(float)'
+    settings_default['span_location'] = [-10., 10.]
+    settings_description['span_location'] = 'Spanwise location of the input streams of the gust'
+
+    settings_table = settings.SettingsTable()
+    __doc__ += settings_table.generate(settings_types, settings_default, settings_description)
+
+    def __init__(self):
+        super().__init__()
+
+        self.span_loc = None
+        self.n_gust = None
+
+    def initialise(self, aero, linuvlm, tsaero0, u_ext, custom_settings=None):
+        super().initialise(aero, linuvlm, tsaero0, u_ext, custom_settings)
+
+        self.span_loc = self.settings['span_location']
+        self.n_gust = len(self.span_loc)
 
-        B_gust = np.zeros((M+1, 6 * Kzeta + 1))
-        B_gust[0, 6 * Kzeta] = 1
+        assert self.n_gust > 1, 'Use LeadingEdge gust for single inputs'
 
-        C_gust = np.zeros((9 * Kzeta, M+1))
+    def assemble(self):
 
-        D_gust = np.zeros((9*Kzeta, 6*Kzeta + 1))
+        n_gust = self.n_gust
+        Kzeta = self.Kzeta
+        span_loc = self.span_loc
 
-        Kout = np.zeros((3 * Kzeta, M+1))
+        # Convection system: needed for as many inputs (since it carries their time histories)
+        x_domain, N = self.discretise_domain()
 
-        for i_surf in range(aero.n_surf):
+        # State Equation
+        # for each input...
+        gust_a = np.zeros((N * n_gust, N * n_gust))
+        gust_b = np.zeros((N * n_gust, n_gust))
+        for ith_gust in range(n_gust):
+            gust_a[ith_gust * N + 1: ith_gust * N + N, ith_gust * N: ith_gust * N + N - 1] = np.eye(N-1)
+            gust_b[ith_gust * N, ith_gust] = 1
 
-            M_surf, N_surf = aero.aero_dimensions[i_surf]
-            Kzeta_start = 3 * sum(linuvlm.MS.KKzeta[:i_surf])  # number of coordinates up to current surface
+        gust_c = np.zeros((3 * Kzeta, n_gust * N))
+        gust_d = np.zeros((gust_c.shape[0], gust_b.shape[1]))
+        for i_surf in range(self.aero.n_surf):
+
+            M_surf, N_surf = self.aero.aero_dimensions[i_surf]
+            Kzeta_start = 3 * sum(self.KKzeta[:i_surf])  # number of coordinates up to current surface
             shape_zeta = (3, M_surf + 1, N_surf + 1)
 
             for i_node_span in range(N_surf + 1):
                 for i_node_chord in range(M_surf + 1):
                     i_vertex = [Kzeta_start + np.ravel_multi_index((i_axis, i_node_chord, i_node_span),
                                                                    shape_zeta) for i_axis in range(3)]
-                    Kout[i_vertex, i_node_chord] = np.array([0, 0, 1])
+                    x_vertex = self.tsaero0.zeta[i_surf][0, i_node_chord, i_node_span]
+                    y_vertex = self.tsaero0.zeta[i_surf][1, i_node_chord, i_node_span]
+                    interpolation_weights, column_indices = spanwise_interpolation(y_vertex, span_loc, x_vertex, x_domain)
+                    for i in range(len(column_indices)):
+                        gust_c[i_vertex, column_indices[i]] = np.array([0, 0, interpolation_weights[i]])
+
+        gustss = self.assemble_gust_statespace(gust_a, gust_b, gust_c, gust_d)
+        return gustss
+
+
+def get_freestream_velocity(data):
+    try:
+        u_inf = data.settings['StaticUvlm']['aero_solver_settings']['u_inf']
+        u_inf_direction = data.settings['StaticCoupled']['aero_solver_settings']['u_inf_direction']
+    except KeyError:
+        try:
+            u_inf = data.settings['StaticCoupled']['aero_solver_settings']['velocity_field_input']['u_inf']
+            u_inf_direction = data.settings['StaticCoupled']['aero_solver_settings']['velocity_field_input']['u_inf_direction']
+        except KeyError:
+            cout.cout_wrap('Unable to find free stream velocity settings in StaticUvlm or StaticCoupled,'
+                           'please ensure these settings are provided in the config .sharpy file. If'
+                           'you are running a restart simulation make sure they are included too, regardless'
+                           'of these solvers being present in the SHARPy flow', 4)
+            raise KeyError
+
+    try:
+        v0 = u_inf * u_inf_direction
+    except TypeError:
+        # For restart solutions, where the settings may have not been processed and thus may
+        # exist but in string format
+        try:
+            u_inf_direction = np.array(u_inf_direction, dtype=float)
+        except ValueError:
+            if u_inf_direction.find(',') < 0:
+                u_inf_direction = np.fromstring(u_inf_direction.strip('[]'), sep=' ', dtype=float)
+            else:
+                u_inf_direction = np.fromstring(u_inf_direction.strip('[]'), sep=',', dtype=float)
+        finally:
+            v0 = np.array(u_inf_direction, dtype=float) * float(u_inf)
+
+    return v0
+
+
+def linear_interpolation_weights(x_vertex, x_domain):
+
+    column_ind_left = np.argwhere(x_domain >= x_vertex)[0][0] - 1
+    if column_ind_left == - 1:
+        column_ind_left = 0
+    column_indices = (column_ind_left, column_ind_left + 1)
+    interpolation_weights = np.array([x_domain[column_ind_left + 1] - x_vertex, x_vertex - x_domain[column_ind_left]])
+    interpolation_weights /= (x_domain[column_ind_left + 1] - x_domain[column_ind_left])
+    return interpolation_weights, column_indices
+
+
+def chordwise_interpolation(x_vertex, x_domain):
+
+    column_ind_left = np.argwhere(x_domain >= x_vertex)[0][0] - 1
+    if column_ind_left == - 1:
+        column_ind_left = 0
+    column_indices = (column_ind_left, column_ind_left + 1)
+    interpolation_weights = np.array([x_domain[column_ind_left + 1] - x_vertex, x_vertex - x_domain[column_ind_left]])
+    interpolation_weights /= (x_domain[column_ind_left + 1] - x_domain[column_ind_left])
+    return interpolation_weights, column_indices
+
+
+def spanwise_interpolation(y_vertex, span_loc, x_vertex, x_domain):
+    r"""
+    Returns the interpolation weights and indices of said weights for a span wise interpolation. The indices refer to
+    column indices of a row vector containing the gust disturbance at each chordwise control point.
+
+    This is used in non-span uniform gusts with multiple inputs, where each set of chordwise
+    control points in the state vector corresponds to one gust (i.e. its time history).
+
+    The interpolation is performed as follows.
+
+    For an arbitrary point within a grid delimited by ``(0, 0)``, ``(0, 1)``, ``(1, 0)`` and ``(1, 1)``, the velocity
+    at a given point is:
+
+    .. math::
+
+        u_z = \alpha_1 u_1 + \alpha_0 u_0
+
+    where :math:`\alpha_1 = \frac{y - y_0}{y_{1} - y_{0}}` and :math:`\alpha_0 = \frac{y_1-y}{y_1-y_0}`.
+
+    The velocities :math:`u_1` and :math:`u_0` are at the current chordwise point, thus translating into
+
+    .. math::
+
+        u_1 = \beta_{11} u_{11} + \beta_{10} u_{10} \\
+        u_0 = \beta_{01} u_{01} + \beta_{00} u_{00}
+
+    where :math:`\beta_{11} = \frac{x - x_0}{x_1 - x_0}` and the rest follow a similar pattern.
+
+    The resulting interpolation scheme gives
+
+    .. math::
+
+        u_z = \begin{pmatrix}
+        \alpha_1 \beta_{11} u_{11} &  \alpha_1 \beta{10} &  \alpha_0 \beta_{01} & \alpha_0 \beta_{00}
+        \end{pmatrix}
+        \begin{bmatrix} u_{11} \\ u_{10} \\ u_{01} \\ u_{00} \end{bmatrix}.
+
+    The indices returned as part of the output correspond to the column indices above to match with the
+    corresponding interpolation control points.
+
+    Args:
+        y_vertex (np.float): y (span) coordinate of point
+        span_loc (np.array): Domain of y-coordinates
+        x_vertex (np.float): x (chord) coordinate of point
+        x_domain (np.array): Domain of x coordinates
+
+    Returns:
+        tuple: 2-tuple containing i) the 4-array of interpolation weights and ii) the 4-array of column
+          indicies to place such weights.
+    """
+    N = len(x_domain)
+    span_weights, span_indices = linear_interpolation_weights(y_vertex, span_loc)
+    interpolation_weights = np.zeros(4)
+    interpolation_columns = []
+
+    chord_weights, chord_ind = linear_interpolation_weights(x_vertex, x_domain)
+
+    for i_span, span_location in enumerate(span_indices):
+        interpolation_weights[2 * i_span - 1] = span_weights[i_span] * chord_weights[0]
+        interpolation_columns.append(span_location * N + chord_ind[0])
+        interpolation_weights[2 * i_span] = span_weights[i_span] * chord_weights[1]
+        interpolation_columns.append(span_location * N + chord_ind[1])
+
+    return interpolation_weights, interpolation_columns
+
+
+def campbell(sigma_w, length_scale, velocity, dt=None):
+    """
+    Campbell approximation to the Von Karman turbulence filter.
+
+    Args:
+        sigma_w (float): Turbulence intensity in feet/s
+        length_scale (float): Turbulence length scale in feet
+        velocity (float): Flight velocity in feet/s
+        dt (float (optional)): Discrete-time time step for discrete time systems
+
+    Returns:
+        libss.StateSpace: SHARPy State space representation of the Campbell approximation to the Von Karman
+          filter
+
+    References:
+        Palacios, R. and Cesnik, C.. Dynamics of Flexible Aircraft: Coupled flight dynamics, aeroelasticity and control.
+        pg 28.
+    """
+    a = 1.339
+    time_scale = a * length_scale / velocity
+    num_tf = np.array([91/12, 52, 60]) * sigma_w * np.sqrt(time_scale / a / np.pi)
+    den_tf = np.array([935/216, 561/12, 102, 60])
+    if dt is not None:
+        num_tf, den_tf, dt = scsig.cont2discrete((num_tf, den_tf), dt, method='bilinear')
+        camp_tf = scsig.ltisys.TransferFunctionDiscrete(num_tf, den_tf, dt=dt)
+    else:
+        camp_tf = scsig.ltisys.TransferFunction(num_tf, den_tf)
+    ss_turb = libss.StateSpace.from_scipy(camp_tf.to_ss())
+
+    ss_turb.initialise_variables(({'name': 'noise_in', 'size': 1}), var_type='in')
+    ss_turb.initialise_variables(({'name': 'u_gust', 'size': 1}), var_type='out')
+    ss_turb.initialise_variables(({'name': 'campbell_state', 'size': ss_turb.states}), var_type='state')
+
+    return ss_turb
+
+
+if __name__ == '__main__':
+    pass
+    # sys = campbell(90, 1750, 30, dt=0.1)
+
+    import unittest
+
+    class TestInterpolation(unittest.TestCase):
+
+        def test_interpolation(self):
+
+            x_domain = np.linspace(0, 1, 4)
+            span_loc = np.linspace(0, 1, 2)
+
+            # mesh coordinates
+            x_grid = np.linspace(0.25, 0.75, 3)
+            y_grid = np.linspace(0, 1, 3)
+            x_mesh, y_mesh = np.meshgrid(x_grid, y_grid)
+
+            print(x_mesh)
+            print(y_mesh)
+            print(y_mesh.shape)
+            for i in range(len(x_grid)):
+                for j in range(len(y_grid)):
+                    print(i)
+                    print(j)
+                    print('Vertex x({:g}) = {:.2f}, y({:g}) = {:.2f}'.format(j, x_mesh[j, i],
+                                                                             i, y_mesh[j, i]))
+                    weights, columns = spanwise_interpolation(y_mesh[j, i], span_loc,
+                                                                              x_mesh[j, i], x_domain)
+
+                    print('Weights', weights)
+                    print('Columns', columns)
+                    print('\n')
 
-        C_gust[-3 * Kzeta:] = Kout
-        D_gust[:6 * Kzeta, :6 * Kzeta] = np.eye(6 * Kzeta)
 
-        self.Kout = Kout
-        return A_gust, B_gust, C_gust, D_gust
+    unittest.main()
\ No newline at end of file
diff --git a/sharpy/linear/assembler/linearuvlm.py b/sharpy/linear/assembler/linearuvlm.py
index 8b6f32f5f..b9b20e277 100644
--- a/sharpy/linear/assembler/linearuvlm.py
+++ b/sharpy/linear/assembler/linearuvlm.py
@@ -11,6 +11,7 @@
 import sharpy.utils.rom_interface as rom_interface
 import sharpy.linear.src.libss as libss
 from sharpy.utils.constants import vortex_radius_def
+from sharpy.linear.utils.ss_interface import VectorVariable, LinearVector, StateVariable
 
 
 @ss_interface.linear_system
@@ -66,6 +67,12 @@ class LinearUVLM(ss_interface.BaseElement):
     .. math:: \dot{\mathbf{\Gamma}}^{n+1} = \frac{3\mathbf{\Gamma}^{n+1}-4\mathbf{\Gamma}^n + \mathbf{\Gamma}^{n-1}}
         {2\Delta t}
 
+    Note:
+        Control surface deflections are implemented using :class:`~sharpy.linear.assembler.lincontrolsurfacedeflector.LinControlSurfaceDeflector`
+        and the sign convention differs from the nonlinear solver. In the linear solver, the control
+        surface deflects according to the local :math:`x_B` vector. See the control surface deflection class for more
+        details.
+
     References:
         [1] Franklin, GF and Powell, JD. Digital Control of Dynamic Systems, Addison-Wesley Publishing Company, 1980
 
@@ -113,7 +120,11 @@ class LinearUVLM(ss_interface.BaseElement):
     settings_types['gust_assembler'] = 'str'
     settings_default['gust_assembler'] = ''
     settings_description['gust_assembler'] = 'Selected linear gust assembler.'
-    settings_options['gust_assembler'] = ['leading_edge']
+    settings_options['gust_assembler'] = ['LeadingEdge', 'MultiLeadingEdge']
+
+    settings_types['gust_assembler_inputs'] = 'dict'
+    settings_default['gust_assembler_inputs'] = dict()
+    settings_description['gust_assembler_inputs'] = 'Selected linear gust assembler parameter inputs.'
 
     settings_types['rom_method'] = 'list(str)'
     settings_default['rom_method'] = []
@@ -132,6 +143,11 @@ class LinearUVLM(ss_interface.BaseElement):
     settings_default['cfl1'] = True
     settings_description['cfl1'] = 'If it is ``True``, it assumes that the discretisation complies with CFL=1'
 
+    settings_types['convert_to_ct'] = 'bool'
+    settings_default['convert_to_ct'] = False
+    settings_description['convert_to_ct'] = 'Convert system to Continuous Time. Note: features above the original ' \
+                                            'Nyquist frequency limit will not be captured.'
+
     settings_table = settings.SettingsTable()
     __doc__ += settings_table.generate(settings_types, settings_default, settings_description, settings_options)
 
@@ -160,7 +176,7 @@ def __init__(self):
         self.sys = None
         self.ss = None
         self.tsaero0 = None
-        self.rom = None
+        self.rom = None  # dict: rom_name: rom_class dictionary
 
         self.settings = dict()
         self.state_variables = None
@@ -175,13 +191,16 @@ def __init__(self):
 
         self.linearisation_vectors = dict()  # reference conditions at the linearisation
 
+        self.input_gain = None
+
     def initialise(self, data, custom_settings=None):
 
         if custom_settings:
             self.settings = custom_settings
         else:
             try:
-                self.settings = data.settings['LinearAssembler']['linear_system_settings']  # Load settings, the settings should be stored in data.linear.settings
+                self.settings = data.settings['LinearAssembler'][
+                    'linear_system_settings']  # Load settings, the settings should be stored in data.linear.settings
             except KeyError:
                 pass
 
@@ -211,42 +230,40 @@ def initialise(self, data, custom_settings=None):
         self.tsaero0 = data.linear.tsaero0
         self.sys = uvlm
 
-        input_variables_database = {'zeta': [0, 3*self.sys.Kzeta],
-                                    'zeta_dot': [3*self.sys.Kzeta, 6*self.sys.Kzeta],
-                                    'u_gust': [6*self.sys.Kzeta, 9*self.sys.Kzeta]}
-        state_variables_database = {'gamma': [0, self.sys.K],
-                                    'gamma_w': [self.sys.K, self.sys.K_star],
-                                    'dtgamma_dot': [self.sys.K + self.sys.K_star, 2*self.sys.K + self.sys.K_star],
-                                    'gamma_m1': [2*self.sys.K + self.sys.K_star, 3*self.sys.K + self.sys.K_star]}
-
+        state_variables_list = [
+            VectorVariable('gamma', size=self.sys.K, index=0),
+            VectorVariable('gamma_w', size=self.sys.K_star, index=1),
+            VectorVariable('dtgamma_dot', size=self.sys.K, index=2),
+            VectorVariable('gamma_m1', size=self.sys.K, index=3),
+        ]
         self.linearisation_vectors['zeta'] = np.concatenate([self.tsaero0.zeta[i_surf].reshape(-1, order='C')
                                                              for i_surf in range(self.tsaero0.n_surf)])
         self.linearisation_vectors['zeta_dot'] = np.concatenate([self.tsaero0.zeta_dot[i_surf].reshape(-1, order='C')
                                                                  for i_surf in range(self.tsaero0.n_surf)])
         self.linearisation_vectors['u_ext'] = np.concatenate([self.tsaero0.u_ext[i_surf].reshape(-1, order='C')
                                                               for i_surf in range(self.tsaero0.n_surf)])
-        self.linearisation_vectors['forces_aero'] = np.concatenate([self.tsaero0.forces[i_surf][:3].reshape(-1, order='C')
-                                                                    for i_surf in range(self.tsaero0.n_surf)])
-
-        self.input_variables = ss_interface.LinearVector(input_variables_database, self.sys_id)
-        self.state_variables = ss_interface.LinearVector(state_variables_database, self.sys_id)
+        self.linearisation_vectors['forces_aero'] = np.concatenate(
+            [self.tsaero0.forces[i_surf][:3].reshape(-1, order='C')
+             for i_surf in range(self.tsaero0.n_surf)])
 
         if data.aero.n_control_surfaces >= 1:
             import sharpy.linear.assembler.lincontrolsurfacedeflector as lincontrolsurfacedeflector
             self.control_surface = lincontrolsurfacedeflector.LinControlSurfaceDeflector()
             self.control_surface.initialise(data, uvlm)
 
-        if self.settings['rom_method'] != '':
+        if self.settings['rom_method']:
             # Initialise ROM
             self.rom = dict()
             for rom_name in self.settings['rom_method']:
                 self.rom[rom_name] = rom_interface.initialise_rom(rom_name)
                 self.rom[rom_name].initialise(self.settings['rom_method_settings'][rom_name])
 
-        if 'u_gust' not in self.settings['remove_inputs'] and self.settings['gust_assembler'] == 'leading_edge':
+        if 'u_gust' not in self.settings['remove_inputs'] and self.settings['gust_assembler'] != '':
             import sharpy.linear.assembler.lineargustassembler as lineargust
-            self.gust_assembler = lineargust.LinearGustGenerator()
-            self.gust_assembler.initialise(data.aero)
+            self.gust_assembler = lineargust.gust_from_string(self.settings['gust_assembler'])
+            self.gust_assembler.initialise(data.aero, self.sys, self.tsaero0,
+                                           u_ext=lineargust.get_freestream_velocity(data),
+                                           custom_settings=self.settings['gust_assembler_inputs'])
 
     def assemble(self, track_body=False, wake_prop_settings=None):
         r"""
@@ -266,38 +283,32 @@ def assemble(self, track_body=False, wake_prop_settings=None):
 
         if self.scaled:
             self.sys.nondimss()
-        self.ss = self.sys.SS
-        self.C_to_vertex_forces = self.ss.C.copy()
 
-        nzeta = 3 * self.sys.Kzeta
+        if self.settings['convert_to_ct']:
+            self.sys.SS = libss.disc2cont(self.sys.SS)
+
+        self.ss = self.sys.SS
 
         if self.settings['remove_inputs']:
             self.remove_inputs(self.settings['remove_inputs'])
 
         if self.gust_assembler is not None:
-            A, B, C, D = self.gust_assembler.generate(self.sys, aero=None)
-            ss_gust = libss.ss(A, B, C, D, dt=self.ss.dt)
-            self.gust_assembler.ss_gust = ss_gust
-            self.ss = libss.series(ss_gust, self.ss)
+            self.ss = self.gust_assembler.apply(self.ss)
+
+        self.input_gain = libss.Gain(np.eye(self.ss.inputs),
+                                     input_vars=self.ss.input_variables.copy(),
+                                     output_vars=LinearVector.transform(self.ss.input_variables,
+                                                                        to_type=ss_interface.OutputVariable))
 
         if self.control_surface is not None:
-            Kzeta_delta, Kdzeta_ddelta = self.control_surface.generate()
-            n_zeta, n_ctrl_sfc = Kzeta_delta.shape
-
-            # Modify the state space system with a gain at the input side
-            # such that the control surface deflections are last
-            if self.sys.use_sparse:
-                gain_cs = sp.eye(self.ss.inputs, self.ss.inputs + 2 * self.control_surface.n_control_surfaces,
-                                 format='lil')
-                gain_cs[:n_zeta, self.ss.inputs: self.ss.inputs + n_ctrl_sfc] = Kzeta_delta
-                gain_cs[n_zeta: 2*n_zeta, self.ss.inputs + n_ctrl_sfc: self.ss.inputs + 2 * n_ctrl_sfc] = Kdzeta_ddelta
-                gain_cs = libsp.csc_matrix(gain_cs)
-            else:
-                gain_cs = np.eye(self.ss.inputs, self.ss.inputs + 2 * self.control_surface.n_control_surfaces)
-                gain_cs[:n_zeta, self.ss.inputs: self.ss.inputs + n_ctrl_sfc] = Kzeta_delta
-                gain_cs[n_zeta: 2*n_zeta, self.ss.inputs + n_ctrl_sfc: self.ss.inputs + 2 * n_ctrl_sfc] = Kdzeta_ddelta
-            self.ss.addGain(gain_cs, where='in')
-            self.gain_cs = gain_cs
+            ss2 = self.control_surface.apply(self.ss)
+            self.gain_cs = self.control_surface.gain_cs
+            self.connect_input(self.gain_cs)
+            # np.testing.assert_almost_equal(ss2.B, self.ss.B)
+
+        self.D_to_vertex_forces = self.ss.D.copy()  # post-processing issue
+        self.B_to_vertex_forces = self.ss.B.copy()  # post-processing issue
+        self.C_to_vertex_forces = self.ss.C.copy()  # post-processing issue
 
     def remove_inputs(self, remove_list=list):
         """
@@ -309,21 +320,9 @@ def remove_inputs(self, remove_list=list):
         Args:
             remove_list (list): Inputs to remove
         """
+        self.sys.SS.remove_inputs(*remove_list)
 
-        self.input_variables.remove(remove_list)
-
-        i = 0
-        for variable in self.input_variables.vector_vars:
-            if i == 0:
-                trim_array = self.input_variables.vector_vars[variable].cols_loc
-            else:
-                trim_array = np.hstack((trim_array, self.input_variables.vector_vars[variable].cols_loc))
-            i += 1
-
-        self.sys.SS.B = libsp.csc_matrix(self.sys.SS.B[:, trim_array])
-        self.sys.SS.D = libsp.csc_matrix(self.sys.SS.D[:, trim_array])
-
-    def unpack_ss_vector(self, data, x_n, aero_tstep, track_body=False):
+    def unpack_ss_vector(self, data, x_n, u_aero, aero_tstep, track_body=False, state_variables=None, gust_in=False):
         r"""
         Transform column vectors used in the state space formulation into SHARPy format
 
@@ -384,9 +383,32 @@ def unpack_ss_vector(self, data, x_n, aero_tstep, track_body=False):
 
         else:
             Cg_uvlm = np.eye(3)
-        y_n = self.C_to_vertex_forces.dot(x_n)
-        # y_n = np.zeros((3 * self.sys.Kzeta))
 
+        if self.rom is not None:
+            try:
+                rom = self.rom['Krylov']
+            except KeyError:
+                pass
+                # The krylov ROM variable names are applied here
+                # the remaining are applied in the balanced rom class
+            else:
+                x_n = rom.projection_gain.dot(x_n).real
+                state_variables = LinearVector.transform(rom.projection_gain.output_variables, StateVariable)
+
+        try:
+            gust_vars_size = state_variables.get_variable_from_name('gust').size
+            gust_state = x_n[:gust_vars_size]
+        except ValueError:
+            gust_vars_size = 0
+            gust_state = []
+
+        y_n = self.C_to_vertex_forces.dot(x_n) + self.D_to_vertex_forces.dot(u_aero)
+
+        if self.sys.remove_predictor:
+            # x_n += self.B_to_vertex_forces.dot(u_aero)
+            pass
+
+        x_n = x_n[gust_vars_size:]
         gamma_vec, gamma_star_vec, gamma_dot_vec = self.sys.unpack_state(x_n)
 
         # Reshape output into forces[i_surface] where forces[i_surface] is a (6,M+1,N+1) matrix and circulation terms
@@ -413,7 +435,7 @@ def unpack_ss_vector(self, data, x_n, aero_tstep, track_body=False):
 
             # Append reshaped forces to each entry in list (one for each surface)
             f_aero = y_n
-            forces.append(f_aero[worked_points:worked_points+points_in_surface].reshape(dimensions, order='C'))
+            forces.append(f_aero[worked_points:worked_points + points_in_surface].reshape(dimensions, order='C'))
 
             ### project forces.
             # - forces are in UVLM linearisation frame. Hence, these  are projected
@@ -421,50 +443,59 @@ def unpack_ss_vector(self, data, x_n, aero_tstep, track_body=False):
             if track_body:
                 for mm in range(dimensions[1]):
                     for nn in range(dimensions[2]):
-                        forces[i_surf][:,mm,nn] = np.dot(Cg_uvlm, forces[i_surf][:,mm,nn])
+                        forces[i_surf][:, mm, nn] = np.dot(Cg_uvlm, forces[i_surf][:, mm, nn])
 
             # Add the null bottom 3 rows to to the forces entry
             forces[i_surf] = np.concatenate((forces[i_surf], np.zeros(dimensions)))
 
             # Reshape bound circulation terms
-            gamma.append(gamma_vec[worked_panels:worked_panels+panels_in_surface].reshape(
+            gamma.append(gamma_vec[worked_panels:worked_panels + panels_in_surface].reshape(
                 dimensions_gamma, order='C'))
-            gamma_dot.append(gamma_dot_vec[worked_panels:worked_panels+panels_in_surface].reshape(
+            gamma_dot.append(gamma_dot_vec[worked_panels:worked_panels + panels_in_surface].reshape(
                 dimensions_gamma, order='C'))
 
             # Reshape wake circulation terms
-            gamma_star.append(gamma_star_vec[worked_wake_panels:worked_wake_panels+panels_in_wake].reshape(
+            gamma_star.append(gamma_star_vec[worked_wake_panels:worked_wake_panels + panels_in_wake].reshape(
                 dimensions_wake, order='C'))
 
             worked_points += points_in_surface
             worked_panels += panels_in_surface
             worked_wake_panels += panels_in_wake
 
-        return forces, gamma, gamma_dot, gamma_star
+        if gust_in:
+            return forces, gamma, gamma_dot, gamma_star, gust_state
+        else:
+            return forces, gamma, gamma_dot, gamma_star
 
-    def unpack_input_vector(self, u_n):
+    def unpack_input_vector(self, u_n, u_ext_gust, input_variables):
         """
         Unpacks the input vector into the corresponding grid coordinates, velocities and external velocities.
 
         Args:
             u_n (np.ndarray): UVLM input vector. May contain control surface deflections and external velocities.
+            u_ext_gust (np.ndarray): Inputs to the Gust system only. Optional, an empty array may be parsed.
+            input_variables (LinearVector): Vector of input variables to the aerodynamic system
 
         Returns:
             tuple: Tuple containing ``zeta``, ``zeta_dot`` and ``u_ext``, accounting for the effect of control surfaces.
         """
 
-        # if self.gust_assembler is not None:
-        #     u_n = self.gust_assembler.ss_gust
-
         if self.control_surface is not None:
             u_n = self.gain_cs.dot(u_n)
 
-        input_vars = self.input_variables.vector_vars
         tsaero0 = self.tsaero0
 
         input_vectors = dict()
-        for var in input_vars:
-            input_vectors[input_vars[var].name] = u_n[input_vars[var].cols_loc]
+        for var in input_variables:
+            try:
+                if var.name == 'u_gust':
+                    # if len(u_ext_gust) != var.size:
+                    # continue # provided input for external velocities does not match size. will be zero
+                    input_vectors['u_gust'] = u_ext_gust
+                else:
+                    input_vectors[var.name] = u_n[var.cols_loc]
+            except IndexError:
+                break
 
         zeta = []
         zeta_dot = []
@@ -474,15 +505,18 @@ def unpack_input_vector(self, u_n):
         for i_surf in range(tsaero0.n_surf):
             vertices_in_surface = tsaero0.zeta[i_surf].size
             dimensions_zeta = tsaero0.zeta[i_surf].shape
-            zeta.append(input_vectors['zeta'][worked_vertices:worked_vertices+vertices_in_surface].reshape(
+            zeta.append(input_vectors['zeta'][worked_vertices:worked_vertices + vertices_in_surface].reshape(
                 dimensions_zeta, order='C'))
-            zeta_dot.append(input_vectors['zeta_dot'][worked_vertices:worked_vertices+vertices_in_surface].reshape(
+            zeta_dot.append(input_vectors['zeta_dot'][worked_vertices:worked_vertices + vertices_in_surface].reshape(
                 dimensions_zeta, order='C'))
             try:
                 u_gust = input_vectors['u_gust']
+                # TODO: fix this check because it is not correct
+                # u_gust is not 3 * vertices *n_surf because different surfaces can have different vertices
+                # take outside of loop and fix at the top!
             except KeyError:
-                u_gust = np.zeros(3*vertices_in_surface*tsaero0.n_surf)
-            u_ext.append(u_gust[worked_vertices:worked_vertices+vertices_in_surface].reshape(
+                u_gust = np.zeros(3 * vertices_in_surface * tsaero0.n_surf)
+            u_ext.append(u_gust[worked_vertices:worked_vertices + vertices_in_surface].reshape(
                 dimensions_zeta, order='C'))
 
             zeta[i_surf] += tsaero0.zeta[i_surf]
@@ -491,3 +525,37 @@ def unpack_input_vector(self, u_n):
             worked_vertices += vertices_in_surface
 
         return zeta, zeta_dot, u_ext
+
+    def connect_input(self, element):
+        """
+        Connect a gain or a StateSpace to the input of the UVLM
+
+        Args:
+            element (libss.StateSpace or libss.Gain): element to connect to the input of the UVLM
+
+        """
+        if type(element) is libss.StateSpace:
+            self.ss = libss.series(element, self.ss)
+        elif type(element) is libss.Gain:
+            self.ss.addGain(element, where='in')
+            self.input_gain = self.input_gain.dot(element)
+        else:
+            TypeError('Unable to connect system that is not StateSpace or Gain')
+
+    def connect_output(self, element):
+        """
+        Connect a gain or a StateSpace to the output of the UVLM
+
+        Args:
+            element (libss.StateSpace or libss.Gain): element to connect to the output of the UVLM
+
+        """
+        if type(element) is libss.StateSpace:
+            self.ss = libss.series(self.ss, element)
+        elif type(element) is libss.Gain:
+            self.ss.addGain(element, where='out')
+        else:
+            TypeError('Unable to connect system that is not StateSpace or Gain')
+
+    def unpack(self, u):
+        return self.input_gain.value.dot(u)
diff --git a/sharpy/linear/src/libfit.py b/sharpy/linear/src/libfit.py
index 90d882a5e..833a486a0 100644
--- a/sharpy/linear/src/libfit.py
+++ b/sharpy/linear/src/libfit.py
@@ -461,7 +461,7 @@ def rfa_mimo(Yfull, kv, ds, tolAbs, Nnum, Nden, Dmatrix=None, NtrialMax=6, Ncpu=
 #     cfnum = np.array([4, 1.25, 1.5])
 #     cfden = np.array([2, .5, 1])
 #     A, B, C, D = tf2ss(cfnum, cfden)
-#     SS = libss.ss(A, B, C, D, dt=ds)
+#     SS = libss.StateSpace(A, B, C, D, dt=ds)
 #     Cvref = libss.freqresp(SS, kv)
 #     Cvref = Cvref[0, 0, :]
 #
diff --git a/sharpy/linear/src/libss.py b/sharpy/linear/src/libss.py
index 85af3babe..4df2428be 100644
--- a/sharpy/linear/src/libss.py
+++ b/sharpy/linear/src/libss.py
@@ -9,7 +9,7 @@
 The module includes:
 
 Classes:
-- ss: provides a class to build DLTI/LTI systems with full and/or sparse
+- StateSpace: provides a class to build DLTI/LTI systems with full and/or sparse
 	matrices and wraps many of the methods in these library. Methods include:
 	- freqresp: wraps the freqresp function
 	- addGain: adds gains in input/output. This is not a wrapper of addGain, as
@@ -57,7 +57,10 @@
 import numpy as np
 import scipy.signal as scsig
 import scipy.linalg as scalg
+from sharpy.linear.utils.ss_interface import LinearVector, StateVariable, InputVariable, OutputVariable
 import scipy.interpolate as scint
+import h5py
+import sharpy.utils.h5utils as h5utils
 
 # dependency
 import sharpy.linear.src.libsparse as libsp
@@ -65,7 +68,7 @@
 
 # ------------------------------------------------------------- Dedicated class
 
-class ss():
+class StateSpace:
     """
     Wrap state-space models allocation into a single class and support both
     full and sparse matrices. The class emulates
@@ -94,15 +97,10 @@ def __init__(self, A, B, C, D, dt=None):
         self.dt = dt
         self.check_types()
 
-        # determine inputs/outputs/states
-        if self.B.shape.__len__() == 1:
-            # Allow for SISO systems
-            self.inputs = 1
-            self.states = self.B.shape[0]
-        else:
-            (self.states, self.inputs) = self.B.shape
-
-        self.outputs = self.C.shape[0]
+        # vector variable tracking
+        self._input_variables = None  # type: LinearVector
+        self._state_variables = None
+        self._output_variables = None
 
         # verify dimensions
         assert self.A.shape == (self.states, self.states), 'A and B rows not matching'
@@ -117,36 +115,102 @@ def __init__(self, A, B, C, D, dt=None):
     def inputs(self):
         """Number of inputs :math:`m` to the system."""
         if self.B.shape.__len__() == 1:
-            self.inputs = 1
+            return 1
         else:
-            self.inputs = self.B.shape[1]
-
-        return self._inputs
-
-    @inputs.setter
-    def inputs(self, value):
-        # print('Setting Number of inputs')
-        self._inputs = value
+            return self.B.shape[1]
 
     @property
     def outputs(self):
         """Number of outputs :math:`p` of the system."""
-        self.outputs = self.C.shape[0]
-        return self._outputs
-
-    @outputs.setter
-    def outputs(self, value):
-        self._outputs = value
+        return self.C.shape[0]
 
     @property
     def states(self):
         """Number of states :math:`n` of the system."""
-        self.states = self.A.shape[0]
-        return self._states
+        return self.A.shape[0]
 
-    @states.setter
-    def states(self, value):
-        self._states = value
+    @property
+    def input_variables(self):
+        return self._input_variables
+
+    @input_variables.setter
+    def input_variables(self, variables):
+        if variables.variable_class is not InputVariable:
+            raise TypeError('LinearVector does not include InputVariable s')
+        if variables.size != self.inputs:
+            raise IndexError('Size of LinearVector of InputVariable s ({:g}) is not the same as the number of '
+                             'inputs in the '
+                             'system ({:g})'.format(variables.size, self.inputs))
+        self._input_variables = variables
+
+    @property
+    def output_variables(self):
+        return self._output_variables
+
+    @output_variables.setter
+    def output_variables(self, variables):
+        if variables.variable_class is not OutputVariable:
+            raise TypeError('LinearVector does not include OutputVariable s')
+        if variables.size != self.outputs:
+            raise IndexError('Size of LinearVector of OutputVariable s ({:g}) is not the same as the number of '
+                             'outputs in the '
+                             'system ({:g})'.format(variables.size, self.outputs))
+        self._output_variables = variables
+
+    @property
+    def state_variables(self):
+        return self._state_variables
+
+    @state_variables.setter
+    def state_variables(self, variables):
+        if variables.variable_class is not StateVariable:
+            raise TypeError('LinearVector does not include StateVariable s')
+        if variables.size != self.states:
+            raise IndexError('Size of LinearVector of StateVariable s ({:g}) is not the same as the number '
+                             'of states in the '
+                             'system ({:g})'.format(variables.size, self.states))
+        self._state_variables = variables
+
+    def initialise_variables(self, *variable_tuple, var_type='in'):
+        if var_type == 'in' or var_type == 'input':
+            var_class = InputVariable
+        elif var_type == 'out' or var_type == 'output':
+            var_class = OutputVariable
+        elif var_type == 'state':
+            var_class = StateVariable
+        else:
+            raise TypeError('Unknown variable type')
+
+        list_of_variables = []
+        for ith, var_dict in enumerate(variable_tuple):
+            list_of_variables.append(var_class(name=var_dict['name'],
+                                               size=var_dict['size'],
+                                               index=var_dict.get('index', ith)))
+
+        if var_type == 'in' or var_type == 'input':
+            self._input_variables = LinearVector(list_of_variables)
+        elif var_type == 'out' or var_type == 'output':
+            self._output_variables = LinearVector(list_of_variables)
+        elif var_type == 'state':
+            self._state_variables = LinearVector(list_of_variables)
+
+    def __repr__(self):
+        str_out = ''
+        str_out += 'State-space object\n'
+        str_out += 'States: {:g}\n'.format(self.states)
+        str_out += 'Inputs: {:g}\n'.format(self.inputs)
+        str_out += 'Outputs: {:g}\n'.format(self.outputs)
+        if self.dt is not None:
+            str_out += 'dt: {:g}'.format(self.dt)
+
+        if self.input_variables is not None:
+            str_out += '\nInput Variables:\n' + str(self.input_variables)
+        if self.state_variables is not None:
+            str_out += 'State Variables:\n' + str(self.state_variables)
+        if self.output_variables is not None:
+            str_out += 'Output Variables:\n' + str(self.output_variables)
+
+        return str_out
 
     def check_types(self):
         assert type(self.A) in libsp.SupportedTypes, \
@@ -168,7 +232,8 @@ def freqresp(self, wv):
         Note: this wraps frequency response function.
         """
         dlti = True
-        if self.dt == None: dlti = False
+        if self.dt is None:
+            dlti = False
         return freqresp(self, wv, dlti=dlti)
 
     def addGain(self, K, where):
@@ -181,25 +246,35 @@ def addGain(self, K, where):
             - where='out': outputs are projected such that:
                  u -> SS -> y -> y_new=K*y => u -> SSnew -> ynew
 
-        Warning: this is not a wrapper of the addGain method in this module, as
-        the state-space matrices are directly overwritten.
+        Args:
+            K (np.array or Gain): gain matrix or Gain object
+            where (str): ``in`` or ``out``
+
+        Warning:
+            This is not a wrapper of the addGain method in this module, as
+            the state-space matrices are directly overwritten.
         """
 
         assert where in ['in', 'out'], \
             'Specify whether gains are added to input or output'
 
+        with_vars = False
+        if isinstance(K, Gain):
+            gain = K
+            K = K.value
+            with_vars = True
+
         if where == 'in':
             self.B = libsp.dot(self.B, K)
             self.D = libsp.dot(self.D, K)
-            # try:     # No need to update inputs/outputs as they are now properties accessed on demand NG 26/3/19
-            #     self._inputs = K.shape[1]
-            # except IndexError:
-            #     self._inputs = 1
+            if with_vars:
+                self._input_variables = gain.input_variables
 
         if where == 'out':
             self.C = libsp.dot(K, self.C)
             self.D = libsp.dot(K, self.D)
-            # self.outputs = K.shape[0]
+            if with_vars:
+                self._output_variables = gain.output_variables
 
     def scale(self, input_scal=1., output_scal=1., state_scal=1.):
         """
@@ -222,30 +297,38 @@ def scale(self, input_scal=1., output_scal=1., state_scal=1.):
         """
         scale_SS(self, input_scal, output_scal, state_scal, byref=True)
 
-
-    def project(self,WT,V):
-        '''
-        Given 2 transformation matrices, (WT,V) of shapes (Nk,self.states) and
-        (self.states,Nk) respectively, this routine projects the state space
+    def project(self, wt, v):
+        """
+        Given 2 transformation matrices, ``(WT, V)`` of shapes ``(Nk, self.states)`` and
+        ``(self.states, Nk)`` respectively, this routine projects the state space
         model states according to:
 
-            Anew = WT A V
-            Bnew = WT B
-            Cnew = C V
-            Dnew = D
+
+        .. math::
+            Anew = WT A V \\
+            Bnew = WT B \\
+            Cnew = C V \\
+            Dnew = D \\
 
         The projected model has the same number of inputs/outputs as the original
         one, but Nk states.
-        '''
-
-        self.A = libsp.dot( WT, libsp.dot(self.A, V) )
-        self.B = libsp.dot( WT, self.B)
-        self.C = libsp.dot( self.C, V)
-        self.states=V.shape[1]
 
+        Args:
+            wt (Gain or np.ndarray): Left projection matrix
+            v (Gain or np.ndarray): Righty projection matrix
+        """
+        if isinstance(wt, Gain) and isinstance(v, Gain):
+            self.A = libsp.dot(wt.value, libsp.dot(self.A, v.value))
+            self.B = libsp.dot(wt.value, self.B)
+            self.C = libsp.dot(self.C, v.value)
+            self.state_variables = LinearVector.transform(v.input_variables, to_type=StateVariable)
+        else:
+            self.A = libsp.dot(wt, libsp.dot(self.A, v))
+            self.B = libsp.dot(wt, self.B)
+            self.C = libsp.dot(self.C, v)
 
     def truncate(self, N):
-        ''' Retains only the first N states. '''
+        """ Retains only the first N states. """
 
         assert N > 0 and N <= self.states, 'N must be in [1,self.states]'
 
@@ -255,9 +338,9 @@ def truncate(self, N):
         # self.states = N  # No need to update, states is now a property. NG 26/3/19
 
     def max_eig(self):
-        '''
+        """
         Returns most unstable eigenvalue
-        '''
+        """
 
         ev = np.linalg.eigvals(self.A)
 
@@ -272,7 +355,7 @@ def eigvals(self):
             np.ndarray: Eigenvalues of the system
 
         """
-        if ss.dt:
+        if self.dt:
             return eigvals(self.A, dlti=True)
         else:
             return eigvals(self.A, dlti=False)
@@ -287,8 +370,15 @@ def disc2cont(self):
         if self.dt:
             self = disc2cont(self)
 
-    def remove_inout_channels(self, retain_channels, where):
-        remove_inout_channels(self, retain_channels, where)
+    def retain_inout_channels(self, retain_channels, where):
+        """
+        Retain selected input or output channels only.
+
+        Args:
+            retain_channels (list): List of channels to retain
+            where (str): ``in`` or ``out`` for input/output channels
+        """
+        retain_inout_channels(self, retain_channels, where)
 
     def summary(self):
         msg = 'State-space system\nStates: %g\nInputs: %g\nOutputs: %g\n' % (self.states, self.inputs, self.outputs)
@@ -310,10 +400,367 @@ def transfer_function_evaluation(self, s):
 
         return c.dot(scalg.inv(s * np.eye(n) - a)).dot(b) + d
 
+    def save(self, path):
+        """Save state-space object to h5 file"""
+        with h5py.File(path, 'w') as f:
+            f.create_dataset('a', data=self.A)
+            f.create_dataset('b', data=self.B)
+            f.create_dataset('c', data=self.C)
+            f.create_dataset('d', data=self.D)
+            if self.dt:
+                f.create_dataset('dt', data=self.dt)
+
+            if self.input_variables is not None:
+                self.input_variables.add_to_h5_file(f)
+                self.output_variables.add_to_h5_file(f)
+                self.state_variables.add_to_h5_file(f)
+
+    @classmethod
+    def load_from_h5(cls, h5_file_name):
+        """
+        Loads a state-space object from an h5 file, including variable information
+
+        Args:
+            h5_file_name (str): Path to file
+
+        Returns:
+            StateSpace: loaded state-space from file
+        """
+
+        with h5py.File(h5_file_name, 'r') as f:
+            data_dict = h5utils.load_h5_in_dict(f)
+
+        new_ss = cls(data_dict['a'],
+                     data_dict['b'],
+                     data_dict['c'],
+                     data_dict['d'],
+                     dt=data_dict.get('dt'))
+
+        input_variables = data_dict.get('InputVariable')
+        if input_variables is not None:
+            new_ss.input_variables = LinearVector.load_from_h5_file('InputVariable',
+                                                                    data_dict['InputVariable'])
+            new_ss.output_variables = LinearVector.load_from_h5_file('OutputVariable',
+                                                                     data_dict['OutputVariable'])
+            new_ss.state_variables = LinearVector.load_from_h5_file('StateVariable',
+                                                                    data_dict['StateVariable'])
+
+            return new_ss
+        else:
+            return new_ss
+
+    def remove_inputs(self, *input_remove_list):
+        """
+        Removes inputs through their variable names.
+
+        Needs that the ``StateSpace`` attribute ``input_variables`` is defined.
+
+        Args:
+            input_remove_list (list(str)): List of inputs to remove
+
+        """
+        if self.input_variables is None:
+            raise AttributeError('No input variables have been defined for the current state-space object. Define '
+                                 'some variables prior to using the remove_inputs() method.')
+
+        self.input_variables.remove(*input_remove_list)
+
+        i = 0
+        retain_input_array = None
+        for variable in self.input_variables:
+            if i == 0:
+                retain_input_array = variable.cols_loc
+            else:
+                retain_input_array = np.hstack((retain_input_array, variable.cols_loc))
+            i += 1
+
+        if retain_input_array is not None:
+            if type(self.B) is libsp.csc_matrix:
+                self.B = libsp.csc_matrix(self.B[:, retain_input_array])
+                self.D = libsp.csc_matrix(self.D[:, retain_input_array])
+            else:
+                self.B = self.B[:, retain_input_array]
+                self.D = self.D[:, retain_input_array]
+
+        self.input_variables.update_locations()
+
+    def remove_outputs(self, *output_remove_list):
+        """
+        Removes outputs through their variable names.
+
+        Needs that the ``StateSpace`` attribute ``output_variables`` is defined.
+
+        Args:
+            output_remove_list (list(str)): List of outputs to remove
+
+        """
+        if self.output_variables is None:
+            raise AttributeError('No output variables have been defined for the current state-space object. Define '
+                                 'some variables prior to using the remove_outputs() method.')
+
+        new_outputs = 0
+        for variable in self.output_variables:
+            if variable.name not in output_remove_list:
+                new_outputs += variable.size
+
+        out_gain = np.zeros((new_outputs, self.outputs))
+        worked_outputs = 0
+        for variable in self.output_variables:
+            if variable.name not in output_remove_list:
+                index = variable.rows_loc
+                out_gain[worked_outputs:worked_outputs + variable.size, index] = np.eye(variable.size)
+                worked_outputs += variable.size
+
+        if new_outputs != self.outputs:
+            if type(self.B) is libsp.csc_matrix:
+                self.C = libsp.csc_matrix(out_gain.dot(self.C))
+                self.D = libsp.csc_matrix(out_gain.dot(self.D))
+            else:
+                self.C = out_gain.dot(self.C)
+                self.D = out_gain.dot(self.D)
+
+        self.output_variables.remove(*output_remove_list)
+        self.output_variables.update_locations()
+
+    @classmethod
+    def from_scipy(cls, scipy_ss):
+        """
+        Transforms a ``scipy.signal.lti`` or dlti into a StateSpace class
+
+        Args:
+            scipy_ss (scipy.signal.ltisys.StateSpaceContinous or scipy.signal.ltisys.StateSpaceDiscrete): Scipy
+              State Space object.
+
+        Returns:
+            StateSpace: SHARPy state space object
+        """
+        a = scipy_ss.A
+        b = scipy_ss.B
+        c = scipy_ss.C
+        d = scipy_ss.D
+
+        return cls(a, b, c, d, dt=scipy_ss.dt)
+
+
+class Gain:
+
+    def __init__(self, value, input_vars=None, output_vars=None):
+        self.value = value
+        self._input_variables = None
+        self._output_variables = None
+
+        if input_vars is not None:
+            self.input_variables = input_vars
+        if output_vars is not None:
+            self.output_variables = output_vars
+
+    @property
+    def input_variables(self):
+        return self._input_variables
+
+    @input_variables.setter
+    def input_variables(self, variables):
+        if variables.variable_class is not InputVariable:
+            raise TypeError('LinearVector does not include InputVariable s')
+        if variables.size != self.inputs:
+            raise IndexError('Size of LinearVector of InputVariable s ({:g}) is not the same as the number of '
+                             'inputs in the '
+                             'system ({:g})'.format(variables.size, self.inputs))
+        self._input_variables = variables
+
+    @property
+    def output_variables(self):
+        return self._output_variables
+
+    @output_variables.setter
+    def output_variables(self, variables):
+        if variables.variable_class is not OutputVariable:
+            raise TypeError('LinearVector does not include OutputVariable s')
+        if variables.size != self.outputs:
+            raise IndexError('Size of LinearVector of OutputVariable s ({:g}) is not the same as the number of '
+                             'outputs in the '
+                             'system ({:g})'.format(variables.size, self.outputs))
+        self._output_variables = variables
+
+    @property
+    def inputs(self):
+        """Number of inputs :math:`m` to the system."""
+        if self.value.shape.__len__() == 1:
+            return 1
+        else:
+            return self.value.shape[1]
+
+    @property
+    def outputs(self):
+        """Number of outputs :math:`p` of the gain."""
+        return self.value.shape[0]
+
+    def dot(self, elem):
+        """
+        Dot product of two Gains
+
+        Args:
+            elem (np.array or Gain):
+
+        Returns:
+            np.array or Gain: new matrix/Gain containing the dot product
+        """
+        if type(elem) is Gain:
+            LinearVector.check_connection(elem.output_variables, self.input_variables)
+            new_gain_value = libsp.dot(self.value, elem.value)
+            return Gain(new_gain_value,
+                        input_vars=elem.input_variables.copy(),
+                        output_vars=self.output_variables.copy())
+        else:
+            return self.value.dot(elem)
+
+    def __repr__(self):
+        str_out = ''
+        str_out += 'Gain object\n'
+        str_out += 'Inputs: {:g}\n'.format(self.inputs)
+        str_out += 'Outputs: {:g}\n'.format(self.outputs)
+
+        if self.input_variables is not None:
+            str_out += '\nInput Variables:\n' + str(self.input_variables)
+        if self.output_variables is not None:
+            str_out += 'Output Variables:\n' + str(self.output_variables)
+
+        return str_out
+
+    def transpose(self):
+        """
+        Transposes the gain, such that the inputs become the outputs and vice-versa.
+        """
+
+        if self.input_variables is not None:
+            temp_input_var = self.input_variables.copy()
+            input_variables = LinearVector.transform(self.output_variables,
+                                                     to_type=InputVariable)
+            output_variables = LinearVector.transform(temp_input_var,
+                                                      to_type=OutputVariable)
+
+            return Gain(self.value.T,
+                        input_vars=input_variables,
+                        output_vars=output_variables)
+        else:
+            return Gain(self.value.T)
+
+    @property
+    def T(self):
+        return self.transpose()
+
+    def copy(self):
+        if self.input_variables is not None:
+            return Gain(self.value, input_vars=self.input_variables.copy(), output_vars=self.output_variables.copy())
+        else:
+            return Gain(self.value)
+
+    def save(self, path):
+        """Save gain object to h5 file"""
+        with h5py.File(path, 'w') as f:
+            f.create_dataset('gain', data=self.value)
+
+            if self.input_variables is not None:
+                self.input_variables.add_to_h5_file(f)
+                self.output_variables.add_to_h5_file(f)
+
+    def add_as_group_to_h5(self, h5_file_handle, group_name):
+        """
+        Adds gain to an h5 file handle
+        
+        Args:
+            h5_file_handle (h5py.File): writeable h5 file handle
+            group_name (str): Desired group name to save gain in h5
+
+        """
+        gain_group = h5_file_handle.create_group(group_name)
+        gain_group.create_dataset(name='gain', data=self.value)
+
+        if self.input_variables is not None:
+            self.input_variables.add_to_h5_file(gain_group)
+            self.output_variables.add_to_h5_file(gain_group)
+
+    @classmethod
+    def load_from_h5(cls, h5_file_name):
+        """
+        Returns a gain object from an .h5 file
+
+        Args:
+            h5_file_name (str): Path to h5 file
+
+        Returns:
+            Gain: instance of a Gain
+        """
+        with h5py.File(h5_file_name, 'r') as f:
+            data_dict = h5utils.load_h5_in_dict(f)
+
+        return cls.load_from_dict(data_dict)
+
+    @classmethod
+    def load_from_dict(cls, data_dict):
+        """
+
+        Returns a Gain from a dictionary of data, useful for loading from a group of gains in a single
+        .h5 file
+
+        Args:
+            data_dict (dict): Dictionary with keys: ``gain`` and (if available) ``InputVariable``
+              and ``OutputVariable``.
+
+        Returns:
+            Gain: instance of Gain
+        """
+        input_variables = data_dict.get('InputVariable')
+        if input_variables is not None:
+            input_variables = LinearVector.load_from_h5_file('InputVariable',
+                                                             data_dict['InputVariable'])
+            output_variables = LinearVector.load_from_h5_file('OutputVariable',
+                                                              data_dict['OutputVariable'])
+
+            return cls(data_dict['gain'], input_vars=input_variables,
+                       output_vars=output_variables)
+        else:
+            return cls(data_dict['gain'])
+
+    @classmethod
+    def save_multiple_gains(cls, h5_file_name, *gains_names_tuple):
+        """
+        Saves multiple gains to a single h5 file
+
+        Args:
+            h5_file_name (str): Path to h5 file
+            *gains_names_tuple (tuple): ``(gain_name (str), gain(Gain))`` tuples to save. The gain name will be the name
+              given on the h5 file
+
+        """
+        with h5py.File(h5_file_name, 'w') as f:
+            for name, gain in gains_names_tuple:
+                gain.add_as_group_to_h5(f, name)
+
+    @classmethod
+    def load_multiple_gains(cls, h5_file_name):
+        """
+        Loads multiple gains from a single h5 file
+
+        Args:
+            h5_file_name (str): Path to h5 file
+
+        Returns:
+            dict: Dictionary of loaded gains in a gain_name: Gain dictionary
+        """
+        with h5py.File(h5_file_name, 'r') as f:
+            data_dict = h5utils.load_h5_in_dict(f)
+
+        out_gains = {}
+        for gain_name, gain_data in data_dict.items():
+            out_gains[gain_name] = cls.load_from_dict(gain_data)
+
+        return out_gains
+
 
 class ss_block():
-    '''
-    State-space model in block form. This class has the same purpose as "ss",
+    """
+    State-space model in block form. This class has the same purpose as "StateSpace",
     but the A, B, C, D are allocated in the form of nested lists. The format is
     similar to the one used in numpy.block but:
         1. Block matrices can contain both dense and sparse matrices
@@ -323,10 +770,10 @@ class ss_block():
     - remove_block: drop one of the blocks from the s-s model
     - addGain: project inputs/outputs
     - project: project state
-    '''
+    """
 
     def __init__(self, A, B, C, D, S_states, S_inputs, S_outputs, dt=None):
-        '''
+        """
         Allocate state-space model (A,B,C,D) in block form starting from nested
         lists of full/sparse matrices (as per numpy.block).
 
@@ -335,7 +782,7 @@ def __init__(self, A, B, C, D, S_states, S_inputs, S_outputs, dt=None):
         - S_states, S_inputs, S_outputs: lists with dimensions of of each block
         representing the states, inputs and outputs of the model.
         - dt: time-step. In None, a continuous-time system is assumed.
-        '''
+        """
 
         self.A = A
         self.B = B
@@ -363,13 +810,13 @@ def check_sizes(self):
         pass
 
     def remove_block(self, where, index):
-        '''
+        """
         Remove a block from either inputs or outputs.
 
         Inputs:
         - where = {'in', 'out'}: determined whether to remove inputs or outputs
         - index: index of block to remove
-        '''
+        """
 
         assert where in ['in', 'out'], "'where' must be equal to {'in', 'out'}"
 
@@ -385,7 +832,7 @@ def remove_block(self, where, index):
                 del self.D[ii]
 
     def addGain(self, K, where):
-        '''
+        """
         Projects input u or output y the state-space system through the gain
         block matrix K. The input 'where' determines whether inputs or outputs
         are projected as:
@@ -396,7 +843,7 @@ def addGain(self, K, where):
 
         Input: K must be a list of list of matrices. The size of K must be
         compatible with either B or C for block matrix product.
-        '''
+        """
 
         assert where in ['in', 'out'], \
             'Specify whether gains are added to input or output'
@@ -418,9 +865,9 @@ def addGain(self, K, where):
             self.outputs = sum(rows)
 
     def get_sizes(self, M):
-        '''
+        """
         Get the size of each block in M.
-        '''
+        """
 
         rM, cM = len(M), len(M[0])
         rows = rM * [None]
@@ -446,7 +893,7 @@ def get_sizes(self, M):
         return rows, cols
 
     def project(self, WT, V, by_arrays=True, overwrite=False):
-        '''
+        """
         Given 2 transformation matrices, (W,V) of shape (Nk,self.states), this
         routine projects the state space model states according to:
 
@@ -464,7 +911,7 @@ def project(self, WT, V, by_arrays=True, overwrite=False):
         - by_arrays: if True, W, V are either numpy.array or sparse matrices. If
           False, they are block matrices.
         - overwrite: if True, overwrites the A, B, C matrices
-        '''
+        """
 
         if by_arrays:  # transform to block structures
 
@@ -489,7 +936,6 @@ def project(self, WT, V, by_arrays=True, overwrite=False):
                     libsp.block_dot(WTblock, self.B),
                     libsp.block_dot(self.C, Vblock))
 
-
     def solve_step(self, xn, un):
 
         # TODO: add options about predictor ...
@@ -500,8 +946,8 @@ def solve_step(self, xn, un):
 
 
 # ---------------------------------------- Methods for state-space manipulation
-def project(ss_here,WT,V):
-    '''
+def project(ss_here, WT, V):
+    """
     Given 2 transformation matrices, (WT,V) of shapes (Nk,self.states) and
     (self.states,Nk) respectively, this routine returns a projection of the
     state space ss_here according to:
@@ -513,13 +959,14 @@ def project(ss_here,WT,V):
 
     The projected model has the same number of inputs/outputs as the original
     one, but Nk states.
-    '''
+    """
+
+    Ap = libsp.dot(WT, libsp.dot(ss_here.A, V))
+    Bp = libsp.dot(WT, ss_here.B)
+    Cp = libsp.dot(ss_here.C, V)
 
-    Ap = libsp.dot( WT, libsp.dot(ss_here.A, V) )
-    Bp = libsp.dot( WT, ss_here.B)
-    Cp = libsp.dot( ss_here.C, V)
+    return StateSpace(Ap, Bp, Cp, ss_here.D, ss_here.dt)
 
-    return ss(Ap,Bp,Cp,ss_here.D,ss_here.dt)
 
 def couple(ss01, ss02, K12, K21, out_sparse=False):
     """
@@ -527,14 +974,32 @@ def couple(ss01, ss02, K12, K21, out_sparse=False):
     K12 transforms the output of ss02 into an input of ss01.
 
     Other inputs:
-    - out_sparse: if True, the output system is stored as sparse (not recommended)
+        - out_sparse: if True, the output system is stored as sparse (not recommended)
     """
-
-    assert np.abs(ss01.dt - ss02.dt) < 1e-10 * ss01.dt, 'Time-steps not matching!'
-    assert K12.shape == (ss01.inputs, ss02.outputs), \
-        'Gain K12 shape not matching with systems number of inputs/outputs'
-    assert K21.shape == (ss02.inputs, ss01.outputs), \
-        'Gain K21 shape not matching with systems number of inputs/outputs'
+    if ss01.dt is None and ss02.dt is None:
+        pass
+    else:
+        try:
+            assert np.abs(ss01.dt - ss02.dt) < 1e-10 * ss01.dt, 'Time-steps not matching!'
+        except TypeError:
+            raise TypeError('One of the systems to couple is discrete and the other continuous')
+
+    if ss01.input_variables is not None and ss02.input_variables is not None \
+            and isinstance(K12, Gain) and isinstance(K21, Gain):
+        with_enhanced_vars = True
+        LinearVector.check_connection(K12.output_variables, ss01.input_variables)
+        LinearVector.check_connection(ss02.output_variables, K12.input_variables)
+
+        LinearVector.check_connection(K21.output_variables, ss02.input_variables)
+        LinearVector.check_connection(ss01.output_variables, K21.input_variables)
+        K21 = K21.value
+        K12 = K12.value
+    else:
+        with_enhanced_vars = False
+        assert K12.shape == (ss01.inputs, ss02.outputs), \
+            'Gain K12 shape not matching with systems number of inputs/outputs'
+        assert K21.shape == (ss02.inputs, ss01.outputs), \
+            'Gain K21 shape not matching with systems number of inputs/outputs'
 
     A1, B1, C1, D1 = ss01.get_mats()
     A2, B2, C2, D2 = ss02.get_mats()
@@ -594,7 +1059,14 @@ def couple(ss01, ss02, K12, K21, out_sparse=False):
             [libsp.dense(libsp.dot(libsp.dot(D2, cpl_21), D1)),
              libsp.dense(D2 + libsp.dot(libsp.dot(D2, cpl_22), D2))]])
 
-    return ss(A, B, C, D, dt=ss01.dt)
+    coupled_ss = StateSpace(A, B, C, D, dt=ss01.dt)
+    if with_enhanced_vars:
+        coupled_ss.state_variables = LinearVector.merge(ss01.state_variables, ss02.state_variables)
+        coupled_ss.input_variables = LinearVector.merge(ss01.input_variables, ss02.input_variables)
+        coupled_ss.output_variables = LinearVector.merge(ss01.output_variables, ss02.output_variables)
+
+    return coupled_ss
+
 
 def disc2cont(sys):
     r"""
@@ -615,10 +1087,10 @@ def disc2cont(sys):
         MIT OCW 6.245
 
     Args:
-        sys (libss.ss): SHARPy discrete-time state-space object.
+        sys (libss.StateSpace): SHARPy discrete-time state-space object.
 
     Returns:
-        libss.ss: Converted continuous-time state-space object.
+        libss.StateSpace: Converted continuous-time state-space object.
     """
 
     assert sys.dt is not None, 'System to transform is not a discrete-time system.'
@@ -633,17 +1105,37 @@ def disc2cont(sys):
     c = np.sqrt(2 * omega_0) * sys.C.dot(eye_a_inv)
     d = sys.D - sys.C.dot(eye_a_inv.dot(sys.B))
 
-    return ss(a, b, c, d)
+    sys_ct = StateSpace(a, b, c, d)
+
+    if sys.input_variables is not None:
+        sys_ct.input_variables = sys.input_variables
+        sys_ct.state_variables = sys.state_variables
+        sys_ct.output_variables = sys.output_variables
+
+    return sys_ct
+
 
+def retain_inout_channels(sys, retain_channels, where):
+    """
+    Retain selected input or output channels only.
 
-def remove_inout_channels(sys, retain_channels, where):
+    Args:
+        retain_channels (list): List of channels to retain
+        where (str): ``in`` or ``out`` for input/output channels
 
+    Returns:
+        StateSpace: Updated state-space object
+    """
     retain_m = len(retain_channels)  # new number of in/out
 
     if where == 'in':
         m = sys.inputs  # current number of in/out
+        gain_input_vars = sys.input_variables
+        gain_output_vars = LinearVector.transform(sys.input_variables, to_type=OutputVariable)
     elif where == 'out':
         m = sys.outputs
+        gain_input_vars = LinearVector.transform(sys.output_variables, to_type=InputVariable)
+        gain_output_vars = sys.output_variables.copy()
     else:
         raise NameError('Argument ``where`` can only be ``in`` or ``out``.')
 
@@ -651,8 +1143,25 @@ def remove_inout_channels(sys, retain_channels, where):
     for ith, channel in enumerate(retain_channels):
         gain_matrix[ith, channel] = 1
 
+    # Go through variables...
+    for var in gain_input_vars:
+        n_vars = np.sum(
+            (np.array(retain_channels) < var.end_position) * (np.array(retain_channels) >= var.first_position))
+
+        if n_vars == 0:
+            gain_output_vars.remove(var.name)
+        else:
+            gain_output_vars.modify(var.name, size=n_vars)
+
+    gain_output_vars.update_indices()
+    gain_output_vars.update_locations()
+
+    gain_matrix = Gain(gain_matrix,
+                       input_vars=gain_input_vars,
+                       output_vars=gain_output_vars)
+
     if where == 'in':
-        sys.addGain(gain_matrix.T, where='in')
+        sys.addGain(gain_matrix.transpose(), where='in')
     elif where == 'out':
         sys.addGain(gain_matrix, where='out')
     else:
@@ -661,158 +1170,12 @@ def remove_inout_channels(sys, retain_channels, where):
     return sys
 
 
-
-
-# def couple_wrong02(ss01, ss02, K12, K21):
-#     """
-#     Couples 2 dlti systems ss01 and ss02 through the gains K12 and K21, where
-#     K12 transforms the output of ss02 into an input of ss01.
-#     """
-
-#     assert ss01.dt == ss02.dt, 'Time-steps not matching!'
-#     assert K12.shape == (ss01.inputs, ss02.outputs), \
-#         'Gain K12 shape not matching with systems number of inputs/outputs'
-#     assert K21.shape == (ss02.inputs, ss01.outputs), \
-#         'Gain K21 shape not matching with systems number of inputs/outputs'
-
-#     A1, B1, C1, D1 = ss01.A, ss01.B, ss01.C, ss01.D
-#     A2, B2, C2, D2 = ss02.A, ss02.B, ss02.C, ss02.D
-
-#     # extract size
-#     Nx1, Nu1 = B1.shape
-#     Ny1 = C1.shape[0]
-#     Nx2, Nu2 = B2.shape
-#     Ny2 = C2.shape[0]
-
-#     #  terms to invert
-#     maxD1 = np.max(np.abs(D1))
-#     maxD2 = np.max(np.abs(D2))
-#     if maxD1 < 1e-32:
-#         pass
-#     if maxD2 < 1e-32:
-#         pass
-
-#     # terms solving for u21 (input of ss02 due to ss01)
-#     K11 = np.dot(K12, np.dot(D2, K21))
-#     # L1=np.eye(Nu1)-np.dot(K11,D1)
-#     L1inv = np.linalg.inv(np.eye(Nu1) - np.dot(K11, D1))
-
-#     # coupling terms for u21
-#     cpl_11 = np.dot(L1inv, K11)
-#     cpl_12 = np.dot(L1inv, K12)
-
-#     # terms solving for u12 (input of ss01 due to ss02)
-#     T = np.dot(np.dot(K21, D1), L1inv)
-
-#     # coupling terms for u21
-#     cpl_21 = K21 + np.dot(T, K11)
-#     cpl_22 = np.dot(T, K12)
-
-#     # Build coupled system
-#     A = np.block([
-#         [A1 + np.dot(np.dot(B1, cpl_11), C1), np.dot(np.dot(B1, cpl_12), C2)],
-#         [np.dot(np.dot(B2, cpl_21), C1), A2 + np.dot(np.dot(B2, cpl_22), C2)]])
-
-#     C = np.block([
-#         [C1 + np.dot(np.dot(D1, cpl_11), C1), np.dot(np.dot(D1, cpl_12), C2)],
-#         [np.dot(np.dot(D2, cpl_21), C1), C2 + np.dot(np.dot(D2, cpl_22), C2)]])
-
-#     B = np.block([
-#         [B1 + np.dot(np.dot(B1, cpl_11), D1), np.dot(np.dot(B1, cpl_12), D2)],
-#         [np.dot(np.dot(B2, cpl_21), D1), B2 + np.dot(np.dot(B2, cpl_22), D2)]])
-
-#     D = np.block([
-#         [D1 + np.dot(np.dot(D1, cpl_11), D1), np.dot(np.dot(D1, cpl_12), D2)],
-#         [np.dot(np.dot(D2, cpl_21), D1), D2 + np.dot(np.dot(D2, cpl_22), D2)]])
-
-#     if ss01.dt is None:
-#         sstot = scsig.lti(A, B, C, D)
-#     else:
-#         sstot = scsig.dlti(A, B, C, D, dt=ss01.dt)
-#     return sstot
-
-
-# def couple_wrong(ss01, ss02, K12, K21):
-#     """
-#     Couples 2 dlti systems ss01 and ss02 through the gains K12 and K21, where
-#     K12 transforms the output of ss02 into an input of ss01.
-#     """
-
-#     assert ss01.dt == ss02.dt, 'Time-steps not matching!'
-#     assert K12.shape == (ss01.inputs, ss02.outputs), \
-#         'Gain K12 shape not matching with systems number of inputs/outputs'
-#     assert K21.shape == (ss02.inputs, ss01.outputs), \
-#         'Gain K21 shape not matching with systems number of inputs/outputs'
-
-#     A1, B1, C1, D1 = ss01.A, ss01.B, ss01.C, ss01.D
-#     A2, B2, C2, D2 = ss02.A, ss02.B, ss02.C, ss02.D
-
-#     # extract size
-#     Nx1, Nu1 = B1.shape
-#     Ny1 = C1.shape[0]
-#     Nx2, Nu2 = B2.shape
-#     Ny2 = C2.shape[0]
-
-#     #  terms to invert
-#     maxD1 = np.max(np.abs(D1))
-#     maxD2 = np.max(np.abs(D2))
-#     if maxD1 < 1e-32:
-#         pass
-#     if maxD2 < 1e-32:
-#         pass
-
-#     # compute self-coupling terms
-#     S1 = np.dot(K12, np.dot(D2, K21))
-#     S2 = np.dot(K21, np.dot(D1, K12))
-
-#     # left hand side terms
-#     L1 = np.eye(Nu1) - np.dot(S1, D1)
-#     L2 = np.eye(Nu2) - np.dot(S2, D2)
-
-#     # invert left hand side terms
-#     L1inv = np.linalg.inv(L1)
-#     L2inv = np.linalg.inv(L2)
-
-#     # recurrent terms
-#     L1invS1 = np.dot(L1inv, S1)
-#     L2invS2 = np.dot(L2inv, S2)
-
-#     L1invK12 = np.dot(L1inv, K12)
-#     L2invK21 = np.dot(L2inv, K21)
-
-#     # Build coupled system
-#     A = np.block([
-#         [A1 + np.dot(np.dot(B1, L1invS1), C1), np.dot(np.dot(B1, L1invK12), C2)],
-#         [np.dot(np.dot(B2, L2invK21), C1), A2 + np.dot(np.dot(B2, L2invS2), C2)]])
-
-#     C = np.block([
-#         [C1 + np.dot(np.dot(D1, L1invS1), C1), np.dot(np.dot(D1, L1invK12), C2)],
-#         [np.dot(np.dot(D2, L2invK21), C1), C2 + np.dot(np.dot(D2, L2invS2), C2)]])
-
-#     B = np.block([
-#         [B1 + np.dot(np.dot(B1, L1invS1), D1), np.dot(np.dot(B1, L1invK12), D2)],
-#         [np.dot(np.dot(B2, L2invK21), D1), B2 + np.dot(np.dot(B2, L2invS2), D2)]])
-
-#     D = np.block([
-#         [D1 + np.dot(np.dot(D1, L1invS1), D1), np.dot(np.dot(D1, L1invK12), D2)],
-#         [np.dot(np.dot(D2, L2invK21), D1), D2 + np.dot(np.dot(D2, L2invS2), D2)]])
-
-#     if ss01.dt is None:
-#         sstot = scsig.lti(A, B, C, D)
-#     else:
-#         sstot = scsig.dlti(A, B, C, D, dt=ss01.dt)
-#     return sstot
-
-
-
-
-
 def freqresp(SS, wv, dlti=True):
     """
     In-house frequency response function supporting dense/sparse types
 
     Inputs:
-    - SS: instance of ss class, or scipy.signal.StateSpace*
+    - SS: instance of StateSpace class, or scipy.signal.StateSpace*
     - wv: frequency range
     - dlti: True if discrete-time system is considered.
 
@@ -825,8 +1188,8 @@ def freqresp(SS, wv, dlti=True):
     matrices.
     """
 
-    assert type(SS) == ss, \
-        'Type %s of state-space model not supported. Use libss.ss instead!' % type(SS)
+    assert type(SS) == StateSpace, \
+        'Type %s of state-space model not supported. Use libss.StateSpace instead!' % type(SS)
     SS.check_types()
 
     if hasattr(SS, 'dt') and dlti:
@@ -867,18 +1230,32 @@ def series(SS01, SS02):
         u \rightarrow \mathsf{SS01} \rightarrow \mathsf{SS02} \rightarrow y \Longrightarrow
         u \rightarrow \mathsf{SStot} \rightarrow y
 
+    where the state vector :math:`x` is :math:`[x_1, x_2]`.
+
     Args:
-        SS01 (libss.ss): State Space 1 instance. Can be DLTI/CLTI, dense or sparse.
-        SS02 (libss.ss): State Space 2 instance. Can be DLTI/CLTI, dense or sparse.
+        SS01 (libss.StateSpace): State Space 1 instance. Can be DLTI/CLTI, dense or sparse.
+        SS02 (libss.StateSpace): State Space 2 instance. Can be DLTI/CLTI, dense or sparse.
 
     Returns
-        libss.ss: Combined state space system in series in dense format.
+        libss.StateSpace: Combined state space system in series in dense format.
     """
 
     if type(SS01) is not type(SS02):
-        raise NameError('The two input systems need to have the same size!')
+        raise TypeError('The two input systems are not of the same type')
     if SS01.dt != SS02.dt:
-        raise NameError('DLTI systems do not have the same time-step!')
+        raise NameError('DLTI systems do not have the same time-step. SS01 dt={:f}, SS02 dt={:f}'.format(
+            SS01.dt, SS02.dt))
+
+    # check series connection
+    if SS01.output_variables is not None and SS02.input_variables is not None:
+        LinearVector.check_connection(SS01.output_variables, SS02.input_variables)
+        # for i_var in range(SS01.output_variables.num_variables):
+        #     out1 = SS01.output_variables[i_var]
+        #     in2 = SS02.input_variables[i_var]
+        #     if out1.name != in2.name:
+        #         raise NameError('Series coupling outputs1 and inputs2 have different names')
+        #     if not (out1.rows_loc == in2.cols_loc).all:
+        #         raise IndexError('Series coupling. Output1 channels do not line up with input2 channels.')
 
     # determine size of total system
     Nst01, Nst02 = SS01.states, SS02.states
@@ -886,6 +1263,9 @@ def series(SS01, SS02):
     Nin = SS01.inputs
     Nout = SS02.outputs
 
+    if SS01.outputs != SS02.inputs:
+        raise ValueError('SS01 outputs not equal to SS02 inputs,\nSS01={:s}\nSS02={:s}'.format(str(SS01), str(SS02)))
+
     # Build A matrix
     A = np.zeros((Nst, Nst))
 
@@ -898,7 +1278,14 @@ def series(SS01, SS02):
     C = np.concatenate((libsp.dense(libsp.dot(SS02.D, SS01.C)), libsp.dense(SS02.C)), axis=1)
     D = libsp.dense(libsp.dot(SS02.D, SS01.D))
 
-    SStot = ss(A, B, C, D, dt=SS01.dt)
+    SStot = StateSpace(A, B, C, D, dt=SS01.dt)
+
+    SStot.input_variables = SS01.input_variables
+    try:
+        SStot.state_variables = LinearVector.merge(SS01.state_variables, SS02.state_variables)
+    except AttributeError:
+        SStot.state_variables = None
+    SStot.output_variables = SS02.output_variables
 
     return SStot
 
@@ -1071,9 +1458,9 @@ def addGain(SShere, Kmat, where):
         D = np.block([Kmat, SShere.D])
 
     if SShere.dt == None:
-        SSnew = ss(A, B, C, D)
+        SSnew = StateSpace(A, B, C, D)
     else:
-        SSnew = ss(A, B, C, D, dt=SShere.dt)
+        SSnew = StateSpace(A, B, C, D, dt=SShere.dt)
 
     return SSnew
 
@@ -1113,7 +1500,6 @@ def join2(SS1, SS2):
         SStot = np.block([[SS1, np.zeros((Nout01, Nin02))],
                           [np.zeros((Nout02, Nin01)), SS2]])
 
-
     elif isinstance(SS1, np.ndarray) and isinstance(SS2, type_dlti):
 
         Nin01, Nout01 = SS1.shape[1], SS1.shape[0]
@@ -1129,7 +1515,6 @@ def join2(SS1, SS2):
 
         SStot = scsig.StateSpace(A, B, C, D, dt=SS2.dt)
 
-
     elif isinstance(SS1, type_dlti) and isinstance(SS2, np.ndarray):
 
         Nin01, Nout01 = SS1.inputs, SS1.outputs
@@ -1145,7 +1530,6 @@ def join2(SS1, SS2):
 
         SStot = scsig.StateSpace(A, B, C, D, dt=SS1.dt)
 
-
     elif isinstance(SS1, type_dlti) and isinstance(SS2, type_dlti):
 
         assert SS1.dt == SS2.dt, 'State-space models must have the same time-step'
@@ -1164,50 +1548,51 @@ def join2(SS1, SS2):
                       [np.zeros((Nout02, Nin01)), SS2.D]])
         SStot = scsig.StateSpace(A, B, C, D, dt=SS1.dt)
 
-
     else:
         raise NameError('Input types not recognised in any implemented option!')
 
     return SStot
 
-def join(SS_list,wv=None):
-	'''
-	Given a list of state-space models belonging to the ss class, creates a
-	joined system whose output is the sum of the state-space outputs. If wv is
-	not None, this is a list of weights, such that the output is:
 
-		y = sum( wv[ii] y_ii )
+def join(SS_list, wv=None):
+    """
+    Given a list of state-space models belonging to the StateSpace class, creates a
+    joined system whose output is the sum of the state-space outputs. If wv is
+    not None, this is a list of weights, such that the output is:
+
+        y = sum( wv[ii] y_ii )
 
-	Ref: equation (4.22) of
-	Benner, P., Gugercin, S. & Willcox, K., 2015. A Survey of Projection-Based
-	Model Reduction Methods for Parametric Dynamical Systems. SIAM Review, 57(4),
-	pp.483–531.
+    Ref: equation (4.22) of
+    Benner, P., Gugercin, S. & Willcox, K., 2015. A Survey of Projection-Based
+    Model Reduction Methods for Parametric Dynamical Systems. SIAM Review, 57(4),
+    pp.483–531.
+
+    Warnings:
+        - system matrices must be numpy arrays
+        - the function does not perform any check!
+    """
 
-	Warning:
-	- system matrices must be numpy arrays
-	- the function does not perform any check!
-	'''
+    N = len(SS_list)
+    if wv is not None:
+        assert N == len(wv), "'weights input should have'"
 
-	N = len(SS_list)
-	if wv is not None:
-		assert N==len(wv), "'weights input should have'"
+    A = scalg.block_diag(*[getattr(ss, 'A') for ss in SS_list])
+    B = np.block([[getattr(ss, 'B')] for ss in SS_list])
 
-	A = scalg.block_diag(*[ getattr(ss,'A') for ss in SS_list ])
-	B = np.block([ [getattr(ss,'B')] for ss in SS_list ])
+    if wv is None:
+        C = np.block([getattr(ss, 'C') for ss in SS_list])
+    else:
+        C = np.block([ww * getattr(ss, 'C') for ww, ss in zip(wv, SS_list)])
 
-	if wv is None:
-		C = np.block( [ getattr(ss,'C') for ss in SS_list ] )
-	else:
-		C = np.block( [ ww*getattr(ss,'C') for ww,ss in zip(wv,SS_list) ] )
+    D = np.zeros_like(SS_list[0].D)
+    for ii in range(N):
+        if wv is None:
+            D += SS_list[ii].D
+        else:
+            D += wv[ii] * SS_list[ii].D
 
-	D=np.zeros_like(SS_list[0].D)
-	for ii in range(N):
-		if wv is None:
-			D += SS_list[ii].D
-		else:
-			D += wv[ii]*SS_list[ii].D
+    return StateSpace(A, B, C, D, SS_list[0].dt)
 
-	return ss(A,B,C,D,SS_list[0].dt)
 
 def sum_ss(SS1, SS2, negative=False):
     """
@@ -1358,7 +1743,9 @@ def scale_SS(SSin, input_scal=1., output_scal=1., state_scal=1., byref=True):
 def simulate(SShere, U, x0=None):
     """
     Routine to simulate response to generic input.
-    @warning: this routine is for testing and may lack of robustness. Use
+
+    Warnings:
+        This routine is for testing and may lack of robustness. Use
         scipy.signal instead.
     """
 
@@ -1596,37 +1983,53 @@ def eigvals(a, dlti=False):
 
     return eigs[order]
 
+
 # --------------------------------------------------------------------- Testing
 
 
 def random_ss(Nx, Nu, Ny, dt=None, use_sparse=False, stable=True):
     """
     Define random system from number of states (Nx), inputs (Nu) and output (Ny).
+
+    Args:
+        Nx (int): Number of states
+        Nu (int): Number of inputs
+        Ny (int): Number of outputs
+        dt (float (optional)): Time step for discrete systems
+        use_sparse (bool): Use sparse matrices
+        stable (bool): Ensure the system is stable
+
+    Returns:
+        StateSpace: State space object
     """
 
     A = np.random.rand(Nx, Nx)
     if stable:
-        ev,U=np.linalg.eig(A)
-        evabs=np.abs(ev)
+        ev, U = np.linalg.eig(A)
+        evabs = np.abs(ev)
 
         for ee in range(len(ev)):
-            if evabs[ee]>0.99:
-                ev[ee]/=1.1*evabs[ee]
-        A = np.dot(U*ev, np.linalg.inv(U) ).real
+            if evabs[ee] > 0.99:
+                ev[ee] /= 1.1 * evabs[ee]
+        A = np.dot(U * ev, np.linalg.inv(U)).real
     B = np.random.rand(Nx, Nu)
     C = np.random.rand(Ny, Nx)
     D = np.random.rand(Ny, Nu)
 
     if use_sparse:
-        SS = ss(libsp.csc_matrix(A),
-                libsp.csc_matrix(B),
-                libsp.csc_matrix(C),
-                libsp.csc_matrix(D),
-                dt=dt)
+        ss = StateSpace(libsp.csc_matrix(A),
+                        libsp.csc_matrix(B),
+                        libsp.csc_matrix(C),
+                        libsp.csc_matrix(D),
+                        dt=dt)
     else:
-        SS = ss(A, B, C, D, dt=dt)
+        ss = StateSpace(A, B, C, D, dt=dt)
 
-    return SS
+    ss.initialise_variables(({'name': 'input_variable', 'size': Nu}), var_type='in')
+    ss.initialise_variables(({'name': 'output_variable', 'size': Ny}), var_type='out')
+    ss.initialise_variables(({'name': 'state_variable', 'size': Nx}), var_type='state')
+
+    return ss
 
 
 def compare_ss(SS1, SS2, tol=1e-10, Print=False):
@@ -1658,171 +2061,12 @@ def compare_ss(SS1, SS2, tol=1e-10, Print=False):
 # -----------------------------------------------------------------------------
 
 
-if __name__ == '__main__':
-    import unittest
-
-
-    class Test_dlti(unittest.TestCase):
-        """ Test methods into this module for DLTI systems """
-
-        def setUp(self):
-            # allocate some state-space model (dense and sparse)
-            dt = 0.3
-            Ny, Nx, Nu = 4, 3, 2
-            A = np.random.rand(Nx, Nx)
-            B = np.random.rand(Nx, Nu)
-            C = np.random.rand(Ny, Nx)
-            D = np.random.rand(Ny, Nu)
-            self.SS = ss(A, B, C, D, dt=dt)
-            self.SSsp = ss(libsp.csc_matrix(A), libsp.csc_matrix(B), C, D, dt=dt)
-
-        def test_SSconv(self):
-
-            SS = self.SS
-            SSsp = self.SSsp
-            Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
-            A, B, C, D = SS.get_mats()
-
-            # remove predictor: try different scenario
-            B1 = np.random.rand(Nx, Nu)
-            SSpr0 = ss(*SSconv(A, B, B1, C, D), dt=0.3)
-            SSpr1 = ss(*SSconv(A, B, libsp.csc_matrix(B1), C, D), dt=0.3)
-            SSpr2 = ss(*SSconv(
-                libsp.csc_matrix(A), B, libsp.csc_matrix(B1), C, D), dt=0.3)
-            SSpr3 = ss(*SSconv(
-                libsp.csc_matrix(A), libsp.csc_matrix(B), B1, C, D), dt=0.3)
-            SSpr4 = ss(*SSconv(
-                libsp.csc_matrix(A), libsp.csc_matrix(B), libsp.csc_matrix(B1), C, D), dt=0.3)
-            compare_ss(SSpr0, SSpr1)
-            compare_ss(SSpr0, SSpr2)
-            compare_ss(SSpr0, SSpr3)
-            compare_ss(SSpr0, SSpr4)
-
-        def test_scale_SS(self):
-
-            SS = self.SS
-            SSsp = self.SSsp
-            Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
-
-            # scale (hard-copy)
-            insc = np.random.rand(Nu)
-            stsc = np.random.rand(Nx)
-            outsc = np.random.rand(Ny)
-            SSadim = scale_SS(SS, insc, outsc, stsc, byref=False)
-            SSadim_sp = scale_SS(SSsp, insc, outsc, stsc, byref=False)
-            compare_ss(SSadim, SSadim_sp)
-
-            # scale (by reference)
-            SS.scale(insc, outsc, stsc)
-            SSsp.scale(insc, outsc, stsc)
-            compare_ss(SS, SSsp)
-
-        def test_addGain(self):
-
-            SS = self.SS
-            SSsp = self.SSsp
-            Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
-
-            # add gains
-            Kin = np.random.rand(Nu, 5)
-            Kout = np.random.rand(4, Ny)
-            SS.addGain(Kin, 'in')
-            SS.addGain(Kout, 'out')
-            SSsp.addGain(Kin, 'in')
-            SSsp.addGain(Kout, 'out')
-            compare_ss(SS, SSsp)
-
-        def test_freqresp(self):
-            # freq response: try different scenario
-
-            SS = self.SS
-            SSsp = self.SSsp
-            Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
-
-            kv = np.linspace(0, 1, 8)
-            Y = SS.freqresp(kv)
-            Ysp = SSsp.freqresp(kv)
-            er = np.max(np.abs(Y - Ysp))
-            assert er < 1e-10, 'Test on freqresp failed'
-
-            SS.D = libsp.csc_matrix(SS.D)
-            Y1 = SS.freqresp(kv)
-            er = np.max(np.abs(Y - Y1))
-            assert er < 1e-10, 'Test on freqresp failed'
-
-        def test_couple(self):
-            dt = .2
-            Nx1, Nu1, Ny1 = 3, 4, 2
-            Nx2, Nu2, Ny2 = 4, 3, 2
-            K12 = np.random.rand(Nu1, Ny2)
-            K21 = np.random.rand(Nu2, Ny1)
-            SS1 = random_ss(Nx1, Nu1, Ny1, dt=.2)
-            SS2 = random_ss(Nx2, Nu2, Ny2, dt=.2)
-
-            SS1sp = ss(libsp.csc_matrix(SS1.A),
-                       libsp.csc_matrix(SS1.B),
-                       libsp.csc_matrix(SS1.C),
-                       libsp.csc_matrix(SS1.D), dt=dt)
-            SS2sp = ss(libsp.csc_matrix(SS2.A),
-                       libsp.csc_matrix(SS2.B),
-                       libsp.csc_matrix(SS2.C),
-                       libsp.csc_matrix(SS2.D), dt=dt)
-            K12sp = libsp.csc_matrix(K12)
-            K21sp = libsp.csc_matrix(K21)
-
-            # SCref=couple_full(SS1,SS2,K12,K21)
-            SC0 = couple(SS1, SS2, K12, K21)
-            # compare_ss(SCref,SC0)
-            for SSa in [SS1, SS1sp]:
-                for SSb in [SS2, SS2sp]:
-                    for k12 in [K12, K12sp]:
-                        for k21 in [K21, K21sp]:
-                            SChere = couple(SSa, SSb, k12, k21)
-                            compare_ss(SC0, SChere)
-
-        def test_join(self):
-
-            Nx,Nu,Ny = 4, 3, 2
-            SS_list = [random_ss(Nx,Nu,Ny,dt=.2) for ii in range(3)]
-
-            wv = [.3, .5, .2]
-            SSjoin = join(SS_list,wv)
-
-            kv = np.array([0., 1., 3.])
-            Yjoin = SSjoin.freqresp(kv)
-
-            Yref = np.zeros_like(Yjoin)
-            for ii in range(3):
-            	Yref += wv[ii]*SS_list[ii].freqresp(kv)
-
-            er = np.max(np.abs(Yjoin - Yref))
-            assert er<1e-14, 'test_join error %.3e too large' %er
-
-        def test_disc2cont(self):
-            # not the best test given that eigenvalue comparison is not great with random systems. (error grows near
-            # nyquist frequency)
-
-            # this test is for execution purposes only.
-            sys = copy.deepcopy(self.SS)
-            self.SS.disc2cont()
-
-            ct_sys = disc2cont(sys)
-
-
-
-    outprint = 'Testing libss'
-    print('\n' + 70 * '-')
-    print((70 - len(outprint)) * ' ' + outprint)
-    print(70 * '-')
-    unittest.main()
-
-
 def ss_to_scipy(ss):
     """
     Converts to a scipy.signal linear time invariant system
 
     Args:
-        ss (libss.ss): SHARPy state space object
+        ss (libss.StateSpace): SHARPy state space object
 
     Returns:
         scipy.signal.dlti
@@ -1834,43 +2078,3 @@ def ss_to_scipy(ss):
         sys = scsig.dlti(ss.A, ss.B, ss.C, ss.D, dt=ss.dt)
 
     return sys
-
-# 1/0
-
-# # check parallel connector
-# Nout=2
-# Nin01,Nin02=2,3
-# Nst01,Nst02=4,2
-
-# # build random systems
-# fac=0.1
-# A01,A02=fac*np.random.rand(Nst01,Nst01),fac*np.random.rand(Nst02,Nst02)
-# B01,B02=np.random.rand(Nst01,Nin01),np.random.rand(Nst02,Nin02)
-# C01,C02=np.random.rand(Nout,Nst01),np.random.rand(Nout,Nst02)
-# D01,D02=np.random.rand(Nout,Nin01),np.random.rand(Nout,Nin02)
-
-# dt=0.1
-# SS01=scsig.StateSpace( A01,B01,C01,D01,dt=dt )
-# SS02=scsig.StateSpace( A02,B02,C02,D02,dt=dt )
-
-# # simulate
-# NT=11
-# U01,U02=np.random.rand(NT,Nin01),np.random.rand(NT,Nin02)
-
-# # reference
-# Y01,X01=simulate(SS01,U01)
-# Y02,X02=simulate(SS02,U02)
-# Yref=Y01+Y02
-
-# # parallel
-# SStot=parallel(SS01,SS02)
-# Utot=np.block([U01,U02])
-# Ytot,Xtot=simulate(SStot,Utot)
-
-# # join method
-# SStot=join(SS01,SS02)
-# K=np.array([[1,2,3],[4,5,6]])
-# SStot=join(K,SS02)
-# SStot=join(SS02,K)
-# K2=np.array([[10,20,30],[40,50,60]]).T
-# Ktot=join(K,K2)
diff --git a/sharpy/linear/src/lingebm.py b/sharpy/linear/src/lingebm.py
index ea7d59022..ea1fc3338 100644
--- a/sharpy/linear/src/lingebm.py
+++ b/sharpy/linear/src/lingebm.py
@@ -14,6 +14,7 @@
 import sharpy.utils.cout_utils as cout
 import sharpy.structure.utils.modalutils as modalutils
 import warnings
+from sharpy.linear.utils.ss_interface import LinearVector, StateVariable, OutputVariable, InputVariable
 
 
 class FlexDynamic():
@@ -140,7 +141,7 @@ def __init__(self, tsinfo, structure=None, custom_settings=dict()):
         self.discr_method = self.settings['discr_method']
         self.newmark_damp = self.settings['newmark_damp']
         self.use_euler = self.settings['use_euler']
-        self.use_principal_axes = self.settings.get('rigid_modes_cg', False)  # this setting is inherited from the setting in Modal solver
+        self.use_principal_axes = self.settings.get('rigid_modes_ppal_axes', False)  # this setting is inherited from the setting in Modal solver
 
         ### set state-space variables
         self.SScont = None
@@ -160,6 +161,7 @@ def __init__(self, tsinfo, structure=None, custom_settings=dict()):
 
         if self.Mstr.shape[0] == 6*(self.tsstruct0.num_node - 1):
             self.clamped = True
+            self.num_dof_rig = 0
         else:
             self.clamped = False
 
@@ -168,7 +170,8 @@ def __init__(self, tsinfo, structure=None, custom_settings=dict()):
         else:
             self.num_dof_rig = 10
 
-        self.update_modal()
+        if self.modal:
+            self.update_modal()
 
         self.num_dof_flex = np.sum(structure.vdof >= 0)*6
 
@@ -900,21 +903,11 @@ def assemble(self, Nmodes=None):
         assert self.inout_coords in ['modes', 'nodes'], \
             'inout_coords=%s not implemented!' % self.inout_coords
 
-        # cond_mass_matrix = np.linalg.cond(self.Mstr)
-        # if np.log10(cond_mass_matrix) >= 10.:
-        #     warnings.warn('Mass matrix is poorly conditioned (Cond = 10^%f). Inverse may not be correct.'
-        #                   % np.log10(cond_mass_matrix), 3)
-        # else:
-        #     cout.cout_wrap('Mass matrix condition = %e' % cond_mass_matrix)
-
         dlti = self.dlti
         modal = self.modal
         num_dof = self.num_dof
         if Nmodes is None or Nmodes >= self.num_modes:
             Nmodes = self.num_modes
-        # else:
-        #     # Modal truncation
-        #     self.update_truncated_modes(Nmodes)
 
         if dlti:  # ---------------------------------- assemble discrete time
 
@@ -938,31 +931,51 @@ def assemble(self, Nmodes=None):
                         else:
                             Ccut = np.dot(Phi.T, np.dot(self.Cstr, Phi))
 
-                        # Ass, Bss, Css, Dss = newmark_ss(
-                        #     np.eye(Nmodes),
-                        #     Ccut,
-                        #     np.diag(self.freq_natural[:Nmodes] ** 2),
-                        #     self.dt,
-                        #     self.newmark_damp)
                         Ass, Bss, Css, Dss = newmark_ss(
-                            # Phi.T.dot(self.Mstr.dot(Phi)),
                             np.linalg.inv(np.dot(self.U[:, :Nmodes].T, np.dot(self.Mstr, self.U[:, :Nmodes]))),
-                            # np.eye(Nmodes),
                             Ccut,
                             np.dot(self.U[:, :Nmodes].T, np.dot(self.Kstr, self.U[:, :Nmodes])),
-                            # Phi.T.dot(self.Kstr.dot(Phi)),
-                            # np.diag(self.freq_natural[:Nmodes]**2),
                             self.dt,
                             self.newmark_damp)
-                        self.Kin = Phi.T
-                        self.Kout = sc.linalg.block_diag(*[Phi, Phi])
+
+                        self.Kin = libss.Gain(Phi.T)
+                        self.Kin.input_variables = LinearVector([InputVariable('forces_n',
+                                                                             size=self.Mstr.shape[0],
+                                                                             index=0)])
+                        self.Kin.output_variables = LinearVector([OutputVariable('Q',
+                                                                               size=Nmodes,
+                                                                               index=0)])
+
+                        self.Kout = libss.Gain(sc.linalg.block_diag(*[Phi, Phi]))
+                        self.Kout.input_variables = LinearVector([InputVariable('q', size=Nmodes, index=0),
+                                                                  InputVariable('q_dot', size=Nmodes, index=1)])
+                        output_variables = LinearVector([OutputVariable('eta', size=self.num_dof_flex, index=0),
+                                                         OutputVariable('eta_dot', size=self.num_dof_flex, index=1)])
+                        if not self.clamped:
+                            output_variables.add('beta_bar', size=self.num_dof_rig, index=0.5)
+                            output_variables.append('beta', size=self.num_dof_rig)
+
+                        self.Kout.output_variables = output_variables
+
                     else:
                         raise NameError(
                             'Newmark-beta discretisation not available ' \
                             'for projection on damped eigenvectors')
 
                     # build state-space model
-                    self.SSdisc = libss.ss(Ass, Bss, Css, Dss, dt=self.dt)
+                    self.SSdisc = libss.StateSpace(Ass, Bss, Css, Dss, dt=self.dt)
+                    input_variables = LinearVector([InputVariable('Q', size=Nmodes, index=0)])
+
+                    output_variables = LinearVector([OutputVariable('q', size=Nmodes, index=0),
+                                                     OutputVariable('q_dot', size=Nmodes, index=1)])
+
+                    state_variables = output_variables.transform(output_variables,
+                                                                 to_type=StateVariable)
+
+                    self.SSdisc.input_variables = input_variables
+                    self.SSdisc.output_variables = output_variables
+                    self.SSdisc.state_variables = state_variables
+
                     if self.inout_coords == 'nodes':
                         self.SSdisc = libss.addGain(self.SSdisc, self.Kin, 'in')
                         self.SSdisc = libss.addGain(self.SSdisc, self.Kout, 'out')
@@ -977,8 +990,21 @@ def assemble(self, Nmodes=None):
                         self.dt, self.newmark_damp)
                     self.Kin = None
                     self.Kout = None
-                    self.SSdisc = libss.ss(Ass, Bss, Css, Dss, dt=self.dt)
+                    self.SSdisc = libss.StateSpace(Ass, Bss, Css, Dss, dt=self.dt)
+
+                    input_variables = LinearVector([InputVariable('forces_n',
+                                                                           size=self.Mstr.shape[0],
+                                                                           index=0)])
+
+                    output_variables = LinearVector([OutputVariable('eta', size=self.num_dof_flex, index=0),
+                                                     OutputVariable('eta_dot', size=self.num_dof_flex, index=1)])
+                    if not self.clamped:
+                        output_variables.add('beta_bar', size=self.num_dof_rig, index=0.5)
+                        output_variables.append('beta', size=self.num_dof_rig)
 
+                    self.SSdisc.output_variables = output_variables
+                    self.SSdisc.input_variables = input_variables
+                    self.SSdisc.state_variables = LinearVector.transform(output_variables, to_type=StateVariable)
             else:
                 raise NameError(
                     'Discretisation method %s not available' % self.discr_method)
@@ -1002,8 +1028,24 @@ def assemble(self, Nmodes=None):
                     Bss = np.zeros((2 * Nmodes, Nmodes))
                     Dss = np.zeros((2 * Nmodes, Nmodes))
                     Bss[Nmodes + iivec, iivec] = 1.
-                    self.Kin = Phi.T
-                    self.Kout = sc.linalg.block_diag(*(Phi, Phi))
+                    self.Kin = libss.Gain(Phi.T)
+                    self.Kin.input_variables = LinearVector([InputVariable('forces_n',
+                                                                           size=self.Mstr.shape[0],
+                                                                           index=0)])
+                    self.Kin.output_variables = LinearVector([OutputVariable('Q',
+                                                                             size=Nmodes,
+                                                                             index=0)])
+                    self.Kout = libss.Gain(sc.linalg.block_diag(*[Phi, Phi]))
+                    self.Kout.input_variables = LinearVector([InputVariable('q', size=Nmodes, index=0),
+                                                              InputVariable('q_dot', size=Nmodes, index=1)])
+
+                    output_variables = LinearVector([OutputVariable('eta', size=self.num_dof_flex, index=0),
+                                                     OutputVariable('eta_dot', size=self.num_dof_flex, index=1)])
+                    if not self.clamped:
+                        output_variables.add('beta_bar', size=self.num_dof_rig, index=0.5)
+                        output_variables.append('beta', size=self.num_dof_rig)
+
+                    self.Kout.output_variables = output_variables
                 else:  # damped mode shapes
                     # The algorithm assumes that for each couple of complex conj
                     # eigenvalues, only one eigenvalue (and the eigenvectors
@@ -1023,7 +1065,18 @@ def assemble(self, Nmodes=None):
                     self.Kout = np.block([2. * U.real, (-2.) * U.imag])
 
                 # build state-space model
-                self.SScont = libss.ss(Ass, Bss, Css, Dss)
+                self.SScont = libss.StateSpace(Ass, Bss, Css, Dss)
+                input_variables = LinearVector([InputVariable('Q', size=Nmodes, index=0)])
+
+                output_variables = LinearVector([OutputVariable('q', size=Nmodes, index=0),
+                                                 OutputVariable('q_dot', size=Nmodes, index=1)])
+
+                state_variables = output_variables.transform(output_variables,
+                                                             to_type=StateVariable)
+
+                self.SScont.input_variables = input_variables
+                self.SScont.output_variables = output_variables
+                self.SScont.state_variables = state_variables
                 if self.inout_coords == 'nodes':
                     self.SScont = libss.addGain(self.SScont, self.Kin, 'in')
                     self.SScont = libss.addGain(self.SScont, self.Kout, 'out')
@@ -1045,7 +1098,22 @@ def assemble(self, Nmodes=None):
                 Bss[num_dof:, :] = -Minv_neg
                 self.Kin = None
                 self.Kout = None
-                self.SScont = libss.ss(Ass, Bss, Css, Dss)
+                self.SScont = libss.StateSpace(Ass, Bss, Css, Dss)
+
+                input_variables = LinearVector([InputVariable('forces_n',
+                                                              size=self.Mstr.shape[0],
+                                                              index=0)])
+
+                output_variables = LinearVector([OutputVariable('eta', size=self.num_dof_flex, index=0),
+                                                 OutputVariable('eta_dot', size=self.num_dof_flex, index=1)])
+                if not self.clamped:
+                    output_variables.add('beta_bar', size=self.num_dof_rig, index=0.5)
+                    output_variables.append('beta', size=self.num_dof_rig)
+
+                self.SScont.output_variables = output_variables
+                self.SScont.input_variables = input_variables
+                self.SScont.state_variables = LinearVector.transform(output_variables, to_type=StateVariable)
+
 
     def freqresp(self, wv=None, bode=True):
         """
@@ -1237,11 +1305,14 @@ def scale_system_normalised_time(self, time_ref):
         # if time_ref != 1.0 and time_ref is not None:
         if self.num_rig_dof != 0:
             warnings.warn('Time normalisation not yet implemented with rigid body motion.')
-        self.scaled_reference_matrices['dt'] = self.dt
-        self.dt /= time_ref
+
+        if self.dlti:
+            self.scaled_reference_matrices['dt'] = self.dt
+            self.dt /= time_ref
         if self.settings['print_info']:
             cout.cout_wrap('Scaling beam according to reduced time...', 0)
-            cout.cout_wrap('\tSetting the beam time step to (%.4f)' % self.dt, 1)
+            if self.dlti:
+                cout.cout_wrap('\tSetting the beam time step to (%.4f)' % self.dt, 1)
 
         self.scaled_reference_matrices['C'] = self.Cstr.copy()
         self.scaled_reference_matrices['K'] = self.Kstr.copy()
@@ -1273,13 +1344,13 @@ def cont2disc(self, dt=None):
             self.dt = dt
         else:
             assert self.dt is not None, \
-                'Provide time-step for convertion to discrete-time'
+                'Provide time-step for conversion to discrete-time'
 
         SScont = self.SScont
         tpl = scsig.cont2discrete(
             (SScont.A, SScont.B, SScont.C, SScont.D),
             dt=self.dt, method=self.discr_method)
-        self.SSdisc = libss.ss(*tpl[:-1], dt=tpl[-1])
+        self.SSdisc = libss.StateSpace(*tpl[:-1], dt=tpl[-1])
         self.dlti = True
 
 
@@ -1412,7 +1483,7 @@ def newmark_ss(Minv, C, K, dt, num_damp=1e-4):
     MinvK = np.dot(Minv, K)
     MinvC = np.dot(Minv, C)
 
-    # build ss
+    # build StateSpace
     Ass0 = np.block([[Imat - a0 * MinvK, dt * Imat - a0 * MinvC],
                      [-b0 * MinvK, Imat - b0 * MinvC]])
     Ass1 = np.block([[Imat + a1 * MinvK, a1 * MinvC],
diff --git a/sharpy/linear/src/linuvlm.py b/sharpy/linear/src/linuvlm.py
index e9e61aa63..69435ded7 100644
--- a/sharpy/linear/src/linuvlm.py
+++ b/sharpy/linear/src/linuvlm.py
@@ -35,6 +35,7 @@
 import sharpy.utils.cout_utils as cout
 import sharpy.utils.exceptions as exceptions
 from sharpy.utils.constants import vortex_radius_def
+from sharpy.linear.utils.ss_interface import LinearVector, StateVariable, InputVariable, OutputVariable
 
 settings_types_static = dict()
 settings_default_static = dict()
@@ -114,6 +115,15 @@ def __init__(self, tsdata, custom_settings=None, for_vel=np.zeros((6,))):
         self.zeta_dot = np.zeros((3 * self.Kzeta))
         self.u_ext = np.zeros((3 * self.Kzeta))
 
+        self.input_variables_list = [InputVariable('zeta', size=3 * self.Kzeta, index=0),
+                                     InputVariable('zeta_dot', size=3 * self.Kzeta, index=1),
+                                     InputVariable('u_gust', size=3 * self.Kzeta, index=2)]
+
+        self.state_variables_list = [StateVariable('gamma', size=self.K, index=0),
+                                     StateVariable('gamma_w', size=self.K_star, index=1),
+                                     StateVariable('gamma_m1', size=self.K, index=2)]
+
+        self.output_variables_list = [OutputVariable('forces_v', size=3 * self.Kzeta, index=0)]
 
         # profiling output
         self.prof_out = './asbly.prof'
@@ -619,6 +629,20 @@ def __init__(self, tsdata, dt=None, dynamic_settings=None, integr_order=2,
         ScalingFacts['force'] = ScalingFacts['dyn_pressure'] * ScalingFacts['length'] ** 2
         self.ScalingFacts = ScalingFacts
 
+        self.input_variables_list = [InputVariable('zeta', size=3 * self.Kzeta, index=0),
+                                     InputVariable('zeta_dot', size=3 * self.Kzeta, index=1),
+                                     InputVariable('u_gust', size=3 * self.Kzeta, index=2)]
+
+        self.state_variables_list = [StateVariable('gamma', size=self.K, index=0),
+                                     StateVariable('gamma_w', size=self.K_star, index=1),
+                                     StateVariable('dtgamma_dot', size=self.K, index=2),
+                                     StateVariable('gamma_m1', size=self.K, index=3)]
+
+        self.output_variables_list = [OutputVariable('forces_v', size=3 * self.Kzeta, index=0)]
+
+        if self.integr_order == 1:
+            self.state_variables_list.pop(2) # remove time derivative state
+
         ### collect statistics
         self.cpu_summary = {'dim': 0.,
                             'nondim': 0.,
@@ -935,7 +959,7 @@ def assemble_ss(self, wake_prop_settings=None):
         if self.remove_predictor:
             Ass, Bmod, Css, Dmod = \
                 libss.SSconv(Ass, None, Bss, Css, Dss, Bm1=None)
-            self.SS = libss.ss(Ass, Bmod, Css, Dmod, dt=self.dt)
+            self.SS = libss.StateSpace(Ass, Bmod, Css, Dmod, dt=self.dt)
 
             # Store original B matrix for state unpacking
             self.B_predictor = Bss
@@ -945,10 +969,15 @@ def assemble_ss(self, wake_prop_settings=None):
                            '\t\th_{n+1} = A h_{n} + B u_{n}\n\t' \
                            '\t\twith:\n\tx_n = h_n + Bp u_n', 1)
         else:
-            self.SS = libss.ss(Ass, Bss, Css, Dss, dt=self.dt)
+            self.SS = libss.StateSpace(Ass, Bss, Css, Dss, dt=self.dt)
             cout.cout_wrap('\tstate-space model produced in form:\n\t' \
                            'x_{n+1} = A x_{n} + Bp u_{n+1}', 1)
 
+        # add variable tracker
+        self.SS.input_variables = LinearVector(self.input_variables_list)
+        self.SS.state_variables = LinearVector(self.state_variables_list)
+        self.SS.output_variables = LinearVector(self.output_variables_list)
+
         self.cpu_summary['assemble'] = time.time() - t0
         cout.cout_wrap('\t\t\t...done in %.2f sec' % self.cpu_summary['assemble'])
 
@@ -1356,7 +1385,7 @@ def balfreq(self, DictBalFreq, wake_prop_settings=None):
         Ab = libsp.dot(Ti, libsp.dot(self.SS.A, T))
         Bb = libsp.dot(Ti, self.SS.B)
         Cb = libsp.dot(self.SS.C, T)
-        SSb = libss.ss(Ab, Bb, Cb, self.SS.D, dt=self.SS.dt)
+        SSb = libss.StateSpace(Ab, Bb, Cb, self.SS.D, dt=self.SS.dt)
 
         ### Eliminate unstable modes - if any:
         if DictBalFreq['check_stability']:
@@ -2042,6 +2071,11 @@ def assemble_ss(self, wake_prop_settings=None):
         cout.cout_wrap('\tstate-space model produced in form:\n\t' \
                        'x_{n+1} = A x_{n} + Bp u_{n+1}', 1)
 
+        # add variable tracker
+        self.SS.input_variables = LinearVector(self.input_variables_list)
+        self.SS.state_variables = LinearVector(self.state_variables_list)
+        self.SS.output_variables = LinearVector(self.output_variables_list)
+
         self.cpu_summary['assemble'] = time.time() - t0
         cout.cout_wrap('\t\t\t...done in %.2f sec' % self.cpu_summary['assemble'], 1)
 
@@ -2389,10 +2423,10 @@ def balfreq(self, DictBalFreq, wake_prop_settings=None):
             Ab, Bb, Cb, Db = \
                 libss.SSconv(np.block(Ab), None, np.block(Bb),
                              np.block(Cb), np.block(self.SS.D), Bm1=None)
-            SSb = libss.ss(Ab, Bb, Cb, Db, dt=self.dt)
+            SSb = libss.StateSpace(Ab, Bb, Cb, Db, dt=self.dt)
         else:
-            SSb = libss.ss(np.block(Ab), np.block(Bb),
-                           np.block(Cb), np.block(self.SS.D), dt=self.SS.dt)
+            SSb = libss.StateSpace(np.block(Ab), np.block(Bb),
+                                   np.block(Cb), np.block(self.SS.D), dt=self.SS.dt)
 
         ### Eliminate unstable modes - if any:
         if DictBalFreq['check_stability']:
diff --git a/sharpy/linear/utils/derivatives.py b/sharpy/linear/utils/derivatives.py
new file mode 100644
index 000000000..f4eed49d2
--- /dev/null
+++ b/sharpy/linear/utils/derivatives.py
@@ -0,0 +1,390 @@
+import h5py
+import numpy as np
+
+from sharpy.utils import algebra as algebra, cout_utils as cout
+
+
+class Derivatives:
+    """
+    Class containing the derivatives set for a given state-space system (i.e. aeroelastic or aerodynamic)
+    """
+    def __init__(self, reference_dimensions, static_state, target_system=None):
+
+        self.target_system = target_system  # type: str # name of target system (aerodynamic/aeroelastic)
+        self.transfer_function = None  # type: np.array # matrix of steady-state TF for target system
+
+        self.static_state = static_state  # type: tuple # [fx, fy, fz] at ref state
+        self.reference_dimensions = reference_dimensions  # type: dict # name: ref_dimension_value dictionary
+
+        self.separator = '\n' + 80 * '#' + '\n'
+
+        self.dict_of_derivatives = {}  # type: dict # {name:DerivativeSet} Each of the derivative sets DerivativeSet
+
+        s_ref = self.reference_dimensions['S_ref']
+        b_ref = self.reference_dimensions['b_ref']
+        c_ref = self.reference_dimensions['c_ref']
+        u_inf = self.reference_dimensions['u_inf']
+        rho = self.reference_dimensions['rho']
+        self.dynamic_pressure = 0.5 * rho * u_inf ** 2
+
+        self.coefficients = {'force': self.dynamic_pressure * s_ref,
+                             'moment_lon': self.dynamic_pressure * s_ref * c_ref,
+                             'moment_lat': self.dynamic_pressure * s_ref * b_ref,
+                             'force_angular_vel': self.dynamic_pressure * s_ref * c_ref / u_inf,
+                             'moment_lon_angular_vel': self.dynamic_pressure * s_ref * c_ref * c_ref / u_inf}  # missing rates
+
+        self.steady_coefficients = np.array(self.static_state) / self.coefficients['force']
+
+        self.filename = 'stability_derivatives.txt'
+        if target_system is not None:
+            self.filename = target_system + '_' + self.filename
+
+        self.cg = None
+
+    def initialise_derivatives(self, state_space, steady_forces, quat, v0, phi=None, cg=None, tpa=None):
+        """
+        Initialises the required class attributes for all derivative calculations/
+
+        Args:
+            state_space (sharpy.linear.src.libss.StateSpace): State-space object for the target system
+            steady_forces (np.array): Array of steady forces (at the linearisation) expressed in the beam degrees of
+              freedom and with size equal to the number of structural degrees of freedom
+            quat (np.array): Quaternion at the linearisation reference state
+            v0 (np.array): Free stream velocity vector at the linearisation condition
+            phi (np.array (optional)): Mode shape matrix for modal systems
+            tpa (np.array (optional)): Transformation matrix onto principal axes
+
+        """
+        cls = DerivativeSet  # explain what is the DerivativeSet class
+        if cls.quat is None:
+            cls.quat = quat
+            cls.cga = algebra.quat2rotation(cls.quat)
+            cls.v0 = v0
+            cls.coefficients = self.coefficients
+
+            if phi is not None:
+                cls.modal = True
+                cls.phi = phi[-9:-3, :6]
+                cls.inv_phi_forces = np.linalg.inv(phi[-9:-3, :6].T)
+                cls.inv_phi_vel = np.linalg.inv(phi[-9:-3, :6])
+            else:
+                cls.modal = False
+        cls.steady_forces = steady_forces
+
+        H0 = state_space.freqresp(np.array([1e-5]))[:, :, 0].real
+        if cls.modal:
+            vel_inputs_variables = state_space.input_variables.get_variable_from_name('q_dot')
+            output_indices = state_space.output_variables.get_variable_from_name('Q').rows_loc[:6]
+            cls.steady_forces = cls.inv_phi_forces.dot(cls.steady_forces[output_indices])
+        else:
+            vel_inputs_variables = state_space.input_variables.get_variable_from_name('beta')
+            output_indices = state_space.output_variables.get_variable_from_name('forces_n').rows_loc[-9:-3]
+            cls.steady_forces = cls.steady_forces[output_indices]
+        rbm_indices = vel_inputs_variables.cols_loc[:9]
+
+        # look for control surfaces
+        try:
+            cs_input_variables = state_space.input_variables.get_variable_from_name('control_surface_deflection')
+            dot_cs_input_variables = state_space.input_variables.get_variable_from_name('dot_control_surface_deflection')
+        except ValueError:
+            cs_indices = np.array([], dtype=int)
+            dot_cs_indices = np.array([], dtype=int)
+            cls.n_control_surfaces = 0
+        else:
+            cs_indices = cs_input_variables.cols_loc
+            dot_cs_indices = dot_cs_input_variables.cols_loc
+            cls.n_control_surfaces = cs_input_variables.size
+        finally:
+            input_indices = np.concatenate((rbm_indices, cs_indices, dot_cs_indices))
+
+        self.transfer_function = H0[np.ix_(output_indices, input_indices)].real
+
+        self.cg = cg
+        self.tpa = tpa
+
+    def save(self, output_route):
+        with h5py.File(output_route + '/' + self.filename.replace('.txt', '.h5'), 'w') as f:
+            for k, v in self.dict_of_derivatives.items():
+                if v.matrix is None:
+                    continue
+                f.create_dataset(name=k, data=v.matrix)
+
+    def savetxt(self, folder):
+
+        filename = self.filename
+
+        u_inf = self.reference_dimensions['u_inf']
+        s_ref = self.reference_dimensions['S_ref']
+        b_ref = self.reference_dimensions['b_ref']
+        c_ref = self.reference_dimensions['c_ref']
+        rho = self.reference_dimensions['rho']
+        quat = self.reference_dimensions['quat']
+        euler_orient = algebra.quat2euler(quat) * 180/np.pi
+
+        labels_out = ['CD', 'CY', 'CL', 'Cl', 'Cm', 'Cn']
+
+        separator = '\n' + 80*'#' + '\n'
+
+        with open(folder + '/' + filename, mode='w') as outfile:
+            outfile.write('SHARPy Stability Derivatives Analysis\n')
+
+            outfile.write('State:\n')
+            outfile.write('\t{:4f}\t\t\t # Free stream velocity\n'.format(u_inf))
+            outfile.write('\t{:4f}\t\t\t # Free stream density\n'.format(rho))
+            outfile.write('\t{:4f}\t\t\t # Alpha [deg]\n'.format(euler_orient[1]))
+            outfile.write('\t{:4f}\t\t\t # Beta [deg]\n'.format(euler_orient[2]))
+
+            if self.cg is not None:
+                outfile.write(separator)
+                outfile.write('Centre of Gravity:\n')
+                lab = ('x', 'y', 'z')
+                for i in range(3):
+                    outfile.write('\t{:s}_A = {:.4f}\t\t\t # [m]\n'.format(lab[i], self.cg[i]))
+
+                if self.tpa is not None:
+                    outfile.write('Principal Axes Directions (expressed in the A frame):\n')
+                    for i in range(3):
+                        outfile.write('\t{:s}_ppal in A = [{:.4f}, {:.4f}, {:.4f}]\t\t\t\n'.format(
+                            lab[i], *self.tpa.dot(np.eye(3)[:, i])))
+
+            outfile.write(separator)
+            outfile.write('\nReference Dimensions:\n')
+            outfile.write('\t{:4f}\t\t\t # Reference planform area\n'.format(s_ref))
+            outfile.write('\t{:4f}\t\t\t # Reference chord\n'.format(c_ref))
+            outfile.write('\t{:4f}\t\t\t # Reference span\n'.format(b_ref))
+
+            outfile.write(separator)
+            outfile.write('\nCoefficients:\n')
+            for ith, coeff in enumerate(self.steady_coefficients):
+                outfile.write('\t{:4e}\t\t\t # {:s}\n'.format(coeff,  labels_out[ith]))
+
+            outfile.write(separator)
+
+        # this needs to be out of with open as it is done in each of the Derivatives objects
+        for derivative_set in self.dict_of_derivatives.values():
+            if derivative_set.matrix is None:
+                continue
+            derivative_set.print(derivative_filename=folder + '/' + filename)
+
+    def new_derivative(self, frame_of_reference, derivative_calculation=None, name=None):
+        """
+        Returns a DerivativeSet() instance with the appropriate transfer function included
+        for the relevant target system.
+
+        Args:
+            frame_of_reference (str): Output frame of reference. Body or Stability. (Not Yet Implemented)
+            derivative_calculation (str): Name of function used to create derivative set
+            name (str (optional)): Optional custom name to use as title in output.
+
+        Returns:
+            DerivativeSet: Instance of class with the relevant transfer function for the current
+              target system.
+        """
+        new_derivative = DerivativeSet(frame_of_reference, derivative_calculation,
+                                       name,
+                                       transfer_function=self.transfer_function)
+
+        return new_derivative
+
+
+class DerivativeSet:
+    """
+    Class containing the stability derivative set for each of the input/output combinations. A derivative set may be
+    force/angle or force/control_surface, for example.
+
+    The class attributes contain the parameters common across all derivative sets. The instance attributes those
+    pertaining to the specific set.
+    """
+    steady_forces = None
+    coefficients = None
+    quat = None
+
+    cga = None
+    n_control_surfaces = None
+
+    v0 = None
+
+    # Modal cases
+    modal = None
+    phi = None
+    inv_phi_forces = None
+    inv_phi_vel = None
+
+    def __init__(self, frame_of_reference, derivative_calculation=None, name=None,
+                 transfer_function=None):
+        """
+
+        Args:
+            frame_of_reference (str): Name of the frame of reference (stability or body axes)
+            derivative_calculation (str): Name of the method to compute derivatives
+            name (str): Name of the derivative set
+            transfer_function (np.array): steady state transfer function for the desired input output channels
+        """
+
+        self.transfer_function = transfer_function  # type: np.array # steady-state TF for the specific out/in
+        self.matrix = None  # type: np.array # matrix of stability derivatives
+        self.labels_in = []  # type: list(str) # strings describing the input channels
+        self.labels_out = []  # type list(str) # strings describing the output channels
+        self.frame_of_reference = frame_of_reference  # type: str # name of the FoR (stability or body axes)
+
+        self.table = None  # type: cout.TablePrinter
+        self.name = name  # type: str # name of the set
+
+        # TODO: remove in clean up and make derivative_calculation a position argument
+        if derivative_calculation is not None:
+            self.__getattribute__(derivative_calculation)()
+
+    def print(self, derivative_filename=None):
+        if self.name is not None:
+            cout.cout_wrap(self.name)
+            with open(derivative_filename, 'a') as f:
+                f.write('Derivative set: {:s}\n'.format(self.name))
+                f.write('Axes {:s}\n'.format(self.frame_of_reference))
+        self.table = cout.TablePrinter(n_fields=len(self.labels_in)+1,
+                                       field_types=['s']+len(self.labels_in) * ['e'], filename=derivative_filename)
+        self.table.print_header(field_names=list(['der'] + self.labels_in))
+        for i in range(len(self.labels_out)):
+            out_list = [self.labels_out[i]] + list(self.matrix[i, :])
+            self.table.print_line(out_list)
+        self.table.print_divider_line()
+        self.table.character_return(n_lines=2)
+
+    def save(self, derivative_name, output_name):
+        with h5py.File(output_name + '.stability.h5', 'w') as f:
+            f.create_dataset(derivative_name, data=self.matrix)
+
+    def angle_derivatives(self):
+        r"""
+        Stability derivatives against aerodynamic angles (angle of attack and sideslip) expressed in stability axes, i.e
+        forces are lift, drag...
+
+        Linearised forces in stability axes are expressed as
+
+        .. math::
+            F^S = F_0^S + \frac{\partial}{\partial \alpha}\left(C^{GA}(\alpha)F_0^A\right)\delta\alpha + C_0^{GA}\delta F^A
+
+        Therefore, the stability derivative becomes
+
+        .. math:: \frac{\partial\F^S}{\partial\alpha} =\frac{\partial}{\partial \alpha}\left(C^{GA}(\alpha)F_0^A\right) +
+           C_0^{GA}\frac{\partial F^A}{\partial\alpha}
+
+        where
+
+        .. math:: \frac{\partial F^A}{\partial\alpha} = \frac{\partial F^A}{\partial v^A}\frac{\partial v^A}{\partial\alpha}
+
+        and
+
+        .. math:: \frac{\partial v^A}{\partial\alpha} = C^{AG}\frac{\partial}{\partial\alpha}\left(C(0)V_0^G\right).
+
+        The term :math:`\frac{\partial F^A}{\partial v^A}` is obtained directly from the steady state transfer
+        function of the linear UVLM expressed in the beam degrees of freedoms.
+
+        """
+        self.labels_in = ['phi', 'alpha', 'beta']
+        self.labels_out = ['CD', 'CY', 'CL', 'Cl', 'Cm', 'Cn']
+        self.matrix = np.zeros((6, 3))
+
+        # Get free stream velocity direction
+        v0 = self.v0
+
+        f0a = self.steady_forces[:3]
+        m0a = self.steady_forces[-3:]
+
+        euler0 = algebra.quat2euler(self.quat)
+        cga = self.cga
+
+        # first term in the stability derivative expression
+        stab_der_trans = algebra.der_Ceuler_by_v(euler0, f0a)
+        stab_der_mom = algebra.der_Ceuler_by_v(euler0, m0a)
+
+        # second term in the stability derivative expression
+        if self.modal:
+            delta_nodal_vel = np.linalg.inv(self.phi[:3, :3]).dot(cga.T.dot(algebra.der_Peuler_by_v(euler0 * 0, v0)))
+            delta_nodal_forces = self.inv_phi_forces.dot(self.transfer_function[:6, :3].real.dot(delta_nodal_vel))
+        else:
+            delta_nodal_vel = cga.T.dot(algebra.der_Peuler_by_v(euler0 * 0, v0))
+            delta_nodal_forces = self.transfer_function[:6, :3].real.dot(delta_nodal_vel)
+
+        stab_der_trans2 = cga.dot(delta_nodal_forces[:3, :])
+        stab_der_mom2 = cga.dot(delta_nodal_forces[3:, :])
+
+        self.matrix[:3, :] = stab_der_trans + stab_der_trans2
+        self.matrix[3:6, :] = stab_der_mom + stab_der_mom2
+
+        self.apply_coefficients()
+
+    def angle_derivatives_tb(self):
+        self.name = 'Force/Angle derivatives via angle input'
+        self.labels_in = ['phi', 'alpha', 'beta']
+        self.labels_out = ['CD', 'CY', 'CL', 'Cl', 'Cm', 'Cn']
+        self.matrix = np.zeros((6, 3))
+
+        # Get free stream velocity direction
+        v0 = self.v0
+
+        f0a = self.steady_forces[:3]
+        m0a = self.steady_forces[-3:]
+
+        euler0 = algebra.quat2euler(self.quat)
+        cga = self.cga
+
+        # first term in the stability derivative expression
+        stab_der_trans = algebra.der_Ceuler_by_v(euler0, f0a)
+        stab_der_mom = algebra.der_Ceuler_by_v(euler0, m0a)
+
+        # second term in the stability derivative expression
+        if self.modal:
+            delta_nodal_forces = self.inv_phi_forces.dot(self.transfer_function[:6, 6:9].real)
+        else:
+            delta_nodal_forces = self.transfer_function[:6, 6:9].real
+
+        stab_der_trans2 = cga.dot(delta_nodal_forces[:3, :])
+        stab_der_mom2 = cga.dot(delta_nodal_forces[3:, :])
+
+        self.matrix[:3, :] = stab_der_trans + stab_der_trans2
+        self.matrix[3:6, :] = stab_der_mom + stab_der_mom2
+
+        self.apply_coefficients()
+
+    def body_derivatives(self):
+        self.name = 'Force derivatives to rigid body velocities - Body derivatives'
+        self.labels_in = ['uA', 'vA', 'wA', 'pA', 'qA', 'rA']
+        self.labels_out = ['C_XA', 'C_YA', 'C_ZA', 'C_LA', 'C_MA', 'C_NA']
+        self.matrix = np.zeros((6, 6))
+
+        body_derivatives = self.transfer_function[:6, :6]
+
+        if self.modal:
+            body_derivatives = self.inv_phi_forces.dot(body_derivatives).dot(self.inv_phi_vel)
+
+        self.matrix = body_derivatives
+        self.apply_coefficients()
+
+    def control_surface_derivatives(self):
+        n_control_surfaces = self.n_control_surfaces
+        if n_control_surfaces == 0:
+            return None
+
+        self.name = 'Force derivatives wrt control surface inputs - Body axes'
+        self.labels_out = ['C_XA', 'C_YA', 'C_ZA', 'C_LA', 'C_MA', 'C_NA']
+        labels_in_deflection = []
+        labels_in_rate = []
+        for i in range(n_control_surfaces):
+            labels_in_deflection.append('delta_{:g}'.format(i))
+            labels_in_rate.append('dot(delta)_{:g}'.format(i))
+        self.labels_in = labels_in_deflection + labels_in_rate
+
+        body_derivatives = self.transfer_function[:6, 9:]
+        assert body_derivatives.shape == (6, 2 * self.n_control_surfaces), 'Incorrect TF shape'
+
+        if self.modal:
+            self.matrix = self.inv_phi_forces.dot(body_derivatives)
+        else:
+            self.matrix = body_derivatives
+
+        self.apply_coefficients()
+
+    def apply_coefficients(self):
+        self.matrix[:3, :] /= self.coefficients['force']
+        self.matrix[np.ix_([3, 5]), :] /= self.coefficients['moment_lat']
+        self.matrix[4, :] /= self.coefficients['moment_lon']
diff --git a/sharpy/linear/utils/ss_interface.py b/sharpy/linear/utils/ss_interface.py
index 1b21ff7e4..2f6252715 100644
--- a/sharpy/linear/utils/ss_interface.py
+++ b/sharpy/linear/utils/ss_interface.py
@@ -1,8 +1,12 @@
 """State-space modules loading utilities"""
 import os
+
+import h5py
+
 import sharpy.utils.cout_utils as cout
 from abc import ABCMeta, abstractmethod
 import numpy as np
+import copy
 
 dict_of_systems = dict()
 systems_dict_import = dict()
@@ -85,95 +89,427 @@ def dictionary_of_systems():
     return dictionary
 
 
-class VectorVariable(object):
-
-    def __init__(self, name, pos_list, var_system):
+class VectorVariable:
+    # state variable
+    def __init__(self, name, size, index, var_system=None):
 
         self.name = name
         self.var_system = var_system
+        self.size = size
+        self._index = index
 
-        self.first_pos = pos_list[0]
-        self.end_pos = pos_list[1]
-        self.rows_loc = np.arange(self.first_pos, self.end_pos, dtype=int) # Original location, should not update
+        self._first_position = 0
 
+        self._rows_loc = None
+        self._cols_loc = None
+
+    @property
+    def cols_loc(self):
+        return np.arange(self.first_position, self.end_position, dtype=int)
+
+    @property
+    def index(self):
+        return self._index
+
+    @index.setter
+    def index(self, value):
+        self._index = value
+
+    @property
+    def first_position(self):
+        return self._first_position
 
-    # add methods to reorganise into SHARPy method?
+    @first_position.setter
+    def first_position(self, position):
+        self._first_position = position
+
+    @property
+    def end_position(self):
+        return self.first_position + self.size
+
+    @property
+    def rows_loc(self):
+        return np.arange(self.first_position, self.end_position, dtype=int)
+
+    def __repr__(self):
+        return '({:s}: {:s}, size: {:g}, index: {:g}, starting at: {:g}, finishing at: {:g})'.format(
+            type(self).__name__,
+            self.name,
+            self.size,
+            self.index,
+            self.first_position,
+            self.end_position)
+
+    def copy(self):
+        return copy.deepcopy(self)
+
+    def save(self):
+        """
+        Return:
+            tuple: info to save to h5
+        """
+        return type(self).__name__, self.name, self.size, self.index
+
+
+class OutputVariable(VectorVariable):
 
     @property
     def cols_loc(self):
-        return np.arange(self.first_pos, self.end_pos, dtype=int)
+        return self._cols_loc
+
+    @cols_loc.setter
+    def cols_loc(self, value=None):
+        if self._cols_loc is None:
+            self._cols_loc = np.arange(self.first_position, self.end_position, dtype=int) # Original location, should not update
+
+    @property
+    def rows_loc(self):
+        # In the output variables the rows can change, the columns should stay fixed
+        return np.arange(self.first_position, self.end_position, dtype=int)
+
+
+class InputVariable(VectorVariable):
+
+    @property
+    def rows_loc(self):
+        return self._rows_loc
+
+    @rows_loc.setter
+    def rows_loc(self, value=None):
+        if self._rows_loc is None:
+            self._rows_loc = np.arange(self.first_position, self.end_position, dtype=int) # Original location, should not update
+
+    @property
+    def cols_loc(self):
+        return np.arange(self.first_position, self.end_position, dtype=int)
+
+
+class StateVariable(VectorVariable):
+    pass
+
+
+class LinearVector:
+
+    def __init__(self, list_of_vector_variables):
+        self.vector_variables = list_of_vector_variables
+
+        # check they are all the same type
+        self.check_sametype_vars_full_vector()
+
+        # initialise position of variables
+        self.update_indices()
+        self.update_locations()
+
+        self.variable_class = type(self.vector_variables[0])  # class
+
+    @property
+    def num_variables(self):
+        return len(self.vector_variables)
 
     @property
     def size(self):
-        return self.end_pos - self.first_pos
+        return sum([variable.size for variable in self.vector_variables])
 
+    def remove(self, *variable_name_list):
 
-class LinearVector():
+        for variable_name in variable_name_list:
+            list_of_variable_names = [variable.name for variable in self.vector_variables]
+            try:
+                remove_variable_index = list_of_variable_names.index(variable_name)
+            except ValueError:
+                raise ValueError('Trying to remove non-existent {:s} variable'.format(variable_name))
+            else:
+                self.__remove_variable(remove_variable_index)
+
+    def __remove_variable(self, index):
+        self.vector_variables.pop(index)
+        self.update_indices()
+
+    def modify(self, variable_name, **kwargs):
+        """
+        Modifies the attributes of the desired variable. The new attributes are passed as **kwargs. If an attribute is
+        not recognised a ``NameError`` is returned.
+
+        Note:
+            If changing the size of the variable, you should then update the linear vector.
+
+        Args:
+            variable_name (str): Name of the variable to modify
+            **kwargs: Key-word arguments containing the attributes of the variable as keys and the new values as values.
+
+        """
+        variable = self.get_variable_from_name(variable_name)
+        for var_attribute, new_var_value in kwargs.items():
+            if hasattr(variable, var_attribute):
+                variable.__setattr__(var_attribute, new_var_value)
+            else:
+                raise NameError('Unknown variable attribute {:s}'.format(var_attribute))
+
+    def add(self, vector_variable, **kwargs):
+
+        if isinstance(vector_variable, self.variable_class):
+            self.__add_vector_variable(vector_variable)
+        elif type(vector_variable) is str:
+            new_variable = self.variable_class(name=vector_variable, **kwargs)
+            self.__add_vector_variable(new_variable)
+        else:
+            raise TypeError('Only can add a variable from either a {:s} object or with name and kwargs'.format(
+                self.variable_class.__name__
+            ))
+
+    def append(self, vector_variable, **kwargs):
+        self.update_indices()
+        appending_index = self.num_variables
+        if isinstance(vector_variable, VectorVariable):
+            vector_variable.index = appending_index
+        elif type(vector_variable) is str:
+            kwargs['index'] = appending_index
+        else:
+            raise TypeError('Only can add a variable from either a {:s} object or with name and kwargs'.format(
+                self.variable_class.__name__
+            ))
+
+        self.add(vector_variable, **kwargs)
+
+    def __add_vector_variable(self, vector_variable):
+        list_of_indices = [variable.index for variable in self.vector_variables]
+
+        if vector_variable.index in list_of_indices:
+            raise IndexError('New variable index is already in use')
+        self.vector_variables.append(vector_variable)
+        self.update_indices()
+        self.update_locations()
+
+    def update_indices(self):
+
+        list_of_indices = [variable.index for variable in self.vector_variables]
+        ordered_list = sorted(list_of_indices)
+
+        index_variable_dict = {key_index: self.vector_variables[i] for i, key_index in enumerate(list_of_indices)}
+        updated_list = []
+        for i_var in range(self.num_variables):
+
+            current_variable_index = ordered_list[i_var]
+            current_variable = index_variable_dict[current_variable_index]
+            current_variable.index = i_var
+
+            updated_list.append(current_variable)
+
+        self.vector_variables = updated_list
 
-    def __init__(self, dof_db, sys_id):
-        self.vector_vars = dict()
+    def update_locations(self):
+        for i_var in range(self.num_variables):
+            if i_var == 0:
+                self.vector_variables[i_var].first_position = 0
+            elif i_var > 0:
+                # since end item is not included in range methods, set the first position to last variable end position
+                self.vector_variables[i_var].first_position = self.vector_variables[i_var - 1].end_position
 
-        vec_db = dict()
-        for item in dof_db:
-            vector_var = VectorVariable(item, dof_db[item], sys_id)
-            vec_db[item] = vector_var
+    def check_sametype_vars_full_vector(self):
+        variable_class = type(self.vector_variables[0])
 
-        self.vector_vars = vec_db
+        for var in self.vector_variables:
+            if not isinstance(var, variable_class):
+                raise TypeError('All variables in the vector are not of the same kind.')
 
-    def remove(self, trim_list):
-        vec_db = self.vector_vars
-        used_vars_db = self.vector_vars.copy()
+    @classmethod
+    def merge(cls, vec1, vec2):
+        """
+        Merges two instances of LinearVectors
 
-        # Variables to remove
-        removed_dofs = 0
-        removed_db = dict()
-        for item in trim_list:
+        Args:
+            vec1 (LinearVector): Vector 1
+            vec2 (LinearVector): Vector 2
+
+        Returns:
+            LinearVector: Merged vectors 1 and 2
+        """
+        if vec1.variable_class is not vec2.variable_class:
+            raise TypeError('Unable to merge two different kinds of vectors')
+
+        list_of_variables_1 = [variable.copy() for variable in vec1]
+        list_of_variables_2 = [variable.copy() for variable in vec2]
+
+        for ith, variable in enumerate(list_of_variables_1 + list_of_variables_2):
+            variable.index = ith
+
+        merged_vector = cls(list_of_variables_1 + list_of_variables_2)
+        return merged_vector
+
+    @classmethod
+    def transform(cls, linear_vector, to_type):
+        if not (to_type is InputVariable or to_type is StateVariable or to_type is OutputVariable):
+            raise TypeError('Argument should be the class to which the linear vector should be transformed')
+
+        list_of_variables = []
+        for variable in linear_vector.copy():
+            list_of_variables.append(to_type(name=variable.name,
+                                             size=variable.size,
+                                             index=variable.index))
+
+        return cls(list_of_variables)
+
+    @staticmethod
+    def check_connection(output_vector, input_vector):
+        """
+        Checks an output_vector can be connected to an input channel.
+
+        """
+        with_error = False
+        err_log = ''
+
+        for ith, out_variable in enumerate(output_vector):
             try:
-                removed_db[item] = vec_db[item]
-            except KeyError:
-                continue
-            removed_dofs += vec_db[item].size
-            del used_vars_db[item]
-
-        # Update variables position
-        for rem_item in removed_db:
-            for item in used_vars_db:
-                if used_vars_db[item].first_pos < removed_db[rem_item].first_pos:
-                    pass
-                else:
-                    # Update position
-                    used_vars_db[item].first_pos -= removed_db[rem_item].size
-                    used_vars_db[item].end_pos -= removed_db[rem_item].size
-
-        self.vector_vars = used_vars_db
-
-        return removed_dofs
-
-def remove_variables(trim_list, dof_db, sys_id):
-    # Remove should be a method of class
-    # Create class of variables
-    # All variables
-    vec_db = dict()
-    for item in dof_db:
-        vector_var = VectorVariable(item, dof_db[item], sys_id)
-        vec_db[item] = vector_var
-
-    used_vars_db = vec_db.copy()
-
-    # Variables to remove
-    removed_dofs = 0
-    removed_db = dict()
-    for item in trim_list:
-        removed_db[item] = vec_db[item]
-        removed_dofs += vec_db[item].size
-        del used_vars_db[item]
-
-    # Update variables position
-    for rem_item in removed_db:
-        for item in used_vars_db:
-            if used_vars_db[item].first_pos < removed_db[rem_item].first_pos:
-                pass
+                in_variable = input_vector.get_variable_from_name(out_variable.name)
+            except ValueError:
+                err_log += '\nUnable to connect both systems, no input variable named {:s}\n'.format(out_variable.name)
+                with_error = True
+            else:
+                if not out_variable.size == in_variable.size:
+                    err_log += 'Variable {:s} and {:s} not the same size'
+                    with_error = True
+                if not out_variable.rows_loc[-1] == in_variable.cols_loc[-1]:
+                    # checking the last index should be sufficient (and more efficient than checking the whole array)
+                    err_log += 'Variable {:s}Output rows not coincident with input columns for variable\n'.format(
+                        out_variable.name)
+                    err_log += str(out_variable) + '\n'
+                    err_log += str(in_variable) + '\n'
+                    with_error = True
+
+        if with_error:
+            raise ValueError(err_log)
+
+    @staticmethod
+    def check_same_vectors(vec1, vec2):
+        """
+        Checks that two linear vectors contain the same variables, regardless of type
+        Args:
+            vec1 (LinearVector): Vector 1
+            vec2 (LinearVector): Vector 2
+        """
+
+        assert vec1.num_variables == vec2.num_variables, 'Number of variables is not equal'
+        assert vec1.size == vec2.size, 'Size is not equal'
+
+        for i_var in range(vec1.num_variables):
+            assert vec1[i_var].name == vec2[i_var].name, 'Variable name is not the same'
+            assert vec1[i_var].index == vec2[i_var].index, 'Index is not the same'
+            assert vec1[i_var].size == vec2[i_var].size, 'Variable size is not the same'
+            assert vec1[i_var] is not vec2[i_var], 'Variables in LinearVector reference the same object in memory. ' \
+                                                   'Careful!'
+
+    def differentiate(self):
+        pass
+        # going from second order to first order systems
+
+    def get_variable_from_name(self, name):
+        list_of_variable_names = [variable.name for variable in self.vector_variables]
+        try:
+            variable_index = list_of_variable_names.index(name)
+        except ValueError:
+            raise ValueError('Variable {:s} is non existent'.format(name))
+        else:
+            return self.vector_variables[variable_index]
+
+    def __call__(self, variable_name):
+        """
+
+        Args:
+            variable_name (str): Variable name
+
+        Returns:
+            VectorVariable: Vector variable within LinearVector
+        """
+        return self.get_variable_from_name(variable_name)
+
+    def copy(self):
+        return copy.deepcopy(self)
+
+    def add_to_h5_file(self, file_handle):
+        """
+        Given an h5 handle ``file_handle``, creates and adds a group named with the type of variable and adds the fields
+        ``names`` (encoded in ascii), ``sizes`` and ``indices``.
+
+        Args:
+            file_handle (h5py.File): file handle of h5py.File
+
+        Returns:
+            file_handle
+        """
+        variable_data = []
+        for variable in self.vector_variables:
+            variable_data.append(variable.save())
+
+        types, names, sizes, indices = zip(*variable_data)
+
+        # encode strings to bytes
+        names = [name.encode('ascii', 'ignore') for name in names]
+
+        variable_group = file_handle.create_group(types[0])
+        variable_group.create_dataset(name='names', data=names)
+        variable_group.create_dataset(name='sizes', data=sizes, dtype=int)
+        variable_group.create_dataset(name='indices', data=indices, dtype=int)
+
+        return file_handle
+
+    @classmethod
+    def load_from_h5_file(cls, variable_type, variables_data):
+        """
+        Loads data from the information saved in an h5file
+
+        Args:
+            variable_type (str): Type of variable (InputVariable, OutputVariable or StateVariable)
+            variables_data (dict): Dictionary containing variable info from h5 with keys: names, sizes and indices
+
+        Returns:
+            LinearVector: of ``variable_type``
+        """
+        var_class_dict = {'InputVariable': InputVariable,
+                          'OutputVariable': OutputVariable,
+                          'StateVariable': StateVariable}
+        n_variables = len(variables_data['names'])
+        list_of_variables = []
+        try:
+            var_class = var_class_dict[variable_type]
+        except KeyError:
+            raise KeyError(f'Unknown variable type {variable_type}. Must be either InputVariable, OutputVariable '
+                           f'or StateVariable')
+        for ith_variable in range(n_variables):
+            name = variables_data['names'][ith_variable].astype('U13')  # decode to unicode
+            list_of_variables.append(var_class(name,
+                                               size=variables_data['sizes'][ith_variable],
+                                               index=variables_data['indices'][ith_variable]),
+                                     )
+        return cls(list_of_variables)
+
+    def __iter__(self):
+        return SetIterator(self)
+
+    def __getitem__(self, item):
+        return self.vector_variables[item]
+
+    def __repr__(self):
+        string_out = ''
+        for var in self.vector_variables:
+            try:
+                out_var = str(repr(var))
+            except TypeError:
+                print('Error printing var {:s}'.format(var.name))
             else:
-                # Update order and position
-                used_vars_db[item].first_pos -= removed_db[rem_item].size
-                used_vars_db[item].end_pos -= removed_db[rem_item].size
+                string_out += '\t' + out_var + '\n'
+        return string_out
+
+
+class SetIterator:
+
+    def __init__(self, linear_vector):
+        self._set_cases = linear_vector
+        self._index = 0
+
+    def __next__(self):
+        if self._index < self._set_cases.num_variables:
+            res = self._set_cases.vector_variables[self._index]
+            self._index += 1
+            return res
+
+        raise StopIteration
diff --git a/sharpy/postproc/aeroforcescalculator.py b/sharpy/postproc/aeroforcescalculator.py
index bb32c1626..0e74b722f 100644
--- a/sharpy/postproc/aeroforcescalculator.py
+++ b/sharpy/postproc/aeroforcescalculator.py
@@ -233,6 +233,8 @@ def file_output(self, filename):
         header += 'fx_unsteady_G, fy_unsteady_G, fz_unsteady_G, '
         header += 'fx_steady_a, fy_steady_a, fz_steady_a, '
         header += 'fx_unsteady_a, fy_unsteady_a, fz_unsteady_a'
+        header += 'mx_total_G, my_total_G, mz_total_G'
+        header += 'mx_total_a, my_total_a, mz_total_a'
 
         np.savetxt(self.folder + 'forces_' + filename,
                    force_matrix,
diff --git a/sharpy/postproc/asymptoticstability.py b/sharpy/postproc/asymptoticstability.py
index 6239fe9df..266d8295f 100644
--- a/sharpy/postproc/asymptoticstability.py
+++ b/sharpy/postproc/asymptoticstability.py
@@ -8,22 +8,25 @@
 import sharpy.utils.algebra as algebra
 import sharpy.solvers.lindynamicsim as lindynamicsim
 import sharpy.structure.utils.modalutils as modalutils
-import scipy.sparse as scsp
+import sharpy.utils.frequencyutils as frequencyutils
+import sharpy.linear.src.libss as libss
+import h5py
 
 
 @solver
 class AsymptoticStability(BaseSolver):
     """
-    Calculates the asymptotic stability properties of the linearised aeroelastic system by computing
-    the corresponding eigenvalues.
+    Calculates the asymptotic stability properties of the linearised system by computing
+    the corresponding eigenvalues and eigenvectors.
 
-    To use an iterative eigenvalue solver, the setting ``iterative_eigvals`` should be set to ``on``. This
-    will be beneficial when deailing with very large systems. However, the direct method is
-    preferred and more efficient when the system is of a relatively small size (typically around 5000 states).
+    The stability of the systems specified in ``target_systems`` is performed. If the system has been previously
+    scaled, a ``reference_velocity`` should be provided to compute the stability at such point.
 
-    Warnings:
-        The setting ``modes_to_plot`` to plot the eigenvectors in Paraview is currently under development.
+    The eigenvalues can be truncated, keeping a minimum ``num_evals`` (sorted by decreasing real part) or by limiting
+    the higher frequency modes through ``frequency_cutoff``.
 
+    Results can be saved to file using ``export_eigenvalues``. The setting ``display_root_locus`` shows a simple
+    Argand diagram where the continuous time eigenvalues are displayed.
     """
     solver_id = 'AsymptoticStability'
     solver_classification = 'post-processor'
@@ -31,6 +34,7 @@ class AsymptoticStability(BaseSolver):
     settings_types = dict()
     settings_default = dict()
     settings_description = dict()
+    settings_options = dict()
 
     settings_types['print_info'] = 'bool'
     settings_default['print_info'] = False
@@ -46,7 +50,12 @@ class AsymptoticStability(BaseSolver):
 
     settings_types['export_eigenvalues'] = 'bool'
     settings_default['export_eigenvalues'] = False
-    settings_description['export_eigenvalues'] = 'Save eigenvalues and eigenvectors to file. '
+    settings_description['export_eigenvalues'] = 'Save eigenvalues and eigenvectors to file.'
+
+    settings_types['output_file_format'] = 'str'
+    settings_default['output_file_format'] = 'dat'
+    settings_description['output_file_format'] = 'Eigenvalue/eigenvector output file format. HDF5 or text (.dat) files.'
+    settings_options['output_file_format'] = ['h5', 'dat']
 
     settings_types['display_root_locus'] = 'bool'
     settings_default['display_root_locus'] = False
@@ -57,9 +66,10 @@ class AsymptoticStability(BaseSolver):
     settings_description['velocity_analysis'] = 'List containing min, max and number ' \
                                                 'of velocities to analyse the system'
 
-    settings_types['iterative_eigvals'] = 'bool'
-    settings_default['iterative_eigvals'] = False
-    settings_description['iterative_eigvals'] = 'Calculate the first ``num_evals`` using an iterative solver.'
+    settings_types['target_system'] = 'list(str)'
+    settings_default['target_system'] = ['aeroelastic']
+    settings_description['target_system'] = 'System or systems for which to find frequency response.'
+    settings_options['target_system'] = ['aeroelastic', 'aerodynamic', 'structural']
 
     settings_types['num_evals'] = 'int'
     settings_default['num_evals'] = 200
@@ -69,26 +79,17 @@ class AsymptoticStability(BaseSolver):
     settings_default['modes_to_plot'] = []
     settings_description['modes_to_plot'] = 'List of mode numbers to simulate and plot'
 
-    settings_types['postprocessors'] = 'list(str)'
-    settings_default['postprocessors'] = list()
-    settings_description['postprocessors'] = 'To be used with ``modes_to_plot``. Under development.'
-
-    settings_types['postprocessors_settings'] = 'dict'
-    settings_default['postprocessors_settings'] = dict()
-    settings_description['postprocessors_settings'] = 'To be used with ``modes_to_plot``. Under development.'
-
     settings_table = settings.SettingsTable()
-    __doc__ += settings_table.generate(settings_types, settings_default, settings_description)
+    __doc__ += settings_table.generate(settings_types, settings_default, settings_description,
+                                       settings_options=settings_options)
 
     def __init__(self):
         self.settings = None
         self.data = None
         self.folder = None
+        self.print_info = False
 
-        self.eigenvalues = None
-        self.eigenvectors = None
         self.frequency_cutoff = np.inf
-        self.eigenvalue_table = None
         self.num_evals = None
 
         self.postprocessors = dict()
@@ -103,7 +104,9 @@ def initialise(self, data, custom_settings=None, caller=None):
         else:
             self.settings = custom_settings
 
-        settings.to_custom_types(self.settings, self.settings_types, self.settings_default, no_ctype=True)
+        settings.to_custom_types(self.settings, self.settings_types, self.settings_default,
+                                 options=self.settings_options,
+                                 no_ctype=True)
 
         self.num_evals = self.settings['num_evals']
 
@@ -112,12 +115,17 @@ def initialise(self, data, custom_settings=None, caller=None):
             os.makedirs(self.folder)
 
         if self.settings['print_info']:
-            cout.cout_wrap('Dynamical System Eigenvalues')
-            eigenvalue_description_file = self.folder + '/eigenvaluetable.txt'
-            self.eigenvalue_table = modalutils.EigenvalueTable(filename=eigenvalue_description_file)
+            self.print_info = True
+        else:
+            self.print_info = False
 
-        # Output dict
-        self.data.linear.stability = dict()
+        try:
+            self.frequency_cutoff = self.settings['frequency_cutoff']
+        except AttributeError:
+            self.frequency_cutoff = float(self.settings['frequency_cutoff'])
+
+        if self.frequency_cutoff == 0:
+            self.frequency_cutoff = np.inf
 
         self.caller = caller
 
@@ -130,111 +138,200 @@ def run(self, online=False):
             eigenvectors (np.ndarray): Corresponding mode shapes
 
         """
-        try:
-            self.frequency_cutoff = self.settings['frequency_cutoff']
-        except AttributeError:
-            self.frequency_cutoff = float(self.settings['frequency_cutoff'])
-
-        if self.frequency_cutoff == 0:
-            self.frequency_cutoff = np.inf
 
+        # if the system is scaled, only one system can be analysed
         if self.settings['reference_velocity'] != 1. and self.data.linear.linear_system.uvlm.scaled:
-            ss = self.data.linear.linear_system.update(self.settings['reference_velocity'])
+            ss_list = [self.data.linear.linear_system.update(self.settings['reference_velocity'])]
+            not_scaled = False
+            system_name_list = ['aeroelastic']
+            if len(self.settings['target_system']) > 1:
+                cout.cout_wrap('Warning: the system is scaled thus the only analysis currently supported is'
+                               ' for the aeroelastic system', 3)
         else:
-            ss = self.data.linear.ss
+            ss_list = [frequencyutils.find_target_system(self.data, system_name) for system_name in
+                       self.settings['target_system']]
+            not_scaled = True
+            system_name_list = self.settings['target_system']
 
-        if self.settings['print_info']:
-            cout.cout_wrap('Calculating eigenvalues using direct method')
-        eigenvalues, eigenvectors = sclalg.eig(ss.A)
+        self.compute_eigenvalues(ss_list, system_name_list, not_scaled)
 
-        # Convert DT eigenvalues into CT
-        if ss.dt:
-            # Obtain dimensional time step
-            try:
-                ScalingFacts = self.data.linear.linear_system.uvlm.sys.ScalingFacts
-                if ScalingFacts['length'] != 1.0 and ScalingFacts['time'] != 1.0:
-                    dt = ScalingFacts['length'] / self.settings['reference_velocity'] * ss.dt
+        return self.data
+
+    def compute_eigenvalues(self, ss, system_name_list=None, not_scaled=True):
+        """
+        Computes the eigenvalues and eigenvectors of the state-space
+
+        Args:
+            ss (libss.StateSpace or list([libss.StateSpace]): State-space or list of state-spaces
+            system_name_list (list([str]): Names of systems in the case multiple systems are required
+            not_scaled (bool): Flag to indicate whether the systems are assembled in non-dimensional time
+        """
+
+        if type(ss) is libss.StateSpace:
+            ss_list = [ss]
+            if system_name_list is None:
+                system_name_list = ['']
+        elif type(ss) is list:
+            ss_list = ss
+
+            if system_name_list is None:
+                system_name_list = []
+                for sys, sys_number in enumerate(ss_list):
+                    system_name_list.append(f'system{sys_number:g}')
+                    if type(sys) is not libss.StateSpace:
+                        raise TypeError(f'State-space {sys_number} is not type libss.StateSpace')
+        else:
+            raise TypeError('ss input must be either a libss.StateSpace instance or a list[libss.StateSpace]')
+
+        for ith, system in enumerate(ss_list):
+            system_name = system_name_list[ith]
+
+            if self.print_info:
+                cout.cout_wrap('Calculating %s eigenvalues using direct method' % system_name)
+
+            eigenvalues, eigenvectors = sclalg.eig(system.A)
+
+            # Convert DT eigenvalues into CT
+            if system.dt:
+                eigenvalues = self.convert_to_continuoustime(system.dt, eigenvalues, not_scaled)
+
+            num_evals = min(self.num_evals, len(eigenvalues))
+
+            eigenvalues, eigenvectors = self.sort_eigenvalues(eigenvalues, eigenvectors, self.frequency_cutoff,
+                                                              number_of_eigenvalues=num_evals)
+
+            if self.settings['export_eigenvalues']:
+                self.export_eigenvalues(num_evals, eigenvalues, eigenvectors, filename=system_name)
+
+            if self.settings['print_info']:
+                cout.cout_wrap(f'Dynamical System Eigenvalues - {system_name} system')
+                if system_name != '':
+                    eig_table_filename = system_name + '_'
                 else:
-                    dt = ss.dt
-            except AttributeError:
-                dt = ss.dt
-            eigenvalues = np.log(eigenvalues) / dt
+                    eig_table_filename = ''
+                eigenvalue_description_file = self.folder + '/{:s}eigenvaluetable.txt'.format(eig_table_filename)
+                eigenvalue_table = modalutils.EigenvalueTable(filename=eigenvalue_description_file)
+                eigenvalue_table.print_header(eigenvalue_table.headers)
+                eigenvalue_table.print_evals(eigenvalues[:self.num_evals])
+                eigenvalue_table.close_file()
 
-        self.num_evals = min(self.num_evals, len(eigenvalues))
+            if self.settings['display_root_locus']:
+                self.display_root_locus(eigenvalues)
 
-        self.eigenvalues, self.eigenvectors = self.sort_eigenvalues(eigenvalues, eigenvectors, self.frequency_cutoff)
+            if len(self.settings['velocity_analysis']) == 3 and system_name == 'aeroelastic':
+                assert self.data.linear.linear_system.uvlm.scaled, \
+                    'The UVLM system is unscaled, unable to rescale the structural equations only. Rerun with a ' \
+                    'normalised UVLM system.'
+                self.velocity_analysis()
 
-        if self.settings['export_eigenvalues']:
-            self.export_eigenvalues(self.num_evals)
+            # Under development
+            if len(self.settings['modes_to_plot']) != 0:
+                warn.warn('Plotting modes is under development')
+                self.plot_modes()
 
-        if self.settings['print_info']:
-            self.eigenvalue_table.print_header(self.eigenvalue_table.headers)
-            self.eigenvalue_table.print_evals(self.eigenvalues[:self.num_evals])
-            self.eigenvalue_table.close_file()
+        return eigenvalues, eigenvectors
 
-        if self.settings['display_root_locus']:
-            self.display_root_locus()
+    def convert_to_continuoustime(self, dt, discrete_time_eigenvalues, not_scaled=False):
+        r"""
+        Convert eigenvalues to discrete time. The ``not_scaled`` argument can be used to bypass the search from
+        within SHARPy of scaling factors. For instance, when the state-space of choice is not part of a standard
+        SHARPy case but rather an interpolated ROM etc.
 
-        # Under development
-        if len(self.settings['modes_to_plot']) != 0:
-            warn.warn('Plotting modes is under development')
-            self.plot_modes()
+        The eigenvalues are converted to continuous time using
 
-        if len(self.settings['velocity_analysis']) == 3:
-            assert self.data.linear.linear_system.uvlm.scaled, 'The UVLM system is unscaled, unable to rescale the ' \
-                                                               'structural equations only. Rerun with a normalised ' \
-                                                               'UVLM system.'
-            self.velocity_analysis()
+        .. math:: \lambda_{ct} = \frac{\log (\lambda_{dt})}{\Delta t}
 
-        self.data.linear.stability['eigenvectors'] = self.eigenvectors
-        self.data.linear.stability['eigenvalues'] = self.eigenvalues
+        If the system is scaled, the dimensional time step is retrieved as
 
-        return self.data
+        .. math:: \Delta t_{dim} = \bar{\Delta t} \frac{l_{ref}}{U_{\infty, actual}}
 
-    def export_eigenvalues(self, num_evals):
+        where :math:`l_{ref}` is the reference length and :math:`U_{\infty, actual}` is the free stream velocity at
+        which to calculate the eigenvalues.
+
+        Args:
+            dt (float): Discrete time increment.
+            discrete_time_eigenvalues (np.ndarray): Array of discrete time eigenvalues.
+            not_scaled (bool): Treat the system as not scaled. No Scaling Factors will be searched in SHARPy.
         """
-        Saves a ``num_evals`` number of eigenvalues and eigenvectors to file. The files are saved in the output directoy
-        and include:
+        if not_scaled:
+            dt = dt
+        else:
+            try:
+                ScalingFacts = self.data.linear.linear_system.uvlm.sys.ScalingFacts
+                if ScalingFacts['length'] != 1.0 and ScalingFacts['time'] != 1.0:
+                    dt *= ScalingFacts['length'] / self.settings['reference_velocity']
+                else:
+                    dt = dt
+            except AttributeError:
+                dt = dt
 
-            * ``eigenvectors.dat``: ``(num_dof, num_evals)`` array of eigenvectors
+        return np.log(discrete_time_eigenvalues) / dt
 
-            * ``eigenvalues_r.dat``: ``(num_evals, 1)`` array of the real part of the eigenvalues
+    def export_eigenvalues(self, num_evals, eigenvalues, eigenvectors, filename=None):
+        """
+        Saves a ``num_evals`` number of eigenvalues and eigenvectors to file.
 
-            * ``eigenvalues_i.dat``: ``(num_evals, 1)`` array of the imaginary part of the eigenvalues.
+        The files are saved in the output directory and include:
 
-        The units of the eigenvalues are ``rad/s``
+            * ``{system_name}_eigenvalues.dat``: Array of eigenvalues of shape ``(num_evals, 2)`` where the first column corresponds
+              to the real part and the second column to the imaginary part.
 
-        References:
-            Loading and saving complex arrays:
-            https://stackoverflow.com/questions/6494102/how-to-save-and-load-an-array-of-complex-numbers-using-numpy-savetxt/6522396
+            * ``{system_name}_stability.h5``: An ``.h5`` file containing the desired number of eigenvalues and eigenvectors of the
+              chosen systems.
+
+        The units of the eigenvalues are ``rad/s``.
 
         Args:
-            num_evals: Number of eigenvalues to save
+            num_evals (int): Number of eigenvalues to save.
+            eigenvalues (np.ndarray): Eigenvalue array.
+            eigenvectors (np.ndarray): Matrix of eigenvectors.
+            filename (str (optional)): Optional prefix of the output filenames.
+
+        See Also:
+            Loading and saving complex arrays:
+            https://stackoverflow.com/questions/6494102/how-to-save-and-load-an-array-of-complex-numbers-using-numpy-savetxt/6522396
         """
 
-        num_evals = min(num_evals, self.eigenvalues.shape[0])
+        stability_folder_path = self.folder
 
-        np.savetxt(self.folder + '/eigenvalues.dat', self.eigenvalues[:num_evals].view(float).reshape(-1, 2))
-        np.savetxt(self.folder + '/eigenvectors_r.dat', self.eigenvectors.real[:, :num_evals])
-        np.savetxt(self.folder + '/eigenvectors_i.dat', self.eigenvectors.imag[:, :num_evals])
+        if filename is None or filename == '':
+            filename = ''
+        else:
+            filename += '_'
+
+        if self.settings['output_file_format'] == 'dat':
+            np.savetxt(self.folder + f'/{filename:s}eigenvalues.dat', eigenvalues.view(float).reshape(-1, 2))
+            np.savetxt(self.folder + f'/{filename:s}eigenvectors_r.dat', eigenvectors.real)
+            np.savetxt(self.folder + f'/{filename:s}eigenvectors_i.dat', eigenvectors.imag)
+        elif self.settings['output_file_format'] == 'h5':
+            with h5py.File(stability_folder_path + f'/{filename:s}stability.h5', 'w') as f:
+                f.create_dataset('eigenvalues', data=eigenvalues, dtype=complex)
+                f.create_dataset('eigenvectors', data=eigenvectors, dtype=complex)
+                f.create_dataset('num_eigenvalues', data=num_evals, dtype=int)
+        else:
+            raise TypeError(f'Unrecognised file type saving option {self.settings["output_file_format"]}')
+            # this shouldn't happen as the settings_options should check the validity of the setting
 
-    def print_eigenvalues(self):
+    def velocity_analysis(self):
         """
-        Prints the eigenvalues to a table with the corresponding natural frequency, period and damping ratios
+        Velocity analysis for scaled systems.
 
-        """
+        Runs the stability analysis for different velocities for aeroelastic systems that have been previously scaled.
 
-        self.eigenvalue_table.print_evals(self.eigenvalues[:self.settings['num_evals']])
+        For every velocity, the linear system is updated. This involves updating the structural matrices and the
+        coupling matrix. The eigenvalues saved are in continuous time.
 
-    def velocity_analysis(self):
+        It saves the results to a ``.dat`` file where the first column corresponds to the free stream velocity and the
+        second and third columns to the real and imaginary parts of the eigenvalues.
+        """
 
         ulb, uub, num_u = self.settings['velocity_analysis']
 
         if self.settings['print_info']:
             cout.cout_wrap('Velocity Asymptotic Stability Analysis', 1)
-            cout.cout_wrap('Initial velocity: %.2f m/s' % ulb, 1)
-            cout.cout_wrap('Final velocity: %.2f m/s' % uub, 1)
-            cout.cout_wrap('Number of evaluations: %g' % num_u, 1)
+            cout.cout_wrap('Initial velocity: {:02f} m/s'.format(ulb), 1)
+            cout.cout_wrap('Final velocity: {:02f} m/s'.format(uub), 1)
+            cout.cout_wrap('Number of evaluations: {:g}'.format(num_u), 1)
 
         u_inf_vec = np.linspace(ulb, uub, int(num_u))
 
@@ -257,32 +354,35 @@ def velocity_analysis(self):
             Nunst = np.sum(eigs_cont.real > 0)
             fn = np.abs(eigs_cont)
 
-            cout.cout_wrap('LTI\tu: %.2f m/2\tmax. CT eig. real: %.6f\t' \
-                           % (u_inf_vec[i], np.max(eigs_cont.real)))
-            cout.cout_wrap('\tN unstab.: %.3d' % (Nunst,))
-            cout.cout_wrap('\tUnstable aeroelastic natural frequency CT(rad/s):' + Nunst * '\t%.2f' % tuple(fn[:Nunst]))
+            if self.settings['print_info']:
+                cout.cout_wrap('LTI\tu: %.2f m/2\tmax. CT eig. real: %.6f\t' \
+                               % (u_inf_vec[i], np.max(eigs_cont.real)))
+                cout.cout_wrap('\tN unstab.: %.3d' % (Nunst,))
+                cout.cout_wrap(
+                    '\tUnstable aeroelastic natural frequency CT(rad/s):' + Nunst * '\t%.2f' % tuple(fn[:Nunst]))
 
             # Store eigenvalues for plot
             real_part_plot.append(eigs_cont.real)
             imag_part_plot.append(eigs_cont.imag)
-            uinf_part_plot.append(np.ones_like(eigs_cont.real)*u_inf_vec[i])
+            uinf_part_plot.append(np.ones_like(eigs_cont.real) * u_inf_vec[i])
 
         real_part_plot = np.hstack(real_part_plot)
         imag_part_plot = np.hstack(imag_part_plot)
         uinf_part_plot = np.hstack(uinf_part_plot)
 
-        cout.cout_wrap('Saving velocity analysis results...')
-        np.savetxt(self.folder + '/velocity_analysis_min%04d_max%04d_nvel%04d.dat' %(ulb*10, uub*10, num_u),
+        velocity_file_name = self.folder + '/velocity_analysis_min{:04g}_max{:04g}_nvel{:04g}.dat'.format(
+            ulb * 10,
+            uub * 10,
+            num_u)
+
+        np.savetxt(velocity_file_name,
                    np.concatenate((uinf_part_plot, real_part_plot, imag_part_plot)).reshape((-1, 3), order='F'))
-        cout.cout_wrap('\tSuccessful', 1)
 
-        self.velocity_results = dict()
-        self.data.linear.stability['velocity_results'] = dict()
-        self.data.linear.stability['velocity_results']['u_inf'] = uinf_part_plot
-        self.data.linear.stability['velocity_results']['evals_real'] = real_part_plot
-        self.data.linear.stability['velocity_results']['evals_imag'] = imag_part_plot
+        if self.print_info:
+            cout.cout_wrap('\t\tSuccessfully saved velocity analysis to {:s}'.format(velocity_file_name), 2)
 
-    def display_root_locus(self):
+    @staticmethod
+    def display_root_locus(eigenvalues):
         """
         Displays root locus diagrams.
 
@@ -300,7 +400,7 @@ def display_root_locus(self):
             return
         fig, ax = plt.subplots()
 
-        ax.scatter(np.real(self.eigenvalues), np.imag(self.eigenvalues),
+        ax.scatter(np.real(eigenvalues), np.imag(eigenvalues),
                    s=6,
                    color='k',
                    marker='s')
@@ -324,53 +424,49 @@ def plot_modes(self):
         except ModuleNotFoundError:
             cout.cout_wrap('Could not plot in asymptoticstability beacuse there is no Matplotlib', 4)
             return
+
         mode_shape_list = self.settings['modes_to_plot']
+
+        route = self.folder + '/modes/'
+        if not os.path.isdir(route):
+            os.makedirs(route, exist_ok=True)
+
         for mode in mode_shape_list:
             # Scale mode
-            aero_states = self.data.linear.linear_system.uvlm.ss.states
-            displacement_states = self.data.linear.linear_system.beam.ss.states // 2
+
+            beam = self.data.linear.linear_system.beam
+            displacement_states = beam.ss.states // 2
+            structural_states = beam.ss.states
+            structural_modal_coords = beam.sys.modal
+
+            v = self.eigenvectors[-structural_states:-structural_states//2, mode]
+            v_dot = self.eigenvectors[-structural_states//2:, mode]
+
+            if beam.sys.clamped:
+                num_dof_rig = 1 # to get the full vector (if 0 line356 returns empty array)
+            else:
+                num_dof_rig = beam.sys.num_dof_rig
+
+            if structural_modal_coords:
+                # project aeroelastic mode back to modal coordinates
+                phi = beam.sys.U
+                eta = phi.dot(v).real
+                eta_dot = phi.dot(v_dot)[:-num_dof_rig].real
+            else:
+                eta = v[:-num_dof_rig].real
+                eta_dot = v_dot[:-num_dof_rig].real
+
             amplitude_factor = modalutils.scale_mode(self.data,
-                                                self.eigenvectors[aero_states:aero_states + displacement_states-9,
-                                                mode], rot_max_deg=10, perc_max=0.1)
-
-            fact_rbm = self.scale_rigid_body_mode(self.eigenvectors[:, mode], self.eigenvalues[mode].imag)* 100
-            print(fact_rbm)
-
-            t, x = self.mode_time_domain(amplitude_factor, fact_rbm, mode)
-
-            # Initialise postprocessors - new folder for each mode
-            # initialise postprocessors
-            route = self.folder + '/stability/mode_%06d/' % mode
-            postprocessors = dict()
-            postprocessor_list = ['AerogridPlot', 'BeamPlot']
-            postprocessors_settings = dict()
-            postprocessors_settings['AerogridPlot'] = {'include_rbm': 'on',
-                                    'include_applied_forces': 'on',
-                                    'minus_m_star': 0,
-                                    'u_inf': 1
-                                    }
-            postprocessors_settings['BeamPlot'] = {'include_rbm': 'on',
-                                    'include_applied_forces': 'on'}
-
-            for postproc in postprocessor_list:
-                postprocessors[postproc] = initialise_solver(postproc)
-                postprocessors[postproc].initialise(
-                    self.data, postprocessors_settings[postproc])
-
-            # Plot reference
-            for postproc in postprocessor_list:
-                self.data = postprocessors[postproc].run(online=True)
-            for n in range(t.shape[1]):
-                aero_tstep, struct_tstep = lindynamicsim.state_to_timestep(self.data, x[:, n])
-                self.data.aero.timestep_info.append(aero_tstep)
-                self.data.structure.timestep_info.append(struct_tstep)
-
-                for postproc in postprocessor_list:
-                    self.data = postprocessors[postproc].run(online=True)
-
-            # Delete 'modal' timesteps ready for next mode
-            del self.data.structure.timestep_info[1:]
-            del self.data.aero.timestep_info[1:]
+                                                     eta,
+                                                     rot_max_deg=10, perc_max=0.15)
+            eta *= amplitude_factor
+            zeta_mode = modalutils.get_mode_zeta(self.data, eta)
+            modalutils.write_zeta_vtk(zeta_mode, self.data.linear.tsaero0.zeta, filename_root=route + 'mode_{:06g}'.format(mode))
+
+        # Reference - linearisation state
+        eta *= 0
+        zeta_mode = modalutils.get_mode_zeta(self.data, eta)
+        modalutils.write_zeta_vtk(zeta_mode, self.data.linear.tsaero0.zeta, filename_root=route + 'mode_ref')
 
     def mode_time_domain(self, fact, fact_rbm, mode_num, cycles=2):
         """
@@ -386,14 +482,13 @@ def mode_time_domain(self, fact, fact_rbm, mode_num, cycles=2):
             tuple: Time domain array and scaled eigenvector in time.
         """
 
-
         # Time domain representation of the mode
         eigenvalue = self.eigenvalues[mode_num]
         natural_freq = np.abs(eigenvalue)
         damping = eigenvalue.real / natural_freq
-        period = 2*np.pi / natural_freq
-        dt = period/100
-        t_dom = np.linspace(0, 2 * period, int(np.ceil(2 * cycles * period/dt)))
+        period = 2 * np.pi / natural_freq
+        dt = period / 100
+        t_dom = np.linspace(0, 2 * period, int(np.ceil(2 * cycles * period / dt)))
         t_dom.shape = (1, len(t_dom))
         eigenvector = self.eigenvectors[:, mode_num]
         eigenvector.shape = (len(eigenvector), 1)
@@ -419,7 +514,7 @@ def reconstruct_mode(self, eig):
         return gamma, gamma_dot, gamma_star, x_struct
 
     @staticmethod
-    def sort_eigenvalues(eigenvalues, eigenvectors, frequency_cutoff=0):
+    def sort_eigenvalues(eigenvalues, eigenvectors, frequency_cutoff=0, number_of_eigenvalues=None):
         """
         Sort continuous-time eigenvalues by order of magnitude.
 
@@ -430,9 +525,10 @@ def sort_eigenvalues(eigenvalues, eigenvectors, frequency_cutoff=0):
             eigenvalues (np.ndarray): Continuous-time eigenvalues
             eigenvectors (np.ndarray): Corresponding right eigenvectors
             frequency_cutoff (float): Cutoff frequency for truncation ``[rad/s]``
+            number_of_eigenvalues (int (optional)): Number of eigenvalues to retain
 
         Returns:
-
+            tuple(np.array, np.array): eigenvalues and eigenvectors
         """
 
         if frequency_cutoff == 0:
@@ -446,13 +542,19 @@ def sort_eigenvalues(eigenvalues, eigenvectors, frequency_cutoff=0):
 
         order = np.argsort(eigenvalues_truncated.real)[::-1]
 
+        if number_of_eigenvalues is not None:
+            if number_of_eigenvalues > len(order):
+                cout.cout_wrap(f'Desired number of eigenvalues ({number_of_eigenvalues}) exceeds system size '
+                               f'({len(order)}) after frequency truncation.', 3)
+            else:
+                order = order[:number_of_eigenvalues]
         return eigenvalues_truncated[order], eigenvectors_truncated[:, order]
 
     @staticmethod
     def scale_rigid_body_mode(eigenvector, freq_d):
         rigid_body_mode = eigenvector[-10:]
 
-        max_angle = 10 * np.pi/180
+        max_angle = 10 * np.pi / 180
 
         v = rigid_body_mode[0:3].real
         omega = rigid_body_mode[3:6].real
diff --git a/sharpy/postproc/beamloads.py b/sharpy/postproc/beamloads.py
index 82430fba9..9eedbe92c 100644
--- a/sharpy/postproc/beamloads.py
+++ b/sharpy/postproc/beamloads.py
@@ -1,5 +1,6 @@
 import os
 import numpy as np
+import os
 from sharpy.utils.solver_interface import solver, BaseSolver
 import sharpy.utils.settings as settings
 import sharpy.structure.utils.xbeamlib as xbeamlib
@@ -98,15 +99,11 @@ def print_loads(self, online):
     def calculate_loads(self, online):
         if online:
             it = -1
-            (self.data.structure.timestep_info[it].postproc_cell['strain'],
-             self.data.structure.timestep_info[it].postproc_cell['loads']) = xbeamlib.cbeam3_loads(self.data.structure,
-                                                                                                   it)
+            timestep_add_loads(self.data.structure, self.data.structure.timestep_info[it])
             self.calculate_coords_a(self.data.structure.timestep_info[it])
         else:
             for it in range(len(self.data.structure.timestep_info)):
-                (self.data.structure.timestep_info[it].postproc_cell['strain'],
-                 self.data.structure.timestep_info[it].postproc_cell['loads']) = xbeamlib.cbeam3_loads(self.data.structure,
-                                                                                                       it)
+                timestep_add_loads(self.data.structure, self.data.structure.timestep_info[it])
                 self.calculate_coords_a(self.data.structure.timestep_info[it])
 
     def calculate_coords_a(self, timestep_info):
@@ -115,6 +112,11 @@ def calculate_coords_a(self, timestep_info):
             iglobal_node = self.data.structure.connectivities[ielem, 2]
             timestep_info.postproc_cell['coords_a'][ielem, :] = timestep_info.pos[iglobal_node, :]
 
+
+def timestep_add_loads(structure, timestep):
+    timestep.postproc_cell['strain'], timestep.postproc_cell['loads'] = \
+        xbeamlib.cbeam3_loads(structure, timestep)
+
     # def calculate_loads(self):
     #     # initial (ini) loads
     #     tstep = self.data.structure.ini_info
diff --git a/sharpy/postproc/frequencyresponse.py b/sharpy/postproc/frequencyresponse.py
index d43e12582..65834ebbf 100644
--- a/sharpy/postproc/frequencyresponse.py
+++ b/sharpy/postproc/frequencyresponse.py
@@ -53,6 +53,24 @@ class FrequencyResponse(solver_interface.BaseSolver):
     settings_description['frequency_unit'] = 'Units of frequency, ``w`` for rad/s, ``k`` reduced.'
     settings_options['frequency_unit'] = ['w', 'k']
 
+    settings_types['frequency_scaling'] = 'dict'
+    settings_default['frequency_scaling'] = {}
+    settings_description['frequency_scaling'] = 'Dictionary containing the frequency scaling factors, if the ' \
+                                                'aerodynamic system has not been previously scaled. Applied also if ' \
+                                                'the desired unit is reduced frequency.'
+
+    scaling_types = dict()
+    scaling_default = dict()
+    scaling_description = dict()
+
+    scaling_types['length'] = 'float'
+    scaling_default['length'] = 1.
+    scaling_description['length'] = 'Length scaling factor.'
+
+    scaling_types['speed'] = 'float'
+    scaling_default['speed'] = 1.
+    scaling_description['speed'] = 'Speed scaling factor.'
+
     settings_types['frequency_bounds'] = 'list(float)'
     settings_default['frequency_bounds'] = [1e-3, 1]
     settings_description['frequency_bounds'] = 'Lower and upper frequency bounds in the corresponding unit.'
@@ -77,6 +95,10 @@ class FrequencyResponse(solver_interface.BaseSolver):
     settings_table = settings_utils.SettingsTable()
     __doc__ += settings_table.generate(settings_types, settings_default, settings_description, settings_options)
 
+    scaling_table = settings_utils.SettingsTable()
+    __doc__ += scaling_table.generate(scaling_types, scaling_default, scaling_description,
+                                      header_line='The scaling dictionary takes the following entries:')
+
     def __init__(self):
 
         self.settings = None
@@ -84,6 +106,7 @@ def __init__(self):
         self.folder = None
         self.print_info = False
 
+        self.scaled = False
         self.w_to_k = 1
         self.wv = None
         self.caller = None
@@ -102,15 +125,19 @@ def initialise(self, data, custom_settings=None, caller=None):
 
         self.print_info = self.settings['print_info']
 
-        try:
-            scaling = self.data.linear.linear_system.uvlm.sys.ScalingFacts
-            if self.settings['frequency_unit'] == 'k':
-                self.w_to_k = scaling['length'] / scaling['speed']
+        if self.settings['frequency_unit'] == 'k':
+            self.scaled = True
+            if self.data.linear.linear_system.uvlm.scaled:
+                scaling = self.data.linear.linear_system.uvlm.sys.ScalingFacts
             else:
-                self.w_to_k = 1.
-        except AttributeError:
+                scaling = self.settings['frequency_scaling']
+                settings_utils.to_custom_types(scaling, self.scaling_types, self.scaling_default,
+                                               no_ctype=True)
+            self.w_to_k = scaling['length'] / scaling['speed']
+        else:
             self.w_to_k = 1.
 
+        # Frequency bounds in radians
         lb = self.settings['frequency_bounds'][0] / self.w_to_k
         ub = self.settings['frequency_bounds'][1] / self.w_to_k
 
@@ -132,7 +159,7 @@ def run(self, ss=None, online=False):
         Computes the frequency response of the linear state-space.
 
         Args:
-            ss (sharpy.linear.src.libss.ss (Optional)): State-space object for which to compute the frequency response.
+            ss (sharpy.linear.src.libss.StateSpace (Optional)): State-space object for which to compute the frequency response.
               If not given, the response for the previously assembled systems and specified in ``target_system`` will
               be performed.
         """
@@ -140,12 +167,12 @@ def run(self, ss=None, online=False):
 
         if ss is None:
             ss_list = [find_target_system(self.data, system_name) for system_name in self.settings['target_system']]
-        elif type(ss) is libss.ss:
+        elif type(ss) is libss.StateSpace:
             ss_list = [ss]
         elif type(ss) is list:
             ss_list = ss
         else:
-            raise TypeError('ss input must be either a libss.ss instance or a list of libss.ss')
+            raise TypeError('StateSpace input must be either a libss.StateSpace instance or a list of libss.StateSpace')
 
         for ith, system in enumerate(ss_list):
             if self.print_info:
@@ -177,7 +204,7 @@ def run(self, ss=None, online=False):
             else:
                 hinf = None
 
-            self.save_freq_resp(self.wv, y_freq_fom, system_name=system_name, hinf=hinf)
+            self.save_freq_resp(self.wv * self.w_to_k, y_freq_fom, system_name=system_name, hinf=hinf)
 
             cout.cout_wrap('\tComputed the frequency response in %f s' % tfom, 2)
 
@@ -204,7 +231,7 @@ def save_freq_resp(self, wv, Yfreq, system_name=None, hinf=None):
         with open(self.folder + '/freqdata_readme.txt', 'w') as outfile:
             outfile.write('Frequency Response Data Output\n\n')
             outfile.write('Frequency data found in the relevant .h5 file\n')
-            outfile.write('If the system has not been scaled, the units of frequency are rad/s\nThe frequency' \
+            outfile.write('The units of frequency are rad/s\nThe frequency' \
                           'response is given in complex form.')
 
         case_name = ''
@@ -246,7 +273,7 @@ def quick_plot(self, y_freq_fom=None, subfolder=None):
                     if y_freq_fom is not None:
                         ax1[0].plot(self.wv * self.w_to_k, 20 * np.log10(np.abs(y_freq_fom[pj, mj, :])), color='C0')
                         ax1[1].plot(self.wv * self.w_to_k, np.angle(y_freq_fom[pj, mj, :]), '-', color='C0')
-                    if self.settings['frequency_unit'] == 'k':
+                    if self.scaled:
                         ax1[1].set_xlabel('Reduced Frequency, k [-]')
                     else:
                         ax1[1].set_xlabel(r'Frequency, $\omega$ [rad/s]')
diff --git a/sharpy/postproc/savedata.py b/sharpy/postproc/savedata.py
index 7cb9fab7c..360393064 100644
--- a/sharpy/postproc/savedata.py
+++ b/sharpy/postproc/savedata.py
@@ -182,11 +182,11 @@ def initialise(self, data, custom_settings=None, caller=None):
                                        sharpy.linear.assembler.linearaeroelastic.LinearAeroelastic,
                                        sharpy.linear.assembler.linearbeam.LinearBeam,
                                        sharpy.linear.assembler.linearuvlm.LinearUVLM,
-                                       sharpy.linear.src.libss.ss,
+                                       sharpy.linear.src.libss.ss_block,
                                        sharpy.linear.src.lingebm.FlexDynamic,)
 
             if self.settings['save_linear_uvlm']:
-                self.ClassesToSave += (sharpy.solvers.linearassembler.Linear, sharpy.linear.src.libss.ss)
+                self.ClassesToSave += (sharpy.solvers.linearassembler.Linear, sharpy.linear.src.libss.ss_block)
         self.caller = caller
 
     def run(self, online=False):
@@ -267,28 +267,25 @@ def run(self, online=False):
                             'D': D}
                 for k, v in linearisation_vectors.items():
                     savedict[k] = v
-                try:
-                    dt = self.data.linear.ss.dt
+                dt = self.data.linear.ss.dt
+                if dt is not None:
                     savedict['dt'] = dt
-                except AttributeError:
-                    pass
                 savemat(matfilename, savedict)
 
             if self.settings['save_linear_uvlm']:
                 matfilename = self.filename.replace('.data.h5', '.uvlmss.mat')
                 linearisation_vectors = self.data.linear.linear_system.uvlm.linearisation_vectors
                 A, B, C, D = self.data.linear.linear_system.uvlm.ss.get_mats()
+
                 savedict = {'A': A,
                             'B': B,
                             'C': C,
                             'D': D}
                 for k, v in linearisation_vectors.items():
                     savedict[k] = v
-                try:
-                    dt = self.data.linear.ss.dt
+                dt = self.data.linear.linear_system.uvlm.ss.dt
+                if dt is not None:
                     savedict['dt'] = dt
-                except AttributeError:
-                    pass
                 savemat(matfilename, savedict)
 
         return self.data
diff --git a/sharpy/postproc/saveparametriccase.py b/sharpy/postproc/saveparametriccase.py
index b16d733ba..f348f3a6d 100644
--- a/sharpy/postproc/saveparametriccase.py
+++ b/sharpy/postproc/saveparametriccase.py
@@ -3,6 +3,7 @@
 import configobj
 import os
 import sharpy.utils.cout_utils as cout
+import warnings
 
 
 @solver
@@ -16,6 +17,16 @@ class SaveParametricCase(BaseSolver):
     If the setting ``save_case`` is selected and the post processor :class:`~sharpy.solvers.pickledata.PickleData`
     is not present in the SHARPy flow, this solver will pickle the data to the path given in the ``folder`` setting.
 
+    The setting ``save_pmor_items`` saves to h5 the following state-spaces and gains, if present:
+        * Aeroelastic state-space saved to: <output_folder> / save_pmor_data / <case_name>_statespace.h5
+        * Aerodynamic ROM reduced order bases saved to: <output_folder> / save_pmor_data / <case_name>_aerorob.h5
+        * Structural ROM reduced order bases saved to: <output_folder> / save_pmor_data / <case_name>_modal_structrob.h5
+
+    The setting ``save_pmor_subsystem saves the additional state-spaces to h5 files:
+        * Structural matrices saved to: <output_folder> / save_pmor_data / <case_name>_struct_matrices.h5
+        * Structural state-space saved to: <output_folder> / save_pmor_data / <case_name>_beamstatespace.h5
+        * Aerodynamic state-space saved to: <output_folder> / save_pmor_data / <case_name>_aerostatespace.h5
+
     Examples:
 
         In the case you are running several SHARPy cases, varying for instance the velocity, the settings would
@@ -41,13 +52,25 @@ class SaveParametricCase(BaseSolver):
     settings_description = dict()
 
     settings_types['save_case'] = 'bool'
-    settings_default['save_case'] = True
-    settings_description['save_case'] = 'Save a .pkl of the SHARPy case. Required for PMOR.'
+    settings_default['save_case'] = False
+    settings_description['save_case'] = 'DeprecationWarning - Save a .pkl of the SHARPy case. Required for PMOR.'
 
     settings_types['parameters'] = 'dict'
     settings_default['parameters'] = None
     settings_description['parameters'] = 'Dictionary containing the chosen simulation parameters and their values.'
 
+    settings_types['save_pmor_items'] = 'bool'
+    settings_default['save_pmor_items'] = False
+    settings_description['save_pmor_items'] = 'Saves to h5 the items required for PMOR interpolation: the aerodynamic ' \
+                                              'reduced order bases, the structural modal matrix and the ' \
+                                              'reduced state-space'
+
+    settings_types['save_pmor_subsystems'] = 'bool'
+    settings_default['save_pmor_subsystems'] = False
+    settings_description['save_pmor_subsystems'] = 'Saves to h5 the statespaces and matrices of the UVLM and beam and ' \
+                                                   'the M, C, K matrices. The setting ``save_pmor_items`` ' \
+                                                   'should be set to `on`'
+
     settings_table = settings.SettingsTable()
     __doc__ += settings_table.generate(settings_types, settings_default, settings_description)
 
@@ -55,6 +78,7 @@ def __init__(self):
         self.data = None
         self.settings = None
         self.folder = None
+        self.case_name = None
 
     def initialise(self, data, custom_settings=None):
         self.data = data
@@ -70,6 +94,7 @@ def initialise(self, data, custom_settings=None):
                                  )
 
         self.folder = data.output_folder
+        self.case_name = self.data.settings['SHARPy']['case']
 
     def run(self):
 
@@ -85,14 +110,64 @@ def run(self):
         sim_info['case'] = self.data.settings['SHARPy']['case']
 
         if 'PickleData' not in self.data.settings['SHARPy']['flow'] and self.settings['save_case']:
+            warnings.warn('Post-proc: SaveParametricCase: Saving a pickle is not recommended - try saving required '
+                          'attributes individually',
+                          DeprecationWarning)
             pickle_solver = initialise_solver('PickleData')
             pickle_solver.initialise(self.data)
             self.data = pickle_solver.run()
             sim_info['path_to_data'] = os.path.abspath(self.folder)
-        else:
-            sim_info['path_to_data'] = os.path.abspath(self.folder)
+
+        sim_info['path_to_data'] = os.path.abspath(self.folder)
 
         config['sim_info'] = sim_info
         config.write()
 
+        if self.settings['save_pmor_items']:
+            try:
+                self.data.linear
+            except AttributeError:
+                pass
+            else:
+                if not os.path.exists(self.folder + '/save_pmor_data/'):
+                    os.makedirs(self.folder + '/save_pmor_data/')
+                self.save_state_space()
+                self.save_aero_rom_bases()
+                self.save_structural_modal_matrix()
+
+                if self.settings['save_pmor_subsystems']:
+                    self.save_structural_matrices()
+                    self.save_aero_state_space()
+
         return self.data
+
+    def save_aero_rom_bases(self):
+        """Save the aerodynamic reduced order bases to h5 files in the output directory"""
+        # check rom's exist
+        rom_dict = self.data.linear.linear_system.uvlm.rom
+        if rom_dict is None:
+            return None
+
+        for rom_name, rom_class in rom_dict.items():
+            rom_class.save_reduced_order_bases(self.base_name + f'_{rom_name.lower()}_aerorob.h5')
+
+    def save_structural_modal_matrix(self):
+        if not self.data.linear.linear_system.beam.sys.modal:
+            return None
+
+        self.data.linear.linear_system.beam.save_reduced_order_bases(self.base_name + f'_modal_structrob.h5')
+
+    def save_state_space(self):
+        self.data.linear.linear_system.ss.save(self.base_name + '_statespace.h5')
+
+    def save_structural_matrices(self):
+        self.data.linear.linear_system.beam.ss.save(self.base_name + '_beamstatespace.h5')
+        self.data.linear.linear_system.beam.save_structural_matrices(self.base_name + '_struct_matrices.h5')
+
+    def save_aero_state_space(self):
+        self.data.linear.linear_system.uvlm.ss.save(self.base_name + '_aerostatespace.h5')
+
+    @property
+    def base_name(self):
+        return self.folder + '/save_pmor_data/' + self.case_name
+
diff --git a/sharpy/postproc/stabilityderivatives.py b/sharpy/postproc/stabilityderivatives.py
new file mode 100644
index 000000000..b2c8782d6
--- /dev/null
+++ b/sharpy/postproc/stabilityderivatives.py
@@ -0,0 +1,215 @@
+import sharpy.utils.solver_interface as solver_interface
+import os
+import numpy as np
+import sharpy.utils.cout_utils as cout
+import sharpy.utils.settings as settings
+from sharpy.linear.utils.derivatives import Derivatives, DerivativeSet
+
+
+@solver_interface.solver
+class StabilityDerivatives(solver_interface.BaseSolver):
+    """
+    Outputs the stability derivatives of a free-flying aircraft
+
+    """
+    solver_id = 'StabilityDerivatives'
+    solver_classification = 'post-processor'
+
+    settings_default = dict()
+    settings_description = dict()
+    settings_types = dict()
+    settings_options = dict()
+
+    settings_types['print_info'] = 'bool'
+    settings_default['print_info'] = True
+    settings_description['print_info'] = 'Display info to screen'
+
+    settings_types['u_inf'] = 'float'
+    settings_default['u_inf'] = 1.
+    settings_description['u_inf'] = 'Free stream reference velocity'
+
+    settings_types['S_ref'] = 'float'
+    settings_default['S_ref'] = 1.
+    settings_description['S_ref'] = 'Reference planform area'
+
+    settings_types['b_ref'] = 'float'
+    settings_default['b_ref'] = 1.
+    settings_description['b_ref'] = 'Reference span'
+
+    settings_types['c_ref'] = 'float'
+    settings_default['c_ref'] = 1.
+    settings_description['c_ref'] = 'Reference chord'
+
+    settings_table = settings.SettingsTable()
+    __doc__ += settings_table.generate(settings_types, settings_default, settings_description)
+
+    def __init__(self):
+        self.data = None
+        self.settings = dict()
+
+        self.u_inf = 1
+        self.inputs = 0
+        self.caller = None
+        self.folder = None
+
+        self.ppal_axes = None
+        self.n_control_surfaces = None
+
+        self.coefficients = dict  # type: dict # name: scaling coefficient
+
+    def initialise(self, data, custom_settings=None, caller=None):
+        self.data = data
+
+        if custom_settings:
+            self.settings = custom_settings
+        else:
+            self.settings = self.data.settings[self.solver_id]
+
+        settings.to_custom_types(self.settings, self.settings_types, self.settings_default,
+                                 options=self.settings_options,
+                                 no_ctype=True)
+        self.caller = caller
+        self.folder = data.output_folder + '/derivatives/'
+        if not os.path.isdir(self.folder):
+            os.makedirs(self.folder, exist_ok=True)
+
+        u_inf = self.settings['u_inf']
+        s_ref = self.settings['S_ref']
+        b_ref = self.settings['b_ref']
+        c_ref = self.settings['c_ref']
+        rho = self.data.linear.tsaero0.rho
+        self.ppal_axes = self.data.settings['Modal']['rigid_modes_ppal_axes']
+
+        # need to decide whether coefficients stay here or goes just in Derivatives class
+        self.coefficients = {'force': 0.5 * rho * u_inf ** 2 * s_ref,
+                             'moment_lon': 0.5 * rho * u_inf ** 2 * s_ref * c_ref,
+                             'moment_lat': 0.5 * rho * u_inf ** 2 * s_ref * b_ref,
+                             'force_angular_vel': 0.5 * rho * u_inf ** 2 * s_ref * c_ref / u_inf,
+                             'moment_lon_angular_vel': 0.5 * rho * u_inf ** 2 * s_ref * c_ref * c_ref / u_inf}  # missing rates
+
+        reference_dimensions = {}
+        for k in ['S_ref', 'b_ref', 'c_ref', 'u_inf']:
+            reference_dimensions[k] = self.settings[k]
+        reference_dimensions['rho'] = rho
+        reference_dimensions['quat'] = self.data.linear.tsstruct0.quat
+
+        self.data.linear.derivatives = dict()  # {str:Derivatives()} (sharpy.linear.utils.derivatives.Derivatives)
+        self.data.linear.derivatives['aerodynamic'] = Derivatives(reference_dimensions,
+                                                                  static_state=self.steady_aero_forces(),
+                                                                  target_system='aerodynamic')
+        self.data.linear.derivatives['aeroelastic'] = Derivatives(reference_dimensions,
+                                                                  static_state=self.steady_aero_forces(),
+                                                                  target_system='aeroelastic')
+
+    def run(self, online=False):
+
+        # TODO: consider running all required solvers inside this one to keep the correct settings
+        # i.e: run Modal, Linear Assembly
+
+        derivatives = self.data.linear.derivatives  # {str:Derivatives()} (sharpy.linear.utils.derivatives.Derivatives)
+        if self.data.linear.linear_system.beam.sys.modal:
+            phi = self.data.linear.linear_system.linearisation_vectors['mode_shapes'].real
+        else:
+            phi = None
+
+        steady_forces = self.data.linear.linear_system.linearisation_vectors['forces_aero_beam_dof']
+        v0 = self.get_freestream_velocity()
+        quat = self.data.linear.tsstruct0.quat
+
+        try:
+            tpa = self.data.linear.tsstruct0.modal['t_pa']
+        except KeyError:
+            tpa = None
+
+        if self.data.linear.linear_system.uvlm.scaled:
+            raise NotImplementedError('Stability Derivatives not yet implented for scaled system')
+            self.data.linear.linear_system.update(self.settings['u_inf'])
+
+        for target_system in ['aerodynamic', 'aeroelastic']:
+            state_space = self.get_state_space(target_system)
+            target_system_derivatives = derivatives[target_system]
+
+            target_system_derivatives.initialise_derivatives(state_space,
+                                                             steady_forces,
+                                                             quat,
+                                                             v0,
+                                                             phi,
+                                                             self.data.linear.tsstruct0.modal['cg'],
+                                                             tpa=tpa)
+            target_system_derivatives.dict_of_derivatives[
+                'force_angle_velocity'] = target_system_derivatives.new_derivative(
+                'stability',
+                'angle_derivatives',
+                'Force/Angle via velocity')
+
+            # useful to double check the effect of the ``track_body`` == 'on' setting
+            # current_derivative.dict_of_derivatives['force_angle_angle'] = current_derivative.new_derivative(
+            #     'stability',
+            #     'angle_derivatives_tb',
+            #     'Force/Angle via Track Body')
+
+            target_system_derivatives.dict_of_derivatives['force_velocity'] = target_system_derivatives.new_derivative(
+                'body',
+                'body_derivatives')
+
+            target_system_derivatives.dict_of_derivatives['force_cs'] = target_system_derivatives.new_derivative(
+                'body',
+                'control_surface_derivatives')
+            target_system_derivatives.save(self.folder)
+            target_system_derivatives.savetxt(self.folder)
+
+        return self.data
+
+    def get_freestream_velocity(self):
+        try:
+            u_inf = self.data.settings['StaticUvlm']['aero_solver_settings']['u_inf']
+            u_inf_direction = self.data.settings['StaticCoupled']['aero_solver_settings']['u_inf_direction']
+        except KeyError:
+            try:
+                u_inf = self.data.settings['StaticCoupled']['aero_solver_settings']['velocity_field_input']['u_inf']
+                u_inf_direction = self.data.settings['StaticCoupled']['aero_solver_settings']['velocity_field_input']['u_inf_direction']
+            except KeyError:
+                cout.cout_wrap('Unable to find free stream velocity settings in StaticUvlm or StaticCoupled,'
+                               'please ensure these settings are provided in the config .sharpy file. If'
+                               'you are running a restart simulation make sure they are included too, regardless'
+                               'of these solvers being present in the SHARPy flow', 4)
+                raise KeyError
+
+        try:
+            v0 = u_inf * u_inf_direction * -1
+        except TypeError:
+            # For restart solutions, where the settings may have not been processed and thus may
+            # exist but in string format
+            try:
+                u_inf_direction = np.array(u_inf_direction, dtype=float)
+            except ValueError:
+                if u_inf_direction.find(',') < 0:
+                    u_inf_direction = np.fromstring(u_inf_direction.strip('[]'), sep=' ', dtype=float)
+                else:
+                    u_inf_direction = np.fromstring(u_inf_direction.strip('[]'), sep=',', dtype=float)
+            finally:
+                v0 = np.array(u_inf_direction, dtype=float) * float(u_inf) * -1
+
+        return v0
+
+    def get_state_space(self, target_system):
+        if target_system == 'aerodynamic':
+            ss = self.data.linear.linear_system.uvlm.ss
+        elif target_system == 'aeroelastic':
+            ss = self.data.linear.ss
+        else:
+            raise NameError('Unknown target system {:s}'.format(target_system))
+
+        return ss
+
+    def steady_aero_forces(self):
+        fx = np.sum(self.data.aero.timestep_info[0].inertial_steady_forces[:, 0], 0) + \
+             np.sum(self.data.aero.timestep_info[0].inertial_unsteady_forces[:, 0], 0)
+
+        fy = np.sum(self.data.aero.timestep_info[0].inertial_steady_forces[:, 1], 0) + \
+             np.sum(self.data.aero.timestep_info[0].inertial_unsteady_forces[:, 1], 0)
+
+        fz = np.sum(self.data.aero.timestep_info[0].inertial_steady_forces[:, 2], 0) + \
+             np.sum(self.data.aero.timestep_info[0].inertial_unsteady_forces[:, 2], 0)
+
+        return fx, fy, fz
diff --git a/sharpy/rom/balanced.py b/sharpy/rom/balanced.py
index cd41c1431..d092c9d13 100644
--- a/sharpy/rom/balanced.py
+++ b/sharpy/rom/balanced.py
@@ -21,6 +21,7 @@
 import sharpy.rom.utils.librom as librom
 import sharpy.linear.src.libss as libss
 import time
+from sharpy.linear.utils.ss_interface import LinearVector, StateVariable
 
 dict_of_balancing_roms = dict()
 
@@ -116,9 +117,9 @@ def run(self, ss):
         Cr = C.dot(Tinv)
 
         if dtsystem:
-            ss_bal = libss.ss(Ar, Br, Cr, D, dt=ss.dt)
+            ss_bal = libss.StateSpace(Ar, Br, Cr, D, dt=ss.dt)
         else:
-            ss_bal = libss.ss(Ar, Br, Cr, D)
+            ss_bal = libss.StateSpace(Ar, Br, Cr, D)
 
         t1 = time.time()
         if self.print_info:
@@ -293,7 +294,7 @@ def run(self, ss):
         Br = Tinv.dot(B)
         Cr = C.dot(T)
 
-        ssrom = libss.ss(Ar, Br, Cr, D, dt=ss.dt)
+        ssrom = libss.StateSpace(Ar, Br, Cr, D, dt=ss.dt)
         return ssrom
 
 
@@ -369,14 +370,23 @@ def run(self, ss):
 
         out = self.algorithm.run(ss)
 
-        if type(out) == libss.ss:
+        if type(out) == libss.StateSpace:
             self.ssrom = out
 
         else:
             Ar, Br, Cr = out
             if self.dtsystem:
-                self.ssrom = libss.ss(Ar, Br, Cr, D, dt=self.ss.dt)
+                self.ssrom = libss.StateSpace(Ar, Br, Cr, D, dt=self.ss.dt)
             else:
-                self.ssrom = libss.ss(Ar, Br, Cr, D)
+                self.ssrom = libss.StateSpace(Ar, Br, Cr, D)
+
+        try:
+            self.ssrom.input_variables = self.ss.input_variables.copy()
+            self.ssrom.output_variables = self.ss.output_variables.copy()
+            self.ssrom.state_variables = LinearVector(
+                [StateVariable('balanced_{:s}'.format(self.settings['algorithm'].lower()),
+                               size=self.ssrom.states, index=0)])
+        except AttributeError:
+            pass
 
         return self.ssrom
diff --git a/sharpy/rom/krylov.py b/sharpy/rom/krylov.py
index 0c1e5c194..2f70e8ddf 100644
--- a/sharpy/rom/krylov.py
+++ b/sharpy/rom/krylov.py
@@ -11,6 +11,7 @@
 import sharpy.rom.utils.krylovutils as krylovutils
 import warnings as warn
 import h5py
+from sharpy.linear.utils.ss_interface import LinearVector, StateVariable, InputVariable, OutputVariable
 
 @rom_interface.rom
 class Krylov(rom_interface.BaseRom):
@@ -161,9 +162,33 @@ def save(self, filename):
                           compress_float=True)
             h5.add_as_grp(self.ssrom, outfile,
                           grpname='ssrom',
-                          ClassesToSave=(libss.ss, ),
+                          ClassesToSave=(libss.StateSpace, ),
                           compress_float=True)
 
+    def save_reduced_order_bases(self, file_name):
+        """
+        Save reduced order bases to an h5 file
+
+        Args:
+            file_name (str): path to h5 file
+
+        """
+        if not isinstance(self.V, libss.Gain):
+            V = libss.Gain(self.V)
+            W = libss.Gain(self.W)
+
+        else:
+            V = self.V
+            W = self.W
+
+        if self.settings['single_side'] == 'observability' or self.settings['single_side'] == 'controllability':
+            # if single sided, the projector is V and W = V therefore no need to duplicate
+            libss.Gain.save_multiple_gains(file_name, ('V', V))
+            cout.cout_wrap(f'Saved Krylov reduced order bases, V to file: {file_name}', 1)
+        else:
+            libss.Gain.save_multiple_gains(file_name, ('V', V), ('W', W))
+            cout.cout_wrap(f'Saved Krylov reduced order bases, V and W to file: {file_name}', 1)
+
     def run(self, ss):
         """
         Performs Model Order Reduction employing Krylov space projection methods.
@@ -181,10 +206,10 @@ def run(self, ss):
         =========================  ====================  ==========================================================
 
         Args:
-            ss (sharpy.linear.src.libss.ss): State space to reduce
+            ss (sharpy.linear.src.libss.StateSpace): State space to reduce
 
         Returns:
-            (libss.ss): Reduced state space system
+            (libss.StateSpace): Reduced state space system
         """
         self.ss = ss
 
@@ -202,7 +227,13 @@ def run(self, ss):
 
         Ar, Br, Cr = self.__getattribute__(self.algorithm)(self.frequency, self.r)
 
-        self.ssrom = libss.ss(Ar, Br, Cr, self.ss.D, self.ss.dt)
+        self.ssrom = libss.StateSpace(Ar, Br, Cr, self.ss.D, self.ss.dt)
+        try:
+            self.ssrom.input_variables = self.ss.input_variables.copy()
+            self.ssrom.output_variables = self.ss.output_variables.copy()
+            self.ssrom.state_variables = LinearVector([StateVariable('krylov', size=self.ssrom.states, index=0)])
+        except AttributeError:
+            pass
 
         self.stable = self.check_stability(restart_arnoldi=self.restart_arnoldi)
 
@@ -685,6 +716,8 @@ def mimo_rational_arnoldi(self, frequency, r):
         if self.settings['single_side'] == 'controllability' or self.settings['single_side'] == 'observability':
             if self.settings['single_side'] == 'observability':
                 V = W
+            if self.settings['single_side'] == 'controllability':
+                W = V  # needed to properly save gains
             Ar = V.T.dot(self.ss.A.dot(V))
             Br = V.T.dot(self.ss.B)
             Cr = self.ss.C.dot(V)
@@ -708,8 +741,17 @@ def mimo_rational_arnoldi(self, frequency, r):
             Br = W.T.dot(self.ss.B)
             Cr = self.ss.C.dot(V.dot(Tinv))
 
-        self.W = W
-        self.V = V
+        self.W = libss.Gain(W,
+                            input_vars=LinearVector([InputVariable('krylov', size=W.shape[1], index=0)]),
+                            output_vars=LinearVector.transform(self.ss.state_variables, OutputVariable))
+        self.V = libss.Gain(V,
+                            input_vars=LinearVector([InputVariable('krylov', size=V.shape[1], index=0)]),
+                            output_vars=LinearVector.transform(self.ss.state_variables, OutputVariable))
+
+        # for state recovery purposes
+        self.projection_gain = libss.Gain(V,
+                                          input_vars=LinearVector([InputVariable('krylov', size=V.shape[1], index=0)]),
+                                          output_vars=LinearVector.transform(self.ss.state_variables, OutputVariable))
 
         return Ar, Br, Cr
 
diff --git a/sharpy/rom/utils/librom.py b/sharpy/rom/utils/librom.py
index 4518d0776..c8e27d1cf 100644
--- a/sharpy/rom/utils/librom.py
+++ b/sharpy/rom/utils/librom.py
@@ -979,7 +979,7 @@ def balfreq(SS, DictBalFreq):
     Ab = libsp.dot(Ti, libsp.dot(SS.A, T))
     Bb = libsp.dot(Ti, SS.B)
     Cb = libsp.dot(SS.C, T)
-    SSb = libss.ss(Ab, Bb, Cb, SS.D, dt=SS.dt)
+    SSb = libss.StateSpace(Ab, Bb, Cb, SS.D, dt=SS.dt)
 
     ### Eliminate unstable modes - if any:
     if DictBalFreq['check_stability']:
@@ -1016,7 +1016,7 @@ def modred(SSb, N, method='residualisation'):
 
     Nb = SSb.A.shape[0]
     if Nb == N:
-        SSrom = libss.ss(SSb.A, SSb.B, SSb.C, SSb.D, dt=SSb.dt)
+        SSrom = libss.StateSpace(SSb.A, SSb.B, SSb.C, SSb.D, dt=SSb.dt)
         return SSrom
 
     A11 = SSb.A[:N, :N]
@@ -1025,7 +1025,7 @@ def modred(SSb, N, method='residualisation'):
     D = SSb.D
 
     if method is 'truncation':
-        SSrom = libss.ss(A11, B11, C11, D, dt=SSb.dt)
+        SSrom = libss.StateSpace(A11, B11, C11, D, dt=SSb.dt)
     else:
         Nb = SSb.A.shape[0]
         IA22inv = -SSb.A[N:, N:].copy()
@@ -1033,7 +1033,7 @@ def modred(SSb, N, method='residualisation'):
         IA22inv[eevec, eevec] += 1.
         IA22inv = scalg.inv(IA22inv, overwrite_a=True)
 
-        SSrom = libss.ss(
+        SSrom = libss.StateSpace(
             A11 + np.dot(SSb.A[:N, N:], np.dot(IA22inv, SSb.A[N:, :N])),
             B11 + np.dot(SSb.A[:N, N:], np.dot(IA22inv, SSb.B[N:, :])),
             C11 + np.dot(SSb.C[:, N:], np.dot(IA22inv, SSb.A[N:, :N])),
diff --git a/sharpy/rom/utils/librom_interp.py b/sharpy/rom/utils/librom_interp.py
index 5d7981289..d84ec6711 100644
--- a/sharpy/rom/utils/librom_interp.py
+++ b/sharpy/rom/utils/librom_interp.py
@@ -51,7 +51,7 @@ def transfer_function(SS_list, wv):
           fast on-line evaluation
 
     Args:
-        SS_list (list): List of state-space models instances of :class:`sharpy.linear.src.libss.ss` class.
+        SS_list (list): List of state-space models instances of :class:`sharpy.linear.src.libss.StateSpace` class.
         wv (list): list of interpolatory weights.
 
     Notes:
@@ -86,7 +86,7 @@ def FLB_transfer_function(SS_list, wv, U_list, VT_list, hsv_list=None, M_list=No
 
 
     Args:
-        SS_list (list): List of state-space models instances of :class:`sharpy.linear.src.libss.ss` class.
+        SS_list (list): List of state-space models instances of :class:`sharpy.linear.src.libss.StateSpace` class.
         wv (list): list of interpolatory weights.
         U_list (list): small size, thin SVD factors of Gramians square roots of each state space model (:math:`\mathbf{U}`).
         VT_list (list): small size, thin SVD factors of Gramians square roots of each state space model (:math:`\mathbf{V}^\top`).
@@ -200,7 +200,7 @@ def FLB_transfer_function(SS_list, wv, U_list, VT_list, hsv_list=None, M_list=No
         C_int += wv[ii] * np.dot(SS_list[ii].C, T_int_list[ii])
         D_int += wv[ii] * SS_list[ii].D
 
-    return libss.ss(A_int, B_int, C_int, D_int, dt=SS_list[0].dt), hsv_int
+    return libss.StateSpace(A_int, B_int, C_int, D_int, dt=SS_list[0].dt), hsv_int
 
 
 class InterpROM:
@@ -228,7 +228,7 @@ class InterpROM:
 
     Inputs:
 
-    - SS: list of state-space models (instances of libss.ss class)
+    - SS: list of state-space models (instances of libss.StateSpace class)
 
     - VV: list of V matrices used to produce SS. If None, it is assumed that
       ROMs are defined over the same basis
@@ -342,7 +342,7 @@ def __call__(self, wv):
             Cint += wv[ii] * self.CC[ii]
             Dint += wv[ii] * self.DD[ii]
 
-        return libss.ss(Aint, Bint, Cint, Dint, self.SS[0].dt)
+        return libss.StateSpace(Aint, Bint, Cint, Dint, self.SS[0].dt)
 
     def project(self):
         """
@@ -506,7 +506,7 @@ def setUp(self):
             B = np.random.rand(Nx, Nu)
             C = np.random.rand(Ny, Nx)
             D = np.random.rand(Ny, Nu)
-            self.SS = libss.ss(A, B, C, D, dt=dt)
+            self.SS = libss.StateSpace(A, B, C, D, dt=dt)
 
 # class Interp1d():
 #     """
@@ -577,7 +577,7 @@ def setUp(self):
 
 #     def __call__(self, zint):
 #         """
-#         Evaluate at interpolation point zint. Returns a list of classes ss
+#         Evaluate at interpolation point zint. Returns a list of classes StateSpace
 #         """
 
 #         Nint=len(zint)
@@ -619,7 +619,7 @@ def setUp(self):
 #         # and pack everything
 #         SSint=[]
 #         for ii in range(Nint):
-#             SSint.append( ss( Aint[ii,:,:], Bint[ii,:,:], 
+#             SSint.append( StateSpace( Aint[ii,:,:], Bint[ii,:,:],
 #                               Cint[ii,:,:], Dint[ii,:,:], dt=self.SS[0].dt))
 
 #         return SSint
diff --git a/sharpy/solvers/lindynamicsim.py b/sharpy/solvers/lindynamicsim.py
index 4284e1bf1..b28771191 100644
--- a/sharpy/solvers/lindynamicsim.py
+++ b/sharpy/solvers/lindynamicsim.py
@@ -7,6 +7,7 @@
 import scipy.linalg as sclalg
 import sharpy.utils.h5utils as h5utils
 from sharpy.utils.datastructures import LinearTimeStepInfo
+from sharpy.linear.utils.ss_interface import InputVariable, LinearVector
 import sharpy.utils.cout_utils as cout
 import time
 import warnings
@@ -17,7 +18,14 @@ class LinDynamicSim(BaseSolver):
 
     Uses the derived linear time invariant systems and solves it in time domain.
 
-    Requires a ``case_name.lininput.h5`` file in the case root folder that contains:
+    The inputs are provided by means of a list of dictionaries to the setting
+    ``input_generators``. For each input you want, you need a dictionary entry containing
+    ``name`` (str) which is the name of the variable, ``index`` (int) for the index of the variable
+    in the case of multidimensional variables (if unspecified reverts to ``0``) and the ``file_path``
+    to a text file containing the time series of the input. If the input variable is multidimensional,
+    ``index`` may be a list of indices for each column of time series in the array in the input file.
+
+    Alternatively a ``case_name.lininput.h5`` file in the case root folder can be used with the following entries:
 
         * ``x0`` (optional): Initial state vector
         * ``input_vec``: Input vector ``(n_tsteps, n_inputs)``.
@@ -51,6 +59,10 @@ class LinDynamicSim(BaseSolver):
     settings_default['physical_time'] = 2.
     settings_description['physical_time'] = 'Time to run'
 
+    settings_types['input_generators'] = 'list(dict)'
+    settings_default['input_generators'] = []
+    settings_description['input_generators'] = 'List of dictionaries for each input'
+
     settings_default['dt'] = 0.001
     settings_types['dt'] = 'float'
     settings_description['dt'] = 'Time increment for the solution of systems without a specified dt'
@@ -83,7 +95,7 @@ def initialise(self, data, custom_settings=None):
             self.settings = custom_settings
         else:
             self.settings = data.settings[self.solver_id]
-        settings.to_custom_types(self.settings, self.settings_types, self.settings_default)
+        settings.to_custom_types(self.settings, self.settings_types, self.settings_default, no_ctype=True)
 
         # Read initial state and input data and store in dictionary
         self.read_files()
@@ -102,13 +114,51 @@ def initialise(self, data, custom_settings=None):
             self.postprocessors[postproc].initialise(
                 self.data, self.settings['postprocessors_settings'][postproc], caller=self)
 
+    def input_vector(self, ss):
+        """
+        Generates an input vector ``u`` of size ``n_tsteps x inputs`` and populates the
+        correct columns with the time series arrays provided as text files in the settings
+        ``input_generators``.
+
+        Args:
+            ss (libss.StateSpace): State Space object for which to generate input
+
+        Returns:
+            np.array: Input vector.
+        """
+        n_steps = self.settings['n_tsteps']
+        u_vect = np.zeros((n_steps, ss.inputs))
+
+        for in_settings in self.settings['input_generators']:
+            var_name = in_settings['name']
+            index = in_settings.get('index', 0)
+            file_path = in_settings['file_path']
+
+            variable = ss.input_variables.get_variable_from_name(var_name)
+            input_data = np.loadtxt(file_path)
+
+            cout.cout_wrap('Found input for {:s}'.format(str(variable)))
+
+            if type(index) is list:
+                for ith, i_ind in enumerate(index):
+                    in_channel = variable.cols_loc[i_ind]
+                    u_vect[:, in_channel] = input_data[:, ith]
+            else:
+                in_channel = variable.cols_loc[index]
+                u_vect[:, in_channel] = input_data
+
+        return u_vect
+
     def run(self):
 
         ss = self.data.linear.ss
 
         n_steps = self.settings['n_tsteps']
         x0 = self.input_data_dict.get('x0', np.zeros(ss.states))
-        u = self.input_data_dict['u']
+        if len(self.settings['input_generators']) != 0:
+            u = self.input_vector(ss)
+        else:
+            u = self.input_data_dict['u']
 
         if len(x0) != ss.states:
             warnings.warn('Number of states in the initial state vector not equal to the number of states')
@@ -128,14 +178,14 @@ def run(self):
             dt = self.settings['dt']
 
         # Total time to run
-        T = n_steps*dt
+        T = (n_steps - 1) * dt
 
         u_ref = self.settings['reference_velocity']
         # If the system is scaled:
         if u_ref != 1.:
             scaling_factors = self.data.linear.linear_system.uvlm.sys.ScalingFacts
             dt_dimensional = scaling_factors['length'] / u_ref
-            T_dimensional = n_steps * dt_dimensional
+            T_dimensional = (n_steps - 1) * dt_dimensional
             T = T_dimensional / scaling_factors['time']
             ss = self.data.linear.linear_system.update(self.settings['reference_velocity'])
         t_dom = np.linspace(0, T, n_steps)
@@ -144,7 +194,6 @@ def run(self):
         sys = libss.ss_to_scipy(ss)
         cout.cout_wrap('Solving linear system using scipy...')
         t0 = time.time()
-        # breakpoint()
         out = sys.output(u, t=t_dom, x0=x0)
         ts = time.time() - t0
         cout.cout_wrap('\tSolved in %.2fs' % ts, 1)
@@ -232,19 +281,13 @@ def state_to_timestep(data, x, u=None, y=None):
         modal = True
     else:
         modal = False
-    # modal = True
-    if data.linear.linear_system.uvlm.gust_assembler:
-        start_x_aero = data.linear.linear_system.uvlm.gust_assembler.ss_gust.states
-    else:
-        start_x_aero = 0
-    x_aero = x[start_x_aero:data.linear.linear_system.uvlm.ss.states]
-    x_struct = x[-data.linear.linear_system.beam.ss.states:]
-    # u_aero = TODO: external velocities
-    phi = data.linear.linear_system.beam.sys.U
-    Kas = data.linear.linear_system.couplings['Kas']
+
+    aero_state = x[:-data.linear.linear_system.beam.ss.states]
+    beam_state = x[-data.linear.linear_system.beam.ss.states:] # after aero all the rest is beam, but should not use
+    # in case it is in DT the state variables do not mean the same!
 
     # Beam output
-    y_beam = x_struct
+    y_beam = y[-data.linear.linear_system.beam.ss.outputs:]
 
     u_q = np.zeros(data.linear.linear_system.uvlm.ss.inputs)
     if u is not None:
@@ -253,26 +296,36 @@ def state_to_timestep(data, x, u=None, y=None):
     else:
         u_q[:y_beam.shape[0]] += y_beam
 
+    Kas = data.linear.linear_system.couplings['Kas']
     if modal:
-        # add eye matrix for extra inputs
-        n_modes = phi.shape[1]
-        n_inputs_aero_only = len(u_q) - 2*n_modes  # Inputs to the UVLM other than structural inputs
-        u_aero = Kas.dot(sclalg.block_diag(phi, phi, np.eye(n_inputs_aero_only)).dot(u_q))
+        # Transform to aerodynamic raw inputs
+        uvlm_in_mode_gain = data.linear.linear_system.couplings['in_mode_gain']
+        aero_input = Kas.dot(uvlm_in_mode_gain.dot(u_q))
     else:
-        # if u_q.shape[0] !=
-        # u_aero_zero = data.linear.tsaero0
-        u_aero = Kas.dot(u_q)
-
-    # Unpack input
-    zeta, zeta_dot, u_ext = data.linear.linear_system.uvlm.unpack_input_vector(u_aero)
+        aero_input = Kas.dot(u_q)
 
-    # Also add the beam forces. I have a feeling there is a minus there as well....
     # Aero
-    forces, gamma, gamma_dot, gamma_star = data.linear.linear_system.uvlm.unpack_ss_vector(
+    forces, gamma, gamma_dot, gamma_star, gust_state_vec = data.linear.linear_system.uvlm.unpack_ss_vector(
         data,
-        x_n=x_aero,
+        x_n=aero_state,
+        u_aero=aero_input,
         aero_tstep=data.linear.tsaero0,
-        track_body=True)
+        track_body=True,
+        state_variables=data.linear.linear_system.uvlm.ss.state_variables,
+        gust_in=True)
+
+    if data.linear.linear_system.uvlm.gust_assembler:
+        gust_in_loc = data.linear.ss.input_variables('u_gust').cols_loc
+        u_in_gust = u[gust_in_loc]
+        gust_assembler = data.linear.linear_system.uvlm.gust_assembler
+        u_ext_gust = gust_assembler.state_to_uext.dot(gust_state_vec) + gust_assembler.uin_to_uext.dot(u_in_gust)
+    else:
+        u_ext_gust = np.array([])
+
+    uvlm_input_variables = LinearVector.transform(Kas.output_variables, InputVariable)
+    # Unpack input
+    zeta, zeta_dot, u_ext = data.linear.linear_system.uvlm.unpack_input_vector(aero_input, u_ext_gust,
+                                                                               input_variables=uvlm_input_variables)
 
     current_aero_tstep = data.aero.timestep_info[-1].copy()
     current_aero_tstep.forces = [forces[i_surf] + data.linear.tsaero0.forces[i_surf] for i_surf in
@@ -289,8 +342,8 @@ def state_to_timestep(data, x, u=None, y=None):
 
     # self.data.aero.timestep_info.append(current_aero_tstep)
 
-    aero_forces = data.linear.linear_system.uvlm.C_to_vertex_forces.dot(x_aero)
-    beam_forces = data.linear.linear_system.couplings['Ksa'].dot(aero_forces)
+    aero_forces_vec = np.concatenate([forces[i_surf][:3, :, :].reshape(-1, order='C') for i_surf in range(len(forces))])
+    beam_forces = data.linear.linear_system.couplings['Ksa'].dot(aero_forces_vec)
 
     if u is not None:
         u_struct = u[-data.linear.linear_system.beam.ss.inputs:]
@@ -299,9 +352,9 @@ def state_to_timestep(data, x, u=None, y=None):
     # Reconstruct the state if modal
     if modal:
         phi = data.linear.linear_system.beam.sys.U
-        x_s = sclalg.block_diag(phi, phi).dot(x_struct)
+        x_s = sclalg.block_diag(phi, phi).dot(y_beam)
     else:
-        x_s = x_struct
+        x_s = y_beam
     y_s = beam_forces #+ phi.dot(u_struct)
     # y_s = self.data.linear.lsys['LinearBeam'].sys.U.T.dot(y_struct)
 
diff --git a/sharpy/solvers/linearassembler.py b/sharpy/solvers/linearassembler.py
index aec3d8683..e65a97bed 100644
--- a/sharpy/solvers/linearassembler.py
+++ b/sharpy/solvers/linearassembler.py
@@ -91,6 +91,20 @@ class LinearAssembler(BaseSolver):
     settings_default['retain_outputs'] = []
     settings_description['retain_outputs'] = 'List of output channels to retain in the chosen ``inout_coordinates``.'
 
+    settings_types['retain_input_variables'] = 'list(str)'
+    settings_default['retain_input_variables'] = []
+    settings_description['retain_input_variables'] = 'List of input channels to retain in the chosen ' \
+                                                     '``inout_coordinates``.'
+
+    settings_types['retain_output_variables'] = 'list(str)'
+    settings_default['retain_output_variables'] = []
+    settings_description['retain_output_variables'] = 'List of output channels to retain in the chosen ' \
+                                                      '``inout_coordinates``.'
+
+    settings_types['recover_accelerations'] = 'bool'
+    settings_default['recover_accelerations'] = False
+    settings_description['recover_accelerations'] = 'Recover structural system accelerations as additional outputs.'
+
     settings_table = settings.SettingsTable()
     __doc__ += settings_table.generate(settings_types, settings_default, settings_description, settings_options)
 
@@ -141,6 +155,10 @@ def run(self):
 
         self.data.linear.ss = self.data.linear.linear_system.assemble()
 
+        if self.settings['recover_accelerations']:
+            gain = self.data.linear.linear_system.beam.recover_accelerations(self.data.linear.ss)
+            self.data.linear.ss.addGain(gain, where='out')
+
         # modify inout coordinates
         if self.settings['inout_coordinates'] == 'nodes':
             try:
@@ -150,11 +168,30 @@ def run(self):
 
         # retain only selected inputs and outputs
         if len(self.settings['retain_inputs']) != 0:
-            self.data.linear.ss.remove_inout_channels(self.settings['retain_inputs'], where='in')
+            self.data.linear.ss.retain_inout_channels(self.settings['retain_inputs'], where='in')
         if len(self.settings['retain_outputs']) != 0:
-            self.data.linear.ss.remove_inout_channels(self.settings['retain_outputs'], where='out')
+            self.data.linear.ss.retain_inout_channels(self.settings['retain_outputs'], where='out')
+
+        if len(self.settings['retain_input_variables']) != 0:
+            ss = self.data.linear.ss
+            input_vars = ss.input_variables
+            removed_variables = []
+            for variable in input_vars:
+                if variable.name not in self.settings['retain_input_variables']:
+                    removed_variables.append(variable.name)
+            ss.remove_inputs(*removed_variables)
+
+        if len(self.settings['retain_output_variables']) != 0:
+            ss = self.data.linear.ss
+            output_vars = ss.output_variables
+            removed_variables = []
+            for variable in output_vars:
+                if variable.name not in self.settings['retain_output_variables']:
+                    removed_variables.append(variable.name)
+            ss.remove_outputs(*removed_variables)
+
         cout.cout_wrap('Final system is:', 1)
-        cout.cout_wrap(self.data.linear.ss.summary(), 1)
+        cout.cout_wrap(str(self.data.linear.ss), 2)
 
         return self.data
 
diff --git a/sharpy/solvers/modal.py b/sharpy/solvers/modal.py
index 553c8d8d0..a9c5ea69e 100644
--- a/sharpy/solvers/modal.py
+++ b/sharpy/solvers/modal.py
@@ -45,10 +45,6 @@ class Modal(BaseSolver):
     settings_default['NumLambda'] = 20  # doubles if use_undamped_modes is False
     settings_description['NumLambda'] = 'Number of modes to retain'
 
-    settings_types['keep_linear_matrices'] = 'bool'  # attach linear M,C,K matrices to output dictionary
-    settings_default['keep_linear_matrices'] = True
-    settings_description['keep_linear_matrices'] = 'Save M, C and K matrices to output dictionary'
-
     # output options
     settings_types['write_modes_vtk'] = 'bool'  # write displacements mode shapes in vtk file
     settings_default['write_modes_vtk'] = True
@@ -58,9 +54,9 @@ class Modal(BaseSolver):
     settings_default['print_matrices'] = False
     settings_description['print_matrices']  = 'Write M, C and K matrices to file'
 
-    settings_types['write_dat'] = 'bool'  # write modes shapes/freq./damp. to dat file
-    settings_default['write_dat'] = True
-    settings_description['write_dat'] = 'Write mode shapes, frequencies and damping to file'
+    settings_types['save_data'] = 'bool'  # write modes shapes/freq./damp. to dat file
+    settings_default['save_data'] = True
+    settings_description['save_data'] = 'Write mode shapes, frequencies and damping to file'
 
     settings_types['continuous_eigenvalues'] = 'bool'
     settings_default['continuous_eigenvalues'] = False
@@ -90,9 +86,14 @@ class Modal(BaseSolver):
     settings_default['use_custom_timestep'] = -1
     settings_description['use_custom_timestep'] = 'If > -1, it will use that time step geometry for calculating the modes'
 
+    settings_types['rigid_modes_ppal_axes'] = 'bool'
+    settings_default['rigid_modes_ppal_axes'] = False
+    settings_description['rigid_modes_ppal_axes'] = 'Modify the ridid body modes such that they are defined wrt ' \
+                                                    'to the CG and aligned with the principal axes of inertia'
+
     settings_types['rigid_modes_cg'] = 'bool'
     settings_default['rigid_modes_cg'] = False
-    settings_description['rigid_modes_cg'] = 'Modify the ridid body modes such that they are defined wrt to the CG'
+    settings_description['rigid_modes_cg'] = 'Not implemente yet'
 
     settings_table = settings.SettingsTable()
     __doc__ += settings_table.generate(settings_types, settings_default, settings_description)
@@ -242,10 +243,13 @@ def run(self):
             FullMglobal, FullCglobal, FullKglobal, FullQ = xbeamlib.xbeam3_asbly_dynamic(self.data.structure,
                                           self.data.structure.timestep_info[self.data.ts],
                                           full_matrix_settings)
+
+            cg = modalutils.cg(FullMglobal)
         else:
             xbeamlib.cbeam3_solv_modal(self.data.structure,
                                        self.settings, self.data.ts,
                                        FullMglobal, FullCglobal, FullKglobal)
+            cg = None
 
         # Print matrices
         if self.settings['print_matrices']:
@@ -347,11 +351,15 @@ def run(self):
             eigenvectors_left = eigenvectors_left[:, order].conj()
 
         # Modify rigid body modes for them to be defined wrt the CG
-        eigenvectors = modalutils.mode_sign_convention(self.data.structure.boundary_conditions, eigenvectors, self.rigid_body_motion)
-        if self.settings['rigid_modes_cg']:
-            if not eigenvectors_left:
-                eigenvectors = modalutils.free_modes_principal_axes(eigenvectors, FullMglobal)
-
+        eigenvectors = modalutils.mode_sign_convention(self.data.structure.boundary_conditions, eigenvectors,
+                                                       self.rigid_body_motion)
+        if not eigenvectors_left:
+            if self.settings['rigid_modes_ppal_axes']:
+                eigenvectors, t_pa, r_pa = modalutils.free_modes_principal_axes(eigenvectors, FullMglobal,
+                                                                          return_transform=True)
+            else:
+                t_pa = None  # Transformation matrix from the A frame to the P frame (principal axes of inertia)
+                r_pa = None
         # Scaling
         eigenvectors, eigenvectors_left = self.scale_modes_unit_mass_matrix(eigenvectors, FullMglobal, eigenvectors_left)
 
@@ -381,7 +389,7 @@ def run(self):
                 warnings.warn('Unable to import matplotlib, skipping plot')
 
         # Write dat files
-        if self.settings['write_dat']:
+        if self.settings['save_data']:
             if type(eigenvalues) == complex:
                 np.savetxt(self.folder + "eigenvalues.dat", eigenvalues.view(float).reshape(-1, 2), fmt='%.12f',
                            delimiter='\t', newline='\n')
@@ -405,12 +413,9 @@ def run(self):
         if self.settings['write_modes_vtk']:
             try:
                 self.data.aero
-                aero_model = True
             except AttributeError:
                 warnings.warn('No aerodynamic model found - unable to project the mode onto aerodynamic grid')
-                aero_model = False
-
-            if aero_model:
+            else:
                 modalutils.write_modes_vtk(
                     self.data,
                     eigenvectors[:num_dof],
@@ -444,10 +449,16 @@ def run(self):
         if not self.settings['use_undamped_modes']:
             outdict['eigenvectors_left'] = eigenvectors_left
 
-        if self.settings['keep_linear_matrices']:
-            outdict['M'] = FullMglobal
-            outdict['C'] = FullCglobal
-            outdict['K'] = FullKglobal
+        if cg is not None:
+            outdict['cg'] = cg
+
+        outdict['M'] = FullMglobal
+        outdict['C'] = FullCglobal
+        outdict['K'] = FullKglobal
+
+        if t_pa is not None:
+            outdict['t_pa'] = t_pa
+            outdict['r_pa'] = r_pa
         self.data.structure.timestep_info[self.data.ts].modal = outdict
 
         if self.settings['print_info']:
diff --git a/sharpy/structure/utils/modalutils.py b/sharpy/structure/utils/modalutils.py
index e0dd0db3a..bdcbd7781 100644
--- a/sharpy/structure/utils/modalutils.py
+++ b/sharpy/structure/utils/modalutils.py
@@ -181,21 +181,18 @@ def get_mode_zeta(data, eigvect):
 
 
 def write_zeta_vtk(zeta, zeta_ref, filename_root):
-    '''
+    """
     Given a list of arrays representing the coordinates of a set of n_surf UVLM
     lattices and organised as:
         zeta[n_surf][3,M+1,N=1]
     this function writes a vtk for each of the n_surf surfaces.
 
-    Input:
-        - zeta: lattice coordinates to plot
-        - zeta_ref: reference lattice used to compute the magnitude of displacements
-        - filename_root: initial part of filename (full path) without file
-        extension (.vtk)
-    '''
+    Args:
+        zeta (np.array): lattice coordinates to plot
+        zeta_ref (np.array): reference lattice used to compute the magnitude of displacements
+        filename_root (str): initial part of filename (full path) without file extension (.vtk)
+    """
 
-    # from IPython import embed
-    # embed()
     for i_surf in range(len(zeta)):
 
         filename = filename_root + "_%02u.vtu" % (i_surf,)
@@ -295,11 +292,23 @@ def write_modes_vtk(data, eigenvectors, NumLambda, filename_root,
         write_zeta_vtk(zeta_mode, tsaero.zeta, filename_root + "_%06u" % (mode,))
 
 
-def free_modes_principal_axes(phi, mass_matrix, use_euler=False):
+def free_modes_principal_axes(phi, mass_matrix, use_euler=False, **kwargs):
     """
     Transforms the rigid body modes defined at with the A frame as reference to the centre of mass position and aligned
     with the principal axes of inertia.
 
+    Args:
+        phi (np.array): Eigenvectors defined at the ``A`` frame.
+        mass_matrix (np.array): System mass matrix
+        use_euler (bool): Use Euler rotation parametrisation rather than quaternions.
+
+    Keyword Args:
+        return_transform (bool): Return tuple containing transformed modes and the transformation from the ``A`` frame
+          to the ``P`` frame.
+
+    Returns:
+        np.array: Mass normalised modes with rigid modes defined at the centre of gravity and aligned with the
+          principal axes of inertia.
 
     References:
         Marc Artola, 2020
@@ -314,7 +323,7 @@ def free_modes_principal_axes(phi, mass_matrix, use_euler=False):
     m = mrr[0, 0]  # mass
 
     # principal axes of inertia matrix and transformation matrix
-    j_cm, t_rb = np.linalg.eig(mrr[-3:, -3:] + algebra.multiply_matrices(algebra.skew(r_cg), algebra.skew(r_cg)) * m)
+    j_cm, t_rb = principal_axes_inertia(mrr[-3:, -3:], r_cg, m)
 
     # rigid body mass matrix about CM and inertia in principal axes
     m_cm = np.eye(6) * m
@@ -335,7 +344,43 @@ def free_modes_principal_axes(phi, mass_matrix, use_euler=False):
 
     phit[-num_rigid_modes + 6:, 6:num_rigid_modes] = np.eye(num_rigid_modes - 6)  # euler or quaternion modes
 
-    return phit
+    if kwargs.get('return_transform', False):
+        return phit, t_rb, np.block([[np.eye(3), algebra.skew(r_cg)], [np.zeros((3, 3)), np.eye(3)]]).dot(trb_diag)
+    else:
+        return phit
+
+
+def principal_axes_inertia(j_a, r_cg, m):
+    r"""
+    Transform the inertia tensor :math:`\boldsymbol{j}_a` defined about the ``A`` frame of reference to the centre of
+    gravity and aligned with the principal axes of inertia.
+
+    The inertia tensor about the centre of gravity is obtained using the parallel axes theorem
+
+    .. math:: \boldsymbol{j}_{cm}  = \boldsymbol{j}_a + \tilde{r}_{cg}\tilde{r}_{cg}m
+
+    and rotated such that it is aligned with its eigenvectors and thus represents the inertia tensor about the principal
+    axes of inertia
+
+    .. math:: \boldsymbol{j}_p = T_{pa}^\top \boldsymbol{j}_{cm} T^{pa}
+
+    where :math:`T^{pa}` is the transformation matrix from the ``A`` frame to the principal axes ``P`` frame.
+
+    Args:
+        j_a (np.array): Inertia tensor defined about the ``A`` frame.
+        r_cg (np.array): Centre of gravity position defined in ``A`` coordinates.
+        m (float): Mass.
+
+    Returns:
+        tuple: Containing :math:`\boldsymbol{j}_p` and :math:`T^{pa}`
+
+    """
+
+    j_p, t_pa = np.linalg.eig(j_a + algebra.multiply_matrices(algebra.skew(r_cg), algebra.skew(r_cg)) * m)
+
+    t_pa, j_p = order_eigenvectors(t_pa, j_p)
+
+    return j_p, t_pa
 
 
 def mode_sign_convention(bocos, eigenvectors, rigid_body_motion=False, use_euler=False):
@@ -428,6 +473,24 @@ def order_rigid_body_modes(eigenvectors, use_euler):
     return eigenvectors
 
 
+def order_eigenvectors(eigenvectors, eigenvalues):
+    ordered_eigenvectors = np.zeros_like(eigenvectors)
+    new_order = []
+    for i in range(eigenvectors.shape[1]):
+        index_max_node = np.where(np.abs(eigenvectors[:, i]) == np.max(np.abs(eigenvectors[:, i])))[0][0]
+        ordered_eigenvectors[:, index_max_node] = eigenvectors[:, i] * np.sign(eigenvectors[index_max_node, i])
+        new_order.append(index_max_node)
+
+    try:
+        eigenvalues.shape[1]
+    except IndexError:
+        new_eigenvalues = eigenvalues[new_order]
+    else:
+        new_eigenvalues = eigenvalues[:, new_order]
+
+    return ordered_eigenvectors, new_eigenvalues
+
+
 def scale_mass_normalised_modes(eigenvectors, mass_matrix):
     r"""
     Scales eigenvector matrix such that the modes are mass normalised:
@@ -500,3 +563,96 @@ def assert_modes_mass_normalised(phi, m, tolerance, raise_error=False):
             raise e
         else:
             cout.cout_wrap('Eigenvectors are not mass normalised', 3)
+
+
+def modes_to_cg_ref(phi, M, rigid_body_motion=False, use_euler=False):
+    r"""
+
+    Returns the rigid body modes defined with respect to the centre of gravity
+
+    The transformation from the modes defined at the FoR A origin, :math:`\boldsymbol{\Phi}`, to the modes defined
+    using the centre of gravity as a reference is
+
+
+    .. math:: \boldsymbol{\Phi}_{rr,CG}|_{TRA} = \boldsymbol{\Phi}_{RR}|_{TRA} + \tilde{\mathbf{r}}_{CG}
+        \boldsymbol{\Phi}_{RR}|_{ROT}
+
+    .. math:: \boldsymbol{\Phi}_{rr,CG}|_{ROT} = \boldsymbol{\Phi}_{RR}|_{ROT}
+
+    Returns:
+        (np.array): Transformed eigenvectors
+    """
+    # if not rigid_body_motion:
+    #     return phi
+    # NG - 26/7/19 This is the transformation being performed by K_vec
+    # Leaving this here for now in case it becomes necessary
+    # .. math:: \boldsymbol{\Phi}_{ss,CG}|_{TRA} = \boldsymbol{\Phi}_{SS}|_{TRA} +\boldsymbol{\Phi}_{RS}|_{TRA}  -
+    # \tilde{\mathbf{r}}_{A}\boldsymbol{\Phi}_{RS}|_{ROT}
+    #
+    # .. math:: \boldsymbol{\Phi}_{ss,CG}|_{ROT} = \boldsymbol{\Phi}_{SS}|_{ROT}
+    # + (\mathbf{T}(\boldsymbol{\Psi})^\top)^{-1}\boldsymbol{\Phi}_{RS}|_{ROT}
+    # pos = self.data.structure.timestep_info[self.data.ts].pos
+    r_cg = cg(M)
+
+    # jj = 0
+    K_vec = np.zeros((phi.shape[0], phi.shape[0]))
+
+    # jj_for_vel = range(self.data.structure.num_dof.value, self.data.structure.num_dof.value + 3)
+    # jj_for_rot = range(self.data.structure.num_dof.value + 3, self.data.structure.num_dof.value + 6)
+
+    # for node_glob in range(self.data.structure.num_node):
+    #     ### detect bc at node (and no. of dofs)
+    #     bc_here = self.data.structure.boundary_conditions[node_glob]
+    #
+    #     if bc_here == 1:  # clamp (only rigid-body)
+    #         dofs_here = 0
+    #         jj_tra, jj_rot = [], []
+    #         continue
+    #
+    #     elif bc_here == -1 or bc_here == 0:  # (rigid+flex body)
+    #         dofs_here = 6
+    #         jj_tra = 6 * self.data.structure.vdof[node_glob] + np.array([0, 1, 2], dtype=int)
+    #         jj_rot = 6 * self.data.structure.vdof[node_glob] + np.array([3, 4, 5], dtype=int)
+    #     # jj_tra=[jj  ,jj+1,jj+2]
+    #     # jj_rot=[jj+3,jj+4,jj+5]
+    #     else:
+    #         raise NameError('Invalid boundary condition (%d) at node %d!' \
+    #                         % (bc_here, node_glob))
+    #
+    #     jj += dofs_here
+    #
+    #     ee, node_loc = self.data.structure.node_master_elem[node_glob, :]
+    #     psi = self.data.structure.timestep_info[self.data.ts].psi[ee, node_loc, :]
+    #
+    #     Ra = pos[node_glob, :]  # in A FoR with respect to G
+    #
+    #     K_vec[np.ix_(jj_tra, jj_tra)] += np.eye(3)
+    #     K_vec[np.ix_(jj_tra, jj_for_vel)] += np.eye(3)
+    #     K_vec[np.ix_(jj_tra, jj_for_rot)] -= algebra.skew(Ra)
+    #
+    #     K_vec[np.ix_(jj_rot, jj_rot)] += np.eye(3)
+    #     K_vec[np.ix_(jj_rot, jj_for_rot)] += np.linalg.inv(algebra.crv2tan(psi).T)
+    # NG - 26/7/19 - Transformation of the rigid part of the elastic modes ended up not being necessary but leaving
+    # here in case it becomes useful in the future (using K_vec)
+
+    # Rigid-Rigid modes transform
+    if use_euler:
+        num_rig_dof = 9
+    else:
+        num_rig_dof = 10
+    Krr = np.eye(num_rig_dof)
+    Krr[np.ix_([0, 1, 2], [3, 4, 5])] += algebra.skew(r_cg)
+
+    # Assemble transformed modes
+    phirr = Krr.dot(phi[-num_rig_dof:, :num_rig_dof])
+    # phiss = K_vec.dot(phi[:, 10:])
+
+    # Get rigid body modes to be positive in translation and rotation
+    for i in range(num_rig_dof):
+        ind = np.argmax(np.abs(phirr[:, i]))
+        phirr[:, i] = np.sign(phirr[ind, i]) * phirr[:, i]
+
+    phit = np.block([np.zeros((phi.shape[0], num_rig_dof)), phi[:, num_rig_dof:]])
+    phit[-num_rig_dof:, :num_rig_dof] = phirr
+
+    return phit
diff --git a/sharpy/structure/utils/xbeamlib.py b/sharpy/structure/utils/xbeamlib.py
index df4759ce6..82979b06c 100644
--- a/sharpy/structure/utils/xbeamlib.py
+++ b/sharpy/structure/utils/xbeamlib.py
@@ -132,9 +132,15 @@ def cbeam3_solv_nlnstatic(beam, settings, ts):
                             )
 
 
-def cbeam3_loads(beam, ts):
-    """@brief Python wrapper for f_cbeam3_loads
-     Alfonso del Carre
+def cbeam3_loads(beam, timestep):
+    """Python wrapper for f_cbeam3_loads
+    
+    Args:
+        beam (sharpy.structure.models.beam.Beam): Structural info class
+        timestep (sharpy.utils.datastructures.StructTimeStepInfo): Structural time step class
+
+    Returns:
+        tuple: Tuple containing the ``strains`` and ``loads``.
     """
     f_cbeam3_loads = xbeamlib.cbeam3_loads
     f_cbeam3_loads.restype = None
@@ -150,9 +156,9 @@ def cbeam3_loads(beam, ts):
                    ct.byref(n_nodes),
                    beam.fortran['connectivities'].ctypes.data_as(intP),
                    beam.ini_info.pos.ctypes.data_as(doubleP),
-                   beam.timestep_info[ts].pos.ctypes.data_as(doubleP),
+                   timestep.pos.ctypes.data_as(doubleP),
                    beam.ini_info.psi.ctypes.data_as(doubleP),
-                   beam.timestep_info[ts].psi.ctypes.data_as(doubleP),
+                   timestep.psi.ctypes.data_as(doubleP),
                    beam.fortran['stiffness_indices'].ctypes.data_as(intP),
                    ct.byref(n_stiff),
                    beam.fortran['stiffness'].ctypes.data_as(doubleP),
diff --git a/sharpy/utils/cout_utils.py b/sharpy/utils/cout_utils.py
index eaf24febb..3a14bcc4f 100644
--- a/sharpy/utils/cout_utils.py
+++ b/sharpy/utils/cout_utils.py
@@ -161,8 +161,9 @@ def __init__(self, n_fields=3, field_length=12, field_types=[['g']]*100, filenam
         if cout_wrap is None:
             start_writer()
 
-        if filename is not None:
-            self.file = open(filename, 'w')
+        # if filename is not None:
+            # self.file = open(filename, 'w')
+        self.file = filename
 
     def print_header(self, field_names):
         self.field_names = field_names
@@ -194,9 +195,10 @@ def print_header(self, field_names):
         cout_wrap(divider_line)
 
         if self.file is not None:
-            self.file.write(divider_line)
-            self.file.write('\n' + string.format(*(self.field_names)))
-            self.file.write('\n' + divider_line)
+            with open(self.file, 'a+') as f:
+                f.write(divider_line)
+                f.write('\n' + string.format(*(self.field_names)))
+                f.write('\n' + divider_line)
 
     def print_line(self, line_data):
         string = ''
@@ -214,18 +216,25 @@ def print_line(self, line_data):
         string += self.divider_char
         cout_wrap(string.format(line_data))
         if self.file is not None:
-            self.file.write('\n'+string.format(line_data))
+            with open(self.file, 'a') as f:
+                f.write('\n'+string.format(line_data))
 
     def close_file(self):
         if self.file is not None:
-            try:
-                self.file.write('\n' + self.divider_line)
-            except ValueError:
-                pass
-            self.file.close()
+            with open(self.file, 'a+') as f:
+                f.write('\n' + self.divider_line)
 
     def print_divider_line(self):
         cout_wrap(self.divider_line)
+        if self.file is not None:
+            with open(self.file, 'a+') as f:
+                f.write('\n' + self.divider_line)
+
+    def character_return(self, n_lines=1):
+        cout_wrap(n_lines * '\n')
+        if self.file is not None:
+            with open(self.file, 'a+') as f:
+                f.write(n_lines * '\n')
 
 
 # version tracker and output
diff --git a/sharpy/utils/datastructures.py b/sharpy/utils/datastructures.py
index d3ee1ac6c..c3c5408fe 100644
--- a/sharpy/utils/datastructures.py
+++ b/sharpy/utils/datastructures.py
@@ -920,7 +920,7 @@ class Linear(object):
     as class attributes the following classes that describe the linearised problem.
 
     Attributes:
-        ss (sharpy.linear.src.libss.ss): State-space system
+        ss (sharpy.linear.src.libss.StateSpace): State-space system
         linear_system (sharpy.linear.utils.ss_interface.BaseElement): Assemble system properties
         tsaero0 (sharpy.utils.datastructures.AeroTimeStepInfo): Linearisation aerodynamic timestep
         tsstruct0 (sharpy.utils.datastructures.StructTimeStepInfo): Linearisation structural timestep
diff --git a/sharpy/utils/frequencyutils.py b/sharpy/utils/frequencyutils.py
index ad59929af..a69656bc7 100644
--- a/sharpy/utils/frequencyutils.py
+++ b/sharpy/utils/frequencyutils.py
@@ -216,7 +216,7 @@ def hamiltonian(gamma, ss):
 
     Args:
         gamma (float): Evaluation point.
-        ss (sharpy.linear.src.libss.ss): Linear system.
+        ss (sharpy.linear.src.libss.StateSpace): Linear system.
 
     Returns:
         np.ndarray: Hamiltonian evaluated at ``gamma``.
@@ -255,7 +255,7 @@ def h_infinity_norm(ss, **kwargs):
         matrix. Systems and Control Letters, 14(4), 287–293. https://doi.org/10.1016/0167-6911(90)90049-Z
 
     Args:
-        ss (sharpy.linear.src.libss.ss): Multi input multi output system.
+        ss (sharpy.linear.src.libss.StateSpace): Multi input multi output system.
         **kwargs: Key-word arguments.
 
     Keyword Args:
@@ -395,7 +395,7 @@ def find_target_system(data, target_system):
         target_system (str): Desired target system.
 
     Returns:
-        sharpy.linear.src.libss.ss: State-space object of target system
+        sharpy.linear.src.libss.StateSpace: State-space object of target system
     """
 
     if target_system == 'aeroelastic':
diff --git a/sharpy/utils/linearutils.py b/sharpy/utils/linearutils.py
new file mode 100644
index 000000000..ec4ec7462
--- /dev/null
+++ b/sharpy/utils/linearutils.py
@@ -0,0 +1,110 @@
+"""Linear state-space vector manipulation utilities"""
+import numpy as np
+import sharpy.utils.algebra as algebra
+
+
+def structural_vector_to_timestep(vector, tstruct, structure, phi=None, num_rig_dof=0, copy_tstep=True):
+    """Transform a state-space structural vector into a time step object
+
+    This adds to a reference time step the following variables extracted from the vector:
+
+        * ``pos`` and ``pos_dot``
+        * ``psi`` and ``psi_dot``
+        * ``quat``
+        * ``for_pos`` and ``for_vel``
+
+    The rest of the structural time step variables are left unchanged
+
+    Args:
+        vector(np.ndarray): Vector to plot
+        tstruct (sharpy.utils.datastructures.StructTimeStepInfo): Reference time step
+        structure (sharpy.structure.models.beam.Beam): Structural information class
+        phi (np.ndarray, optional): Eigenvector matrix to transform vector back to nodal coordinates
+        num_rig_dof (int): Number of rigid degrees of freedom
+        copy_tstep (bool): Return a copy of the reference time step. Else, modify the input one.
+
+    Returns:
+        sharpy.utils.datastructures.StructTimeStepInfo: Time step with the aforementioned variables populated from
+          state-space vector.
+    """
+    n_dof = vector.shape[0]
+    v = vector[:n_dof//2]
+    v_dot = vector[n_dof//2:]
+    if phi is not None:  # modal coordinates
+        eta = phi.dot(v)
+        eta_dot = phi.dot(v_dot)
+    else:
+        eta = v
+        eta_dot = v_dot
+
+    if num_rig_dof != 0:
+        eta = eta[:-num_rig_dof]
+        eta_dot = eta_dot[:-num_rig_dof]
+        beta = eta_dot[-num_rig_dof:]
+        beta_bar = np.zeros_like(beta)
+    else:
+        beta = np.array([])
+        beta_bar = np.array([])
+
+    if copy_tstep:
+        tstep = tstruct.copy()
+    else:
+        tstep = tstruct
+
+    vdof = structure.vdof
+    num_dof = 6*sum(vdof >= 0)
+
+    q = np.zeros((num_dof + num_rig_dof))
+    dqdt = np.zeros_like(q)
+    dqddt = np.zeros_like(q)
+
+    pos = np.zeros_like(tstep.pos)
+    pos_dot = np.zeros_like(tstep.pos_dot)
+
+    psi = np.zeros_like(tstep.psi)
+    psi_dot = np.zeros_like(tstep.psi_dot)
+
+    for_pos = np.zeros_like(tstep.for_pos)
+    for_vel = np.zeros_like(tstep.for_vel)
+    for_acc = np.zeros_like(tstep.for_acc)
+    quat = np.zeros_like(tstep.quat)
+
+    q[:num_dof + num_rig_dof] = np.concatenate((eta, beta_bar))
+    dqdt[:num_dof + num_rig_dof] = np.concatenate((eta_dot, beta))
+
+    for i_node in vdof[vdof >= 0]:
+        pos[i_node + 1, :] = q[6*i_node: 6*i_node + 3]
+        pos_dot[i_node + 1, :] = dqdt[6*i_node + 0: 6*i_node + 3]
+
+    for i_elem in range(tstep.num_elem):
+        for i_node in range(tstep.num_node_elem):
+            psi[i_elem, i_node, :] = np.linalg.inv(algebra.crv2tan(tstep.psi[i_elem, i_node]).T).dot(q[i_node + 3: i_node + 6])
+            psi_dot[i_elem, i_node, :] = dqdt[i_node + 3: i_node + 6]
+
+    if num_rig_dof != 0: # beam is clamped
+        for_vel = beta[:6]
+        if num_rig_dof == 9:
+            quat = algebra.euler2quat(beta[-3:])
+        elif num_rig_dof == 10:
+            quat = beta[-4:]
+        else:
+            raise NotImplementedError('Structural vector to timestep for cases without 9 or 10 rigid'
+                                      'degrees of freedom not yet supported.')
+
+    tstep.q[:len(q)] += q
+    tstep.dqdt[:len(q)] += dqdt
+
+    tstep.pos += pos
+    tstep.pos_dot += pos_dot
+
+    tstep.psi += psi
+    tstep.psi_dot += psi_dot
+
+    tstep.for_pos += for_pos
+    tstep.for_vel += for_vel
+
+    tstep.quat += quat
+    tstep.quat /= np.linalg.norm(tstep.quat)  # normalise quaternion
+
+    return tstep
+
diff --git a/sharpy/utils/settings.py b/sharpy/utils/settings.py
index 50955d2d7..140a1c494 100644
--- a/sharpy/utils/settings.py
+++ b/sharpy/utils/settings.py
@@ -171,7 +171,7 @@ def to_custom_types(dictionary, types, default, options=dict(), no_ctype=True):
                 continue
             if isinstance(dictionary[k], list):
                 for i in range(len(dictionary[k])):
-                    dictionary[k][i] = float(dictionary[k][i])
+                    dictionary[k][i] = complex(dictionary[k][i])
                 dictionary[k] = np.array(dictionary[k])
                 continue
             # dictionary[k] = dictionary[k].split(',')


Test patch:
diff --git a/.github/workflows/sharpy_no_test_needed.yaml b/.github/workflows/sharpy_no_test_needed.yaml
new file mode 100644
index 000000000..f5fa4656d
--- /dev/null
+++ b/.github/workflows/sharpy_no_test_needed.yaml
@@ -0,0 +1,18 @@
+name: Python package
+
+on:
+  pull_request:
+    branches:
+      - master
+      - develop
+      - 'rc*'
+    paths-ignore:
+      - '*.py'
+      - 'lib/**'
+      - '.github/workflows/**'
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - run: 'echo "No changes to python files, submodules or workflows, no run required" '
diff --git a/.github/workflows/sharpy_tests.yaml b/.github/workflows/sharpy_tests.yaml
index 620713e3d..77754c028 100644
--- a/.github/workflows/sharpy_tests.yaml
+++ b/.github/workflows/sharpy_tests.yaml
@@ -4,6 +4,8 @@ on:
   push:
     paths:
       - '*.py'
+      - 'lib/*'
+      - '.github/workflows/*'
   pull_request:
     branches:
       - master
diff --git a/tests/coupled/prescribed/WindTurbine/test_rotor.py b/tests/coupled/prescribed/WindTurbine/test_rotor.py
index 2e8c1ba80..2d29faf76 100644
--- a/tests/coupled/prescribed/WindTurbine/test_rotor.py
+++ b/tests/coupled/prescribed/WindTurbine/test_rotor.py
@@ -100,7 +100,7 @@ def setUp(self):
         SimInfo.solvers['BeamLoader']['unsteady'] = 'on'
 
         SimInfo.solvers['Modal']['write_modes_vtk'] = False
-        SimInfo.solvers['Modal']['write_dat'] = True
+        SimInfo.solvers['Modal']['save_data'] = True
 
         ######################################################################
         #######################  GENERATE FILES  #############################
diff --git a/tests/io/generate_pazy_udpout.py b/tests/io/generate_pazy_udpout.py
index 8d1ecdff1..2cb47caad 100644
--- a/tests/io/generate_pazy_udpout.py
+++ b/tests/io/generate_pazy_udpout.py
@@ -170,8 +170,7 @@ def generate_pazy_udp(u_inf, case_name, output_folder='/output/', cases_folder='
     ws.config['Modal'] = {'NumLambda': 20,
                           'rigid_body_modes': 'off',
                           'print_matrices': 'on',
-                          'keep_linear_matrices': 'on',
-                          'write_dat': 'off',
+                          'save_data': 'off',
                           'rigid_modes_cg': 'off',
                           'continuous_eigenvalues': 'off',
                           'dt': 0,
diff --git a/tests/io/sample_udp_inout/generate_pazy_test_io_local.py b/tests/io/sample_udp_inout/generate_pazy_test_io_local.py
index c9d225da2..f74d93f8b 100644
--- a/tests/io/sample_udp_inout/generate_pazy_test_io_local.py
+++ b/tests/io/sample_udp_inout/generate_pazy_test_io_local.py
@@ -163,8 +163,7 @@ def generate_pazy(u_inf, case_name, output_folder='/output/', cases_folder='', *
     ws.config['Modal'] = {'NumLambda': 20,
                           'rigid_body_modes': 'off',
                           'print_matrices': 'on',
-                          'keep_linear_matrices': 'on',
-                          'write_dat': 'off',
+                          'save_data': 'off',
                           'rigid_modes_cg': 'off',
                           'continuous_eigenvalues': 'off',
                           'dt': 0,
diff --git a/tests/linear/control_surfaces/test_control_surfaces.py b/tests/linear/control_surfaces/test_control_surfaces.py
index 370b7a608..a8f8e64f2 100644
--- a/tests/linear/control_surfaces/test_control_surfaces.py
+++ b/tests/linear/control_surfaces/test_control_surfaces.py
@@ -3,17 +3,20 @@
 import unittest
 import sharpy.cases.templates.flying_wings as wings
 import sharpy.sharpy_main
+import pickle
 
 
-@unittest.skip('Control Surface Test for visual inspection')
+# @unittest.skip('Control Surface Test for visual inspection')
 class TestGolandControlSurface(unittest.TestCase):
 
+
     def setup(self):
+        self.deflection_degrees = 5
         # Problem Set up
         u_inf = 1.
         alpha_deg = 0.
         rho = 1.02
-        num_modes = 4
+        num_modes = 2
 
         # Lattice Discretisation
         M = 4
@@ -56,6 +59,11 @@ def setup(self):
         ws.update_derived_params()
         ws.set_default_config_dict()
 
+        # Full moving control surface on the right wing
+        ws.control_surface_chord[0] = M
+        ws.control_surface_hinge_coord[0] = 0.5
+        ws.control_surface[4, :] = 1
+
         ws.generate_aero_file()
         ws.generate_fem_file()
 
@@ -63,7 +71,8 @@ def setup(self):
         lin_tsteps = 10
         u_vec = np.zeros((lin_tsteps, 8))
         # elevator
-        u_vec[:, 4] = 10 * np.pi / 180
+        u_vec[:, 4] = self.deflection_degrees * np.pi / 180
+        np.savetxt(self.route_test_dir + '/cases/elevator.txt', u_vec[:, 4])
         ws.create_linear_files(x0, u_vec)
 
         ws.config['SHARPy'] = {
@@ -74,11 +83,12 @@ def setup(self):
                  'BeamPlot',
                  'Modal',
                  'LinearAssembler',
-                 'FrequencyResponse',
+                 # 'FrequencyResponse',
                  'LinDynamicSim',
+                 'PickleData',
                  ],
             'case': ws.case_name, 'route': ws.route,
-            'write_screen': 'on', 'write_log': 'on',
+            'write_screen': 'off', 'write_log': 'on',
             'log_folder': self.route_test_dir + '/output/' + ws.case_name + '/',
             'log_file': ws.case_name + '.log'}
 
@@ -154,8 +164,7 @@ def setup(self):
         ws.config['Modal'] = {'NumLambda': 20,
                               'rigid_body_modes': 'off',
                               'print_matrices': 'on',
-                              'keep_linear_matrices': 'on',
-                              'write_dat': 'off',
+                              'save_data': 'off',
                               'rigid_modes_cg': 'off',
                               'continuous_eigenvalues': 'off',
                               'dt': 0,
@@ -186,24 +195,59 @@ def setup(self):
                                                               'remove_predictor': remove_predictor,
                                                               'use_sparse': use_sparse,
                                                               'remove_inputs': ['u_gust']},
-                                            'rigid_body_motion': 'off'}}
+                                        }
+                                        }
 
         ws.config['LinDynamicSim'] = {'n_tsteps': lin_tsteps,
-                                     'dt': ws.dt,
-                                     'postprocessors': ['AerogridPlot'],
-                                     'postprocessors_settings':
-                                         {'AerogridPlot': {'include_rbm': 'on',
-                                                           'include_applied_forces': 'on',
-                                                           'minus_m_star': 0}, }
-                                     }
+                                      'dt': ws.dt,
+                                      'input_generators': [{'name': 'control_surface_deflection',
+                                                            'index': 0,
+                                                            'file_path': self.route_test_dir + '/cases/elevator.txt'},
+                                                           {'name': 'control_surface_deflection',
+                                                            'index': 1,
+                                                            'file_path': self.route_test_dir + '/cases/elevator.txt'}],
+                                      'postprocessors': ['AerogridPlot'],
+                                      'postprocessors_settings':
+                                          {'AerogridPlot': {'include_rbm': 'on',
+                                                            'include_applied_forces': 'on',
+                                                            'minus_m_star': 0}, }
+                                      }
+
+        ws.config['PickleData'] = {'folder': self.route_test_dir + '/output/'}
 
         ws.config.write()
 
-        self.data = sharpy.sharpy_main.main(['', ws.route + ws.case_name + '.sharpy'])
+        restart = False # useful for debugging to load pickle
+        if not restart:
+            self.data = sharpy.sharpy_main.main(['', ws.route + ws.case_name + '.sharpy'])
+        else:
+            with open(self.route_test_dir + '/output/' + ws.case_name + '.pkl', 'rb') as f:
+                self.data = pickle.load(f)
 
     def test_control_surface(self):
         self.setup()
 
+        for i_surf in range(2):
+            with self.subTest(i_surf=i_surf):
+                zeta = self.data.aero.timestep_info[-1].zeta[i_surf]
+                zeta0 = self.data.linear.tsaero0.zeta[i_surf]
+
+                # zeta indices [(xyz), chord, span]
+                if i_surf == 0:
+                    # Full moving control surface with hinge at c=0.5
+                    zeta_elev = zeta[:, -1, -1] - zeta[:, 0, -1]  # elevator starts at chordwise node 0
+                    zeta_0elev = zeta0[:, -1, -1] - zeta0[:, 0, -1]
+                elif i_surf == 1:
+                    # mirrored surface. span index 0 is the wing tip
+                    zeta_elev = zeta[:, -1, 0] - zeta[:, 2, 0]  # elevator starts at chordwise node 2
+                    zeta_0elev = zeta0[:, -1, 0] - zeta0[:, 2, 0]
+
+                deflection = np.arccos((zeta_elev.dot(zeta_0elev)) / np.linalg.norm(zeta_elev) / np.linalg.norm(zeta_0elev))
+
+                deflection_actual_deg = deflection * 180 / np.pi
+                np.testing.assert_array_almost_equal(self.deflection_degrees, deflection_actual_deg,
+                                                     decimal=2)
+
     def tearDown(self):
         import shutil
         folders = ['cases', 'figures', 'output']
diff --git a/tests/linear/derivatives/test_stabilityderivatives.py b/tests/linear/derivatives/test_stabilityderivatives.py
new file mode 100644
index 000000000..60213caa8
--- /dev/null
+++ b/tests/linear/derivatives/test_stabilityderivatives.py
@@ -0,0 +1,511 @@
+import numpy as np
+import os
+import unittest
+import sharpy.cases.templates.flying_wings as wings
+import sharpy.sharpy_main
+import h5py
+import sharpy.utils.h5utils as h5utils
+import sharpy.utils.algebra as algebra
+import sharpy.linear.src.libss as libss
+
+
+class TestLinearDerivatives(unittest.TestCase):
+
+    print_info = False
+
+    def run_sharpy(self, alpha_deg, flow, target_system, not_run=False):
+        # Problem Set up
+        u_inf = 10.
+        rho = 1.02
+        num_modes = 4
+
+        # Lattice Discretisation
+        M = 4
+        N = 4
+        M_star_fact = 10
+
+        # Linear UVLM settings
+        integration_order = 2
+        remove_predictor = False
+        use_sparse = True
+
+        # ROM Properties
+        rom_settings = dict()
+        rom_settings['algorithm'] = 'mimo_rational_arnoldi'
+        rom_settings['r'] = 6
+        rom_settings['single_side'] = 'observability'
+        frequency_continuous_k = np.array([0.])
+
+        # Case Admin - Create results folders
+        case_name = 'wing'
+        if target_system == 'aerodynamic':
+            case_name += '_uvlm'
+        elif target_system == 'aeroelastic':
+            case_name += '_coupled'
+        else:
+            NameError('Unrecognised system')
+
+        case_nlin_info = '_uinf{:04g}_a{:04g}'.format(u_inf*10, alpha_deg*100)
+        case_name += case_nlin_info
+
+        self.route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
+        # SHARPy nonlinear reference solution
+        ws = wings.QuasiInfinite(M=M,
+                                 aspect_ratio=10,
+                                 N=N,
+                                 Mstar_fact=M_star_fact,
+                                 u_inf=u_inf,
+                                 alpha=alpha_deg,
+                                 rho=rho,
+                                 sweep=0,
+                                 n_surfaces=2,
+                                 route=self.route_test_dir + '/cases',
+                                 case_name=case_name)
+
+        ws.gust_intensity = 0.01
+        ws.sigma = 1e-1
+
+        ws.clean_test_files()
+        ws.update_derived_params()
+        ws.set_default_config_dict()
+
+        ws.generate_aero_file()
+        ws.generate_fem_file()
+
+        frequency_continuous_w = 2 * u_inf * frequency_continuous_k / ws.c_ref
+        rom_settings['frequency'] = frequency_continuous_w
+
+        ws.config['SHARPy'] = {
+            'flow': flow,
+            'case': ws.case_name, 'route': ws.route,
+            'write_screen': 'off', 'write_log': 'on',
+            'log_folder': self.route_test_dir + '/output/',
+            'log_file': ws.case_name + '.log'}
+
+        ws.config['BeamLoader'] = {
+            'unsteady': 'off',
+            'orientation': ws.quat}
+
+        ws.config['AerogridLoader'] = {
+            'unsteady': 'off',
+            'aligned_grid': 'on',
+            'mstar': ws.Mstar_fact * ws.M,
+            'freestream_dir': ws.u_inf_direction,
+            'wake_shape_generator': 'StraightWake',
+            'wake_shape_generator_input': {'u_inf': ws.u_inf,
+                                           'u_inf_direction': ws.u_inf_direction,
+                                           'dt': ws.dt}}
+
+        ws.config['StaticUvlm'] = {
+            'rho': ws.rho,
+            'velocity_field_generator': 'SteadyVelocityField',
+            'velocity_field_input': {
+                'u_inf': ws.u_inf,
+                'u_inf_direction': ws.u_inf_direction},
+            'rollup_dt': ws.dt,
+            'print_info': 'on',
+            'horseshoe': 'off',
+            'num_cores': 4,
+            'n_rollup': 0,
+            'rollup_aic_refresh': 0,
+            'rollup_tolerance': 1e-4}
+
+        ws.config['StaticCoupled'] = {
+            'print_info': 'on',
+            'max_iter': 200,
+            'n_load_steps': 1,
+            'tolerance': 1e-10,
+            'relaxation_factor': 0.,
+            'aero_solver': 'StaticUvlm',
+            'aero_solver_settings': {
+                'rho': ws.rho,
+                'print_info': 'off',
+                'horseshoe': 'off',
+                'num_cores': 4,
+                'n_rollup': 0,
+                'rollup_dt': ws.dt,
+                'rollup_aic_refresh': 1,
+                'rollup_tolerance': 1e-4,
+                'velocity_field_generator': 'SteadyVelocityField',
+                'velocity_field_input': {
+                    'u_inf': ws.u_inf,
+                    'u_inf_direction': ws.u_inf_direction}},
+            'structural_solver': 'NonLinearStatic',
+            'structural_solver_settings': {'print_info': 'off',
+                                           'max_iterations': 150,
+                                           'num_load_steps': 4,
+                                           'delta_curved': 1e-1,
+                                           'min_delta': 1e-10,
+                                           'gravity_on': 'off',
+                                           'gravity': 9.81}}
+
+        ws.config['AerogridPlot'] = {'include_rbm': 'off',
+                                     'include_applied_forces': 'on',
+                                     'minus_m_star': 0}
+
+        ws.config['AeroForcesCalculator'] = {'write_text_file': 'on',
+                                             'screen_output': 'on'}
+
+        ws.config['BeamPlot'] = {'include_rbm': 'off',
+                                 'include_applied_forces': 'on'}
+
+        ws.config['BeamCsvOutput'] = {'output_pos': 'on',
+                                      'output_psi': 'on',
+                                      'screen_output': 'on'}
+
+        ws.config['Modal'] = {'print_info': 'on',
+                              'use_undamped_modes': 'on',
+                              'NumLambda': 20,
+                              'rigid_body_modes': 'on',
+                              'write_modes_vtk': 'on',
+                              'print_matrices': 'off',
+                              'save_data': 'on',
+                              'rigid_modes_cg': 'off'}
+
+        settings = {}
+        settings['NonLinearDynamicCoupledStep'] = {'print_info': 'off',
+                                                   'max_iterations': 950,
+                                                   'delta_curved': 1e-1,
+                                                   'min_delta': ws.tolerance,
+                                                   'newmark_damp': 5e-3,
+                                                   'gravity_on': 'on',
+                                                   'gravity': 9.81,
+                                                   'num_steps': 4,
+                                                   'dt': ws.dt,
+                                                   'initial_velocity': u_inf}
+
+        relative_motion = 'off'
+        settings['StepUvlm'] = {'print_info': 'off',
+                                'horseshoe': 'off',
+                                'num_cores': 4,
+                                'n_rollup': 0,
+                                'convection_scheme': 2,
+                                'rollup_dt': ws.dt,
+                                'rollup_aic_refresh': 1,
+                                'rollup_tolerance': 1e-4,
+                                'gamma_dot_filtering': 6,
+                                'vortex_radius': 1e-8,
+                                'velocity_field_generator': 'SteadyVelocityField',
+                                'velocity_field_input': {'u_inf': 0 * u_inf,
+                                                         'u_inf_direction': [1., 0, 0]},
+                                'rho': rho,
+                                'n_time_steps': 1,
+                                'dt': ws.dt}
+
+        solver = 'NonLinearDynamicCoupledStep'
+        ws.config['DynamicCoupled'] = {'structural_solver': solver,
+                                      'structural_solver_settings': settings[solver],
+                                      'aero_solver': 'StepUvlm',
+                                      'aero_solver_settings': settings['StepUvlm'],
+                                      'fsi_substeps': 200,
+                                      'fsi_tolerance': ws.tolerance,
+                                      'relaxation_factor': ws.relaxation_factor,
+                                      'minimum_steps': 1,
+                                      'relaxation_steps': 150,
+                                      'final_relaxation_factor': 0.5,
+                                      'n_time_steps': 1,
+                                      'dt': ws.dt,
+                                      'include_unsteady_force_contribution': 'off',
+                                                                  }
+
+        ws.config['LinearAssembler'] = {'linear_system': 'LinearAeroelastic',
+                                       'linear_system_settings': {
+                                           'track_body': 'off',
+                                           'beam_settings': {'modal_projection': 'on',
+                                                             'inout_coords': 'modes',
+                                                             'discrete_time': 'off',
+                                                             'newmark_damp': 5e-4,
+                                                             'discr_method': 'newmark',
+                                                             'dt': ws.dt,
+                                                             'proj_modes': 'undamped',
+                                                             'use_euler': 'on',
+                                                             'num_modes': 20,
+                                                             'print_info': 'on',
+                                                             'gravity': 'off',
+                                                             'remove_dofs': [],
+                                                             'remove_rigid_states': 'on'},
+                                           'aero_settings': {'dt': ws.dt,
+                                                             # 'ScalingDict': {'density': rho,
+                                                             #                 'length': ws.c_ref * 0.5,
+                                                             #                 'speed': u_inf},
+                                                             'integr_order': 2,
+                                                             'density': rho,
+                                                             'remove_predictor': 'off',
+                                                             'use_sparse': 'off',
+                                                             'vortex_radius': 1e-8,
+                                                             'convert_to_ct': 'on',
+                                                             'remove_inputs': ['u_gust'],
+                                                             # 'rom_method': ['Krylov'],
+                                                             'rom_method_settings': {
+                                                                 'Krylov': {'algorithm': 'mimo_rational_arnoldi',
+                                                                            'frequency': [0.],
+                                                                            'r': 4,
+                                                                            'single_side': 'observability'}}
+                                                             },
+                                           'use_euler': 'on',
+                                       },
+                                       }
+
+        ws.config['FrequencyResponse'] = {'quick_plot': 'off',
+                                          'frequency_unit': 'k',
+                                          'frequency_bounds': [0.0001, 1.0],
+                                          'num_freqs': 100,
+                                          'frequency_spacing': 'log',
+                                          'target_system': ['aeroelastic'],
+                                          }
+
+        ws.config['StabilityDerivatives'] = {'u_inf': ws.u_inf,
+                                             'c_ref': ws.main_chord,
+                                             'b_ref': ws.wing_span,
+                                             'S_ref': ws.wing_span * ws.main_chord,
+                                             }
+
+        ws.config['AsymptoticStability'] = {'print_info': 'on'}
+
+        ws.config['SaveParametricCase'] = {'save_case': 'off',
+                                           'parameters': {'alpha': alpha_deg},
+                                           'save_pmor_items': 'on',
+                                           'save_pmor_subsystems': 'on'}
+
+        ws.config['SaveData'] = {'save_aero': 'off',
+                                 'save_struct': 'off',
+                                 'save_linear': 'on',
+                                 'save_linear_uvlm': 'off',
+                                 'format': 'mat',
+                                 }
+
+        ws.config.write()
+
+        if not not_run:
+            self.data = sharpy.sharpy_main.main(['', ws.route + ws.case_name + '.sharpy'])
+
+        return ws
+
+    def test_derivatives(self):
+        # target_system_list = ['aerodynamic']#, 'aeroelastic']
+        # target_system_list = ['aeroelastic']
+        target_system_list = ['aerodynamic', 'aeroelastic']
+        for system in target_system_list:
+            with self.subTest(target_system=system):
+                self.run_case(target_system=system)
+
+    def run_case(self, target_system):
+
+        if target_system == 'aerodynamic':
+            nonlinear_solver = ['StaticUvlm']
+        elif target_system == 'aeroelastic':
+            nonlinear_solver = ['StaticCoupled']
+        else:
+            NameError('Unrecognised system')
+
+        case_name_db = []
+        not_run = False # for debugging
+        # Reference Case at 4 degrees
+        # Run nonlinear simulation and save linerised ROM
+        alpha_deg_ref = 0
+        alpha0 = alpha_deg_ref * np.pi/180
+        ref = self.run_sharpy(alpha_deg=alpha_deg_ref,
+                              flow=['BeamLoader',
+                                    'AerogridLoader', ] +
+                                   nonlinear_solver +
+                                   ['AeroForcesCalculator',
+                                    'Modal',
+                                    'LinearAssembler',
+                                    # 'AsymptoticStability',
+                                    'StabilityDerivatives',
+                                    'SaveData',
+                                    'SaveParametricCase'],
+                              target_system=target_system,
+                              not_run=False)
+        u_inf = ref.u_inf
+        ref_case_name = ref.case_name
+        case_name_db.append(ref_case_name)
+        qS = 0.5 * ref.rho * u_inf ** 2 * ref.c_ref * ref.wing_span
+        if self.print_info:
+            print(f'Reference span: {ref.wing_span} m')
+            print(f'Reference chord: {ref.main_chord} m')
+            print(f'Aspect ratio: {ref.aspect_ratio}')
+
+        # Run nonlinear cases in the vicinity
+        nonlinear_sim_flow = ['BeamLoader',
+                              'AerogridLoader'] + nonlinear_solver + ['AeroForcesCalculator', 'SaveParametricCase']
+        alpha_deg_min = alpha_deg_ref - 5e-3
+        alpha_deg_max = alpha_deg_ref + 5e-3
+        n_evals = 11
+        alpha_vec = np.linspace(alpha_deg_min, alpha_deg_max, n_evals)
+        nlin_forces_g = np.zeros((n_evals, 3))
+        nlin_forces_a = np.zeros((n_evals, 3))
+        for ith, alpha in enumerate(alpha_vec):
+            if alpha == alpha_deg_ref:
+                case_name = ref_case_name
+                continue
+            else:
+                case = self.run_sharpy(alpha, flow=nonlinear_sim_flow, target_system=target_system, not_run=not_run)
+                case_name_db.append(case.case_name)
+                case_name = case.case_name
+            nlin_forces = np.loadtxt(self.route_test_dir +
+                                     '/output/{:s}/forces/forces_aeroforces.txt'.format(case_name),
+                                     skiprows=1,
+                                     delimiter=',')
+            nlin_forces_g[ith, :3] = nlin_forces[1:4]
+            nlin_forces_a[ith, :3] = nlin_forces[7:10]
+        nlin_forces_g /= qS
+        nlin_forces_a /= qS
+
+        lift_poly = np.polyfit(alpha_vec * np.pi/180, nlin_forces_g[:, 2], deg=1)
+        nonlin_cla = lift_poly[0]
+        drag_poly = np.polyfit(alpha_vec * np.pi/180, nlin_forces_g[:, 0], deg=2)
+        nonlin_cda = 2 * drag_poly[0] * alpha0 + drag_poly[1]
+        if self.print_info:
+            print('Nonlinear coefficients')
+            print('CLa', nonlin_cla)
+            print('CDa', nonlin_cda)
+
+        lift_poly = np.polyfit(alpha_vec * np.pi/180, nlin_forces_a[:, 2], deg=1)
+        nonlin_cza = lift_poly[0]
+        drag_poly = np.polyfit(alpha_vec * np.pi/180, nlin_forces_a[:, 0], deg=2)
+        nonlin_cxa = 2 * drag_poly[0] * alpha0 + drag_poly[1]
+        if self.print_info:
+            print('Nonlinear coefficients - body axes')
+            print('CZa', nonlin_cza)
+            print('CXa', nonlin_cxa)
+
+        # Get Linear ROM at reference case
+        import scipy.io as scio
+        linss_data = scio.loadmat(self.route_test_dir + '/output/{:s}/savedata/{:s}.linss.mat'.format(ref_case_name,
+                                                                                                  ref_case_name))
+        
+        # Steady State transfer function
+        if target_system == 'aerodynamic':
+            ss = libss.StateSpace.load_from_h5(self.route_test_dir +
+                                               f'/output/{ref_case_name}/' +
+                                               f'save_pmor_data/{ref_case_name}_aerostatespace.h5')
+        else:
+            ss = libss.StateSpace.load_from_h5(self.route_test_dir +
+                                               f'/output/{ref_case_name}/' +
+                                               f'save_pmor_data/{ref_case_name}_statespace.h5')
+
+        H0 = ss.freqresp(np.array([1e-5]))[:, :, 0].real
+
+        cga = algebra.quat2rotation(algebra.euler2quat(np.array([0, alpha0, 0])))
+
+        vx_ind = 20  # x_a input index
+        vz_ind = 20 + 2  # z_a input index
+
+        n_evals = 2
+        forces = np.zeros((n_evals, 4))
+        moments = np.zeros_like(forces)
+        body_forces = np.zeros_like(forces)
+
+        phi = libss.Gain.load_from_h5(self.route_test_dir +
+                                      f'/output/{ref_case_name}/' +
+                                      f'save_pmor_data/{ref_case_name}_modal_structrob.h5').value.real # other Gain features not needed
+
+        eps = 1e-5
+        dalpha_vec = np.array([-eps, +eps])
+        for i_alpha, dalpha in enumerate(dalpha_vec):
+            alpha = alpha0 + dalpha  # rad
+            deuler = np.array([0, dalpha, 0])
+            euler0 = np.array([0, alpha0, 0])
+
+            u = np.zeros(ss.inputs)  # input vector
+            V0 = np.array([-1, 0, 0], dtype=float) * u_inf  # G
+            Vp = u_inf * np.array([-np.cos(dalpha), 0, -np.sin(dalpha)])  # G
+
+            dvg = Vp - V0  # G
+            dva = cga.T.dot(dvg)  # A
+            dvz = dva[2]
+            dvx = dva[0]
+
+            #         print(Vp)
+            #         print(algebra.euler2rot(deuler).T.dot(V0))
+            #         print(algebra.der_Peuler_by_v(euler0*0, V0))
+            Vp2 = algebra.euler2rot(euler0 * 0).T.dot(V0) + algebra.der_Peuler_by_v(euler0 * 0, V0).dot(deuler)
+            dva2 = cga.T.dot(algebra.euler2rot(deuler).T.dot(V0) - V0)
+            dva3 = cga.T.dot(Vp2 - V0)
+            #         print('{:.4f}\t'.format((alpha0 + dalpha) * 180 / np.pi), dva2, dva3, dva3-dva2)
+            dvz = dva3[2]
+            dvx = dva3[0]
+
+            # Need to scale the mode shapes by the rigid body mode factor
+            u[vx_ind] = dvx / phi[-9, 0]
+            u[vz_ind] = dvz / phi[-7, 2]
+
+            # and the same with the output forces
+            flin = H0.dot(u)[:3].real / phi[-9, 0]  # A
+            mlin = np.linalg.inv(phi[-6:-3, 3:6].T).dot(H0.dot(u)[3:6].real)  # A
+            F0A = linss_data['forces_aero_beam_dof'][0, :3].real / phi[-9, 0]  # A - forces at the linearisation
+            M0A = linss_data['forces_aero_beam_dof'][0, 3:6].real / phi[-6, 3]  # A - forces at the linearisation
+            LD0 = cga.dot(F0A)  # Lift and drag at the linearisation point
+            M0G = cga.dot(M0A)
+
+            forces[i_alpha, 0] = (alpha0 + dalpha) * 180 / np.pi  # deg
+            LD = LD0 + algebra.der_Ceuler_by_v(euler0, F0A).dot(deuler) + cga.dot(flin)  # stability axes
+            forces[i_alpha, 1:] = LD / qS
+
+            u_body = np.zeros_like(u)
+            u_body[vz_ind] = dvz / phi[-7, 2]
+            body_forces[i_alpha, 1:] = H0.dot(u_body)[:3].real / phi[-9, 0] / qS # C_Z_w in A frame
+            if self.print_info:
+                print('Transfer function coefficients (normalised by qS)')
+                print(H0[:3, vx_ind:vx_ind+3].real / phi[-7, 2] / phi[-9, 0] / qS)
+
+            MD = M0G + algebra.der_Ceuler_by_v(euler0, M0A).dot(deuler) + cga.dot(mlin)
+            moments[i_alpha, 0] = forces[i_alpha, 0]
+            moments[i_alpha, 1:] = MD / qS / ref.main_chord
+
+        cla = (forces[-1, -1] - forces[0, -1]) / (forces[-1, 0] - forces[0, 0]) * 180 / np.pi
+        cda = (forces[-1, 1] - forces[0, 1]) / (forces[-1, 0] - forces[0, 0]) * 180 / np.pi
+        cma = (moments[-1, 2] - moments[0, 2]) / (moments[-1, 0] - moments[0, 0]) * 180 / np.pi
+
+        if self.print_info:
+            print('Lift curve slope perturbation {:.6e}'.format(cla))
+            print('Drag curve slope perturbation {:.6e}'.format(cda))
+            print('Moment curve slope perturbation {:.6e}'.format(cma))
+
+        # body derivative
+        # czwa = (body_forces[-1, -1] - body_forces[0, -1]) / (forces[-1, 0] - forces[0, 0]) * 180 / np.pi
+        # print('Vertical force with vertical velocity perturbation {:.6e}'.format(czwa))
+
+        with h5py.File(self.route_test_dir + '/output/' + ref_case_name +
+                       '/derivatives/aerodynamic_stability_derivatives.h5', 'r') as f:
+            sharpy_force_angle = h5utils.load_h5_in_dict(f)['force_angle_velocity']
+
+        linsubtests = ((2, cla, 'lift'),
+                       (0, cda, 'drag'),
+                       (4, cma, 'moment'))
+        for test in linsubtests:
+            with self.subTest(test):
+                try:
+                    np.testing.assert_almost_equal(sharpy_force_angle[test[0], 1], test[1], decimal=3)
+                except AssertionError as e:
+                    print('Error Linear perturbation in {:s}'.format(test[2]))
+                    print(e)
+                    print('Pct error', np.abs(sharpy_force_angle[test[0], 1] - test[1]) / test[1] * 100)
+                    raise AssertionError
+
+        nonlinsubtests = ((2, nonlin_cla, 'lift'),
+                          (0, nonlin_cda, 'drag'),
+                          )
+
+        for test in nonlinsubtests:
+            with self.subTest(test):
+                try:
+                    np.testing.assert_almost_equal(sharpy_force_angle[test[0], 1], test[1], decimal=2)
+                except AssertionError as e:
+                    print('Error nonlinear perturbation in {:s}'.format(test[2]))
+                    print(e)
+                    print('Pct error', np.abs(sharpy_force_angle[test[0], 1] - test[1]) / test[1] * 100)
+                    raise AssertionError
+        return forces, moments
+
+    def tearDown(self):
+        import shutil
+        folders = ['cases', 'output']
+        for folder in folders:
+            shutil.rmtree(self.route_test_dir + '/' + folder)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/linear/frequencyutils/test_frequency_utils.py b/tests/linear/frequencyutils/test_frequency_utils.py
index 312de7830..bb4194d5f 100644
--- a/tests/linear/frequencyutils/test_frequency_utils.py
+++ b/tests/linear/frequencyutils/test_frequency_utils.py
@@ -21,7 +21,7 @@ def setUp(self):
         c = np.load(self.test_dir + '/src/c.npy')
         d = np.load(self.test_dir + '/src/d.npy')
 
-        self.sys = libss.ss(a, b, c, d, dt=None)
+        self.sys = libss.StateSpace(a, b, c, d, dt=None)
         # self.sys = libss.random_ss(10, 4, 3, dt=0.1, stable=True)
         # self.sys = libss.disc2cont(self.sys)
 
diff --git a/tests/linear/goland_wing/test_goland_flutter.py b/tests/linear/goland_wing/test_goland_flutter.py
index 93e102299..7bec6fcb9 100644
--- a/tests/linear/goland_wing/test_goland_flutter.py
+++ b/tests/linear/goland_wing/test_goland_flutter.py
@@ -160,8 +160,7 @@ def setup(self):
         ws.config['Modal'] = {'NumLambda': 20,
                               'rigid_body_modes': 'off',
                               'print_matrices': 'on',
-                              'keep_linear_matrices': 'on',
-                              'write_dat': 'off',
+                              'save_data': 'off',
                               'rigid_modes_cg': 'off',
                               'continuous_eigenvalues': 'off',
                               'dt': 0,
diff --git a/tests/linear/gusts/__init__.py b/tests/linear/gusts/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/linear/gusts/test_external_gust.py b/tests/linear/gusts/test_external_gust.py
new file mode 100644
index 000000000..d397f059d
--- /dev/null
+++ b/tests/linear/gusts/test_external_gust.py
@@ -0,0 +1,475 @@
+import numpy as np
+import sharpy.utils.algebra as algebra
+import sharpy.sharpy_main as smain
+import unittest
+import sharpy.cases.templates.flying_wings as wings
+import os
+import shutil
+
+
+def generate_sharpy(alpha=0., case_name='hale_static', case_route='./', **kwargs):
+
+    output_route = kwargs.get('output_route', './output/')
+    m = kwargs.get('M', 4)
+    rho = kwargs.get('rho', 1.225)
+    tolerance = kwargs.get('tolerance', 1e-5)
+    u_inf = kwargs.get('u_inf', 10)
+    tsteps = kwargs.get('tsteps', 100)
+
+    ws = wings.QuasiInfinite(M=m,
+                             aspect_ratio=10,
+                             N=8,
+                             Mstar_fact=10,
+                             u_inf=u_inf,
+                             alpha=alpha,
+                             rho=rho,
+                             sweep=0,
+                             n_surfaces=2,
+                             route=case_route,
+                             case_name=case_name)
+
+    ws.gust_intensity = 0.01
+    ws.sigma = 1e-1
+
+    ws.clean_test_files()
+    ws.update_derived_params()
+    ws.set_default_config_dict()
+
+    ws.generate_aero_file()
+    ws.generate_fem_file()
+
+    settings = dict()
+
+    settings['SHARPy'] = {'case': case_name,
+                          'route': case_route,
+                          'flow': kwargs.get('flow', []),
+                          'write_screen': 'off',
+                          'write_log': 'on',
+                          'log_folder': output_route,
+                          'log_file': case_name + '.log'}
+
+    settings['BeamLoader'] = {'unsteady': 'on',
+                              'orientation': algebra.euler2quat(np.array([0.,
+                                                                          alpha,
+                                                                          0.]))}
+
+    settings['AerogridLoader'] = {'unsteady': 'on',
+                                  'aligned_grid': 'on',
+                                  'mstar': int(kwargs.get('wake_length', 10) * m),
+                                  'control_surface_deflection': ['', ''],
+                                  'control_surface_deflection_generator_settings':
+                                      {'0': {},
+                                       '1': {}},
+                                  'wake_shape_generator': 'StraightWake',
+                                  'wake_shape_generator_input': {
+                                      'u_inf': u_inf,
+                                      'u_inf_direction': [1., 0., 0.],
+                                      'dt': ws.dt,
+                                  },
+                                  }
+
+    settings['NonLinearStatic'] = {'print_info': 'off',
+                                   'max_iterations': 150,
+                                   'num_load_steps': 1,
+                                   'delta_curved': 1e-1,
+                                   'min_delta': tolerance,
+                                   'gravity_on': kwargs.get('gravity', 'on'),
+                                   'gravity': 9.81,
+                                   'initial_position': [0., 0., 0.]}
+
+    settings['StaticUvlm'] = {'print_info': 'on',
+                              'horseshoe': kwargs.get('horseshoe', 'off'),
+                              'num_cores': 4,
+                              'velocity_field_generator': 'SteadyVelocityField',
+                              'velocity_field_input': {'u_inf': u_inf,
+                                                       'u_inf_direction': [1., 0, 0]},
+                              'rho': rho}
+
+    settings['StaticCoupled'] = {'print_info': 'off',
+                                 'structural_solver': 'NonLinearStatic',
+                                 'structural_solver_settings': settings['NonLinearStatic'],
+                                 'aero_solver': 'StaticUvlm',
+                                 'aero_solver_settings': settings['StaticUvlm'],
+                                 'max_iter': 100,
+                                 'n_load_steps': kwargs.get('n_load_steps', 1),
+                                 'tolerance': kwargs.get('fsi_tolerance', 1e-5),
+                                 'relaxation_factor': kwargs.get('relaxation_factor', 0.2)}
+
+    settings['BeamPlot'] = {'include_FoR': 'on'}
+
+    settings['AerogridPlot'] = {'include_rbm': 'off',
+                                'include_applied_forces': 'on',
+                                'minus_m_star': 0,
+                                'u_inf': u_inf
+                                }
+
+    settings['AeroForcesCalculator'] = {'write_text_file': 'on',
+                                        'text_file_name': 'aeroforces.txt',
+                                        'screen_output': 'on',
+                                        'coefficients': True,
+                                        'q_ref': 0.5 * rho * u_inf ** 2,
+                                        'S_ref': 12.809,
+                                        }
+
+    settings['BeamPlot'] = {'include_rbm': 'on',
+                            'include_applied_forces': 'on',
+                            'include_FoR': 'on'}
+
+    struct_solver_settings = {'print_info': 'off',
+                              'max_iterations': 950,
+                              'delta_curved': 1e-6,
+                              'min_delta': tolerance,
+                              'newmark_damp': 5e-3,
+                              'gravity_on': kwargs.get('gravity', 'on'),
+                              'gravity': 9.81,
+                              'num_steps': 1,
+                              'dt': ws.dt}
+
+    gust_vec = kwargs.get('nl_gust', None)
+    if gust_vec is not None:
+        t_dom = np.linspace(0, ws.dt * (tsteps-1), tsteps)
+        np.savetxt(ws.route + '/gust.txt', np.column_stack((
+            t_dom,
+            np.zeros_like(t_dom),
+            np.zeros_like(t_dom),
+            gust_vec
+        )))
+
+    step_uvlm_settings = {'print_info': 'on',
+                          'num_cores': 4,
+                          'convection_scheme': 0, #ws.wake_type,
+                          'vortex_radius': 1e-7,
+                          'velocity_field_generator': 'GustVelocityField',
+                          'velocity_field_input': {'u_inf': u_inf * 1,
+                                                   'u_inf_direction': [1., 0., 0.],
+                                                   'relative_motion': 'on',
+                                                   'gust_shape': 'time varying',
+                                                   'gust_parameters': {
+                                                       'file': ws.route + '/gust.txt',
+                                                   }},
+                          'rho': rho,
+                          'n_time_steps': tsteps,
+                          'dt': ws.dt,
+                          'gamma_dot_filtering': 3}
+
+    settings['DynamicCoupled'] = {'print_info': 'on',
+                                  'structural_solver': 'NonLinearDynamicPrescribedStep',
+                                  'structural_solver_settings': struct_solver_settings,
+                                  'aero_solver': 'StepUvlm',
+                                  'aero_solver_settings': step_uvlm_settings,
+                                  'fsi_substeps': 200,
+                                  'fsi_tolerance': tolerance,
+                                  'relaxation_factor': 0.3,
+                                  'minimum_steps': 1,
+                                  'relaxation_steps': 150,
+                                  'final_relaxation_factor': 0.5,
+                                  'n_time_steps': tsteps,  # ws.n_tstep,
+                                  'dt': ws.dt,
+                                  'include_unsteady_force_contribution': 'on',
+                                  'steps_without_unsteady_force': 5,
+                                  'postprocessors': ['BeamPlot', 'AerogridPlot', 'WriteVariablesTime'],
+                                  'postprocessors_settings': {'BeamLoads': {'csv_output': 'off'},
+                                                              'BeamPlot': {'include_rbm': 'on',
+                                                                           'include_applied_forces': 'on'},
+                                                              'AerogridPlot': {
+                                                                  'u_inf': u_inf,
+                                                                  'include_rbm': 'on',
+                                                                  'include_applied_forces': 'on',
+                                                                  'minus_m_star': 0},
+                                                              'WriteVariablesTime': {
+                                                                  'cleanup_old_solution': 'on',
+                                                                  'delimiter': ',',
+                                                                  'FoR_variables': ['total_forces',
+                                                                                    'total_gravity_forces',
+                                                                                    'for_pos', 'quat'],
+                                                              }}}
+
+    settings['Modal'] = {'print_info': True,
+                         'use_undamped_modes': True,
+                         'NumLambda': 50,
+                         'rigid_body_modes': 'off',
+                         'write_modes_vtk': 'on',
+                         'print_matrices': 'on',
+                         'save_data': 'on',
+                         'continuous_eigenvalues': 'off',
+                         'dt': ws.dt,
+                         'plot_eigenvalues': False,
+                         'rigid_modes_ppal_axes': 'on'}
+    # ROM settings
+    rom_settings = dict()
+    rom_settings['algorithm'] = 'mimo_rational_arnoldi'
+    rom_settings['r'] = 10
+    rom_settings['frequency'] = np.array([0], dtype=float)
+    rom_settings['single_side'] = 'observability'
+
+    settings['LinearAssembler'] = {'linear_system': 'LinearAeroelastic',
+                                   'linearisation_tstep': -1,
+                                   'linear_system_settings': {
+                                       'beam_settings': {'modal_projection': 'off',
+                                                         'inout_coords': 'modes',
+                                                         'discrete_time': 'on',
+                                                         'newmark_damp': 0.5e-4,
+                                                         'discr_method': 'newmark',
+                                                         'dt': ws.dt,
+                                                         'proj_modes': 'undamped',
+                                                         'use_euler': 'on',
+                                                         'num_modes': 20,
+                                                         'print_info': 'on',
+                                                         'gravity': kwargs.get('gravity', 'on'),
+                                                         'remove_dofs': [],
+                                                         'remove_rigid_states': 'off'},
+                                       'aero_settings': {'dt': ws.dt,
+                                                         'integr_order': 2,
+                                                         'density': rho,
+                                                         'remove_predictor': 'off',
+                                                         'use_sparse': False,
+                                                         'vortex_radius': 1e-7,
+                                                         'convert_to_ct': 'off',
+                                                         'gust_assembler': kwargs.get('gust_name', ''),
+                                                         'gust_assembler_inputs': kwargs.get('gust_settings', {}),
+                                                         },
+                                       'track_body': 'on',
+                                       'use_euler': 'on',
+                                   },
+                                   }
+
+    settings['AsymptoticStability'] = {
+        'print_info': 'on',
+        'modes_to_plot': [],
+        # 'velocity_analysis': [27, 29, 3],
+        'display_root_locus': 'off',
+        'frequency_cutoff': 0,
+        'export_eigenvalues': 'on',
+        'num_evals': 100}
+
+    settings['FrequencyResponse'] = {'target_system': ['aeroelastic', 'aerodynamic', 'structural'],
+                                     'quick_plot': 'off',
+                                     'frequency_spacing': 'log',
+                                     'frequency_unit': 'w',
+                                     'frequency_bounds': [1e-3, 1e3],
+                                     'num_freqs': 200,
+                                     'print_info': 'on'}
+
+    settings['PickleData'] = {}
+
+    settings['LinDynamicSim'] = {'n_tsteps': tsteps,
+                                 'dt': ws.dt,
+                                 'write_dat': ['x', 'y', 'u'],
+                                 'input_generators': kwargs.get('linear_input_generators', []),
+                                 'postprocessors': ['AerogridPlot'],
+                                 'postprocessors_settings':
+                                     {'AerogridPlot': {'include_rbm': 'on',
+                                                       'include_applied_forces': 'on',
+                                                       'minus_m_star': 0}, }
+                                 }
+
+    case_file = ws.settings_to_config(settings)
+    return case_file
+
+
+def run_case(case_file, pickle_file=None):
+    if pickle_file is not None:
+        restart = True
+    else:
+        restart = False
+    if restart:
+        data = smain.main(['', case_file, '-r', pickle_file])
+    else:
+        data = smain.main(['', case_file])
+
+    return data
+
+
+class TestGustAssembly(unittest.TestCase):
+
+    route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+    directories = ['cases', 'output', 'inputs']
+
+    time_steps = 80
+    gust_intensity = 0.01
+    gust_ramp = 5
+
+    M = 4
+    u_inf = 1
+
+    gust_vec = np.zeros(time_steps)
+
+    @classmethod
+    def setUpClass(cls):
+        for folder in cls.directories:
+            os.makedirs(cls.route_test_dir + '/' + folder, exist_ok=True)
+
+        # Gust input profile
+        cls.gust_vec[5:cls.gust_ramp+5] = np.linspace(0, cls.gust_intensity, cls.gust_ramp)
+        cls.gust_vec[5+ cls.gust_ramp:] = cls.gust_intensity
+
+    @classmethod
+    def tearDownClass(cls):
+        for folder in cls.directories:
+            if os.path.isdir(cls.route_test_dir + '/' + folder):
+                shutil.rmtree(cls.route_test_dir + '/' + folder)
+
+    def linear_response(self):
+
+        np.savetxt(self.route_test_dir + '/inputs/gust_linear.txt', self.gust_vec)
+
+        input_generators = [{'name': 'u_gust',
+                             'index': 0,
+                             'file_path': self.route_test_dir + '/inputs/gust_linear.txt'}]
+
+        data = self.run_sharpy_linear(gust_name='LeadingEdge', linear_input_generators=input_generators)#
+
+        return data
+
+    def nonlinear_response(self):
+
+        data = self.run_sharpy_nonlinear(nl_gust=self.gust_vec)
+
+        return data
+
+    def test_gust(self):
+
+        linear = self.linear_response()
+        nonlinear = self.nonlinear_response()
+        final_linear = linear.aero.timestep_info[-1]
+        final_nonlinear = nonlinear.aero.timestep_info[-1]
+        with self.subTest('zeta'):
+            np.testing.assert_almost_equal(final_linear.zeta[0][2, 0, -1],
+                                           final_nonlinear.zeta[0][2, 0, -1], decimal=4)
+        with self.subTest('forces'):
+            np.testing.assert_almost_equal(np.sum(final_linear.forces[0][2, :, 0]),
+                                           np.sum(final_nonlinear.forces[0][2, :, 0]), decimal=4)
+
+        with self.subTest('gamma'):
+            np.testing.assert_almost_equal(final_linear.gamma[0][2, 2],
+                                           final_nonlinear.gamma[0][2, 2], decimal=4)
+
+    def wingtip_timeseries(self, data):
+        nsteps = len(data.aero.timestep_info)
+
+        wingtip = np.zeros(nsteps)
+
+        for i in range(nsteps):
+            wingtip[i] = data.aero.timestep_info[i].zeta[0][2, 0, -1]
+
+        return wingtip
+
+    def gamma_timeseries(self, data):
+        nsteps = len(data.aero.timestep_info)
+
+        wingtip = np.zeros(nsteps)
+
+        for i in range(nsteps):
+            wingtip[i] = data.aero.timestep_info[i].gamma[0][0, -1]
+
+        return wingtip
+
+    def run_sharpy_nonlinear(self, **kwargs):
+        flow = [
+            'BeamLoader',
+            'AerogridLoader',
+            'StaticUvlm',
+            'DynamicCoupled',
+            # 'AeroForcesCalculator',
+            'PickleData',
+        ]
+
+
+        alpha = 0 #2 * np.pi / 180
+
+        # Discretisation
+        n_elem_multiplier = 1.5
+        wake_length = 10
+        horseshoe = False
+
+        case_file = generate_sharpy(alpha=alpha,
+                                    case_name='nonlinear',
+                                    case_route=self.route_test_dir + '/cases/',
+                                    output_route=self.route_test_dir + '/output/',
+                                    flow=flow,
+                                    u_inf=self.u_inf,
+                                    M=self.M,
+                                    n_elem_multiplier=n_elem_multiplier,
+                                    horseshoe=horseshoe,
+                                    wake_length=wake_length,
+                                    relaxation_factor=0.6,
+                                    tolerance=1e-5,
+                                    gravity='off',
+                                    fsi_tolerance=1e-5,
+                                    tsteps=self.time_steps,
+                                    **kwargs,
+                                    )
+
+        data = run_case(case_file)
+
+        return data
+
+    def run_sharpy_linear(self, **kwargs):
+
+        flow = [
+            'BeamLoader',
+            'AerogridLoader',
+            'StaticCoupled',
+            'Modal',
+            'AeroForcesCalculator',
+            'LinearAssembler',
+            'LinDynamicSim',
+            'PickleData',
+        ]
+
+        restart = False
+        if restart:
+            flow = ['LinDynamicSim']
+            pickle_file = self.route_test_dir + '/output/linear.pkl'
+        else:
+            pickle_file = None
+        alpha = 0 #2 * np.pi / 180
+        elevator = 0 #-0.5 * np.pi / 180
+        thrust = 0 #5
+
+        # Discretisation
+        M = 4
+        n_elem_multiplier = 1.5
+        wake_length = 10
+        horseshoe = False
+
+        case_file = generate_sharpy(alpha=alpha,
+                                    case_name='linear',
+                                    case_route=self.route_test_dir + '/cases/',
+                                    output_route=self.route_test_dir + '/output',
+                                    flow=flow,
+                                    u_inf=self.u_inf,
+                                    M=self.M,
+                                    n_elem_multiplier=n_elem_multiplier,
+                                    horseshoe=horseshoe,
+                                    wake_length=wake_length,
+                                    relaxation_factor=0.6,
+                                    tolerance=1e-5,
+                                    gravity='off',
+                                    fsi_tolerance=1e-5,
+                                    tsteps=self.time_steps,
+                                    **kwargs,
+                                    )
+
+        data = run_case(case_file, pickle_file)
+
+        return data
+
+    def assert_gust_propagation(self, path_to_input, output_route):
+        x_sharpy = np.loadtxt(output_route + '/lindynamicsim/x_out.dat')
+        u_in = np.loadtxt(path_to_input)
+
+        np.testing.assert_array_almost_equal(x_sharpy[1:, 0], u_in[:-1])
+
+    def tearDown(self):
+        folders = ['cases', 'output']
+        import shutil
+
+        for folder in folders:
+            path = self.route_test_dir + '/' + folder
+            if os.path.isdir(path):
+                shutil.rmtree(path)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/linear/gusts/test_linear_gusts.py b/tests/linear/gusts/test_linear_gusts.py
new file mode 100644
index 000000000..5f6079a3e
--- /dev/null
+++ b/tests/linear/gusts/test_linear_gusts.py
@@ -0,0 +1,305 @@
+import numpy as np
+import os
+import unittest
+import sharpy.cases.templates.flying_wings as wings
+import sharpy.sharpy_main
+from sharpy.linear.assembler.lineargustassembler import campbell
+import pickle
+
+
+class TestGolandControlSurface(unittest.TestCase):
+
+    def setUp(self):
+        self.route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
+    def run_sharpy(self, flow, **kwargs):
+        # Problem Set up
+        u_inf = 2.
+        alpha_deg = 0.
+        rho = 1.02
+        num_modes = 4
+        restart = kwargs.get('restart', False)
+
+        # Lattice Discretisation
+        M = 4
+        N = 16
+        M_star_fact = 1
+
+        # Linear UVLM settings
+        integration_order = 2
+        remove_predictor = kwargs.get('remove_predictor', False)
+        use_sparse = True
+
+        # Case Admin - Create results folders
+        case_name = 'goland_cs'
+        case_nlin_info = 'M%dN%dMs%d_nmodes%d' % (M, N, M_star_fact, num_modes)
+
+        case_name += case_nlin_info
+
+        fig_folder = self.route_test_dir + '/figures/'
+        os.makedirs(fig_folder, exist_ok=True)
+
+        # SHARPy nonlinear reference solution
+        ws = wings.GolandControlSurface(M=M,
+                                        N=N,
+                                        Mstar_fact=M_star_fact,
+                                        u_inf=u_inf,
+                                        alpha=alpha_deg,
+                                        # cs_deflection=[0, 0],
+                                        rho=rho,
+                                        sweep=0,
+                                        physical_time=2,
+                                        n_surfaces=2,
+                                        route=self.route_test_dir + '/cases',
+                                        case_name=case_name)
+
+        ws.gust_intensity = 0.01
+        ws.sigma = 1
+
+        ws.clean_test_files()
+        ws.update_derived_params()
+        ws.set_default_config_dict()
+
+        ws.generate_aero_file()
+        ws.generate_fem_file()
+
+        x0 = np.zeros(256)
+        lin_tsteps = 40
+        u_vec = np.zeros((lin_tsteps, num_modes // 2 * 3 + 1 + 2))
+        # elevator
+        u_vec[5:, 5] = 10 * np.pi / 180
+        # gust
+        u_vec[:, 4] = np.sin(np.linspace(0, 2*np.pi, lin_tsteps))
+        ws.create_linear_files(x0, u_vec)
+        np.savetxt(self.route_test_dir + '/cases/elevator.txt', u_vec[:, 5])
+        np.savetxt(self.route_test_dir + '/cases/gust.txt', u_vec[:, 4])
+
+        ws.config['SHARPy'] = {
+            'flow': flow,
+            'case': ws.case_name, 'route': ws.route,
+            'write_screen': 'off', 'write_log': 'on',
+            'log_folder': self.route_test_dir + '/output/' + ws.case_name + '/',
+            'log_file': ws.case_name + '.log'}
+
+        ws.config['BeamLoader'] = {
+            'unsteady': 'off',
+            'orientation': ws.quat}
+
+        ws.config['AerogridLoader'] = {
+            'unsteady': 'off',
+            'aligned_grid': 'on',
+            'mstar': ws.Mstar_fact * ws.M,
+            'freestream_dir': ws.u_inf_direction,                                                                                                       
+            'wake_shape_generator': 'StraightWake',                                                                                                  
+            'wake_shape_generator_input': {'u_inf': ws.u_inf,                                                                                           
+                                           'u_inf_direction': ws.u_inf_direction,                                                                
+                                           'dt': ws.dt}}
+
+        ws.config['StaticUvlm'] = {
+            'rho': ws.rho,
+            'velocity_field_generator': 'SteadyVelocityField',
+            'velocity_field_input': {
+                'u_inf': ws.u_inf,
+                'u_inf_direction': ws.u_inf_direction},
+            'rollup_dt': ws.dt,
+            'print_info': 'on',
+            'horseshoe': 'off',
+            'num_cores': 4,
+            'n_rollup': 0,
+            'rollup_aic_refresh': 0,
+            'rollup_tolerance': 1e-4}
+
+        ws.config['StaticCoupled'] = {
+            'print_info': 'on',
+            'max_iter': 200,
+            'n_load_steps': 1,
+            'tolerance': 1e-10,
+            'relaxation_factor': 0.,
+            'aero_solver': 'StaticUvlm',
+            'aero_solver_settings': {
+                'rho': ws.rho,
+                'print_info': 'off',
+                'horseshoe': 'off',
+                'num_cores': 4,
+                'n_rollup': 0,
+                'rollup_dt': ws.dt,
+                'rollup_aic_refresh': 1,
+                'rollup_tolerance': 1e-4,
+                'velocity_field_generator': 'SteadyVelocityField',
+                'velocity_field_input': {
+                    'u_inf': ws.u_inf,
+                    'u_inf_direction': ws.u_inf_direction}},
+            'structural_solver': 'NonLinearStatic',
+            'structural_solver_settings': {'print_info': 'off',
+                                           'max_iterations': 150,
+                                           'num_load_steps': 4,
+                                           'delta_curved': 1e-1,
+                                           'min_delta': 1e-10,
+                                           'gravity_on': 'on',
+                                           'gravity': 9.81}}
+
+        ws.config['AerogridPlot'] = {'include_rbm': 'off',
+                                     'include_applied_forces': 'on',
+                                     'minus_m_star': 0}
+
+        ws.config['AeroForcesCalculator'] = {'write_text_file': 'on',
+                                             'text_file_name': ws.case_name + '_aeroforces.csv',
+                                             'screen_output': 'on',
+                                             'unsteady': 'off'}
+
+        ws.config['BeamPlot'] = {'include_rbm': 'off',
+                                 'include_applied_forces': 'on'}
+
+        ws.config['Modal'] = {'NumLambda': 20,
+                              'rigid_body_modes': 'off',
+                              'print_matrices': 'on',
+                              'save_data': 'off',
+                              'rigid_modes_cg': 'off',
+                              'continuous_eigenvalues': 'off',
+                              'dt': 0,
+                              'plot_eigenvalues': False,
+                              'max_rotation_deg': 15.,
+                              'max_displacement': 0.15,
+                              'write_modes_vtk': True,
+                              'use_undamped_modes': True}
+
+        ws.config['LinearAssembler'] = {'linear_system': 'LinearAeroelastic',
+                                        'linear_system_settings': {
+                                            'beam_settings': {'modal_projection': 'on',
+                                                              'inout_coords': 'modes',
+                                                              'discrete_time': 'on',
+                                                              'newmark_damp': 0.5e-4,
+                                                              'discr_method': 'newmark',
+                                                              'dt': ws.dt,
+                                                              'proj_modes': 'undamped',
+                                                              'use_euler': 'off',
+                                                              'num_modes': num_modes,
+                                                              'print_info': 'off',
+                                                              'gravity': 'on',
+                                                              'remove_sym_modes': 'on',
+                                                              'remove_dofs': []},
+                                            'aero_settings': {'dt': ws.dt,
+                                                              'integr_order': integration_order,
+                                                              'density': ws.rho,
+                                                              'remove_predictor': remove_predictor,
+                                                              'use_sparse': use_sparse,
+                                                              'remove_inputs': [],
+                                                              'gust_assembler': 'LeadingEdge',
+                                                              },
+                                        }
+                                        }
+
+        ws.config['LinDynamicSim'] = {'n_tsteps': lin_tsteps,
+                                      'dt': ws.dt,
+                                      'input_generators': [
+                                          {'name': 'control_surface_deflection',
+                                           'index': 0,
+                                           'file_path': self.route_test_dir + '/cases/elevator.txt'},
+                                          {'name': 'u_gust',
+                                           'index': 0,
+                                           'file_path': self.route_test_dir + '/cases/gust.txt'}
+                                      ],
+                                      'postprocessors': ['AerogridPlot'],
+                                      'postprocessors_settings':
+                                          {'AerogridPlot': {'include_rbm': 'on',
+                                                            'include_applied_forces': 'on',
+                                                            'minus_m_star': 0}, }
+                                      }
+
+        ws.config['PickleData'] = {}
+        ws.config.write()
+
+        if not restart:
+            data = sharpy.sharpy_main.main(['', ws.route + ws.case_name + '.sharpy'])
+        else:
+            with open(self.route_test_dir + '/output/{:s}.pkl'.format(ws.case_name), 'rb') as f:
+                data = pickle.load(f)
+
+        return data
+
+    def run_linear_sharpy(self, **kwargs):
+        flow = ['BeamLoader', 'AerogridLoader',
+                'StaticCoupled',
+                'Modal',
+                'LinearAssembler',
+                'LinDynamicSim',
+                'PickleData']
+
+        restart = False
+        if restart:
+            flow = ['LinDynamicSim']
+
+        data = self.run_sharpy(flow, restart=restart, **kwargs)
+
+        return data
+
+    def extract_u_inf(self, data):
+        ts = len(data.aero.timestep_info)
+        _, m_chord, n_span = data.aero.timestep_info[0].u_ext[0].shape
+        u_inf_ext = np.zeros((ts, m_chord, n_span))
+        for i_ts in range(ts):
+            u_inf_ext[i_ts, :, :] = data.aero.timestep_info[i_ts].u_ext[0][2, :, :]
+
+        return u_inf_ext
+
+    def test_linear_gust(self):
+        test_conditions = [
+            {'remove_predictor': True},
+            {'remove_predictor': False},
+        ]
+        for test in test_conditions:
+            with self.subTest(test):
+                data = self.run_linear_sharpy(**test)
+                u_gust_in = np.loadtxt(self.route_test_dir + '/cases/gust.txt')
+                u_inf_ext = self.extract_u_inf(data)
+                if test['remove_predictor']:
+                    predictor_offset = 0
+                else:
+                    # input defined at time step n+1
+                    predictor_offset = 1
+
+                # test leading edge value is equal to input
+                np.testing.assert_array_almost_equal(u_inf_ext[1 + predictor_offset:, 0, 0],
+                                                     u_gust_in[:-1-predictor_offset])
+
+                # check convection in panels downstream
+                for i_chord in range(1, u_inf_ext.shape[1]):
+                    np.testing.assert_array_almost_equal(u_inf_ext[predictor_offset + 1 + i_chord:-i_chord, i_chord, 0],
+                                                         u_inf_ext[predictor_offset + 1 + i_chord - 1:-(i_chord + 1), i_chord - 1, 0])
+
+    def tearDown(self):
+        import shutil
+        folders = ['cases', 'figures', 'output']
+        for folder in folders:
+            if os.path.isdir(self.route_test_dir + '/' + folder):
+                shutil.rmtree(self.route_test_dir + '/' + folder)
+
+
+class TestGusts(unittest.TestCase):
+
+    def test_campbell(self):
+        """
+        Test that the Campell approximation to the Von Karman filter is equivalent in continuous and
+        discrete time
+
+        """
+        sigma_w = 1
+        length_scale = 1
+        velocity = 1
+        dt = 1e-1
+        omega_w = np.logspace(-3, 0, 10)
+
+        ss_ct = campbell(sigma_w, length_scale, velocity)
+
+        ss_dt = campbell(sigma_w, length_scale, velocity, dt=dt)
+
+        G_ct = ss_ct.freqresp(omega_w)
+        G_dt = ss_dt.freqresp(omega_w)
+
+        np.testing.assert_array_almost_equal(G_ct[0, 0, :].real, G_dt[0, 0, :].real, decimal=3)
+
+
+if __name__ == '__main__':
+    unittest.main()
+
+
diff --git a/tests/linear/horten/__init__.py b/tests/linear/horten/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/linear/horten/test_horten.py b/tests/linear/horten/test_horten.py
new file mode 100644
index 000000000..5c8eef3e5
--- /dev/null
+++ b/tests/linear/horten/test_horten.py
@@ -0,0 +1,366 @@
+import sharpy.utils.algebra as algebra
+from sharpy.cases.hangar.richards_wing import Baseline
+import sharpy.sharpy_main
+import numpy as np
+import configobj
+import unittest
+import os
+import shutil
+
+route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+
+
+def run_rom_convergence(case_name, case_route='./cases/', output_folder='./output/', **kwargs):
+    M = kwargs.get('M', 4)
+    N = kwargs.get('N', 11)
+    Msf = kwargs.get('Msf', 10)
+
+    trim = kwargs.get('trim', True)
+
+    rho_fact = 1.
+    track_body = True
+    payload = 0
+    u_inf = kwargs.get('u_inf', 30)
+
+    use_euler = True
+    if use_euler:
+        orient = 'euler'
+    else:
+        orient = 'quat'
+
+    case_name += 'M%gN%gMsf%g_u%g' % (M, N, Msf, u_inf)
+
+    # M4N11Msf5
+    alpha_deg = 3.974
+    cs_deflection = 0.3582
+    thrust = 4.8062
+
+    # ROM settings
+    rom_settings = dict()
+    rom_settings['algorithm'] = 'mimo_rational_arnoldi'
+    rom_settings['r'] = 10
+    rom_settings['frequency'] = np.array([0], dtype=float)
+    rom_settings['single_side'] = 'observability'
+
+    case_name += 'rom_%g_%s' % (rom_settings['r'], rom_settings['single_side'][:3])
+
+    ws = Baseline(M=M,
+                  N=N,
+                  Mstarfactor=Msf,
+                  u_inf=u_inf,
+                  rho=1.02,
+                  alpha_deg=alpha_deg,  # 7.7563783342984385,
+                  roll_deg=0,
+                  cs_deflection_deg=cs_deflection,  # -6.733360628875144,
+                  thrust=thrust,  # 10.140622253017584,
+                  physical_time=20,
+                  case_name=case_name,
+                  case_route=case_route,
+                  case_name_format=2)
+
+    ws.set_properties()
+    ws.initialise()
+    ws.clean_test_files()
+
+    # ws.update_mass_stiffness(sigma=1., sigma_mass=1.5)
+    ws.update_mass_stiffness(sigma=.5, sigma_mass=1.0, payload=payload)
+    ws.update_fem_prop()
+    ws.generate_fem_file()
+    ws.update_aero_properties()
+    ws.generate_aero_file()
+
+    flow = ['BeamLoader',
+            'AerogridLoader',
+            'StaticTrim',
+            'BeamPlot',
+            'AerogridPlot',
+            'AeroForcesCalculator',
+            'DynamicCoupled',
+            'Modal',
+            'LinearAssembler',
+            'AsymptoticStability',
+            'SaveParametricCase'
+            ]
+
+    if not trim:
+        flow[2] = 'StaticCoupled'
+
+    settings = dict()
+    settings['SHARPy'] = {'case': ws.case_name,
+                          'route': ws.case_route,
+                          'flow': flow,
+                          'write_screen': 'off',
+                          'write_log': 'on',
+                          'log_folder': output_folder,
+                          'log_file': ws.case_name + '.log'}
+
+    settings['BeamLoader'] = {'unsteady': 'off',
+                              'orientation': algebra.euler2quat(np.array([ws.roll,
+                                                                          ws.alpha,
+                                                                          ws.beta]))}
+
+    settings['AerogridLoader'] = {'unsteady': 'off',
+                                  'aligned_grid': 'on',
+                                  'mstar': int(ws.M * ws.Mstarfactor),
+                                  'freestream_dir': ['1', '0', '0'],
+                                  'control_surface_deflection': [''],
+                                  'wake_shape_generator': 'StraightWake',
+                                  'wake_shape_generator_input': {'u_inf': u_inf,
+                                                                 'u_inf_direction': [1., 0., 0.],
+                                                                 'dt': ws.dt},
+                                  }
+
+    if ws.horseshoe is True:
+        settings['AerogridLoader']['mstar'] = 1
+
+    settings['StaticCoupled'] = {'print_info': 'on',
+                                 'structural_solver': 'NonLinearStatic',
+                                 'structural_solver_settings': {'print_info': 'off',
+                                                                'max_iterations': 200,
+                                                                'num_load_steps': 1,
+                                                                'delta_curved': 1e-5,
+                                                                'min_delta': ws.tolerance,
+                                                                'gravity_on': 'on',
+                                                                'gravity': 9.81},
+                                 'aero_solver': 'StaticUvlm',
+                                 'aero_solver_settings': {'print_info': 'on',
+                                                          'horseshoe': ws.horseshoe,
+                                                          'num_cores': 4,
+                                                          'n_rollup': int(0),
+                                                          'rollup_dt': ws.dt,
+                                                          'rollup_aic_refresh': 1,
+                                                          'rollup_tolerance': 1e-4,
+                                                          'vortex_radius': 1e-6,
+                                                          'velocity_field_generator': 'SteadyVelocityField',
+                                                          'velocity_field_input': {'u_inf': ws.u_inf,
+                                                                                   'u_inf_direction': [1., 0, 0]},
+                                                          'rho': ws.rho},
+                                 'max_iter': 200,
+                                 'n_load_steps': 1,
+                                 'tolerance': ws.tolerance,
+                                 'relaxation_factor': 0.2}
+
+    settings['StaticTrim'] = {'solver': 'StaticCoupled',
+                              'solver_settings': settings['StaticCoupled'],
+                              'thrust_nodes': ws.thrust_nodes,
+                              'initial_alpha': ws.alpha,
+                              'initial_deflection': ws.cs_deflection,
+                              'initial_thrust': ws.thrust,
+                              'max_iter': 200,
+                              'fz_tolerance': 1e-2,
+                              'fx_tolerance': 1e-2,
+                              'm_tolerance': 1e-2,
+                              'save_info': 'on'}
+
+    settings['AerogridPlot'] = {'include_rbm': 'off',
+                                'include_applied_forces': 'on',
+                                'minus_m_star': 0,
+                                'u_inf': ws.u_inf
+                                }
+    settings['AeroForcesCalculator'] = {'write_text_file': 'off',
+                                        'text_file_name': ws.case_name + '_aeroforces.csv',
+                                        'screen_output': 'on',
+                                        'coefficients': True,
+                                        'q_ref': 0.5 * ws.rho * ws.u_inf ** 2,
+                                        'S_ref': 12.809,
+                                        }
+
+    settings['BeamPlot'] = {'include_rbm': 'on',
+                            'include_applied_forces': 'on',
+                            'include_FoR': 'on'}
+
+    struct_solver_settings = {'print_info': 'off',
+                              'initial_velocity_direction': [-1., 0., 0.],
+                              'max_iterations': 950,
+                              'delta_curved': 1e-6,
+                              'min_delta': ws.tolerance,
+                              'newmark_damp': 5e-3,
+                              'gravity_on': True,
+                              'gravity': 9.81,
+                              'num_steps': ws.n_tstep,
+                              'dt': ws.dt,
+                              'initial_velocity': ws.u_inf * 1}
+
+    step_uvlm_settings = {'print_info': 'on',
+                          'num_cores': 4,
+                          'convection_scheme': ws.wake_type,
+                          'vortex_radius': 1e-6,
+                          'velocity_field_generator': 'SteadyVelocityField',
+                          'velocity_field_input': {'u_inf': ws.u_inf * 0,
+                                                   'u_inf_direction': [1., 0., 0.]},
+                          'rho': ws.rho,
+                          'n_time_steps': ws.n_tstep,
+                          'dt': ws.dt,
+                          'gamma_dot_filtering': 3}
+
+    settings['DynamicCoupled'] = {'print_info': 'on',
+                                  'structural_solver': 'NonLinearDynamicCoupledStep',
+                                  'structural_solver_settings': struct_solver_settings,
+                                  'aero_solver': 'StepUvlm',
+                                  'aero_solver_settings': step_uvlm_settings,
+                                  'fsi_substeps': 200,
+                                  'fsi_tolerance': ws.fsi_tolerance,
+                                  'relaxation_factor': ws.relaxation_factor,
+                                  'minimum_steps': 1,
+                                  'relaxation_steps': 150,
+                                  'final_relaxation_factor': 0.5,
+                                  'n_time_steps': 1,  # ws.n_tstep,
+                                  'dt': ws.dt,
+                                  'include_unsteady_force_contribution': 'off',
+                                  'postprocessors': ['BeamLoads', 'BeamPlot', 'AerogridPlot', 'WriteVariablesTime'],
+                                  'postprocessors_settings': {'BeamLoads': {'csv_output': 'off'},
+                                                              'BeamPlot': {'include_rbm': 'on',
+                                                                           'include_applied_forces': 'on'},
+                                                              'AerogridPlot': {
+                                                                  'u_inf': ws.u_inf,
+                                                                  'include_rbm': 'on',
+                                                                  'include_applied_forces': 'on',
+                                                                  'minus_m_star': 0},
+                                                              'WriteVariablesTime': {
+                                                                  'cleanup_old_solution': 'on',
+                                                                  'delimiter': ',',
+                                                                  'FoR_variables': ['total_forces',
+                                                                                    'total_gravity_forces',
+                                                                                    'for_pos', 'quat'],
+                                                              }}}
+
+    settings['Modal'] = {'print_info': True,
+                         'use_undamped_modes': True,
+                         'NumLambda': 30,
+                         'rigid_body_modes': True,
+                         'write_modes_vtk': 'on',
+                         'print_matrices': 'on',
+                         'save_data': 'on',
+                         'continuous_eigenvalues': 'off',
+                         'dt': ws.dt,
+                         'plot_eigenvalues': False,
+                         'rigid_modes_cg': False}
+
+    settings['LinearAssembler'] = {'linear_system': 'LinearAeroelastic',
+                                   'linearisation_tstep': -1,
+                                   'linear_system_settings': {
+                                       'beam_settings': {'modal_projection': 'on',
+                                                         'inout_coords': 'modes',
+                                                         'discrete_time': True,
+                                                         'newmark_damp': 0.5e-2,
+                                                         'discr_method': 'newmark',
+                                                         'dt': ws.dt,
+                                                         'proj_modes': 'undamped',
+                                                         'use_euler': use_euler,
+                                                         'num_modes': 20,
+                                                         'print_info': 'on',
+                                                         'gravity': 'on',
+                                                         'remove_dofs': []},
+                                       'aero_settings': {'dt': ws.dt,
+                                                         'integr_order': 2,
+                                                         'density': ws.rho * rho_fact,
+                                                         'remove_predictor': False,
+                                                         'use_sparse': False,
+                                                         'vortex_radius': 1e-6,
+                                                         'remove_inputs': ['u_gust'],
+                                                         'rom_method': ['Krylov'],
+                                                         'rom_method_settings': {'Krylov': rom_settings}},
+                                       'track_body': track_body,
+                                       'use_euler': use_euler,
+                                   }}
+
+    settings['AsymptoticStability'] = {
+        'print_info': 'on',
+        'modes_to_plot': [],
+        # 'velocity_analysis': [27, 29, 3],
+        'display_root_locus': 'off',
+        'frequency_cutoff': 0,
+        'export_eigenvalues': 'on',
+        'target_system': ['aeroelastic', 'aerodynamic', 'structural'],
+        'output_file_format': 'dat',
+        'num_evals': 100}
+
+    settings['FrequencyResponse'] = {'print_info': 'on',
+                                     'frequency_bounds': [0.1, 100]}
+
+    settings['LinDynamicSim'] = {'dt': ws.dt,
+                                 'n_tsteps': ws.n_tstep,
+                                 'sys_id': 'LinearAeroelastic',
+                                 # 'reference_velocity': ws.u_inf,
+                                 'write_dat': ['x', 'y', 't', 'u'],
+                                 # 'write_dat': 'on',
+                                 'postprocessors': ['BeamPlot', 'AerogridPlot'],
+                                 'postprocessors_settings': {'AerogridPlot': {
+                                     'u_inf': ws.u_inf,
+                                     'include_rbm': 'on',
+                                     'include_applied_forces': 'on',
+                                     'minus_m_star': 0},
+                                     'BeamPlot': {'include_rbm': 'on',
+                                                  'include_applied_forces': 'on'}}}
+
+    settings['StabilityDerivatives'] = {'u_inf': ws.u_inf,
+                                        'S_ref': 12.809,
+                                        'b_ref': ws.span,
+                                        'c_ref': 0.719}
+
+    settings['SaveData'] = {'save_aero': 'off',
+                            'save_struct': 'off',
+                            'save_linear': 'on',
+                            'save_linear_uvlm': 'on'}
+
+    settings['PickleData'] = {}
+
+    settings['SaveParametricCase'] = {'parameters': {'r': rom_settings['r']}}
+
+    config = configobj.ConfigObj()
+    file_name = ws.case_route + '/' + ws.case_name + '.solver.txt'
+    config.filename = file_name
+    for k, v in settings.items():
+        config[k] = v
+    config.write()
+
+    return sharpy.sharpy_main.main(['', ws.case_route + '/' + ws.case_name + '.solver.txt'])
+
+
+class TestHortenWing(unittest.TestCase):
+
+
+    def test_horten(self):
+
+        M = 4
+        N = 11
+        Msf = 5
+
+        trim = False
+
+        case_name = 'horten_'
+
+        case_route = route_test_dir + '/cases/'
+        output_route = route_test_dir + '/output/'
+
+        data = run_rom_convergence(case_name=case_name, case_route=case_route,
+                                   output_folder=output_route,
+                                   M=M, N=N, Msf=Msf, trim=trim)
+
+        # check first 10 eigs are zero (9 integro states + yaw)
+        path_to_eigs = data.output_folder + '/stability/aeroelastic_eigenvalues.dat'
+        eigs = np.loadtxt(path_to_eigs)
+
+        # check that aerodynamic and structural eigs are also written
+        target_system = ['aerodynamic', 'structural']
+        for sys in target_system:
+            np.loadtxt(data.output_folder + f'/stability/{sys}_eigenvalues.dat')
+
+        np.testing.assert_allclose(np.abs(eigs[:10]), 0, atol=1e-8)
+
+        # check that the phugoid mode is there (more or less, very high tolerance)
+        phugoid_eigs = eigs[11]
+        wn_phugoid = np.linalg.norm(phugoid_eigs)
+        period_phugoid = 2 * np.pi / wn_phugoid
+        np.testing.assert_allclose(period_phugoid, 20.7, atol=0.1, rtol=0.1)
+
+    def tearDown(self):
+        folders = ['output/', 'cases/']
+
+        for folder in folders:
+            if os.path.isdir(route_test_dir + '/' + folder):
+                shutil.rmtree(route_test_dir + '/' + folder)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/linear/rom/test_krylov.py b/tests/linear/rom/test_krylov.py
index c43ec618e..24ce8d7d4 100644
--- a/tests/linear/rom/test_krylov.py
+++ b/tests/linear/rom/test_krylov.py
@@ -31,7 +31,7 @@ def setUp(self):
 
         A = A.todense()
 
-        self.ss = libss.ss(A, B, C, D)
+        self.ss = libss.StateSpace(A, B, C, D)
 
         self.rom = krylov.Krylov()
 
diff --git a/tests/linear/rom/test_rom_framework.py b/tests/linear/rom/test_rom_framework.py
index 9edfe75ed..7d187b54d 100644
--- a/tests/linear/rom/test_rom_framework.py
+++ b/tests/linear/rom/test_rom_framework.py
@@ -149,8 +149,7 @@ def setUp(self):
         ws.config['Modal'] = {'NumLambda': 20,
                               'rigid_body_modes': 'off',
                               'print_matrices': 'on',
-                              'keep_linear_matrices': 'on',
-                              'write_dat': 'off',
+                              'save_data': 'off',
                               'rigid_modes_cg': 'off',
                               'continuous_eigenvalues': 'off',
                               'dt': 0,
diff --git a/tests/linear/rom/test_springmasssystem.py b/tests/linear/rom/test_springmasssystem.py
index f43c06b5a..77adcd7bd 100644
--- a/tests/linear/rom/test_springmasssystem.py
+++ b/tests/linear/rom/test_springmasssystem.py
@@ -129,14 +129,14 @@ def build_system(self, system_inputs, system_time):
 
         # Build State Space
         if system_time == 'ct':
-            system = libss.ss(A, b, c, d, dt=None)
+            system = libss.StateSpace(A, b, c, d, dt=None)
 
         else:
             # Discrete time system
             dt = 1e-2
             Adt, Bdt, Cdt, Ddt = lingebm.newmark_ss(Minv, C, k, dt=dt, num_damp=0)
 
-            system = libss.ss(Adt, Bdt, Cdt, Ddt, dt=dt)
+            system = libss.StateSpace(Adt, Bdt, Cdt, Ddt, dt=dt)
 
             # SISO Gains for DT system
             if system_inputs == 'SISO':
diff --git a/tests/linear/statespace/__init__.py b/tests/linear/statespace/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/linear/statespace/test_statespace.py b/tests/linear/statespace/test_statespace.py
new file mode 100644
index 000000000..0e436b189
--- /dev/null
+++ b/tests/linear/statespace/test_statespace.py
@@ -0,0 +1,427 @@
+import copy
+import unittest
+import os
+
+import numpy as np
+
+from sharpy.linear.src import libsparse as libsp
+from sharpy.linear.src.libss import StateSpace, SSconv, compare_ss, scale_SS, Gain, random_ss, couple, join, disc2cont, series
+from sharpy.linear.utils.ss_interface import LinearVector, InputVariable, StateVariable, OutputVariable
+
+
+class Test_dlti(unittest.TestCase):
+    """ Test methods into this module for DLTI systems """
+
+    def setUp(self):
+        # allocate some state-space model (dense and sparse)
+        dt = 0.3
+        Ny, Nx, Nu = 8, 3, 5
+        A = np.random.rand(Nx, Nx)
+        B = np.random.rand(Nx, Nu)
+        C = np.random.rand(Ny, Nx)
+        D = np.random.rand(Ny, Nu)
+        self.SS = StateSpace(A, B, C, D, dt=dt)
+        self.SSsp = StateSpace(libsp.csc_matrix(A), libsp.csc_matrix(B), C, D, dt=dt)
+
+        self.SS.input_variables = LinearVector([InputVariable('input1', size=3, index=0),
+                                                InputVariable('input2', size=2, index=1)])
+        self.SS.state_variables = LinearVector([StateVariable('state1', size=3, index=0)])
+        self.SS.output_variables = LinearVector([OutputVariable('output1', size=3, index=0),
+                                                 OutputVariable('output2', size=5, index=1)])
+
+        self.SSsp.input_variables = self.SS.input_variables
+        self.SSsp.output_variables = self.SS.output_variables
+        self.SSsp.state_variables = self.SS.state_variables
+
+    def test_SSconv(self):
+
+        SS = self.SS
+        SSsp = self.SSsp
+        Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
+        A, B, C, D = SS.get_mats()
+
+        # remove predictor: try different scenario
+        B1 = np.random.rand(Nx, Nu)
+        SSpr0 = StateSpace(*SSconv(A, B, B1, C, D), dt=0.3)
+        SSpr1 = StateSpace(*SSconv(A, B, libsp.csc_matrix(B1), C, D), dt=0.3)
+        SSpr2 = StateSpace(*SSconv(
+            libsp.csc_matrix(A), B, libsp.csc_matrix(B1), C, D), dt=0.3)
+        SSpr3 = StateSpace(*SSconv(
+            libsp.csc_matrix(A), libsp.csc_matrix(B), B1, C, D), dt=0.3)
+        SSpr4 = StateSpace(*SSconv(
+            libsp.csc_matrix(A), libsp.csc_matrix(B), libsp.csc_matrix(B1), C, D), dt=0.3)
+        compare_ss(SSpr0, SSpr1)
+        compare_ss(SSpr0, SSpr2)
+        compare_ss(SSpr0, SSpr3)
+        compare_ss(SSpr0, SSpr4)
+
+    def test_scale_SS(self):
+
+        SS = self.SS
+        SSsp = self.SSsp
+        Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
+
+        # scale (hard-copy)
+        insc = np.random.rand(Nu)
+        stsc = np.random.rand(Nx)
+        outsc = np.random.rand(Ny)
+        SSadim = scale_SS(SS, insc, outsc, stsc, byref=False)
+        SSadim_sp = scale_SS(SSsp, insc, outsc, stsc, byref=False)
+        compare_ss(SSadim, SSadim_sp)
+
+        # scale (by reference)
+        SS.scale(insc, outsc, stsc)
+        SSsp.scale(insc, outsc, stsc)
+        compare_ss(SS, SSsp)
+
+    def test_addGain(self):
+
+        SS = self.SS
+        SSsp = self.SSsp
+        Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
+
+        # add gains
+        Kin = np.random.rand(Nu, 5)
+        Kout = np.random.rand(4, Ny)
+
+        gain_in = Gain(Kin)
+        gain_in.input_variables = LinearVector([InputVariable('input1', size=5, index=0)])
+        gain_in.output_variables = LinearVector([OutputVariable('output1', size=Nu, index=0)])
+
+        gain_out = Gain(Kout)
+        gain_out.input_variables = LinearVector.transform(self.SS.output_variables, InputVariable)
+        gain_out.output_variables = LinearVector([OutputVariable('final_output', size=gain_out.outputs, index=0)])
+
+        SS.addGain(gain_in, 'in')
+        SS.addGain(gain_out, 'out')
+        SSsp.addGain(gain_in, 'in')
+        SSsp.addGain(gain_out, 'out')
+        compare_ss(SS, SSsp)
+
+    def test_freqresp(self):
+        # freq response: try different scenario
+
+        SS = self.SS
+        SSsp = self.SSsp
+        Nu, Nx, Ny = SS.inputs, SS.states, SS.outputs
+
+        kv = np.linspace(0, 1, 8)
+        Y = SS.freqresp(kv)
+        Ysp = SSsp.freqresp(kv)
+        er = np.max(np.abs(Y - Ysp))
+        assert er < 1e-10, 'Test on freqresp failed'
+
+        SS.D = libsp.csc_matrix(SS.D)
+        Y1 = SS.freqresp(kv)
+        er = np.max(np.abs(Y - Y1))
+        assert er < 1e-10, 'Test on freqresp failed'
+
+    def test_couple(self):
+        dt = .2
+        Nx1, Nu1, Ny1 = 3, 4, 2
+        Nx2, Nu2, Ny2 = 4, 3, 2
+        K12 = np.random.rand(Nu1, Ny2)
+        K21 = np.random.rand(Nu2, Ny1)
+        SS1 = random_ss(Nx1, Nu1, Ny1, dt=.2)
+        SS2 = random_ss(Nx2, Nu2, Ny2, dt=.2)
+
+        SS1sp = StateSpace(libsp.csc_matrix(SS1.A),
+                           libsp.csc_matrix(SS1.B),
+                           libsp.csc_matrix(SS1.C),
+                           libsp.csc_matrix(SS1.D), dt=dt)
+        SS2sp = StateSpace(libsp.csc_matrix(SS2.A),
+                           libsp.csc_matrix(SS2.B),
+                           libsp.csc_matrix(SS2.C),
+                           libsp.csc_matrix(SS2.D), dt=dt)
+        K12sp = libsp.csc_matrix(K12)
+        K21sp = libsp.csc_matrix(K21)
+
+        # SCref=couple_full(SS1,SS2,K12,K21)
+        SC0 = couple(SS1, SS2, K12, K21)
+        # compare_ss(SCref,SC0)
+        for SSa in [SS1, SS1sp]:
+            for SSb in [SS2, SS2sp]:
+                for k12 in [K12, K12sp]:
+                    for k21 in [K21, K21sp]:
+                        SChere = couple(SSa, SSb, k12, k21)
+                        compare_ss(SC0, SChere)
+
+    def test_join(self):
+
+        Nx, Nu, Ny = 4, 3, 2
+        SS_list = [random_ss(Nx, Nu, Ny, dt=.2) for ii in range(3)]
+
+        wv = [.3, .5, .2]
+        SSjoin = join(SS_list, wv)
+
+        kv = np.array([0., 1., 3.])
+        Yjoin = SSjoin.freqresp(kv)
+
+        Yref = np.zeros_like(Yjoin)
+        for ii in range(3):
+            Yref += wv[ii] * SS_list[ii].freqresp(kv)
+
+        er = np.max(np.abs(Yjoin - Yref))
+        assert er < 1e-14, 'test_join error %.3e too large' % er
+
+    def test_disc2cont(self):
+        # not the best test given that eigenvalue comparison is not great with random systems. (error grows near
+        # nyquist frequency)
+
+        # this test is for execution purposes only.
+        sys = copy.deepcopy(self.SS)
+        self.SS.disc2cont()
+
+        ct_sys = disc2cont(sys)
+
+    def test_remove_inputs(self):
+        dt = 0.3
+        Ny, Nx, Nu = 4, 3, 10
+        A = np.random.rand(Nx, Nx)
+        B = np.random.rand(Nx, Nu)
+        C = np.random.rand(Ny, Nx)
+        D = np.random.rand(Ny, Nu)
+        self.SS = StateSpace(A, B, C, D, dt=dt)
+        self.SSsp = StateSpace(libsp.csc_matrix(A), libsp.csc_matrix(B), C, D, dt=dt)
+
+        self.SS.input_variables = LinearVector([InputVariable('input1', size=3, index=0),
+                                                InputVariable('input2', size=4, index=1),
+                                                InputVariable('input3', size=2, index=2),
+                                                InputVariable('input4', size=1, index=3)])
+        self.SSsp.input_variables = self.SS.input_variables.copy()
+
+        rows_loc = self.SS.input_variables.num_variables * [None]
+        for ith, variable in enumerate(self.SS.input_variables):
+            rows_loc[ith] = variable.rows_loc
+
+        self.SS.remove_inputs('input2', 'input4')
+
+        assert self.SS.B.shape == (Nx, self.SS.input_variables.size), 'B matrix not trimmed correctly'
+        assert self.SS.D.shape == (Ny, self.SS.input_variables.size), 'D matrix not trimmed correctly'
+
+        assert self.SS.input_variables[0].rows_loc == rows_loc[0], \
+            'Rows of input 1 not retained correctly'
+        assert self.SS.input_variables[1].rows_loc == rows_loc[2], \
+            'Rows of input 3 not retained correctly'
+
+        # sparse system
+        self.SSsp.remove_inputs('input2', 'input4')
+        assert self.SSsp.B.shape == (Nx, self.SSsp.input_variables.size), 'Bsp matrix not trimmed correctly'
+        assert self.SSsp.D.shape == (Ny, self.SSsp.input_variables.size), 'Dsp matrix not trimmed correctly'
+
+        assert self.SSsp.input_variables[0].rows_loc == rows_loc[0], \
+            'Rows of input 1 not retained correctly in sparse system'
+        assert self.SSsp.input_variables[1].rows_loc == rows_loc[2], \
+            'Rows of input 3 not retained correctly in sparse system'
+
+    def test_series(self):
+        """
+        Test Series connection
+
+        input11 -- >>> SS2 ----> input1, input2 -----> self.SS ----> y
+        """
+        Nx2, Nu2, Ny2 = 4, 3, self.SS.inputs
+        SS2 = random_ss(Nx2, Nu2, Ny2, dt=self.SS.dt)
+        SS2.input_variables = LinearVector([InputVariable('input11', size=3, index=0)])
+        SS2.state_variables = LinearVector([StateVariable('state11', size=4, index=0)])
+        SS2.output_variables = LinearVector([OutputVariable('input1', size=3, index=0),
+                                             OutputVariable('input2', size=2, index=1)])
+
+        SSnew = series(SS2, self.SS)
+        state_vars = LinearVector.merge(SS2.state_variables, self.SS.state_variables)
+
+        LinearVector.check_same_vectors(SSnew.state_variables, state_vars)
+
+        assert SSnew.outputs == self.SS.outputs, 'Number of outputs not the same as self.SS'
+        assert SSnew.inputs == SS2.inputs, 'Number of inputs not the same as SS2'
+
+
+class TestStateSpaceManipulation(unittest.TestCase):
+
+    route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+    files_created_by_test = []
+
+    def setUp(self):
+        Ny, Nx, Nu = 6, 3, 10
+        A = np.random.rand(Nx, Nx)
+        B = np.random.rand(Nx, Nu)
+        C = np.random.rand(Ny, Nx)
+        D = np.random.rand(Ny, Nu)
+        self.ss = StateSpace(A, B, C, D)
+        self.SSsp = StateSpace(libsp.csc_matrix(A), libsp.csc_matrix(B), C, D)
+
+        self.ss.input_variables = LinearVector([InputVariable('input1', size=3, index=0),
+                                                InputVariable('input2', size=4, index=1),
+                                                InputVariable('input3', size=2, index=2),
+                                                InputVariable('input4', size=1, index=3)])
+
+        self.ss.state_variables = LinearVector([StateVariable('state1', size=Nx, index=0)])
+        self.ss.output_variables = LinearVector([OutputVariable('output1', size=1, index=0),
+                                                 OutputVariable('output2', size=3, index=1),
+                                                 OutputVariable('output3', size=2, index=2)])
+
+    def test_remove_outputs(self):
+        self.ss.remove_outputs('output1')  # size 1
+        self.ss.remove_outputs('output2')  # size 3
+
+        assert self.ss.outputs == 2, 'Number of outputs not correct'
+
+    def test_retain_channels(self):
+        """
+        Retain certain input and output channels only
+        """
+        retain_input_channels = [0, 7]
+        self.ss.retain_inout_channels(retain_input_channels, where='in')
+
+        assert self.ss.inputs == len(retain_input_channels), 'Number of inputs not correct'
+        assert self.ss.input_variables[0].size == 1, 'input1 not correct size'
+        assert self.ss.input_variables[1].name == 'input3', 'input2 not properly removed'
+
+        retain_output_channels = [0, 5]
+        self.ss.retain_inout_channels(retain_output_channels, where='out')
+        assert self.ss.outputs == len(retain_output_channels), 'Number of outputs not correct'
+        assert self.ss.output_variables[0].size == 1, 'output1 not correct size'
+        assert self.ss.output_variables[1].name == 'output3', 'output2 not properly removed'
+        assert self.ss.output_variables[1].size == 1, 'output3 not properly trimmed'
+        assert self.ss.output_variables[1].index == 1, 'output3 not properly indexed'
+
+    def test_input_output(self):
+
+        h5_filename = self.route_test_dir + '/file_test_statespace_inout.h5'
+        self.files_created_by_test.append(h5_filename)
+
+        self.ss.save(h5_filename)
+
+        loaded_ss = StateSpace.load_from_h5(h5_filename)
+
+        LinearVector.check_same_vectors(self.ss.input_variables,
+                                        loaded_ss.input_variables)
+
+        LinearVector.check_same_vectors(self.ss.output_variables,
+                                        loaded_ss.output_variables)
+
+        LinearVector.check_same_vectors(self.ss.state_variables,
+                                        loaded_ss.state_variables)
+
+        np.testing.assert_array_almost_equal(self.ss.A, loaded_ss.A)
+        np.testing.assert_array_almost_equal(self.ss.B, loaded_ss.B)
+        np.testing.assert_array_almost_equal(self.ss.C, loaded_ss.C)
+        np.testing.assert_array_almost_equal(self.ss.D, loaded_ss.D)
+
+    @classmethod
+    def tearDownClass(cls):
+        for file in cls.files_created_by_test:
+            if os.path.exists(file):
+                os.remove(file)
+
+
+class TestGains(unittest.TestCase):
+
+    route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+    files_created_by_test = []
+
+    def test_dot(self):
+        """
+        u ---> K1 ----> K2 ---->y
+        Returns:
+
+        """
+        m1 = 4
+        p1 = 3
+        k1 = np.random.rand(p1, m1)
+        gain1 = Gain(k1,
+                     input_vars=LinearVector([InputVariable('input', size=m1, index=0)]),
+                     output_vars=LinearVector([OutputVariable('output1', size=p1, index=0)]))
+
+        m2 = p1
+        p2 = 5
+        k2 = np.random.rand(p2, m2)
+        gain2 = Gain(k2,
+                     input_vars=LinearVector([InputVariable('output1', size=m2, index=0)]),
+                     output_vars=LinearVector([OutputVariable('output', size=p2, index=0)]))
+
+        gain = gain2.dot(gain1)
+        np.testing.assert_array_almost_equal(gain.value, k2.dot(k1))
+
+    @unittest.expectedFailure
+    def test_fail_connection(self):
+        """
+        This one should fail because of dimension mismatch
+
+        u ---> K1 ----> K2 ---->y
+
+        """
+        m1 = 4
+        p1 = 3
+        k1 = np.random.rand(p1, m1)
+        gain1 = Gain(k1,
+                     input_vars=LinearVector([InputVariable('input', size=m1, index=0)]),
+                     output_vars=LinearVector([OutputVariable('output1', size=p1, index=0)]))
+
+        m2 = 2
+        p2 = 5
+        k2 = np.random.rand(p2, m2)
+        gain2 = Gain(k2,
+                     input_vars=LinearVector([InputVariable('output1', size=m2, index=0)]),
+                     output_vars=LinearVector([OutputVariable('output', size=p2, index=0)]))
+
+        # check the check fails :)
+        with self.subTest('ss_interface_check'):
+            LinearVector.check_connection(gain1.output_variables, gain2.input_variables)
+
+        with self.subTest('connection_check'):
+            gain = gain2.dot(gain1)
+            np.testing.assert_array_almost_equal(gain.value, k2.dot(k1))
+
+    def test_input_output_gains(self):
+        m1 = 4
+        p1 = 3
+        k1 = np.random.rand(p1, m1)
+        gain1 = Gain(k1,
+                     input_vars=LinearVector([InputVariable('input', size=m1, index=0)]),
+                     output_vars=LinearVector([OutputVariable('output1', size=p1, index=0)]))
+
+        m2 = 2
+        p2 = 5
+        k2 = np.random.rand(p2, m2)
+        gain2 = Gain(k2,
+                     input_vars=LinearVector([InputVariable('output1', size=m2, index=0)]),
+                     output_vars=LinearVector([OutputVariable('output', size=p2, index=0)]))
+
+        with self.subTest('Save to single h5 file'):
+            h5_file_name = self.route_test_dir + '/gain1.h5'
+            self.files_created_by_test.append(h5_file_name)
+            gain1.save(h5_file_name)
+
+        with self.subTest('Load from single h5 file'):
+            loaded_gain = Gain.load_from_h5(h5_file_name)
+            self.check_gains_equal(gain1, loaded_gain)
+
+        with self.subTest('Save both gains to single h5'):
+            h5_file_name = self.route_test_dir + '/multi_gain.h5'
+            self.files_created_by_test.append(h5_file_name)
+
+            Gain.save_multiple_gains(h5_file_name, ('gain1', gain1), ('gain2', gain2))
+
+        with self.subTest('Load multiple gains from a single h5'):
+            out_gains = Gain.load_multiple_gains(h5_file_name)
+            self.check_gains_equal(gain1, out_gains['gain1'])
+            self.check_gains_equal(gain2, out_gains['gain2'])
+
+    @staticmethod
+    def check_gains_equal(gain1, gain2):
+        np.testing.assert_array_almost_equal(gain1.value, gain2.value)
+        LinearVector.check_same_vectors(gain1.input_variables,
+                                        gain2.input_variables)
+        LinearVector.check_same_vectors(gain1.output_variables,
+                                        gain2.output_variables)
+
+    @classmethod
+    def tearDownClass(cls):
+        for file in cls.files_created_by_test:
+            if os.path.exists(file):
+                os.remove(file)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/linear/statespace/test_variable_tracker.py b/tests/linear/statespace/test_variable_tracker.py
new file mode 100644
index 000000000..35d66cfe2
--- /dev/null
+++ b/tests/linear/statespace/test_variable_tracker.py
@@ -0,0 +1,168 @@
+import unittest
+import os
+import h5py
+import sharpy.utils.h5utils as h5utils
+
+from sharpy.linear.utils.ss_interface import InputVariable, LinearVector, OutputVariable
+
+
+class TestVariables(unittest.TestCase):
+
+    route_test_dir = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))
+    files_created_by_test = []
+
+    def setUp(self):
+        # initialising with index out of order
+        Kzeta = 4
+        input_variables_list = [InputVariable('zeta', size=3 * Kzeta, index=0),
+                                InputVariable('zeta_dot', size=3 * Kzeta, index=1),  # this should be one
+                                InputVariable('u_gust', size=3 * Kzeta, index=2)]
+
+        self.input_variables = LinearVector(input_variables_list)
+
+    def test_initialisation(self):
+        Kzeta = 4
+        input_variables_list = [InputVariable('zeta', size=3 * Kzeta, index=0),
+                                InputVariable('u_gust', size=3 * Kzeta, index=2),
+                                InputVariable('zeta_dot', size=3 * Kzeta, index=1)]
+
+        input_variables = LinearVector(input_variables_list)
+
+        sorted_indices = [variable.index for variable in input_variables]
+        variable_names = [variable.name for variable in input_variables]
+        assert sorted_indices == [0, 1, 2], 'Error sorting indices in initialisation'
+        assert variable_names == ['zeta', 'zeta_dot', 'u_gust'], 'Error sorting indices in initialisation'
+
+    def test_remove(self):
+
+        self.input_variables.remove('zeta_dot')
+
+        sorted_indices = [variable.index for variable in self.input_variables]
+        assert sorted_indices == [0, 1], 'Error removing variable'
+
+    def test_modify(self):
+
+        new_values = {'name': 'new_u_gust',
+                      'size': 5,
+                      }
+
+        self.input_variables.modify('u_gust', **new_values)
+
+        assert self.input_variables.get_variable_from_name('new_u_gust').size == 5, 'Variable not modified correctly'
+
+    def test_add(self):
+
+        new_variable = InputVariable('control_surface', size=2, index=3)
+
+        self.input_variables.add(new_variable)
+        self.input_variables.update_locations()
+
+        # test it's properly included - it will raise an error internally if not
+        included_var = self.input_variables.get_variable_from_name('control_surface')
+
+        new_variable_repeated_index = InputVariable('control_surface_2', size=2, index=3)
+        try:
+            self.input_variables.add(new_variable_repeated_index)
+        except IndexError:
+            # correct behaviour
+            pass
+        else:
+            raise IndexError('Variable was added when it should have been rejected because of repeated index')
+
+        new_variable_inbetween = InputVariable('first_variable', size=2, index=-1)
+        self.input_variables.add(new_variable_inbetween)
+        self.input_variables.update_locations()
+
+        assert self.input_variables[0].name == new_variable_inbetween.name, 'New variable not added at the start of' \
+                                                                            ' the list'
+
+        self.input_variables.add('var_from_string', size=3, index=10)
+        assert self.input_variables[-1].name == 'var_from_string', 'Variable from string not added correctly'
+
+        self.input_variables.update_indices()
+
+    def test_append(self):
+        self.input_variables.append('last_var', size=3)
+        self.input_variables.update_indices()
+
+        assert self.input_variables[-1].name == 'last_var', 'Variable not properly appended'
+
+        # test adding an incorrect input
+        out_var = OutputVariable('out_var', size=1, index=4)
+
+        try:
+            self.input_variables.append(out_var)
+        except TypeError:
+            pass  # this is what should happen
+        else:
+            raise TypeError('Error. Able to append an output variable to an input variable')
+
+        self.input_variables.append(InputVariable(name='last_last_var', size=2, index=0))
+
+        assert self.input_variables[-1].name == 'last_last_var', 'Variable not properly appended'
+
+    def test_merge(self):
+        second_variables_list = [InputVariable('eta', size=3, index=0),
+                                 InputVariable('eta_dot', size=3, index=1)]
+
+        second_vector = LinearVector(second_variables_list)
+
+        merged_vector = LinearVector.merge(self.input_variables, second_vector)
+
+        assert merged_vector[-1].name == 'eta_dot', 'Vectors not coupled properly'
+        assert merged_vector[2].name == 'u_gust', 'Vectors not coupled properly'
+
+    def test_copy(self):
+        vec1 = self.input_variables
+        vec2 = vec1.copy()
+
+        assert vec1 is not vec2, 'Object not deep copied'
+
+    def test_transform(self):
+
+        new_vector = LinearVector.transform(self.input_variables, to_type=OutputVariable)
+
+        LinearVector.check_same_vectors(new_vector, self.input_variables)
+        assert new_vector.num_variables == self.input_variables.num_variables, 'Number of variables not equal'
+        assert new_vector.size == self.input_variables.size, 'Size not equal'
+
+        for i_var in range(new_vector.num_variables):
+            assert new_vector[i_var].name == self.input_variables[i_var].name, 'Name does not match'
+            assert new_vector[i_var].index == self.input_variables[i_var].index, 'Index does not match'
+            assert new_vector[i_var].size == self.input_variables[i_var].size, 'Size does not match'
+
+        new_vector.append(OutputVariable('last_var', size=2, index=0))
+
+        assert new_vector.num_variables == self.input_variables.num_variables + 1, 'Number of variables got ' \
+                                                                                   'updated in the original object'
+        assert new_vector[-1].name == 'last_var', 'Variable not correctly appended'
+        assert new_vector[-1].index == new_vector.num_variables - 1, 'Variable not correctly appended'
+
+    def test_save_to_h5(self):
+        Kzeta = 4
+        input_variables_list = [InputVariable('zeta', size=3 * Kzeta, index=0),
+                                InputVariable('zeta_dot', size=3 * Kzeta, index=1),  # this should be one
+                                InputVariable('u_gust', size=3 * Kzeta, index=2)]
+
+        input_variables = LinearVector(input_variables_list)
+
+        h5_file_name = self.route_test_dir + '/test_save_variables.h5'
+        self.files_created_by_test.append(h5_file_name)
+        with h5py.File(h5_file_name, 'w') as fid:
+            input_variables.add_to_h5_file(fid)
+
+        with h5py.File(h5_file_name, 'r') as fid:
+            data_dict = h5utils.load_h5_in_dict(fid)
+
+        loaded_variable = LinearVector.load_from_h5_file('InputVariable', data_dict['InputVariable'])
+
+        LinearVector.check_same_vectors(input_variables, loaded_variable)
+
+    def tearDown(self):
+        for file in self.files_created_by_test:
+            if os.path.exists(file):
+                os.remove(file)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/tests/uvlm/__init__.py b/tests/uvlm/__init__.py
index 0049f82cb..e69de29bb 100644
--- a/tests/uvlm/__init__.py
+++ b/tests/uvlm/__init__.py
@@ -1,2 +0,0 @@
-# from tests.uvlm.static import *
-# from tests.uvlm.dynamic import *
diff --git a/tests/uvlm/dynamic/test_wake_cfl_n1.py b/tests/uvlm/dynamic/test_wake_cfl_n1.py
index 52fa6040b..06fff066a 100755
--- a/tests/uvlm/dynamic/test_wake_cfl_n1.py
+++ b/tests/uvlm/dynamic/test_wake_cfl_n1.py
@@ -109,7 +109,7 @@ def generate_geometry(self):
         nodes_y[:num_node_semispan] = -1*nodes_y_semispan[::-1]
         nodes_y[num_node_semispan - 1:] = nodes_y_semispan
         
-        assert not (np.diff(nodes_y) == 0).any(), "Repited nodes"
+        assert not (np.diff(nodes_y) == 0).any(), "Repeated nodes"
         
         nodes = np.zeros((len(nodes_y), 3))
         nodes[:, 1] = nodes_y
@@ -187,7 +187,7 @@ def generate_files(self, case_header, airfoil):
         SimInfo.solvers['BeamLoader']['unsteady'] = 'on'
 
         import sharpy.utils.generator_interface as gi
-        if case_header == 'traditional':        
+        if case_header == 'traditional' or case_header == 'statespace_cfl1':
 
             SimInfo.solvers['AerogridLoader']['wake_shape_generator'] = 'StraightWake'
             SimInfo.solvers['AerogridLoader']['wake_shape_generator_input'] = {'u_inf': self.uinf,
@@ -214,7 +214,7 @@ def generate_files(self, case_header, airfoil):
             SimInfo.solvers['AerogridLoader']['mstar'] = mstar
             SimInfo.solvers['AerogridLoader']['freestream_dir'] = np.array([0.,0.,0.])
         
-        elif case_header in ['new_nonlinear', 'new_linear']:
+        elif case_header in ['new_nonlinear', 'new_linear', 'statespace']:
 
             SimInfo.solvers['AerogridLoader']['wake_shape_generator_input'] = {'u_inf': self.uinf,
                                                 'u_inf_direction': self.uinf_dir,
@@ -273,7 +273,7 @@ def generate_files(self, case_header, airfoil):
 
             SimInfo.solvers['StepLinearUVLM']['dt'] = self.dt
             SimInfo.solvers['StepLinearUVLM']['integr_order'] = 2
-            SimInfo.solvers['StepLinearUVLM']['remove_predictor'] = True
+            SimInfo.solvers['StepLinearUVLM']['remove_predictor'] = 'off'
             SimInfo.solvers['StepLinearUVLM']['use_sparse'] = True
             SimInfo.solvers['StepLinearUVLM']['density'] = self.air_density
             SimInfo.solvers['StepLinearUVLM']['vortex_radius'] = 1e-6
@@ -314,17 +314,67 @@ def generate_files(self, case_header, airfoil):
         SimInfo.solvers['DynamicCoupled']['dynamic_relaxation'] = False
         SimInfo.solvers['DynamicCoupled']['relaxation_steps'] = 0
         SimInfo.solvers['DynamicCoupled']['fsi_tolerance'] = 1e-6
-        
+
         # Time discretization
         time_steps = int(self.final_ndtime / self.nd_dt - 1)
         SimInfo.define_num_steps(time_steps)
-        
+
         SimInfo.with_forced_vel = True
         SimInfo.for_vel = np.zeros((time_steps,6), dtype=float)
         SimInfo.for_acc = np.zeros((time_steps,6), dtype=float)
         SimInfo.with_dynamic_forces = True
         SimInfo.dynamic_forces = np.zeros((time_steps,airfoil.StructuralInformation.num_node,6), dtype=float)
-        
+
+        SimInfo.solvers['PickleData'] = {'folder': route + '/output/'}
+        if 'statespace' in case_header:
+            SimInfo.solvers['SHARPy']['flow'] = ['BeamLoader',
+                                                 'AerogridLoader',
+                                                 'StaticCoupled',
+                                                 'Modal',
+                                                 'LinearAssembler',
+                                                 'AsymptoticStability',
+                                                 ]
+
+            SimInfo.solvers['SHARPy']['write_screen'] = 'on'
+            SimInfo.solvers['Modal'] = {'save_data': 'off',
+                                        'NumLambda': 50,
+                                        'rigid_body_modes': 'off'}
+
+            SimInfo.solvers['LinearAssembler'] = {'linear_system': 'LinearAeroelastic',
+                                   'linear_system_settings': {
+                                       'beam_settings': {'modal_projection': 'off',
+                                                         'inout_coords': 'nodes',
+                                                         'discrete_time': 'on',
+                                                         'newmark_damp': 0.5e-4,
+                                                         'discr_method': 'newmark',
+                                                         'dt': self.dt,
+                                                         'proj_modes': 'undamped',
+                                                         'use_euler': 'on',
+                                                         'num_modes': 2,
+                                                         'print_info': 'on',
+                                                         'gravity': 'on',
+                                                         'remove_dofs': [],
+                                                         'remove_rigid_states': 'off'},
+                                       'aero_settings': {'dt': self.dt,
+                                                         'integr_order': 2,
+                                                         'density': self.air_density,
+                                                         'remove_predictor': 'off',
+                                                         'use_sparse': 'off',
+                                                         'rigid_body_motion': 'off',
+                                                         'use_euler': 'on',
+                                                         'vortex_radius': 1e-6,
+                                                         'convert_to_ct': 'off',
+                                                         'gust_assembler': 'LeadingEdge',
+                                                         'gust_assembler_inputs': {},
+                                                         },
+                                       'rigid_body_motion': 'off',
+                                       'track_body': 'on',
+                                       'use_euler': 'on',
+                                       'linearisation_tstep': -1
+                                   },
+                                                  }
+            SimInfo.solvers['AsymptoticStability'] = {'num_evals': 100}
+
         gc.clean_test_files(SimInfo.solvers['SHARPy']['route'], SimInfo.solvers['SHARPy']['case'])
         airfoil.generate_h5_files(SimInfo.solvers['SHARPy']['route'], SimInfo.solvers['SHARPy']['case'])
         SimInfo.generate_solver_file()
@@ -332,7 +382,6 @@ def generate_files(self, case_header, airfoil):
         
         return case, route
 
-
     def run_test(self, case_header):
 
         airfoil = self.generate_geometry()
@@ -351,25 +400,37 @@ def run_test(self, case_header):
         factor = 0.5*self.air_density*self.uinf**2*self.chord*2.5e6
         sharpy_cl_clss = -lift/factor/clsteady
         kussner_cl_clss = 2*np.pi*self.aoa_ini_deg*deg2rad
-        kussner_cl_clss += 2*np.pi*daoa*deg2rad*np.interp(nd_time,
-                                                        kussner_function[:, 0],
-                                                        kussner_function[:, 1])
+        kussner_cl_clss += 2 * np.pi * daoa * deg2rad * np.interp(nd_time,
+                                                                  kussner_function[:, 0],
+                                                                  kussner_function[:, 1])
         kussner_cl_clss /= clsteady
 
         self.assertAlmostEqual(sharpy_cl_clss, kussner_cl_clss, 1)
 
-    
     def test_traditional(self):
         self.run_test('traditional')
-       
- 
+
+
     def test_new_nonlinear(self):
         self.run_test('new_nonlinear')
 
-        
+
     def test_new_linear(self):
         self.run_test('new_linear')
-        
+
+    def test_statespace(self):
+        results = {}
+        for case in ['statespace', 'statespace_cfl1']:
+            with self.subTest(case):
+                results[case] = self.run_statespace(case)
+
+    def run_statespace(self, case_header):
+        airfoil = self.generate_geometry()
+
+        case, route = self.generate_files(case_header, airfoil)
+        sharpy_file = route + case + '.sharpy'
+        return sharpy.sharpy_main.main(['', sharpy_file])
+
     @classmethod
     def tearDownClass(cls):
         solver_path = os.path.abspath(os.path.dirname(os.path.realpath(__file__)))