diff --git a/README.md b/README.md
index fcca63f3dfcd228ea3479e5f4cb197eb45721d95..a5a8976e196f8525d9954547ac1456962afa6c22 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@ DART is currently capable of rapidly solving steady transonic flows on arbitrary
 * Physical model
   - unstructured meshes for 2D and 3D geometries using [gmsh](https://gmsh.info/)
   - steady transonic flows
-  - viscous-inviscid interation (2D only)
+  - viscous-inviscid interaction (2D only)
 * Numerical methods
   - linear algrebra using [Eigen](http://eigen.tuxfamily.org/) and [Intel MKL](https://software.intel.com/content/www/us/en/develop/tools/oneapi/components/onemkl.html#gs.a3o4w5) or [openBLAS](https://www.openblas.net/)
   - direct (forward) and adjoint (backward) modes
@@ -20,12 +20,12 @@ DART is currently capable of rapidly solving steady transonic flows on arbitrary
   - [CUPyDO](https://github.com/ulgltas/CUPyDO)
   - [openMDAO](https://openmdao.org/)
 
-## Build
-Detailed build instructions can be found in the [wiki](https://gitlab.uliege.be/am-dept/dartflo/wikis/home).
+## Documentation
+Detailed build and use instructions can be found in the [wiki](https://gitlab.uliege.be/am-dept/dartflo/wikis/home).
 
 ## References
 Crovato Adrien, [Steady Transonic Aerodynamic and Aeroelastic Modeling for Preliminary Aircraft Design](http://hdl.handle.net/2268/251906), PhD thesis, University of Liège, 2020.
 
-Crovato A., Boman R., Guner H., Terrapon V., Dimitriadis G., Almeida H., Prado A., Breviglieri C., Cabral P., and Silva, G., [A Full Potential Static Aeroelastic Solver for Preliminary Aircraft Design](http://hdl.handle.net/2268/237955), 18th International Forum on Aeroelasticity and Structural Dynamics, IFASD 2019.
+Crovato A., Boman R., et al., [A Full Potential Static Aeroelastic Solver for Preliminary Aircraft Design](http://hdl.handle.net/2268/237955), 18th International Forum on Aeroelasticity and Structural Dynamics, IFASD 2019.
 
 Bilocq Amaury, [Implementation of a viscous-inviscid interaction scheme in a finite element full potential solver](http://hdl.handle.net/2268/252195), Master thesis, University of Liège, 2020.
diff --git a/dart/CMakeLists.txt b/dart/CMakeLists.txt
index 0ac4587bff42c5e93fce7ce74c10c5ead64ffe07..2d2803f7ecc66b2a8e3cf5a3ea602f8a7f61b5d6 100644
--- a/dart/CMakeLists.txt
+++ b/dart/CMakeLists.txt
@@ -23,7 +23,6 @@ MACRO_AddTest(${CMAKE_CURRENT_SOURCE_DIR}/tests)
 INSTALL(FILES ${CMAKE_CURRENT_LIST_DIR}/__init__.py
               ${CMAKE_CURRENT_LIST_DIR}/utils.py
         DESTINATION dart)
-INSTALL(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/interfaces
-                  ${CMAKE_CURRENT_LIST_DIR}/scripts
+INSTALL(DIRECTORY ${CMAKE_CURRENT_LIST_DIR}/api
                   ${CMAKE_CURRENT_LIST_DIR}/viscous
         DESTINATION dart)
diff --git a/dart/README.md b/dart/README.md
index 62c0342d592267c3e39c94ee677f75c20b43196d..71a9c79ff3dab1a952da7c1cc5059a044104fce1 100644
--- a/dart/README.md
+++ b/dart/README.md
@@ -2,5 +2,6 @@
 * [tests](dart/tests): simple tests to be run in battery checking the basic functionnalities of the solver
 * [validation](dart/validation): large-scale tests used to validate the solver results
 * [benchmark](dart/benchmark): large-scale tests used to monitor the performance
-* [cases](dart/cases) : sample configuration files meant to be run with scripts, user-defined case should be derived from these
-* [scripts](dart/scripts): various scripts for running the solver automatically
+* [viscous](dart/viscous): viscous-inviscid interaction module
+* [api](dart/api): APIs for DART
+* [cases](dart/cases): sample cases meant to be run using the APIs
diff --git a/dart/interfaces/__init__.py b/dart/api/__init__.py
similarity index 100%
rename from dart/interfaces/__init__.py
rename to dart/api/__init__.py
diff --git a/dart/scripts/__init__.py b/dart/api/internal/__init__.py
similarity index 100%
rename from dart/scripts/__init__.py
rename to dart/api/internal/__init__.py
diff --git a/dart/scripts/config.py b/dart/api/internal/config.py
similarity index 98%
rename from dart/scripts/config.py
rename to dart/api/internal/config.py
index 64ba2ab4299e56cc954b2159a1ee1790fc3588c8..7c40318b66d7c2b55419632469bb8f8edf8d713b 100644
--- a/dart/scripts/config.py
+++ b/dart/api/internal/config.py
@@ -90,7 +90,7 @@ class Config:
 
         # mesh the airfoil
         print(ccolors.ANSI_BLUE + 'Meshing the airfoil...' + ccolors.ANSI_RESET)
-        self.msh = gmsh.MeshLoader(p['File'],__file__).execute(**p['Pars'])
+        self.msh = gmsh.MeshLoader(p['File']).execute(**p['Pars'])
         if p['Dim'] == 2:
             mshCrck = tbox.MshCrack(self.msh, p['Dim'])
             mshCrck.setCrack(p['Wake'])
diff --git a/dart/scripts/polar.py b/dart/api/internal/polar.py
similarity index 99%
rename from dart/scripts/polar.py
rename to dart/api/internal/polar.py
index f0f0cf391f7500d9c646a61cd17d9a9597ce47d1..75644e5cde0b313276aecb63c148544b7fc0da47 100644
--- a/dart/scripts/polar.py
+++ b/dart/api/internal/polar.py
@@ -25,7 +25,7 @@ import math
 import dart.utils as dU
 import tbox.utils as tU
 from fwk.coloring import ccolors
-from dart.scripts.config import Config
+from .config import Config
 
 class Polar(Config):
     """Utility to compute the polar of a lifting surface
diff --git a/dart/scripts/trim.py b/dart/api/internal/trim.py
similarity index 99%
rename from dart/scripts/trim.py
rename to dart/api/internal/trim.py
index fc2a15a713e5eb8be8973810304341bfe875b9a3..34b3d0b6e6e6c80f9a510d0fd3e55fdb6c4bc482 100644
--- a/dart/scripts/trim.py
+++ b/dart/api/internal/trim.py
@@ -25,7 +25,7 @@ import math
 import dart.utils as dU
 import tbox.utils as tU
 from fwk.coloring import ccolors
-from dart.scripts.config import Config
+from .config import Config
 
 class Trim(Config):
     """Utility to perform the trim analysis of a given lifting configuration
diff --git a/dart/interfaces/mphys.py b/dart/api/mphys.py
similarity index 97%
rename from dart/interfaces/mphys.py
rename to dart/api/mphys.py
index 8de8b09b753ff25fc10473a4553659d70f4eb121..06f0ca06d1b5598cdd47d6b1ec57f7f0197cc40f 100644
--- a/dart/interfaces/mphys.py
+++ b/dart/api/mphys.py
@@ -1,662 +1,662 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2021 University of Liège
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-## MPHYS interface
-# Adrien Crovato
-#
-# Structure:
-#   - AeroBuilder
-#       - AeroSurfaceMesh
-#       - AeroGroup
-#           - Morpher
-#           - Solver
-#           - Loads
-#       - AeroCoefficients
-#
-# Remarks:
-# - for each component that uses the openMDAO matrix-free API, the partial
-#   gradients are first computed internally in dart::Adjoint, through
-#   om.ImplicitComponent.linearize() or om.ExplicitComponent.compute_partials(),
-#   and the matrix-vector product is then computed in dart:: Adjoint through
-#   om.ImplicitComponent.apply_linear() or om.ExplicitComponent.compute_jacvec_product().
-#   This allows faster gradient evaluations, at the cost of a reasonable
-#   increase in memory storage, without interfacing any sparse matrices through
-#   python.
-
-import numpy as np
-import openmdao.api as om
-from mphys.builder import Builder
-
-# Surface mesh
-class DartMesh(om.IndepVarComp):
-    """Initial surface mesh of moving body
-    For compatibility, the node coordinates of connected surfaces are always 3D
-
-    Attributes
-    ----------
-    dim : int
-        Problem dimension
-    bnd : dart.Body object
-        Moving body
-    """
-    def initialize(self):
-        self.options.declare('dim', desc='problem dimension')
-        self.options.declare('bnd', desc='moving body', recordable=False)
-
-    def setup(self):
-        self.bnd = self.options['bnd']
-        # Store all the node coordinates and add as output
-        x_aero0 = np.zeros(3 * len(self.bnd.nodes)) # surface node coordinates are 3D
-        for i in range(len(self.bnd.nodes)):
-            for j in range(3):
-                x_aero0[3 * i + j] = self.bnd.nodes[i].pos[j]
-        self.add_output('x_aero0', val=x_aero0, desc='initial aerodynamic surface node coordinates', tags=['mphys_coordinates'])
-
-    def mphys_get_triangulated_surface(self):
-        """Triangulate the surface
-        Only for 2D surfaces in 3D problems
-        Return a list containing a point and two vectors for each surface element
-             o p0
-            / \
-        v0 /___\ v1
-        """
-        if self.options['dim'] == 2:
-            raise RuntimeError('DartMesh - cannot triangulate 1D surface!\n')
-        # Create list of point coordinates and vector components
-        p0 = [[0.]*3 for _ in range(len(self.bnd.groups[0].tag.elems))]
-        v0 = [[0.]*3 for _ in range(len(self.bnd.groups[0].tag.elems))]
-        v1 = [[0.]*3 for _ in range(len(self.bnd.groups[0].tag.elems))]
-        for i in range(len(self.bnd.groups[0].tag.elems)):
-            e = self.bnd.groups[0].tag.elems[i]
-            p0i = e.nodes[0].pos
-            v0i = e.nodes[1].pos - e.nodes[0].pos
-            v1i = e.nodes[2].pos - e.nodes[0].pos
-            for j in range(3):
-                p0[i][j] = p0i[j]
-                v0[i][j] = v0i[j]
-                v1[i][j] = v1i[j]
-        return [p0, v0, v1]
-
-# Mesh morphers
-class DartMorpher(om.ImplicitComponent):
-    """Volume mesh morpher
-    For compatibility, the node coordinates of connected surfaces are always 3D, while the volume mesh coordinates (only used internaly) can be 2D or 3D
-
-    Attributes
-    ----------
-    dim : int
-        Problem dimension, used to set the length of the volume node coordinates vector
-    bnd : dart.Body object
-        Moving body
-    mrf : tbox.MshDeform object
-        Mesh morpher
-    adj : dart.Adjoint object
-        Adjoint solver
-    """
-    def initialize(self):
-        self.options.declare('dim', desc='problem dimension')
-        self.options.declare('bnd', desc='moving body', recordable=False)
-        self.options.declare('mrf', desc='mesh morpher', recordable=False)
-        self.options.declare('adj', desc='adjoint solver', recordable=False)
-
-    def setup(self):
-        self.dim = self.options['dim']
-        self.bnd = self.options['bnd']
-        self.mrf = self.options['mrf']
-        self.adj = self.options['adj']
-        # I/O
-        self.add_input('x_aero', shape_by_conn=True, desc='aerodynamic surface node coordinates', tags=['mphys_coupling'])
-        self.add_output('xv', val=np.zeros(self.dim * len(self.mrf.msh.nodes)), desc='aerodynamic volume node coordinates', tags=['mphys_coupling']) # volume node coordinates can be 2D or 3D
-        # Partials
-        # self.declare_partials(of=['xv'], wrt=['x_aero'])
-
-    def solve_nonlinear(self, inputs, outputs):
-        """Deform the volume mesh
-        """
-        # Update inputs
-        self.mrf.savePos()
-        for i in range(len(self.bnd.nodes)):
-            for j in range(3):
-                self.bnd.nodes[i].pos[j] = inputs['x_aero'][3 * i + j]
-        # Compute
-        self.mrf.deform()
-        # Update outputs
-        for i in range(len(self.mrf.msh.nodes)):
-            for j in range(self.dim):
-                outputs['xv'][self.dim * i + j] = self.mrf.msh.nodes[i].pos[j]
-
-    def apply_nonlinear(self, inputs, outputs, residuals):
-        """Compute the residuals of the mesh coordinates
-        """
-        raise NotImplementedError('DartMorpher - cannot compute the residuals!\n')
-
-    def linearize(self, inputs, outputs, partials):
-        """Compute the mesh jacobian matrix
-        """
-        self.adj.linearizeMesh()
-
-    def apply_linear(self, inputs, outputs, d_inputs, d_outputs, d_residuals, mode):
-        """Perform the matrix-vector product using the mesh Jacobian
-        """
-        if mode == 'rev':
-            if 'xv' in d_residuals:
-                if 'xv' in d_outputs:
-                    d_outputs['xv'] += self.adj.computeJacVecMesh(d_residuals['xv']) # dX = dRx_dX^T * dRx
-                if 'x_aero' in d_inputs:
-                    for i in range(len(self.bnd.nodes)):
-                        for j in range(self.dim):
-                            d_inputs['x_aero'][3 * i + j] -= d_residuals['xv'][self.dim * self.bnd.nodes[i].row + j] # dXs = -dX
-        elif mode == 'fwd':
-            raise NotImplementedError('DartMorpher - forward mode not implemented!\n')
-
-    def solve_linear(self, d_outputs, d_residuals, mode):
-        """Solve for the mesh residuals using the mesh Jacobian
-        """
-        if mode == 'rev':
-            d_residuals['xv'] = self.adj.solveMesh(d_outputs['xv']) # dRx = dRx_dX^-T * dX
-        elif mode == 'fwd':
-            raise NotImplementedError('DartMorpher - forward mode not implemented!\n')
-
-class DartDummyMorpher(om.ExplicitComponent):
-    """Dummy volume mesh morpher
-    Allow to define and link the surface mesh coordinates to the volume mesh coordinates, in case the mesh do not deform.
-    For compatibility, the node coordinates of connected surfaces are always 3D, while the volume mesh coordinates (only used internaly) can be 2D or 3D
-    """
-    def initialize(self):
-        self.options.declare('dim', desc='problem dimension')
-        self.options.declare('msh', desc='mesh data structure', recordable=False)
-
-    def setup(self):
-        dim = self.options['dim']
-        msh = self.options['msh']
-        # Set initial values
-        xv = np.zeros(dim * len(msh.nodes)) # volume node coordinates can be 2D or 3D
-        for i in range(len(msh.nodes)):
-            for j in range(dim):
-                xv[dim * i + j] = msh.nodes[i].pos[j]
-        # I/O
-        self.add_input('x_aero', shape_by_conn=True, desc='aerodynamic surface node coordinates', tags=['mphys_coupling'])
-        self.add_output('xv', val=xv, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
-
-    def compute(self, inputs, outputs):
-        """DO NOT deform the volume mesh
-        """
-        pass
-
-    def compute_partials(self, inputs, partials):
-        """DO NOT compute the partials derivatives
-        """
-        pass
-
-# Solver
-class DartSolver(om.ImplicitComponent):
-    """Aerodynamic solver
-
-    Attributes
-    ----------
-    sol : dart.Newton object
-        Direct Newton solver
-    adj : dart.Adjoint object
-        Adjoint solver
-    """
-    def initialize(self):
-        self.options.declare('sol', desc='direct solver', recordable=False)
-        self.options.declare('adj', desc='adjoint solver', recordable=False)
-
-    def setup(self):
-        self.sol = self.options['sol']
-        self.adj = self.options['adj']
-        # I/O
-        self.add_input('aoa', val=0., units='rad', desc='angle of attack', tags=['mphys_input'])
-        self.add_input('xv', shape_by_conn=True, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
-        self.add_output('phi', val=np.zeros(len(self.sol.pbl.msh.nodes)), desc='dart variables (potential)', tags=['mphys_coupling'])
-        # Partials
-        # self.declare_partials(of=['phi'], wrt=['aoa', 'xv', 'phi'])
-
-    def solve_nonlinear(self, inputs, outputs):
-        """Compute the flow variables
-        """
-        # Update inputs
-        # NB: the mesh is already up-to-date mesh already up-to-date because DartMorpher has already been run
-        self.sol.pbl.update(inputs['aoa'][0])
-        # Compute
-        self.sol.reset() # resets ICs (slows down convergence, but increases robustness for aero-structural)
-        self.sol.run()
-        # Update outputs
-        outputs['phi'] = self.sol.phi
-
-    def apply_nonlinear(self, inputs, outputs, residuals):
-        """Compute the residuals of the flow variables
-        """
-        raise NotImplementedError('DartSolver - cannot compute the residuals!\n')
-
-    def linearize(self, inputs, outputs, partials):
-        """Compute the flow jacobian matrix (and the other partials of the flow residuals)
-        """
-        self.adj.linearizeFlow()
-
-    def apply_linear(self, inputs, outputs, d_inputs, d_outputs, d_residuals, mode):
-        """Perform the matrix-vector product using the partial gradients of the flow residuals
-        """
-        if mode == 'rev':
-            if 'phi' in d_residuals:
-                if 'phi' in d_outputs:
-                    d_outputs['phi'] += self.adj.computeJacVecFlow(d_residuals['phi']) # dU = dRu_dU^T * dRu
-                if 'aoa' in d_inputs:
-                    d_inputs['aoa'] += self.adj.computeJacVecFlowAoa(d_residuals['phi']) # dA = dRu_dA^T * dRu
-                if 'xv' in d_inputs:
-                    d_inputs['xv'] += self.adj.computeJacVecFlowMesh(d_residuals['phi']) # dX = dRu_dX^T * dRu
-        elif mode == 'fwd':
-            raise NotImplementedError('DartSolver - forward mode not implemented!\n')
-
-    def solve_linear(self, d_outputs, d_residuals, mode):
-        """Solve for the flow residuals using the flow Jacobian
-        """
-        if mode == 'rev':
-            d_residuals['phi'] = self.adj.solveFlow(d_outputs['phi']) # dRu = dRu_dU^-T * dU
-        elif mode == 'fwd':
-            raise NotImplementedError('DartSolver - forward mode not implemented!\n')
-
-# Aerodynamic loads
-class DartLoads(om.ExplicitComponent):
-    """Aerodynamic loads on moving body
-    For compatibility, the forces of connected surfaces are always 3D
-
-    Attributes
-    ----------
-    qinf : dart.Newton object
-        Freestream dynamic pressure
-    bnd : dart.Body object
-        Moving body
-    adj : dart.Adjoint object
-        Adjoint solver
-    """
-    def initialize(self):
-        self.options.declare('qinf', desc='freestream dynamic pressure')
-        self.options.declare('bnd', desc='moving body', recordable=False)
-        self.options.declare('adj', desc='adjoint solver', recordable=False)
-
-    def setup(self):
-        self.qinf = self.options['qinf']
-        self.bnd = self.options['bnd']
-        self.adj = self.options['adj']
-        # I/O
-        self.add_input('xv', shape_by_conn=True, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
-        self.add_input('phi', shape_by_conn=True, desc='flow variables (potential)', tags=['mphys_coupling'])
-        self.add_output('f_aero', val=np.zeros(3 * len(self.bnd.nodes)), desc='aerodynamic loads', tags=['mphys_coupling'])
-        # Partials
-        # self.declare_partials(of=['f_aero'], wrt=['xv', 'phi'])
-
-    def compute(self, inputs, outputs):
-        """Get the forces on moving body
-        """
-        # NB: inputs already up-to-date because DartSolver has already been run
-        for i in range(len(self.bnd.nodes)):
-            for j in range(3):
-                outputs['f_aero'][3 * i + j] = self.qinf * self.bnd.nLoads[i][j]
-
-    def compute_partials(self, inputs, partials):
-        """Compute the partials gradients of the loads
-        """
-        self.adj.linearizeLoads(self.bnd)
-
-    def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode):
-        """Perform the matrix-vector product using the partial gradients of the loads
-        """
-        if mode == 'rev':
-            if 'f_aero' in d_outputs:
-                if 'xv' in d_inputs:
-                    d_inputs['xv'] += self.qinf * np.asarray(self.adj.computeJacVecLoadsMesh(d_outputs['f_aero'])) # dX = dL_dX^T * dL
-                if 'phi' in d_inputs:
-                    d_inputs['phi'] += self.qinf * np.asarray(self.adj.computeJacVecLoadsFlow(d_outputs['f_aero'])) # dU = dL_dU^T * dL
-        elif mode == 'fwd':
-            raise NotImplementedError('DartLoads - forward mode not implemented!\n')
-
-# Aerodynamic coefficients
-class DartCoefficients(om.ExplicitComponent):
-    """Aerodynamic load coefficients for full aircraft
-    Also write solution to disk
-
-    Attributes
-    ----------
-    iscn : int
-        ID of scenario (default: 0)
-    sol : dart.Newton object
-        Direct Newton sovler
-    adj : dart.Adjoint object
-        Adjoint solver
-    """
-    def initialize(self):
-        self.options.declare('sol', desc='direct solver', recordable=False)
-        self.options.declare('adj', desc='adjoint solver', recordable=False)
-        self.options.declare('wrtr', desc='data writer', recordable=False)
-        self.options.declare('morph', default=False, desc='whether the mesh can deform or not')
-
-    def setup(self):
-        self.iscn = 0 # id of scenario
-        self.sol = self.options['sol']
-        self.adj = self.options['adj']
-        # I/O
-        self.add_input('aoa', val=0., units='rad', desc='angle of attack', tags=['mphys_input'])
-        self.add_input('xv', shape_by_conn=True, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
-        self.add_input('phi', shape_by_conn=True, desc='flow variables (potential)', tags=['mphys_coupling'])
-        self.add_output('cl', val=0., desc='lift coefficient', tags=['mphys_result'])
-        self.add_output('cd', val=0., desc='drag coefficient', tags=['mphys_result'])
-        # Partials
-        self.declare_partials(of=['cl', 'cd'], wrt=['aoa', 'xv', 'phi'])
-
-    def mphys_set_iscn(self, iscn):
-        """Set the id of the scenario
-        Allow to save the mesh and results in different files
-        """
-        self.iscn = iscn
-
-    def compute(self, inputs, outputs):
-        """Get the coefficients for the full aircraft
-        """
-        # NB: inputs already up-to-date because DartSolver has already been run
-        # Write to disk
-        if self.options['morph'] and str(type(self.options['wrtr'])) == '<class \'tboxw.GmshExport\'>':
-            if self.iscn == 0:
-                self.options['wrtr'].save(self.sol.pbl.msh.name)
-            else:
-                self.options['wrtr'].save(self.sol.pbl.msh.name + '_{0:d}'.format(self.iscn))
-        self.sol.save(self.options['wrtr'], self.iscn)
-        # Update outputs
-        outputs['cl'] = self.sol.Cl
-        outputs['cd'] = self.sol.Cd
-
-    def compute_partials(self, inputs, partials):
-        """Compute the partials gradients of the load coefficients
-        """
-        # Gradients wrt to angle of attack
-        dCfAoa = self.adj.computeGradientCoefficientsAoa()
-        partials['cl', 'aoa'] = dCfAoa[0]
-        partials['cd', 'aoa'] = dCfAoa[1]
-        # Gradients wrt to mesh
-        dCfMsh = self.adj.computeGradientCoefficientsMesh()
-        partials['cl', 'xv'] = dCfMsh[0]
-        partials['cd', 'xv'] = dCfMsh[1]
-        # Gradients wrt to flow variables
-        dCfPhi = self.adj.computeGradientCoefficientsFlow()
-        partials['cl', 'phi'] = dCfPhi[0]
-        partials['cd', 'phi'] = dCfPhi[1]
-
-# Aerodynamic group
-class DartGroup(om.Group):
-    """Aerodynamic group
-    Integrate the aerodynamic computations in an aero-structural coupling procedure
-
-    Components
-    ----------
-    - Mesh morpher
-    - Direct and adjoint solvers
-    - Loads computation
-    """
-    def initialize(self):
-        self.options.declare('qinf', desc='freestream dynamic pressure')
-        self.options.declare('bnd', desc='moving body', recordable=False)
-        self.options.declare('sol', desc='direct solver', recordable=False)
-        self.options.declare('mrf', desc='mesh morpher', recordable=False)
-        self.options.declare('adj', desc='adjoint solver', recordable=False)
-
-    def setup(self):
-        # Components
-        if self.options['mrf'] is None:
-            self.add_subsystem('morpher', DartDummyMorpher(dim = self.options['sol'].pbl.nDim, msh = self.options['sol'].pbl.msh), promotes_inputs=['x_aero'], promotes_outputs=['xv'])
-        else:
-            self.add_subsystem('morpher', DartMorpher(dim = self.options['sol'].pbl.nDim, bnd = self.options['bnd'], mrf = self.options['mrf'], adj = self.options['adj']), promotes_inputs=['x_aero'], promotes_outputs=['xv'])
-        self.add_subsystem('solver', DartSolver(sol = self.options['sol'], adj = self.options['adj']), promotes_inputs=['aoa', 'xv'], promotes_outputs=['phi'])
-        if self.options['qinf'] != 0:
-            self.add_subsystem('loads', DartLoads(qinf = self.options['qinf'], bnd = self.options['bnd'], adj = self.options['adj']), promotes_inputs=['xv', 'phi'], promotes_outputs=['f_aero'])
-
-# Builder
-class DartBuilder(Builder):
-    """Dart builder for MPHYS
-
-    Attributes
-    ----------
-    __dim : int
-        Dimension of the problem
-    __qinf : float
-        Freestream dynamic pressure
-    __msh : tbox.MshData object
-        Mesh data structure
-    __wrtr : tbox.MshExport object
-        Mesh writer
-    __mrf : tbox.MshDeform object
-        Mesh morpher
-    __pbl : dart.Probem object
-        Problem definition
-    __sol : dart.Newton object
-        Direct Newton solver
-    __adj : dart.Adjoint object
-        Adjoint solver
-    """
-    def __init__(self, params, scenario='aerodynamic', task='analysis'):
-        """Instantiate and initialize all the solver components.
-        Because we do not use MPI, we do not use the initialize method.
-
-        Parameters
-        ----------
-        params : dict
-            Dictonnary of parameters for the mesh, solver, ...
-        scenario : string, optional
-            Type of scenario (available: aerodynamic, aerostructural) (default: aerodynamic)
-        task : string, optional
-            Type of task (available: analysis, optimization) (default: analysis)
-        """
-        # Initialize
-        print('*'*31 + 'mphys.DartBuilder' + '*'*31 + '\n')
-
-        # Imports
-        from dartflo import tbox
-        from dartflo.tbox import gmsh
-        from dartflo import dart
-
-        # Basic checks and config
-        # dimension
-        if params['Dim'] != 2 and params['Dim'] != 3:
-            raise RuntimeError('Problem dimension should be 2 or 3, but ' + params['Dim'] + ' was given!\n')
-        else:
-            self.__dim = params['Dim']
-        # aoa and aos
-        if 'AoA' in params:
-            alpha = params['AoA'] * np.pi / 180
-        else:
-            alpha = 0
-        if self.__dim == 2:
-            if 'AoS' in params and params['AoS'] != 0:
-                raise RuntimeError('Angle of sideslip (AoS) should be zero for 2D problems!\n')
-            else:
-                beta = 0
-            if 'Symmetry' in params:
-                raise RuntimeError('Symmetry boundary condition cannot be used for 2D problems!\n')
-        else:
-            if 'AoS' in params:
-                if params['AoS'] != 0 and 'Symmetry' in params:
-                    raise RuntimeError('Symmetry boundary condition cannot be used with nonzero angle of sideslip (AoS)!\n')
-                beta = params['AoS'] * np.pi / 180
-            else:
-                beta = 0
-        # save format
-        if params['Format'] == 'vtk':
-            try:
-               import tboxVtk
-               Writer = tboxVtk.VtkExport
-               print('VTK libraries found! Results will be saved in VTK format.\n')
-            except:
-               Writer = tbox.GmshExport
-               print('VTK libraries not found! Results will be saved in gmsh format.\n')
-        else:
-            Writer = tbox.GmshExport
-            print('Results will be saved in gmsh format.\n')
-        # number of threads
-        if 'Threads' in params:
-            nthrd = params['Threads']
-        else:
-            nthrd = 1
-        # verbosity
-        if 'Verb' in params:
-            verb = params['Verb']
-        else:
-            verb = 0
-        # scenario and task type
-        if scenario != 'aerodynamic' and scenario != 'aerostructural':
-            raise RuntimeError('Scenario should be aerodynamic or aerostructural, but "' + scenario + '" was given!\n')
-        if task != 'analysis' and task != 'optimization':
-            raise RuntimeError('Task should be analysis or optimization, but "' + scenario + '" was given!\n')
-        # dynamic pressure
-        if scenario == 'aerostructural':
-            self.__qinf = params['Qinf']
-        else:
-            self.__qinf = 0
-
-        # Mesh creation
-        self.__msh = gmsh.MeshLoader(params['File']).execute(**params['Pars'])
-        # add the wake
-        if self.__dim == 2:
-            mshCrck = tbox.MshCrack(self.__msh, self.__dim)
-            mshCrck.setCrack(params['Wake'])
-            mshCrck.addBoundaries([params['Fluid'], params['Farfield'][-1], params['Wing']])
-            mshCrck.run()
-        else:
-            for i in range(len(params['Wakes'])):
-                mshCrck = tbox.MshCrack(self.__msh, self.__dim)
-                mshCrck.setCrack(params['Wakes'][i])
-                mshCrck.addBoundaries([params['Fluid'], params['Farfield'][-1], params['Wings'][i]])
-                if 'Fuselage' in params:
-                    mshCrck.addBoundaries([params['Fuselage']])
-                if 'Symmetry' in params:
-                    mshCrck.addBoundaries([params['Symmetry']])
-                if 'WakeTips' in params:
-                    mshCrck.setExcluded(params['WakeTips'][i]) # 2.5D computations
-                mshCrck.run()
-        # save the updated mesh in native (gmsh) format and then set the writer to the requested format
-        tbox.GmshExport(self.__msh).save(self.__msh.name)
-        del mshCrck
-        self.__wrtr = Writer(self.__msh)
-        print('\n')
-
-        # Mesh morpher creation
-        if scenario == 'aerostructural' or task == 'optimization':
-            self.__mrf = tbox.MshDeform(self.__msh, self.__dim)
-            self.__mrf.nthreads = nthrd
-            self.__mrf.setField(params['Fluid'])
-            self.__mrf.addFixed(params['Farfield'])
-            if self.__dim == 2:
-                self.__mrf.addMoving([params['Wing']])
-                self.__mrf.addInternal([params['Wake'], params['Wake']+'_'])
-            else:
-                if 'Fuselage' in params:
-                    self.__mrf.addFixed(params['Fuselage'])
-                self.__mrf.setSymmetry(params['Symmetry'], 1)
-                for i in range(len(params['Wings'])):
-                    if i == 0:
-                        self.__mrf.addMoving([params['Wings'][i]]) # TODO body of interest (FSI/OPT) is always first given body
-                    else:
-                        self.__mrf.addFixed([params['Wings'][i]])
-                    self.__mrf.addInternal([params['Wakes'][i], params['Wakes'][i]+'_'])
-            self.__mrf.initialize()
-        else:
-            self.__mrf = None
-
-        # Problem creation
-        self.__pbl = dart.Problem(self.__msh, self.__dim, alpha, beta, params['M_inf'], params['S_ref'], params['c_ref'], params['x_ref'], params['y_ref'], params['z_ref'])
-        # add the field
-        self.__pbl.set(dart.Fluid(self.__msh, params['Fluid'], params['M_inf'], self.__dim, alpha, beta))
-        # add the initial condition
-        self.__pbl.set(dart.Initial(self.__msh, params['Fluid'], self.__dim, alpha, beta))
-        # add farfield boundary conditions (Neumann only, a DOF will be pinned automatically)
-        for i in range (len(params['Farfield'])):
-            self.__pbl.add(dart.Freestream(self.__msh, params['Farfield'][i], self.__dim, alpha, beta))
-        # add solid boundaries
-        if self.__dim == 2:
-            bnd = dart.Body(self.__msh, [params['Wing'], params['Fluid']])
-            self.__bnd = bnd
-            self.__pbl.add(bnd)
-        else:
-            self.__bnd = None
-            for bd in params['Wings']:
-                bnd = dart.Body(self.__msh, [bd, params['Fluid']])
-                if self.__bnd is None:
-                    self.__bnd = bnd # TODO body of interest (FSI/OPT) is always first given body
-                self.__pbl.add(bnd)
-            if 'Fuselage' in params:
-                bnd = dart.Body(self.__msh, [params['Fuselage'], params['Fluid']])
-                self.__pbl.add(bnd)
-        # add Wake/Kutta boundary conditions
-        if self.__dim == 2:
-            self.__pbl.add(dart.Wake(self.__msh, [params['Wake'], params['Wake']+'_', params['Fluid']]))
-            self.__pbl.add(dart.Kutta(self.__msh, [params['Te'], params['Wake']+'_', params['Wing'], params['Fluid']]))
-        else:
-            for i in range(len(params['Wakes'])):
-                self.__pbl.add(dart.Wake(self.__msh, [params['Wakes'][i], params['Wakes'][i]+'_', params['Fluid'], params['TeTips'][i]]))
-
-        # Direct (forward) solver creation
-        # NB: more solvers/options are available but we restrict the user's choice to the most efficient ones
-        # initialize the linear (inner) solver
-        if params['LSolver'] == 'PARDISO':
-            linsol = tbox.Pardiso()
-        elif params['LSolver'] == 'MUMPS':
-            linsol = tbox.Mumps()
-        elif params['LSolver'] == 'GMRES':
-            linsol = tbox.Gmres()
-            linsol.setFillFactor(params['G_fill']) if 'G_fill' in params else linsol.setFillFactor(2)
-            linsol.setRestart(params['G_restart']) if 'G_restart' in params else linsol.setRestart(50)
-            linsol.setTolerance(params['G_tol']) if 'G_tol' in params else linsol.setTolerance(1e-5)
-        else:
-            raise RuntimeError('Available linear solvers: PARDISO, MUMPS or GMRES, but ' + params['LSolver'] + ' was given!\n')
-        # initialize the nonlinear (outer) solver
-        self.__sol = dart.Newton(self.__pbl, linsol, rTol=params['Rel_tol'], aTol=params['Abs_tol'], mIt=params['Max_it'], nthrds=nthrd, vrb=verb)
-
-        # Adjoint (reverse) solver creation
-        if task == 'optimization':
-            if params['LSolver'] == 'PARDISO':
-                alinsol = tbox.Pardiso()
-            elif params['LSolver'] == 'MUMPS':
-                alinsol = tbox.Mumps()
-            else:
-                alinsol = tbox.Gmres()
-                alinsol.setFillFactor(params['G_fill']) if 'G_fill' in params else alinsol.setFillFactor(2)
-                alinsol.setRestart(params['G_restart']) if 'G_restart' in params else alinsol.setRestart(50)
-                alinsol.setTolerance(1e-12)
-            self.__adj = dart.Adjoint(self.__sol, self.__mrf, alinsol)
-        else:
-            self.__adj = None
-
-        # Finalize
-        print('*'*31 + 'mphys.DartBuilder' + '*'*31 + '\n')
-
-    def get_mesh_coordinate_subsystem(self):
-        """Return openMDAO component to get the initial surface mesh coordinates
-        """
-        return DartMesh(dim = self.__dim, bnd = self.__bnd)
-
-    def get_coupling_group_subsystem(self):
-        """Return openMDAO group containing the morpher, solver and loads
-        """
-        return DartGroup(qinf = self.__qinf, bnd = self.__bnd, sol = self.__sol, mrf = self.__mrf, adj = self.__adj)
-
-    def get_post_coupling_subsystem(self):
-        """Return openMDAO component that computes the aero coefficients and writes data to disk
-        """
-        return DartCoefficients(sol = self.__sol, adj = self.__adj, wrtr = self.__wrtr, morph = False if self.__mrf is None else True)
-
-    def get_number_of_nodes(self):
-        """Return the number of surface nodes
-        """
-        return len(self.__bnd.nodes)
+# -*- coding: utf-8 -*-
+
+# Copyright 2021 University of Liège
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+## MPHYS interface
+# Adrien Crovato
+#
+# Structure:
+#   - AeroBuilder
+#       - AeroSurfaceMesh
+#       - AeroGroup
+#           - Morpher
+#           - Solver
+#           - Loads
+#       - AeroCoefficients
+#
+# Remarks:
+# - for each component that uses the openMDAO matrix-free API, the partial
+#   gradients are first computed internally in dart::Adjoint, through
+#   om.ImplicitComponent.linearize() or om.ExplicitComponent.compute_partials(),
+#   and the matrix-vector product is then computed in dart:: Adjoint through
+#   om.ImplicitComponent.apply_linear() or om.ExplicitComponent.compute_jacvec_product().
+#   This allows faster gradient evaluations, at the cost of a reasonable
+#   increase in memory storage, without interfacing any sparse matrices through
+#   python.
+
+import numpy as np
+import openmdao.api as om
+from mphys.builder import Builder
+
+# Surface mesh
+class DartMesh(om.IndepVarComp):
+    """Initial surface mesh of moving body
+    For compatibility, the node coordinates of connected surfaces are always 3D
+
+    Attributes
+    ----------
+    dim : int
+        Problem dimension
+    bnd : dart.Body object
+        Moving body
+    """
+    def initialize(self):
+        self.options.declare('dim', desc='problem dimension')
+        self.options.declare('bnd', desc='moving body', recordable=False)
+
+    def setup(self):
+        self.bnd = self.options['bnd']
+        # Store all the node coordinates and add as output
+        x_aero0 = np.zeros(3 * len(self.bnd.nodes)) # surface node coordinates are 3D
+        for i in range(len(self.bnd.nodes)):
+            for j in range(3):
+                x_aero0[3 * i + j] = self.bnd.nodes[i].pos[j]
+        self.add_output('x_aero0', val=x_aero0, desc='initial aerodynamic surface node coordinates', tags=['mphys_coordinates'])
+
+    def mphys_get_triangulated_surface(self):
+        """Triangulate the surface
+        Only for 2D surfaces in 3D problems
+        Return a list containing a point and two vectors for each surface element
+             o p0
+            / \
+        v0 /___\ v1
+        """
+        if self.options['dim'] == 2:
+            raise RuntimeError('DartMesh - cannot triangulate 1D surface!\n')
+        # Create list of point coordinates and vector components
+        p0 = [[0.]*3 for _ in range(len(self.bnd.groups[0].tag.elems))]
+        v0 = [[0.]*3 for _ in range(len(self.bnd.groups[0].tag.elems))]
+        v1 = [[0.]*3 for _ in range(len(self.bnd.groups[0].tag.elems))]
+        for i in range(len(self.bnd.groups[0].tag.elems)):
+            e = self.bnd.groups[0].tag.elems[i]
+            p0i = e.nodes[0].pos
+            v0i = e.nodes[1].pos - e.nodes[0].pos
+            v1i = e.nodes[2].pos - e.nodes[0].pos
+            for j in range(3):
+                p0[i][j] = p0i[j]
+                v0[i][j] = v0i[j]
+                v1[i][j] = v1i[j]
+        return [p0, v0, v1]
+
+# Mesh morphers
+class DartMorpher(om.ImplicitComponent):
+    """Volume mesh morpher
+    For compatibility, the node coordinates of connected surfaces are always 3D, while the volume mesh coordinates (only used internaly) can be 2D or 3D
+
+    Attributes
+    ----------
+    dim : int
+        Problem dimension, used to set the length of the volume node coordinates vector
+    bnd : dart.Body object
+        Moving body
+    mrf : tbox.MshDeform object
+        Mesh morpher
+    adj : dart.Adjoint object
+        Adjoint solver
+    """
+    def initialize(self):
+        self.options.declare('dim', desc='problem dimension')
+        self.options.declare('bnd', desc='moving body', recordable=False)
+        self.options.declare('mrf', desc='mesh morpher', recordable=False)
+        self.options.declare('adj', desc='adjoint solver', recordable=False)
+
+    def setup(self):
+        self.dim = self.options['dim']
+        self.bnd = self.options['bnd']
+        self.mrf = self.options['mrf']
+        self.adj = self.options['adj']
+        # I/O
+        self.add_input('x_aero', shape_by_conn=True, desc='aerodynamic surface node coordinates', tags=['mphys_coupling'])
+        self.add_output('xv', val=np.zeros(self.dim * len(self.mrf.msh.nodes)), desc='aerodynamic volume node coordinates', tags=['mphys_coupling']) # volume node coordinates can be 2D or 3D
+        # Partials
+        # self.declare_partials(of=['xv'], wrt=['x_aero'])
+
+    def solve_nonlinear(self, inputs, outputs):
+        """Deform the volume mesh
+        """
+        # Update inputs
+        self.mrf.savePos()
+        for i in range(len(self.bnd.nodes)):
+            for j in range(3):
+                self.bnd.nodes[i].pos[j] = inputs['x_aero'][3 * i + j]
+        # Compute
+        self.mrf.deform()
+        # Update outputs
+        for i in range(len(self.mrf.msh.nodes)):
+            for j in range(self.dim):
+                outputs['xv'][self.dim * i + j] = self.mrf.msh.nodes[i].pos[j]
+
+    def apply_nonlinear(self, inputs, outputs, residuals):
+        """Compute the residuals of the mesh coordinates
+        """
+        raise NotImplementedError('DartMorpher - cannot compute the residuals!\n')
+
+    def linearize(self, inputs, outputs, partials):
+        """Compute the mesh jacobian matrix
+        """
+        self.adj.linearizeMesh()
+
+    def apply_linear(self, inputs, outputs, d_inputs, d_outputs, d_residuals, mode):
+        """Perform the matrix-vector product using the mesh Jacobian
+        """
+        if mode == 'rev':
+            if 'xv' in d_residuals:
+                if 'xv' in d_outputs:
+                    d_outputs['xv'] += self.adj.computeJacVecMesh(d_residuals['xv']) # dX = dRx_dX^T * dRx
+                if 'x_aero' in d_inputs:
+                    for i in range(len(self.bnd.nodes)):
+                        for j in range(self.dim):
+                            d_inputs['x_aero'][3 * i + j] -= d_residuals['xv'][self.dim * self.bnd.nodes[i].row + j] # dXs = -dX
+        elif mode == 'fwd':
+            raise NotImplementedError('DartMorpher - forward mode not implemented!\n')
+
+    def solve_linear(self, d_outputs, d_residuals, mode):
+        """Solve for the mesh residuals using the mesh Jacobian
+        """
+        if mode == 'rev':
+            d_residuals['xv'] = self.adj.solveMesh(d_outputs['xv']) # dRx = dRx_dX^-T * dX
+        elif mode == 'fwd':
+            raise NotImplementedError('DartMorpher - forward mode not implemented!\n')
+
+class DartDummyMorpher(om.ExplicitComponent):
+    """Dummy volume mesh morpher
+    Allow to define and link the surface mesh coordinates to the volume mesh coordinates, in case the mesh do not deform.
+    For compatibility, the node coordinates of connected surfaces are always 3D, while the volume mesh coordinates (only used internaly) can be 2D or 3D
+    """
+    def initialize(self):
+        self.options.declare('dim', desc='problem dimension')
+        self.options.declare('msh', desc='mesh data structure', recordable=False)
+
+    def setup(self):
+        dim = self.options['dim']
+        msh = self.options['msh']
+        # Set initial values
+        xv = np.zeros(dim * len(msh.nodes)) # volume node coordinates can be 2D or 3D
+        for i in range(len(msh.nodes)):
+            for j in range(dim):
+                xv[dim * i + j] = msh.nodes[i].pos[j]
+        # I/O
+        self.add_input('x_aero', shape_by_conn=True, desc='aerodynamic surface node coordinates', tags=['mphys_coupling'])
+        self.add_output('xv', val=xv, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
+
+    def compute(self, inputs, outputs):
+        """DO NOT deform the volume mesh
+        """
+        pass
+
+    def compute_partials(self, inputs, partials):
+        """DO NOT compute the partials derivatives
+        """
+        pass
+
+# Solver
+class DartSolver(om.ImplicitComponent):
+    """Aerodynamic solver
+
+    Attributes
+    ----------
+    sol : dart.Newton object
+        Direct Newton solver
+    adj : dart.Adjoint object
+        Adjoint solver
+    """
+    def initialize(self):
+        self.options.declare('sol', desc='direct solver', recordable=False)
+        self.options.declare('adj', desc='adjoint solver', recordable=False)
+
+    def setup(self):
+        self.sol = self.options['sol']
+        self.adj = self.options['adj']
+        # I/O
+        self.add_input('aoa', val=0., units='rad', desc='angle of attack', tags=['mphys_input'])
+        self.add_input('xv', shape_by_conn=True, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
+        self.add_output('phi', val=np.zeros(len(self.sol.pbl.msh.nodes)), desc='dart variables (potential)', tags=['mphys_coupling'])
+        # Partials
+        # self.declare_partials(of=['phi'], wrt=['aoa', 'xv', 'phi'])
+
+    def solve_nonlinear(self, inputs, outputs):
+        """Compute the flow variables
+        """
+        # Update inputs
+        # NB: the mesh is already up-to-date mesh already up-to-date because DartMorpher has already been run
+        self.sol.pbl.update(inputs['aoa'][0])
+        # Compute
+        self.sol.reset() # resets ICs (slows down convergence, but increases robustness for aero-structural)
+        self.sol.run()
+        # Update outputs
+        outputs['phi'] = self.sol.phi
+
+    def apply_nonlinear(self, inputs, outputs, residuals):
+        """Compute the residuals of the flow variables
+        """
+        raise NotImplementedError('DartSolver - cannot compute the residuals!\n')
+
+    def linearize(self, inputs, outputs, partials):
+        """Compute the flow jacobian matrix (and the other partials of the flow residuals)
+        """
+        self.adj.linearizeFlow()
+
+    def apply_linear(self, inputs, outputs, d_inputs, d_outputs, d_residuals, mode):
+        """Perform the matrix-vector product using the partial gradients of the flow residuals
+        """
+        if mode == 'rev':
+            if 'phi' in d_residuals:
+                if 'phi' in d_outputs:
+                    d_outputs['phi'] += self.adj.computeJacVecFlow(d_residuals['phi']) # dU = dRu_dU^T * dRu
+                if 'aoa' in d_inputs:
+                    d_inputs['aoa'] += self.adj.computeJacVecFlowAoa(d_residuals['phi']) # dA = dRu_dA^T * dRu
+                if 'xv' in d_inputs:
+                    d_inputs['xv'] += self.adj.computeJacVecFlowMesh(d_residuals['phi']) # dX = dRu_dX^T * dRu
+        elif mode == 'fwd':
+            raise NotImplementedError('DartSolver - forward mode not implemented!\n')
+
+    def solve_linear(self, d_outputs, d_residuals, mode):
+        """Solve for the flow residuals using the flow Jacobian
+        """
+        if mode == 'rev':
+            d_residuals['phi'] = self.adj.solveFlow(d_outputs['phi']) # dRu = dRu_dU^-T * dU
+        elif mode == 'fwd':
+            raise NotImplementedError('DartSolver - forward mode not implemented!\n')
+
+# Aerodynamic loads
+class DartLoads(om.ExplicitComponent):
+    """Aerodynamic loads on moving body
+    For compatibility, the forces of connected surfaces are always 3D
+
+    Attributes
+    ----------
+    qinf : dart.Newton object
+        Freestream dynamic pressure
+    bnd : dart.Body object
+        Moving body
+    adj : dart.Adjoint object
+        Adjoint solver
+    """
+    def initialize(self):
+        self.options.declare('qinf', desc='freestream dynamic pressure')
+        self.options.declare('bnd', desc='moving body', recordable=False)
+        self.options.declare('adj', desc='adjoint solver', recordable=False)
+
+    def setup(self):
+        self.qinf = self.options['qinf']
+        self.bnd = self.options['bnd']
+        self.adj = self.options['adj']
+        # I/O
+        self.add_input('xv', shape_by_conn=True, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
+        self.add_input('phi', shape_by_conn=True, desc='flow variables (potential)', tags=['mphys_coupling'])
+        self.add_output('f_aero', val=np.zeros(3 * len(self.bnd.nodes)), desc='aerodynamic loads', tags=['mphys_coupling'])
+        # Partials
+        # self.declare_partials(of=['f_aero'], wrt=['xv', 'phi'])
+
+    def compute(self, inputs, outputs):
+        """Get the forces on moving body
+        """
+        # NB: inputs already up-to-date because DartSolver has already been run
+        for i in range(len(self.bnd.nodes)):
+            for j in range(3):
+                outputs['f_aero'][3 * i + j] = self.qinf * self.bnd.nLoads[i][j]
+
+    def compute_partials(self, inputs, partials):
+        """Compute the partials gradients of the loads
+        """
+        self.adj.linearizeLoads(self.bnd)
+
+    def compute_jacvec_product(self, inputs, d_inputs, d_outputs, mode):
+        """Perform the matrix-vector product using the partial gradients of the loads
+        """
+        if mode == 'rev':
+            if 'f_aero' in d_outputs:
+                if 'xv' in d_inputs:
+                    d_inputs['xv'] += self.qinf * np.asarray(self.adj.computeJacVecLoadsMesh(d_outputs['f_aero'])) # dX = dL_dX^T * dL
+                if 'phi' in d_inputs:
+                    d_inputs['phi'] += self.qinf * np.asarray(self.adj.computeJacVecLoadsFlow(d_outputs['f_aero'])) # dU = dL_dU^T * dL
+        elif mode == 'fwd':
+            raise NotImplementedError('DartLoads - forward mode not implemented!\n')
+
+# Aerodynamic coefficients
+class DartCoefficients(om.ExplicitComponent):
+    """Aerodynamic load coefficients for full aircraft
+    Also write solution to disk
+
+    Attributes
+    ----------
+    iscn : int
+        ID of scenario (default: 0)
+    sol : dart.Newton object
+        Direct Newton sovler
+    adj : dart.Adjoint object
+        Adjoint solver
+    """
+    def initialize(self):
+        self.options.declare('sol', desc='direct solver', recordable=False)
+        self.options.declare('adj', desc='adjoint solver', recordable=False)
+        self.options.declare('wrtr', desc='data writer', recordable=False)
+        self.options.declare('morph', default=False, desc='whether the mesh can deform or not')
+
+    def setup(self):
+        self.iscn = 0 # id of scenario
+        self.sol = self.options['sol']
+        self.adj = self.options['adj']
+        # I/O
+        self.add_input('aoa', val=0., units='rad', desc='angle of attack', tags=['mphys_input'])
+        self.add_input('xv', shape_by_conn=True, desc='aerodynamic volume node coordinates', tags=['mphys_coupling'])
+        self.add_input('phi', shape_by_conn=True, desc='flow variables (potential)', tags=['mphys_coupling'])
+        self.add_output('cl', val=0., desc='lift coefficient', tags=['mphys_result'])
+        self.add_output('cd', val=0., desc='drag coefficient', tags=['mphys_result'])
+        # Partials
+        self.declare_partials(of=['cl', 'cd'], wrt=['aoa', 'xv', 'phi'])
+
+    def mphys_set_iscn(self, iscn):
+        """Set the id of the scenario
+        Allow to save the mesh and results in different files
+        """
+        self.iscn = iscn
+
+    def compute(self, inputs, outputs):
+        """Get the coefficients for the full aircraft
+        """
+        # NB: inputs already up-to-date because DartSolver has already been run
+        # Write to disk
+        if self.options['morph'] and str(type(self.options['wrtr'])) == '<class \'tboxw.GmshExport\'>':
+            if self.iscn == 0:
+                self.options['wrtr'].save(self.sol.pbl.msh.name)
+            else:
+                self.options['wrtr'].save(self.sol.pbl.msh.name + '_{0:d}'.format(self.iscn))
+        self.sol.save(self.options['wrtr'], self.iscn)
+        # Update outputs
+        outputs['cl'] = self.sol.Cl
+        outputs['cd'] = self.sol.Cd
+
+    def compute_partials(self, inputs, partials):
+        """Compute the partials gradients of the load coefficients
+        """
+        # Gradients wrt to angle of attack
+        dCfAoa = self.adj.computeGradientCoefficientsAoa()
+        partials['cl', 'aoa'] = dCfAoa[0]
+        partials['cd', 'aoa'] = dCfAoa[1]
+        # Gradients wrt to mesh
+        dCfMsh = self.adj.computeGradientCoefficientsMesh()
+        partials['cl', 'xv'] = dCfMsh[0]
+        partials['cd', 'xv'] = dCfMsh[1]
+        # Gradients wrt to flow variables
+        dCfPhi = self.adj.computeGradientCoefficientsFlow()
+        partials['cl', 'phi'] = dCfPhi[0]
+        partials['cd', 'phi'] = dCfPhi[1]
+
+# Aerodynamic group
+class DartGroup(om.Group):
+    """Aerodynamic group
+    Integrate the aerodynamic computations in an aero-structural coupling procedure
+
+    Components
+    ----------
+    - Mesh morpher
+    - Direct and adjoint solvers
+    - Loads computation
+    """
+    def initialize(self):
+        self.options.declare('qinf', desc='freestream dynamic pressure')
+        self.options.declare('bnd', desc='moving body', recordable=False)
+        self.options.declare('sol', desc='direct solver', recordable=False)
+        self.options.declare('mrf', desc='mesh morpher', recordable=False)
+        self.options.declare('adj', desc='adjoint solver', recordable=False)
+
+    def setup(self):
+        # Components
+        if self.options['mrf'] is None:
+            self.add_subsystem('morpher', DartDummyMorpher(dim = self.options['sol'].pbl.nDim, msh = self.options['sol'].pbl.msh), promotes_inputs=['x_aero'], promotes_outputs=['xv'])
+        else:
+            self.add_subsystem('morpher', DartMorpher(dim = self.options['sol'].pbl.nDim, bnd = self.options['bnd'], mrf = self.options['mrf'], adj = self.options['adj']), promotes_inputs=['x_aero'], promotes_outputs=['xv'])
+        self.add_subsystem('solver', DartSolver(sol = self.options['sol'], adj = self.options['adj']), promotes_inputs=['aoa', 'xv'], promotes_outputs=['phi'])
+        if self.options['qinf'] != 0:
+            self.add_subsystem('loads', DartLoads(qinf = self.options['qinf'], bnd = self.options['bnd'], adj = self.options['adj']), promotes_inputs=['xv', 'phi'], promotes_outputs=['f_aero'])
+
+# Builder
+class DartBuilder(Builder):
+    """Dart builder for MPHYS
+
+    Attributes
+    ----------
+    __dim : int
+        Dimension of the problem
+    __qinf : float
+        Freestream dynamic pressure
+    __msh : tbox.MshData object
+        Mesh data structure
+    __wrtr : tbox.MshExport object
+        Mesh writer
+    __mrf : tbox.MshDeform object
+        Mesh morpher
+    __pbl : dart.Probem object
+        Problem definition
+    __sol : dart.Newton object
+        Direct Newton solver
+    __adj : dart.Adjoint object
+        Adjoint solver
+    """
+    def __init__(self, params, scenario='aerodynamic', task='analysis'):
+        """Instantiate and initialize all the solver components.
+        Because we do not use MPI, we do not use the initialize method.
+
+        Parameters
+        ----------
+        params : dict
+            Dictonnary of parameters for the mesh, solver, ...
+        scenario : string, optional
+            Type of scenario (available: aerodynamic, aerostructural) (default: aerodynamic)
+        task : string, optional
+            Type of task (available: analysis, optimization) (default: analysis)
+        """
+        # Initialize
+        print('*'*31 + 'mphys.DartBuilder' + '*'*31 + '\n')
+
+        # Imports
+        from dartflo import tbox
+        from dartflo.tbox import gmsh
+        from dartflo import dart
+
+        # Basic checks and config
+        # dimension
+        if params['Dim'] != 2 and params['Dim'] != 3:
+            raise RuntimeError('Problem dimension should be 2 or 3, but ' + params['Dim'] + ' was given!\n')
+        else:
+            self.__dim = params['Dim']
+        # aoa and aos
+        if 'AoA' in params:
+            alpha = params['AoA'] * np.pi / 180
+        else:
+            alpha = 0
+        if self.__dim == 2:
+            if 'AoS' in params and params['AoS'] != 0:
+                raise RuntimeError('Angle of sideslip (AoS) should be zero for 2D problems!\n')
+            else:
+                beta = 0
+            if 'Symmetry' in params:
+                raise RuntimeError('Symmetry boundary condition cannot be used for 2D problems!\n')
+        else:
+            if 'AoS' in params:
+                if params['AoS'] != 0 and 'Symmetry' in params:
+                    raise RuntimeError('Symmetry boundary condition cannot be used with nonzero angle of sideslip (AoS)!\n')
+                beta = params['AoS'] * np.pi / 180
+            else:
+                beta = 0
+        # save format
+        if params['Format'] == 'vtk':
+            try:
+               import tboxVtk
+               Writer = tboxVtk.VtkExport
+               print('VTK libraries found! Results will be saved in VTK format.\n')
+            except:
+               Writer = tbox.GmshExport
+               print('VTK libraries not found! Results will be saved in gmsh format.\n')
+        else:
+            Writer = tbox.GmshExport
+            print('Results will be saved in gmsh format.\n')
+        # number of threads
+        if 'Threads' in params:
+            nthrd = params['Threads']
+        else:
+            nthrd = 1
+        # verbosity
+        if 'Verb' in params:
+            verb = params['Verb']
+        else:
+            verb = 0
+        # scenario and task type
+        if scenario != 'aerodynamic' and scenario != 'aerostructural':
+            raise RuntimeError('Scenario should be aerodynamic or aerostructural, but "' + scenario + '" was given!\n')
+        if task != 'analysis' and task != 'optimization':
+            raise RuntimeError('Task should be analysis or optimization, but "' + scenario + '" was given!\n')
+        # dynamic pressure
+        if scenario == 'aerostructural':
+            self.__qinf = params['Q_inf']
+        else:
+            self.__qinf = 0
+
+        # Mesh creation
+        self.__msh = gmsh.MeshLoader(params['File']).execute(**params['Pars'])
+        # add the wake
+        if self.__dim == 2:
+            mshCrck = tbox.MshCrack(self.__msh, self.__dim)
+            mshCrck.setCrack(params['Wake'])
+            mshCrck.addBoundaries([params['Fluid'], params['Farfield'][-1], params['Wing']])
+            mshCrck.run()
+        else:
+            for i in range(len(params['Wakes'])):
+                mshCrck = tbox.MshCrack(self.__msh, self.__dim)
+                mshCrck.setCrack(params['Wakes'][i])
+                mshCrck.addBoundaries([params['Fluid'], params['Farfield'][-1], params['Wings'][i]])
+                if 'Fuselage' in params:
+                    mshCrck.addBoundaries([params['Fuselage']])
+                if 'Symmetry' in params:
+                    mshCrck.addBoundaries([params['Symmetry']])
+                if 'WakeTips' in params:
+                    mshCrck.setExcluded(params['WakeTips'][i]) # 2.5D computations
+                mshCrck.run()
+        # save the updated mesh in native (gmsh) format and then set the writer to the requested format
+        tbox.GmshExport(self.__msh).save(self.__msh.name)
+        del mshCrck
+        self.__wrtr = Writer(self.__msh)
+        print('\n')
+
+        # Mesh morpher creation
+        if scenario == 'aerostructural' or task == 'optimization':
+            self.__mrf = tbox.MshDeform(self.__msh, self.__dim)
+            self.__mrf.nthreads = nthrd
+            self.__mrf.setField(params['Fluid'])
+            self.__mrf.addFixed(params['Farfield'])
+            if self.__dim == 2:
+                self.__mrf.addMoving([params['Wing']])
+                self.__mrf.addInternal([params['Wake'], params['Wake']+'_'])
+            else:
+                if 'Fuselage' in params:
+                    self.__mrf.addFixed(params['Fuselage'])
+                self.__mrf.setSymmetry(params['Symmetry'], 1)
+                for i in range(len(params['Wings'])):
+                    if i == 0:
+                        self.__mrf.addMoving([params['Wings'][i]]) # TODO body of interest (FSI/OPT) is always first given body
+                    else:
+                        self.__mrf.addFixed([params['Wings'][i]])
+                    self.__mrf.addInternal([params['Wakes'][i], params['Wakes'][i]+'_'])
+            self.__mrf.initialize()
+        else:
+            self.__mrf = None
+
+        # Problem creation
+        self.__pbl = dart.Problem(self.__msh, self.__dim, alpha, beta, params['M_inf'], params['S_ref'], params['c_ref'], params['x_ref'], params['y_ref'], params['z_ref'])
+        # add the field
+        self.__pbl.set(dart.Fluid(self.__msh, params['Fluid'], params['M_inf'], self.__dim, alpha, beta))
+        # add the initial condition
+        self.__pbl.set(dart.Initial(self.__msh, params['Fluid'], self.__dim, alpha, beta))
+        # add farfield boundary conditions (Neumann only, a DOF will be pinned automatically)
+        for i in range (len(params['Farfield'])):
+            self.__pbl.add(dart.Freestream(self.__msh, params['Farfield'][i], self.__dim, alpha, beta))
+        # add solid boundaries
+        if self.__dim == 2:
+            bnd = dart.Body(self.__msh, [params['Wing'], params['Fluid']])
+            self.__bnd = bnd
+            self.__pbl.add(bnd)
+        else:
+            self.__bnd = None
+            for bd in params['Wings']:
+                bnd = dart.Body(self.__msh, [bd, params['Fluid']])
+                if self.__bnd is None:
+                    self.__bnd = bnd # TODO body of interest (FSI/OPT) is always first given body
+                self.__pbl.add(bnd)
+            if 'Fuselage' in params:
+                bnd = dart.Body(self.__msh, [params['Fuselage'], params['Fluid']])
+                self.__pbl.add(bnd)
+        # add Wake/Kutta boundary conditions
+        if self.__dim == 2:
+            self.__pbl.add(dart.Wake(self.__msh, [params['Wake'], params['Wake']+'_', params['Fluid']]))
+            self.__pbl.add(dart.Kutta(self.__msh, [params['Te'], params['Wake']+'_', params['Wing'], params['Fluid']]))
+        else:
+            for i in range(len(params['Wakes'])):
+                self.__pbl.add(dart.Wake(self.__msh, [params['Wakes'][i], params['Wakes'][i]+'_', params['Fluid'], params['TeTips'][i]]))
+
+        # Direct (forward) solver creation
+        # NB: more solvers/options are available but we restrict the user's choice to the most efficient ones
+        # initialize the linear (inner) solver
+        if params['LSolver'] == 'PARDISO':
+            linsol = tbox.Pardiso()
+        elif params['LSolver'] == 'MUMPS':
+            linsol = tbox.Mumps()
+        elif params['LSolver'] == 'GMRES':
+            linsol = tbox.Gmres()
+            linsol.setFillFactor(params['G_fill']) if 'G_fill' in params else linsol.setFillFactor(2)
+            linsol.setRestart(params['G_restart']) if 'G_restart' in params else linsol.setRestart(50)
+            linsol.setTolerance(params['G_tol']) if 'G_tol' in params else linsol.setTolerance(1e-5)
+        else:
+            raise RuntimeError('Available linear solvers: PARDISO, MUMPS or GMRES, but ' + params['LSolver'] + ' was given!\n')
+        # initialize the nonlinear (outer) solver
+        self.__sol = dart.Newton(self.__pbl, linsol, rTol=params['Rel_tol'], aTol=params['Abs_tol'], mIt=params['Max_it'], nthrds=nthrd, vrb=verb)
+
+        # Adjoint (reverse) solver creation
+        if task == 'optimization':
+            if params['LSolver'] == 'PARDISO':
+                alinsol = tbox.Pardiso()
+            elif params['LSolver'] == 'MUMPS':
+                alinsol = tbox.Mumps()
+            else:
+                alinsol = tbox.Gmres()
+                alinsol.setFillFactor(params['G_fill']) if 'G_fill' in params else alinsol.setFillFactor(2)
+                alinsol.setRestart(params['G_restart']) if 'G_restart' in params else alinsol.setRestart(50)
+                alinsol.setTolerance(1e-12)
+            self.__adj = dart.Adjoint(self.__sol, self.__mrf, alinsol)
+        else:
+            self.__adj = None
+
+        # Finalize
+        print('*'*31 + 'mphys.DartBuilder' + '*'*31 + '\n')
+
+    def get_mesh_coordinate_subsystem(self):
+        """Return openMDAO component to get the initial surface mesh coordinates
+        """
+        return DartMesh(dim = self.__dim, bnd = self.__bnd)
+
+    def get_coupling_group_subsystem(self):
+        """Return openMDAO group containing the morpher, solver and loads
+        """
+        return DartGroup(qinf = self.__qinf, bnd = self.__bnd, sol = self.__sol, mrf = self.__mrf, adj = self.__adj)
+
+    def get_post_coupling_subsystem(self):
+        """Return openMDAO component that computes the aero coefficients and writes data to disk
+        """
+        return DartCoefficients(sol = self.__sol, adj = self.__adj, wrtr = self.__wrtr, morph = False if self.__mrf is None else True)
+
+    def get_number_of_nodes(self):
+        """Return the number of surface nodes
+        """
+        return len(self.__bnd.nodes)
diff --git a/dart/cases/coyote.py b/dart/cases/coyote.py
index 2d5d214bb5d2ab830ec32cd76e6260e138b7c236..e23edac7696331c98e4ed432539fdc088367e4ca 100644
--- a/dart/cases/coyote.py
+++ b/dart/cases/coyote.py
@@ -24,6 +24,7 @@
 # Valid mesh obtained with gmsh 4.4.1
 
 def getParam():
+    import os.path
     p = {}
     # Specific
     scl = 2 # scaling factor for lifting surface mesh size
@@ -32,7 +33,7 @@ def getParam():
     fums = 0.1 # fuselage mesh size
     fams = 5. # farfield mesh size
     # Input/Output
-    p['File'] = '../models/coyote.geo' # Input file containing the model
+    p['File'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../models/coyote.geo') # Input file containing the model
     p['Pars'] = {'msWing' : wrtems, 'msTail' : trtems,
                  'msFus' : fums, 'msFar' : fams} # Parameters for input file model
     p['Dim'] = 3 # Problem dimension
@@ -75,7 +76,7 @@ def getParam():
 
 def main():
     # Script call
-    import dart.scripts.polar as p
+    import dart.api.internal.polar as p
     polar = p.Polar(getParam())
     polar.run()
     polar.disp()
diff --git a/dart/cases/n0012.py b/dart/cases/n0012.py
index 5427c47a9ac1581187ef7da55dd1e59afafdc7fd..f984557d0a4ac95309b850fb30f77014a371cf3f 100644
--- a/dart/cases/n0012.py
+++ b/dart/cases/n0012.py
@@ -20,9 +20,10 @@
 # Adrien Crovato
 
 def getParam():
+    import os.path
     p = {}
     # Input/Output
-    p['File'] = '../models/n0012.geo' # Input file containing the model
+    p['File'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../models/n0012.geo') # Input file containing the model
     p['Pars'] = {'xLgt' : 5, 'yLgt' : 5, 'msF' : 1.0, 'msTe' : 0.01, 'msLe' : 0.005} # Parameters for input file model
     p['Dim'] = 2 # Problem dimension
     p['Format'] = 'vtk' # Save format (vtk or gmsh)
@@ -54,7 +55,7 @@ def getParam():
 
 def main():
     # Script call
-    import dart.scripts.polar as p
+    import dart.api.internal.polar as p
     polar = p.Polar(getParam())
     polar.run()
     polar.disp()
diff --git a/dart/cases/n0012_3.py b/dart/cases/n0012_3.py
index 6039734e9e1d761152ab73e1ed8012600412f6f4..11338607c8935df66e8a9c486a850780820e2b6f 100644
--- a/dart/cases/n0012_3.py
+++ b/dart/cases/n0012_3.py
@@ -20,11 +20,12 @@
 # Adrien Crovato
 
 def getParam():
+    import os.path
     p = {}
     # Specific
     span = 1. # span length
     # Input/Output
-    p['File'] = '../models/n0012_3.geo' # Input file containing the model
+    p['File'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../models/n0012_3.geo') # Input file containing the model
     p['Pars'] = {'spn' : span, 'lgt' : 3., 'wdt' : 3., 'hgt' : 3., 'msLe' : 0.02, 'msTe' : 0.02, 'msF' : 1.} # Parameters for input file model
     p['Dim'] = 3 # Problem dimension
     p['Format'] = 'vtk' # Save format (vtk or gmsh)
@@ -62,7 +63,7 @@ def getParam():
 
 def main():
     # Script call
-    import dart.scripts.polar as p
+    import dart.api.internal.polar as p
     polar = p.Polar(getParam())
     polar.run()
     polar.disp()
diff --git a/dart/cases/n64A410.py b/dart/cases/n64A410.py
index ba98f07bb25fbc11b2c182c82b29610f727a039f..0ef97fcdde17c9cb70329faa74ece232e62552ac 100644
--- a/dart/cases/n64A410.py
+++ b/dart/cases/n64A410.py
@@ -20,9 +20,10 @@
 # Adrien Crovato
 
 def getParam():
+    import os.path
     p = {}
     # Input/Output
-    p['File'] = '../models/n64A410.geo' # Input file containing the model
+    p['File'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../models/n64A410.geo') # Input file containing the model
     p['Pars'] = {'xLgt' : 5, 'yLgt' : 5, 'msF' : 1.0, 'msTe' : 0.01, 'msLe' : 0.005} # Parameters for input file model
     p['Dim'] = 2 # Problem dimension
     p['Format'] = 'vtk' # Save format (vtk or gmsh)
@@ -56,7 +57,7 @@ def getParam():
 
 def main():
     # Script call
-    import dart.scripts.trim as t
+    import dart.api.internal.trim as t
     trim = t.Trim(getParam())
     trim.run()
     trim.disp()
diff --git a/dart/cases/rae2822.py b/dart/cases/rae2822.py
index 67bb16d39064575a98c5570568371bfbc65e15df..cf515702b802afe84263d3dbe38362153024b9a6 100644
--- a/dart/cases/rae2822.py
+++ b/dart/cases/rae2822.py
@@ -20,9 +20,10 @@
 # Adrien Crovato
 
 def getParam():
+    import os.path
     p = {}
     # Input/Output
-    p['File'] = '../models/rae2822.geo' # Input file containing the model
+    p['File'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../models/rae2822.geo') # Input file containing the model
     p['Pars'] = {'xLgt' : 5, 'yLgt' : 5, 'msF' : 1.0, 'msTe' : 0.01, 'msLe' : 0.005} # Parameters for input file model
     p['Dim'] = 2 # Problem dimension
     p['Format'] = 'vtk' # Save format (vtk or gmsh)
@@ -59,7 +60,7 @@ def getParam():
 
 def main():
     # Script call
-    import dart.scripts.polar as p
+    import dart.api.internal.polar as p
     polar = p.Polar(getParam())
     polar.run()
     polar.disp()
diff --git a/dart/cases/wbht.py b/dart/cases/wbht.py
index 8744e6a92cbca86c05b98369ee26c40df2a65130..4255fffd60dff15f1a0e5c7b763343f48a1a821e 100644
--- a/dart/cases/wbht.py
+++ b/dart/cases/wbht.py
@@ -23,6 +23,7 @@
 # Gmsh might not generate the wing/tail surfaces properly. Results might not be accurate
 
 def getParam():
+    import os.path
     p = {}
     # Specific
     scl = 2 # scaling factor for lifting surface mesh size
@@ -40,7 +41,7 @@ def getParam():
     fucms = 0.01 # fuselage caps mesh size
     fams = 5. # farfield mesh size
     # Input/Output
-    p['File'] = '../models/wbht.geo' # Input file containing the model
+    p['File'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../models/wbht.geo') # Input file containing the model
     p['Pars'] = {'msLeW0' : wrlems, 'msTeW0' : wrtems,
                  'msLeW1' : wtlems, 'msTeW1' : wttems,
                  'msLeW2' : wwlems, 'msTeW2' : wwtems,
@@ -87,7 +88,7 @@ def getParam():
 
 def main():
     # Script call
-    import dart.scripts.trim as t
+    import dart.api.internal.trim as t
     trim = t.Trim(getParam())
     trim.run()
     trim.disp()
diff --git a/dart/cases/wht.py b/dart/cases/wht.py
index 3c68e0990d4236a1490929f7fc6e922d1defe3ca..c5f90f507ab3b8dd2615c4a156c5598ef7fcd127 100644
--- a/dart/cases/wht.py
+++ b/dart/cases/wht.py
@@ -20,6 +20,7 @@
 # Adrien Crovato
 
 def getParam():
+    import os.path
     p = {}
     # Specific
     scl = 2 # scaling factor for lifting surface mesh size
@@ -35,7 +36,7 @@ def getParam():
     tttems = scl*0.0058 # tail tip trailing mesh size
     fams = 5. # farfield mesh size
     # Input/Output
-    p['File'] = '../models/wht.geo' # Input file containing the model
+    p['File'] = os.path.join(os.path.abspath(os.path.dirname(__file__)), '../models/wht.geo') # Input file containing the model
     p['Pars'] = {'msLeW0' : wrlems, 'msTeW0' : wrtems,
                  'msLeW1' : wtlems, 'msTeW1' : wttems,
                  'msLeW2' : wwlems, 'msTeW2' : wwtems,
@@ -78,7 +79,7 @@ def getParam():
 
 def main():
     # Script call
-    import dart.scripts.trim as t
+    import dart.api.internal.trim as t
     trim = t.Trim(getParam())
     trim.run()
     trim.disp()
diff --git a/dart/src/wNewton.cpp b/dart/src/wNewton.cpp
index af6135b7881fcc6b928787fbe1b38703100ab5d8..1d801953e86634863fba0c643f8c2c3c633c975f 100644
--- a/dart/src/wNewton.cpp
+++ b/dart/src/wNewton.cpp
@@ -89,8 +89,8 @@ bool Newton::run()
     // Display current freestream conditions
     std::cout << std::setprecision(2)
               << "- Mach " << pbl->M_inf << ", "
-              << pbl->alpha * 180 / 3.14159 << "° AoA, "
-              << pbl->beta * 180 / 3.14159 << "° AoS"
+              << pbl->alpha * 180 / 3.14159 << " deg AoA, "
+              << pbl->beta * 180 / 3.14159 << " deg AoS"
               << std::endl;
 
     // Initialize solver loop
diff --git a/dart/src/wPicard.cpp b/dart/src/wPicard.cpp
index 03981f908dfeb94ba60f267941feb9a249717050..ea218f7d4c02d3dd4e14c3816776fef51755279b 100644
--- a/dart/src/wPicard.cpp
+++ b/dart/src/wPicard.cpp
@@ -83,8 +83,8 @@ bool Picard::run()
     // Display current freestream conditions
     std::cout << std::setprecision(2)
               << "- Mach " << pbl->M_inf << ", "
-              << pbl->alpha * 180 / 3.14159 << "° AoA, "
-              << pbl->beta * 180 / 3.14159 << "° AoS"
+              << pbl->alpha * 180 / 3.14159 << " deg AoA, "
+              << pbl->beta * 180 / 3.14159 << " deg AoS"
               << std::endl;
 
     // Initialize solver loop