diff --git a/blast/api/mdaAPI.py b/blast/api/mdaAPI.py
index 43fc618cf91f0024418d3b763a26f61f3c9d11db..8a4cae98f25e36d9aea21f5cf2499de41a5abe1e 100644
--- a/blast/api/mdaAPI.py
+++ b/blast/api/mdaAPI.py
@@ -20,9 +20,13 @@
 # Paul Dechamps
 
 import numpy as np
+from fwk.coloring import ccolors
 import openmdao.api as om
 import blast
 import blast.utils as viscUtils
+from pygeo import DVGeometryCST
+from scipy.optimize import least_squares
+import fwk
 
 # Solver openMDAO object
 class BlasterSolver(om.ExplicitComponent):
@@ -49,6 +53,7 @@ class BlasterSolver(om.ExplicitComponent):
         super().__init__()
 
         self.nCpt = 0
+        self.tms = fwk.Timers()
 
         if iSolverName == 'DART':
             from blast.interfaces.dart.DartInterface import DartInterface as interface
@@ -75,10 +80,11 @@ class BlasterSolver(om.ExplicitComponent):
         print('object created')
     
     def setup(self):
-        self.add_input('aoa', val=0.0)
-        self.add_input('x_aero', val=np.zeros(self.isol.iobj['bnd'].nodes.size() * self.isol.getnDim()))
-        self.add_output('cl', val=0.0)
-        self.add_output('cd', val=0.0)
+        self.add_input('aoa', val=0.0, desc='Flow angle of attack')
+        self.add_input('x_aero', val=np.zeros(self.isol.iobj['bnd'].nodes.size() * self.isol.getnDim()), desc='Surface mesh coordinates')
+        self.add_input('visc_mode', val=True) # Inviscid or viscous mode
+        self.add_output('cl', val=0.0, desc='Lift coefficient')
+        self.add_output('cd', val=0.0, desc='Drag coefficient')
     
     def setup_partials(self):
         self.declare_partials('cl', 'aoa', method='exact')
@@ -90,63 +96,177 @@ class BlasterSolver(om.ExplicitComponent):
     def compute_partials(self, inputs, partials, discrete_inputs=None):
         """Compute partial derivatives using the adjoint solver
         """
-        # Run adjoint solver
+        nDim = self.isol.getnDim()
+        self.tms['adj'].start()
+
+        print(ccolors.ANSI_BLUE, 'Computing derivatives in mode '+('viscous' if inputs['visc_mode'][0] else 'inviscid'), ccolors.ANSI_RESET)
+        
+        # Run inviscid adjoint solver
         self.isol.iobj['adj'].run()
-        self.adjointSolver.reset()
-        self.adjointSolver.run()
+
+        dClaoa = 0.
+        dCdaoa = 0.
+        dClx = np.zeros(self.isol.iobj['bnd'].nodes.size() * nDim)
+        dCdx = np.zeros(self.isol.iobj['bnd'].nodes.size() * nDim)
+
+        if inputs['visc_mode'][0]:
+            self.adjointSolver.reset()
+            self.adjointSolver.run()
+            dClaoa = self.adjointSolver.tdCl_AoA
+            dCdaoa = self.adjointSolver.tdCd_AoA
+
+            # Mesh derivatives
+            for inod, n in enumerate(self.isol.iobj['bnd'].nodes):
+                for idim in range(nDim):
+                    dClx[inod*nDim + idim] = self.adjointSolver.tdCl_x[n.row][idim]
+                    dCdx[inod*nDim + idim] = self.adjointSolver.tdCd_x[n.row][idim]
+        elif not inputs['visc_mode'][0]:
+            dClaoa = self.isol.iobj['adj'].dClAoa
+            dCdaoa = self.isol.iobj['adj'].dCdAoa
+
+            # Mesh derivatives
+            for inod, n in enumerate(self.isol.iobj['bnd'].nodes):
+                for idim in range(nDim):
+                    dClx[inod*nDim + idim] = self.isol.iobj['adj'].dClMsh[n.row][idim]
+                    dCdx[inod*nDim + idim] = self.isol.iobj['adj'].dCdMsh[n.row][idim]
+        else:
+            raise ValueError('omBlasterSolver::compute_partials - Invalid mode', inputs['visc_mode'][0])
 
         # AoA derivatives
-        partials['cl', 'aoa'] = self.adjointSolver.tdCl_AoA
-        partials['cd', 'aoa'] = self.adjointSolver.tdCd_AoA
+        partials['cl', 'aoa'] = dClaoa * np.pi/180 # /°
+        partials['cd', 'aoa'] = dCdaoa * np.pi/180 # /°
+        partials['cl', 'x_aero'][0] = dClx
+        partials['cd', 'x_aero'][0] = dCdx
+        self.tms['adj'].stop()
 
-        # Mesh derivative
-        nDim = self.isol.getnDim()
-        # Get mesh matrix
-        for inod, n in enumerate(self.isol.iobj['bnd'].nodes):
-            for idim in range(nDim):
-                partials['cl', 'x_aero'][0][inod*nDim + idim] = self.adjointSolver.tdCl_x[n.row][idim]
-                partials['cd', 'x_aero'][0][inod*nDim + idim] = self.adjointSolver.tdCd_x[n.row][idim]
-    
-    def compute(self, inputs, outputs):
+    def compute(self, inputs, outputs, discrete_inputs=None):
         """Update the mesh, aoa and run the solver
         """
+        self.tms['update'].start()
         nDim = self.isol.getnDim()
+
         # Modify surface and deform mesh
         self.isol.iobj['mrf'].savePos()
         for inod, n in enumerate(self.isol.iobj['bnd'].nodes):
             for idim in range(nDim):
                 n.pos[idim] = inputs['x_aero'][inod*nDim + idim]
         self.isol.iobj['mrf'].deform()
-
-        # Update viscous mesh
         self.isol.updateMesh()
 
         # Update angle of attack
         aoa = inputs['aoa'][0]
         self.isol.setAoA(aoa*np.pi/180) # aoa is given in deg
-
-        # Reset coupler
-        self.coupler.reset()
+        self.tms['update'].stop()
 
         # Run forward problem
-        self.coupler.run(write=False)
-        outputs['cl'] = self.isol.getCl()
-        outputs['cd'] = self.isol.getCd() + self.vsol.Cdf
+        self.tms['fwd'].start()
+        print(ccolors.ANSI_BLUE, 'Computing flow for AoA=', round(aoa,2), 'in mode '+('viscous' if inputs['visc_mode'][0] else 'inviscid'), ccolors.ANSI_RESET)
+
+        if inputs['visc_mode'][0]:
+            self.coupler.reset()
+            self.coupler.run(write=False)
+        elif not inputs['visc_mode'][0]:
+            self.isol.reset()
+            self.isol.run()
+        else:
+            raise ValueError('omBlasterSolver::compute - Invalid mode', inputs['visc_mode'][0])
+        self.tms['fwd'].stop()
+
+        # write solution
+        self.__write()
 
-        # Write results
-        self.isol.writeCp(sfx='_'+str(self.nCpt))
-        viscUtils.getSolution(self.isol.sec, write=True, toW=['x', 'y', 'cf'], sfx='_'+str(self.nCpt))
+        outputs['cl'] = self.isol.getCl()
+        outputs['cd'] = self.isol.getCd()
+        if inputs['visc_mode'][0]:
+            outputs['cd'] += self.vsol.Cdf # Add viscous contribution
         self.nCpt += 1
 
+    def __write(self):
+        saveAll = False
+        if self.nCpt == 0:
+            sfx = '_0'
+        elif saveAll:
+            sfx = '_'+str(self.nCpt)
+        else:
+            sfx = '_opt'
+        viscUtils.getSolution(self.isol.sec, write=True, toW=['x', 'y', 'cf'], sfx=sfx) # Boundary layer
+        self.isol.writeCp(sfx=sfx) # Cp
+        self.isol.iobj['wrt'].save(self.isol.iobj['pbl'].msh.name + sfx) # Mesh
+        self.isol.save(sfx) # Solution
+        np.savetxt('shape'+sfx+'.txt', self.isol.vBnd[0][0].nodesCoord) # Shape
+        np.savetxt('loads'+sfx+'.txt', [self.isol.getCl(), self.isol.getCd() + self.vsol.Cdf, self.isol.getCm()]) # Loads
+
     def getBoundaryMesh(self):
         """Initial surface mesh coordinates
         """
         return BlasterMesh(nDim=self.isol.getnDim(), bnd=self.isol.iobj['bnd'])
     
-    def getBoundaryGeometry(self):
-        """Airfoil geometry
+    def getBoundaryNACAGeometry(self):
+        """Airfoil naca-4digits geometry
+        """
+        return BlasterNACAGeometry(nDim=self.isol.getnDim(), nNodes=self.isol.iobj['bnd'].nodes.size(), nodeRows=self.isol.vBnd[0][0].connectListNodes)
+    
+    def getBoundaryFFDGeometry(self):
+        """Airfoil FFD geometry
         """
-        return BlasterGeometry(nDim=self.isol.getnDim(), nNodes=self.isol.iobj['bnd'].nodes.size(), nodeRows=self.isol.vBnd[0][0].connectListNodes)
+        with open('surface_DVGeo.dat', 'w') as f:
+            for i, node_coord in enumerate(self.isol.vBnd[0][0].nodesCoord):
+                f.write(f"{node_coord[0]} {node_coord[1]}\n")
+        
+        _dvGeo = DVGeometryCST('surface_DVGeo.dat', numCST=4)
+        _dvGeo.addDV("upper_shape", dvType="upper", lowerBound=-0.1, upperBound=0.5)
+        _dvGeo.addDV("lower_shape", dvType="lower", lowerBound=-0.5, upperBound=0.1)
+        _dvGeo.addDV("class_shape_n1", dvType="N1")
+        _dvGeo.addDV("class_shape_n2", dvType="N2")
+        _dvGeo.addDV("chord", dvType="chord")
+        points = np.array(np.loadtxt('surface_DVGeo.dat'))
+        points = np.hstack((points, np.zeros((points.shape[0], 1))))  # add 0s for z coordinates (unused)
+        _dvGeo.addPointSet(points, "airfoil_coords")
+        _dvGeo.update("airfoil_coords")
+        return BlasterFFDGeometry(nDim=self.isol.getnDim(), nNodes=self.isol.iobj['bnd'].nodes.size(), nodeRows=self.isol.vBnd[0][0].connectListNodes, dvGeo=_dvGeo)
+
+# Updater to activate viscous mode
+class BlasterModeUpdater(om.ExplicitComponent):
+    def initialize(self):
+        self.options.declare('tolerance', default=1e-6, types=float, desc='Tolerance value to trigger visc_mode')
+        self.options.declare('force_mode', default=None, types=str, desc='Force ''visc'' mode or ''inviscid'' mode')
+
+    def setup(self):
+        self.add_input('current_objective', val=1.0)
+        self.add_output('visc_mode', val=0.0)
+        self._previous_objective = 0.0
+        self._lock = False
+
+    def compute(self, inputs, outputs):
+
+        # Forced mode case
+        if self.options['force_mode'] is not None:
+            print('coucou', self.options['force_mode'])
+            if self.options['force_mode'] == 'visc':
+                print('passe dans visc')
+                outputs['visc_mode'] = True
+            elif self.options['force_mode'] == 'inv':
+                print('passe dans inv')
+                outputs['visc_mode'] = False
+            else:
+                raise RuntimeError('Invalid force_mode', self.options['force_mode'])
+            print('output visc1', outputs['visc_mode'])
+            return
+
+        tolerance = self.options['tolerance']
+        relDiff = abs((inputs['current_objective']-self._previous_objective)/inputs['current_objective'])
+        print('curr', inputs['current_objective'][0], 'prev', self._previous_objective)
+        print('relDiff', relDiff, 'tolerance', tolerance)
+        if relDiff <= tolerance and relDiff != 0. or self._lock:
+            outputs['visc_mode'] = True
+            self._lock = True
+        else:
+            outputs['visc_mode'] = False
+        print('output visc', outputs['visc_mode'])
+        self._previous_objective = inputs['current_objective'][0]
+    
+    def compute_partials(self, inputs, partials):
+        pass
 
 # Surface mesh
 class BlasterMesh(om.IndepVarComp):
@@ -175,7 +295,7 @@ class BlasterMesh(om.IndepVarComp):
                 x_aero0[inod*nDim + idim] = n.pos[idim]
         self.add_output('x_aero0', val=x_aero0)
 
-class BlasterGeometry(om.ExplicitComponent):
+class BlasterNACAGeometry(om.ExplicitComponent):
     """Airfoil parametrization as a NACA 4-digit airfoil.
     Uses the NACA 4-digit airfoil parametrization to generate an airfoil from a given x
     distribution.
@@ -292,3 +412,103 @@ class BlasterGeometry(om.ExplicitComponent):
                 yB[i] = YBar[i] + 0.5 * T[i] * np.cos(np.arctan(dYBar_dx[i]))
                 xB[i] = x[i]    - 0.5 * T[i] * np.sin(np.arctan(dYBar_dx[i]))    
         return (xB, yB)
+
+class BlasterFFDGeometry(om.ExplicitComponent):
+    """Airfoil parametrization as a FFD airfoil.
+    Uses the FFD airfoil parametrization to generate an airfoil from a given x
+    distribution.
+    """
+
+    def initialize(self):
+        self.options.declare('nDim', desc='Number of dimensions')
+        self.options.declare('nNodes', desc='Number of surface nodes')
+        self.options.declare('nodeRows', desc='Nodal index of the surface nodes sorted in seilig format')
+        self.options.declare('dvGeo', desc='Parametrization of the geometry', recordable=False)
+
+    def setup(self):
+        nNodes = self.options['nNodes']
+        nDim = self.options['nDim']
+
+        self.add_input('x_aero0', np.zeros(nNodes*nDim)) # Initial mesh
+        self.add_input('upper_shape', [0.17553542, 0.14723497, 0.14531762, 0.13826955])
+        self.add_input('lower_shape', [-0.16935506, -0.15405552, -0.13912672, -0.14168947])
+        self.add_input('class_shape_n1', 0.5)
+        self.add_input('class_shape_n2', 1.0)
+        self.add_output('x_aero_out', val = np.zeros(nNodes*nDim))
+        self.add_output('thickness', val = .12)
+        self.add_output('LEradius', val = 0.02)
+
+    def setup_partials(self):
+        self.declare_partials('x_aero_out', 'upper_shape', method='fd')
+        self.declare_partials('x_aero_out', 'lower_shape', method='fd')
+        #self.declare_partials('x_aero_out', 'class_shape_n1', method='fd')
+        #self.declare_partials('x_aero_out', 'class_shape_n2', method='fd')
+        self.declare_partials('thickness', 'upper_shape', method='fd')
+        self.declare_partials('thickness', 'lower_shape', method='fd')
+        # self.declare_partials('thickness', 'class_shape_n1', method='fd')
+        # self.declare_partials('thickness', 'class_shape_n2', method='fd')
+        self.declare_partials('LEradius', 'upper_shape', method='fd')
+        self.declare_partials('LEradius', 'lower_shape', method='fd')
+        # self.declare_partials('LEradius', 'class_shape_n1', method='fd')
+        # self.declare_partials('LEradius', 'class_shape_n2', method='fd')
+
+    def compute(self, inputs, outputs):
+        nDim = self.options['nDim']
+
+        self.options['dvGeo'].setDesignVars({"upper_shape": inputs['upper_shape'],
+                             "lower_shape": inputs['lower_shape'],
+                             "class_shape_n1": inputs['class_shape_n1'],
+                             "class_shape_n2": inputs['class_shape_n2']})
+        points1 = self.options['dvGeo'].update("airfoil_coords")
+
+        # Compute thickness at 1/4 chord
+        upper_side = points1[:len(points1)//2]
+        lower_side = points1[len(points1)//2:]
+        arg_up = np.argmin(np.abs(upper_side[:, 0] - 0.38))
+        arg_low = np.argmin(np.abs(lower_side[:, 0] - 0.38))
+        maxThickness = upper_side[arg_up, 1] - lower_side[arg_low, 1]
+
+        # Extract leading edge points (assuming points are sorted by x)
+        le_points = points1[np.abs(points1[:, 0]) < 0.01]  # Adjust threshold as needed
+
+        # Separate upper and lower surface points
+        upper_surface = le_points[le_points[:, 1] >= 0]
+        lower_surface = le_points[le_points[:, 1] < 0]
+
+        # Combine upper and lower surface points for circle fitting
+        le_points_combined = np.vstack((upper_surface, lower_surface))
+
+        # Fit circle to leading edge points
+        center, radius = self.fit_circle(le_points_combined[:, 0], le_points_combined[:, 1])
+
+        # print('Upper shape', inputs['upper_shape'])
+        # print('Lower shape', inputs['lower_shape'])
+        # print('Max thickness', round(maxThickness, 4))
+        # print('Center:', center)
+        # print('Radius:', radius)
+
+        # Go back to normal mesh
+        x_aero_out = np.zeros(len(inputs['x_aero0']))
+        for i, val in enumerate(self.options['nodeRows']):
+            for idim in range(nDim):
+                x_aero_out[val*nDim+idim] = points1[i, idim]
+        outputs['x_aero_out'] = x_aero_out
+        outputs['thickness'] = maxThickness
+        outputs['LEradius'] = radius
+
+    def fit_circle(self, x, y):
+        def calc_R(xc, yc):
+            return np.sqrt((x - xc)**2 + (y - yc)**2)
+
+        def f_2(c):
+            Ri = calc_R(*c)
+            return Ri - Ri.mean()
+
+        x_m = np.mean(x)
+        y_m = np.mean(y)
+        center_estimate = x_m, y_m
+        result = least_squares(f_2, center_estimate)
+        center = result.x
+        Ri = calc_R(*center)
+        R = Ri.mean()
+        return center, R
diff --git a/blast/mdao/naca0012_mdao.py b/blast/mdao/opti_NACA.py
similarity index 99%
rename from blast/mdao/naca0012_mdao.py
rename to blast/mdao/opti_NACA.py
index ec34a6ead36e2ad5799ead7bb2584b3b4f2385ba..4c9daebdf11d35b78ff94d6156a86fd4681bae72 100644
--- a/blast/mdao/naca0012_mdao.py
+++ b/blast/mdao/opti_NACA.py
@@ -85,7 +85,7 @@ class groupMDA(om.Group):
         # Design variables box
         self.add_subsystem('dvs', om.IndepVarComp(), promotes=['*'])
         self.add_subsystem('mesh', blaster.getBoundaryMesh())
-        self.add_subsystem('geometry', blaster.getBoundaryGeometry())
+        self.add_subsystem('geometry', blaster.getBoundaryNACAGeometry())
         self.add_subsystem('solver', blaster)
 
         # Add objective function
diff --git a/blast/mdao/opti_subsonic.py b/blast/mdao/opti_subsonic.py
new file mode 100644
index 0000000000000000000000000000000000000000..bbe44968afb258523e5be46855d5303f40172497
--- /dev/null
+++ b/blast/mdao/opti_subsonic.py
@@ -0,0 +1,215 @@
+import blast.utils as viscUtils
+import numpy as np
+
+from fwk.wutils import parseargs
+import fwk
+from fwk.testing import *
+from fwk.coloring import ccolors
+
+from blast.api.mdaAPI import BlasterSolver
+
+import openmdao.api as om
+
+import math
+
+def cfgInviscid(nthrds, verb):
+    import os.path
+    # Parameters
+    return {
+    # Options
+    'scenario' : 'aerodynamic',
+    'task' : 'optimization',
+    'Threads' : nthrds, # number of threads
+    'Verb' : verb, # verbosity
+    # Model (geometry or mesh)
+    'File' : os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + '/models/dart/n0012.geo', # Input file containing the model
+    'Pars' : {'xLgt' : 50, 'yLgt' : 50, 'msF' : 25, 'msTe' : 0.0075, 'msLe' : 0.001}, # parameters for input file model
+    'Dim' : 2, # problem dimension
+    'Format' : 'gmsh', # save format (vtk or gmsh)
+    # Markers
+    'Fluid' : 'field', # name of physical group containing the fluid
+    'Farfield' : ['upstream', 'farfield', 'downstream'], # LIST of names of physical groups containing the farfield boundaries (upstream/downstream should be first/last element)
+    'Wing' : 'airfoil', # LIST of names of physical groups containing the lifting surface boundary
+    'Wake' : 'wake', # LIST of names of physical group containing the wake
+    'WakeTip' : 'wakeTip', # LIST of names of physical group containing the edge of the wake
+    'Te' : 'te', # LIST of names of physical group containing the trailing edge
+    'dbc' : False,
+    'Upstream' : 'upstream',
+    # Freestream
+    'M_inf' : 0.1, # freestream Mach number
+    'AoA' : 1., # freestream angle of attack
+    # Geometry
+    'S_ref' : 1., # reference surface length
+    'c_ref' : 1., # reference chord length
+    'x_ref' : .25, # reference point for moment computation (x)
+    'y_ref' : 0.0, # reference point for moment computation (y)
+    'z_ref' : 0.0, # reference point for moment computation (z)
+    # Numerical
+    'LSolver' : 'SparseLu', # inner solver (Pardiso, MUMPS or GMRES)
+    'G_fill' : 2, # fill-in factor for GMRES preconditioner
+    'G_tol' : 1e-5, # tolerance for GMRES
+    'G_restart' : 50, # restart for GMRES
+    'Rel_tol' : 1e-6, # relative tolerance on solver residual
+    'Abs_tol' : 1e-8, # absolute tolerance on solver residual
+    'Max_it' : 20 # solver maximum number of iterations
+    }
+
+def cfgBlast(verb):
+    return {
+        'Re' : 1e7,                 # Freestream Reynolds number
+        'Minf' : 0.1,               # Freestream Mach number (used for the computation of the time step only)
+        'CFL0' : 1.,                 # Inital CFL number of the calculation
+        'Verb': verb,               # Verbosity level of the solver
+        'couplIter': 50,            # Maximum number of iterations
+        'couplTol' : 1e-4,          # Tolerance of the VII methodology
+        'iterPrint': 5,             # int, number of iterations between outputs
+        'resetInv' : True,          # bool, flag to reset the inviscid calculation at every iteration.
+        'sections' : [0],           # List of sections for boundary layer calculation
+        'xtrF' : [None, None],       # Forced transition location
+        'interpolator' : 'Matching',# Interpolator for the coupling
+        'nSections' : 1,            # Number of sections in the domain
+        'span' : 1.0                # Wing span
+    }
+
+class groupMDA(om.Group):
+    def setup(self):
+        args = parseargs()
+        icfg = cfgInviscid(args.k, args.verb)
+        vcfg = cfgBlast(args.verb)
+
+        #cycle = self.add_subsystem('cycle', om.Group())
+
+        # Add the MDAO components
+        blaster = BlasterSolver(vcfg, icfg)
+
+        # Design variables box
+        self.add_subsystem('dvs', om.IndepVarComp(), promotes=['*'])
+        self.add_subsystem('mesh', blaster.getBoundaryMesh())
+        self.add_subsystem('geometry', blaster.getBoundaryFFDGeometry())
+        # Add an intermediate variable for the constraint (such that upper_shape[0] = lower_shape[0])
+        self.add_subsystem('constraint_comp', om.ExecComp('constraint_var = lower_shape[0] + upper_shape[0]',
+                                                          lower_shape=np.zeros(4), upper_shape=np.zeros(4)), promotes=['*'])
+        self.add_subsystem('solver', blaster)
+
+        # Add objective function
+        self.add_subsystem('obj_cmp', om.ExecComp('obj = cd'), promotes=['obj', 'cd'])
+
+        # Connect meshes
+        self.connect('mesh.x_aero0', 'geometry.x_aero0')
+        self.connect('geometry.x_aero_out', 'solver.x_aero')
+
+        #nonlinear_solver = om.NewtonSolver(solve_subsystems=False)
+        self.linear_solver = om.DirectSolver()
+    
+    def configure(self):
+        # Design vars
+        self.dvs.add_output('aoa', val=1.0)
+        self.connect('aoa', 'solver.aoa')
+        #self.add_design_var('aoa', lower=0.0, upper=3.0)
+
+        self.dvs.add_output('upper_shape', val=[0.17553542, 0.14723497, 0.14531762, 0.13826955])
+        self.connect('upper_shape', 'geometry.upper_shape')
+        self.add_design_var('upper_shape', lower=-0.1, upper=0.4)
+
+        self.dvs.add_output('lower_shape', val=[-0.17553542, -0.15405552, -0.13912672, -0.14168947])
+        self.connect('lower_shape', 'geometry.lower_shape')
+        self.add_design_var('lower_shape', lower=-0.5, upper=0.2)
+
+        self.dvs.add_output('class_shape_n1', val=0.5)
+        self.connect('class_shape_n1', 'geometry.class_shape_n1')
+        # self.add_design_var('class_shape_n1', lower=-0.5, upper=0.1)
+
+        self.dvs.add_output('class_shape_n2', val=1.)
+        self.connect('class_shape_n2', 'geometry.class_shape_n2')
+        # self.add_design_var('class_shape_n2', lower=-0.5, upper=0.1)
+
+        # Constraints
+        # Cl constraint
+        self.add_constraint('solver.cl', equals=0.6)
+        self.add_constraint('geometry.thickness', equals=.09)
+        self.add_constraint('geometry.LEradius', equals=0.0224)
+        self.add_constraint('constraint_var', equals=0., linear=True)
+
+        #self.connect('solver.cl', 'cl')
+        self.connect('solver.cd', 'cd')
+        self.add_objective('obj')
+
+def main():
+    # Timer.
+    tms = fwk.Timers()
+    tms['total'].start()
+
+    prob = om.Problem()
+    prob.model = groupMDA()
+
+    prob.driver = om.ScipyOptimizeDriver()
+    prob.driver.options['optimizer'] = 'SLSQP'
+    prob.driver.options['tol'] = 1e-3
+
+    recorder = om.SqliteRecorder('reports/BlasterSolver_file.sql')
+
+    prob.driver.add_recorder(recorder)
+    prob.driver.recording_options['record_objectives'] = True
+    prob.driver.recording_options['record_desvars'] = True
+    prob.driver.recording_options['includes'] = ['aoa', 'solver.cl', 'solver.cd', 'mesh.x_aero0', 'geometry.x_aero_out']
+
+    prob.setup()
+    #prob.check_partials(includes=['cl', 'aoa'], compact_print=True)
+    prob.run_driver()
+
+    print(prob.model.solver.tms)
+
+    # Instantiate your CaseReader
+    cr = om.CaseReader(prob.get_reports_dir() / "../BlasterSolver_file.sql")
+    # Isolate "problem" as your source
+    driver_cases = cr.list_cases('driver', out_stream=None)
+
+    # Get evolution of f and x
+    aoa_evol = []
+    cl_evol = []
+    cd_evol = []
+    shape = []
+    shape0 = []
+
+    for c in driver_cases:
+        case = cr.get_case(c)
+        aoa_evol.append(case.get_val('aoa')[0]*180/np.pi)
+        cl_evol.append(case.get_val('solver.cl')[0])
+        cd_evol.append(case.get_val('solver.cd')[0])
+        shape0.append(case.get_val('mesh.x_aero0'))
+        shape.append(case.get_val('geometry.x_aero_out'))
+
+    print('success')
+    print('aoa:', prob['aoa']*np.pi/180)
+    print('cl:', prob['solver.cl'])
+    print('cd:', prob['solver.cd'])
+    print('obj:', prob['obj'])
+
+    # Recover selig format
+    shape0_selig = np.array(shape0[0])
+    shape1_selig = np.array(shape[-1])
+    for i, val in enumerate(prob.model.solver.isol.vBnd[0][0].connectListNodes):
+        for j in range(prob.model.solver.isol.getnDim()):
+            shape0_selig[i*prob.model.solver.isol.getnDim()+j] = shape0[0][val*prob.model.solver.isol.getnDim()+j]
+            shape1_selig[i*prob.model.solver.isol.getnDim()+j] = shape[-1][val*prob.model.solver.isol.getnDim()+j]
+    from matplotlib import pyplot as plt
+
+    plt.plot(shape0_selig[0::2], shape0_selig[1::2], '--', color='grey', label='Initial shape - cl = {:.2f} - cd = {:.5f}'.format(cl_evol[0], cd_evol[0]))
+    plt.plot(shape1_selig[0::2], shape1_selig[1::2], '-', color='blue', label='Final shape - cl = {:.2f} - cd = {:.5f}'.format(cl_evol[-1], cd_evol[-1]))
+    plt.legend()
+    plt.axis('equal')
+    plt.show()
+    quit()
+
+
+
+    plt.plot(shape0[0][::2], shape0[0][1::2], 'x', color='grey')
+    for xx in shape:
+        plt.plot(xx[::2], xx[1::2], 'x', color='blue')
+    plt.axis('equal')
+    plt.show()
+    quit()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/blast/mdao/opti_transonic.py b/blast/mdao/opti_transonic.py
new file mode 100644
index 0000000000000000000000000000000000000000..5cbdeca570d336aa4ecd0760a0f99bf4b2951b1f
--- /dev/null
+++ b/blast/mdao/opti_transonic.py
@@ -0,0 +1,226 @@
+import blast.utils as viscUtils
+import numpy as np
+
+from fwk.wutils import parseargs
+import fwk
+from fwk.testing import *
+from fwk.coloring import ccolors
+
+from blast.api.mdaAPI import BlasterSolver
+from blast.api.mdaAPI import BlasterModeUpdater
+
+import openmdao.api as om
+
+import math
+
+def cfgInviscid(nthrds, verb):
+    import os.path
+    # Parameters
+    return {
+    # Options
+    'scenario' : 'aerodynamic',
+    'task' : 'optimization',
+    'Threads' : nthrds, # number of threads
+    'Verb' : verb, # verbosity
+    # Model (geometry or mesh)
+    'File' : os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + '/models/dart/n0012.geo', # Input file containing the model
+    'Pars' : {'xLgt' : 50, 'yLgt' : 50, 'msF' : 20, 'msTe' : 0.01, 'msLe' : 0.002}, # parameters for input file model
+    'Dim' : 2, # problem dimension
+    'Format' : 'gmsh', # save format (vtk or gmsh)
+    # Markers
+    'Fluid' : 'field', # name of physical group containing the fluid
+    'Farfield' : ['upstream', 'farfield', 'downstream'], # LIST of names of physical groups containing the farfield boundaries (upstream/downstream should be first/last element)
+    'Wing' : 'airfoil', # LIST of names of physical groups containing the lifting surface boundary
+    'Wake' : 'wake', # LIST of names of physical group containing the wake
+    'WakeTip' : 'wakeTip', # LIST of names of physical group containing the edge of the wake
+    'Te' : 'te', # LIST of names of physical group containing the trailing edge
+    'dbc' : False,
+    'Upstream' : 'upstream',
+    # Freestream
+    'M_inf' : 0.1, # freestream Mach number
+    'AoA' : 0., # freestream angle of attack
+    # Geometry
+    'S_ref' : 1., # reference surface length
+    'c_ref' : 1., # reference chord length
+    'x_ref' : .25, # reference point for moment computation (x)
+    'y_ref' : 0.0, # reference point for moment computation (y)
+    'z_ref' : 0.0, # reference point for moment computation (z)
+    # Numerical
+    'LSolver' : 'SparseLu', # inner solver (Pardiso, MUMPS or GMRES)
+    'G_fill' : 2, # fill-in factor for GMRES preconditioner
+    'G_tol' : 1e-5, # tolerance for GMRES
+    'G_restart' : 50, # restart for GMRES
+    'Rel_tol' : 1e-6, # relative tolerance on solver residual
+    'Abs_tol' : 1e-8, # absolute tolerance on solver residual
+    'Max_it' : 20 # solver maximum number of iterations
+    }
+
+def cfgBlast(verb):
+    return {
+        'Re' : 1e8,                 # Freestream Reynolds number
+        'Minf' : 0.1,               # Freestream Mach number (used for the computation of the time step only)
+        'CFL0' : 1.,                # Inital CFL number of the calculation
+        'Verb': verb,               # Verbosity level of the solver
+        'couplIter': 75,            # Maximum number of iterations
+        'couplTol' : 1e-5,          # Tolerance of the VII methodology
+        'iterPrint': 5,             # int, number of iterations between outputs
+        'resetInv' : True,          # bool, flag to reset the inviscid calculation at every iteration.
+        'sections' : [0],           # List of sections for boundary layer calculation
+        'xtrF' : [0.05, 0.05],       # Forced transition location
+        'interpolator' : 'Matching',# Interpolator for the coupling
+        'nSections' : 1,            # Number of sections in the domain
+        'span' : 1.0                # Wing span
+    }
+
+class groupMDA(om.Group):
+    def setup(self):
+        args = parseargs()
+        icfg = cfgInviscid(args.k, args.verb)
+        vcfg = cfgBlast(args.verb)
+        gr = 1.055
+        if gr == 1.0:
+            icfg['Pars']['msF'] = icfg['Pars']['msLe']
+        else:
+            n = np.log10(1-(1-gr)*icfg['Pars']['xLgt']/icfg['Pars']['msLe'])/np.log10(gr)
+            icfg['Pars']['msF'] = icfg['Pars']['msLe']*gr**(n-1)
+
+        # Add the MDAO components
+        blaster = BlasterSolver(vcfg, icfg)
+
+        self.add_subsystem('dvs', om.IndepVarComp(), promotes=['*']) # Design variables box
+        self.add_subsystem('mod', BlasterModeUpdater(tolerance=10., force_mode='visc')) # Mode updater
+        self.add_subsystem('mesh', blaster.getBoundaryMesh()) # Initial mesh
+        self.add_subsystem('geometry', blaster.getBoundaryFFDGeometry()) # Geometry computation
+        # Add an intermediate variable for the constraint (such that upper_shape[0] = lower_shape[0])
+        self.add_subsystem('constraint_comp', om.ExecComp('constraint_var = lower_shape[0] + upper_shape[0]',
+                                                          lower_shape=np.zeros(4), upper_shape=np.zeros(4)), promotes=['*'])
+        self.add_subsystem('solver', blaster) # Blaster solver
+        self.add_subsystem('obj_cmp', om.ExecComp('obj = cd'), promotes=['obj', 'cd']) # Objective function
+
+        # Connect meshes
+        self.connect('mesh.x_aero0', 'geometry.x_aero0')
+        self.connect('geometry.x_aero_out', 'solver.x_aero')
+        # Connect mode updater
+        self.connect('mod.visc_mode', 'solver.visc_mode')
+
+        #nonlinear_solver = om.NewtonSolver(solve_subsystems=False)
+        self.linear_solver = om.DirectSolver()
+    
+    def configure(self):
+        # Design vars
+        self.dvs.add_output('aoa', val=0.)
+        self.connect('aoa', 'solver.aoa')
+        self.add_design_var('aoa', lower=-5.0, upper=5.0)
+
+        self.dvs.add_output('upper_shape', val=[0.17553542, 0.14723497, 0.14531762, 0.13826955]) # NACA 0012
+        #self.dvs.add_output('upper_shape', val=[0.25367349, 0.24936591, 0.26996167, 0.28365274]) # NACA4412
+        self.connect('upper_shape', 'geometry.upper_shape')
+        self.add_design_var('upper_shape', lower=-0.1, upper=0.22)
+
+        self.dvs.add_output('lower_shape', val=[-0.16935506, -0.15405552, -0.13912672, -0.14168947]) # NACA 0012
+        #self.dvs.add_output('lower_shape', val=[-0.1330126,  -0.01246078, -0.04065711, -0.0123221]) # NACA 4412
+        self.connect('lower_shape', 'geometry.lower_shape')
+        self.add_design_var('lower_shape', lower=-0.2, upper=0.2)
+
+        self.dvs.add_output('class_shape_n1', val=0.5)
+        self.connect('class_shape_n1', 'geometry.class_shape_n1')
+        # self.add_design_var('class_shape_n1', lower=-0.5, upper=0.1)
+
+        self.dvs.add_output('class_shape_n2', val=1.)
+        self.connect('class_shape_n2', 'geometry.class_shape_n2')
+        # self.add_design_var('class_shape_n2', lower=-0.5, upper=0.1)
+
+        #self.connect('obj', 'mod.current_objective')
+
+        # Constraints
+        # Cl constraint
+        self.add_constraint('solver.cl', equals=0.7)
+        self.add_constraint('geometry.thickness', lower=0.12)
+        self.add_constraint('geometry.LEradius', equals=0.0224)
+        self.add_constraint('constraint_var', equals=0., linear=True)
+
+        #self.connect('solver.cl', 'cl')
+        self.connect('solver.cd', 'cd')
+        self.add_objective('obj')
+
+def main():
+    # Timer.
+    tms = fwk.Timers()
+    tms['total'].start()
+
+    prob = om.Problem()
+    prob.model = groupMDA()
+
+    prob.driver = om.ScipyOptimizeDriver()
+    prob.driver.options['optimizer'] = 'SLSQP'
+    prob.driver.options['tol'] = 1e-3
+
+    recorder = om.SqliteRecorder('reports/BlasterSolver_file.sql')
+
+    prob.driver.add_recorder(recorder)
+    prob.driver.recording_options['record_objectives'] = True
+    prob.driver.recording_options['record_desvars'] = True
+    prob.driver.recording_options['includes'] = ['aoa', 'solver.cl', 'solver.cd', 'mesh.x_aero0', 'geometry.x_aero_out']
+
+    prob.setup()
+    print('check')
+    #prob.check_partials(includes=['cl', 'cd'], compact_print=True)
+    print('done')
+    prob.run_driver()
+
+    print(prob.model.solver.tms)
+
+    # Instantiate your CaseReader
+    cr = om.CaseReader(prob.get_reports_dir() / "../BlasterSolver_file.sql")
+    # Isolate "problem" as your source
+    driver_cases = cr.list_cases('driver', out_stream=None)
+
+    # Get evolution of f and x
+    aoa_evol = []
+    cl_evol = []
+    cd_evol = []
+    shape = []
+    shape0 = []
+
+    for c in driver_cases:
+        case = cr.get_case(c)
+        aoa_evol.append(case.get_val('aoa')[0])
+        cl_evol.append(case.get_val('solver.cl')[0])
+        cd_evol.append(case.get_val('solver.cd')[0])
+        shape0.append(case.get_val('mesh.x_aero0'))
+        shape.append(case.get_val('geometry.x_aero_out'))
+
+    print('success')
+    print('aoa:', prob['aoa'])
+    print('cl:', prob['solver.cl'])
+    print('cd:', prob['solver.cd'])
+    print('obj:', prob['obj'])
+
+    # Recover selig format
+    shape0_selig = np.array(shape0[0])
+    shape1_selig = np.array(shape[-1])
+    for i, val in enumerate(prob.model.solver.isol.vBnd[0][0].connectListNodes):
+        for j in range(prob.model.solver.isol.getnDim()):
+            shape0_selig[i*prob.model.solver.isol.getnDim()+j] = shape0[0][val*prob.model.solver.isol.getnDim()+j]
+            shape1_selig[i*prob.model.solver.isol.getnDim()+j] = shape[-1][val*prob.model.solver.isol.getnDim()+j]
+    from matplotlib import pyplot as plt
+
+    plt.plot(shape0_selig[0::2], shape0_selig[1::2], '--', color='grey', label='Initial shape - cl = {:.2f} - cd = {:.5f}'.format(cl_evol[0], cd_evol[0]))
+    plt.plot(shape1_selig[0::2], shape1_selig[1::2], '-', color='blue', label='Final shape - cl = {:.2f} - cd = {:.5f}'.format(cl_evol[-1], cd_evol[-1]))
+    plt.legend()
+    plt.axis('equal')
+    plt.show()
+    quit()
+
+
+
+    plt.plot(shape0[0][::2], shape0[0][1::2], 'x', color='grey')
+    for xx in shape:
+        plt.plot(xx[::2], xx[1::2], 'x', color='blue')
+    plt.axis('equal')
+    plt.show()
+    quit()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/blast/src/DCoupledAdjoint.cpp b/blast/src/DCoupledAdjoint.cpp
index 4cb1449ab6b6acacdaaa12f225c187019582ed4d..90d440a9dc75ed595ab5075f764d4cf62b1f78c4 100644
--- a/blast/src/DCoupledAdjoint.cpp
+++ b/blast/src/DCoupledAdjoint.cpp
@@ -349,8 +349,15 @@ void CoupledAdjoint::run()
     tms["2-Solve"].stop();
 
     // Print output
-    // aoa gradients
     std::cout << "-------------- Adjoint solution --------------" << std::endl;
+    // aoa gradients
+    std::cout << std::setw(15) << std::left << "Adjoint AoA"
+              << std::setw(15) << std::right << "Res[Cl_AoA]"
+              << std::setw(15) << std::right << "Res[Cd_AoA]" << std::endl;
+    std::cout << std::fixed << std::setprecision(5);
+    std::cout << std::setw(15) << std::left << ""
+              << std::setw(15) << std::right << log10((A_adjoint * Eigen::Map<Eigen::VectorXd>(lambdaCl.data(), lambdaCl.size()) - Eigen::Map<Eigen::VectorXd>(rhsCl.data(), rhsCl.size())).norm())
+              << std::setw(15) << std::right << log10((A_adjoint * Eigen::Map<Eigen::VectorXd>(lambdaCd.data(), lambdaCd.size()) - Eigen::Map<Eigen::VectorXd>(rhsCd.data(), rhsCd.size())).norm()) << std::endl;
     std::cout << std::setw(15) << std::left << "AoA gradients"
               << std::setw(15) << std::right << "dCl_dAoA"
               << std::setw(15) << std::right << "dCd_dAoA" << std::endl;
@@ -359,6 +366,13 @@ void CoupledAdjoint::run()
               << std::setw(15) << std::right << tdCl_AoA
               << std::setw(15) << std::right << tdCd_AoA << std::endl;
     // mesh gradients
+    std::cout << std::setw(15) << std::left << "Adjoint mesh"
+              << std::setw(15) << std::right << "Res[Cl_x]"
+              << std::setw(15) << std::right << "Res[Cd_x]" << std::endl;
+    std::cout << std::fixed << std::setprecision(5);
+    std::cout << std::setw(15) << std::left << ""
+              << std::setw(15) << std::right << log10((dRx_x.transpose() * Eigen::Map<Eigen::VectorXd>(lambdaCl_x.data(), lambdaCl_x.size()) - Eigen::Map<Eigen::VectorXd>(rhsCl_x.data(), rhsCl_x.size())).norm())
+              << std::setw(15) << std::right << log10((dRx_x.transpose() * Eigen::Map<Eigen::VectorXd>(lambdaCd_x.data(), lambdaCd_x.size()) - Eigen::Map<Eigen::VectorXd>(rhsCd_x.data(), rhsCd_x.size())).norm()) << std::endl;
     std::cout << std::setw(15) << std::left << "Mesh gradients"
               << std::setw(15) << std::right << "Norm[dCl_dX]"
               << std::setw(15) << std::right << "Norm[dCd_dX]" << std::endl;
@@ -366,7 +380,7 @@ void CoupledAdjoint::run()
     std::cout << std::setw(15) << std::left << ""
               << std::setw(15) << std::right << Eigen::Map<Eigen::VectorXd>(lambdaCl_x.data(), lambdaCl_x.size()).norm()
               << std::setw(15) << std::right << Eigen::Map<Eigen::VectorXd>(lambdaCd_x.data(), lambdaCd_x.size()).norm() << std::endl;
-
+    tms["0-Total"].stop();
     std::cout << "--------------- Adjoint timers ---------------" << std::endl;
     std::cout << tms << "----------------------------------------------" << std::endl;
     if (vsol->verbose > 2)
@@ -994,8 +1008,8 @@ void CoupledAdjoint::gradientwrtMesh(){
 Eigen::VectorXd CoupledAdjoint::runViscous()
 {
     int exitCode = vsol->run();
-    if (exitCode != 0)
-        std::cout << "vsol terminated with exit code " << exitCode << std::endl;
+    // if (exitCode != 0)
+    //     std::cout << "vsol terminated with exit code " << exitCode << std::endl;
     
     // Extract deltaStar
     std::vector<Eigen::VectorXd> dStar_p;
diff --git a/logo/logo.png b/logo/logo.png
index 52167245921b0e54b21c69be15c15829eac13dd7..ab93625029c9a6c45c6c48169588bbd72e9a538d 100644
Binary files a/logo/logo.png and b/logo/logo.png differ