diff --git a/.coverage b/.coverage
new file mode 100644
index 0000000000000000000000000000000000000000..e61eed4b7c43a3f551e07789589a565c8682ff7d
Binary files /dev/null and b/.coverage differ
diff --git a/.coverage.DESKTOP-ATMEKSV.24064.XFYCqtPx b/.coverage.DESKTOP-ATMEKSV.24064.XFYCqtPx
new file mode 100644
index 0000000000000000000000000000000000000000..e1f7e6f75f5aa9a7a33884193b67b7cc22082200
Binary files /dev/null and b/.coverage.DESKTOP-ATMEKSV.24064.XFYCqtPx differ
diff --git a/Outputs_SeqPosteriorComparison/posterior/Z.npy b/Outputs_SeqPosteriorComparison/posterior/Z.npy
new file mode 100644
index 0000000000000000000000000000000000000000..8d89efa6714257ec2d867aa5eb95b7f23b915010
Binary files /dev/null and b/Outputs_SeqPosteriorComparison/posterior/Z.npy differ
diff --git a/docs/diagrams/.$Structure_BayesInf.drawio.bkp b/docs/diagrams/.$Structure_BayesInf.drawio.bkp
new file mode 100644
index 0000000000000000000000000000000000000000..cd3914505bc5b8bdc582e24e4edcce7eda04e86b
--- /dev/null
+++ b/docs/diagrams/.$Structure_BayesInf.drawio.bkp
@@ -0,0 +1,67 @@
+<mxfile host="Electron" modified="2024-04-10T08:40:20.425Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.1.11 Chrome/114.0.5735.289 Electron/25.9.8 Safari/537.36" etag="ww-3i4-3angrh_Lyrbhs" version="22.1.11" type="device">
+  <diagram name="Page-1" id="efOe0Jku58RX-i1bv-3b">
+    <mxGraphModel dx="1434" dy="956" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
+      <root>
+        <mxCell id="0" />
+        <mxCell id="1" parent="0" />
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-1" value="kernel_rbf" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
+          <mxGeometry x="220" y="130" width="120" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-2" value="_logpdf" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
+          <mxGeometry x="420" y="130" width="120" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-10" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;&lt;b&gt;BayesInf&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot;&gt;&lt;div style=&quot;height:2px;&quot;&gt;&lt;/div&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="60" y="300" width="610" height="390" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-9" target="xary-zVek9Bg-A1b1ZmA-13">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-9" value="create_inference" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="340" y="340" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-25" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-13" target="xary-zVek9Bg-A1b1ZmA-14">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-27" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-13" target="xary-zVek9Bg-A1b1ZmA-15">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-13" value="perform_bootstrap" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="90" y="350" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-14" value="_perturb_data" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="90" y="540" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-15" value="_eval_model" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="500" y="610" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-16" value="normpdf" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="500" y="360" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-17" value="_corr_factor_BME" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="340" y="430" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-18" value="_rejection_sampling" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="160" y="420" width="120" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-26" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-19" target="xary-zVek9Bg-A1b1ZmA-15">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-19" value="_posterior_predictive" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="490" y="430" width="130" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-20" target="xary-zVek9Bg-A1b1ZmA-15">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-20" value="_plot_max_a_posteriori" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="240" y="610" width="140" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-21" value="plot_post_predictive" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="240" y="550" width="120" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-22" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;&lt;b&gt;MCMC&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot;&gt;&lt;div style=&quot;height:2px;&quot;&gt;&lt;/div&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="170" y="750" width="140" height="60" as="geometry" />
+        </mxCell>
+      </root>
+    </mxGraphModel>
+  </diagram>
+</mxfile>
diff --git a/docs/diagrams/Structure_BayesInf.drawio b/docs/diagrams/Structure_BayesInf.drawio
new file mode 100644
index 0000000000000000000000000000000000000000..1651230d37cc9da9120a7f0ae60ff3450d8f225a
--- /dev/null
+++ b/docs/diagrams/Structure_BayesInf.drawio
@@ -0,0 +1,161 @@
+<mxfile host="Electron" modified="2024-04-10T11:36:50.177Z" agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/22.1.11 Chrome/114.0.5735.289 Electron/25.9.8 Safari/537.36" etag="zH0uklPxig_FBrKRFuLP" version="22.1.11" type="device">
+  <diagram name="Page-1" id="efOe0Jku58RX-i1bv-3b">
+    <mxGraphModel dx="836" dy="1114" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
+      <root>
+        <mxCell id="0" />
+        <mxCell id="1" parent="0" />
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-1" value="_kernel_rbf" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
+          <mxGeometry x="1070" y="200" width="120" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-2" value="_logpdf" style="rounded=0;whiteSpace=wrap;html=1;" vertex="1" parent="1">
+          <mxGeometry x="860" y="130" width="120" height="60" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-10" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;&lt;b&gt;BayesInf&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot;&gt;&lt;div style=&quot;height:2px;&quot;&gt;&lt;/div&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="40" y="280" width="1150" height="620" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-24" value="if self.bootstrap &lt;br&gt;or self.bayes_loocv &lt;br&gt;or self.just_analysis" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;labelBackgroundColor=#ffae00;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-9" target="xary-zVek9Bg-A1b1ZmA-13">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-9" target="xary-zVek9Bg-A1b1ZmA-18">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-42" value="if self.name != &#39;valid&#39;&lt;br&gt;and self.inference_method != &#39;rejection&#39;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=default;" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-31">
+          <mxGeometry x="0.5646" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-9" target="xary-zVek9Bg-A1b1ZmA-22">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-43" value="if self.inference_method == &#39;mcmc&#39;" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-32">
+          <mxGeometry x="-0.0958" y="-1" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-33" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-9" target="xary-zVek9Bg-A1b1ZmA-19">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-52" value="always" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=#C2C2C2;" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-33">
+          <mxGeometry x="-0.112" y="1" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-34" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-9" target="xary-zVek9Bg-A1b1ZmA-21">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-47" value="if self.plot_post_pred" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-34">
+          <mxGeometry x="0.2399" y="-1" relative="1" as="geometry">
+            <mxPoint y="1" as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-35" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-9" target="xary-zVek9Bg-A1b1ZmA-20">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-46" value="if self.plot_map_pred" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-35">
+          <mxGeometry x="0.4183" y="-1" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-9" value="create_inference" style="html=1;whiteSpace=wrap;strokeWidth=2;" vertex="1" parent="1">
+          <mxGeometry x="405" y="539" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-25" value="if len(self.perturbed_data) == 0" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-13" target="xary-zVek9Bg-A1b1ZmA-14">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-27" value="if not self.emulator" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-13" target="xary-zVek9Bg-A1b1ZmA-15">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-29" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-13" target="xary-zVek9Bg-A1b1ZmA-16">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-44" value="always" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=#cdcbcb;" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-29">
+          <mxGeometry x="0.4722" y="1" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-30" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-13" target="xary-zVek9Bg-A1b1ZmA-17">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-41" value="if self.emulator" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-30">
+          <mxGeometry x="0.6143" y="-3" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-13" value="perform_bootstrap" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="150" y="310" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-14" value="_perturb_data" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="150" y="760" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-15" value="_eval_model" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="1050" y="660" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-38" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-16" target="xary-zVek9Bg-A1b1ZmA-1">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-49" value="if hasattr bias_inputs&amp;nbsp;&lt;br&gt;and not hasattr error_model" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=#ffae00;" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-38">
+          <mxGeometry x="0.3126" y="-3" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-16" target="xary-zVek9Bg-A1b1ZmA-2">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-16" value="normpdf" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="650" y="390" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-40" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-17" target="xary-zVek9Bg-A1b1ZmA-2">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-50" value="always" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=#cdcbcb;" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-40">
+          <mxGeometry x="-0.6073" y="-5" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-17" value="_corr_factor_BME" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="650" y="450" width="110" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-18" value="_rejection_sampling" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="330" y="790" width="120" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-26" value="if not self.emulator&amp;nbsp;&lt;br&gt;and not self.inference_method == &#39;rejection&#39;&amp;nbsp;&lt;br&gt;and self.name == &#39;calib" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-19" target="xary-zVek9Bg-A1b1ZmA-15">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-37" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-19" target="xary-zVek9Bg-A1b1ZmA-1">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-48" value="if sigma2_prior is not None&lt;br&gt;and if hasattr bias_inputs&lt;br&gt;and if not hasattr error_model" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=#ffae00;" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-37">
+          <mxGeometry x="-0.5544" y="-1" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-19" value="_posterior_predictive" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="675" y="590" width="130" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-28" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;" edge="1" parent="1" source="xary-zVek9Bg-A1b1ZmA-20" target="xary-zVek9Bg-A1b1ZmA-15">
+          <mxGeometry relative="1" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-45" value="always" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];labelBackgroundColor=#cdcbcb;" vertex="1" connectable="0" parent="xary-zVek9Bg-A1b1ZmA-28">
+          <mxGeometry x="0.0517" relative="1" as="geometry">
+            <mxPoint as="offset" />
+          </mxGeometry>
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-20" value="_plot_max_a_posteriori" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="495" y="790" width="140" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-21" value="plot_post_predictive" style="html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="630" y="720" width="120" height="50" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-22" value="&lt;p style=&quot;margin:0px;margin-top:4px;text-align:center;&quot;&gt;&lt;b&gt;MCMC&lt;/b&gt;&lt;/p&gt;&lt;hr size=&quot;1&quot;&gt;&lt;div style=&quot;height:2px;&quot;&gt;&lt;/div&gt;" style="verticalAlign=top;align=left;overflow=fill;fontSize=12;fontFamily=Helvetica;html=1;whiteSpace=wrap;" vertex="1" parent="1">
+          <mxGeometry x="1230" y="425" width="760" height="275" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-36" value="Note: Arrows indicate function calls, beginning calls the end" style="text;html=1;strokeColor=none;fillColor=none;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
+          <mxGeometry x="10" y="10" width="190" height="30" as="geometry" />
+        </mxCell>
+        <mxCell id="xary-zVek9Bg-A1b1ZmA-51" value="Color meanings:&lt;br&gt;&lt;span style=&quot;white-space: pre;&quot;&gt;&#x9;&lt;/span&gt;red: wrong, change&lt;br&gt;&lt;span style=&quot;white-space: pre;&quot;&gt;&#x9;&lt;/span&gt;orange: seems off, look at again" style="text;html=1;strokeColor=none;fillColor=none;align=left;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="1">
+          <mxGeometry x="20" y="70" width="220" height="30" as="geometry" />
+        </mxCell>
+      </root>
+    </mxGraphModel>
+  </diagram>
+</mxfile>
diff --git a/examples/analytical-function/bayesvalidrox/__init__.py b/examples/analytical-function/bayesvalidrox/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e865af80652b8dd29203c2c85f8d1c717e335bc
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+__version__ = "0.0.5"
+
+from .pylink.pylink import PyLinkForwardModel
+from .surrogate_models.surrogate_models import MetaModel
+#from .surrogate_models.meta_model_engine import MetaModelEngine
+from .surrogate_models.engine import Engine
+from .surrogate_models.inputs import Input
+from .post_processing.post_processing import PostProcessing
+from .bayes_inference.bayes_inference import BayesInference
+from .bayes_inference.bayes_model_comparison import BayesModelComparison
+from .bayes_inference.discrepancy import Discrepancy
+
+__all__ = [
+    "__version__",
+    "PyLinkForwardModel",
+    "Input",
+    "Discrepancy",
+    "MetaModel",
+    #"MetaModelEngine",
+    "Engine",
+    "PostProcessing",
+    "BayesInference",
+    "BayesModelComparison"
+    ]
diff --git a/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..beaab3c798a63fcfbc361982388fdf10830a787e
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..afe0b0529fcde6ca9dbc06ed8932485f099ca476
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d5ebaedb4c8b77b5d7dbd0a6945f09079d8b10e4
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__init__.py b/examples/analytical-function/bayesvalidrox/bayes_inference/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..df8d935680f96ab487cf087866e8bfd504762945
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/bayes_inference/__init__.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from .bayes_inference import BayesInference
+from .mcmc import MCMC
+
+__all__ = [
+    "BayesInference",
+    "MCMC"
+    ]
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..430b9885a8c8bd658da24bbc4ac1a6a0a74f69e6
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..72c63a98588c54dfec12536a99537cfa3a67e0cf
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..287257c1ca9b3f3a6d7e176e006ad432f8c685bf
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e89dfb5e6b3a873ac2f40dcc2084aa52caaedcd6
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..05467b4a003342c6353478050dd6b67db4347dcb
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a37330680c29bbfdba5a1bfd98041dda24957604
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..57322b839ff7d50ea32d3b36ce011c09cf91e232
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5f608d0ecf37288d6330349caabd5e7789533748
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b58f792ac36504c702205afc34228600f9bbba77
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bc71688a4cae4f74ec3a67838fca659881c520c9
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f505c78056d5629a52177e4fc2d67abdbd2aa48f
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f154800917aeaef9c499abf13d47fcca6ffc639b
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1d2122246d54ec5697803cbce28900e077b2306a
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..245146ac5f5f72cb819fa504cdf288da3a5b75d3
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b9fe4689524895c2149048d489b22b08e85026df
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/bayes_inference.py b/examples/analytical-function/bayesvalidrox/bayes_inference/bayes_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..7ac43b62becf7441b2db90cf9b4ffeaab33c54bb
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/bayes_inference/bayes_inference.py
@@ -0,0 +1,1530 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import os
+import copy
+import pandas as pd
+from tqdm import tqdm
+from scipy import stats
+import scipy.linalg as spla
+import joblib
+import seaborn as sns
+import corner
+import h5py
+import multiprocessing
+import gc
+from sklearn.metrics import mean_squared_error, r2_score
+from sklearn import preprocessing
+from matplotlib.patches import Patch
+import matplotlib.lines as mlines
+from matplotlib.backends.backend_pdf import PdfPages
+import matplotlib.pylab as plt
+
+from .mcmc import MCMC
+
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class BayesInference:
+    """
+    A class to perform Bayesian Analysis.
+
+
+    Attributes
+    ----------
+    MetaModel : obj
+        Meta model object.
+    discrepancy : obj
+        The discrepancy object for the sigma2s, i.e. the diagonal entries
+        of the variance matrix for a multivariate normal likelihood.
+    name : str, optional
+        The type of analysis, either calibration (`Calib`) or validation
+        (`Valid`). The default is `'Calib'`.
+    emulator : bool, optional
+        Analysis with emulator (MetaModel). The default is `True`.
+    bootstrap : bool, optional
+        Bootstrap the analysis. The default is `False`.
+    req_outputs : list, optional
+        The list of requested output to be used for the analysis.
+        The default is `None`. If None, all the defined outputs for the model
+        object is used.
+    selected_indices : dict, optional
+        A dictionary with the selected indices of each model output. The
+        default is `None`. If `None`, all measurement points are used in the
+        analysis.
+    samples : array of shape (n_samples, n_params), optional
+        The samples to be used in the analysis. The default is `None`. If
+        None the samples are drawn from the probablistic input parameter
+        object of the MetaModel object.
+    n_samples : int, optional
+        Number of samples to be used in the analysis. The default is `500000`.
+        If samples is not `None`, this argument will be assigned based on the
+        number of samples given.
+    measured_data : dict, optional
+        A dictionary containing the observation data. The default is `None`.
+        if `None`, the observation defined in the Model object of the
+        MetaModel is used.
+    inference_method : str, optional
+        A method for approximating the posterior distribution in the Bayesian
+        inference step. The default is `'rejection'`, which stands for
+        rejection sampling. A Markov Chain Monte Carlo sampler can be simply
+        selected by passing `'MCMC'`.
+    mcmc_params : dict, optional
+        A dictionary with args required for the Bayesian inference with
+        `MCMC`. The default is `None`.
+
+        Pass the mcmc_params like the following:
+
+            >>> mcmc_params:{
+                'init_samples': None,  # initial samples
+                'n_walkers': 100,  # number of walkers (chain)
+                'n_steps': 100000,  # number of maximum steps
+                'n_burn': 200,  # number of burn-in steps
+                'moves': None,  # Moves for the emcee sampler
+                'multiprocessing': False,  # multiprocessing
+                'verbose': False # verbosity
+                }
+        The items shown above are the default values. If any parmeter is
+        not defined, the default value will be assigned to it.
+    bayes_loocv : bool, optional
+        Bayesian Leave-one-out Cross Validation. The default is `False`. If
+        `True`, the LOOCV procedure is used to estimate the bayesian Model
+        Evidence (BME).
+    n_bootstrap_itrs : int, optional
+        Number of bootstrap iteration. The default is `1`. If bayes_loocv is
+        `True`, this is qualt to the total length of the observation data
+        set.
+    perturbed_data : array of shape (n_bootstrap_itrs, n_obs), optional
+        User defined perturbed data. The default is `[]`.
+    bootstrap_noise : float, optional
+        A noise level to perturb the data set. The default is `0.05`.
+    just_analysis : bool, optional
+        Justifiability analysis. The default is False.
+    valid_metrics : list, optional
+        List of the validation metrics. The following metrics are supported:
+
+        1. log_BME : logarithm of the Bayesian model evidence
+        2. KLD : Kullback-Leibler Divergence
+        3. inf_entropy: Information entropy
+        The default is `['log_BME']`.
+    plot_post_pred : bool, optional
+        Plot posterior predictive plots. The default is `True`.
+    plot_map_pred : bool, optional
+        Plot the model outputs vs the metamodel predictions for the maximum
+        a posteriori (defined as `max_a_posteriori`) parameter set. The
+        default is `False`.
+    max_a_posteriori : str, optional
+        Maximum a posteriori. `'mean'` and `'mode'` are available. The default
+        is `'mean'`.
+    corner_title_fmt : str, optional
+        Title format for the posterior distribution plot with python
+        package `corner`. The default is `'.2e'`.
+
+    """
+
+    def __init__(self, engine, MetaModel = None, discrepancy=None, emulator=True,
+                 name='Calib', bootstrap=False, req_outputs=None,
+                 selected_indices=None, samples=None, n_samples=100000,
+                 measured_data=None, inference_method='rejection',
+                 mcmc_params=None, bayes_loocv=False, n_bootstrap_itrs=1,
+                 perturbed_data=[], bootstrap_noise=0.05, just_analysis=False,
+                 valid_metrics=['BME'], plot_post_pred=True,
+                 plot_map_pred=False, max_a_posteriori='mean',
+                 corner_title_fmt='.2e'):
+
+        self.engine = engine
+        self.MetaModel = engine.MetaModel
+        self.Discrepancy = discrepancy
+        self.emulator = emulator
+        self.name = name
+        self.bootstrap = bootstrap
+        self.req_outputs = req_outputs
+        self.selected_indices = selected_indices
+        self.samples = samples
+        self.n_samples = n_samples
+        self.measured_data = measured_data
+        self.inference_method = inference_method
+        self.mcmc_params = mcmc_params
+        self.perturbed_data = perturbed_data
+        self.bayes_loocv = bayes_loocv
+        self.n_bootstrap_itrs = n_bootstrap_itrs
+        self.bootstrap_noise = bootstrap_noise
+        self.just_analysis = just_analysis
+        self.valid_metrics = valid_metrics
+        self.plot_post_pred = plot_post_pred
+        self.plot_map_pred = plot_map_pred
+        self.max_a_posteriori = max_a_posteriori
+        self.corner_title_fmt = corner_title_fmt
+
+    # -------------------------------------------------------------------------
+    def create_inference(self):
+        """
+        Starts the inference.
+
+        Returns
+        -------
+        BayesInference : obj
+            The Bayes inference object.
+
+        """
+
+        # Set some variables
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+        n_params = MetaModel.n_params
+        output_names = Model.Output.names
+        par_names = self.engine.ExpDesign.par_names
+
+        # If the prior is set by the user, take it.
+        if self.samples is None:
+            self.samples = self.engine.ExpDesign.generate_samples(
+                self.n_samples, 'random')
+        else:
+            try:
+                samples = self.samples.values
+            except AttributeError:
+                samples = self.samples
+
+            # Take care of an additional Sigma2s
+            self.samples = samples[:, :n_params]
+
+            # Update number of samples
+            self.n_samples = self.samples.shape[0]
+
+        # ---------- Preparation of observation data ----------
+        # Read observation data and perturb it if requested.
+        if self.measured_data is None:
+            self.measured_data = Model.read_observation(case=self.name)
+        # Convert measured_data to a data frame
+        if not isinstance(self.measured_data, pd.DataFrame):
+            self.measured_data = pd.DataFrame(self.measured_data)
+
+        # Extract the total number of measurement points
+        if self.name.lower() == 'calib':
+            self.n_tot_measurement = Model.n_obs
+        else:
+            self.n_tot_measurement = Model.n_obs_valid
+
+        # Find measurement error (if not given) for post predictive plot
+        if not hasattr(self, 'measurement_error'):
+            if isinstance(self.Discrepancy, dict):
+                Disc = self.Discrepancy['known']
+            else:
+                Disc = self.Discrepancy
+            if isinstance(Disc.parameters, dict):
+                self.measurement_error = {k: np.sqrt(Disc.parameters[k]) for k
+                                          in Disc.parameters.keys()}
+            else:
+                try:
+                    self.measurement_error = np.sqrt(Disc.parameters)
+                except TypeError:
+                    pass
+
+        # ---------- Preparation of variance for covariance matrix ----------
+        # Independent and identically distributed
+        total_sigma2 = dict()
+        opt_sigma_flag = isinstance(self.Discrepancy, dict)
+        opt_sigma = None
+        for key_idx, key in enumerate(output_names):
+            # Find opt_sigma
+            if opt_sigma_flag and opt_sigma is None:
+                # Option A: known error with unknown bias term
+                opt_sigma = 'A'
+                known_discrepancy = self.Discrepancy['known']  # TODO: the syntax here looks different from expected
+                self.Discrepancy = self.Discrepancy['infer'] # TODO: the syntax here looks different from expected
+                sigma2 = np.array(known_discrepancy.parameters[key])
+
+            elif self.Discrepancy.parameters is not None:
+                # Option B: The sigma2 is known (no bias term)
+                opt_sigma = 'B'
+                sigma2 = np.array(self.Discrepancy.parameters[key])
+
+            elif not isinstance(self.Discrepancy.InputDisc, str):
+                # Option C: The sigma2 is unknown (bias term including error)
+                opt_sigma = 'C'
+                n_measurement = self.measured_data[key].values.shape
+                sigma2 = np.zeros((n_measurement[0]))
+
+            total_sigma2[key] = sigma2
+
+        self.Discrepancy.opt_sigma = opt_sigma
+        self.Discrepancy.total_sigma2 = total_sigma2
+
+        # If inferred sigma2s obtained from e.g. calibration are given
+        try:
+            self.sigma2s = self.Discrepancy.get_sample(self.n_samples)
+        except:
+            pass #TODO: should an error be raised in this case?
+
+        # ---------------- Bootstrap & TOM --------------------
+        if self.bootstrap or self.bayes_loocv or self.just_analysis:
+            if len(self.perturbed_data) == 0:
+                # zero mean noise Adding some noise to the observation function
+                self.perturbed_data = self._perturb_data(
+                    self.measured_data, output_names
+                    )
+            else:
+                self.n_bootstrap_itrs = len(self.perturbed_data)
+
+            # -------- Model Discrepancy -----------
+            if hasattr(self, 'error_model') and self.error_model \
+               and self.name.lower() != 'calib':
+                # Select posterior mean as MAP
+                MAP_theta = self.samples.mean(axis=0).reshape((1, n_params))
+                # MAP_theta = stats.mode(self.samples,axis=0)[0]
+
+                # Evaluate the (meta-)model at the MAP
+                y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=MAP_theta)
+
+                # Train a GPR meta-model using MAP
+                self.error_MetaModel = MetaModel.create_model_error(
+                    self.bias_inputs, y_MAP, Name=self.name
+                    )
+
+            # -----------------------------------------------------
+            # ----- Loop over the perturbed observation data ------
+            # -----------------------------------------------------
+            # Initilize arrays
+            logLikelihoods = np.zeros((self.n_samples, self.n_bootstrap_itrs),
+                                      dtype=np.float16)
+            BME_Corr = np.zeros((self.n_bootstrap_itrs))
+            log_BME = np.zeros((self.n_bootstrap_itrs))
+            KLD = np.zeros((self.n_bootstrap_itrs))
+            inf_entropy = np.zeros((self.n_bootstrap_itrs))
+
+            # Compute the prior predtions
+            # Evaluate the MetaModel
+            if self.emulator:
+                y_hat, y_std = MetaModel.eval_metamodel(samples=self.samples)
+                self.__mean_pce_prior_pred = y_hat
+                self._std_pce_prior_pred = y_std
+
+                # Correct the predictions with Model discrepancy
+                if hasattr(self, 'error_model') and self.error_model:
+                    y_hat_corr, y_std = self.error_MetaModel.eval_model_error(
+                        self.bias_inputs, self.__mean_pce_prior_pred
+                        )
+                    self.__mean_pce_prior_pred = y_hat_corr
+                    self._std_pce_prior_pred = y_std
+
+                # Surrogate model's error using RMSE of test data
+                if hasattr(MetaModel, 'rmse'):
+                    surrError = MetaModel.rmse
+                else:
+                    surrError = None
+
+            else:
+                # Evaluate the original model
+                self.__model_prior_pred = self._eval_model(
+                    samples=self.samples, key='PriorPred'
+                    )
+                surrError = None
+
+            # Start the likelihood-BME computations for the perturbed data
+            for itr_idx, data in tqdm(
+                    enumerate(self.perturbed_data),
+                    total=self.n_bootstrap_itrs,
+                    desc="Bootstrapping the BME calculations", ascii=True
+                    ):
+
+                # ---------------- Likelihood calculation ----------------
+                if self.emulator:
+                    model_evals = self.__mean_pce_prior_pred
+                else:
+                    model_evals = self.__model_prior_pred
+
+                # Leave one out
+                if self.bayes_loocv or self.just_analysis:
+                    self.selected_indices = np.nonzero(data)[0]
+
+                # Prepare data dataframe
+                nobs = list(self.measured_data.count().values[1:])
+                numbers = list(np.cumsum(nobs))
+                indices = list(zip([0] + numbers, numbers))
+                data_dict = {
+                    output_names[i]: data[j:k] for i, (j, k) in
+                    enumerate(indices)
+                    }
+                #print(output_names)
+                #print(indices)
+                #print(numbers)
+                #print(nobs)
+                #print(self.measured_data)
+                #for i, (j, k) in enumerate(indices):
+                #    print(i,j,k)
+                #print(data)
+                #print(data_dict)
+                #stop
+
+                # Unknown sigma2
+                if opt_sigma == 'C' or hasattr(self, 'sigma2s'):
+                    logLikelihoods[:, itr_idx] = self.normpdf(
+                        model_evals, data_dict, total_sigma2,
+                        sigma2=self.sigma2s, std=surrError
+                        )
+                else:
+                    # known sigma2
+                    logLikelihoods[:, itr_idx] = self.normpdf(
+                        model_evals, data_dict, total_sigma2,
+                        std=surrError
+                        )
+                # ---------------- BME Calculations ----------------
+                # BME (log)
+                log_BME[itr_idx] = np.log(
+                    np.nanmean(np.exp(logLikelihoods[:, itr_idx],
+                                      dtype=np.longdouble))#float128))
+                    )
+
+                # BME correction when using Emulator
+                if self.emulator:
+                    BME_Corr[itr_idx] = self.__corr_factor_BME(
+                        data_dict, total_sigma2, log_BME[itr_idx]
+                        )
+
+                # Rejection Step
+                if 'kld' in list(map(str.lower, self.valid_metrics)) and\
+                   'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+                    # Random numbers between 0 and 1
+                    unif = np.random.rand(1, self.n_samples)[0]
+
+                    # Reject the poorly performed prior
+                    Likelihoods = np.exp(logLikelihoods[:, itr_idx],
+                                         dtype=np.float64)
+                    accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+                    posterior = self.samples[accepted]
+
+                    # Posterior-based expectation of likelihoods
+                    postExpLikelihoods = np.mean(
+                        logLikelihoods[:, itr_idx][accepted]
+                        )
+
+                    # Calculate Kullback-Leibler Divergence
+                    KLD[itr_idx] = postExpLikelihoods - log_BME[itr_idx]
+
+                # Posterior-based expectation of prior densities
+                if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+                    n_thread = int(0.875 * multiprocessing.cpu_count())
+                    with multiprocessing.Pool(n_thread) as p:
+                        postExpPrior = np.mean(np.concatenate(
+                            p.map(
+                                self.engine.ExpDesign.JDist.pdf,
+                                np.array_split(posterior.T, n_thread, axis=1))
+                            )
+                            )
+                    # Information Entropy based on Entropy paper Eq. 38
+                    inf_entropy[itr_idx] = log_BME[itr_idx] - postExpPrior - \
+                        postExpLikelihoods
+
+                # Clear memory
+                gc.collect(generation=2)
+
+            # ---------- Store metrics for perturbed data set ----------------
+            # Likelihoods (Size: n_samples, n_bootstrap_itr)
+            self.log_likes = logLikelihoods
+
+            # BME (log), KLD, infEntropy (Size: 1,n_bootstrap_itr)
+            self.log_BME = log_BME
+
+            # BMECorrFactor (log) (Size: 1,n_bootstrap_itr)
+            if self.emulator:
+                self.log_BME_corr_factor = BME_Corr
+
+            if 'kld' in list(map(str.lower, self.valid_metrics)):
+                self.KLD = KLD
+            if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+                self.inf_entropy = inf_entropy
+
+            # BME = BME + BMECorrFactor
+            if self.emulator:
+                self.log_BME += self.log_BME_corr_factor
+
+        # ---------------- Parameter Bayesian inference ----------------
+        if self.inference_method.lower() == 'mcmc':
+            # Instantiate the MCMC object
+            MCMC_Obj = MCMC(self)
+            self.posterior_df = MCMC_Obj.run_sampler(
+                self.measured_data, total_sigma2
+                )
+
+        elif self.name.lower() == 'valid':
+            # Convert to a dataframe if samples are provided after calibration.
+            self.posterior_df = pd.DataFrame(self.samples, columns=par_names)
+
+        else:
+            # Rejection sampling
+            self.posterior_df = self._rejection_sampling()
+
+        # Provide posterior's summary
+        print('\n')
+        print('-'*15 + 'Posterior summary' + '-'*15)
+        pd.options.display.max_columns = None
+        pd.options.display.max_rows = None
+        print(self.posterior_df.describe())
+        print('-'*50)
+
+        # -------- Model Discrepancy -----------
+        if hasattr(self, 'error_model') and self.error_model \
+           and self.name.lower() == 'calib':
+            if self.inference_method.lower() == 'mcmc':
+                self.error_MetaModel = MCMC_Obj.error_MetaModel
+            else:
+                # Select posterior mean as MAP
+                if opt_sigma == "B":
+                    posterior_df = self.posterior_df.values
+                else:
+                    posterior_df = self.posterior_df.values[:, :-Model.n_outputs]
+
+                # Select posterior mean as Maximum a posteriori
+                map_theta = posterior_df.mean(axis=0).reshape((1, n_params))
+                # map_theta = stats.mode(Posterior_df,axis=0)[0]
+
+                # Evaluate the (meta-)model at the MAP
+                y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=map_theta)
+
+                # Train a GPR meta-model using MAP
+                self.error_MetaModel = MetaModel.create_model_error(
+                    self.bias_inputs, y_MAP, Name=self.name
+                    )
+
+        # -------- Posterior perdictive -----------
+        self._posterior_predictive()
+
+        # -----------------------------------------------------
+        # ------------------ Visualization --------------------
+        # -----------------------------------------------------
+        # Create Output directory, if it doesn't exist already.
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        os.makedirs(out_dir, exist_ok=True)
+
+        # -------- Posteior parameters --------
+        if opt_sigma != "B":
+            par_names.extend(
+                [self.Discrepancy.InputDisc.Marginals[i].name for i
+                 in range(len(self.Discrepancy.InputDisc.Marginals))]
+                )
+        # Pot with corner
+        figPosterior = corner.corner(self.posterior_df.to_numpy(),
+                                     labels=par_names,
+                                     quantiles=[0.15, 0.5, 0.85],
+                                     show_titles=True,
+                                     title_fmt=self.corner_title_fmt,
+                                     labelpad=0.2,
+                                     use_math_text=True,
+                                     title_kwargs={"fontsize": 28},
+                                     plot_datapoints=False,
+                                     plot_density=False,
+                                     fill_contours=True,
+                                     smooth=0.5,
+                                     smooth1d=0.5)
+
+        # Loop over axes and set x limits
+        if opt_sigma == "B":
+            axes = np.array(figPosterior.axes).reshape(
+                (len(par_names), len(par_names))
+                )
+            for yi in range(len(par_names)):
+                ax = axes[yi, yi]
+                ax.set_xlim(self.engine.ExpDesign.bound_tuples[yi])
+                for xi in range(yi):
+                    ax = axes[yi, xi]
+                    ax.set_xlim(self.engine.ExpDesign.bound_tuples[xi])
+        plt.close()
+
+        # Turn off gridlines
+        for ax in figPosterior.axes:
+            ax.grid(False)
+
+        if self.emulator:
+            plotname = f'/Posterior_Dist_{Model.name}_emulator'
+        else:
+            plotname = f'/Posterior_Dist_{Model.name}'
+
+        figPosterior.set_size_inches((24, 16))
+        figPosterior.savefig(f'./{out_dir}{plotname}.pdf',
+                             bbox_inches='tight')
+
+        # -------- Plot MAP --------
+        if self.plot_map_pred:
+            self._plot_max_a_posteriori()
+
+        # -------- Plot log_BME dist --------
+        if self.bootstrap:
+
+            # Computing the TOM performance
+            self.log_BME_tom = stats.chi2.rvs(
+                self.n_tot_measurement, size=self.log_BME.shape[0]
+                )
+
+            fig, ax = plt.subplots()
+            sns.kdeplot(self.log_BME_tom, ax=ax, color="green", shade=True)
+            sns.kdeplot(
+                self.log_BME, ax=ax, color="blue", shade=True,
+                label='Model BME')
+
+            ax.set_xlabel('log$_{10}$(BME)')
+            ax.set_ylabel('Probability density')
+
+            legend_elements = [
+                Patch(facecolor='green', edgecolor='green', label='TOM BME'),
+                Patch(facecolor='blue', edgecolor='blue', label='Model BME')
+                ]
+            ax.legend(handles=legend_elements)
+
+            if self.emulator:
+                plotname = f'/BME_hist_{Model.name}_emulator'
+            else:
+                plotname = f'/BME_hist_{Model.name}'
+
+            plt.savefig(f'./{out_dir}{plotname}.pdf', bbox_inches='tight')
+            plt.show()
+            plt.close()
+
+        # -------- Posteior perdictives --------
+        if self.plot_post_pred:
+            # Plot the posterior predictive
+            self._plot_post_predictive()
+
+        return self
+
+    # -------------------------------------------------------------------------
+    def _perturb_data(self, data, output_names):
+        """
+        Returns an array with n_bootstrap_itrs rowsof perturbed data.
+        The first row includes the original observation data.
+        If `self.bayes_loocv` is True, a 2d-array will be returned with
+        repeated rows and zero diagonal entries.
+
+        Parameters
+        ----------
+        data : pandas DataFrame
+            Observation data.
+        output_names : list
+            List of the output names.
+
+        Returns
+        -------
+        final_data : array
+            Perturbed data set.
+
+        """
+        noise_level = self.bootstrap_noise
+        obs_data = data[output_names].values
+        n_measurement, n_outs = obs_data.shape
+        self.n_tot_measurement = obs_data[~np.isnan(obs_data)].shape[0]
+        # Number of bootstrap iterations
+        if self.bayes_loocv:
+            self.n_bootstrap_itrs = self.n_tot_measurement
+
+        # Pass loocv dataset
+        if self.bayes_loocv:
+            obs = obs_data.T[~np.isnan(obs_data.T)]
+            final_data = np.repeat(np.atleast_2d(obs), self.n_bootstrap_itrs,
+                                   axis=0)
+            np.fill_diagonal(final_data, 0)
+            return final_data
+
+        else:
+            final_data = np.zeros(
+                (self.n_bootstrap_itrs, self.n_tot_measurement)
+                )
+            final_data[0] = obs_data.T[~np.isnan(obs_data.T)]
+            for itrIdx in range(1, self.n_bootstrap_itrs):
+                data = np.zeros((n_measurement, n_outs))
+                for idx in range(len(output_names)):
+                    std = np.nanstd(obs_data[:, idx])
+                    if std == 0:
+                        std = 0.001
+                    noise = std * noise_level
+                    data[:, idx] = np.add(
+                        obs_data[:, idx],
+                        np.random.normal(0, 1, obs_data.shape[0]) * noise,
+                    )
+
+                final_data[itrIdx] = data.T[~np.isnan(data.T)]
+
+            return final_data
+
+    # -------------------------------------------------------------------------
+    def _logpdf(self, x, mean, cov):
+        """
+        computes the likelihood based on a multivariate normal distribution.
+
+        Parameters
+        ----------
+        x : TYPE
+            DESCRIPTION.
+        mean : array_like
+            Observation data.
+        cov : 2d array
+            Covariance matrix of the distribution.
+
+        Returns
+        -------
+        log_lik : float
+            Log likelihood.
+
+        """
+        n = len(mean)
+        L = spla.cholesky(cov, lower=True)
+        beta = np.sum(np.log(np.diag(L)))
+        dev = x - mean
+        alpha = dev.dot(spla.cho_solve((L, True), dev))
+        log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+        return log_lik
+
+    # -------------------------------------------------------------------------
+    def _eval_model(self, samples=None, key='MAP'):
+        """
+        Evaluates Forward Model.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Parameter sets. The default is None.
+        key : str, optional
+            Key string to be passed to the run_model_parallel method.
+            The default is 'MAP'.
+
+        Returns
+        -------
+        model_outputs : dict
+            Model outputs.
+
+        """
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+
+        if samples is None:
+            self.samples = self.engine.ExpDesign.generate_samples(
+                self.n_samples, 'random')
+        else:
+            self.samples = samples
+            self.n_samples = len(samples)
+
+        model_outputs, _ = Model.run_model_parallel(
+            self.samples, key_str=key+self.name)
+
+        # Clean up
+        # Zip the subdirectories
+        try:
+            dir_name = f'{Model.name}MAP{self.name}'
+            key = dir_name + '_'
+            Model.zip_subdirs(dir_name, key)
+        except:
+            pass
+
+        return model_outputs
+
+    # -------------------------------------------------------------------------
+    def _kernel_rbf(self, X, hyperparameters):
+        """
+        Isotropic squared exponential kernel.
+
+        Higher l values lead to smoother functions and therefore to coarser
+        approximations of the training data. Lower l values make functions
+        more wiggly with wide uncertainty regions between training data points.
+
+        sigma_f controls the marginal variance of b(x)
+
+        Parameters
+        ----------
+        X : ndarray of shape (n_samples_X, n_features)
+
+        hyperparameters : Dict
+            Lambda characteristic length
+            sigma_f controls the marginal variance of b(x)
+            sigma_0 unresolvable error nugget term, interpreted as random
+                    error that cannot be attributed to measurement error.
+        Returns
+        -------
+        var_cov_matrix : ndarray of shape (n_samples_X,n_samples_X)
+            Kernel k(X, X).
+
+        """
+        from sklearn.gaussian_process.kernels import RBF
+        min_max_scaler = preprocessing.MinMaxScaler()
+        X_minmax = min_max_scaler.fit_transform(X)
+
+        nparams = len(hyperparameters)
+        # characteristic length (0,1]
+        Lambda = hyperparameters[0]
+        # sigma_f controls the marginal variance of b(x)
+        sigma2_f = hyperparameters[1]
+
+        # cov_matrix = sigma2_f*rbf_kernel(X_minmax, gamma = 1/Lambda**2)
+
+        rbf = RBF(length_scale=Lambda)
+        cov_matrix = sigma2_f * rbf(X_minmax)
+        if nparams > 2:
+            # (unresolvable error) nugget term that is interpreted as random
+            # error that cannot be attributed to measurement error.
+            sigma2_0 = hyperparameters[2:]
+            for i, j in np.ndindex(cov_matrix.shape):
+                cov_matrix[i, j] += np.sum(sigma2_0) if i == j else 0
+
+        return cov_matrix
+
+    # -------------------------------------------------------------------------
+    def normpdf(self, outputs, obs_data, total_sigma2s, sigma2=None, std=None):
+        """
+        Calculates the likelihood of simulation outputs compared with
+        observation data.
+
+        Parameters
+        ----------
+        outputs : dict
+            A dictionary containing the simulation outputs as array of shape
+            (n_samples, n_measurement) for each model output.
+        obs_data : dict
+            A dictionary/dataframe containing the observation data.
+        total_sigma2s : dict
+            A dictionary with known values of the covariance diagonal entries,
+            a.k.a sigma^2.
+        sigma2 : array, optional
+            An array of the sigma^2 samples, when the covariance diagonal
+            entries are unknown and are being jointly inferred. The default is
+            None.
+        std : dict, optional
+            A dictionary containing the root mean squared error as array of
+            shape (n_samples, n_measurement) for each model output. The default
+            is None.
+
+        Returns
+        -------
+        logLik : array of shape (n_samples)
+            Likelihoods.
+
+        """
+        Model = self.engine.Model
+        logLik = 0.0
+
+        # Extract the requested model outputs for likelihood calulation
+        if self.req_outputs is None:
+            req_outputs = Model.Output.names
+        else:
+            req_outputs = list(self.req_outputs)
+
+        # Loop over the outputs
+        for idx, out in enumerate(req_outputs):
+
+            # (Meta)Model Output
+            nsamples, nout = outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Add the std of the PCE is chosen as emulator.
+            if self.emulator:
+                if std is not None:
+                    tot_sigma2s += std[out]**2
+
+            # Covariance Matrix
+            covMatrix = np.diag(tot_sigma2s)
+
+            # Select the data points to compare
+            try:
+                indices = self.selected_indices[out]
+            except:
+                indices = list(range(nout))
+            covMatrix = np.diag(covMatrix[indices, indices])
+
+            # If sigma2 is not given, use given total_sigma2s
+            if sigma2 is None:
+                logLik += stats.multivariate_normal.logpdf(
+                    outputs[out][:, indices], data[indices], covMatrix)
+                #print('In if')
+                #print(logLik)
+                continue
+
+            # Loop over each run/sample and calculate logLikelihood
+            logliks = np.zeros(nsamples)
+            for s_idx in range(nsamples):
+
+                # Simulation run
+                tot_outputs = outputs[out]
+
+                # Covariance Matrix
+                covMatrix = np.diag(tot_sigma2s)
+
+                if sigma2 is not None:
+                    # Check the type error term
+                    if hasattr(self, 'bias_inputs') and \
+                       not hasattr(self, 'error_model'):
+                        # Infer a Bias model usig Gaussian Process Regression
+                        bias_inputs = np.hstack(
+                            (self.bias_inputs[out],
+                             tot_outputs[s_idx].reshape(-1, 1)))
+
+                        params = sigma2[s_idx, idx*3:(idx+1)*3]
+                        covMatrix = self._kernel_rbf(bias_inputs, params)
+                    else:
+                        # Infer equal sigma2s
+                        try:
+                            sigma_2 = sigma2[s_idx, idx]
+                        except TypeError:
+                            sigma_2 = 0.0
+
+                        covMatrix += sigma_2 * np.eye(nout)
+                        # covMatrix = np.diag(sigma2 * total_sigma2s)
+
+                # Select the data points to compare
+                try:
+                    indices = self.selected_indices[out]
+                except:
+                    indices = list(range(nout))
+                covMatrix = np.diag(covMatrix[indices, indices])
+
+                # Compute loglikelihood
+                logliks[s_idx] = self._logpdf(
+                    tot_outputs[s_idx, indices], data[indices], covMatrix
+                    )
+            #print('Continued')
+            #print(logliks)
+            logLik += logliks
+        #print(logLik)
+        return logLik
+
+    # -------------------------------------------------------------------------
+    def _corr_factor_BME_old(self, Data, total_sigma2s, posterior):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        OrigModelOutput = self.engine.ExpDesign.Y
+        Model = self.engine.Model
+
+        # Posterior with guassian-likelihood
+        postDist = stats.gaussian_kde(posterior.T)
+
+        # Remove NaN
+        Data = Data[~np.isnan(Data)]
+        total_sigma2s = total_sigma2s[~np.isnan(total_sigma2s)]
+
+        # Covariance Matrix
+        covMatrix = np.diag(total_sigma2s[:self.n_tot_measurement])
+
+        # Extract the requested model outputs for likelihood calulation
+        if self.req_outputs is None:
+            OutputType = Model.Output.names
+        else:
+            OutputType = list(self.req_outputs)
+
+        # SampleSize = OrigModelOutput[OutputType[0]].shape[0]
+
+
+        # Flatten the OutputType for OrigModel
+        TotalOutputs = np.concatenate([OrigModelOutput[x] for x in OutputType], 1)
+
+        NrofBayesSamples = self.n_samples
+        # Evaluate MetaModel on the experimental design
+        Samples = self.engine.ExpDesign.X
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=Samples)
+
+        # Reset the NrofSamples to NrofBayesSamples
+        self.n_samples = NrofBayesSamples
+
+        # Flatten the OutputType for MetaModel
+        TotalPCEOutputs = np.concatenate([OutputRS[x] for x in OutputRS], 1)
+        TotalPCEstdOutputRS= np.concatenate([stdOutputRS[x] for x in stdOutputRS], 1)
+
+        logweight = 0
+        for i, sample in enumerate(Samples):
+            # Compute likelilhood output vs RS
+            covMatrix = np.diag(TotalPCEstdOutputRS[i]**2)
+            logLik = self._logpdf(TotalOutputs[i], TotalPCEOutputs[i], covMatrix)
+            # Compute posterior likelihood of the collocation points
+            logpostLik = np.log(postDist.pdf(sample[:, None]))[0]
+            if logpostLik != -np.inf:
+                logweight += logLik + logpostLik
+        return logweight
+
+    # -------------------------------------------------------------------------
+    def __corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = self.engine.ExpDesign.X
+        model_outputs = self.engine.ExpDesign.Y
+        Model = self.engine.Model
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = Model.Output.names
+
+        # Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                covMatrix = np.eye(len(y_m)) * 1/(2*np.pi)
+
+                # Select the data points to compare
+                try:
+                    indices = self.selected_indices[out]
+                except:
+                    indices = list(range(nout))
+                covMatrix = np.diag(covMatrix[indices, indices])
+                covMatrix_data = np.diag(covMatrix_data[indices, indices])
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += self._logpdf(
+                    y_m_hat[indices], data[indices],
+                    covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += self._logpdf(
+                    y_m_hat[indices], y_m[indices],
+                    covMatrix
+                    )
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.mean(np.exp(logLik_model+logLik_data))
+
+        return np.log(weights)
+
+    # -------------------------------------------------------------------------
+    def _rejection_sampling(self):
+        """
+        Performs rejection sampling to update the prior distribution on the
+        input parameters.
+
+        Returns
+        -------
+        posterior : pandas.dataframe
+            Posterior samples of the input parameters.
+
+        """
+
+        MetaModel = self.MetaModel
+        try:
+            sigma2_prior = self.Discrepancy.sigma2_prior
+        except:
+            sigma2_prior = None
+
+        # Check if the discrepancy is defined as a distribution:
+        samples = self.samples
+
+        if sigma2_prior is not None:
+            samples = np.hstack((samples, sigma2_prior))
+
+        # Take the first column of Likelihoods (Observation data without noise)
+        if self.just_analysis or self.bayes_loocv:
+            index = self.n_tot_measurement-1
+            likelihoods = np.exp(self.log_likes[:, index], dtype=np.longdouble)#np.float128)
+        else:
+            likelihoods = np.exp(self.log_likes[:, 0], dtype=np.longdouble)#np.float128)
+
+        n_samples = len(likelihoods)
+        norm_ikelihoods = likelihoods / np.max(likelihoods)
+
+        # Normalize based on min if all Likelihoods are zero
+        if all(likelihoods == 0.0):
+            likelihoods = self.log_likes[:, 0]
+            norm_ikelihoods = likelihoods / np.min(likelihoods)
+
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, n_samples)[0]
+
+        # Reject the poorly performed prior
+        accepted_samples = samples[norm_ikelihoods >= unif]
+
+        # Output the Posterior
+        par_names = self.engine.ExpDesign.par_names
+        if sigma2_prior is not None:
+            for name in self.Discrepancy.name:
+                par_names.append(name)
+
+        return pd.DataFrame(accepted_samples, columns=sigma2_prior)
+
+    # -------------------------------------------------------------------------
+    def _posterior_predictive(self):
+        """
+        Stores the prior- and posterior predictive samples, i.e. model
+        evaluations using the samples, into hdf5 files.
+
+        priorPredictive.hdf5 : Prior predictive samples.
+        postPredictive_wo_noise.hdf5 : Posterior predictive samples without
+        the additive noise.
+        postPredictive.hdf5 : Posterior predictive samples with the additive
+        noise.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+
+        # Make a directory to save the prior/posterior predictive
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        os.makedirs(out_dir, exist_ok=True)
+
+        # Read observation data and perturb it if requested
+        if self.measured_data is None:
+            self.measured_data = Model.read_observation(case=self.name)
+
+        if not isinstance(self.measured_data, pd.DataFrame):
+            self.measured_data = pd.DataFrame(self.measured_data)
+
+        # X_values
+        x_values = self.engine.ExpDesign.x_values
+
+        try:
+            sigma2_prior = self.Discrepancy.sigma2_prior
+        except:
+            sigma2_prior = None
+
+        # Extract posterior samples
+        posterior_df = self.posterior_df
+
+        # Take care of the sigma2
+        if sigma2_prior is not None:
+            try:
+                sigma2s = posterior_df[self.Discrepancy.name].values
+                posterior_df = posterior_df.drop(
+                    labels=self.Discrepancy.name, axis=1
+                    )
+            except:
+                sigma2s = self.sigma2s
+
+        # Posterior predictive
+        if self.emulator:
+            if self.inference_method == 'rejection':
+                prior_pred = self.__mean_pce_prior_pred
+            if self.name.lower() != 'calib':
+                post_pred = self.__mean_pce_prior_pred
+                post_pred_std = self._std_pce_prior_pred
+            else:
+                post_pred, post_pred_std = MetaModel.eval_metamodel(
+                    samples=posterior_df.values
+                    )
+
+        else:
+            if self.inference_method == 'rejection':
+                prior_pred = self.__model_prior_pred
+            if self.name.lower() != 'calib':
+                post_pred = self.__mean_pce_prior_pred,
+                post_pred_std = self._std_pce_prior_pred
+            else:
+                post_pred = self._eval_model(
+                    samples=posterior_df.values, key='PostPred'
+                    )
+        # Correct the predictions with Model discrepancy
+        if hasattr(self, 'error_model') and self.error_model:
+            y_hat, y_std = self.error_MetaModel.eval_model_error(
+                self.bias_inputs, post_pred
+                )
+            post_pred, post_pred_std = y_hat, y_std
+
+        # Add discrepancy from likelihood Sample to the current posterior runs
+        total_sigma2 = self.Discrepancy.total_sigma2
+        post_pred_withnoise = copy.deepcopy(post_pred)
+        for varIdx, var in enumerate(Model.Output.names):
+            for i in range(len(post_pred[var])):
+                pred = post_pred[var][i]
+
+                # Known sigma2s
+                clean_sigma2 = total_sigma2[var][~np.isnan(total_sigma2[var])]
+                tot_sigma2 = clean_sigma2[:len(pred)]
+                cov = np.diag(tot_sigma2)
+
+                # Check the type error term
+                if sigma2_prior is not None:
+                    # Inferred sigma2s
+                    if hasattr(self, 'bias_inputs') and \
+                       not hasattr(self, 'error_model'):
+                        # TODO: Infer a Bias model usig GPR
+                        bias_inputs = np.hstack((
+                            self.bias_inputs[var], pred.reshape(-1, 1)))
+                        params = sigma2s[i, varIdx*3:(varIdx+1)*3]
+                        cov = self._kernel_rbf(bias_inputs, params)
+                    else:
+                        # Infer equal sigma2s
+                        try:
+                            sigma2 = sigma2s[i, varIdx]
+                        except TypeError:
+                            sigma2 = 0.0
+
+                        # Convert biasSigma2s to a covMatrix
+                        cov += sigma2 * np.eye(len(pred))
+
+                if self.emulator:
+                    if hasattr(MetaModel, 'rmse') and \
+                       MetaModel.rmse is not None:
+                        stdPCE = MetaModel.rmse[var]
+                    else:
+                        stdPCE = post_pred_std[var][i]
+                    # Expected value of variance (Assump: i.i.d stds)
+                    cov += np.diag(stdPCE**2)
+
+                # Sample a multivariate normal distribution with mean of
+                # prediction and variance of cov
+                post_pred_withnoise[var][i] = np.random.multivariate_normal(
+                    pred, cov, 1
+                    )
+
+        # ----- Prior Predictive -----
+        if self.inference_method.lower() == 'rejection':
+            # Create hdf5 metadata
+            hdf5file = f'{out_dir}/priorPredictive.hdf5'
+            hdf5_exist = os.path.exists(hdf5file)
+            if hdf5_exist:
+                os.remove(hdf5file)
+            file = h5py.File(hdf5file, 'a')
+
+            # Store x_values
+            if type(x_values) is dict:
+                grp_x_values = file.create_group("x_values/")
+                for varIdx, var in enumerate(Model.Output.names):
+                    grp_x_values.create_dataset(var, data=x_values[var])
+            else:
+                file.create_dataset("x_values", data=x_values)
+
+            # Store posterior predictive
+            grpY = file.create_group("EDY/")
+            for varIdx, var in enumerate(Model.Output.names):
+                grpY.create_dataset(var, data=prior_pred[var])
+
+        # ----- Posterior Predictive only model evaluations -----
+        # Create hdf5 metadata
+        hdf5file = out_dir+'/postPredictive_wo_noise.hdf5'
+        hdf5_exist = os.path.exists(hdf5file)
+        if hdf5_exist:
+            os.remove(hdf5file)
+        file = h5py.File(hdf5file, 'a')
+
+        # Store x_values
+        if type(x_values) is dict:
+            grp_x_values = file.create_group("x_values/")
+            for varIdx, var in enumerate(Model.Output.names):
+                grp_x_values.create_dataset(var, data=x_values[var])
+        else:
+            file.create_dataset("x_values", data=x_values)
+
+        # Store posterior predictive
+        grpY = file.create_group("EDY/")
+        for varIdx, var in enumerate(Model.Output.names):
+            grpY.create_dataset(var, data=post_pred[var])
+
+        # ----- Posterior Predictive with noise -----
+        # Create hdf5 metadata
+        hdf5file = out_dir+'/postPredictive.hdf5'
+        hdf5_exist = os.path.exists(hdf5file)
+        if hdf5_exist:
+            os.remove(hdf5file)
+        file = h5py.File(hdf5file, 'a')
+
+        # Store x_values
+        if type(x_values) is dict:
+            grp_x_values = file.create_group("x_values/")
+            for varIdx, var in enumerate(Model.Output.names):
+                grp_x_values.create_dataset(var, data=x_values[var])
+        else:
+            file.create_dataset("x_values", data=x_values)
+
+        # Store posterior predictive
+        grpY = file.create_group("EDY/")
+        for varIdx, var in enumerate(Model.Output.names):
+            grpY.create_dataset(var, data=post_pred_withnoise[var])
+
+        return
+
+    # -------------------------------------------------------------------------
+    def _plot_max_a_posteriori(self):
+        """
+        Plots the response of the model output against that of the metamodel at
+        the maximum a posteriori sample (mean or mode of posterior.)
+
+        Returns
+        -------
+        None.
+
+        """
+
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        opt_sigma = self.Discrepancy.opt_sigma
+
+        # -------- Find MAP and run MetaModel and origModel --------
+        # Compute the MAP
+        if self.max_a_posteriori.lower() == 'mean':
+            if opt_sigma == "B":
+                Posterior_df = self.posterior_df.values
+            else:
+                Posterior_df = self.posterior_df.values[:, :-Model.n_outputs]
+            map_theta = Posterior_df.mean(axis=0).reshape(
+                (1, MetaModel.n_params))
+        else:
+            map_theta = stats.mode(Posterior_df.values, axis=0)[0]
+        # Prin report
+        print("\nPoint estimator:\n", map_theta[0])
+
+        # Run the models for MAP
+        # MetaModel
+        map_metamodel_mean, map_metamodel_std = MetaModel.eval_metamodel(
+            samples=map_theta)
+        self.map_metamodel_mean = map_metamodel_mean
+        self.map_metamodel_std = map_metamodel_std
+
+        # origModel
+        map_orig_model = self._eval_model(samples=map_theta)
+        self.map_orig_model = map_orig_model
+
+        # Extract slicing index
+        x_values = map_orig_model['x_values']
+
+        # List of markers and colors
+        Color = ['k', 'b', 'g', 'r']
+        Marker = 'x'
+
+        # Create a PdfPages object
+        pdf = PdfPages(f'./{out_dir}MAP_PCE_vs_Model_{self.name}.pdf')
+        fig = plt.figure()
+        for i, key in enumerate(Model.Output.names):
+
+            y_val = map_orig_model[key]
+            y_pce_val = map_metamodel_mean[key]
+            y_pce_val_std = map_metamodel_std[key]
+
+            plt.plot(x_values, y_val, color=Color[i], marker=Marker,
+                     lw=2.0, label='$Y_{MAP}^{M}$')
+
+            plt.plot(
+                x_values, y_pce_val[i], color=Color[i], lw=2.0,
+                marker=Marker, linestyle='--', label='$Y_{MAP}^{PCE}$'
+                )
+            # plot the confidence interval
+            plt.fill_between(
+                x_values, y_pce_val[i] - 1.96*y_pce_val_std[i],
+                y_pce_val[i] + 1.96*y_pce_val_std[i],
+                color=Color[i], alpha=0.15
+                )
+
+            # Calculate the adjusted R_squared and RMSE
+            R2 = r2_score(y_pce_val.reshape(-1, 1), y_val.reshape(-1, 1))
+            rmse = np.sqrt(mean_squared_error(y_pce_val, y_val))
+
+            plt.ylabel(key)
+            plt.xlabel("Time [s]")
+            plt.title(f'Model vs MetaModel {key}')
+
+            ax = fig.axes[0]
+            leg = ax.legend(loc='best', frameon=True)
+            fig.canvas.draw()
+            p = leg.get_window_extent().inverse_transformed(ax.transAxes)
+            ax.text(
+                p.p0[1]-0.05, p.p1[1]-0.25,
+                f'RMSE = {rmse:.3f}\n$R^2$ = {R2:.3f}',
+                transform=ax.transAxes, color='black',
+                bbox=dict(facecolor='none', edgecolor='black',
+                          boxstyle='round,pad=1'))
+
+            plt.show()
+
+            # save the current figure
+            pdf.savefig(fig, bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+
+        pdf.close()
+
+    # -------------------------------------------------------------------------
+    def _plot_post_predictive(self):
+        """
+        Plots the posterior predictives against the observation data.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        Model = self.engine.Model
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        # Plot the posterior predictive
+        for out_idx, out_name in enumerate(Model.Output.names):
+            fig, ax = plt.subplots()
+            with sns.axes_style("ticks"):
+                x_key = list(self.measured_data)[0]
+
+                # --- Read prior and posterior predictive ---
+                if self.inference_method == 'rejection' and \
+                   self.name.lower() != 'valid':
+                    #  --- Prior ---
+                    # Load posterior predictive
+                    f = h5py.File(
+                        f'{out_dir}/priorPredictive.hdf5', 'r+')
+
+                    try:
+                        x_coords = np.array(f[f"x_values/{out_name}"])
+                    except:
+                        x_coords = np.array(f["x_values"])
+
+                    X_values = np.repeat(x_coords, 10000)
+
+                    prior_pred_df = {}
+                    prior_pred_df[x_key] = X_values
+                    prior_pred_df[out_name] = np.array(
+                        f[f"EDY/{out_name}"])[:10000].flatten('F')
+                    prior_pred_df = pd.DataFrame(prior_pred_df)
+
+                    tags_post = ['prior'] * len(prior_pred_df)
+                    prior_pred_df.insert(
+                        len(prior_pred_df.columns), "Tags", tags_post,
+                        True)
+                    f.close()
+
+                    # --- Posterior ---
+                    f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+')
+
+                    X_values = np.repeat(
+                        x_coords, np.array(f[f"EDY/{out_name}"]).shape[0])
+
+                    post_pred_df = {}
+                    post_pred_df[x_key] = X_values
+                    post_pred_df[out_name] = np.array(
+                        f[f"EDY/{out_name}"]).flatten('F')
+
+                    post_pred_df = pd.DataFrame(post_pred_df)
+
+                    tags_post = ['posterior'] * len(post_pred_df)
+                    post_pred_df.insert(
+                        len(post_pred_df.columns), "Tags", tags_post, True)
+                    f.close()
+                    # Concatenate two dataframes based on x_values
+                    frames = [prior_pred_df, post_pred_df]
+                    all_pred_df = pd.concat(frames)
+
+                    # --- Plot posterior predictive ---
+                    sns.violinplot(
+                        x_key, y=out_name, data=all_pred_df, hue="Tags",
+                        legend=False, ax=ax, split=True, inner=None,
+                        color=".8")
+
+                    # --- Plot Data ---
+                    # Find the x,y coordinates for each point
+                    x_coords = np.arange(x_coords.shape[0])
+                    first_header = list(self.measured_data)[0]
+                    obs_data = self.measured_data.round({first_header: 6})
+                    sns.pointplot(
+                        x=first_header, y=out_name, color='g', markers='x',
+                        linestyles='', capsize=16, data=obs_data, ax=ax)
+
+                    ax.errorbar(
+                        x_coords, obs_data[out_name].values,
+                        yerr=1.96*self.measurement_error[out_name],
+                        ecolor='g', fmt=' ', zorder=-1)
+
+                    # Add labels to the legend
+                    handles, labels = ax.get_legend_handles_labels()
+                    labels.append('Data')
+
+                    data_marker = mlines.Line2D(
+                        [], [], color='lime', marker='+', linestyle='None',
+                        markersize=10)
+                    handles.append(data_marker)
+
+                    # Add legend
+                    ax.legend(handles=handles, labels=labels, loc='best',
+                              fontsize='large', frameon=True)
+
+                else:
+                    # Load posterior predictive
+                    f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+')
+
+                    try:
+                        x_coords = np.array(f[f"x_values/{out_name}"])
+                    except:
+                        x_coords = np.array(f["x_values"])
+
+                    mu = np.mean(np.array(f[f"EDY/{out_name}"]), axis=0)
+                    std = np.std(np.array(f[f"EDY/{out_name}"]), axis=0)
+
+                    # --- Plot posterior predictive ---
+                    plt.plot(
+                        x_coords, mu, marker='o', color='b',
+                        label='Mean Post. Predictive')
+                    plt.fill_between(
+                        x_coords, mu-1.96*std, mu+1.96*std, color='b',
+                        alpha=0.15)
+
+                    # --- Plot Data ---
+                    ax.plot(
+                        x_coords, self.measured_data[out_name].values,
+                        'ko', label='data', markeredgecolor='w')
+
+                    # --- Plot ExpDesign ---
+                    orig_ED_Y = self.engine.ExpDesign.Y[out_name]
+                    for output in orig_ED_Y:
+                        plt.plot(
+                            x_coords, output, color='grey', alpha=0.15
+                            )
+
+                    # Add labels for axes
+                    plt.xlabel('Time [s]')
+                    plt.ylabel(out_name)
+
+                    # Add labels to the legend
+                    handles, labels = ax.get_legend_handles_labels()
+
+                    patch = Patch(color='b', alpha=0.15)
+                    handles.insert(1, patch)
+                    labels.insert(1, '95 $\\%$ CI')
+
+                    # Add legend
+                    ax.legend(handles=handles, labels=labels, loc='best',
+                              frameon=True)
+
+                # Save figure in pdf format
+                if self.emulator:
+                    plotname = f'/Post_Prior_Perd_{Model.name}_emulator'
+                else:
+                    plotname = f'/Post_Prior_Perd_{Model.name}'
+
+                fig.savefig(f'./{out_dir}{plotname}_{out_name}.pdf',
+                            bbox_inches='tight')
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/bayes_model_comparison.py b/examples/analytical-function/bayesvalidrox/bayes_inference/bayes_model_comparison.py
new file mode 100644
index 0000000000000000000000000000000000000000..769ad2ceaaced2c1fb6f18d22a9ca27278c3e8a1
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/bayes_inference/bayes_model_comparison.py
@@ -0,0 +1,680 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import emcee
+import numpy as np
+import os
+from scipy import stats
+import seaborn as sns
+import matplotlib.patches as patches
+import matplotlib.colors as mcolors
+import matplotlib.pylab as plt
+from .bayes_inference import BayesInference
+
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class BayesModelComparison:
+    """
+    A class to perform Bayesian Analysis.
+
+
+    Attributes
+    ----------
+    justifiability : bool, optional
+        Whether to perform the justifiability analysis. The default is
+        `True`.
+    perturbed_data : array of shape (n_bootstrap_itrs, n_obs), optional
+        User defined perturbed data. The default is `None`.
+    n_bootstrap : int
+        Number of bootstrap iteration. The default is `1000`.
+    data_noise_level : float
+        A noise level to perturb the data set. The default is `0.01`.
+    just_n_meas : int
+        Number of measurements considered for visualization of the
+        justifiability results.
+
+    """
+
+    def __init__(self, justifiability=True, perturbed_data=None,
+                 n_bootstrap=1000, data_noise_level=0.01, just_n_meas=2):
+
+        # TODO: check valid ranges of the parameters
+        
+        self.justifiability = justifiability
+        self.perturbed_data = perturbed_data
+        self.n_bootstrap = n_bootstrap
+        self.data_noise_level = data_noise_level
+        self.just_n_meas = just_n_meas  # TODO: what is this parameter?
+
+    # --------------------------------------------------------------------------
+    def create_model_comparison(self, model_dict, opts_dict):
+        """
+        Starts the two-stage model comparison.
+        Stage I: Compare models using Bayes factors.
+        Stage II: Compare models via justifiability analysis.
+
+        Parameters
+        ----------
+        model_dict : dict
+            A dictionary including the metamodels.
+        opts_dict : dict
+            A dictionary given the `BayesInference` options.
+
+            Example:
+
+                >>> opts_bootstrap = {
+                    "bootstrap": True,
+                    "n_samples": 10000,
+                    "Discrepancy": DiscrepancyOpts,
+                    "emulator": True,
+                    "plot_post_pred": True
+                    }
+
+        Returns
+        -------
+        output : dict
+            A dictionary containing the objects and the model weights for the
+            comparison using Bayes factors and justifiability analysis.
+
+        """
+        # TODO: why are these two separate calls of the same function?
+        # They should be performable at the same time
+        
+        # Bayes factor
+        bayes_dict_bf, model_weights_dict_bf = self.compare_models(
+            model_dict, opts_dict
+            )
+
+        output = {
+            'Bayes objects BF': bayes_dict_bf,
+            'Model weights BF': model_weights_dict_bf
+            }
+
+        # Justifiability analysis
+        if self.justifiability:
+            bayes_dict_ja, model_weights_dict_ja = self.compare_models(
+                model_dict, opts_dict, justifiability=True
+                )
+            
+            # TODO: why does this version of the call not return a summarized confusion matrix?
+            output['Bayes objects JA'] = bayes_dict_ja
+            output['Model weights JA'] = model_weights_dict_ja
+
+        return output
+
+    # --------------------------------------------------------------------------
+    def compare_models(self, model_dict, opts_dict, justifiability=False):
+        """
+        Passes the options to instantiates the BayesInference class for each
+        model and passes the options from `opts_dict`. Then, it starts the
+        computations.
+        It also creates a folder and saves the diagrams, e.g., Bayes factor
+        plot, confusion matrix, etc.
+
+        Parameters
+        ----------
+        model_dict : dict
+            A dictionary including the metamodels.
+        opts_dict : dict
+            A dictionary given the `BayesInference` options.
+        justifiability : bool, optional
+            Whether to perform the justifiability analysis. The default is
+            `False`.
+
+        Returns
+        -------
+        bayes_dict : dict
+            A dictionary with `BayesInference` objects.
+        model_weights_dict : dict
+            A dictionary containing the model weights.
+
+        """
+
+        if not isinstance(model_dict, dict):
+            raise Exception("To run model comparsion, you need to pass a "
+                            "dictionary of models.")
+
+        # Extract model names
+        self.model_names = [*model_dict]
+
+        # Compute total number of the measurement points
+        # TODO: there could be a different option for this here
+        Engine = list(model_dict.items())[0][1]
+        Engine.Model.read_observation()
+        self.n_meas = Engine.Model.n_obs
+
+        # ----- Generate data -----
+        # Find n_bootstrap
+        if self.perturbed_data is None:
+            n_bootstrap = self.n_bootstrap
+        else:
+            n_bootstrap = self.perturbed_data.shape[0]
+
+        # Create dataset
+        justData = self.generate_dataset(
+            model_dict, justifiability, n_bootstarp=n_bootstrap)
+
+        # Run create Interface for each model
+        self.bayes_dict = {}
+        for model in model_dict.keys():
+            print("-"*20)
+            print("Bayesian inference of {}.\n".format(model))
+
+            BayesOpts = BayesInference(model_dict[model])
+            
+            # Explicitly set the settings of the BayesOpts
+            if self.use_Bayes_settings:
+                BayesOpts.emulator= True
+                BayesOpts.plot_post_pred = True
+                #BayesOpts.inference_method = 'rejection'
+                BayesOpts.bootstrap = True
+                BayesOpts.n_bootstrap_itrs = 10
+                BayesOpts.bootstrap_noise = 0.05
+                
+                # Set the MCMC parameters
+                BayesOpts.inference_method = "MCMC"
+                BayesOpts.mcmc_params = {
+                    'n_steps': 1e3,#5,
+                    'n_walkers': 30,
+                    'moves': emcee.moves.KDEMove(),
+                    'multiprocessing': False,
+                    'verbose': False
+                    }
+
+            # Set BayesInference options
+            for key, value in opts_dict.items():
+                if key in BayesOpts.__dict__.keys():
+                    if key == "Discrepancy" and isinstance(value, dict):
+                        setattr(BayesOpts, key, value[model])
+                    else:
+                        setattr(BayesOpts, key, value)
+
+            # Pass justifiability data as perturbed data
+            BayesOpts.perturbed_data = justData
+            BayesOpts.just_analysis = justifiability
+
+            self.bayes_dict[model] = BayesOpts.create_inference()
+            print("-"*20)
+
+        # Compute model weights
+        self.BME_Dict = dict()
+        for modelName, bayesObj in self.bayes_dict.items():
+            self.BME_Dict[modelName] = np.exp(bayesObj.log_BME, dtype=np.longdouble)#float128)
+
+        # BME correction in BayesInference class
+        self.model_weights = self.cal_model_weight(
+            self.BME_Dict, justifiability, n_bootstarp=n_bootstrap)
+
+        # Plot model weights
+        if justifiability:
+            model_names = self.model_names
+            model_names.insert(0, 'Observation')
+
+            # Split the model weights and save in a dict
+            list_ModelWeights = np.split(
+                self.model_weights, self.model_weights.shape[1]/self.n_meas, axis=1)
+            model_weights_dict = {key: weights for key, weights in
+                                  zip(model_names, list_ModelWeights)}
+
+            #self.plot_just_analysis(model_weights_dict)
+        else:
+            # Create box plot for model weights
+            self.plot_model_weights(self.model_weights, 'model_weights')
+
+            # Create kde plot for bayes factors
+            self.plot_bayes_factor(self.BME_Dict, 'kde_plot')
+
+            # Store model weights in a dict
+            model_weights_dict = {key: weights for key, weights in
+                                  zip(self.model_names, self.model_weights)}
+
+        return self.bayes_dict, model_weights_dict
+
+    # -------------------------------------------------------------------------
+    def generate_dataset(self, model_dict, justifiability=False,
+                         n_bootstrap=1):
+        """
+        Generates the perturbed data set for the Bayes factor calculations and
+        the data set for the justifiability analysis.
+
+        Parameters
+        ----------
+        model_dict : dict
+            A dictionary including the metamodels.
+        bool, optional
+            Whether to perform the justifiability analysis. The default is
+            `False`.
+        n_bootstrap : int, optional
+            Number of bootstrap iterations. The default is `1`.
+
+        Returns
+        -------
+        all_just_data: array
+            Created data set.
+
+        """
+        # Compute some variables
+        all_just_data = []
+        Engine = list(model_dict.items())[0][1]
+        out_names = Engine.Model.Output.names
+
+        # Perturb observations for Bayes Factor
+        if self.perturbed_data is None:
+            self.perturbed_data = self.__perturb_data(
+                    Engine.Model.observations, out_names, n_bootstrap,
+                    noise_level=self.data_noise_level)
+
+        # Only for Bayes Factor
+        if not justifiability:
+            return self.perturbed_data
+
+        # Evaluate metamodel
+        runs = {}
+        for key, metaModel in model_dict.items():
+            y_hat, _ = metaModel.eval_metamodel(nsamples=n_bootstrap)
+            runs[key] = y_hat
+
+        # Generate data
+        for i in range(n_bootstrap):
+            y_data = self.perturbed_data[i].reshape(1, -1)
+            justData = np.tril(np.repeat(y_data, y_data.shape[1], axis=0))
+            # Use surrogate runs for data-generating process
+            for key, metaModel in model_dict.items():
+                model_data = np.array(
+                    [runs[key][out][i] for out in out_names]).reshape(y_data.shape)
+                justData = np.vstack((
+                    justData,
+                    np.tril(np.repeat(model_data, model_data.shape[1], axis=0))
+                    ))
+            # Save in a list
+            all_just_data.append(justData)
+
+        # Squeeze the array
+        all_just_data = np.array(all_just_data).transpose(1, 0, 2).reshape(
+            -1, np.array(all_just_data).shape[2]
+            )
+
+        return all_just_data
+
+    # -------------------------------------------------------------------------
+    def __perturb_data(self, data, output_names, n_bootstrap, noise_level):
+        """
+        Returns an array with n_bootstrap_itrs rowsof perturbed data.
+        The first row includes the original observation data.
+        If `self.bayes_loocv` is True, a 2d-array will be returned with
+        repeated rows and zero diagonal entries.
+
+        Parameters
+        ----------
+        data : pandas DataFrame
+            Observation data.
+        output_names : list
+            List of the output names.
+
+        Returns
+        -------
+        final_data : array
+            Perturbed data set.
+
+        """
+        obs_data = data[output_names].values
+        n_measurement, n_outs = obs_data.shape
+        n_tot_measurement = obs_data[~np.isnan(obs_data)].shape[0]
+        final_data = np.zeros(
+            (n_bootstrap, n_tot_measurement)
+            )
+        final_data[0] = obs_data.T[~np.isnan(obs_data.T)]
+        for itrIdx in range(1, n_bootstrap):
+            data = np.zeros((n_measurement, n_outs))
+            for idx in range(len(output_names)):
+                std = np.nanstd(obs_data[:, idx])
+                if std == 0:
+                    std = 0.001
+                noise = std * noise_level
+                data[:, idx] = np.add(
+                    obs_data[:, idx],
+                    np.random.normal(0, 1, obs_data.shape[0]) * noise,
+                )
+
+            final_data[itrIdx] = data.T[~np.isnan(data.T)]
+
+        return final_data
+
+    # -------------------------------------------------------------------------
+    def cal_model_weight(self, BME_Dict, justifiability=False, n_bootstrap=1):
+        """
+        Normalize the BME (Asumption: Model Prior weights are equal for models)
+
+        Parameters
+        ----------
+        BME_Dict : dict
+            A dictionary containing the BME values.
+
+        Returns
+        -------
+        model_weights : array
+            Model weights.
+
+        """
+        # Stack the BME values for all models
+        all_BME = np.vstack(list(BME_Dict.values()))
+
+        if justifiability:
+            # Compute expected log_BME for justifiabiliy analysis
+            all_BME = all_BME.reshape(
+                all_BME.shape[0], -1, n_bootstrap).mean(axis=2)
+
+        # Model weights
+        model_weights = np.divide(all_BME, np.nansum(all_BME, axis=0))
+
+        return model_weights
+
+    # -------------------------------------------------------------------------
+    def plot_just_analysis(self, model_weights_dict):
+        """
+        Visualizes the confusion matrix and the model wights for the
+        justifiability analysis.
+
+        Parameters
+        ----------
+        model_weights_dict : dict
+            Model weights.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        directory = 'Outputs_Comparison/'
+        os.makedirs(directory, exist_ok=True)
+        Color = [*mcolors.TABLEAU_COLORS]
+        names = [*model_weights_dict]
+
+        model_names = [model.replace('_', '$-$') for model in self.model_names]
+        for name in names:
+            fig, ax = plt.subplots()
+            for i, model in enumerate(model_names[1:]):
+                plt.plot(list(range(1, self.n_meas+1)),
+                         model_weights_dict[name][i],
+                         color=Color[i], marker='o',
+                         ms=10, linewidth=2, label=model
+                         )
+
+            plt.title(f"Data generated by: {name.replace('_', '$-$')}")
+            plt.ylabel("Weights")
+            plt.xlabel("No. of measurement points")
+            ax.set_xticks(list(range(1, self.n_meas+1)))
+            plt.legend(loc="best")
+            fig.savefig(
+                f'{directory}modelWeights_{name}.svg', bbox_inches='tight'
+                )
+            plt.close()
+
+        # Confusion matrix for some measurement points
+        epsilon = 1 if self.just_n_meas != 1 else 0
+        for index in range(0, self.n_meas+epsilon, self.just_n_meas):
+            weights = np.array(
+                [model_weights_dict[key][:, index] for key in model_weights_dict]
+                )
+            g = sns.heatmap(
+                weights.T, annot=True, cmap='Blues', xticklabels=model_names,
+                yticklabels=model_names[1:], annot_kws={"size": 24}
+                )
+
+            # x axis on top
+            g.xaxis.tick_top()
+            g.xaxis.set_label_position('top')
+            g.set_xlabel(r"\textbf{Data generated by:}", labelpad=15)
+            g.set_ylabel(r"\textbf{Model weight for:}", labelpad=15)
+            g.figure.savefig(
+                f"{directory}confusionMatrix_ND_{index+1}.pdf",
+                bbox_inches='tight'
+                )
+            plt.close()
+
+    # -------------------------------------------------------------------------
+    def plot_model_weights(self, model_weights, plot_name):
+        """
+        Visualizes the model weights resulting from BMS via the observation
+        data.
+
+        Parameters
+        ----------
+        model_weights : array
+            Model weights.
+        plot_name : str
+            Plot name.
+
+        Returns
+        -------
+        None.
+
+        """
+        font_size = 40
+        # mkdir for plots
+        directory = 'Outputs_Comparison/'
+        os.makedirs(directory, exist_ok=True)
+
+        # Create figure
+        fig, ax = plt.subplots()
+
+        # Filter data using np.isnan
+        mask = ~np.isnan(model_weights.T)
+        filtered_data = [d[m] for d, m in zip(model_weights, mask.T)]
+
+        # Create the boxplot
+        bp = ax.boxplot(filtered_data, patch_artist=True, showfliers=False)
+
+        # change outline color, fill color and linewidth of the boxes
+        for box in bp['boxes']:
+            # change outline color
+            box.set(color='#7570b3', linewidth=4)
+            # change fill color
+            box.set(facecolor='#1b9e77')
+
+        # change color and linewidth of the whiskers
+        for whisker in bp['whiskers']:
+            whisker.set(color='#7570b3', linewidth=2)
+
+        # change color and linewidth of the caps
+        for cap in bp['caps']:
+            cap.set(color='#7570b3', linewidth=2)
+
+        # change color and linewidth of the medians
+        for median in bp['medians']:
+            median.set(color='#b2df8a', linewidth=2)
+
+        # change the style of fliers and their fill
+        # for flier in bp['fliers']:
+        #     flier.set(marker='o', color='#e7298a', alpha=0.75)
+
+        # Custom x-axis labels
+        model_names = [model.replace('_', '$-$') for model in self.model_names]
+        ax.set_xticklabels(model_names)
+
+        ax.set_ylabel('Weight', fontsize=font_size)
+
+        # Title
+        plt.title('Posterior Model Weights')
+
+        # Set y lim
+        ax.set_ylim((-0.05, 1.05))
+
+        # Set size of the ticks
+        for t in ax.get_xticklabels():
+            t.set_fontsize(font_size)
+        for t in ax.get_yticklabels():
+            t.set_fontsize(font_size)
+
+        # Save the figure
+        fig.savefig(
+            f'./{directory}{plot_name}.pdf', bbox_inches='tight'
+            )
+
+        plt.close()
+
+    # -------------------------------------------------------------------------
+    def plot_bayes_factor(self, BME_Dict, plot_name=''):
+        """
+        Plots the Bayes factor distibutions in a :math:`N_m \\times N_m`
+        matrix, where :math:`N_m` is the number of the models.
+
+        Parameters
+        ----------
+        BME_Dict : dict
+            A dictionary containing the BME values of the models.
+        plot_name : str, optional
+            Plot name. The default is ''.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        font_size = 40
+
+        # mkdir for plots
+        directory = 'Outputs_Comparison/'
+        os.makedirs(directory, exist_ok=True)
+
+        Colors = ["blue", "green", "gray", "brown"]
+
+        model_names = list(BME_Dict.keys())
+        nModels = len(model_names)
+
+        # Plots
+        fig, axes = plt.subplots(
+            nrows=nModels, ncols=nModels, sharex=True, sharey=True
+            )
+
+        for i, key_i in enumerate(model_names):
+
+            for j, key_j in enumerate(model_names):
+                ax = axes[i, j]
+                # Set size of the ticks
+                for t in ax.get_xticklabels():
+                    t.set_fontsize(font_size)
+                for t in ax.get_yticklabels():
+                    t.set_fontsize(font_size)
+
+                if j != i:
+
+                    # Null hypothesis: key_j is the better model
+                    BayesFactor = np.log10(
+                        np.divide(BME_Dict[key_i], BME_Dict[key_j])
+                        )
+
+                    # sns.kdeplot(BayesFactor, ax=ax, color=Colors[i], shade=True)
+                    # sns.histplot(BayesFactor, ax=ax, stat="probability",
+                    #              kde=True, element='step',
+                    #              color=Colors[j])
+
+                    # taken from seaborn's source code (utils.py and
+                    # distributions.py)
+                    def seaborn_kde_support(data, bw, gridsize, cut, clip):
+                        if clip is None:
+                            clip = (-np.inf, np.inf)
+                        support_min = max(data.min() - bw * cut, clip[0])
+                        support_max = min(data.max() + bw * cut, clip[1])
+                        return np.linspace(support_min, support_max, gridsize)
+
+                    kde_estim = stats.gaussian_kde(
+                        BayesFactor, bw_method='scott'
+                        )
+
+                    # manual linearization of data
+                    # linearized = np.linspace(
+                    #     quotient.min(), quotient.max(), num=500)
+
+                    # or better: mimic seaborn's internal stuff
+                    bw = kde_estim.scotts_factor() * np.std(BayesFactor)
+                    linearized = seaborn_kde_support(
+                        BayesFactor, bw, 100, 3, None)
+
+                    # computes values of the estimated function on the
+                    # estimated linearized inputs
+                    Z = kde_estim.evaluate(linearized)
+
+                    # https://stackoverflow.com/questions/29661574/normalize-
+                    # numpy-array-columns-in-python
+                    def normalize(x):
+                        return (x - x.min(0)) / x.ptp(0)
+
+                    # normalize so it is between 0;1
+                    Z2 = normalize(Z)
+                    ax.plot(linearized, Z2, "-", color=Colors[i], linewidth=4)
+                    ax.fill_between(
+                        linearized, 0, Z2, color=Colors[i], alpha=0.25
+                        )
+
+                    # Draw BF significant levels according to Jeffreys 1961
+                    # Strong evidence for both models
+                    ax.axvline(
+                        x=np.log10(3), ymin=0, linewidth=4, color='dimgrey'
+                        )
+                    # Strong evidence for one model
+                    ax.axvline(
+                        x=np.log10(10), ymin=0, linewidth=4, color='orange'
+                        )
+                    # Decisive evidence for one model
+                    ax.axvline(
+                        x=np.log10(100), ymin=0, linewidth=4, color='r'
+                        )
+
+                    # legend
+                    BF_label = key_i.replace('_', '$-$') + \
+                        '/' + key_j.replace('_', '$-$')
+                    legend_elements = [
+                        patches.Patch(facecolor=Colors[i], edgecolor=Colors[i],
+                                      label=f'BF({BF_label})')
+                        ]
+                    ax.legend(
+                        loc='upper left', handles=legend_elements,
+                        fontsize=font_size-(nModels+1)*5
+                        )
+
+                elif j == i:
+                    # build a rectangle in axes coords
+                    left, width = 0, 1
+                    bottom, height = 0, 1
+
+                    # axes coordinates are 0,0 is bottom left and 1,1 is upper
+                    # right
+                    p = patches.Rectangle(
+                        (left, bottom), width, height, color='white',
+                        fill=True, transform=ax.transAxes, clip_on=False
+                        )
+                    ax.grid(False)
+                    ax.add_patch(p)
+                    # ax.text(0.5*(left+right), 0.5*(bottom+top), key_i,
+                    fsize = font_size+20 if nModels < 4 else font_size
+                    ax.text(0.5, 0.5, key_i.replace('_', '$-$'),
+                            horizontalalignment='center',
+                            verticalalignment='center',
+                            fontsize=fsize, color=Colors[i],
+                            transform=ax.transAxes)
+
+        # Defining custom 'ylim' values.
+        custom_ylim = (0, 1.05)
+
+        # Setting the values for all axes.
+        plt.setp(axes, ylim=custom_ylim)
+
+        # set labels
+        for i in range(nModels):
+            axes[-1, i].set_xlabel('log$_{10}$(BF)', fontsize=font_size)
+            axes[i, 0].set_ylabel('Probability', fontsize=font_size)
+
+        # Adjust subplots
+        plt.subplots_adjust(wspace=0.2, hspace=0.1)
+
+        plt.savefig(
+            f'./{directory}Bayes_Factor{plot_name}.pdf', bbox_inches='tight'
+            )
+
+        plt.close()
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/discrepancy.py b/examples/analytical-function/bayesvalidrox/bayes_inference/discrepancy.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3c235ebeb6d6ae9e109ca862cc522cc21efb45e
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/bayes_inference/discrepancy.py
@@ -0,0 +1,118 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import scipy.stats as stats
+from bayesvalidrox.surrogate_models.exp_designs import ExpDesigns
+
+
+class Discrepancy:
+    """
+    Discrepancy class for Bayesian inference method.
+    We define the reference or reality to be equal to what we can model and a
+    descripancy term \\( \\epsilon \\). We consider the followin format:
+
+    $$\\textbf{y}_{\\text{reality}} = \\mathcal{M}(\\theta) + \\epsilon,$$
+
+    where \\( \\epsilon \\in R^{N_{out}} \\) represents the the effects of
+    measurement error and model inaccuracy. For simplicity, it can be defined
+    as an additive Gaussian disrepancy with zeromean and given covariance
+    matrix \\( \\Sigma \\):
+
+    $$\\epsilon \\sim \\mathcal{N}(\\epsilon|0, \\Sigma). $$
+
+    In the context of model inversion or calibration, an observation point
+    \\( \\textbf{y}_i \\in \\mathcal{y} \\) is a realization of a Gaussian
+    distribution with mean value of \\(\\mathcal{M}(\\theta) \\) and covariance
+    matrix of \\( \\Sigma \\).
+
+    $$ p(\\textbf{y}|\\theta) = \\mathcal{N}(\\textbf{y}|\\mathcal{M}
+                                             (\\theta))$$
+
+    The following options are available:
+
+    * Option A: With known redidual covariance matrix \\(\\Sigma\\) for
+    independent measurements.
+
+    * Option B: With unknown redidual covariance matrix \\(\\Sigma\\),
+    paramethrized as \\(\\Sigma(\\theta_{\\epsilon})=\\sigma^2 \\textbf{I}_
+    {N_{out}}\\) with unknown residual variances \\(\\sigma^2\\).
+    This term will be jointly infered with the uncertain input parameters. For
+    the inversion, you need to define a prior marginal via `Input` class. Note
+    that \\(\\sigma^2\\) is only a single scalar multiplier for the diagonal
+    entries of the covariance matrix \\(\\Sigma\\).
+
+    Attributes
+    ----------
+    InputDisc : obj
+        Input object. When the \\(\\sigma^2\\) is expected to be inferred
+        jointly with the parameters (`Option B`).If multiple output groups are
+        defined by `Model.Output.names`, each model output needs to have.
+        a prior marginal using the `Input` class. The default is `''`.
+    disc_type : str
+        Type of the noise definition. `'Gaussian'` is only supported so far.
+    parameters : dict or pandas.DataFrame
+        Known residual variance \\(\\sigma^2\\), i.e. diagonal entry of the
+        covariance matrix of the multivariate normal likelihood in case of
+        `Option A`.
+
+    """
+
+    def __init__(self, InputDisc='', disc_type='Gaussian', parameters=None):
+        # Set the values
+        self.InputDisc = InputDisc
+        self.disc_type = disc_type
+        self.parameters = parameters
+        
+        # Other inits
+        self.ExpDesign = None
+        self.n_samples = None
+        self.sigma2_prior = None
+        self.name = None
+        self.opt_sigma = None # This will be set in the inference class and used in mcmc
+    # -------------------------------------------------------------------------
+    def get_sample(self, n_samples):
+        """
+        Generate samples for the \\(\\sigma^2\\), i.e. the diagonal entries of
+        the variance-covariance matrix in the multivariate normal distribution.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of samples (parameter sets).
+
+        Returns
+        -------
+        sigma2_prior: array of shape (n_samples, n_params)
+            \\(\\sigma^2\\) samples.
+
+        """
+        self.n_samples = n_samples # TODO: not used again in here - needed from the outside?
+        
+        if self.InputDisc == '':
+            raise AttributeError('Cannot create new samples, please provide input distributions')
+        
+        # Create and store BoundTuples
+        self.ExpDesign = ExpDesigns(self.InputDisc)
+        self.ExpDesign.sampling_method = 'random'
+        
+        # TODO: why does it call 'generate_ED' instead of 'generate_samples?
+        # ExpDesign.bound_tuples, onp_sigma, prior_space needed from the outside
+        # Discrepancy opt_sigma, InputDisc needed from the outside
+        # TODO: opt_sigma not defined here, but called from the outside??
+        self.ExpDesign.generate_ED(
+            n_samples, max_pce_deg=1
+            )
+        # TODO: need to recheck the following line
+        # This used to simply be the return from the call above
+        self.sigma2_prior = self.ExpDesign.X
+
+        # Naive approach: Fit a gaussian kernel to the provided data
+        self.ExpDesign.JDist = stats.gaussian_kde(self.ExpDesign.raw_data)
+
+        # Save the names of sigmas
+        if len(self.InputDisc.Marginals) != 0:
+            self.name = []
+            for Marginalidx in range(len(self.InputDisc.Marginals)):
+                self.name.append(self.InputDisc.Marginals[Marginalidx].name)
+
+        return self.sigma2_prior
diff --git a/examples/analytical-function/bayesvalidrox/bayes_inference/mcmc.py b/examples/analytical-function/bayesvalidrox/bayes_inference/mcmc.py
new file mode 100644
index 0000000000000000000000000000000000000000..d78d15b5fd90dc4477da7d0fd58da835acc75310
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/bayes_inference/mcmc.py
@@ -0,0 +1,909 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import os
+import numpy as np
+import emcee
+import pandas as pd
+import matplotlib.pyplot as plt
+from matplotlib.backends.backend_pdf import PdfPages
+import multiprocessing
+import scipy.stats as st
+from scipy.linalg import cholesky as chol
+import warnings
+import shutil
+os.environ["OMP_NUM_THREADS"] = "1"
+
+
+class MCMC:
+    """
+    A class for bayesian inference via a Markov-Chain Monte-Carlo (MCMC)
+    Sampler to approximate the posterior distribution of the Bayes theorem:
+    $$p(\\theta|\\mathcal{y}) = \\frac{p(\\mathcal{y}|\\theta) p(\\theta)}
+                                         {p(\\mathcal{y})}.$$
+
+    This class make inference with emcee package [1] using an Affine Invariant
+    Ensemble sampler (AIES) [2].
+
+    [1] Foreman-Mackey, D., Hogg, D.W., Lang, D. and Goodman, J., 2013.emcee:
+        the MCMC hammer. Publications of the Astronomical Society of the
+        Pacific, 125(925), p.306. https://emcee.readthedocs.io/en/stable/
+
+    [2] Goodman, J. and Weare, J., 2010. Ensemble samplers with affine
+        invariance. Communications in applied mathematics and computational
+        science, 5(1), pp.65-80.
+
+
+    Attributes
+    ----------
+    BayesOpts : obj
+        Bayes object.
+    """
+
+    def __init__(self, BayesOpts):
+
+        self.BayesOpts = BayesOpts
+
+    def run_sampler(self, observation, total_sigma2):
+
+        BayesObj = self.BayesOpts
+        MetaModel = BayesObj.engine.MetaModel
+        Model = BayesObj.engine.Model
+        Discrepancy = self.BayesOpts.Discrepancy
+        n_cpus = Model.n_cpus
+        priorDist = BayesObj.engine.ExpDesign.JDist
+        ndim = MetaModel.n_params
+        self.counter = 0
+        output_dir = f'Outputs_Bayes_{Model.name}_{self.BayesOpts.name}'
+        if not os.path.exists(output_dir):
+            os.makedirs(output_dir)
+
+        self.observation = observation
+        self.total_sigma2 = total_sigma2
+
+        # Unpack mcmc parameters given to BayesObj.mcmc_params
+        self.initsamples = None
+        self.nwalkers = 100
+        self.nburn = 200
+        self.nsteps = 100000
+        self.moves = None
+        self.mp = False
+        self.verbose = False
+
+        # Extract initial samples
+        if 'init_samples' in BayesObj.mcmc_params:
+            self.initsamples = BayesObj.mcmc_params['init_samples']
+            if isinstance(self.initsamples, pd.DataFrame):
+                self.initsamples = self.initsamples.values
+
+        # Extract number of steps per walker
+        if 'n_steps' in BayesObj.mcmc_params:
+            self.nsteps = int(BayesObj.mcmc_params['n_steps'])
+        # Extract number of walkers (chains)
+        if 'n_walkers' in BayesObj.mcmc_params:
+            self.nwalkers = int(BayesObj.mcmc_params['n_walkers'])
+        # Extract moves
+        if 'moves' in BayesObj.mcmc_params:
+            self.moves = BayesObj.mcmc_params['moves']
+        # Extract multiprocessing
+        if 'multiprocessing' in BayesObj.mcmc_params:
+            self.mp = BayesObj.mcmc_params['multiprocessing']
+        # Extract verbose
+        if 'verbose' in BayesObj.mcmc_params:
+            self.verbose = BayesObj.mcmc_params['verbose']
+
+        # Set initial samples
+        np.random.seed(0)
+        if self.initsamples is None:
+            try:
+                initsamples = priorDist.sample(self.nwalkers).T
+            except:
+                # when aPCE selected - gaussian kernel distribution
+                inputSamples = self.BayesOpts.engine.ExpDesign.raw_data.T
+                random_indices = np.random.choice(
+                    len(inputSamples), size=self.nwalkers, replace=False
+                    )
+                initsamples = inputSamples[random_indices]
+
+        else:
+            if self.initsamples.ndim == 1:
+                # When MAL is given.
+                theta = self.initsamples
+                initsamples = [theta + 1e-1*np.multiply(
+                    np.random.randn(ndim), theta) for i in
+                               range(self.nwalkers)]
+            else:
+                # Pick samples based on a uniform dist between min and max of
+                # each dim
+                initsamples = np.zeros((self.nwalkers, ndim))
+                bound_tuples = []
+                for idx_dim in range(ndim):
+                    lower = np.min(self.initsamples[:, idx_dim])
+                    upper = np.max(self.initsamples[:, idx_dim])
+                    bound_tuples.append((lower, upper))
+                    dist = st.uniform(loc=lower, scale=upper-lower)
+                    initsamples[:, idx_dim] = dist.rvs(size=self.nwalkers)
+
+                # Update lower and upper
+                MetaModel.ExpDesign.bound_tuples = bound_tuples
+
+        # Check if sigma^2 needs to be inferred
+        if Discrepancy.opt_sigma != 'B':
+            sigma2_samples = Discrepancy.get_sample(self.nwalkers)
+
+            # Update initsamples
+            initsamples = np.hstack((initsamples, sigma2_samples))
+
+            # Update ndim
+            ndim = initsamples.shape[1]
+
+            # Discrepancy bound
+            disc_bound_tuple = Discrepancy.ExpDesign.bound_tuples
+
+            # Update bound_tuples
+            BayesObj.engine.ExpDesign.bound_tuples += disc_bound_tuple
+
+        print("\n>>>> Bayesian inference with MCMC for "
+              f"{self.BayesOpts.name} started. <<<<<<")
+
+        # Set up the backend
+        filename = f"{output_dir}/emcee_sampler.h5"
+        backend = emcee.backends.HDFBackend(filename)
+        # Clear the backend in case the file already exists
+        backend.reset(self.nwalkers, ndim)
+
+        # Define emcee sampler
+        # Here we'll set up the computation. emcee combines multiple "walkers",
+        # each of which is its own MCMC chain. The number of trace results will
+        # be nwalkers * nsteps.
+        if self.mp:
+            # Run in parallel
+            if n_cpus is None:
+                n_cpus = multiprocessing.cpu_count()
+
+            with multiprocessing.Pool(n_cpus) as pool:
+                sampler = emcee.EnsembleSampler(
+                    self.nwalkers, ndim, self.log_posterior, moves=self.moves,
+                    pool=pool, backend=backend
+                    )
+
+                # Check if a burn-in phase is needed!
+                if self.initsamples is None:
+                    # Burn-in
+                    print("\n Burn-in period is starting:")
+                    pos = sampler.run_mcmc(
+                        initsamples, self.nburn, progress=True
+                        )
+
+                    # Reset sampler
+                    sampler.reset()
+                    pos = pos.coords
+                else:
+                    pos = initsamples
+
+                # Production run
+                print("\n Production run is starting:")
+                pos, prob, state = sampler.run_mcmc(
+                    pos, self.nsteps, progress=True
+                    )
+
+        else:
+            # Run in series and monitor the convergence
+            sampler = emcee.EnsembleSampler(
+                self.nwalkers, ndim, self.log_posterior, moves=self.moves,
+                backend=backend, vectorize=True
+                )
+
+            # Check if a burn-in phase is needed!
+            if self.initsamples is None:
+                # Burn-in
+                print("\n Burn-in period is starting:")
+                pos = sampler.run_mcmc(
+                    initsamples, self.nburn, progress=True
+                    )
+
+                # Reset sampler
+                sampler.reset()
+                pos = pos.coords
+            else:
+                pos = initsamples
+
+            # Production run
+            print("\n Production run is starting:")
+
+            # Track how the average autocorrelation time estimate changes
+            autocorrIdx = 0
+            autocorr = np.empty(self.nsteps)
+            tauold = np.inf
+            autocorreverynsteps = 50
+
+            # sample step by step using the generator sampler.sample
+            for sample in sampler.sample(pos,
+                                         iterations=self.nsteps,
+                                         tune=True,
+                                         progress=True):
+
+                # only check convergence every autocorreverynsteps steps
+                if sampler.iteration % autocorreverynsteps:
+                    continue
+
+                # Train model discrepancy/error
+                if hasattr(BayesObj, 'errorModel') and BayesObj.errorModel \
+                   and not sampler.iteration % 3 * autocorreverynsteps:
+                    try:
+                        self.error_MetaModel = self.train_error_model(sampler)
+                    except:
+                        pass
+
+                # Print the current mean acceptance fraction
+                if self.verbose:
+                    print("\nStep: {}".format(sampler.iteration))
+                    acc_fr = np.mean(sampler.acceptance_fraction)
+                    print(f"Mean acceptance fraction: {acc_fr:.3f}")
+
+                # compute the autocorrelation time so far
+                # using tol=0 means that we'll always get an estimate even if
+                # it isn't trustworthy
+                tau = sampler.get_autocorr_time(tol=0)
+                # average over walkers
+                autocorr[autocorrIdx] = np.nanmean(tau)
+                autocorrIdx += 1
+
+                # output current autocorrelation estimate
+                if self.verbose:
+                    print(f"Mean autocorr. time estimate: {np.nanmean(tau):.3f}")
+                    list_gr = np.round(self.gelman_rubin(sampler.chain), 3)
+                    print("Gelman-Rubin Test*: ", list_gr)
+
+                # check convergence
+                converged = np.all(tau*autocorreverynsteps < sampler.iteration)
+                converged &= np.all(np.abs(tauold - tau) / tau < 0.01)
+                converged &= np.all(self.gelman_rubin(sampler.chain) < 1.1)
+
+                if converged:
+                    break
+                tauold = tau
+
+        # Posterior diagnostics
+        try:
+            tau = sampler.get_autocorr_time(tol=0)
+        except emcee.autocorr.AutocorrError:
+            tau = 5
+
+        if all(np.isnan(tau)):
+            tau = 5
+
+        burnin = int(2*np.nanmax(tau))
+        thin = int(0.5*np.nanmin(tau)) if int(0.5*np.nanmin(tau)) != 0 else 1
+        finalsamples = sampler.get_chain(discard=burnin, flat=True, thin=thin)
+        acc_fr = np.nanmean(sampler.acceptance_fraction)
+        list_gr = np.round(self.gelman_rubin(sampler.chain[:, burnin:]), 3)
+
+        # Print summary
+        print('\n')
+        print('-'*15 + 'Posterior diagnostics' + '-'*15)
+        print(f"Mean auto-correlation time: {np.nanmean(tau):.3f}")
+        print(f"Thin: {thin}")
+        print(f"Burn-in: {burnin}")
+        print(f"Flat chain shape: {finalsamples.shape}")
+        print(f"Mean acceptance fraction*: {acc_fr:.3f}")
+        print("Gelman-Rubin Test**: ", list_gr)
+
+        print("\n* This value must lay between 0.234 and 0.5.")
+        print("** These values must be smaller than 1.1.")
+        print('-'*50)
+
+        print(f"\n>>>> Bayesian inference with MCMC for {self.BayesOpts.name} "
+              "successfully completed. <<<<<<\n")
+
+        # Extract parameter names and their prior ranges
+        par_names = self.BayesOpts.engine.ExpDesign.par_names
+
+        if Discrepancy.opt_sigma != 'B':
+            for i in range(len(Discrepancy.InputDisc.Marginals)):
+                par_names.append(Discrepancy.InputDisc.Marginals[i].name)
+
+        params_range = self.BayesOpts.engine.ExpDesign.bound_tuples
+
+        # Plot traces
+        if self.verbose and self.nsteps < 10000:
+            pdf = PdfPages(output_dir+'/traceplots.pdf')
+            fig = plt.figure()
+            for parIdx in range(ndim):
+                # Set up the axes with gridspec
+                fig = plt.figure()
+                grid = plt.GridSpec(4, 4, hspace=0.2, wspace=0.2)
+                main_ax = fig.add_subplot(grid[:-1, :3])
+                y_hist = fig.add_subplot(grid[:-1, -1], xticklabels=[],
+                                         sharey=main_ax)
+
+                for i in range(self.nwalkers):
+                    samples = sampler.chain[i, :, parIdx]
+                    main_ax.plot(samples, '-')
+
+                    # histogram on the attached axes
+                    y_hist.hist(samples[burnin:], 40, histtype='stepfilled',
+                                orientation='horizontal', color='gray')
+
+                main_ax.set_ylim(params_range[parIdx])
+                main_ax.set_title('traceplot for ' + par_names[parIdx])
+                main_ax.set_xlabel('step number')
+
+                # save the current figure
+                pdf.savefig(fig, bbox_inches='tight')
+
+                # Destroy the current plot
+                plt.clf()
+
+            pdf.close()
+
+        # plot development of autocorrelation estimate
+        if not self.mp:
+            fig1 = plt.figure()
+            steps = autocorreverynsteps*np.arange(1, autocorrIdx+1)
+            taus = autocorr[:autocorrIdx]
+            plt.plot(steps, steps / autocorreverynsteps, "--k")
+            plt.plot(steps, taus)
+            plt.xlim(0, steps.max())
+            plt.ylim(0, np.nanmax(taus)+0.1*(np.nanmax(taus)-np.nanmin(taus)))
+            plt.xlabel("number of steps")
+            plt.ylabel(r"mean $\hat{\tau}$")
+            fig1.savefig(f"{output_dir}/autocorrelation_time.pdf",
+                         bbox_inches='tight')
+
+        # logml_dict = self.marginal_llk_emcee(sampler, self.nburn, logp=None,
+        # maxiter=5000)
+        # print('\nThe Bridge Sampling Estimation is "
+        #       f"{logml_dict['logml']:.5f}.')
+
+        # # Posterior-based expectation of posterior probablity
+        # postExpPostLikelihoods = np.mean(sampler.get_log_prob(flat=True)
+        # [self.nburn*self.nwalkers:])
+
+        # # Posterior-based expectation of prior densities
+        # postExpPrior = np.mean(self.log_prior(emcee_trace.T))
+
+        # # Posterior-based expectation of likelihoods
+        # postExpLikelihoods_emcee = postExpPostLikelihoods - postExpPrior
+
+        # # Calculate Kullback-Leibler Divergence
+        # KLD_emcee = postExpLikelihoods_emcee - logml_dict['logml']
+        # print("Kullback-Leibler divergence: %.5f"%KLD_emcee)
+
+        # # Information Entropy based on Entropy paper Eq. 38
+        # infEntropy_emcee = logml_dict['logml'] - postExpPrior -
+        #                    postExpLikelihoods_emcee
+        # print("Information Entropy: %.5f" %infEntropy_emcee)
+
+        Posterior_df = pd.DataFrame(finalsamples, columns=par_names)
+
+        return Posterior_df
+
+    # -------------------------------------------------------------------------
+    def log_prior(self, theta):
+        """
+        Calculates the log prior likelihood \\( p(\\theta)\\) for the given
+        parameter set(s) \\( \\theta \\).
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter sets, i.e. proposals of MCMC chains.
+
+        Returns
+        -------
+        logprior: float or array of shape n_samples
+            Log prior likelihood. If theta has only one row, a single value is
+            returned otherwise an array.
+
+        """
+
+        MetaModel = self.BayesOpts.MetaModel
+        Discrepancy = self.BayesOpts.Discrepancy
+
+        # Find the number of sigma2 parameters
+        if Discrepancy.opt_sigma != 'B':
+            disc_bound_tuples = Discrepancy.ExpDesign.bound_tuples
+            disc_marginals = Discrepancy.ExpDesign.InputObj.Marginals
+            disc_prior_space = Discrepancy.ExpDesign.prior_space
+            n_sigma2 = len(disc_bound_tuples)
+        else:
+            n_sigma2 = -len(theta)
+        prior_dist = self.BayesOpts.engine.ExpDesign.prior_space
+        params_range = self.BayesOpts.engine.ExpDesign.bound_tuples
+        theta = theta if theta.ndim != 1 else theta.reshape((1, -1))
+        nsamples = theta.shape[0]
+        logprior = -np.inf*np.ones(nsamples)
+
+        for i in range(nsamples):
+            # Check if the sample is within the parameters' range
+            if self._check_ranges(theta[i], params_range):
+                # Check if all dists are uniform, if yes priors are equal.
+                if all(MetaModel.input_obj.Marginals[i].dist_type == 'uniform'
+                       for i in range(MetaModel.n_params)):
+                    logprior[i] = 0.0
+                else:
+                    logprior[i] = np.log(
+                        prior_dist.pdf(theta[i, :-n_sigma2].T)
+                        )
+
+                # Check if bias term needs to be inferred
+                if Discrepancy.opt_sigma != 'B':
+                    if self._check_ranges(theta[i, -n_sigma2:],
+                                          disc_bound_tuples):
+                        if all('unif' in disc_marginals[i].dist_type for i in
+                               range(Discrepancy.ExpDesign.ndim)):
+                            logprior[i] = 0.0
+                        else:
+                            logprior[i] += np.log(
+                                disc_prior_space.pdf(theta[i, -n_sigma2:])
+                                )
+
+        if nsamples == 1:
+            return logprior[0]
+        else:
+            return logprior
+
+    # -------------------------------------------------------------------------
+    def log_likelihood(self, theta):
+        """
+        Computes likelihood \\( p(\\mathcal{Y}|\\theta)\\) of the performance
+        of the (meta-)model in reproducing the observation data.
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter set, i.e. proposals of the MCMC chains.
+
+        Returns
+        -------
+        log_like : array of shape (n_samples)
+            Log likelihood.
+
+        """
+
+        BayesOpts = self.BayesOpts
+        MetaModel = BayesOpts.MetaModel
+        Discrepancy = self.BayesOpts.Discrepancy
+
+        # Find the number of sigma2 parameters
+        if Discrepancy.opt_sigma != 'B':
+            disc_bound_tuples = Discrepancy.ExpDesign.bound_tuples
+            n_sigma2 = len(disc_bound_tuples)
+        else:
+            n_sigma2 = -len(theta)
+        # Check if bias term needs to be inferred
+        if Discrepancy.opt_sigma != 'B':
+            sigma2 = theta[:, -n_sigma2:]
+            theta = theta[:, :-n_sigma2]
+        else:
+            sigma2 = None
+        theta = theta if theta.ndim != 1 else theta.reshape((1, -1))
+
+        # Evaluate Model/MetaModel at theta
+        mean_pred, BayesOpts._std_pce_prior_pred = self.eval_model(theta)
+
+        # Surrogate model's error using RMSE of test data
+        surrError = MetaModel.rmse if hasattr(MetaModel, 'rmse') else None
+
+        # Likelihood
+        log_like = BayesOpts.normpdf(
+            mean_pred, self.observation, self.total_sigma2, sigma2,
+            std=surrError
+            )
+        return log_like
+
+    # -------------------------------------------------------------------------
+    def log_posterior(self, theta):
+        """
+        Computes the posterior likelihood \\(p(\\theta| \\mathcal{Y})\\) for
+        the given parameterset.
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter set, i.e. proposals of the MCMC chains.
+
+        Returns
+        -------
+        log_like : array of shape (n_samples)
+            Log posterior likelihood.
+
+        """
+
+        nsamples = 1 if theta.ndim == 1 else theta.shape[0]
+
+        if nsamples == 1:
+            if self.log_prior(theta) == -np.inf:
+                return -np.inf
+            else:
+                # Compute log prior
+                log_prior = self.log_prior(theta)
+                # Compute log Likelihood
+                log_likelihood = self.log_likelihood(theta)
+
+                return log_prior + log_likelihood
+        else:
+            # Compute log prior
+            log_prior = self.log_prior(theta)
+
+            # Initialize log_likelihood
+            log_likelihood = -np.inf*np.ones(nsamples)
+
+            # find the indices for -inf sets
+            non_inf_idx = np.where(log_prior != -np.inf)[0]
+
+            # Compute loLikelihoods
+            if non_inf_idx.size != 0:
+                log_likelihood[non_inf_idx] = self.log_likelihood(
+                    theta[non_inf_idx]
+                    )
+
+            return log_prior + log_likelihood
+
+    # -------------------------------------------------------------------------
+    def eval_model(self, theta):
+        """
+        Evaluates the (meta-) model at the given theta.
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter set, i.e. proposals of the MCMC chains.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean model prediction.
+        std_pred : dict
+            Std of model prediction.
+
+        """
+
+        BayesObj = self.BayesOpts
+        MetaModel = BayesObj.MetaModel
+        Model = BayesObj.engine.Model
+
+        if BayesObj.emulator:
+            # Evaluate the MetaModel
+            mean_pred, std_pred = MetaModel.eval_metamodel(samples=theta)
+        else:
+            # Evaluate the origModel
+            mean_pred, std_pred = dict(), dict()
+
+            model_outs, _ = Model.run_model_parallel(
+                theta, prevRun_No=self.counter,
+                key_str='_MCMC', mp=False, verbose=False)
+
+            # Save outputs in respective dicts
+            for varIdx, var in enumerate(Model.Output.names):
+                mean_pred[var] = model_outs[var]
+                std_pred[var] = np.zeros((mean_pred[var].shape))
+
+            # Remove the folder
+            if Model.link_type.lower() != 'function':
+                shutil.rmtree(f"{Model.name}_MCMC_{self.counter+1}")
+
+            # Add one to the counter
+            self.counter += 1
+
+        if hasattr(self, 'error_MetaModel') and BayesObj.error_model:
+            meanPred, stdPred = self.error_MetaModel.eval_model_error(
+                BayesObj.BiasInputs, mean_pred
+                )
+
+        return mean_pred, std_pred
+
+    # -------------------------------------------------------------------------
+    def train_error_model(self, sampler):
+        """
+        Trains an error model using a Gaussian Process Regression.
+
+        Parameters
+        ----------
+        sampler : obj
+            emcee sampler.
+
+        Returns
+        -------
+        error_MetaModel : obj
+            A error model.
+
+        """
+        BayesObj = self.BayesOpts
+        MetaModel = BayesObj.MetaModel
+
+        # Prepare the poster samples
+        try:
+            tau = sampler.get_autocorr_time(tol=0)
+        except emcee.autocorr.AutocorrError:
+            tau = 5
+
+        if all(np.isnan(tau)):
+            tau = 5
+
+        burnin = int(2*np.nanmax(tau))
+        thin = int(0.5*np.nanmin(tau)) if int(0.5*np.nanmin(tau)) != 0 else 1
+        finalsamples = sampler.get_chain(discard=burnin, flat=True, thin=thin)
+        posterior = finalsamples[:, :MetaModel.n_params]
+
+        # Select posterior mean as MAP
+        map_theta = posterior.mean(axis=0).reshape((1, MetaModel.n_params))
+        # MAP_theta = st.mode(Posterior_df,axis=0)[0]
+
+        # Evaluate the (meta-)model at the MAP
+        y_map, y_std_map = MetaModel.eval_metamodel(samples=map_theta)
+
+        # Train a GPR meta-model using MAP
+        error_MetaModel = MetaModel.create_model_error(
+            BayesObj.BiasInputs, y_map, name='Calib')
+
+        return error_MetaModel
+
+    # -------------------------------------------------------------------------
+    def gelman_rubin(self, chain, return_var=False):
+        """
+        The potential scale reduction factor (PSRF) defined by the variance
+        within one chain, W, with the variance between chains B.
+        Both variances are combined in a weighted sum to obtain an estimate of
+        the variance of a parameter \\( \\theta \\).The square root of the
+        ratio of this estimates variance to the within chain variance is called
+        the potential scale reduction.
+        For a well converged chain it should approach 1. Values greater than
+        1.1 typically indicate that the chains have not yet fully converged.
+
+        Source: http://joergdietrich.github.io/emcee-convergence.html
+
+        https://github.com/jwalton3141/jwalton3141.github.io/blob/master/assets/posts/ESS/rwmh.py
+
+        Parameters
+        ----------
+        chain : array (n_walkers, n_steps, n_params)
+            The emcee ensamples.
+
+        Returns
+        -------
+        R_hat : float
+            The Gelman-Robin values.
+
+        """
+        m_chains, n_iters = chain.shape[:2]
+
+        # Calculate between-chain variance
+        θb = np.mean(chain, axis=1)
+        θbb = np.mean(θb, axis=0)
+        B_over_n = ((θbb - θb)**2).sum(axis=0)
+        B_over_n /= (m_chains - 1)
+
+        # Calculate within-chain variances
+        ssq = np.var(chain, axis=1, ddof=1)
+        W = np.mean(ssq, axis=0)
+
+        # (over) estimate of variance
+        var_θ = W * (n_iters - 1) / n_iters + B_over_n
+
+        if return_var:
+            return var_θ
+        else:
+            # The square root of the ratio of this estimates variance to the
+            # within chain variance
+            R_hat = np.sqrt(var_θ / W)
+            return R_hat
+
+    # -------------------------------------------------------------------------
+    def marginal_llk_emcee(self, sampler, nburn=None, logp=None, maxiter=1000):
+        """
+        The Bridge Sampling Estimator of the Marginal Likelihood based on
+        https://gist.github.com/junpenglao/4d2669d69ddfe1d788318264cdcf0583
+
+        Parameters
+        ----------
+        sampler : TYPE
+            MultiTrace, result of MCMC run.
+        nburn : int, optional
+            Number of burn-in step. The default is None.
+        logp : TYPE, optional
+            Model Log-probability function. The default is None.
+        maxiter : int, optional
+            Maximum number of iterations. The default is 1000.
+
+        Returns
+        -------
+        marg_llk : dict
+            Estimated Marginal log-Likelihood.
+
+        """
+        r0, tol1, tol2 = 0.5, 1e-10, 1e-4
+
+        if logp is None:
+            logp = sampler.log_prob_fn
+
+        # Split the samples into two parts
+        # Use the first 50% for fiting the proposal distribution
+        # and the second 50% in the iterative scheme.
+        if nburn is None:
+            mtrace = sampler.chain
+        else:
+            mtrace = sampler.chain[:, nburn:, :]
+
+        nchain, len_trace, nrofVars = mtrace.shape
+
+        N1_ = len_trace // 2
+        N1 = N1_*nchain
+        N2 = len_trace*nchain - N1
+
+        samples_4_fit = np.zeros((nrofVars, N1))
+        samples_4_iter = np.zeros((nrofVars, N2))
+        effective_n = np.zeros((nrofVars))
+
+        # matrix with already transformed samples
+        for var in range(nrofVars):
+
+            # for fitting the proposal
+            x = mtrace[:, :N1_, var]
+
+            samples_4_fit[var, :] = x.flatten()
+            # for the iterative scheme
+            x2 = mtrace[:, N1_:, var]
+            samples_4_iter[var, :] = x2.flatten()
+
+            # effective sample size of samples_4_iter, scalar
+            effective_n[var] = self._my_ESS(x2)
+
+        # median effective sample size (scalar)
+        neff = np.median(effective_n)
+
+        # get mean & covariance matrix and generate samples from proposal
+        m = np.mean(samples_4_fit, axis=1)
+        V = np.cov(samples_4_fit)
+        L = chol(V, lower=True)
+
+        # Draw N2 samples from the proposal distribution
+        gen_samples = m[:, None] + np.dot(
+            L, st.norm.rvs(0, 1, size=samples_4_iter.shape)
+            )
+
+        # Evaluate proposal distribution for posterior & generated samples
+        q12 = st.multivariate_normal.logpdf(samples_4_iter.T, m, V)
+        q22 = st.multivariate_normal.logpdf(gen_samples.T, m, V)
+
+        # Evaluate unnormalized posterior for posterior & generated samples
+        q11 = logp(samples_4_iter.T)
+        q21 = logp(gen_samples.T)
+
+        # Run iterative scheme:
+        tmp = self._iterative_scheme(
+            N1, N2, q11, q12, q21, q22, r0, neff, tol1, maxiter, 'r'
+            )
+        if ~np.isfinite(tmp['logml']):
+            warnings.warn(
+                "Logml could not be estimated within maxiter, rerunning with "
+                "adjusted starting value. Estimate might be more variable than"
+                " usual.")
+            # use geometric mean as starting value
+            r0_2 = np.sqrt(tmp['r_vals'][-2]*tmp['r_vals'][-1])
+            tmp = self._iterative_scheme(
+                q11, q12, q21, q22, r0_2, neff, tol2, maxiter, 'logml'
+                )
+
+        marg_llk = dict(
+            logml=tmp['logml'], niter=tmp['niter'], method="normal",
+            q11=q11, q12=q12, q21=q21, q22=q22
+            )
+        return marg_llk
+
+    # -------------------------------------------------------------------------
+    def _iterative_scheme(self, N1, N2, q11, q12, q21, q22, r0, neff, tol,
+                          maxiter, criterion):
+        """
+        Iterative scheme as proposed in Meng and Wong (1996) to estimate the
+        marginal likelihood
+
+        """
+        l1 = q11 - q12
+        l2 = q21 - q22
+        # To increase numerical stability,
+        # subtracting the median of l1 from l1 & l2 later
+        lstar = np.median(l1)
+        s1 = neff/(neff + N2)
+        s2 = N2/(neff + N2)
+        r = r0
+        r_vals = [r]
+        logml = np.log(r) + lstar
+        criterion_val = 1 + tol
+
+        i = 0
+        while (i <= maxiter) & (criterion_val > tol):
+            rold = r
+            logmlold = logml
+            numi = np.exp(l2 - lstar)/(s1 * np.exp(l2 - lstar) + s2 * r)
+            deni = 1/(s1 * np.exp(l1 - lstar) + s2 * r)
+            if np.sum(~np.isfinite(numi))+np.sum(~np.isfinite(deni)) > 0:
+                warnings.warn(
+                    """Infinite value in iterative scheme, returning NaN.
+                     Try rerunning with more samples.""")
+            r = (N1/N2) * np.sum(numi)/np.sum(deni)
+            r_vals.append(r)
+            logml = np.log(r) + lstar
+            i += 1
+            if criterion == 'r':
+                criterion_val = np.abs((r - rold)/r)
+            elif criterion == 'logml':
+                criterion_val = np.abs((logml - logmlold)/logml)
+
+        if i >= maxiter:
+            return dict(logml=np.NaN, niter=i, r_vals=np.asarray(r_vals))
+        else:
+            return dict(logml=logml, niter=i)
+
+    # -------------------------------------------------------------------------
+    def _my_ESS(self, x):
+        """
+        Compute the effective sample size of estimand of interest.
+        Vectorised implementation.
+        https://github.com/jwalton3141/jwalton3141.github.io/blob/master/assets/posts/ESS/rwmh.py
+
+
+        Parameters
+        ----------
+        x : array of shape (n_walkers, n_steps)
+            MCMC Samples.
+
+        Returns
+        -------
+        int
+            Effective sample size.
+
+        """
+        m_chains, n_iters = x.shape
+
+        def variogram(t):
+            variogram = ((x[:, t:] - x[:, :(n_iters - t)])**2).sum()
+            variogram /= (m_chains * (n_iters - t))
+            return variogram
+
+        post_var = self.gelman_rubin(x, return_var=True)
+
+        t = 1
+        rho = np.ones(n_iters)
+        negative_autocorr = False
+
+        # Iterate until the sum of consecutive estimates of autocorrelation is
+        # negative
+        while not negative_autocorr and (t < n_iters):
+            rho[t] = 1 - variogram(t) / (2 * post_var)
+
+            if not t % 2:
+                negative_autocorr = sum(rho[t-1:t+1]) < 0
+
+            t += 1
+
+        return int(m_chains*n_iters / (1 + 2*rho[1:t].sum()))
+
+    # -------------------------------------------------------------------------
+    def _check_ranges(self, theta, ranges):
+        """
+        This function checks if theta lies in the given ranges.
+
+        Parameters
+        ----------
+        theta : array
+            Proposed parameter set.
+        ranges : nested list
+            List of the praremeter ranges.
+
+        Returns
+        -------
+        c : bool
+            If it lies in the given range, it return True else False.
+
+        """
+        c = True
+        # traverse in the list1
+        for i, bounds in enumerate(ranges):
+            x = theta[i]
+            # condition check
+            if x < bounds[0] or x > bounds[1]:
+                c = False
+                return c
+        return c
diff --git a/examples/analytical-function/bayesvalidrox/bayesvalidrox.mplstyle b/examples/analytical-function/bayesvalidrox/bayesvalidrox.mplstyle
new file mode 100644
index 0000000000000000000000000000000000000000..1f31c01f24597de0e0be741be4d3a706c4213a6c
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/bayesvalidrox.mplstyle
@@ -0,0 +1,16 @@
+figure.titlesize : 30
+axes.titlesize : 30
+axes.labelsize : 30
+axes.linewidth : 3
+axes.grid : True
+lines.linewidth : 3
+lines.markersize : 10
+xtick.labelsize : 30
+ytick.labelsize : 30
+legend.fontsize : 30
+font.family : serif
+font.serif : Arial
+font.size : 30
+text.usetex : True
+grid.linestyle : -
+figure.figsize : 24, 16
diff --git a/examples/analytical-function/bayesvalidrox/desktop.ini b/examples/analytical-function/bayesvalidrox/desktop.ini
new file mode 100644
index 0000000000000000000000000000000000000000..632de13ae6b61cecf0d9fdbf9c97cfb16bfb51a4
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/desktop.ini
@@ -0,0 +1,2 @@
+[LocalizedFileNames]
+exploration.py=@exploration.py,0
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/__init__.py b/examples/analytical-function/bayesvalidrox/post_processing/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..81c9825420b6ed3f027fb3c141be8af05a89f695
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/post_processing/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from .post_processing import PostProcessing
+
+__all__ = [
+    "PostProcessing"
+    ]
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c8590a242166b2e8d40de7ee2eece71980bd1571
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e84acd550fed2f7af8a071adf99001f44547bdf6
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..58a0eb24635d0b97a14d13708e616de6a0659976
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0924d8afac04d4fe82ebe791bc55a8ae48d7c117
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..797c6f82a4d9b29d81d7edc3a3df54e9cf3983aa
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..312575d7655db85df423489051f494f3dce62692
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/post_processing/post_processing.py b/examples/analytical-function/bayesvalidrox/post_processing/post_processing.py
new file mode 100644
index 0000000000000000000000000000000000000000..24ca8d9b9d4ea17268bc395c629c563db46899f4
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/post_processing/post_processing.py
@@ -0,0 +1,1491 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import math
+import os
+from itertools import combinations, cycle
+import pandas as pd
+import scipy.stats as stats
+from sklearn.linear_model import LinearRegression
+from sklearn.metrics import mean_squared_error, r2_score
+import matplotlib.pyplot as plt
+import matplotlib.ticker as ticker
+from matplotlib.offsetbox import AnchoredText
+from matplotlib.patches import Patch
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class PostProcessing:
+    """
+    This class provides many helper functions to post-process the trained
+    meta-model.
+
+    Attributes
+    ----------
+    MetaModel : obj
+        MetaModel object to do postprocessing on.
+    name : str
+        Type of the anaylsis. The default is `'calib'`. If a validation is
+        expected to be performed change this to `'valid'`.
+    out_dir : str
+        Directory to print the results into
+    """
+
+    def __init__(self, engine, name='calib', out_dir = '.'):
+        self.engine = engine
+        self.MetaModel = engine.MetaModel
+        self.ExpDesign = engine.ExpDesign
+        self.ModelObj = engine.Model
+        self.name = name
+        self.out_dir = out_dir
+        
+        newpath = (f'Outputs_PostProcessing_{self.name}/')
+        if not os.path.exists(self.out_dir+newpath):
+            os.makedirs(self.out_dir+newpath)
+
+    # -------------------------------------------------------------------------
+    
+    def plot_moments(self, xlabel='Time [s]', plot_type=None, 
+                     use_mc=False, num_mc_samples=1e2):
+        """
+        Plots the moments in a pdf format in the directory
+        `Outputs_PostProcessing`.
+
+        Parameters
+        ----------
+        xlabel : str, optional
+            String to be displayed as x-label. The default is `'Time [s]'`.
+        plot_type : str, optional
+            Options: bar or line. The default is `None`.
+        use_mc : bool, optional
+            Toggles if the moments are calculated from pce coefficients or 
+            are estimated from samples
+        num_mc_samples : int, optional
+            The number of samples to use for estimating the moments via MC
+
+        Returns
+        -------
+        pce_means: dict
+            Mean of the model outputs.
+        pce_means: dict
+            Standard deviation of the model outputs.
+
+        """
+
+        bar_plot = True if plot_type == 'bar' else False
+        meta_model_type = self.MetaModel.meta_model_type
+        Model = self.ModelObj
+
+        # Read Monte-Carlo reference
+        self.mc_reference = Model.read_observation('mc_ref')
+
+        # Set the x values
+        x_values_orig = self.engine.ExpDesign.x_values
+
+        # Compute the moments with the PCEModel object
+        # TODO: optimize this call, what is really wanted is if it has polynomial coefficients to use here
+        if self.engine.MetaModel.meta_model_type.lower() == 'gpe' or use_mc == True:
+            self.pce_means, self.pce_stds = self.compute_mc_moments(num_mc_samples)
+        else:
+            self.pce_means, self.pce_stds = self.compute_pce_moments()
+
+        # Get the variables
+        out_names = Model.Output.names
+
+        # Open a pdf for the plots
+        newpath = (f'Outputs_PostProcessing_{self.name}/')
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # Plot the best fit line, set the linewidth (lw), color and
+        # transparency (alpha) of the line
+        for key in out_names:
+            fig, ax = plt.subplots(nrows=1, ncols=2)
+
+            # Extract mean and std
+            mean_data = self.pce_means[key]
+            std_data = self.pce_stds[key]
+
+            # Extract a list of x values
+            if type(x_values_orig) is dict:
+                x = x_values_orig[key]
+            else:
+                x = x_values_orig
+
+            # Plot: bar plot or line plot
+            if bar_plot:
+                ax[0].bar(list(map(str, x)), mean_data, color='b',
+                          width=0.25)
+                ax[1].bar(list(map(str, x)), std_data, color='b',
+                          width=0.25)
+                ax[0].legend(labels=[meta_model_type])
+                ax[1].legend(labels=[meta_model_type])
+            else:
+                ax[0].plot(x, mean_data, lw=3, color='k', marker='x',
+                           label=meta_model_type)
+                ax[1].plot(x, std_data, lw=3, color='k', marker='x',
+                           label=meta_model_type)
+
+            if self.mc_reference is not None:
+                if bar_plot:
+                    ax[0].bar(list(map(str, x)), self.mc_reference['mean'],
+                              color='r', width=0.25)
+                    ax[1].bar(list(map(str, x)), self.mc_reference['std'],
+                              color='r', width=0.25)
+                    ax[0].legend(labels=[meta_model_type])
+                    ax[1].legend(labels=[meta_model_type])
+                else:
+                    ax[0].plot(x, self.mc_reference['mean'], lw=3, marker='x',
+                               color='r', label='Ref.')
+                    ax[1].plot(x, self.mc_reference['std'], lw=3, marker='x',
+                               color='r', label='Ref.')
+
+            # Label the axes and provide a title
+            ax[0].set_xlabel(xlabel)
+            ax[1].set_xlabel(xlabel)
+            ax[0].set_ylabel(key)
+            ax[1].set_ylabel(key)
+
+            # Provide a title
+            ax[0].set_title('Mean of ' + key)
+            ax[1].set_title('Std of ' + key)
+
+            if not bar_plot:
+                ax[0].legend(loc='best')
+                ax[1].legend(loc='best')
+
+            plt.tight_layout()
+
+            # save the current figure
+            fig.savefig(
+                f'{self.out_dir}/{newpath}Mean_Std_PCE_{key}.pdf',
+                bbox_inches='tight'
+                )
+
+        return self.pce_means, self.pce_stds
+
+    # -------------------------------------------------------------------------
+    def valid_metamodel(self, n_samples=1, samples=None, model_out_dict=None,
+                        x_axis='Time [s]'):
+        """
+        Evaluates and plots the meta model and the PCEModel outputs for the
+        given number of samples or the given samples.
+
+        Parameters
+        ----------
+        n_samples : int, optional
+            Number of samples to be evaluated. The default is 1.
+        samples : array of shape (n_samples, n_params), optional
+            Samples to be evaluated. The default is None.
+        model_out_dict: dict
+            The model runs using the samples provided.
+        x_axis : str, optional
+            Label of x axis. The default is `'Time [s]'`.
+
+        Returns
+        -------
+        None.
+
+        """
+        MetaModel = self.MetaModel
+        Model = self.ModelObj
+
+        if samples is None:
+            self.n_samples = n_samples
+            samples = self._get_sample()
+        else:
+            self.n_samples = samples.shape[0]
+
+        # Extract x_values
+        x_values = self.engine.ExpDesign.x_values
+
+        if model_out_dict is not None:
+            self.model_out_dict = model_out_dict
+        else:
+            self.model_out_dict = self._eval_model(samples, key_str='valid')
+        self.pce_out_mean, self.pce_out_std = MetaModel.eval_metamodel(samples)
+
+        try:
+            key = Model.Output.names[1]
+        except IndexError:
+            key = Model.Output.names[0]
+
+        n_obs = self.model_out_dict[key].shape[1]
+
+        if n_obs == 1:
+            self._plot_validation()
+        else:
+            self._plot_validation_multi(x_values=x_values, x_axis=x_axis)
+
+    # -------------------------------------------------------------------------
+    def check_accuracy(self, n_samples=None, samples=None, outputs=None):
+        """
+        Checks accuracy of the metamodel by computing the root mean square
+        error and validation error for all outputs.
+
+        Parameters
+        ----------
+        n_samples : int, optional
+            Number of samples. The default is None.
+        samples : array of shape (n_samples, n_params), optional
+            Parameter sets to be checked. The default is None.
+        outputs : dict, optional
+            Output dictionary with model outputs for all given output types in
+            `Model.Output.names`. The default is None.
+
+        Raises
+        ------
+        Exception
+            When neither n_samples nor samples are provided.
+
+        Returns
+        -------
+        rmse: dict
+            Root mean squared error for each output.
+        valid_error : dict
+            Validation error for each output.
+
+        """
+        MetaModel = self.MetaModel
+        Model = self.ModelObj
+
+        # Set the number of samples
+        if n_samples:
+            self.n_samples = n_samples
+        elif samples is not None:
+            self.n_samples = samples.shape[0]
+        else:
+            raise Exception("Please provide either samples or pass the number"
+                            " of samples!")
+
+        # Generate random samples if necessary
+        Samples = self._get_sample() if samples is None else samples
+
+        # Run the original model with the generated samples
+        if outputs is None:
+            outputs = self._eval_model(Samples, key_str='validSet')
+
+        # Run the PCE model with the generated samples
+        pce_outputs, _ = MetaModel.eval_metamodel(samples=Samples)
+
+        self.rmse = {}
+        self.valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in Model.Output.names:
+            # Root mean square
+            self.rmse[key] = mean_squared_error(outputs[key], pce_outputs[key],
+                                                squared=False,
+                                                multioutput='raw_values')
+            # Validation error
+            self.valid_error[key] = (self.rmse[key]**2) / \
+                np.var(outputs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(self.rmse[key],
+                                             self.valid_error[key]))))
+        # Save error dicts in PCEModel object
+        self.MetaModel.rmse = self.rmse
+        self.MetaModel.valid_error = self.valid_error
+
+        return
+
+    # -------------------------------------------------------------------------
+    def plot_seq_design_diagnostics(self, ref_BME_KLD=None):
+        """
+        Plots the Bayesian Model Evidence (BME) and Kullback-Leibler divergence
+        (KLD) for the sequential design.
+
+        Parameters
+        ----------
+        ref_BME_KLD : array, optional
+            Reference BME and KLD . The default is `None`.
+
+        Returns
+        -------
+        None.
+
+        """
+        engine = self.engine
+        PCEModel = self.MetaModel
+        n_init_samples = engine.ExpDesign.n_init_samples
+        n_total_samples = engine.ExpDesign.X.shape[0]
+
+        newpath = f'Outputs_PostProcessing_{self.name}/seq_design_diagnostics/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        plotList = ['Modified LOO error', 'Validation error', 'KLD', 'BME',
+                    'RMSEMean', 'RMSEStd', 'Hellinger distance']
+        seqList = [engine.SeqModifiedLOO, engine.seqValidError,
+                   engine.SeqKLD, engine.SeqBME, engine.seqRMSEMean,
+                   engine.seqRMSEStd, engine.SeqDistHellinger]
+
+        markers = ('x', 'o', 'd', '*', '+')
+        colors = ('k', 'darkgreen', 'b', 'navy', 'darkred')
+
+        # Plot the evolution of the diagnostic criteria of the
+        # Sequential Experimental Design.
+        for plotidx, plot in enumerate(plotList):
+            fig, ax = plt.subplots()
+            seq_dict = seqList[plotidx]
+            name_util = list(seq_dict.keys())
+
+            if len(name_util) == 0:
+                continue
+
+            # Box plot when Replications have been detected.
+            if any(int(name.split("rep_", 1)[1]) > 1 for name in name_util):
+                # Extract the values from dict
+                sorted_seq_opt = {}
+                # Number of replications
+                n_reps = engine.ExpDesign.n_replication
+
+                # Get the list of utility function names
+                # Handle if only one UtilityFunction is provided
+                if not isinstance(engine.ExpDesign.util_func, list):
+                    util_funcs = [engine.ExpDesign.util_func]
+                else:
+                    util_funcs = engine.ExpDesign.util_func
+
+                for util in util_funcs:
+                    sortedSeq = {}
+                    # min number of runs available from reps
+                    n_runs = min([seq_dict[f'{util}_rep_{i+1}'].shape[0]
+                                 for i in range(n_reps)])
+
+                    for runIdx in range(n_runs):
+                        values = []
+                        for key in seq_dict.keys():
+                            if util in key:
+                                values.append(seq_dict[key][runIdx].mean())
+                        sortedSeq['SeqItr_'+str(runIdx)] = np.array(values)
+                    sorted_seq_opt[util] = sortedSeq
+
+                # BoxPlot
+                def draw_plot(data, labels, edge_color, fill_color, idx):
+                    pos = labels - (idx-1)
+                    bp = plt.boxplot(data, positions=pos, labels=labels,
+                                     patch_artist=True, sym='', widths=0.75)
+                    elements = ['boxes', 'whiskers', 'fliers', 'means',
+                                'medians', 'caps']
+                    for element in elements:
+                        plt.setp(bp[element], color=edge_color[idx])
+
+                    for patch in bp['boxes']:
+                        patch.set(facecolor=fill_color[idx])
+
+                if engine.ExpDesign.n_new_samples != 1:
+                    step1 = engine.ExpDesign.n_new_samples
+                    step2 = 1
+                else:
+                    step1 = 5
+                    step2 = 5
+                edge_color = ['red', 'blue', 'green']
+                fill_color = ['tan', 'cyan', 'lightgreen']
+                plot_label = plot
+                # Plot for different Utility Functions
+                for idx, util in enumerate(util_funcs):
+                    all_errors = np.empty((n_reps, 0))
+
+                    for key in list(sorted_seq_opt[util].keys()):
+                        errors = sorted_seq_opt.get(util, {}).get(key)[:, None]
+                        all_errors = np.hstack((all_errors, errors))
+
+                    # Special cases for BME and KLD
+                    if plot == 'KLD' or plot == 'BME':
+                        # BME convergence if refBME is provided
+                        if ref_BME_KLD is not None:
+                            if plot == 'BME':
+                                refValue = ref_BME_KLD[0]
+                                plot_label = r'BME/BME$^{Ref.}$'
+                            if plot == 'KLD':
+                                refValue = ref_BME_KLD[1]
+                                plot_label = '$D_{KL}[p(\\theta|y_*),p(\\theta)]'\
+                                    ' / D_{KL}^{Ref.}[p(\\theta|y_*), '\
+                                    'p(\\theta)]$'
+
+                            # Difference between BME/KLD and the ref. values
+                            all_errors = np.divide(all_errors,
+                                                   np.full((all_errors.shape),
+                                                           refValue))
+
+                            # Plot baseline for zero, i.e. no difference
+                            plt.axhline(y=1.0, xmin=0, xmax=1, c='green',
+                                        ls='--', lw=2)
+
+                    # Plot each UtilFuncs
+                    labels = np.arange(n_init_samples, n_total_samples+1, step1)
+                    draw_plot(all_errors[:, ::step2], labels, edge_color,
+                              fill_color, idx)
+
+                plt.xticks(labels, labels)
+                # Set the major and minor locators
+                ax.xaxis.set_major_locator(ticker.AutoLocator())
+                ax.xaxis.set_minor_locator(ticker.AutoMinorLocator())
+                ax.xaxis.grid(True, which='major', linestyle='-')
+                ax.xaxis.grid(True, which='minor', linestyle='--')
+
+                # Legend
+                legend_elements = []
+                for idx, util in enumerate(util_funcs):
+                    legend_elements.append(Patch(facecolor=fill_color[idx],
+                                                 edgecolor=edge_color[idx],
+                                                 label=util))
+                plt.legend(handles=legend_elements[::-1], loc='best')
+
+                if plot != 'BME' and plot != 'KLD':
+                    plt.yscale('log')
+                plt.autoscale(True)
+                plt.xlabel('\\# of training samples')
+                plt.ylabel(plot_label)
+                plt.title(plot)
+
+                # save the current figure
+                plot_name = plot.replace(' ', '_')
+                fig.savefig(
+                    f'{self.out_dir}/{newpath}/seq_{plot_name}.pdf',
+                    bbox_inches='tight'
+                    )
+                # Destroy the current plot
+                plt.clf()
+                # Save arrays into files
+                f = open(f'{self.out_dir}/{newpath}/seq_{plot_name}.txt', 'w')
+                f.write(str(sorted_seq_opt))
+                f.close()
+            else:
+                for idx, name in enumerate(name_util):
+                    seq_values = seq_dict[name]
+                    if engine.ExpDesign.n_new_samples != 1:
+                        step = engine.ExpDesign.n_new_samples
+                    else:
+                        step = 1
+                    x_idx = np.arange(n_init_samples, n_total_samples+1, step)
+                    if n_total_samples not in x_idx:
+                        x_idx = np.hstack((x_idx, n_total_samples))
+
+                    if plot == 'KLD' or plot == 'BME':
+                        # BME convergence if refBME is provided
+                        if ref_BME_KLD is not None:
+                            if plot == 'BME':
+                                refValue = ref_BME_KLD[0]
+                                plot_label = r'BME/BME$^{Ref.}$'
+                            if plot == 'KLD':
+                                refValue = ref_BME_KLD[1]
+                                plot_label = '$D_{KL}[p(\\theta|y_*),p(\\theta)]'\
+                                    ' / D_{KL}^{Ref.}[p(\\theta|y_*), '\
+                                    'p(\\theta)]$'
+
+                            # Difference between BME/KLD and the ref. values
+                            values = np.divide(seq_values,
+                                               np.full((seq_values.shape),
+                                                       refValue))
+
+                            # Plot baseline for zero, i.e. no difference
+                            plt.axhline(y=1.0, xmin=0, xmax=1, c='green',
+                                        ls='--', lw=2)
+
+                            # Set the limits
+                            plt.ylim([1e-1, 1e1])
+
+                            # Create the plots
+                            plt.semilogy(x_idx, values, marker=markers[idx],
+                                         color=colors[idx], ls='--', lw=2,
+                                         label=name.split("_rep", 1)[0])
+                        else:
+                            plot_label = plot
+
+                            # Create the plots
+                            plt.plot(x_idx, seq_values, marker=markers[idx],
+                                     color=colors[idx], ls='--', lw=2,
+                                     label=name.split("_rep", 1)[0])
+
+                    else:
+                        plot_label = plot
+                        seq_values = np.nan_to_num(seq_values)
+
+                        # Plot the error evolution for each output
+                        plt.semilogy(x_idx, seq_values.mean(axis=1),
+                                     marker=markers[idx], ls='--', lw=2,
+                                     color=colors[idx],
+                                     label=name.split("_rep", 1)[0])
+
+                # Set the major and minor locators
+                ax.xaxis.set_major_locator(ticker.AutoLocator())
+                ax.xaxis.set_minor_locator(ticker.AutoMinorLocator())
+                ax.xaxis.grid(True, which='major', linestyle='-')
+                ax.xaxis.grid(True, which='minor', linestyle='--')
+
+                ax.tick_params(axis='both', which='major', direction='in',
+                               width=3, length=10)
+                ax.tick_params(axis='both', which='minor', direction='in',
+                               width=2, length=8)
+                plt.xlabel('Number of runs')
+                plt.ylabel(plot_label)
+                plt.title(plot)
+                plt.legend(frameon=True)
+
+                # save the current figure
+                plot_name = plot.replace(' ', '_')
+                fig.savefig(
+                    f'{self.out_dir}/{newpath}/seq_{plot_name}.pdf',
+                    bbox_inches='tight'
+                    )
+                # Destroy the current plot
+                plt.clf()
+
+                # ---------------- Saving arrays into files ---------------
+                np.save(f'{self.out_dir}/{newpath}/seq_{plot_name}.npy', seq_values)
+
+        return
+
+    # -------------------------------------------------------------------------
+    def sobol_indices(self, xlabel='Time [s]', plot_type=None):
+        """
+        Provides Sobol indices as a sensitivity measure to infer the importance
+        of the input parameters. See Eq. 27 in [1] for more details. For the
+        case with Principal component analysis refer to [2].
+
+        [1] Global sensitivity analysis: A flexible and efficient framework
+        with an example from stochastic hydrogeology S. Oladyshkin, F.P.
+        de Barros, W. Nowak  https://doi.org/10.1016/j.advwatres.2011.11.001
+
+        [2] Nagel, J.B., Rieckermann, J. and Sudret, B., 2020. Principal
+        component analysis and sparse polynomial chaos expansions for global
+        sensitivity analysis and model calibration: Application to urban
+        drainage simulation. Reliability Engineering & System Safety, 195,
+        p.106737.
+
+        Parameters
+        ----------
+        xlabel : str, optional
+            Label of the x-axis. The default is `'Time [s]'`.
+        plot_type : str, optional
+            Plot type. The default is `None`. This corresponds to line plot.
+            Bar chart can be selected by `bar`.
+
+        Returns
+        -------
+        sobol_cell: dict
+            Sobol indices.
+        total_sobol: dict
+            Total Sobol indices.
+
+        """
+        # Extract the necessary variables
+        PCEModel = self.MetaModel
+        basis_dict = PCEModel.basis_dict
+        coeffs_dict = PCEModel.coeffs_dict
+        n_params = PCEModel.n_params
+        if hasattr(PCEModel, 'n_inner_params'):
+            n_params = PCEModel.n_inner_params
+        max_order = np.max(PCEModel.pce_deg)
+        sobol_cell_b = {}
+        total_sobol_b = {}
+        cov_Z_p_q = np.zeros((n_params))
+        
+        outputs = self.ModelObj.Output.names
+        if hasattr(PCEModel, 'trafo_type'):
+            if PCEModel.trafo_type == 'space':
+                outputs = ['Z']
+
+        for b_i in range(PCEModel.n_bootstrap_itrs):
+
+            sobol_cell_, total_sobol_ = {}, {}
+
+            for output in outputs:
+
+                n_meas_points = len(coeffs_dict[f'b_{b_i+1}'][output])
+
+                # Initialize the (cell) array containing the (total) Sobol indices.
+                sobol_array = dict.fromkeys(range(1, max_order+1), [])
+                sobol_cell_array = dict.fromkeys(range(1, max_order+1), [])
+
+                for i_order in range(1, max_order+1):
+                    n_comb = math.comb(n_params, i_order)
+
+                    sobol_cell_array[i_order] = np.zeros((n_comb, n_meas_points))
+
+                total_sobol_array = np.zeros((n_params, n_meas_points))
+
+                # Initialize the cell to store the names of the variables
+                TotalVariance = np.zeros((n_meas_points))
+                # Loop over all measurement points and calculate sobol indices
+                for pIdx in range(n_meas_points):
+
+                    # Extract the basis indices (alpha) and coefficients
+                    Basis = basis_dict[f'b_{b_i+1}'][output][f'y_{pIdx+1}']
+
+                    try:
+                        clf_poly = PCEModel.clf_poly[f'b_{b_i+1}'][output][f'y_{pIdx+1}']
+                        PCECoeffs = clf_poly.coef_
+                    except:
+                        PCECoeffs = coeffs_dict[f'b_{b_i+1}'][output][f'y_{pIdx+1}']
+
+                    # Compute total variance
+                    TotalVariance[pIdx] = np.sum(np.square(PCECoeffs[1:]))
+
+                    nzidx = np.where(PCECoeffs != 0)[0]
+                    # Set all the Sobol indices equal to zero in the presence of a
+                    # null output.
+                    if len(nzidx) == 0:
+                        # This is buggy.
+                        for i_order in range(1, max_order+1):
+                            sobol_cell_array[i_order][:, pIdx] = 0
+
+                    # Otherwise compute them by summing well-chosen coefficients
+                    else:
+                        nz_basis = Basis[nzidx]
+                        for i_order in range(1, max_order+1):
+                            idx = np.where(np.sum(nz_basis > 0, axis=1) == i_order)
+                            subbasis = nz_basis[idx]
+                            Z = np.array(list(combinations(range(n_params), i_order)))
+
+                            for q in range(Z.shape[0]):
+                                Zq = Z[q]
+                                subsubbasis = subbasis[:, Zq]
+                                subidx = np.prod(subsubbasis, axis=1) > 0
+                                sum_ind = nzidx[idx[0][subidx]]
+                                if TotalVariance[pIdx] == 0.0:
+                                    sobol_cell_array[i_order][q, pIdx] = 0.0
+                                else:
+                                    sobol = np.sum(np.square(PCECoeffs[sum_ind]))
+                                    sobol /= TotalVariance[pIdx]
+                                    sobol_cell_array[i_order][q, pIdx] = sobol
+
+                        # Compute the TOTAL Sobol indices.
+                        for ParIdx in range(n_params):
+                            idx = nz_basis[:, ParIdx] > 0
+                            sum_ind = nzidx[idx]
+
+                            if TotalVariance[pIdx] == 0.0:
+                                total_sobol_array[ParIdx, pIdx] = 0.0
+                            else:
+                                sobol = np.sum(np.square(PCECoeffs[sum_ind]))
+                                sobol /= TotalVariance[pIdx]
+                                total_sobol_array[ParIdx, pIdx] = sobol
+
+                    # ----- if PCA selected: Compute covariance -----
+                    if PCEModel.dim_red_method.lower() == 'pca':
+                        # Extract the basis indices (alpha) and coefficients for
+                        # next component
+                        if pIdx < n_meas_points-1:
+                            nextBasis = basis_dict[f'b_{b_i+1}'][output][f'y_{pIdx+2}']
+                            if PCEModel.bootstrap_method != 'fast' or b_i == 0:
+                                clf_poly = PCEModel.clf_poly[f'b_{b_i+1}'][output][f'y_{pIdx+2}']
+                                nextPCECoeffs = clf_poly.coef_
+                            else:
+                                nextPCECoeffs = coeffs_dict[f'b_{b_i+1}'][output][f'y_{pIdx+2}']
+
+                            # Choose the common non-zero basis
+                            mask = (Basis[:, None] == nextBasis).all(-1).any(-1)
+                            n_mask = (nextBasis[:, None] == Basis).all(-1).any(-1)
+
+                            # Compute the covariance in Eq 17.
+                            for ParIdx in range(n_params):
+                                idx = (mask) & (Basis[:, ParIdx] > 0)
+                                n_idx = (n_mask) & (nextBasis[:, ParIdx] > 0)
+                                try:
+                                    cov_Z_p_q[ParIdx] += np.sum(np.dot(
+                                        PCECoeffs[idx], nextPCECoeffs[n_idx])
+                                        )
+                                except:
+                                    pass
+
+                # Compute the sobol indices according to Ref. 2
+                if PCEModel.dim_red_method.lower() == 'pca':
+                    n_c_points = self.engine.ExpDesign.Y[output].shape[1]
+                    PCA = PCEModel.pca[f'b_{b_i+1}'][output]
+                    compPCA = PCA.components_
+                    nComp = compPCA.shape[0]
+                    var_Z_p = PCA.explained_variance_
+
+                    # Extract the sobol index of the components
+                    for i_order in range(1, max_order+1):
+                        n_comb = math.comb(n_params, i_order)
+                        sobol_array[i_order] = np.zeros((n_comb, n_c_points))
+                        Z = np.array(list(combinations(range(n_params), i_order)))
+
+                        # Loop over parameters
+                        for q in range(Z.shape[0]):
+                            S_Z_i = sobol_cell_array[i_order][q]
+
+                            for tIdx in range(n_c_points):
+                                var_Y_t = np.var(
+                                    self.engine.ExpDesign.Y[output][:, tIdx])
+                                if var_Y_t == 0.0:
+                                    term1, term2 = 0.0, 0.0
+                                else:
+                                    # Eq. 17
+                                    term1 = 0.0
+                                    for i in range(nComp):
+                                        a = S_Z_i[i] * var_Z_p[i]
+                                        a *= compPCA[i, tIdx]**2
+                                        term1 += a
+
+                                    # TODO: Term 2
+                                    # term2 = 0.0
+                                    # for i in range(nComp-1):
+                                    #     term2 += cov_Z_p_q[q] * compPCA[i, tIdx]
+                                    #     term2 *= compPCA[i+1, tIdx]
+                                    # term2 *= 2
+
+                                sobol_array[i_order][q, tIdx] = term1 #+ term2
+
+                                # Devide over total output variance Eq. 18
+                                sobol_array[i_order][q, tIdx] /= var_Y_t
+
+                    # Compute the TOTAL Sobol indices.
+                    total_sobol = np.zeros((n_params, n_c_points))
+                    for ParIdx in range(n_params):
+                        S_Z_i = total_sobol_array[ParIdx]
+
+                        for tIdx in range(n_c_points):
+                            var_Y_t = np.var(self.engine.ExpDesign.Y[output][:, tIdx])
+                            if var_Y_t == 0.0:
+                                term1, term2 = 0.0, 0.0
+                            else:
+                                term1 = 0
+                                for i in range(nComp):
+                                    term1 += S_Z_i[i] * var_Z_p[i] * \
+                                        (compPCA[i, tIdx]**2)
+
+                                # Term 2
+                                term2 = 0
+                                for i in range(nComp-1):
+                                    term2 += cov_Z_p_q[ParIdx] * compPCA[i, tIdx] \
+                                        * compPCA[i+1, tIdx]
+                                term2 *= 2
+
+                            total_sobol[ParIdx, tIdx] = term1 #+ term2
+
+                            # Devide over total output variance Eq. 18
+                            total_sobol[ParIdx, tIdx] /= var_Y_t
+
+                    sobol_cell_[output] = sobol_array
+                    total_sobol_[output] = total_sobol
+                else:
+                    sobol_cell_[output] = sobol_cell_array
+                    total_sobol_[output] = total_sobol_array
+
+            # Save for each bootsrtap iteration
+            sobol_cell_b[b_i] = sobol_cell_
+            total_sobol_b[b_i] = total_sobol_
+
+        # Average total sobol indices
+        total_sobol_all = {}
+        for i in sorted(total_sobol_b):
+            for k, v in total_sobol_b[i].items():
+                if k not in total_sobol_all:
+                    total_sobol_all[k] = [None] * len(total_sobol_b)
+                total_sobol_all[k][i] = v
+        sobol_all = {}
+        for i in sorted(sobol_cell_b):
+            for k, v in sobol_cell_b[i].items():
+                for l,m in v.items():
+                    if l not in sobol_all:
+                        sobol_all[l]={}
+                    if k not in sobol_all[l]:
+                        sobol_all[l][k] = [None] * len(sobol_cell_b)
+                    sobol_all[l][k][i] = v[l]
+        #print(sobol_all)
+
+        self.sobol = {}
+        # Will receive a set of indices for each possible degree of polynomial/interaction
+        for i_order in range(1, max_order+1):
+            self.sobol[i_order]={}
+            for output in outputs:
+                self.sobol[i_order][output] = np.mean([sobol_all[i_order][output]], axis=0)
+            
+        self.total_sobol = {}
+        for output in outputs:
+            self.total_sobol[output] = np.mean(total_sobol_all[output], axis=0)
+
+        # ---------------- Plot -----------------------
+        par_names = self.engine.ExpDesign.par_names
+        x_values_orig = self.engine.ExpDesign.x_values
+        
+        # Check if the x_values match the number of metamodel outputs
+        if not np.array(x_values_orig).shape[0] == self.total_sobol[outputs[0]].shape[1]:
+            print('The number of MetaModel outputs does not match the x_values'
+                  ' specified in ExpDesign. Images are created with '
+                  'equidistant numbers on the x-axis')
+            x_values_orig = np.arange(0,1,self.total_sobol[output].shape[0])
+         
+        # Check if it uses a wrapper structure
+        if hasattr(PCEModel, 'trafo_type'):
+            if PCEModel.trafo_type == 'time':
+                par_names.append('time')
+
+        newpath = (f'Outputs_PostProcessing_{self.name}/')
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        if 1:
+            fig = plt.figure()
+            
+            for i_order in range(1, max_order+1):
+                # Change labels to combined params sets for higher order indices
+                if i_order == 1:
+                    par_names_i = par_names
+                else:
+                    par_names_i = list(combinations(par_names, i_order))
+                for outIdx, output in enumerate(outputs):
+        
+                    # Extract total Sobol indices
+                    sobol = self.sobol[i_order][output][0]
+        
+                    # Compute quantiles
+                    q_5 = np.quantile(sobol_all[i_order][output], q=0.05, axis=0)
+                    q_97_5 = np.quantile(sobol_all[i_order][output], q=0.975, axis=0)
+        
+                    # Extract a list of x values
+                    if type(x_values_orig) is dict:
+                        x = x_values_orig[output]
+                    else:
+                        x = x_values_orig
+        
+                    if plot_type == 'bar':
+                        ax = fig.add_axes([0, 0, 1, 1])
+                        dict1 = {xlabel: x}
+                        dict2 = {param: sobolIndices for param, sobolIndices
+                                 in zip(par_names_i, sobol)}
+        
+                        df = pd.DataFrame({**dict1, **dict2})
+                        df.plot(x=xlabel, y=par_names_i, kind="bar", ax=ax, rot=0,
+                                colormap='Dark2', yerr=q_97_5-q_5)
+                        ax.set_ylabel('Sobol indices, $S^T$')
+        
+                    else:
+                        for i, sobolIndices in enumerate(sobol):
+                            plt.plot(x, sobolIndices, label=par_names_i[i],
+                                     marker='x', lw=2.5)
+                            plt.fill_between(x, q_5[i], q_97_5[i], alpha=0.15)
+        
+                        plt.ylabel('Sobol indices, $S^T$')
+                        plt.xlabel(xlabel)
+        
+                    plt.title(f'{i_order} degree Sensitivity analysis of {output}')
+                    if plot_type != 'bar':
+                        plt.legend(loc='best', frameon=True)
+        
+                    # Save indices
+                    np.savetxt(f'{self.out_dir}/{newpath}sobol_{i_order}_' +
+                               output.replace('/', '_') + '.csv',
+                               sobol.T, delimiter=',',
+                               header=','.join(par_names), comments='')
+        
+                    # save the current figure
+                    #print(f'{self.out_dir}/{newpath}Sobol_indices_{i_order}_{output}.pdf')
+                    fig.savefig(
+                        f'{self.out_dir}/{newpath}Sobol_indices_{i_order}_{output}.pdf',
+                        bbox_inches='tight'
+                        )
+        
+                    # Destroy the current plot
+                    plt.clf()
+
+        fig = plt.figure()
+
+        for outIdx, output in enumerate(outputs):
+
+            # Extract total Sobol indices
+            total_sobol = self.total_sobol[output]
+
+            # Compute quantiles
+            q_5 = np.quantile(total_sobol_all[output], q=0.05, axis=0)
+            q_97_5 = np.quantile(total_sobol_all[output], q=0.975, axis=0)
+
+            # Extract a list of x values
+            if type(x_values_orig) is dict:
+                x = x_values_orig[output]
+            else:
+                x = x_values_orig
+
+            if plot_type == 'bar':
+                ax = fig.add_axes([0, 0, 1, 1])
+                dict1 = {xlabel: x}
+                dict2 = {param: sobolIndices for param, sobolIndices
+                         in zip(par_names, total_sobol)}
+
+                df = pd.DataFrame({**dict1, **dict2})
+                df.plot(x=xlabel, y=par_names, kind="bar", ax=ax, rot=0,
+                        colormap='Dark2', yerr=q_97_5-q_5)
+                ax.set_ylabel('Total Sobol indices, $S^T$')
+
+            else:
+                for i, sobolIndices in enumerate(total_sobol):
+                    plt.plot(x, sobolIndices, label=par_names[i],
+                             marker='x', lw=2.5)
+                    plt.fill_between(x, q_5[i], q_97_5[i], alpha=0.15)
+
+                plt.ylabel('Total Sobol indices, $S^T$')
+                plt.xlabel(xlabel)
+
+            plt.title(f'Sensitivity analysis of {output}')
+            if plot_type != 'bar':
+                plt.legend(loc='best', frameon=True)
+
+            # Save indices
+            np.savetxt(f'{self.out_dir}/{newpath}totalsobol_' +
+                       output.replace('/', '_') + '.csv',
+                       total_sobol.T, delimiter=',',
+                       header=','.join(par_names), comments='')
+
+            # save the current figure
+            fig.savefig(
+                f'{self.out_dir}/{newpath}TotalSobol_indices_{output}.pdf',
+                bbox_inches='tight'
+                )
+
+            # Destroy the current plot
+            plt.clf()
+
+        return self.sobol, self.total_sobol
+
+    # -------------------------------------------------------------------------
+    def check_reg_quality(self, n_samples=1000, samples=None):
+        """
+        Checks the quality of the metamodel for single output models based on:
+        https://towardsdatascience.com/how-do-you-check-the-quality-of-your-regression-model-in-python-fa61759ff685
+
+
+        Parameters
+        ----------
+        n_samples : int, optional
+            Number of parameter sets to use for the check. The default is 1000.
+        samples : array of shape (n_samples, n_params), optional
+            Parameter sets to use for the check. The default is None.
+
+        Returns
+        -------
+        None.
+
+        """
+        MetaModel = self.MetaModel
+
+        if samples is None:
+            self.n_samples = n_samples
+            samples = self._get_sample()
+        else:
+            self.n_samples = samples.shape[0]
+
+        # Evaluate the original and the surrogate model
+        y_val = self._eval_model(samples, key_str='valid')
+        y_pce_val, _ = MetaModel.eval_metamodel(samples=samples)
+
+        # Open a pdf for the plots
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # Fit the data(train the model)
+        for key in y_pce_val.keys():
+
+            y_pce_val_ = y_pce_val[key]
+            y_val_ = y_val[key]
+            residuals = y_val_ - y_pce_val_
+
+            # ------ Residuals vs. predicting variables ------
+            # Check the assumptions of linearity and independence
+            fig1 = plt.figure()
+            for i, par in enumerate(self.engine.ExpDesign.par_names):
+                plt.title(f"{key}: Residuals vs. {par}")
+                plt.scatter(
+                    x=samples[:, i], y=residuals, color='blue', edgecolor='k')
+                plt.grid(True)
+                xmin, xmax = min(samples[:, i]), max(samples[:, i])
+                plt.hlines(y=0, xmin=xmin*0.9, xmax=xmax*1.1, color='red',
+                           lw=3, linestyle='--')
+                plt.xlabel(par)
+                plt.ylabel('Residuals')
+                plt.show()
+
+                # save the current figure
+                fig1.savefig(f'{self.out_dir}/{newpath}/Residuals_vs_Par_{i+1}.pdf',
+                             bbox_inches='tight')
+                # Destroy the current plot
+                plt.clf()
+
+            # ------ Fitted vs. residuals ------
+            # Check the assumptions of linearity and independence
+            fig2 = plt.figure()
+            plt.title(f"{key}: Residuals vs. fitted values")
+            plt.scatter(x=y_pce_val_, y=residuals, color='blue', edgecolor='k')
+            plt.grid(True)
+            xmin, xmax = min(y_val_), max(y_val_)
+            plt.hlines(y=0, xmin=xmin*0.9, xmax=xmax*1.1, color='red', lw=3,
+                       linestyle='--')
+            plt.xlabel(key)
+            plt.ylabel('Residuals')
+            plt.show()
+
+            # save the current figure
+            fig2.savefig(f'{self.out_dir}/{newpath}/Fitted_vs_Residuals.pdf',
+                         bbox_inches='tight')
+            # Destroy the current plot
+            plt.clf()
+
+            # ------ Histogram of normalized residuals ------
+            fig3 = plt.figure()
+            resid_pearson = residuals / (max(residuals)-min(residuals))
+            plt.hist(resid_pearson, bins=20, edgecolor='k')
+            plt.ylabel('Count')
+            plt.xlabel('Normalized residuals')
+            plt.title(f"{key}: Histogram of normalized residuals")
+
+            # Normality (Shapiro-Wilk) test of the residuals
+            ax = plt.gca()
+            _, p = stats.shapiro(residuals)
+            if p < 0.01:
+                annText = "The residuals seem to come from a Gaussian Process."
+            else:
+                annText = "The normality assumption may not hold."
+            at = AnchoredText(annText, prop=dict(size=30), frameon=True,
+                              loc='upper left')
+            at.patch.set_boxstyle("round,pad=0.,rounding_size=0.2")
+            ax.add_artist(at)
+
+            plt.show()
+
+            # save the current figure
+            fig3.savefig(f'{self.out_dir}/{newpath}/Hist_NormResiduals.pdf',
+                         bbox_inches='tight')
+            # Destroy the current plot
+            plt.clf()
+
+            # ------ Q-Q plot of the normalized residuals ------
+            plt.figure()
+            stats.probplot(residuals[:, 0], plot=plt)
+            plt.xticks()
+            plt.yticks()
+            plt.xlabel("Theoretical quantiles")
+            plt.ylabel("Sample quantiles")
+            plt.title(f"{key}: Q-Q plot of normalized residuals")
+            plt.grid(True)
+            plt.show()
+
+            # save the current figure
+            plt.savefig(f'{self.out_dir}/{newpath}/QQPlot_NormResiduals.pdf',
+                        bbox_inches='tight')
+            # Destroy the current plot
+            plt.clf()
+
+    # -------------------------------------------------------------------------
+    def eval_pce_model_3d(self):
+
+        self.n_samples = 1000
+
+        PCEModel = self.MetaModel
+        Model = self.ModelObj
+        n_samples = self.n_samples
+
+        # Create 3D-Grid
+        # TODO: Make it general
+        x = np.linspace(-5, 10, n_samples)
+        y = np.linspace(0, 15, n_samples)
+
+        X, Y = np.meshgrid(x, y)
+        PCE_Z = np.zeros((self.n_samples, self.n_samples))
+        Model_Z = np.zeros((self.n_samples, self.n_samples))
+
+        for idxMesh in range(self.n_samples):
+            sample_mesh = np.vstack((X[:, idxMesh], Y[:, idxMesh])).T
+
+            univ_p_val = PCEModel.univ_basis_vals(sample_mesh)
+
+            for Outkey, ValuesDict in PCEModel.coeffs_dict.items():
+
+                pce_out_mean = np.zeros((len(sample_mesh), len(ValuesDict)))
+                pce_out_std = np.zeros((len(sample_mesh), len(ValuesDict)))
+                model_outs = np.zeros((len(sample_mesh), len(ValuesDict)))
+
+                for Inkey, InIdxValues in ValuesDict.items():
+                    idx = int(Inkey.split('_')[1]) - 1
+                    basis_deg_ind = PCEModel.basis_dict[Outkey][Inkey]
+                    clf_poly = PCEModel.clf_poly[Outkey][Inkey]
+
+                    PSI_Val = PCEModel.create_psi(basis_deg_ind, univ_p_val)
+
+                    # Perdiction with error bar
+                    y_mean, y_std = clf_poly.predict(PSI_Val, return_std=True)
+
+                    pce_out_mean[:, idx] = y_mean
+                    pce_out_std[:, idx] = y_std
+
+                    # Model evaluation
+                    model_out_dict, _ = Model.run_model_parallel(sample_mesh,
+                                                                 key_str='Valid3D')
+                    model_outs[:, idx] = model_out_dict[Outkey].T
+
+                PCE_Z[:, idxMesh] = y_mean
+                Model_Z[:, idxMesh] = model_outs[:, 0]
+
+        # ---------------- 3D plot for PCEModel -----------------------
+        fig_PCE = plt.figure()
+        ax = plt.axes(projection='3d')
+        ax.plot_surface(X, Y, PCE_Z, rstride=1, cstride=1,
+                        cmap='viridis', edgecolor='none')
+        ax.set_title('PCEModel')
+        ax.set_xlabel('$x_1$')
+        ax.set_ylabel('$x_2$')
+        ax.set_zlabel('$f(x_1,x_2)$')
+
+        plt.grid()
+        plt.show()
+
+        #  Saving the figure
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # save the figure to file
+        fig_PCE.savefig(f'{self.out_dir}/{newpath}/3DPlot_PCEModel.pdf',
+                        bbox_inches='tight')
+        plt.close(fig_PCE)
+
+        # ---------------- 3D plot for Model -----------------------
+        fig_Model = plt.figure()
+        ax = plt.axes(projection='3d')
+        ax.plot_surface(X, Y, PCE_Z, rstride=1, cstride=1,
+                        cmap='viridis', edgecolor='none')
+        ax.set_title('Model')
+        ax.set_xlabel('$x_1$')
+        ax.set_ylabel('$x_2$')
+        ax.set_zlabel('$f(x_1,x_2)$')
+
+        plt.grid()
+        plt.show()
+
+        # Save the figure
+        fig_Model.savefig(f'{self.out_dir}/{newpath}/3DPlot_Model.pdf',
+                          bbox_inches='tight')
+        plt.close(fig_Model)
+
+        return
+
+    # -------------------------------------------------------------------------
+    def compute_pce_moments(self):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moments (mean) of outpust.
+        pce_means: dict
+            The first moments (mean) of outpust.
+
+        """
+
+        MetaModel = self.MetaModel
+        outputs = self.ModelObj.Output.names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(MetaModel.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    PCA = MetaModel.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.inverse_transform(pce_mean)
+                    stds[output] = np.sqrt(np.dot(pce_var,
+                                                  PCA.components_**2))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+            # Print a report table
+            print("\n>>>>> Moments of {} <<<<<".format(output))
+            print("\nIndex  |  Mean   |  Std. deviation")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(pce_means[output],
+                                             pce_stds[output]))))
+        print('-'*40)
+
+        return pce_means, pce_stds
+    
+    # -------------------------------------------------------------------------
+    def compute_mc_moments(self, num_mc_samples):
+        """
+        Computes the first two moments of the metamodel using MC samples.
+
+        Parameters
+        ----------
+        num_mc_samples : int
+            The number of samples to estimate the moments by
+
+        Returns
+        -------
+        pce_means: dict
+            The first moments (mean) of outpust.
+        pce_means: dict
+            The first moments (mean) of outpust.
+
+        """
+        # Get the MC-samples
+        samples = self.engine.ExpDesign.generate_samples(num_mc_samples)
+        print('Random samples are genereated, now starting the evaluations')
+        
+        # Run the metamodel on the samples
+        approx, stds = self.engine.MetaModel.eval_metamodel(samples)
+        
+        # Estimate the overall mean and std from this
+        pce_means = {}
+        pce_stds = {}
+        for key in approx.keys():
+            if key == 'x_values':
+                continue
+            pce_means[key] = np.mean(approx[key], axis = 0)
+            pce_stds[key] = np.std(approx[key], axis = 0)
+                
+        return pce_means, pce_stds
+
+    # -------------------------------------------------------------------------
+    def _get_sample(self, n_samples=None):
+        """
+        Generates random samples taken from the input parameter space.
+
+        Returns
+        -------
+        samples : array of shape (n_samples, n_params)
+            Generated samples.
+
+        """
+        if n_samples is None:
+            n_samples = self.n_samples
+        self.samples = self.ExpDesign.generate_samples(n_samples,
+                                                       sampling_method='random')
+        return self.samples
+
+    # -------------------------------------------------------------------------
+    def _eval_model(self, samples=None, key_str='Valid'):
+        """
+        Evaluates Forward Model for the given number of self.samples or given
+        samples.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Samples to evaluate the model at. The default is None.
+        key_str : str, optional
+            Key string pass to the model. The default is 'Valid'.
+
+        Returns
+        -------
+        model_outs : dict
+            Dictionary of results.
+
+        """
+        Model = self.ModelObj
+
+        if samples is None:
+            samples = self._get_sample()
+            self.samples = samples
+        else:
+            self.n_samples = len(samples)
+
+        model_outs, _ = Model.run_model_parallel(samples, key_str=key_str)
+
+        return model_outs
+
+    # -------------------------------------------------------------------------
+    def _plot_validation(self):
+        """
+        Plots outputs for visual comparison of metamodel outputs with that of
+        the (full) original model.
+
+        Returns
+        -------
+        None.
+
+        """
+        PCEModel = self.MetaModel
+
+        # get the samples
+        x_val = self.samples
+        y_pce_val = self.pce_out_mean
+        y_val = self.model_out_dict
+
+        # Open a pdf for the plots
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        fig = plt.figure()
+        # Fit the data(train the model)
+        for key in y_pce_val.keys():
+
+            y_pce_val_ = y_pce_val[key]
+            y_val_ = y_val[key]
+
+            regression_model = LinearRegression()
+            regression_model.fit(y_pce_val_, y_val_)
+
+            # Predict
+            x_new = np.linspace(np.min(y_pce_val_), np.max(y_val_), 100)
+            y_predicted = regression_model.predict(x_new[:, np.newaxis])
+
+            plt.scatter(y_pce_val_, y_val_, color='gold', linewidth=2)
+            plt.plot(x_new, y_predicted, color='k')
+
+            # Calculate the adjusted R_squared and RMSE
+            # the total number of explanatory variables in the model
+            # (not including the constant term)
+            length_list = []
+            for key, value in PCEModel.coeffs_dict['b_1'][key].items():
+                length_list.append(len(value))
+            n_predictors = min(length_list)
+            n_samples = x_val.shape[0]
+
+            R2 = r2_score(y_pce_val_, y_val_)
+            AdjR2 = 1 - (1 - R2) * (n_samples - 1) / \
+                (n_samples - n_predictors - 1)
+            rmse = mean_squared_error(y_pce_val_, y_val_, squared=False)
+
+            plt.annotate(f'RMSE = {rmse:.3f}\n Adjusted $R^2$ = {AdjR2:.3f}',
+                         xy=(0.05, 0.85), xycoords='axes fraction')
+
+            plt.ylabel("Original Model")
+            plt.xlabel("PCE Model")
+            plt.grid()
+            plt.show()
+
+            # save the current figure
+            plot_name = key.replace(' ', '_')
+            fig.savefig(f'{self.out_dir}/{newpath}/Model_vs_PCEModel_{plot_name}.pdf',
+                        bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+
+    # -------------------------------------------------------------------------
+    def _plot_validation_multi(self, x_values=[], x_axis="x [m]"):
+        """
+        Plots outputs for visual comparison of metamodel outputs with that of
+        the (full) multioutput original model
+
+        Parameters
+        ----------
+        x_values : list or array, optional
+            List of x values. The default is [].
+        x_axis : str, optional
+            Label of the x axis. The default is "x [m]".
+
+        Returns
+        -------
+        None.
+
+        """
+        Model = self.ModelObj
+
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # List of markers and colors
+        color = cycle((['b', 'g', 'r', 'y', 'k']))
+        marker = cycle(('x', 'd', '+', 'o', '*'))
+
+        fig = plt.figure()
+        # Plot the model vs PCE model
+        for keyIdx, key in enumerate(Model.Output.names):
+
+            y_pce_val = self.pce_out_mean[key]
+            y_pce_val_std = self.pce_out_std[key]
+            y_val = self.model_out_dict[key]
+            try:
+                x = self.model_out_dict['x_values'][key]
+            except (TypeError, IndexError):
+                x = x_values
+
+            for idx in range(y_val.shape[0]):
+                Color = next(color)
+                Marker = next(marker)
+
+                plt.plot(x, y_val[idx], color=Color, marker=Marker,
+                         label='$Y_{%s}^M$'%(idx+1))
+
+                plt.plot(x, y_pce_val[idx], color=Color, marker=Marker,
+                         linestyle='--',
+                         label='$Y_{%s}^{PCE}$'%(idx+1))
+                plt.fill_between(x, y_pce_val[idx]-1.96*y_pce_val_std[idx],
+                                 y_pce_val[idx]+1.96*y_pce_val_std[idx],
+                                 color=Color, alpha=0.15)
+
+            # Calculate the RMSE
+            rmse = mean_squared_error(y_pce_val, y_val, squared=False)
+            R2 = r2_score(y_pce_val[idx].reshape(-1, 1),
+                          y_val[idx].reshape(-1, 1))
+
+            plt.annotate(f'RMSE = {rmse:.3f}\n $R^2$ = {R2:.3f}',
+                         xy=(0.85, 0.1), xycoords='axes fraction')
+
+            plt.ylabel(key)
+            plt.xlabel(x_axis)
+            plt.legend(loc='best')
+            plt.grid()
+
+            # save the current figure
+            plot_name = key.replace(' ', '_')
+            fig.savefig(f'{self.out_dir}/{newpath}/Model_vs_PCEModel_{plot_name}.pdf',
+                        bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+
+        # Zip the subdirectories
+        Model.zip_subdirs(f'{Model.name}valid', f'{Model.name}valid_')
diff --git a/examples/analytical-function/bayesvalidrox/pylink/__init__.py b/examples/analytical-function/bayesvalidrox/pylink/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4bd81739faf43956324b30f6d8e5365b29d55677
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/pylink/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from .pylink import PyLinkForwardModel
+
+__all__ = [
+    "PyLinkForwardModel"
+    ]
diff --git a/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5b7c1b3926506fb279b856f55ca6120df31b8888
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1947ad354716d0293953761f0d35193f706cedc1
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0bbb522855ad250ad55bca46123c0f5023076291
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6ae7c14b35b60388e38fcbd3af64d04771a947c
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6b8b695cb653d82323fc0c075eca519e389c960e
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..746c82eb52be2e437c61bd201433f9d38b8ab177
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/pylink/__pycache__/pylink.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/pylink/pylink.py b/examples/analytical-function/bayesvalidrox/pylink/pylink.py
new file mode 100644
index 0000000000000000000000000000000000000000..0ef7d48e293987941d42ff1414efc1d9fc77af65
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/pylink/pylink.py
@@ -0,0 +1,807 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Calls to the model and evaluations
+"""
+
+from dataclasses import dataclass
+
+import os
+import shutil
+import h5py
+import numpy as np
+import time
+import zipfile
+import pandas as pd
+import multiprocessing
+from functools import partial
+import tqdm
+
+#from multiprocessing import get_context
+from multiprocess import get_context
+
+
+
+def within_range(out, minout, maxout):
+    """
+    Checks if all the values in out lie between minout and maxout
+
+    Parameters
+    ----------
+    out : array or list
+        Data to check against range
+    minout : int
+        Lower bound of the range
+    maxout : int
+        Upper bound of the range
+
+    Returns
+    -------
+    inside : bool
+        True if all values in out are in the specified range
+
+    """
+    try:
+        out = np.array(out)
+    except:
+        raise AttributeError('The given values should be a 1D array, but are not')
+    if out.ndim != 1:
+            raise AttributeError('The given values should be a 1D array, but are not')
+        
+    if minout > maxout:
+        raise ValueError('The lower and upper bounds do not form a valid range, they might be switched')
+    
+    inside = False
+    if (out > minout).all() and (out < maxout).all():
+        inside = True
+    return inside
+
+
+class PyLinkForwardModel(object):
+    """
+    A forward model binder
+
+    This calss serves as a code wrapper. This wrapper allows the execution of
+    a third-party software/solver within the scope of BayesValidRox.
+
+    Attributes
+    ----------
+    link_type : str
+        The type of the wrapper. The default is `'pylink'`. This runs the
+        third-party software or an executable using a shell command with given
+        input files.
+        Second option is `'function'` which assumed that model can be run using
+        a function written separately in a Python script.
+    name : str
+        Name of the model.
+    py_file : str
+        Python file name without `.py` extension to be run for the `'function'`
+        wrapper. Note that the name of the python file and that of the function
+        must be simillar. This function must recieve the parameters in an array
+        of shape `(n_samples, n_params)` and returns a dictionary with the
+        x_values and output arrays for given output names.
+    func_args : dict
+        Additional arguments for the python file. The default is `{}`.
+    shell_command : str
+        Shell command to be executed for the `'pylink'` wrapper.
+    input_file : str or list
+        The input file to be passed to the `'pylink'` wrapper.
+    input_template : str or list
+        A template input file to be passed to the `'pylink'` wrapper. This file
+        must be a copy of `input_file` with `<Xi>` place holder for the input
+        parameters defined using `inputs` class, with i being the number of
+        parameter. The file name ending should include `.tpl` before the actual
+        extension of the input file, for example, `params.tpl.input`.
+    aux_file : str or list
+        The list of auxiliary files needed for the `'pylink'` wrapper.
+    exe_path : str
+        Execution path if you wish to run the model for the `'pylink'` wrapper
+        in another directory. The default is `None`, which corresponds to the
+        currecnt working directory.
+    output_file_names : list of str
+        List of the name of the model output text files for the `'pylink'`
+        wrapper.
+    output_names : list of str
+        List of the model outputs to be used for the analysis.
+    output_parser : str
+        Name of the model parser file (without `.py` extension) that recieves
+        the `output_file_names` and returns a 2d-array with the first row being
+        the x_values, e.g. x coordinates or time and the rest of raws pass the
+        simulation output for each model output defined in `output_names`. Note
+        that again here the name of the file and that of the function must be
+        the same.
+    multi_process: bool
+        Whether the model runs to be executed in parallel for the `'pylink'`
+        wrapper. The default is `True`.
+    n_cpus: int
+        The number of cpus to be used for the parallel model execution for the
+        `'pylink'` wrapper. The default is `None`, which corresponds to all
+        available cpus.
+    meas_file : str
+        The name of the measurement text-based file. This file must contain
+        x_values as the first column and one column for each model output. The
+        default is `None`. Only needed for the Bayesian Inference.
+    meas_file_valid : str
+        The name of the measurement text-based file for the validation. The
+        default is `None`. Only needed for the validation with Bayesian
+        Inference.
+    mc_ref_file : str
+        The name of the text file for the Monte-Carlo reference (mean and
+        standard deviation) values. It must contain `x_values` as the first
+        column, `mean` as the second column and `std` as the third. It can be
+        used to compare the estimated moments using meta-model in the post-
+        processing step. This is only available for one output.
+    obs_dict : dict
+        A dictionary containing the measurement text-based file. It must
+        contain `x_values` as the first item and one item for each model output
+        . The default is `{}`. Only needed for the Bayesian Inference.
+    obs_dict_valid : dict
+        A dictionary containing the validation measurement text-based file. It
+        must contain `x_values` as the first item and one item for each model
+        output. The default is `{}`.
+    mc_ref_dict : dict
+        A dictionary containing the Monte-Carlo reference (mean and standard
+        deviation) values. It must contain `x_values` as the first item and
+        `mean` as the second item and `std` as the third. The default is `{}`.
+        This is only available for one output.
+    """
+
+    # Nested class
+    @dataclass
+    class OutputData(object):
+        parser: str = ""
+        names: list = None
+        file_names: list = None
+
+    def __init__(self, link_type='pylink', name=None, py_file=None,
+                 func_args={}, shell_command='', input_file=None,
+                 input_template=None, aux_file=None, exe_path='',
+                 output_file_names=[], output_names=[], output_parser='',
+                 multi_process=True, n_cpus=None, meas_file=None,
+                 meas_file_valid=None, mc_ref_file=None, obs_dict={},
+                 obs_dict_valid={}, mc_ref_dict={}):
+        self.link_type = link_type
+        self.name = name
+        self.shell_command = shell_command
+        self.py_file = py_file
+        self.func_args = func_args
+        self.input_file = input_file
+        self.input_template = input_template
+        self.aux_file = aux_file
+        self.exe_path = exe_path
+        self.multi_process = multi_process
+        self.n_cpus = n_cpus
+        self.Output = self.OutputData(
+            parser=output_parser,
+            names=output_names,
+            file_names=output_file_names,
+        )
+        self.n_outputs = len(self.Output.names)
+        self.meas_file = meas_file
+        self.meas_file_valid = meas_file_valid
+        self.mc_ref_file = mc_ref_file
+        self.observations = obs_dict
+        self.observations_valid = obs_dict_valid
+        self.mc_reference = mc_ref_dict
+
+    # -------------------------------------------------------------------------
+    def read_observation(self, case='calib'):
+        """
+        Reads/prepare the observation/measurement data for
+        calibration.
+        
+        Parameters
+        ----------
+        case : str
+            The type of observation to read in. Can be either 'calib',
+            'valid' or 'mc_ref'
+
+        Returns
+        -------
+        DataFrame
+            A dataframe with the calibration data.
+
+        """
+        # TOOD: check that what is read in/transformed matches the expected form of data/reference
+        if case.lower() == 'calib':
+            if isinstance(self.observations, dict) and bool(self.observations):
+                self.observations = pd.DataFrame.from_dict(self.observations)
+            elif self.meas_file is not None:
+                file_path = os.path.join(os.getcwd(), self.meas_file)
+                self.observations = pd.read_csv(file_path, delimiter=',')
+            elif isinstance(self.observations, pd.DataFrame):
+                self.observations = self.observations
+            else:
+                raise Exception("Please provide the observation data as a "
+                                "dictionary via observations attribute or pass"
+                                " the csv-file path to MeasurementFile "
+                                "attribute")
+            # Compute the number of observation
+            self.n_obs = self.observations[self.Output.names].notnull().sum().values.sum()
+            return self.observations
+            
+        elif case.lower() == 'valid':
+            if isinstance(self.observations_valid, dict) and \
+              bool(self.observations_valid):
+                self.observations_valid = pd.DataFrame.from_dict(self.observations_valid)
+            elif self.meas_file_valid is not None:
+                file_path = os.path.join(os.getcwd(), self.meas_file_valid)
+                self.observations_valid = pd.read_csv(file_path, delimiter=',')
+            elif isinstance(self.observations_valid, pd.DataFrame):
+                self.observations_valid = self.observations_valid
+            else:
+                raise Exception("Please provide the observation data as a "
+                                "dictionary via observations attribute or pass"
+                                " the csv-file path to MeasurementFile "
+                                "attribute")
+            # Compute the number of observation
+            self.n_obs_valid = self.observations_valid[self.Output.names].notnull().sum().values.sum()
+            return self.observations_valid
+                
+        elif case.lower() == 'mc_ref':
+            if self.mc_ref_file is None and \
+               isinstance(self.mc_reference, pd.DataFrame):
+                return self.mc_reference
+            elif isinstance(self.mc_reference, dict) and bool(self.mc_reference):
+                self.mc_reference = pd.DataFrame.from_dict(self.mc_reference)
+            elif self.mc_ref_file is not None:
+                file_path = os.path.join(os.getcwd(), self.mc_ref_file)
+                self.mc_reference = pd.read_csv(file_path, delimiter=',')
+            else:
+                self.mc_reference = None
+            return self.mc_reference
+
+
+    # -------------------------------------------------------------------------
+    def read_output(self):
+        """
+        Reads the the parser output file and returns it as an
+         executable function. It is required when the models returns the
+         simulation outputs in csv files.
+
+        Returns
+        -------
+        Output : func
+            Output parser function.
+
+        """
+        output_func_name = self.Output.parser
+
+        output_func = getattr(__import__(output_func_name), output_func_name)
+
+        file_names = []
+        for File in self.Output.file_names:
+            file_names.append(os.path.join(self.exe_path, File))
+        try:
+            output = output_func(self.name, file_names)
+        except TypeError:
+            output = output_func(file_names)
+        return output
+
+    # -------------------------------------------------------------------------
+    def update_input_params(self, new_input_file, param_set):
+        """
+        Finds this pattern with <X1> in the new_input_file and replace it with
+         the new value from the array param_sets.
+
+        Parameters
+        ----------
+        new_input_file : list
+            List of the input files with the adapted names.
+        param_set : array of shape (n_params)
+            Parameter set.
+
+        Returns
+        -------
+        None.
+
+        """
+        NofPa = param_set.shape[0]
+        text_to_search_list = [f'<X{i+1}>' for i in range(NofPa)]
+
+        for filename in new_input_file:
+            # Read in the file
+            with open(filename, 'r') as file:
+                filedata = file.read()
+
+            # Replace the target string
+            for text_to_search, params in zip(text_to_search_list, param_set):
+                filedata = filedata.replace(text_to_search, f'{params:0.4e}')
+
+            # Write the file out again
+            with open(filename, 'w') as file:
+                file.write(filedata)
+
+    # -------------------------------------------------------------------------
+    def run_command(self, command, output_file_names):
+        """
+        Runs the execution command given by the user to run the given model.
+        It checks if the output files have been generated. If yes, the jobe is
+        done and it extracts and returns the requested output(s). Otherwise,
+        it executes the command again.
+
+        Parameters
+        ----------
+        command : str
+            The shell command to be executed.
+        output_file_names : list
+            Name of the output file names.
+
+        Returns
+        -------
+        simulation_outputs : array of shape (n_obs, n_outputs)
+            Simulation outputs.
+
+        """
+
+        # Check if simulation is finished
+        while True:
+            time.sleep(3)
+            files = os.listdir(".")
+            if all(elem in files for elem in output_file_names):
+                break
+            else:
+                # Run command
+                Process = os.system(f'./../{command}')
+                if Process != 0:
+                    print('\nMessage 1:')
+                    print(f'\tIf the value of \'{Process}\' is a non-zero value'
+                          ', then compilation problems occur \n' % Process)          
+        os.chdir("..")
+
+        # Read the output
+        simulation_outputs = self.read_output()
+
+        return simulation_outputs
+
+    # -------------------------------------------------------------------------
+    def run_forwardmodel(self, xx):
+        """
+        This function creates subdirectory for the current run and copies the
+        necessary files to this directory and renames them. Next, it executes
+        the given command.
+
+        Parameters
+        ----------
+        xx : tuple
+            A tuple including parameter set, simulation number and key string.
+
+        Returns
+        -------
+        output : array of shape (n_outputs+1, n_obs)
+            An array passed by the output paraser containing the x_values as
+            the first row and the simulations results stored in the the rest of
+            the array.
+
+        """
+        c_points, run_no, key_str = xx
+
+        # Handle if only one imput file is provided
+        if not isinstance(self.input_template, list):
+            self.input_template = [self.input_template]
+        if not isinstance(self.input_file, list):
+            self.input_file = [self.input_file]
+
+        new_input_file = []
+        # Loop over the InputTemplates:
+        for in_temp in self.input_template:
+            if '/' in in_temp:
+                in_temp = in_temp.split('/')[-1]
+            new_input_file.append(in_temp.split('.tpl')[0] + key_str +
+                                  f"_{run_no+1}" + in_temp.split('.tpl')[1])
+
+        # Create directories
+        newpath = self.name + key_str + f'_{run_no+1}'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # Copy the necessary files to the directories
+        print(self.input_template)
+        for in_temp in self.input_template:
+            # Input file(s) of the model
+            shutil.copy2(in_temp, newpath)
+        # Auxiliary file
+        if self.aux_file is not None:
+            shutil.copy2(self.aux_file, newpath)  # Auxiliary file
+
+        # Rename the Inputfile and/or auxiliary file
+        os.chdir(newpath)
+        for input_tem, input_file in zip(self.input_template, new_input_file):
+            if '/' in input_tem:
+                input_tem = input_tem.split('/')[-1]
+            os.rename(input_tem, input_file)
+
+        # Update the parametrs in Input file
+        self.update_input_params(new_input_file, c_points)
+
+        # Update the user defined command and the execution path
+        try:
+            new_command = self.shell_command.replace(self.input_file[0],
+                                                     new_input_file[0])
+            new_command = new_command.replace(self.input_file[1],
+                                              new_input_file[1])
+        except:
+            new_command = self.shell_command.replace(self.input_file[0],
+                                                     new_input_file[0])
+        # Set the exe path if not provided
+        if not bool(self.exe_path):
+            self.exe_path = os.getcwd()
+
+        # Run the model
+        print(new_command)
+        output = self.run_command(new_command, self.Output.file_names)
+
+        return output
+
+    # -------------------------------------------------------------------------
+    def run_model_parallel(self, c_points, prevRun_No=0, key_str='',
+                           mp=True, verbose=True):
+        """
+        Runs model simulations. If mp is true (default), then the simulations
+         are started in parallel.
+
+        Parameters
+        ----------
+        c_points : array of shape (n_samples, n_params)
+            Collocation points (training set).
+        prevRun_No : int, optional
+            Previous run number, in case the sequential design is selected.
+            The default is `0`.
+        key_str : str, optional
+            A descriptive string for validation runs. The default is `''`.
+        mp : bool, optional
+            Multiprocessing. The default is `True`.
+        verbose: bool, optional
+            Verbosity. The default is `True`.
+
+        Returns
+        -------
+        all_outputs : dict
+            A dictionary with x values (time step or point id) and all outputs.
+            Each key contains an array of the shape `(n_samples, n_obs)`.
+        new_c_points : array
+            Updated collocation points (training set). If a simulation does not
+            executed successfully, the parameter set is removed.
+
+        """
+
+        # Initilization
+        n_c_points = len(c_points)
+        all_outputs = {}
+        
+        # If the link type is UM-Bridge, then no parallel needs to be started from here
+        if self.link_type.lower() == 'umbridge':
+            import umbridge 
+            if not hasattr(self, 'x_values'):
+                raise AttributeError('For model type `umbridge` the attribute `x_values` needs to be set for the model!')
+            # Init model
+            #model = umbridge.HTTPModel('http://localhost:4242', 'forward')
+            self.model = umbridge.HTTPModel(self.host, 'forward') # TODO: is this always forward?
+            Function = self.uMBridge_model
+
+        # Extract the function
+        if self.link_type.lower() == 'function':
+            # Prepare the function
+            Function = getattr(__import__(self.py_file), self.py_file)
+        # ---------------------------------------------------------------
+        # -------------- Multiprocessing with Pool Class ----------------
+        # ---------------------------------------------------------------
+        # Start a pool with the number of CPUs
+        if self.n_cpus is None:
+            n_cpus = multiprocessing.cpu_count()
+        else:
+            n_cpus = self.n_cpus
+
+        # Run forward model
+        if n_c_points == 1 or not mp:
+            if n_c_points== 1:
+                if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                    group_results = Function(c_points, **self.func_args)
+                else:
+                    group_results = self.run_forwardmodel(
+                        (c_points[0], prevRun_No, key_str)
+                        )
+            else:
+                for i in range(c_points.shape[0]):
+                    if i == 0:
+                        if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                            group_results = Function(np.array([c_points[0]]), **self.func_args)
+                        else:
+                            group_results = self.run_forwardmodel(
+                                (c_points[0], prevRun_No, key_str)
+                                )
+                        for key in group_results:
+                            if key != 'x_values':
+                                group_results[key] = [group_results[key]]
+                    else: 
+                        if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                            res = Function(np.array([c_points[i]]), **self.func_args)
+                        else:
+                            res = self.run_forwardmodel(
+                                (c_points[i], prevRun_No, key_str)
+                                )
+                        for key in res:
+                            if key != 'x_values':
+                                group_results[key].append(res[key])
+        
+                for key in group_results:
+                    if key != 'x_values':
+                        group_results[key]= np.array(group_results[key])
+
+        elif self.multi_process or mp:
+            with get_context('spawn').Pool(n_cpus) as p:
+            #with multiprocessing.Pool(n_cpus) as p:
+                
+                if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                    imap_var = p.imap(partial(Function, **self.func_args),
+                                      c_points[:, np.newaxis])
+                else:
+                    args = zip(c_points,
+                               [prevRun_No+i for i in range(n_c_points)],
+                               [key_str]*n_c_points)
+                    imap_var = p.imap(self.run_forwardmodel, args)
+
+                if verbose:
+                    desc = f'Running forward model {key_str}'
+                    group_results = list(tqdm.tqdm(imap_var, total=n_c_points,
+                                                   desc=desc))
+                else:
+                    group_results = list(imap_var)
+
+        # Check for NaN
+        for var_i, var in enumerate(self.Output.names):
+            # If results are given as one dictionary
+            if isinstance(group_results, dict):
+                Outputs = np.asarray(group_results[var])
+            # If results are given as list of dictionaries
+            elif isinstance(group_results, list):
+                cnt = 0
+                for item in group_results:
+                    #print(var, c_points[cnt],item[var])
+                    cnt+=1
+                Outputs = np.asarray([item[var] for item in group_results],
+                                     dtype=np.float64)
+            NaN_idx = np.unique(np.argwhere(np.isnan(Outputs))[:, 0])
+            new_c_points = np.delete(c_points, NaN_idx, axis=0)
+            all_outputs[var] = np.atleast_2d(
+                np.delete(Outputs, NaN_idx, axis=0)
+                )
+
+        # Print the collocation points whose simulations crashed
+        if len(NaN_idx) != 0:
+            print('\n')
+            print('*'*20)
+            print("\nThe following parameter sets have been removed:\n",
+                  c_points[NaN_idx])
+            print("\n")
+            print('*'*20)
+
+        # Save time steps or x-values
+        if isinstance(group_results, dict):
+            all_outputs["x_values"] = group_results["x_values"]
+        elif any(isinstance(i, dict) for i in group_results):
+            all_outputs["x_values"] = group_results[0]["x_values"]
+
+        # Store simulations in a hdf5 file
+        self._store_simulations(
+            c_points, all_outputs, NaN_idx, key_str, prevRun_No
+            )
+
+        return all_outputs, new_c_points
+    
+    def uMBridge_model(self, params):
+        """
+        Function that calls a UMBridge model and transforms its output into the 
+        shape expected for the surrogate.
+    
+        Parameters
+        ----------
+        params : 2d np.array, shape (#samples, #params)
+            The parameter values for which the model is run.
+    
+        Returns
+        -------
+        dict
+            The transformed model outputs.
+    
+        """
+        # Run the model
+        #out = np.array(model(np.ndarray.tolist(params), {'level':0}))
+        out = np.array(self.model(np.ndarray.tolist(params), self.modelparams))
+        
+        # Sort into dict
+        out_dict = {}
+        cnt = 0
+        for key in self.Output.names:
+        #    # If needed resort into single-value outputs
+        #    if self.output_type == 'single-valued':
+        #        if out.shape[1]>1:  # TODO: this doesn't fully seem correct??
+        #            for i in range(out[:,key]): # TODO: this doesn't fully seem correct??
+        #                new_key = key+str(i)
+        #                if new_key not in self.Output.names:
+        #                    self.Output.names.append(new_key)
+        #                    if i == 0:
+        #                        self.Ouptut.names.remove(key)
+        #                out_dict[new_key] = out[:,cnt,i] # TODO: not sure about this, need to test
+        #        else: 
+        #            out_dict[key] = out[:,cnt]
+        #            
+        #        
+        #    else:
+            out_dict[key] = out[:,cnt]
+            cnt += 1
+        
+            
+        ## TODO: how to deal with the x-values?
+        #if self.output_type == 'single-valued':
+        #    out_dict['x_values'] = [0]
+        #else:
+        #    out_dict['x_values'] = np.arange(0,out[:,0].shape[0],1)
+        out_dict['x_values'] = self.x_values
+        
+        #return {'T1':out[:,0], 'T2':out[:,1], 'H1':out[:,2], 'H2':out[:,3], 
+       #         'x_values':[0]}
+        return out_dict
+
+    # -------------------------------------------------------------------------
+    def _store_simulations(self, c_points, all_outputs, NaN_idx, key_str,
+                           prevRun_No):
+        """
+        
+
+        Parameters
+        ----------
+        c_points : TYPE
+            DESCRIPTION.
+        all_outputs : TYPE
+            DESCRIPTION.
+        NaN_idx : TYPE
+            DESCRIPTION.
+        key_str : TYPE
+            DESCRIPTION.
+        prevRun_No : TYPE
+            DESCRIPTION.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        # Create hdf5 metadata
+        if key_str == '':
+            hdf5file = f'ExpDesign_{self.name}.hdf5'
+        else:
+            hdf5file = f'ValidSet_{self.name}.hdf5'
+        hdf5_exist = os.path.exists(hdf5file)
+        file = h5py.File(hdf5file, 'a')
+
+        # ---------- Save time steps or x-values ----------
+        if not hdf5_exist:
+            if type(all_outputs["x_values"]) is dict:
+                grp_x_values = file.create_group("x_values/")
+                for varIdx, var in enumerate(self.Output.names):
+                    grp_x_values.create_dataset(
+                        var, data=all_outputs["x_values"][var]
+                        )
+            else:
+                file.create_dataset("x_values", data=all_outputs["x_values"])
+
+        # ---------- Save outputs ----------
+        for varIdx, var in enumerate(self.Output.names):
+            if not hdf5_exist:
+                grpY = file.create_group("EDY/"+var)
+            else:
+                grpY = file.get("EDY/"+var)
+
+            if prevRun_No == 0 and key_str == '':
+                grpY.create_dataset(f'init_{key_str}', data=all_outputs[var])
+            else:
+                try:
+                    oldEDY = np.array(file[f'EDY/{var}/adaptive_{key_str}'])
+                    del file[f'EDY/{var}/adaptive_{key_str}']
+                    data = np.vstack((oldEDY, all_outputs[var]))
+                except KeyError:
+                    data = all_outputs[var]
+                grpY.create_dataset('adaptive_'+key_str, data=data)
+
+            if prevRun_No == 0 and key_str == '':
+                grpY.create_dataset(f"New_init_{key_str}",
+                                    data=all_outputs[var])
+            else:
+                try:
+                    name = f'EDY/{var}/New_adaptive_{key_str}'
+                    oldEDY = np.array(file[name])
+                    del file[f'EDY/{var}/New_adaptive_{key_str}']
+                    data = np.vstack((oldEDY, all_outputs[var]))
+                except KeyError:
+                    data = all_outputs[var]
+                grpY.create_dataset(f'New_adaptive_{key_str}', data=data)
+
+        # ---------- Save CollocationPoints ----------
+        new_c_points = np.delete(c_points, NaN_idx, axis=0)
+        grpX = file.create_group("EDX") if not hdf5_exist else file.get("EDX")
+        if prevRun_No == 0 and key_str == '':
+            grpX.create_dataset("init_"+key_str, data=c_points)
+            if len(NaN_idx) != 0:
+                grpX.create_dataset("New_init_"+key_str, data=new_c_points)
+
+        else:
+            try:
+                name = f'EDX/adaptive_{key_str}'
+                oldCollocationPoints = np.array(file[name])
+                del file[f'EDX/adaptive_{key_str}']
+                data = np.vstack((oldCollocationPoints, new_c_points))
+            except KeyError:
+                data = new_c_points
+            grpX.create_dataset('adaptive_'+key_str, data=data)
+
+            if len(NaN_idx) != 0:
+                try:
+                    name = f'EDX/New_adaptive_{key_str}'
+                    oldCollocationPoints = np.array(file[name])
+                    del file[f'EDX/New_adaptive_{key_str}']
+                    data = np.vstack((oldCollocationPoints, new_c_points))
+                except KeyError:
+                    data = new_c_points
+                grpX.create_dataset('New_adaptive_'+key_str, data=data)
+
+        # Close h5py file
+        file.close()
+
+    # -------------------------------------------------------------------------
+    def zip_subdirs(self, dir_name, key):
+        """
+        Zips all the files containing the key(word).
+
+        Parameters
+        ----------
+        dir_name : str
+            Directory name.
+        key : str
+            Keyword to search for.
+
+        Returns
+        -------
+        None.
+
+        """
+        # setup file paths variable
+        dir_list = []
+        file_paths = []
+
+        # Read all directory, subdirectories and file lists
+        dir_path = os.getcwd()
+
+        for root, directories, files in os.walk(dir_path):
+            for directory in directories:
+                # Create the full filepath by using os module.
+                if key in directory:
+                    folderPath = os.path.join(dir_path, directory)
+                    dir_list.append(folderPath)
+
+        # Loop over the identified directories to store the file paths
+        for direct_name in dir_list:
+            for root, directories, files in os.walk(direct_name):
+                for filename in files:
+                    # Create the full filepath by using os module.
+                    filePath = os.path.join(root, filename)
+                    file_paths.append('.'+filePath.split(dir_path)[1])
+
+        # writing files to a zipfile
+        if len(file_paths) != 0:
+            zip_file = zipfile.ZipFile(dir_name+'.zip', 'w')
+            with zip_file:
+                # writing each file one by one
+                for file in file_paths:
+                    zip_file.write(file)
+
+            file_paths = [path for path in os.listdir('.') if key in path]
+
+            for path in file_paths:
+                shutil.rmtree(path)
+
+            print("\n")
+            print(f'{dir_name}.zip has been created successfully!\n')
+
+        return
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__init__.py b/examples/analytical-function/bayesvalidrox/surrogate_models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..70bfb20f570464c2907a0a4128f4ed99b6c13736
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from .surrogate_models import MetaModel
+
+__all__ = [
+    "MetaModel"
+    ]
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8c10c82287a57ba1e3b4dd428962e57cdfbc5c58
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4b73e63a3096fbc9afc41bae35a3fcc1d7851166
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f1a3fcc2eed66172304cd27ab5fe111ca0198bf5
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/adaptPlot.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/adaptPlot.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2854217e56fecb2456011a91a984951fed9cbcbb
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/adaptPlot.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ffab8b4f67e52a3128aa8740301f958a0d72c502
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..331e387822163df3108bcfc6ee48301f71d7b4c3
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..82737a42dd7351d06b703b3da838031ba95979da
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..92d0cc0e7a0a07123fdfbc2c777d1b9281a43344
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..15917cec53bee82813d19f9029552d6ccba086cf
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..063355b16a397fb5fd89d38daa0d3ca5a8506766
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/engine.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/engine.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..129cf9a600a5d44a75d32f91f8b5a3691faff286
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/engine.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cbfe4d97e8e83ebc276e45ba6e84f514784f1d0b
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1d789b4515f89e02f006dafd2c9d85b8e7bea110
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..27878248cbdaa2abf0af9d51d061aa6e2db86f43
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fc01f0f01b8bc70d438a3317b87d304883456f15
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e6dfce1df1a397fca74f54ccdfe90908cfd615f1
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e7e0226dc1e28c09b9cd09a610599007b2267e3c
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs_.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs_.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..076580dd7fa9e11559ef202903d44e00e25b8a26
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exp_designs_.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..af7431ce432969f095d1c07f429b8129cb5a2def
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3ca9d281b7fd88a0951f52dac6bff569d402740d
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..98964152a4ea29f85f061ea6ec7daa3df7487230
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..16a383b994853cdb226f7b7fb291cdbef789e1f7
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3d5029084d1c61da714440410f73c745713a34cd
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c3f24cf9cb753b59f9e226f828a7c3598ac65a9a
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/input_space.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/input_space.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7ddbbbd244be38568b61c4e2bf42cddfabf3fdc2
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/input_space.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ed2b0f6f101965fb42fe059ec79e8084a4b3a9bf
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1532dc02c21ed5bd3cdc6f0b98478d396f56071b
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b0c91ff3182adb1245aa7f20656e8e94336a438c
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/loss_function.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/loss_function.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a1e26394365e683dbec6da95fbe223ebcee10ef4
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/loss_function.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7ebbfad9c34b1f9e6c819ea7cf7852af65591444
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b2b3f8f40181637b13ee1156ffa8a03c1ffce8b3
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bd6eb8fcd459fd95ff1b85514f996344b6e4880c
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..543416416ef052c2402c2e9a97976dc4aab22866
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ef8b32284f1c504f9e5aaf483cbaceba9a47185d
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9f1ec9479869fec42f363e7f46e07dba2f1c6be5
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..97ed55acc1e800a138ddf489ccea709a8b28f634
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9809c07a1479221297a3af1a9c0efeccc4e6863e
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0843cdf8bd820d9cccfdcde3a1193f3f416ccc10
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..30079dce4bc04802324f720381b872c1a2f64018
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1aff76e3864a194b97534ad92aa7bb35db1038da
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7a3bbc05003eab405d52a934e83053ae50080d25
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/sequential_design.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/sequential_design.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..af35ff42f1ea1e8ee19d476f7b51c19199513cde
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/sequential_design.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-310.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b856dfef2c2658af8ecc6b1ab85b99c499a39705
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-310.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f46bce593fcfaf95cb1a46ef07808179601d655b
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-39.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f44a774b4165e6ff769d8db2f2c13c4dd0cbe8b3
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-39.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/wrapper.cpython-311.pyc b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/wrapper.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a6d61c979e164a0fd590b3f925c2d1ac68adb4fc
Binary files /dev/null and b/examples/analytical-function/bayesvalidrox/surrogate_models/__pycache__/wrapper.cpython-311.pyc differ
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/adaptPlot.py b/examples/analytical-function/bayesvalidrox/surrogate_models/adaptPlot.py
new file mode 100644
index 0000000000000000000000000000000000000000..102f0373c1086ba4420ada2fb2fc723b78bbd53f
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/adaptPlot.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Thu Aug 13 13:46:24 2020
+
+@author: farid
+"""
+import os
+from sklearn.metrics import mean_squared_error, r2_score
+from itertools import cycle
+from matplotlib.backends.backend_pdf import PdfPages
+import matplotlib.pyplot as plt
+
+
+def adaptPlot(PCEModel, Y_Val, Y_PC_Val, Y_PC_Val_std, x_values=[],
+              plotED=False, SaveFig=True):
+
+    NrofSamples = PCEModel.ExpDesign.n_new_samples
+    initNSamples = PCEModel.ExpDesign.n_init_samples
+    itrNr = 1 + (PCEModel.ExpDesign.X.shape[0] - initNSamples)//NrofSamples
+
+    oldEDY = PCEModel.ExpDesign.Y
+
+    if SaveFig:
+        newpath = 'adaptivePlots'
+        os.makedirs(newpath, exist_ok=True)
+
+        # create a PdfPages object
+        pdf = PdfPages(f'./{newpath}/Model_vs_PCEModel_itr_{itrNr}.pdf')
+
+    # List of markers and colors
+    color = cycle((['b', 'g', 'r', 'y', 'k']))
+    marker = cycle(('x', 'd', '+', 'o', '*'))
+
+    OutNames = list(Y_Val.keys())
+    x_axis = 'Time [s]'
+
+    if len(OutNames) == 1:
+        OutNames.insert(0, x_axis)
+    try:
+        x_values = Y_Val['x_values']
+    except KeyError:
+        x_values = x_values
+
+    fig = plt.figure(figsize=(24, 16))
+
+    # Plot the model vs PCE model
+    for keyIdx, key in enumerate(PCEModel.ModelObj.Output.names):
+        Y_PC_Val_ = Y_PC_Val[key]
+        Y_PC_Val_std_ = Y_PC_Val_std[key]
+        Y_Val_ = Y_Val[key]
+        if Y_Val_.ndim == 1:
+            Y_Val_ = Y_Val_.reshape(1, -1)
+        old_EDY = oldEDY[key]
+        if isinstance(x_values, dict):
+            x = x_values[key]
+        else:
+            x = x_values
+
+        for idx, y in enumerate(Y_Val_):
+            Color = next(color)
+            Marker = next(marker)
+
+            plt.plot(
+                x, y, color=Color, marker=Marker,
+                lw=2.0, label='$Y_{%s}^{M}$'%(idx+itrNr)
+                )
+
+            plt.plot(
+                x, Y_PC_Val_[idx], color=Color, marker=Marker,
+                lw=2.0, linestyle='--', label='$Y_{%s}^{PCE}$'%(idx+itrNr)
+                )
+            plt.fill_between(
+                x, Y_PC_Val_[idx]-1.96*Y_PC_Val_std_[idx],
+                Y_PC_Val_[idx]+1.96*Y_PC_Val_std_[idx], color=Color,
+                alpha=0.15
+                )
+
+            if plotED:
+                for output in old_EDY:
+                    plt.plot(x, output, color='grey', alpha=0.1)
+
+        # Calculate the RMSE
+        RMSE = mean_squared_error(Y_PC_Val_, Y_Val_, squared=False)
+        R2 = r2_score(Y_PC_Val_.reshape(-1, 1), Y_Val_.reshape(-1, 1))
+
+        plt.ylabel(key)
+        plt.xlabel(x_axis)
+        plt.title(key)
+
+        ax = fig.axes[0]
+        ax.legend(loc='best', frameon=True)
+        fig.canvas.draw()
+        ax.text(0.65, 0.85,
+                f'RMSE = {round(RMSE, 3)}\n$R^2$ = {round(R2, 3)}',
+                transform=ax.transAxes, color='black',
+                bbox=dict(facecolor='none',
+                          edgecolor='black',
+                          boxstyle='round,pad=1')
+                )
+        plt.grid()
+
+        if SaveFig:
+            # save the current figure
+            pdf.savefig(fig, bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+    pdf.close()
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/apoly_construction.py b/examples/analytical-function/bayesvalidrox/surrogate_models/apoly_construction.py
new file mode 100644
index 0000000000000000000000000000000000000000..40830fe8aaa94248df4828c0c49bd4d23e755abd
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/apoly_construction.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import numpy as np
+
+
+def apoly_construction(Data, degree):
+    """
+    Construction of Data-driven Orthonormal Polynomial Basis
+    Author: Dr.-Ing. habil. Sergey Oladyshkin
+    Department of Stochastic Simulation and Safety Research for Hydrosystems
+    Institute for Modelling Hydraulic and Environmental Systems
+    Universitaet Stuttgart, Pfaffenwaldring 5a, 70569 Stuttgart
+    E-mail: Sergey.Oladyshkin@iws.uni-stuttgart.de
+    http://www.iws-ls3.uni-stuttgart.de
+    The current script is based on definition of arbitrary polynomial chaos
+    expansion (aPC), which is presented in the following manuscript:
+    Oladyshkin, S. and W. Nowak. Data-driven uncertainty quantification using
+    the arbitrary polynomial chaos expansion. Reliability Engineering & System
+    Safety, Elsevier, V. 106, P.  179-190, 2012.
+    DOI: 10.1016/j.ress.2012.05.002.
+
+    Parameters
+    ----------
+    Data : array
+        Raw data.
+    degree : int
+        Maximum polynomial degree.
+
+    Returns
+    -------
+    Polynomial : array
+        The coefficients of the univariate orthonormal polynomials.
+
+    """
+    if Data.ndim !=1:
+        raise AttributeError('Data should be a 1D array')
+
+    # Initialization
+    dd = degree + 1
+    nsamples = len(Data)
+
+    # Forward linear transformation (Avoiding numerical issues)
+    MeanOfData = np.mean(Data)
+    Data = Data/MeanOfData
+
+    # Compute raw moments of input data
+    raw_moments = [np.sum(np.power(Data, p))/nsamples for p in range(2*dd+2)]
+
+    # Main Loop for Polynomial with degree up to dd
+    PolyCoeff_NonNorm = np.empty((0, 1))
+    Polynomial = np.zeros((dd+1, dd+1))
+
+    for degree in range(dd+1):
+        Mm = np.zeros((degree+1, degree+1))
+        Vc = np.zeros((degree+1))
+
+        # Define Moments Matrix Mm
+        for i in range(degree+1):
+            for j in range(degree+1):
+                if (i < degree):
+                    Mm[i, j] = raw_moments[i+j]
+
+                elif (i == degree) and (j == degree):
+                    Mm[i, j] = 1
+
+            # Numerical Optimization for Matrix Solver
+            Mm[i] = Mm[i] / max(abs(Mm[i]))
+
+        # Defenition of Right Hand side ortogonality conditions: Vc
+        for i in range(degree+1):
+            Vc[i] = 1 if i == degree else 0
+
+        # Solution: Coefficients of Non-Normal Orthogonal Polynomial: Vp Eq.(4)
+        try:
+            Vp = np.linalg.solve(Mm, Vc)
+        except:
+            inv_Mm = np.linalg.pinv(Mm)
+            Vp = np.dot(inv_Mm, Vc.T)
+
+        if degree == 0:
+            PolyCoeff_NonNorm = np.append(PolyCoeff_NonNorm, Vp)
+
+        if degree != 0:
+            if degree == 1:
+                zero = [0]
+            else:
+                zero = np.zeros((degree, 1))
+            PolyCoeff_NonNorm = np.hstack((PolyCoeff_NonNorm, zero))
+
+            PolyCoeff_NonNorm = np.vstack((PolyCoeff_NonNorm, Vp))
+
+        if 100*abs(sum(abs(np.dot(Mm, Vp)) - abs(Vc))) > 0.5:
+            print('\n---> Attention: Computational Error too high !')
+            print('\n---> Problem: Convergence of Linear Solver')
+
+        # Original Numerical Normalization of Coefficients with Norm and
+        # orthonormal Basis computation Matrix Storrage
+        # Note: Polynomial(i,j) correspont to coefficient number "j-1"
+        # of polynomial degree "i-1"
+        P_norm = 0
+        for i in range(nsamples):
+            Poly = 0
+            for k in range(degree+1):
+                if degree == 0:
+                    Poly += PolyCoeff_NonNorm[k] * (Data[i]**k)
+                else:
+                    Poly += PolyCoeff_NonNorm[degree, k] * (Data[i]**k)
+
+            P_norm += Poly**2 / nsamples
+
+        P_norm = np.sqrt(P_norm)
+
+        for k in range(degree+1):
+            if degree == 0:
+                Polynomial[degree, k] = PolyCoeff_NonNorm[k]/P_norm
+            else:
+                Polynomial[degree, k] = PolyCoeff_NonNorm[degree, k]/P_norm
+
+    # Backward linear transformation to the real data space
+    Data *= MeanOfData
+    for k in range(len(Polynomial)):
+        Polynomial[:, k] = Polynomial[:, k] / (MeanOfData**(k))
+
+    return Polynomial
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/bayes_linear.py b/examples/analytical-function/bayesvalidrox/surrogate_models/bayes_linear.py
new file mode 100644
index 0000000000000000000000000000000000000000..3bd827ac0ecc5b3a38116b21767e8a8799593b24
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/bayes_linear.py
@@ -0,0 +1,523 @@
+import numpy as np
+from sklearn.base import RegressorMixin
+from sklearn.linear_model._base import LinearModel
+from sklearn.utils import check_X_y, check_array, as_float_array
+from sklearn.utils.validation import check_is_fitted
+from scipy.linalg import svd
+import warnings
+from sklearn.preprocessing import normalize as f_normalize
+
+
+
+class BayesianLinearRegression(RegressorMixin,LinearModel):
+    '''
+    Superclass for Empirical Bayes and Variational Bayes implementations of 
+    Bayesian Linear Regression Model
+    '''
+    def __init__(self, n_iter, tol, fit_intercept,copy_X, verbose):
+        self.n_iter        = n_iter
+        self.fit_intercept = fit_intercept
+        self.copy_X        = copy_X
+        self.verbose       = verbose
+        self.tol           = tol
+        
+        
+    def _check_convergence(self, mu, mu_old):
+        '''
+        Checks convergence of algorithm using changes in mean of posterior
+        distribution of weights
+        '''
+        return np.sum(abs(mu-mu_old)>self.tol) == 0
+        
+        
+    def _center_data(self,X,y):
+        ''' Centers data'''
+        X     = as_float_array(X,copy = self.copy_X)
+        # normalisation should be done in preprocessing!
+        X_std = np.ones(X.shape[1], dtype = X.dtype)
+        if self.fit_intercept:
+            X_mean = np.average(X,axis = 0)
+            y_mean = np.average(y,axis = 0)
+            X     -= X_mean
+            y      = y - y_mean
+        else:
+            X_mean = np.zeros(X.shape[1],dtype = X.dtype)
+            y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype)
+        return X,y, X_mean, y_mean, X_std
+        
+        
+    def predict_dist(self,X):
+        '''
+        Calculates  mean and variance of predictive distribution for each data 
+        point of test set.(Note predictive distribution for each data point is 
+        Gaussian, therefore it is uniquely determined by mean and variance)                    
+                    
+        Parameters
+        ----------
+        x: array-like of size (n_test_samples, n_features)
+            Set of features for which corresponding responses should be predicted
+
+        Returns
+        -------
+        :list of two numpy arrays [mu_pred, var_pred]
+        
+            mu_pred : numpy array of size (n_test_samples,)
+                      Mean of predictive distribution
+                      
+            var_pred: numpy array of size (n_test_samples,)
+                      Variance of predictive distribution        
+        '''
+        # Note check_array and check_is_fitted are done within self._decision_function(X)
+        mu_pred     = self._decision_function(X)
+        data_noise  = 1./self.beta_
+        model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1)
+        var_pred    =  data_noise + model_noise
+        return [mu_pred,var_pred]
+    
+        
+        
+
+class EBLinearRegression(BayesianLinearRegression):
+    '''
+    Bayesian Regression with type II maximum likelihood (Empirical Bayes)
+    
+    Parameters:
+    -----------  
+    n_iter: int, optional (DEFAULT = 300)
+       Maximum number of iterations
+         
+    tol: float, optional (DEFAULT = 1e-3)
+       Threshold for convergence
+       
+    optimizer: str, optional (DEFAULT = 'fp')
+       Method for optimization , either Expectation Maximization or 
+       Fixed Point Gull-MacKay {'em','fp'}. Fixed point iterations are
+       faster, but can be numerically unstable (especially in case of near perfect fit).
+       
+    fit_intercept: bool, optional (DEFAULT = True)
+       If True includes bias term in model
+       
+    perfect_fit_tol: float (DEAFAULT = 1e-5)
+       Prevents overflow of precision parameters (this is smallest value RSS can have).
+       ( !!! Note if using EM instead of fixed-point, try smaller values
+       of perfect_fit_tol, for better estimates of variance of predictive distribution )
+
+    alpha: float (DEFAULT = 1)
+       Initial value of precision paramter for coefficients ( by default we define 
+       very broad distribution )
+       
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied, otherwise will be 
+        
+    verbose: bool, optional (Default = False)
+       If True at each iteration progress report is printed out
+    
+    Attributes
+    ----------
+    coef_  : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+        
+    intercept_: float
+        Value of bias term (if fit_intercept is False, then intercept_ = 0)
+        
+    alpha_ : float
+        Estimated precision of coefficients
+       
+    beta_  : float 
+        Estimated precision of noise
+        
+    eigvals_ : array, shape = (n_features, )
+        Eigenvalues of covariance matrix (from posterior distribution of weights)
+        
+    eigvecs_ : array, shape = (n_features, n_featues)
+        Eigenvectors of covariance matrix (from posterior distribution of weights)
+
+    '''
+    
+    def __init__(self,n_iter = 300, tol = 1e-3, optimizer = 'fp', fit_intercept = True,
+                 normalize=True, perfect_fit_tol = 1e-6, alpha = 1, copy_X = True, verbose = False):
+        super(EBLinearRegression,self).__init__(n_iter, tol, fit_intercept, copy_X, verbose)
+        if optimizer not in ['em','fp']:
+            raise ValueError('Optimizer can be either "em" or "fp" ')
+        self.optimizer     =  optimizer 
+        self.alpha         =  alpha 
+        self.perfect_fit   =  False
+        self.normalize     = True
+        self.scores_       =  [np.NINF]
+        self.perfect_fit_tol = perfect_fit_tol
+    
+    def _check_convergence(self, mu, mu_old):
+        '''
+        Checks convergence of algorithm using changes in mean of posterior
+        distribution of weights
+        '''
+        return np.sum(abs(mu-mu_old)>self.tol) == 0
+        
+        
+    def _center_data(self,X,y):
+        ''' Centers data'''
+        X     = as_float_array(X,copy = self.copy_X)
+        # normalisation should be done in preprocessing!
+        X_std = np.ones(X.shape[1], dtype = X.dtype)
+        if self.fit_intercept:
+            X_mean = np.average(X, axis=0)
+            X -= X_mean
+            if self.normalize:
+                X, X_std = f_normalize(X, axis=0, copy=False,
+                                         return_norm=True)
+            else:
+                X_std = np.ones(X.shape[1], dtype=X.dtype)
+            y_mean = np.average(y, axis=0)
+            y = y - y_mean
+        else:
+            X_mean = np.zeros(X.shape[1],dtype = X.dtype)
+            y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype)
+        return X,y, X_mean, y_mean, X_std
+            
+    def fit(self, X, y):
+        '''
+        Fits Bayesian Linear Regression using Empirical Bayes
+        
+        Parameters
+        ----------
+        X: array-like of size [n_samples,n_features]
+           Matrix of explanatory variables (should not include bias term)
+       
+        y: array-like of size [n_features]
+           Vector of dependent variables.
+           
+        Returns
+        -------
+        object: self
+          self
+    
+        '''
+        # preprocess data
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+        X, y, X_mean, y_mean, X_std = self._center_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean  = y_mean
+        self._x_std   = X_std
+        
+        #  precision of noise & and coefficients
+        alpha   =  self.alpha
+        var_y  = np.var(y)
+        # check that variance is non zero !!!
+        if var_y == 0 :
+            beta = 1e-2
+        else:
+            beta = 1. / np.var(y)
+
+        # to speed all further computations save svd decomposition and reuse it later
+        u,d,vt   = svd(X, full_matrices = False)
+        Uy      = np.dot(u.T,y)
+        dsq     = d**2
+        mu      = 0
+    
+        for i in range(self.n_iter):
+            
+            # find mean for posterior of w ( for EM this is E-step)
+            mu_old  =  mu
+            if n_samples > n_features:
+                 mu =  vt.T *  d/(dsq+alpha/beta) 
+            else:
+                 # clever use of SVD here , faster for large n_features
+                 mu =  u * 1./(dsq + alpha/beta)
+                 mu =  np.dot(X.T,mu)
+            mu =  np.dot(mu,Uy)
+
+            # precompute errors, since both methods use it in estimation
+            error   = y - np.dot(X,mu)
+            sqdErr  = np.sum(error**2)
+            
+            if sqdErr / n_samples < self.perfect_fit_tol:
+                self.perfect_fit = True
+                warnings.warn( ('Almost perfect fit!!! Estimated values of variance '
+                                'for predictive distribution are computed using only RSS'))
+                break
+            
+            if self.optimizer == "fp":           
+                gamma      =  np.sum(beta*dsq/(beta*dsq + alpha))
+                # use updated mu and gamma parameters to update alpha and beta
+                # !!! made computation numerically stable for perfect fit case
+                alpha      =   gamma  / (np.sum(mu**2) + np.finfo(np.float32).eps )
+                beta       =  ( n_samples - gamma ) / (sqdErr + np.finfo(np.float32).eps )
+            else:             
+                # M-step, update parameters alpha and beta to maximize ML TYPE II
+                eigvals    = 1. / (beta * dsq + alpha)
+                alpha      = n_features / ( np.sum(mu**2) + np.sum(1/eigvals) )
+                beta       = n_samples / ( sqdErr + np.sum(dsq/eigvals) )
+
+            # if converged or exceeded maximum number of iterations => terminate
+            converged = self._check_convergence(mu_old,mu)
+            if self.verbose:
+                print( "Iteration {0} completed".format(i) )
+                if converged is True:
+                    print("Algorithm converged after {0} iterations".format(i))
+            if converged or i==self.n_iter -1:
+                break
+        eigvals       = 1./(beta * dsq + alpha)
+        self.coef_    = beta*np.dot(vt.T*d*eigvals ,Uy)
+        self._set_intercept(X_mean,y_mean,X_std)
+        self.beta_    = beta
+        self.alpha_   = alpha
+        self.eigvals_ = eigvals
+        self.eigvecs_ = vt.T
+        
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+
+        return self
+    
+    def predict(self,X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance.
+        
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+           
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+        
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of predictive
+                    distribution)
+           
+             var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+        '''
+        y_hat     = np.dot(X,self.coef_) + self.intercept_
+        
+        if return_std:
+            if self.normalize:
+                X   = (X - self._x_mean_) / self._x_std
+            data_noise  = 1./self.beta_
+            model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1)
+            var_pred    =  data_noise + model_noise
+            std_hat = np.sqrt(var_pred)
+            return y_hat, std_hat
+        else:
+            return y_hat
+            
+            
+# ==============================  VBLR  =========================================
+
+def gamma_mean(a,b):
+    '''
+    Computes mean of gamma distribution
+    
+    Parameters
+    ----------
+    a: float
+      Shape parameter of Gamma distribution
+    
+    b: float
+      Rate parameter of Gamma distribution
+      
+    Returns
+    -------
+    : float
+      Mean of Gamma distribution
+    '''
+    return float(a) / b 
+    
+
+
+class VBLinearRegression(BayesianLinearRegression):
+    '''
+    Implements Bayesian Linear Regression using mean-field approximation.
+    Assumes gamma prior on precision parameters of coefficients and noise.
+
+    Parameters:
+    -----------
+    n_iter: int, optional (DEFAULT = 100)
+       Maximum number of iterations for KL minimization
+
+    tol: float, optional (DEFAULT = 1e-3)
+       Convergence threshold
+       
+    fit_intercept: bool, optional (DEFAULT = True)
+       If True will use bias term in model fitting
+
+    a: float, optional (Default = 1e-4)
+       Shape parameter of Gamma prior for precision of coefficients
+       
+    b: float, optional (Default = 1e-4)
+       Rate parameter of Gamma prior for precision coefficients
+       
+    c: float, optional (Default = 1e-4)
+       Shape parameter of  Gamma prior for precision of noise
+       
+    d: float, optional (Default = 1e-4)
+       Rate parameter of  Gamma prior for precision of noise
+       
+    verbose: bool, optional (Default = False)
+       If True at each iteration progress report is printed out
+       
+    Attributes
+    ----------
+    coef_  : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+        
+    intercept_: float
+        Value of bias term (if fit_intercept is False, then intercept_ = 0)
+        
+    alpha_ : float
+        Mean of precision of coefficients
+       
+    beta_  : float 
+        Mean of precision of noise
+
+    eigvals_ : array, shape = (n_features, )
+        Eigenvalues of covariance matrix (from posterior distribution of weights)
+        
+    eigvecs_ : array, shape = (n_features, n_featues)
+        Eigenvectors of covariance matrix (from posterior distribution of weights)
+
+    '''
+    
+    def __init__(self, n_iter = 100, tol =1e-4, fit_intercept = True, 
+                 a = 1e-4, b = 1e-4, c = 1e-4, d = 1e-4, copy_X = True,
+                 verbose = False):
+        super(VBLinearRegression,self).__init__(n_iter, tol, fit_intercept, copy_X,
+                                                verbose)
+        self.a,self.b   =  a, b
+        self.c,self.d   =  c, d
+
+        
+    def fit(self,X,y):
+        '''
+        Fits Variational Bayesian Linear Regression Model
+        
+        Parameters
+        ----------
+        X: array-like of size [n_samples,n_features]
+           Matrix of explanatory variables (should not include bias term)
+       
+        Y: array-like of size [n_features]
+           Vector of dependent variables.
+           
+        Returns
+        -------
+        object: self
+          self
+        '''
+        # preprocess data
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+        X, y, X_mean, y_mean, X_std = self._center_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean  = y_mean
+        self._x_std   = X_std
+        
+        # SVD decomposition, done once , reused at each iteration
+        u,D,vt = svd(X, full_matrices = False)
+        dsq    = D**2
+        UY     = np.dot(u.T,y)
+        
+        # some parameters of Gamma distribution have closed form solution
+        a      = self.a + 0.5 * n_features
+        c      = self.c + 0.5 * n_samples
+        b,d    = self.b,  self.d
+        
+        # initial mean of posterior for coefficients
+        mu     = 0
+                
+        for i in range(self.n_iter):
+            
+            # update parameters of distribution Q(weights)
+            e_beta       = gamma_mean(c,d)
+            e_alpha      = gamma_mean(a,b)
+            mu_old       = np.copy(mu)
+            mu,eigvals   = self._posterior_weights(e_beta,e_alpha,UY,dsq,u,vt,D,X)
+            
+            # update parameters of distribution Q(precision of weights) 
+            b            = self.b + 0.5*( np.sum(mu**2) + np.sum(eigvals))
+            
+            # update parameters of distribution Q(precision of likelihood)
+            sqderr       = np.sum((y - np.dot(X,mu))**2)
+            xsx          = np.sum(dsq*eigvals)
+            d            = self.d + 0.5*(sqderr + xsx)
+ 
+            # check convergence 
+            converged = self._check_convergence(mu,mu_old)
+            if self.verbose is True:
+                print("Iteration {0} is completed".format(i))
+                if converged is True:
+                    print("Algorithm converged after {0} iterations".format(i))
+               
+            # terminate if convergence or maximum number of iterations are achieved
+            if converged or i==(self.n_iter-1):
+                break
+            
+        # save necessary parameters    
+        self.beta_   = gamma_mean(c,d)
+        self.alpha_  = gamma_mean(a,b)
+        self.coef_, self.eigvals_ = self._posterior_weights(self.beta_, self.alpha_, UY,
+                                                            dsq, u, vt, D, X)
+        self._set_intercept(X_mean,y_mean,X_std)
+        self.eigvecs_ = vt.T
+        return self
+        
+
+    def _posterior_weights(self, e_beta, e_alpha, UY, dsq, u, vt, d, X):
+        '''
+        Calculates parameters of approximate posterior distribution 
+        of weights
+        '''
+        # eigenvalues of covariance matrix
+        sigma = 1./ (e_beta*dsq + e_alpha)
+        
+        # mean of approximate posterior distribution
+        n_samples, n_features = X.shape
+        if n_samples > n_features:
+             mu =  vt.T *  d/(dsq + e_alpha/e_beta)# + np.finfo(np.float64).eps) 
+        else:
+             mu =  u * 1./(dsq + e_alpha/e_beta)# + np.finfo(np.float64).eps)
+             mu =  np.dot(X.T,mu)
+        mu =  np.dot(mu,UY)
+        return mu,sigma
+        
+    def predict(self,X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance.
+        
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+           
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+        
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of predictive
+                    distribution)
+           
+             var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+        '''
+        x         = (X - self._x_mean_) / self._x_std
+        y_hat     = np.dot(x,self.coef_) + self._y_mean
+        
+        if return_std:
+            data_noise  = 1./self.beta_
+            model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1)
+            var_pred    =  data_noise + model_noise
+            std_hat = np.sqrt(var_pred)
+            return y_hat, std_hat
+        else:
+            return y_hat
\ No newline at end of file
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/desktop.ini b/examples/analytical-function/bayesvalidrox/surrogate_models/desktop.ini
new file mode 100644
index 0000000000000000000000000000000000000000..632de13ae6b61cecf0d9fdbf9c97cfb16bfb51a4
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/desktop.ini
@@ -0,0 +1,2 @@
+[LocalizedFileNames]
+exploration.py=@exploration.py,0
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/engine.py b/examples/analytical-function/bayesvalidrox/surrogate_models/engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..387cec5010373a087b01e838aba89404f2069c51
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/engine.py
@@ -0,0 +1,2233 @@
+# -*- coding: utf-8 -*-
+"""
+Engine to train the surrogate
+
+"""
+import copy
+from copy import deepcopy, copy
+import h5py
+import joblib
+import numpy as np
+import os
+
+from scipy import stats, signal, linalg, sparse
+from scipy.spatial import distance
+from tqdm import tqdm
+import scipy.optimize as opt
+from sklearn.metrics import mean_squared_error
+import multiprocessing
+import matplotlib.pyplot as plt
+import pandas as pd
+import sys
+import seaborn as sns
+from joblib import Parallel, delayed
+
+
+from bayesvalidrox.bayes_inference.bayes_inference import BayesInference
+from bayesvalidrox.bayes_inference.discrepancy import Discrepancy
+from .exploration import Exploration
+import pathlib
+
+#from .inputs import Input
+#from .exp_designs import ExpDesigns
+#from .surrogate_models import MetaModel
+#from bayesvalidrox.post_processing.post_processing import PostProcessing
+
+def hellinger_distance(P, Q):
+    """
+    Hellinger distance between two continuous distributions.
+
+    The maximum distance 1 is achieved when P assigns probability zero to
+    every set to which Q assigns a positive probability, and vice versa.
+    0 (identical) and 1 (maximally different)
+
+    Parameters
+    ----------
+    P : array
+        Reference likelihood.
+    Q : array
+        Estimated likelihood.
+
+    Returns
+    -------
+    float
+        Hellinger distance of two distributions.
+
+    """
+    P = np.array(P)
+    Q= np.array(Q)
+    
+    mu1 = P.mean()
+    Sigma1 = np.std(P)
+
+    mu2 = Q.mean()
+    Sigma2 = np.std(Q)
+
+    term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2))
+
+    term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2))
+
+    H_squared = 1 - term1 * term2
+
+    return np.sqrt(H_squared)
+
+
+def logpdf(x, mean, cov):
+    """
+    Computes the likelihood based on a multivariate normal distribution.
+
+    Parameters
+    ----------
+    x : TYPE
+        DESCRIPTION.
+    mean : array_like
+        Observation data.
+    cov : 2d array
+        Covariance matrix of the distribution.
+
+    Returns
+    -------
+    log_lik : float
+        Log likelihood.
+
+    """
+    n = len(mean)
+    L = linalg.cholesky(cov, lower=True)
+    beta = np.sum(np.log(np.diag(L)))
+    dev = x - mean
+    alpha = dev.dot(linalg.cho_solve((L, True), dev))
+    log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+
+    return log_lik
+
+def subdomain(Bounds, n_new_samples):
+    """
+    Divides a domain defined by Bounds into sub domains.
+
+    Parameters
+    ----------
+    Bounds : list of tuples
+        List of lower and upper bounds.
+    n_new_samples : int
+        Number of samples to divide the domain for.
+    n_params : int
+        The number of params to build the subdomains for
+
+    Returns
+    -------
+    Subdomains : List of tuples of tuples
+        Each tuple of tuples divides one set of bounds into n_new_samples parts.
+
+    """
+    n_params = len(Bounds)
+    n_subdomains = n_new_samples + 1
+    LinSpace = np.zeros((n_params, n_subdomains))
+
+    for i in range(n_params):
+        LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1],
+                                  num=n_subdomains)
+    Subdomains = []
+    for k in range(n_subdomains-1):
+        mylist = []
+        for i in range(n_params):
+            mylist.append((LinSpace[i, k+0], LinSpace[i, k+1]))
+        Subdomains.append(tuple(mylist))
+
+    return Subdomains
+
+class Engine():
+    
+    
+    def __init__(self, MetaMod, Model, ExpDes):
+        self.MetaModel = MetaMod
+        self.Model = Model
+        self.ExpDesign = ExpDes
+        self.parallel = False
+        self.trained = False
+        
+    def start_engine(self) -> None:
+        """
+        Do all the preparations that need to be run before the actual training
+
+        Returns
+        -------
+        None
+
+        """
+        self.out_names = self.Model.Output.names
+        self.MetaModel.out_names = self.out_names
+        
+        
+    def train_normal(self, parallel = False, verbose = False, save = False) -> None:
+        """
+        Trains surrogate on static samples only.
+        Samples are taken from the experimental design and the specified 
+        model is run on them.
+        Alternatively the samples can be read in from a provided hdf5 file.
+        
+
+        Returns
+        -------
+        None
+
+        """
+            
+        ExpDesign = self.ExpDesign
+        MetaModel = self.MetaModel
+        
+        # Read ExpDesign (training and targets) from the provided hdf5
+        if ExpDesign.hdf5_file is not None:
+            # TODO: need to run 'generate_ED' as well after this or not?
+            ExpDesign.read_from_file(self.out_names)
+        else:
+            # Check if an old hdf5 file exists: if yes, rename it
+            hdf5file = f'ExpDesign_{self.Model.name}.hdf5'
+            if os.path.exists(hdf5file):
+           #     os.rename(hdf5file, 'old_'+hdf5file)
+                file = pathlib.Path(hdf5file)
+                file.unlink()
+
+        # Prepare X samples 
+        # For training the surrogate use ExpDesign.X_tr, ExpDesign.X is for the model to run on 
+        ExpDesign.generate_ED(ExpDesign.n_init_samples,
+                                              transform=True,
+                                              max_pce_deg=np.max(MetaModel.pce_deg))
+        
+        # Run simulations at X 
+        if not hasattr(ExpDesign, 'Y') or ExpDesign.Y is None:
+            print('\n Now the forward model needs to be run!\n')
+            ED_Y, up_ED_X = self.Model.run_model_parallel(ExpDesign.X, mp = parallel)
+            ExpDesign.Y = ED_Y
+        else:
+            # Check if a dict has been passed.
+            if not type(ExpDesign.Y) is dict:
+                raise Exception('Please provide either a dictionary or a hdf5'
+                                'file to ExpDesign.hdf5_file argument.')
+                
+        # Separate output dict and x-values
+        if 'x_values' in ExpDesign.Y:
+            ExpDesign.x_values = ExpDesign.Y['x_values']
+            del ExpDesign.Y['x_values']
+        else:
+            print('No x_values are given, this might lead to issues during PostProcessing')
+        
+        
+        # Fit the surrogate
+        MetaModel.fit(ExpDesign.X, ExpDesign.Y, parallel, verbose)
+        
+        # Save what there is to save
+        if save:
+            # Save surrogate
+            with open(f'surrogates/surrogate_{self.Model.name}.pk1', 'wb') as output:
+                joblib.dump(MetaModel, output, 2)
+                    
+            # Zip the model run directories
+            if self.Model.link_type.lower() == 'pylink' and\
+               self.ExpDesign.sampling_method.lower() != 'user':
+                self.Model.zip_subdirs(self.Model.name, f'{self.Model.name}_')
+                
+        # Set that training was done
+        self.trained = True
+                
+            
+    def train_sequential(self, parallel = False, verbose = False) -> None:
+        """
+        Train the surrogate in a sequential manner.
+        First build and train evereything on the static samples, then iterate
+        choosing more samples and refitting the surrogate on them.
+
+        Returns
+        -------
+        None
+
+        """
+        #self.train_normal(parallel, verbose)
+        self.parallel = parallel
+        self.train_seq_design(parallel, verbose)
+        
+        
+    # -------------------------------------------------------------------------
+    def eval_metamodel(self, samples=None, nsamples=None,
+                       sampling_method='random', return_samples=False):
+        """
+        Evaluates meta-model at the requested samples. One can also generate
+        nsamples.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Samples to evaluate meta-model at. The default is None.
+        nsamples : int, optional
+            Number of samples to generate, if no `samples` is provided. The
+            default is None.
+        sampling_method : str, optional
+            Type of sampling, if no `samples` is provided. The default is
+            'random'.
+        return_samples : bool, optional
+            Retun samples, if no `samples` is provided. The default is False.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean of the predictions.
+        std_pred : dict
+            Standard deviatioon of the predictions.
+        """
+        # Generate or transform (if need be) samples
+        if samples is None:
+            # Generate
+            samples = self.ExpDesign.generate_samples(
+                nsamples,
+                sampling_method
+                )
+
+        # Transformation to other space is to be done in the MetaModel
+        # TODO: sort the transformations better
+        mean_pred, std_pred = self.MetaModel.eval_metamodel(samples)
+
+        if return_samples:
+            return mean_pred, std_pred, samples
+        else:
+            return mean_pred, std_pred
+        
+        
+    # -------------------------------------------------------------------------
+    def train_seq_design(self, parallel = False, verbose = False):
+        """
+        Starts the adaptive sequential design for refining the surrogate model
+        by selecting training points in a sequential manner.
+
+        Returns
+        -------
+        MetaModel : object
+            Meta model object.
+
+        """
+        self.parallel = parallel
+        
+        # Initialization
+        self.SeqModifiedLOO = {}
+        self.seqValidError = {}
+        self.SeqBME = {}
+        self.SeqKLD = {}
+        self.SeqDistHellinger = {}
+        self.seqRMSEMean = {}
+        self.seqRMSEStd = {}
+        self.seqMinDist = []
+        
+        if not hasattr(self.MetaModel, 'valid_samples'):
+            self.ExpDesign.valid_samples = []
+            self.ExpDesign.valid_model_runs = []
+            self.valid_likelihoods = []
+        
+        validError = None
+
+
+        # Determine the metamodel type
+        if self.MetaModel.meta_model_type.lower() != 'gpe':
+            pce = True
+        else:
+            pce = False
+        mc_ref = True if bool(self.Model.mc_reference) else False
+        if mc_ref:
+            self.Model.read_observation('mc_ref')
+
+        # Get the parameters
+        max_n_samples = self.ExpDesign.n_max_samples
+        mod_LOO_threshold = self.ExpDesign.mod_LOO_threshold
+        n_canddidate = self.ExpDesign.n_canddidate
+        post_snapshot = self.ExpDesign.post_snapshot
+        n_replication = self.ExpDesign.n_replication
+        util_func = self.ExpDesign.util_func
+        output_name = self.out_names
+        
+        # Handle if only one UtilityFunctions is provided
+        if not isinstance(util_func, list):
+            util_func = [self.ExpDesign.util_func]
+
+        # Read observations or MCReference
+        # TODO: recheck the logic in this if statement
+        if (len(self.Model.observations) != 0 or self.Model.meas_file is not None) and hasattr(self.MetaModel, 'Discrepancy'):
+            self.observations = self.Model.read_observation()
+            obs_data = self.observations
+        else:
+            obs_data = []
+            # TODO: TotalSigma2 not defined if not in this else???
+            # TODO: no self.observations if in here
+            TotalSigma2 = {}
+            
+        # ---------- Initial self.MetaModel ----------
+        if not self.trained:
+            self.train_normal(parallel = parallel, verbose=verbose)
+        
+        initMetaModel = deepcopy(self.MetaModel)
+
+        # Validation error if validation set is provided.
+        if self.ExpDesign.valid_model_runs:
+            init_rmse, init_valid_error = self._validError(initMetaModel)
+            init_valid_error = list(init_valid_error.values())
+        else:
+            init_rmse = None
+
+        # Check if discrepancy is provided
+        if len(obs_data) != 0 and hasattr(self.MetaModel, 'Discrepancy'):
+            TotalSigma2 = self.MetaModel.Discrepancy.parameters
+
+            # Calculate the initial BME
+            out = self._BME_Calculator(
+                obs_data, TotalSigma2, init_rmse)
+            init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out
+            print(f"\nInitial BME: {init_BME:.2f}")
+            print(f"Initial KLD: {init_KLD:.2f}")
+
+            # Posterior snapshot (initial)
+            if post_snapshot:
+                parNames = self.ExpDesign.par_names
+                print('Posterior snapshot (initial) is being plotted...')
+                self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init')
+
+        # Check the convergence of the Mean & Std
+        if mc_ref and pce:
+            init_rmse_mean, init_rmse_std = self._error_Mean_Std()
+            print(f"Initial Mean and Std error: {init_rmse_mean:.2f},"
+                  f" {init_rmse_std:.2f}")
+
+        # Read the initial experimental design
+        Xinit = self.ExpDesign.X
+        init_n_samples = len(self.ExpDesign.X)
+        initYprev = self.ExpDesign.Y#initMetaModel.ModelOutputDict
+        #self.MetaModel.ModelOutputDict = self.ExpDesign.Y
+        initLCerror = initMetaModel.LCerror
+        n_itrs = max_n_samples - init_n_samples
+
+        ## Get some initial statistics
+        # Read the initial ModifiedLOO
+        if pce:
+            Scores_all, varExpDesignY = [], []
+            for out_name in output_name:
+                y = self.ExpDesign.Y[out_name]
+                Scores_all.append(list(
+                    self.MetaModel.score_dict['b_1'][out_name].values()))
+                if self.MetaModel.dim_red_method.lower() == 'pca':
+                    pca = self.MetaModel.pca['b_1'][out_name]
+                    components = pca.transform(y)
+                    varExpDesignY.append(np.var(components, axis=0))
+                else:
+                    varExpDesignY.append(np.var(y, axis=0))
+
+            Scores = [item for sublist in Scores_all for item in sublist]
+            weights = [item for sublist in varExpDesignY for item in sublist]
+            init_mod_LOO = [np.average([1-score for score in Scores],
+                                       weights=weights)]
+
+        prevMetaModel_dict = {}
+        #prevExpDesign_dict = {}
+        # Can run sequential design multiple times for comparison
+        for repIdx in range(n_replication):
+            print(f'\n>>>> Replication: {repIdx+1}<<<<')
+
+            # util_func: the function to use inside the type of exploitation
+            for util_f in util_func:
+                print(f'\n>>>> Utility Function: {util_f} <<<<')
+                # To avoid changes ub original aPCE object
+                self.ExpDesign.X = Xinit
+                self.ExpDesign.Y = initYprev
+                self.ExpDesign.LCerror = initLCerror
+
+                # Set the experimental design
+                Xprev = Xinit
+                total_n_samples = init_n_samples
+                Yprev = initYprev
+
+                Xfull = []
+                Yfull = []
+
+                # Store the initial ModifiedLOO
+                if pce:
+                    print("\nInitial ModifiedLOO:", init_mod_LOO)
+                    SeqModifiedLOO = np.array(init_mod_LOO)
+
+                if len(self.ExpDesign.valid_model_runs) != 0:
+                    SeqValidError = np.array(init_valid_error)
+
+                # Check if data is provided
+                if len(obs_data) != 0 and hasattr(self.MetaModel, 'Discrepancy'):
+                    SeqBME = np.array([init_BME])
+                    SeqKLD = np.array([init_KLD])
+                    SeqDistHellinger = np.array([init_dist_hellinger])
+
+                if mc_ref and pce:
+                    seqRMSEMean = np.array([init_rmse_mean])
+                    seqRMSEStd = np.array([init_rmse_std])
+
+                # ------- Start Sequential Experimental Design -------
+                postcnt = 1
+                for itr_no in range(1, n_itrs+1):
+                    print(f'\n>>>> Iteration number {itr_no} <<<<')
+
+                    # Save the metamodel prediction before updating
+                    prevMetaModel_dict[itr_no] = deepcopy(self.MetaModel)
+                    #prevExpDesign_dict[itr_no] = deepcopy(self.ExpDesign)
+                    if itr_no > 1:
+                        pc_model = prevMetaModel_dict[itr_no-1]
+                        self._y_hat_prev, _ = pc_model.eval_metamodel(
+                            samples=Xfull[-1].reshape(1, -1))
+                        del prevMetaModel_dict[itr_no-1]
+
+                    # Optimal Bayesian Design
+                    #self.MetaModel.ExpDesignFlag = 'sequential'
+                    Xnew, updatedPrior = self.choose_next_sample(TotalSigma2,
+                                                            n_canddidate,
+                                                            util_f)
+                    S = np.min(distance.cdist(Xinit, Xnew, 'euclidean'))
+                    self.seqMinDist.append(S)
+                    print(f"\nmin Dist from OldExpDesign: {S:2f}")
+                    print("\n")
+
+                    # Evaluate the full model response at the new sample
+                    Ynew, _ = self.Model.run_model_parallel(
+                        Xnew, prevRun_No=total_n_samples
+                        )
+                    total_n_samples += Xnew.shape[0]
+
+                    # ------ Plot the surrogate model vs Origninal Model ------
+                    if hasattr(self.ExpDesign, 'adapt_verbose') and \
+                       self.ExpDesign.adapt_verbose:
+                        from .adaptPlot import adaptPlot
+                        y_hat, std_hat = self.MetaModel.eval_metamodel(
+                            samples=Xnew
+                            )
+                        adaptPlot(
+                            self.MetaModel, Ynew, y_hat, std_hat,
+                            plotED=False
+                            )
+
+                    # -------- Retrain the surrogate model -------
+                    # Extend new experimental design
+                    Xfull = np.vstack((Xprev, Xnew))
+
+                    # Updating experimental design Y
+                    for out_name in output_name:
+                        Yfull = np.vstack((Yprev[out_name], Ynew[out_name]))
+                        self.ExpDesign.Y[out_name] = Yfull
+
+                    # Pass new design to the metamodel object
+                    self.ExpDesign.sampling_method = 'user'
+                    self.ExpDesign.X = Xfull
+                    #self.ExpDesign.Y = self.MetaModel.ModelOutputDict
+
+                    # Save the Experimental Design for next iteration
+                    Xprev = Xfull
+                    Yprev = self.ExpDesign.Y 
+
+                    # Pass the new prior as the input
+                    # TODO: another look at this - no difference apc to pce to gpe?
+                    self.MetaModel.input_obj.poly_coeffs_flag = False
+                    if updatedPrior is not None:
+                        self.MetaModel.input_obj.poly_coeffs_flag = True
+                        print("updatedPrior:", updatedPrior.shape)
+                        # Arbitrary polynomial chaos
+                        for i in range(updatedPrior.shape[1]):
+                            self.MetaModel.input_obj.Marginals[i].dist_type = None
+                            x = updatedPrior[:, i]
+                            self.MetaModel.input_obj.Marginals[i].raw_data = x
+
+                    # Train the surrogate model for new ExpDesign
+                    self.train_normal(parallel=False)
+
+                    # -------- Evaluate the retrained surrogate model -------
+                    # Extract Modified LOO from Output
+                    if pce:
+                        Scores_all, varExpDesignY = [], []
+                        for out_name in output_name:
+                            y = self.ExpDesign.Y[out_name]
+                            Scores_all.append(list(
+                                self.MetaModel.score_dict['b_1'][out_name].values()))
+                            if self.MetaModel.dim_red_method.lower() == 'pca':
+                                pca = self.MetaModel.pca['b_1'][out_name]
+                                components = pca.transform(y)
+                                varExpDesignY.append(np.var(components,
+                                                            axis=0))
+                            else:
+                                varExpDesignY.append(np.var(y, axis=0))
+                        Scores = [item for sublist in Scores_all for item
+                                  in sublist]
+                        weights = [item for sublist in varExpDesignY for item
+                                   in sublist]
+                        ModifiedLOO = [np.average(
+                            [1-score for score in Scores], weights=weights)]
+
+                        print('\n')
+                        print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO)
+                        print('\n')
+
+                    # Compute the validation error
+                    if self.ExpDesign.valid_model_runs:
+                        rmse, validError = self._validError(self.MetaModel)
+                        ValidError = list(validError.values())
+                    else:
+                        rmse = None
+
+                    # Store updated ModifiedLOO
+                    if pce:
+                        SeqModifiedLOO = np.vstack(
+                            (SeqModifiedLOO, ModifiedLOO))
+                        if len(self.ExpDesign.valid_model_runs) != 0:
+                            SeqValidError = np.vstack(
+                                (SeqValidError, ValidError))
+                    # -------- Caclulation of BME as accuracy metric -------
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        # Calculate the initial BME
+                        out = self._BME_Calculator(obs_data, TotalSigma2, rmse)
+                        BME, KLD, Posterior, likes, DistHellinger = out
+                        print('\n')
+                        print(f"Updated BME: {BME:.2f}")
+                        print(f"Updated KLD: {KLD:.2f}")
+                        print('\n')
+
+                        # Plot some snapshots of the posterior
+                        step_snapshot = self.ExpDesign.step_snapshot
+                        if post_snapshot and postcnt % step_snapshot == 0:
+                            parNames = self.ExpDesign.par_names
+                            print('Posterior snapshot is being plotted...')
+                            self.__posteriorPlot(Posterior, parNames,
+                                                 f'SeqPosterior_{postcnt}')
+                        postcnt += 1
+
+                    # Check the convergence of the Mean&Std
+                    if mc_ref and pce:
+                        print('\n')
+                        RMSE_Mean, RMSE_std = self._error_Mean_Std()
+                        print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, "
+                              f"{RMSE_std:.2f}")
+                        print('\n')
+
+                    # Store the updated BME & KLD
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        SeqBME = np.vstack((SeqBME, BME))
+                        SeqKLD = np.vstack((SeqKLD, KLD))
+                        SeqDistHellinger = np.vstack((SeqDistHellinger,
+                                                      DistHellinger))
+                    if mc_ref and pce:
+                        seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean))
+                        seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std))
+
+                    if pce and any(LOO < mod_LOO_threshold
+                                   for LOO in ModifiedLOO):
+                        break
+
+                    # Clean up
+                    if len(obs_data) != 0:
+                        del out
+                    print()
+                    print('-'*50)
+                    print()
+
+                # Store updated ModifiedLOO and BME in dictonary
+                strKey = f'{util_f}_rep_{repIdx+1}'
+                if pce:
+                    self.SeqModifiedLOO[strKey] = SeqModifiedLOO
+                if len(self.ExpDesign.valid_model_runs) != 0:
+                    self.seqValidError[strKey] = SeqValidError
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    self.SeqBME[strKey] = SeqBME
+                    self.SeqKLD[strKey] = SeqKLD
+                if hasattr(self.MetaModel, 'valid_likelihoods') and \
+                   self.valid_likelihoods:
+                    self.SeqDistHellinger[strKey] = SeqDistHellinger
+                if mc_ref and pce:
+                    self.seqRMSEMean[strKey] = seqRMSEMean
+                    self.seqRMSEStd[strKey] = seqRMSEStd
+
+        # return self.MetaModel
+
+    # -------------------------------------------------------------------------
+    def util_VarBasedDesign(self, X_can, index, util_func='Entropy'):
+        """
+        Computes the exploitation scores based on:
+        active learning MacKay(ALM) and active learning Cohn (ALC)
+        Paper: Sequential Design with Mutual Information for Computer
+        Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas
+        (2016)
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        index : int
+            Model output index.
+        UtilMethod : string, optional
+            Exploitation utility function. The default is 'Entropy'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+        MetaModel = self.MetaModel
+        ED_X = self.ExpDesign.X
+        out_dict_y = self.ExpDesign.Y
+        out_names = self.out_names
+
+        # Run the Metamodel for the candidate
+        X_can = X_can.reshape(1, -1)
+        Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can)
+
+        if util_func.lower() == 'alm':
+            # ----- Entropy/MMSE/active learning MacKay(ALM)  -----
+            # Compute perdiction variance of the old model
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            varPCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                varPCE[KeyIdx] = np.max(canPredVar[key], axis=1)
+            score = np.max(varPCE, axis=0)
+
+        elif util_func.lower() == 'eigf':
+            # ----- Expected Improvement for Global fit -----
+            # Find closest EDX to the candidate
+            distances = distance.cdist(ED_X, X_can, 'euclidean')
+            index = np.argmin(distances)
+
+            # Compute perdiction error and variance of the old model
+            predError = {key: Y_PC_can[key] for key in out_names}
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            # Compute perdiction error and variance of the old model
+            # Eq (5) from Liu et al.(2018)
+            EIGF_PCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                residual = predError[key] - out_dict_y[key][int(index)]
+                var = canPredVar[key]
+                EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1)
+            score = np.max(EIGF_PCE, axis=0)
+
+        return -1 * score   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianActiveDesign(self, y_hat, std, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian active design criterion (var).
+
+        It is based on the following paper:
+        Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak.
+        "Bayesian3 active learning for the gaussian process emulator using
+        information theory." Entropy 22, no. 8 (2020): 890.
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            BAL design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # Get the data
+        obs_data = self.observations
+        # TODO: this should be optimizable to be calculated explicitly
+        if hasattr(self.Model, 'n_obs'):
+            n_obs = self.Model.n_obs
+        else:
+            n_obs = self.n_obs
+        mc_size = 10000
+
+        # Sample a distribution for a normal dist
+        # with Y_mean_can as the mean and Y_std_can as std.
+        Y_MC, std_MC = {}, {}
+        logPriorLikelihoods = np.zeros((mc_size))
+       # print(y_hat)
+       # print(list[y_hat])
+        #print(std)
+        for key in list(y_hat):
+            #print(std[key])
+            cov = np.diag(std[key]**2)
+            #print(y_hat[key], cov)
+            print(key, y_hat[key], std[key])
+            # TODO: added the allow_singular = True here
+            rv = stats.multivariate_normal(mean=y_hat[key], cov=cov,allow_singular = True)
+            Y_MC[key] = rv.rvs(size=mc_size)
+            logPriorLikelihoods += rv.logpdf(Y_MC[key])
+            std_MC[key] = np.zeros((mc_size, y_hat[key].shape[0]))
+
+        #  Likelihood computation (Comparison of data and simulation
+        #  results via PCE with candidate design)
+        likelihoods = self._normpdf(Y_MC, std_MC, obs_data, sigma2Dict)
+        
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, mc_size)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(likelihoods), dtype=np.longdouble)#float128)
+
+        # Posterior-based expectation of likelihoods
+        postLikelihoods = likelihoods[accepted]
+        postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+        # Utility function Eq.2 in Ref. (2)
+        # Posterior covariance matrix after observing data y
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # TODO: Calculate the correction factor for BME
+            # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can,
+            #                                      ObservationData, sigma2Dict)
+            # BME += BMECorrFactor
+            # Haun et al implementation
+            # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+            U_J_d = postExpLikelihoods - logBME
+
+        # Marginal log likelihood
+        elif var == 'BME':
+            U_J_d = np.nanmean(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            logBME = np.log(np.nanmean(likelihoods))
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # Bayesian information criterion
+        elif var == 'BIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxL = np.nanmax(likelihoods)
+            U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams
+
+        # Akaike information criterion
+        elif var == 'AIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxlogL = np.log(np.nanmax(likelihoods))
+            AIC = -2 * maxlogL + 2 * nModelParams
+            # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1)
+            penTerm = 0
+            U_J_d = 1*(AIC + penTerm)
+
+        # Deviance information criterion
+        elif var == 'DIC':
+            # D_theta_bar = np.mean(-2 * Likelihoods)
+            N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0]))
+            Likelihoods_theta_mean = self._normpdf(
+                y_hat, std, obs_data, sigma2Dict
+                )
+            DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p
+
+            U_J_d = DIC
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Handle inf and NaN (replace by zero)
+        if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf:
+            U_J_d = 0.0
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        MetaModel = self.MetaModel
+        out_names = self.out_names
+        if X_can.ndim == 1:
+            X_can = X_can.reshape(1, -1)
+
+        # Compute the mean and std based on the MetaModel
+        # pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+        if var == 'ALC':
+            Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC)
+
+        # Old Experimental design
+        oldExpDesignX = self.ExpDesign.X
+        oldExpDesignY = self.ExpDesign.Y
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can)
+        PCE_Model_can = deepcopy(MetaModel)
+        engine_can = deepcopy(self)
+        # Add the candidate to the ExpDesign
+        NewExpDesignX = np.vstack((oldExpDesignX, X_can))
+
+        NewExpDesignY = {}
+        for key in oldExpDesignY.keys():
+            NewExpDesignY[key] = np.vstack(
+                (oldExpDesignY[key], Y_PC_can[key])
+                )
+
+        engine_can.ExpDesign.sampling_method = 'user'
+        engine_can.ExpDesign.X = NewExpDesignX
+        #engine_can.ModelOutputDict = NewExpDesignY
+        engine_can.ExpDesign.Y = NewExpDesignY
+
+        # Train the model for the observed data using x_can
+        engine_can.MetaModel.input_obj.poly_coeffs_flag = False
+        engine_can.start_engine()
+        engine_can.train_normal(parallel=False)
+        engine_can.MetaModel.fit(NewExpDesignX, NewExpDesignY)
+#        engine_can.train_norm_design(parallel=False)
+
+        # Set the ExpDesign to its original values
+        engine_can.ExpDesign.X = oldExpDesignX
+        engine_can.ModelOutputDict = oldExpDesignY
+        engine_can.ExpDesign.Y = oldExpDesignY
+
+        if var.lower() == 'mi':
+            # Mutual information based on Krause et al
+            # Adapted from Beck & Guillas (MICE) paper
+            _, std_PC_can = engine_can.MetaModel.eval_metamodel(samples=X_can)
+            std_can = {key: std_PC_can[key] for key in out_names}
+
+            std_old = {key: Y_std_can[key] for key in out_names}
+
+            varPCE = np.zeros((len(out_names)))
+            for i, key in enumerate(out_names):
+                varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2)
+            score = np.mean(varPCE)
+
+            return -1 * score
+
+        elif var.lower() == 'alc':
+            # Active learning based on Gramyc and Lee
+            # Adaptive design and analysis of supercomputer experiments Techno-
+            # metrics, 51 (2009), pp. 130–145.
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC_can, Y_MC_std_can = engine_can.MetaModel.eval_metamodel(samples=X_MC)
+
+            # Compute the score
+            score = []
+            for i, key in enumerate(out_names):
+                pce_var = Y_MC_std_can[key]**2
+                pce_var_can = Y_MC_std[key]**2
+                score.append(np.mean(pce_var-pce_var_can, axis=0))
+            score = np.mean(score)
+
+            return -1 * score
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = X_MC.shape[0]
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Enriching Monte Carlo samples if need be
+            if ESS != 0:
+                X_MC = self.ExpDesign.generate_samples(
+                    MCsize, 'random'
+                    )
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and simulation
+            # results via PCE with candidate design)
+            likelihoods = self._normpdf(
+                Y_MC, std_MC, self.observations, sigma2Dict
+                )
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                print("--- increasing MC size---")
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # -------------------- Utility functions --------------------
+        # Utility function Eq.2 in Ref. (2)
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods, dtype=np.longdouble))#float128))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Haun et al implementation
+            U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME)
+
+            # U_J_d = np.sum(G_n_m_all)
+            # Ryan et al (2014) implementation
+            # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0])
+            # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME
+
+            # U_J_d = postExpLikelihoods - logBME
+
+        # Marginal likelihood
+        elif var == 'BME':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+            U_J_d = logBME
+
+        # Bayes risk likelihood
+        elif var == 'BayesRisk':
+
+            U_J_d = -1 * np.var(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postLikelihoods /= np.nansum(likelihoods[accepted])
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Posterior-based expectation of prior densities
+            postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # D-Posterior-precision
+        elif var == 'DPP':
+            X_Posterior = X_MC[accepted]
+            # covariance of the posterior parameters
+            U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior)))
+
+        # A-Posterior-precision
+        elif var == 'APP':
+            X_Posterior = X_MC[accepted]
+            # trace of the posterior parameters
+            U_J_d = -np.log(np.trace(np.cov(X_Posterior)))
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+
+    # -------------------------------------------------------------------------
+    def run_util_func(self, method, candidates, index, sigma2Dict=None,
+                      var=None, X_MC=None):
+        """
+        Runs the utility function based on the given method.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        candidates : array of shape (n_samples, n_params)
+            All candidate parameter sets.
+        index : int
+            ExpDesign index.
+        sigma2Dict : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        var : string, optional
+            Utility function. The default is None.
+        X_MC : TYPE, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        index : TYPE
+            DESCRIPTION.
+        List
+            Scores.
+
+        """
+
+        if method.lower() == 'varoptdesign':
+            # U_J_d = self.util_VarBasedDesign(candidates, index, var)
+            U_J_d = np.zeros((candidates.shape[0]))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="varoptdesign"):
+                U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var)
+
+        elif method.lower() == 'bayesactdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            # Evaluate all candidates
+            y_can, std_can = self.MetaModel.eval_metamodel(samples=candidates)
+            # loop through candidates
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="BAL Design"):
+                y_hat = {key: items[idx] for key, items in y_can.items()}
+                std = {key: items[idx] for key, items in std_can.items()}
+                
+               # print(y_hat)
+               # print(std)
+                U_J_d[idx] = self.util_BayesianActiveDesign(
+                    y_hat, std, sigma2Dict, var)
+
+        elif method.lower() == 'bayesoptdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict,
+                                                      var)
+        return (index, -1 * U_J_d)
+
+    # -------------------------------------------------------------------------
+    def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No,
+                       verbose=False):
+        """
+        Exploration algorithm to find the optimum parameter space.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        Bounds : list of tuples
+            List of lower and upper boundaries of parameters.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        Run_No : int
+            Run number.
+        verbose : bool, optional
+            Print out a summary. The default is False.
+
+        Returns
+        -------
+        Run_No : int
+            Run number.
+        array
+            Optimial candidate.
+
+        """
+
+        Model = self.Model
+        max_func_itr = self.ExpDesign.max_func_itr
+
+        if method == 'VarOptDesign':
+            Res_Global = opt.dual_annealing(self.util_VarBasedDesign,
+                                            bounds=Bounds,
+                                            args=(Model, var),
+                                            maxfun=max_func_itr)
+
+        elif method == 'BayesOptDesign':
+            Res_Global = opt.dual_annealing(self.util_BayesianDesign,
+                                            bounds=Bounds,
+                                            args=(Model, sigma2Dict, var),
+                                            maxfun=max_func_itr)
+
+        if verbose:
+            print(f"Global minimum: xmin = {Res_Global.x}, "
+                  f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}")
+
+        return (Run_No, Res_Global.x)
+
+    # -------------------------------------------------------------------------
+    def tradeoff_weights(self, tradeoff_scheme, old_EDX, old_EDY):
+        """
+        Calculates weights for exploration scores based on the requested
+        scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`.
+
+        `None`: No exploration.
+        `equal`: Same weights for exploration and exploitation scores.
+        `epsilon-decreasing`: Start with more exploration and increase the
+            influence of exploitation along the way with a exponential decay
+            function
+        `adaptive`: An adaptive method based on:
+            Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling
+            approach for Kriging metamodeling by maximizing expected prediction
+            error." Computers & Chemical Engineering 106 (2017): 171-182.
+
+        Parameters
+        ----------
+        tradeoff_scheme : string
+            Trade-off scheme for exloration and exploitation scores.
+        old_EDX : array (n_samples, n_params)
+            Old experimental design (training points).
+        old_EDY : dict
+            Old model responses (targets).
+
+        Returns
+        -------
+        exploration_weight : float
+            Exploration weight.
+        exploitation_weight: float
+            Exploitation weight.
+
+        """
+        if tradeoff_scheme is None:
+            exploration_weight = 0
+
+        elif tradeoff_scheme == 'equal':
+            exploration_weight = 0.5
+
+        elif tradeoff_scheme == 'epsilon-decreasing':
+            # epsilon-decreasing scheme
+            # Start with more exploration and increase the influence of
+            # exploitation along the way with a exponential decay function
+            initNSamples = self.ExpDesign.n_init_samples
+            n_max_samples = self.ExpDesign.n_max_samples
+
+            itrNumber = (self.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.ExpDesign.n_new_samples
+
+            tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8)
+            exploration_weight = signal.exponential(n_max_samples-initNSamples,
+                                                    0, tau2, False)[itrNumber]
+
+        elif tradeoff_scheme == 'adaptive':
+
+            # Extract itrNumber
+            initNSamples = self.ExpDesign.n_init_samples
+            n_max_samples = self.ExpDesign.n_max_samples
+            itrNumber = (self.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.ExpDesign.n_new_samples
+
+            if itrNumber == 0:
+                exploration_weight = 0.5
+            else:
+                # New adaptive trade-off according to Liu et al. (2017)
+                # Mean squared error for last design point
+                last_EDX = old_EDX[-1].reshape(1, -1)
+                lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX)
+                pce_y = np.array(list(lastPCEY.values()))[:, 0]
+                y = np.array(list(old_EDY.values()))[:, -1, :]
+                mseError = mean_squared_error(pce_y, y)
+
+                # Mean squared CV - error for last design point
+                pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0]
+                mseCVError = mean_squared_error(pce_y_prev, y)
+
+                exploration_weight = min([0.5*mseError/mseCVError, 1])
+
+        # Exploitation weight
+        exploitation_weight = 1 - exploration_weight
+
+        return exploration_weight, exploitation_weight
+
+    # -------------------------------------------------------------------------
+    def choose_next_sample(self, sigma2=None, n_candidates=5, var='DKL'):
+        """
+        Runs optimal sequential design.
+
+        Parameters
+        ----------
+        sigma2 : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        n_candidates : int, optional
+            Number of candidate samples. The default is 5.
+        var : string, optional
+            Utility function. The default is None. # TODO: default is set to DKL, not none
+
+        Raises
+        ------
+        NameError
+            Wrong utility function.
+
+        Returns
+        -------
+        Xnew : array (n_samples, n_params)
+            Selected new training point(s).
+        """
+
+        # Initialization
+        Bounds = self.ExpDesign.bound_tuples
+        n_new_samples = self.ExpDesign.n_new_samples
+        explore_method = self.ExpDesign.explore_method
+        exploit_method = self.ExpDesign.exploit_method
+        n_cand_groups = self.ExpDesign.n_cand_groups
+        tradeoff_scheme = self.ExpDesign.tradeoff_scheme
+
+        old_EDX = self.ExpDesign.X
+        old_EDY = self.ExpDesign.Y.copy()
+        ndim = self.ExpDesign.X.shape[1]
+        OutputNames = self.out_names
+
+        # -----------------------------------------
+        # ----------- CUSTOMIZED METHODS ----------
+        # -----------------------------------------
+        # Utility function exploit_method provided by user
+        if exploit_method.lower() == 'user':
+            if not hasattr(self.ExpDesign, 'ExploitFunction'):
+                raise AttributeError('Function `ExploitFunction` not given to the ExpDesign, thus cannor run user-defined sequential scheme')
+            # TODO: syntax does not fully match the rest - can test this??
+            Xnew, filteredSamples = self.ExpDesign.ExploitFunction(self)
+
+            print("\n")
+            print("\nXnew:\n", Xnew)
+
+            return Xnew, filteredSamples
+
+
+        # Dual-Annealing works differently from the rest, so deal with this first
+        # Here exploration and exploitation are performed simulataneously
+        if explore_method == 'dual annealing':
+            # ------- EXPLORATION: OPTIMIZATION -------
+            import time
+            start_time = time.time()
+
+            # Divide the domain to subdomains
+            subdomains = subdomain(Bounds, n_new_samples)
+
+            # Multiprocessing
+            if self.parallel:
+                args = []
+                for i in range(n_new_samples):
+                    args.append((exploit_method, subdomains[i], sigma2, var, i))
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.dual_annealing, args).get()
+
+                # Close the pool
+                pool.close()
+            # Without multiprocessing
+            else:
+                results = []
+                for i in range(n_new_samples):
+                    results.append(self.dual_annealing(exploit_method, subdomains[i], sigma2, var, i))
+                    
+            # New sample
+            Xnew = np.array([results[i][1] for i in range(n_new_samples)])
+            print("\nXnew:\n", Xnew)
+
+            # Computational cost
+            elapsed_time = time.time() - start_time
+            print("\n")
+            print(f"Elapsed_time: {round(elapsed_time,2)} sec.")
+            print('-'*20)
+            
+            return Xnew, None
+        
+        # Generate needed Exploration class
+        explore = Exploration(self.ExpDesign, n_candidates)
+        explore.w = 100  # * ndim #500  # TODO: where does this value come from?
+        
+        # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+        explore.mc_criterion = 'mc-intersite-proj'
+        
+        # Generate the candidate samples
+        # TODO: here use the sampling method provided by the expdesign?
+        sampling_method = self.ExpDesign.sampling_method
+        
+        # TODO: changed this from 'random' for LOOCV
+        if explore_method == 'LOOCV':
+            allCandidates = self.ExpDesign.generate_samples(n_candidates,
+                                                            sampling_method)
+        else:
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+        
+        # -----------------------------------------
+        # ---------- EXPLORATION METHODS ----------
+        # -----------------------------------------
+        if explore_method == 'LOOCV':
+            # -----------------------------------------------------------------
+            # TODO: LOOCV model construnction based on Feng et al. (2020)
+            # 'LOOCV':
+            # Initilize the ExploitScore array
+
+            # Generate random samples
+            allCandidates = self.ExpDesign.generate_samples(n_candidates,
+                                                                'random')
+
+            # Construct error model based on LCerror
+            errorModel = self.MetaModel.create_ModelError(old_EDX, self.LCerror)
+            self.errorModel.append(copy(errorModel))
+
+            # Evaluate the error models for allCandidates
+            eLCAllCands, _ = errorModel.eval_errormodel(allCandidates)
+            # Select the maximum as the representative error
+            eLCAllCands = np.dstack(eLCAllCands.values())
+            eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0]
+
+            # Normalize the error w.r.t the maximum error
+            scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates)
+
+        else:
+            # ------- EXPLORATION: SPACE-FILLING DESIGN -------
+            # Generate candidate samples from Exploration class
+            explore = Exploration(self.ExpDesign, n_candidates)
+            explore.w = 100  # * ndim #500
+            # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+            explore.mc_criterion = 'mc-intersite-proj'
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+
+            # Temp: ---- Plot all candidates -----
+            if ndim == 2:
+                def plotter(points, allCandidates, Method,
+                            scoreExploration=None):
+                    if Method == 'Voronoi':
+                        from scipy.spatial import Voronoi, voronoi_plot_2d
+                        vor = Voronoi(points)
+                        fig = voronoi_plot_2d(vor)
+                        ax1 = fig.axes[0]
+                    else:
+                        fig = plt.figure()
+                        ax1 = fig.add_subplot(111)
+                    ax1.scatter(points[:, 0], points[:, 1], s=10, c='r',
+                                marker="s", label='Old Design Points')
+                    ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10,
+                                c='b', marker="o", label='Design candidates')
+                    for i in range(points.shape[0]):
+                        txt = 'p'+str(i+1)
+                        ax1.annotate(txt, (points[i, 0], points[i, 1]))
+                    if scoreExploration is not None:
+                        for i in range(allCandidates.shape[0]):
+                            txt = str(round(scoreExploration[i], 5))
+                            ax1.annotate(txt, (allCandidates[i, 0],
+                                               allCandidates[i, 1]))
+
+                    plt.xlim(self.bound_tuples[0])
+                    plt.ylim(self.bound_tuples[1])
+                    # plt.show()
+                    plt.legend(loc='upper left')
+
+        # -----------------------------------------
+        # --------- EXPLOITATION METHODS ----------
+        # -----------------------------------------
+        if exploit_method == 'BayesOptDesign' or\
+           exploit_method == 'BayesActDesign':
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradeoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\n Exploration weight={explore_w:0.3f} "
+                  f"Exploitation weight={exploit_w:0.3f}\n")
+
+            # ------- EXPLOITATION: BayesOptDesign & ActiveLearning -------
+            if explore_w != 1.0:
+                # Check if all needed properties are set
+                if not hasattr(self.ExpDesign, 'max_func_itr'):
+                    raise AttributeError('max_func_itr not given to the experimental design')
+
+                # Create a sample pool for rejection sampling
+                MCsize = 15000
+                X_MC = self.ExpDesign.generate_samples(MCsize, 'random')
+                candidates = self.ExpDesign.generate_samples(
+                    n_candidates, 'latin_hypercube')
+
+                # Split the candidates in groups for multiprocessing
+                split_cand = np.array_split(
+                    candidates, n_cand_groups, axis=0
+                    )
+               # print(candidates)
+               # print(split_cand)
+                if self.parallel:
+                    results = Parallel(n_jobs=-1, backend='multiprocessing')(
+                        delayed(self.run_util_func)(
+                            exploit_method, split_cand[i], i, sigma2, var, X_MC)
+                        for i in range(n_cand_groups)) 
+                else:
+                    results = []
+                    for i in range(n_cand_groups):
+                        results.append(self.run_util_func(exploit_method, split_cand[i], i, sigma2, var, X_MC))
+                        
+                # Retrieve the results and append them
+                U_J_d = np.concatenate([results[NofE][1] for NofE in
+                                        range(n_cand_groups)])
+
+                # Check if all scores are inf
+                if np.isinf(U_J_d).all() or np.isnan(U_J_d).all():
+                    U_J_d = np.ones(len(U_J_d))
+
+                # Get the expected value (mean) of the Utility score
+                # for each cell
+                if explore_method == 'Voronoi':
+                    U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1)
+
+                # Normalize U_J_d
+                norm_U_J_d = U_J_d / np.sum(U_J_d)
+            else:
+                norm_U_J_d = np.zeros((len(scoreExploration)))
+
+            # ------- Calculate Total score -------
+            # ------- Trade off between EXPLORATION & EXPLOITATION -------
+            # Accumulate the samples
+            finalCandidates = np.concatenate((allCandidates, candidates), axis = 0)   
+            finalCandidates = np.unique(finalCandidates, axis = 0)
+            
+            # Calculations take into account both exploration and exploitation 
+            # samples without duplicates
+            totalScore = np.zeros(finalCandidates.shape[0])
+            #self.totalScore = totalScore
+            
+            for cand_idx in range(finalCandidates.shape[0]):
+                # find candidate indices
+                idx1 = np.where(allCandidates == finalCandidates[cand_idx])[0]
+                idx2 = np.where(candidates == finalCandidates[cand_idx])[0]
+                
+                # exploration 
+                if idx1 != []:
+                    idx1 = idx1[0]
+                    totalScore[cand_idx] += explore_w * scoreExploration[idx1]
+                    
+                # exploitation
+                if idx2 != []:
+                    idx2 = idx2[0]
+                    totalScore[cand_idx] += exploit_w * norm_U_J_d[idx2]
+                
+
+            # Total score
+            totalScore = exploit_w * norm_U_J_d
+            totalScore += explore_w * scoreExploration
+
+            # temp: Plot
+            # dim = self.ExpDesign.X.shape[1]
+            # if dim == 2:
+            #     plotter(self.ExpDesign.X, allCandidates, explore_method)
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            # select the requested number of samples
+            if explore_method == 'Voronoi':
+                Xnew = np.zeros((n_new_samples, ndim))
+                for i, idx in enumerate(bestIdx):
+                    X_can = explore.closestPoints[idx]
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+            else:
+                # Changed this from allCandiates to full set of candidates 
+                # TODO: still not changed for e.g. 'Voronoi'
+                Xnew = finalCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+
+        elif exploit_method == 'VarOptDesign':
+            # ------- EXPLOITATION: VarOptDesign -------
+            UtilMethod = var
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradeoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\nweightExploration={explore_w:0.3f} "
+                  f"weightExploitation={exploit_w:0.3f}")
+
+            # Generate candidate samples from Exploration class
+            nMeasurement = old_EDY[OutputNames[0]].shape[1]
+            
+           # print(UtilMethod)
+            
+            # Find sensitive region
+            if UtilMethod == 'LOOCV':
+                LCerror = self.MetaModel.LCerror
+                allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames),
+                                           nMeasurement))
+                for y_idx, y_key in enumerate(OutputNames):
+                    for idx, key in enumerate(LCerror[y_key].keys()):
+                        allModifiedLOO[:, y_idx, idx] = abs(
+                            LCerror[y_key][key])
+
+                ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1)
+               # print(allModifiedLOO.shape)
+
+            elif UtilMethod in ['EIGF', 'ALM']:
+                # ----- All other in  ['EIGF', 'ALM'] -----
+                # Initilize the ExploitScore array
+                ExploitScore = np.zeros((len(old_EDX), len(OutputNames)))
+
+                # Split the candidates in groups for multiprocessing
+                if explore_method != 'Voronoi':
+                    split_cand = np.array_split(allCandidates,
+                                                n_cand_groups,
+                                                axis=0)
+                    goodSampleIdx = range(n_cand_groups)
+                else:
+                    # Find indices of the Vornoi cells with samples
+                    goodSampleIdx = []
+                    for idx in range(len(explore.closest_points)):
+                        if len(explore.closest_points[idx]) != 0:
+                            goodSampleIdx.append(idx)
+                    split_cand = explore.closest_points
+
+                # Split the candidates in groups for multiprocessing
+                args = []
+                for index in goodSampleIdx:
+                    args.append((exploit_method, split_cand[index], index,
+                                 sigma2, var))
+
+                # Multiprocessing
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.run_util_func, args).get()
+
+                # Close the pool
+                pool.close()
+
+                # Retrieve the results and append them
+                if explore_method == 'Voronoi':
+                    ExploitScore = [np.mean(results[k][1]) for k in
+                                    range(len(goodSampleIdx))]
+                else:
+                    ExploitScore = np.concatenate(
+                        [results[k][1] for k in range(len(goodSampleIdx))])
+
+            else:
+                raise NameError('The requested utility function is not '
+                                'available.')
+
+            # print("ExploitScore:\n", ExploitScore)
+
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            # Total score
+            # Normalize U_J_d
+            ExploitScore = ExploitScore / np.sum(ExploitScore)
+            totalScore = exploit_w * ExploitScore
+           # print(totalScore.shape)
+           # print(explore_w)
+           # print(scoreExploration.shape)
+            totalScore += explore_w * scoreExploration
+
+            temp = totalScore.copy()
+            sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            Xnew = np.zeros((n_new_samples, ndim))
+            if explore_method != 'Voronoi':
+                Xnew = allCandidates[bestIdx]
+            else:
+                for i, idx in enumerate(bestIdx.flatten()):
+                    X_can = explore.closest_points[idx]
+                    # plotter(self.ExpDesign.X, X_can, explore_method,
+                    # scoreExploration=None)
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+
+        elif exploit_method == 'alphabetic':
+            # ------- EXPLOITATION: ALPHABETIC -------
+            Xnew = self.util_AlphOptDesign(allCandidates, var)
+
+        elif exploit_method == 'Space-filling':
+            # ------- EXPLOITATION: SPACE-FILLING -------
+            totalScore = scoreExploration
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+
+            # select the requested number of samples
+            Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        else:
+            raise NameError('The requested design method is not available.')
+
+        print("\n")
+        print("\nRun No. {}:".format(old_EDX.shape[0]+1))
+        print("Xnew:\n", Xnew)
+
+        # TODO: why does it also return None?
+        return Xnew, None
+
+    # -------------------------------------------------------------------------
+    def util_AlphOptDesign(self, candidates, var='D-Opt'):
+        """
+        Enriches the Experimental design with the requested alphabetic
+        criterion based on exploring the space with number of sampling points.
+
+        Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos
+        expansion: A review of sampling strategies., Computer Methods in
+        Applied Mechanics and Engineering, 332, 382-407.
+
+        Arguments
+        ---------
+        NCandidate : int
+            Number of candidate points to be searched
+
+        var : string
+            Alphabetic optimality criterion
+
+        Returns
+        -------
+        X_new : array of shape (1, n_params)
+            The new sampling location in the input space.
+        """
+        MetaModelOrig = self # TODO: this doesn't fully seem correct?
+        n_new_samples = MetaModelOrig.ExpDesign.n_new_samples
+        NCandidate = candidates.shape[0]
+
+        # TODO: Loop over outputs
+        OutputName = self.out_names[0]
+
+        # To avoid changes ub original aPCE object
+        MetaModel = deepcopy(MetaModelOrig)
+
+        # Old Experimental design
+        oldExpDesignX = self.ExpDesign.X
+
+        # TODO: Only one psi can be selected.
+        # Suggestion: Go for the one with the highest LOO error
+        # TODO: this is just a patch, need to look at again!
+        Scores = list(self.MetaModel.score_dict['b_1'][OutputName].values())
+        #print(Scores)
+        #print(self.MetaModel.score_dict)
+        #print(self.MetaModel.score_dict.values())
+        #print(self.MetaModel.score_dict['b_1'].values())
+        #print(self.MetaModel.score_dict['b_1'][OutputName].values())
+        ModifiedLOO = [1-score for score in Scores]
+        outIdx = np.argmax(ModifiedLOO)
+
+        # Initialize Phi to save the criterion's values
+        Phi = np.zeros((NCandidate))
+
+        # TODO: also patched here
+        BasisIndices = self.MetaModel.basis_dict['b_1'][OutputName]["y_"+str(outIdx+1)]
+        P = len(BasisIndices)
+
+        # ------ Old Psi ------------
+        univ_p_val = self.MetaModel.univ_basis_vals(oldExpDesignX)
+        Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val)
+
+        # ------ New candidates (Psi_c) ------------
+        # Assemble Psi_c
+        univ_p_val_c = self.MetaModel.univ_basis_vals(candidates)
+        Psi_c = self.MetaModel.create_psi(BasisIndices, univ_p_val_c)
+
+        for idx in range(NCandidate):
+
+            # Include the new row to the original Psi
+            Psi_cand = np.vstack((Psi, Psi_c[idx]))
+
+            # Information matrix
+            PsiTPsi = np.dot(Psi_cand.T, Psi_cand)
+            M = PsiTPsi / (len(oldExpDesignX)+1)
+
+            if np.linalg.cond(PsiTPsi) > 1e-12 \
+               and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon:
+                # faster
+                invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray())
+            else:
+                # stabler
+                invM = np.linalg.pinv(M)
+
+            # ---------- Calculate optimality criterion ----------
+            # Optimality criteria according to Section 4.5.1 in Ref.
+
+            # D-Opt
+            if var.lower() == 'd-opt':
+                Phi[idx] = (np.linalg.det(invM)) ** (1/P)
+
+            # A-Opt
+            elif var.lower() == 'a-opt':
+                Phi[idx] = np.trace(invM)
+
+            # K-Opt
+            elif var.lower() == 'k-opt':
+                Phi[idx] = np.linalg.cond(M)
+
+            else:
+               # print(var.lower())
+                raise Exception('The optimality criterion you requested has '
+                      'not been implemented yet!')
+
+        # find an optimal point subset to add to the initial design
+        # by minimization of the Phi
+        sorted_idxtotalScore = np.argsort(Phi)
+
+        # select the requested number of samples
+        Xnew = candidates[sorted_idxtotalScore[:n_new_samples]]
+
+        return Xnew
+
+    # -------------------------------------------------------------------------
+    def _normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s,
+                  rmse=None):
+        """
+        Calculated gaussian likelihood for given y+std based on given obs+sigma
+        # TODO: is this understanding correct?
+        
+        Parameters
+        ----------
+        y_hat_pce : dict of 2d np arrays
+            Mean output of the surrogate.
+        std_pce : dict of 2d np arrays
+            Standard deviation output of the surrogate.
+        obs_data : dict of 1d np arrays
+            Observed data.
+        total_sigma2s : pandas dataframe, matches obs_data
+            Estimated uncertainty for the observed data.
+        rmse : dict, optional
+            RMSE values from validation of the surrogate. The default is None.
+
+        Returns
+        -------
+        likelihoods : dict of float
+            The likelihood for each surrogate eval in y_hat_pce compared to the
+            observations (?).
+
+        """
+
+        likelihoods = 1.0
+
+        # Loop over the outputs
+        for idx, out in enumerate(self.out_names):
+
+            # (Meta)Model Output
+           # print(y_hat_pce[out])
+            nsamples, nout = y_hat_pce[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values
+
+            # Surrogate error if valid dataset is given.
+            if rmse is not None:
+                tot_sigma2s += rmse[out]**2
+            else:
+                tot_sigma2s += np.mean(std_pce[out])**2
+
+            likelihoods *= stats.multivariate_normal.pdf(
+                y_hat_pce[out], data, np.diag(tot_sigma2s),
+                allow_singular=True)
+
+        # TODO: remove this here
+        self.Likelihoods = likelihoods
+
+        return likelihoods
+
+    # -------------------------------------------------------------------------
+    def _corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = self.ExpDesign.X  # valid_samples
+        model_outputs = self.ExpDesign.Y  # valid_model_runs
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = self.out_names
+
+        # TODO: Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                # covMatrix = np.diag(stdOutputRS[out][i]**2)
+                covMatrix = np.diag((y_m-y_m_hat)**2)
+                covMatrix = np.diag(
+                    np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2
+                    )
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += logpdf(
+                    y_m_hat, data, covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += logpdf(y_m_hat, y_m, covMatrix)
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.exp(logLik_model+logLik_data)
+
+        return np.log(np.mean(weights))
+
+    # -------------------------------------------------------------------------
+    def _posteriorPlot(self, posterior, par_names, key):
+        """
+        Plot the posterior of a specific key as a corner plot
+
+        Parameters
+        ----------
+        posterior : 2d np.array
+            Samples of the posterior.
+        par_names : list of strings
+            List of the parameter names.
+        key : string
+            Output key that this posterior belongs to.
+
+        Returns
+        -------
+        figPosterior : corner.corner
+            Plot of the posterior.
+
+        """
+
+        # Initialization
+        newpath = (r'Outputs_SeqPosteriorComparison/posterior')
+        os.makedirs(newpath, exist_ok=True)
+
+        bound_tuples = self.ExpDesign.bound_tuples
+        n_params = len(par_names)
+        font_size = 40
+        if n_params == 2:
+
+            figPosterior, ax = plt.subplots(figsize=(15, 15))
+
+            sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1],
+                        fill=True, ax=ax, cmap=plt.cm.jet,
+                        clip=bound_tuples)
+            # Axis labels
+            plt.xlabel(par_names[0], fontsize=font_size)
+            plt.ylabel(par_names[1], fontsize=font_size)
+
+            # Set axis limit
+            plt.xlim(bound_tuples[0])
+            plt.ylim(bound_tuples[1])
+
+            # Increase font size
+            plt.xticks(fontsize=font_size)
+            plt.yticks(fontsize=font_size)
+
+            # Switch off the grids
+            plt.grid(False)
+
+        else:
+            import corner
+            figPosterior = corner.corner(posterior, labels=par_names,
+                                         title_fmt='.2e', show_titles=True,
+                                         title_kwargs={"fontsize": 12})
+
+        figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight')
+        plt.close()
+
+        # Save the posterior as .npy
+        np.save(f'./{newpath}/{key}.npy', posterior)
+
+        return figPosterior
+
+    
+    # -------------------------------------------------------------------------
+    def _BME_Calculator(self, obs_data, sigma2Dict, rmse=None):
+        """
+        This function computes the Bayesian model evidence (BME) via Monte
+        Carlo integration.
+
+        Parameters
+        ----------
+        obs_data : dict of 1d np arrays
+            Observed data.
+        sigma2Dict : pandas dataframe, matches obs_data
+            Estimated uncertainty for the observed data.
+        rmse : dict of floats, optional
+            RMSE values for each output-key. The dafault is None.
+
+        Returns
+        -------
+        (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+        
+        """
+        # Initializations
+        if hasattr(self, 'valid_likelihoods'):
+            valid_likelihoods = self.valid_likelihoods
+        else:
+            valid_likelihoods = []
+        valid_likelihoods = np.array(valid_likelihoods)
+
+        post_snapshot = self.ExpDesign.post_snapshot
+        if post_snapshot or valid_likelihoods.shape[0] != 0:
+            newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref')
+            os.makedirs(newpath, exist_ok=True)
+
+        SamplingMethod = 'random'
+        MCsize = 10000
+        ESS = 0
+
+        # Estimation of the integral via Monte Varlo integration
+        while (ESS > MCsize) or (ESS < 1):
+
+            # Generate samples for Monte Carlo simulation
+            X_MC = self.ExpDesign.generate_samples(
+                MCsize, SamplingMethod
+                )
+
+            # Monte Carlo simulation for the candidate design
+            Y_MC, std_MC = self.MetaModel.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and
+            # simulation results via PCE with candidate design)
+            Likelihoods = self._normpdf(
+                Y_MC, std_MC, obs_data, sigma2Dict, rmse
+                )
+
+            # Check the Effective Sample Size (1000<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if (ESS > MCsize) or (ESS < 1):
+                print(f'ESS={ESS} MC size should be larger.')
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+        X_Posterior = X_MC[accepted]
+
+        # ------------------------------------------------------------
+        # --- Kullback-Leibler Divergence & Information Entropy ------
+        # ------------------------------------------------------------
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(Likelihoods))
+
+        # TODO: Correction factor
+        # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME)
+
+        # Posterior-based expectation of likelihoods
+        postExpLikelihoods = np.mean(np.log(Likelihoods[accepted]))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(
+            np.log(self.ExpDesign.JDist.pdf(X_Posterior.T))
+            )
+
+        # Calculate Kullback-Leibler Divergence
+        # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+        KLD = postExpLikelihoods - logBME
+
+        # Information Entropy based on Entropy paper Eq. 38
+        infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+        # If post_snapshot is True, plot likelihood vs refrence
+        if post_snapshot or valid_likelihoods:
+            # Hellinger distance
+            valid_likelihoods = np.array(valid_likelihoods)
+            ref_like = np.log(valid_likelihoods[(valid_likelihoods > 0)])
+            est_like = np.log(Likelihoods[Likelihoods > 0])
+            distHellinger = hellinger_distance(ref_like, est_like)
+            
+            idx = len([name for name in os.listdir(newpath) if 'Likelihoods_'
+                       in name and os.path.isfile(os.path.join(newpath, name))])
+            
+            fig, ax = plt.subplots()
+            try:
+                sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]),
+                            shade=True, color="g", label='Ref. Likelihood')
+                sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True,
+                            color="b", label='Likelihood with PCE')
+            except:
+                pass
+
+            text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}"
+            "\n DKL={KLD:.3f}"
+
+            plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat',
+                                                 edgecolor='black',
+                                                 boxstyle='round,pad=1'),
+                     transform=ax.transAxes)
+
+            fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf',
+                        bbox_inches='tight')
+            plt.close()
+
+        else:
+            distHellinger = 0.0
+
+        # Bayesian inference with Emulator only for 2D problem
+        if post_snapshot and self.MetaModel.n_params == 2 and not idx % 5:
+            BayesOpts = BayesInference(self)
+
+            BayesOpts.emulator = True
+            BayesOpts.plot_post_pred = False
+
+            # Select the inference method
+            import emcee
+            BayesOpts.inference_method = "MCMC"
+            # Set the MCMC parameters passed to self.mcmc_params
+            BayesOpts.mcmc_params = {
+                'n_steps': 1e5,
+                'n_walkers': 30,
+                'moves': emcee.moves.KDEMove(),
+                'verbose': False
+                }
+
+            # ----- Define the discrepancy model -------
+            # TODO: check with Farid if this first line is how it should be
+            BayesOpts.measured_data = obs_data
+            obs_data = pd.DataFrame(obs_data, columns=self.out_names)
+            BayesOpts.measurement_error = obs_data
+            # TODO: shouldn't the uncertainty be sigma2Dict instead of obs_data?
+
+            # # -- (Option B) --
+            DiscrepancyOpts = Discrepancy('')
+            DiscrepancyOpts.type = 'Gaussian'
+            DiscrepancyOpts.parameters = obs_data**2
+            BayesOpts.Discrepancy = DiscrepancyOpts
+            # Start the calibration/inference
+            Bayes_PCE = BayesOpts.create_inference()
+            X_Posterior = Bayes_PCE.posterior_df.values
+
+        return (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+
+    # -------------------------------------------------------------------------
+    def _validError(self):
+        """
+        Evaluate the metamodel on the validation samples and calculate the
+        error against the corresponding model runs
+
+        Returns
+        -------
+        rms_error : dict
+            RMSE for each validation run.
+        valid_error : dict
+            Normed (?)RMSE for each validation run.
+
+        """
+        # Extract the original model with the generated samples
+        valid_model_runs = self.ExpDesign.valid_model_runs
+
+        # Run the PCE model with the generated samples
+        valid_PCE_runs, _ = self.MetaModel.eval_metamodel(samples=self.ExpDesign.valid_samples)
+
+        rms_error = {}
+        valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in self.out_names:
+            rms_error[key] = mean_squared_error(
+                valid_model_runs[key], valid_PCE_runs[key],
+                multioutput='raw_values',
+                sample_weight=None,
+                squared=False)
+            # Validation error
+            valid_error[key] = (rms_error[key]**2)
+            valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Updated Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(rms_error[key],
+                                             valid_error[key]))))
+
+        return rms_error, valid_error
+
+    # -------------------------------------------------------------------------
+    def _error_Mean_Std(self):
+        """
+        Calculates the error in the overall mean and std approximation of the
+        surrogate against the mc-reference provided to the model.
+        This can only be applied to metamodels of polynomial type
+
+        Returns
+        -------
+        RMSE_Mean : float
+            RMSE of the means 
+        RMSE_std : float
+            RMSE of the standard deviations
+
+        """
+        # Compute the mean and std based on the MetaModel
+        pce_means, pce_stds = self.MetaModel._compute_pce_moments()
+
+        # Compute the root mean squared error
+        for output in self.out_names:
+
+            # Compute the error between mean and std of MetaModel and OrigModel
+            RMSE_Mean = mean_squared_error(
+                self.Model.mc_reference['mean'], pce_means[output], squared=False
+                )
+            RMSE_std = mean_squared_error(
+                self.Model.mc_reference['std'], pce_stds[output], squared=False
+                )
+
+        return RMSE_Mean, RMSE_std
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/eval_rec_rule.py b/examples/analytical-function/bayesvalidrox/surrogate_models/eval_rec_rule.py
new file mode 100644
index 0000000000000000000000000000000000000000..b583c7eb2ec58d55d19b34130812730d21a12368
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/eval_rec_rule.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+
+
+Based on the implementation in UQLab [1].
+
+References:
+1. S. Marelli, and B. Sudret, UQLab: A framework for uncertainty quantification
+in Matlab, Proc. 2nd Int. Conf. on Vulnerability, Risk Analysis and Management
+(ICVRAM2014), Liverpool, United Kingdom, 2014, 2554-2563.
+
+2. S. Marelli, N. Lüthen, B. Sudret, UQLab user manual – Polynomial chaos
+expansions, Report # UQLab-V1.4-104, Chair of Risk, Safety and Uncertainty
+Quantification, ETH Zurich, Switzerland, 2021.
+
+Author: Farid Mohammadi, M.Sc.
+E-Mail: farid.mohammadi@iws.uni-stuttgart.de
+Department of Hydromechanics and Modelling of Hydrosystems (LH2)
+Institute for Modelling Hydraulic and Environmental Systems (IWS), University
+of Stuttgart, www.iws.uni-stuttgart.de/lh2/
+Pfaffenwaldring 61
+70569 Stuttgart
+
+Created on Fri Jan 14 2022
+"""
+import numpy as np
+from numpy.polynomial.polynomial import polyval
+
+
+def poly_rec_coeffs(n_max, poly_type, params=None):
+    """
+    Computes the recurrence coefficients for classical Wiener-Askey orthogonal
+    polynomials.
+
+    Parameters
+    ----------
+    n_max : int
+        Maximum polynomial degree.
+    poly_type : string
+        Polynomial type.
+    params : list, optional
+        Parameters required for `laguerre` poly type. The default is None.
+
+    Returns
+    -------
+    AB : dict
+        The 3 term recursive coefficients and the applicable ranges.
+
+    """
+
+    if poly_type == 'legendre':
+
+        def an(n):
+            return np.zeros((n+1, 1))
+
+        def sqrt_bn(n):
+            sq_bn = np.zeros((n+1, 1))
+            sq_bn[0, 0] = 1
+            for i in range(1, n+1):
+                sq_bn[i, 0] = np.sqrt(1./(4-i**-2))
+            return sq_bn
+
+        bounds = [-1, 1]
+
+    elif poly_type == 'hermite':
+
+        def an(n):
+            return np.zeros((n+1, 1))
+
+        def sqrt_bn(n):
+            sq_bn = np.zeros((n+1, 1))
+            sq_bn[0, 0] = 1
+            for i in range(1, n+1):
+                sq_bn[i, 0] = np.sqrt(i)
+            return sq_bn
+
+        bounds = [-np.inf, np.inf]
+
+    elif poly_type == 'laguerre':
+
+        def an(n):
+            a = np.zeros((n+1, 1))
+            for i in range(1, n+1):
+                a[i] = 2*n + params[1]
+            return a
+
+        def sqrt_bn(n):
+            sq_bn = np.zeros((n+1, 1))
+            sq_bn[0, 0] = 1
+            for i in range(1, n+1):
+                sq_bn[i, 0] = -np.sqrt(i * (i+params[1]-1))
+            return sq_bn
+
+        bounds = [0, np.inf]
+
+    AB = {'alpha_beta': np.concatenate((an(n_max), sqrt_bn(n_max)), axis=1),
+          'bounds': bounds}
+
+    return AB
+
+
+def eval_rec_rule(x, max_deg, poly_type):
+    """
+    Evaluates the polynomial that corresponds to the Jacobi matrix defined
+    from the AB.
+
+    Parameters
+    ----------
+    x : array (n_samples)
+        Points where the polynomials are evaluated.
+    max_deg : int
+        Maximum degree.
+    poly_type : string
+        Polynomial type.
+
+    Returns
+    -------
+    values : array of shape (n_samples, max_deg+1)
+        Polynomials corresponding to the Jacobi matrix.
+
+    """
+    AB = poly_rec_coeffs(max_deg, poly_type)
+    AB = AB['alpha_beta']
+
+    values = np.zeros((len(x), AB.shape[0]+1))
+    values[:, 1] = 1 / AB[0, 1]
+
+    for k in range(AB.shape[0]-1):
+        values[:, k+2] = np.multiply((x - AB[k, 0]), values[:, k+1]) - \
+                         np.multiply(values[:, k], AB[k, 1])
+        values[:, k+2] = np.divide(values[:, k+2], AB[k+1, 1])
+    return values[:, 1:]
+
+
+def eval_rec_rule_arbitrary(x, max_deg, poly_coeffs):
+    """
+    Evaluates the polynomial at sample array x.
+
+    Parameters
+    ----------
+    x : array (n_samples)
+        Points where the polynomials are evaluated.
+    max_deg : int
+        Maximum degree.
+    poly_coeffs : dict
+        Polynomial coefficients computed based on moments.
+
+    Returns
+    -------
+    values : array of shape (n_samples, max_deg+1)
+        Univariate Polynomials evaluated at samples.
+
+    """
+    values = np.zeros((len(x), max_deg+1))
+
+    for deg in range(max_deg+1):
+        values[:, deg] = polyval(x, poly_coeffs[deg]).T
+
+    return values
+
+
+def eval_univ_basis(x, max_deg, poly_types, apoly_coeffs=None):
+    """
+    Evaluates univariate regressors along input directions.
+
+    Parameters
+    ----------
+    x : array of shape (n_samples, n_params)
+        Training samples.
+    max_deg : int
+        Maximum polynomial degree.
+    poly_types : list of strings
+        List of polynomial types for all parameters.
+    apoly_coeffs : dict , optional
+        Polynomial coefficients computed based on moments. The default is None.
+
+    Returns
+    -------
+    univ_vals : array of shape (n_samples, n_params, max_deg+1)
+        Univariate polynomials for all degrees and parameters evaluated at x.
+
+    """
+    # Initilize the output array
+    n_samples, n_params = x.shape
+    univ_vals = np.zeros((n_samples, n_params, max_deg+1))
+
+    for i in range(n_params):
+
+        if poly_types[i] == 'arbitrary':
+            polycoeffs = apoly_coeffs[f'p_{i+1}']
+            univ_vals[:, i] = eval_rec_rule_arbitrary(x[:, i], max_deg,
+                                                      polycoeffs)
+        else:
+            univ_vals[:, i] = eval_rec_rule(x[:, i], max_deg, poly_types[i])
+
+    return univ_vals
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/exp_designs.py b/examples/analytical-function/bayesvalidrox/surrogate_models/exp_designs.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa03fe17d96fb2c1f19546b7b72fb2fd6dd1c13a
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/exp_designs.py
@@ -0,0 +1,479 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Experimental design with associated sampling methods
+"""
+
+import numpy as np
+import math
+import itertools
+import chaospy
+import scipy.stats as st
+from tqdm import tqdm
+import h5py
+import os
+
+from .apoly_construction import apoly_construction
+from .input_space import InputSpace
+
+# -------------------------------------------------------------------------
+def check_ranges(theta, ranges):
+    """
+    This function checks if theta lies in the given ranges.
+
+    Parameters
+    ----------
+    theta : array
+        Proposed parameter set.
+    ranges : nested list
+        List of the praremeter ranges.
+
+    Returns
+    -------
+    c : bool
+        If it lies in the given range, it return True else False.
+
+    """
+    c = True
+    # traverse in the list1
+    for i, bounds in enumerate(ranges):
+        x = theta[i]
+        # condition check
+        if x < bounds[0] or x > bounds[1]:
+            c = False
+            return c
+    return c
+
+
+class ExpDesigns(InputSpace):
+    """
+    This class generates samples from the prescribed marginals for the model
+    parameters using the `Input` object.
+
+    Attributes
+    ----------
+    Input : obj
+        Input object containing the parameter marginals, i.e. name,
+        distribution type and distribution parameters or available raw data.
+    meta_Model_type : str
+        Type of the meta_Model_type.
+    sampling_method : str
+        Name of the sampling method for the experimental design. The following
+        sampling method are supported:
+
+        * random
+        * latin_hypercube
+        * sobol
+        * halton
+        * hammersley
+        * chebyshev(FT)
+        * grid(FT)
+        * user
+    hdf5_file : str
+        Name of the hdf5 file that contains the experimental design.
+    n_new_samples : int
+        Number of (initial) training points.
+    n_max_samples : int
+        Number of maximum training points.
+    mod_LOO_threshold : float
+        The modified leave-one-out cross validation threshold where the
+        sequential design stops.
+    tradeoff_scheme : str
+        Trade-off scheme to assign weights to the exploration and exploitation
+        scores in the sequential design.
+    n_canddidate : int
+        Number of candidate training sets to calculate the scores for.
+    explore_method : str
+        Type of the exploration method for the sequential design. The following
+        methods are supported:
+
+        * Voronoi
+        * random
+        * latin_hypercube
+        * LOOCV
+        * dual annealing
+    exploit_method : str
+        Type of the exploitation method for the sequential design. The
+        following methods are supported:
+
+        * BayesOptDesign
+        * BayesActDesign
+        * VarOptDesign
+        * alphabetic
+        * Space-filling
+    util_func : str or list
+        The utility function to be specified for the `exploit_method`. For the
+        available utility functions see Note section.
+    n_cand_groups : int
+        Number of candidate groups. Each group of candidate training sets will
+        be evaulated separately in parallel.
+    n_replication : int
+        Number of replications. Only for comparison. The default is 1.
+    post_snapshot : int
+        Whether to plot the posterior in the sequential design. The default is
+        `True`.
+    step_snapshot : int
+        The number of steps to plot the posterior in the sequential design. The
+        default is 1.
+    max_a_post : list or array
+        Maximum a posteriori of the posterior distribution, if known. The
+        default is `[]`.
+    adapt_verbose : bool
+        Whether to plot the model response vs that of metamodel for the new
+        trining point in the sequential design.
+
+    Note
+    ----------
+    The following utiliy functions for the **exploitation** methods are
+    supported:
+
+    #### BayesOptDesign (when data is available)
+    - DKL (Kullback-Leibler Divergence)
+    - DPP (D-Posterior-percision)
+    - APP (A-Posterior-percision)
+
+    #### VarBasedOptDesign -> when data is not available
+    - Entropy (Entropy/MMSE/active learning)
+    - EIGF (Expected Improvement for Global fit)
+    - LOOCV (Leave-one-out Cross Validation)
+
+    #### alphabetic
+    - D-Opt (D-Optimality)
+    - A-Opt (A-Optimality)
+    - K-Opt (K-Optimality)
+    """
+
+    def __init__(self, Input, meta_Model_type='pce',
+                 sampling_method='random', hdf5_file=None,
+                 n_new_samples=1, n_max_samples=None, mod_LOO_threshold=1e-16,
+                 tradeoff_scheme=None, n_canddidate=1, explore_method='random',
+                 exploit_method='Space-filling', util_func='Space-filling',
+                 n_cand_groups=4, n_replication=1, post_snapshot=False,
+                 step_snapshot=1, max_a_post=[], adapt_verbose=False, max_func_itr=1):
+
+        self.InputObj = Input
+        self.meta_Model_type = meta_Model_type
+        self.sampling_method = sampling_method
+        self.hdf5_file = hdf5_file
+        self.n_new_samples = n_new_samples
+        self.n_max_samples = n_max_samples
+        self.mod_LOO_threshold = mod_LOO_threshold
+        self.explore_method = explore_method
+        self.exploit_method = exploit_method
+        self.util_func = util_func
+        self.tradeoff_scheme = tradeoff_scheme
+        self.n_canddidate = n_canddidate
+        self.n_cand_groups = n_cand_groups
+        self.n_replication = n_replication
+        self.post_snapshot = post_snapshot
+        self.step_snapshot = step_snapshot
+        self.max_a_post = max_a_post
+        self.adapt_verbose = adapt_verbose
+        self.max_func_itr = max_func_itr
+        
+        # Other 
+        self.apce = None
+        self.ndim = None
+        
+        # Init 
+        self.check_valid_inputs()
+        
+    # -------------------------------------------------------------------------
+    def generate_samples(self, n_samples, sampling_method='random',
+                         transform=False):
+        """
+        Generates samples with given sampling method
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested samples.
+        sampling_method : str, optional
+            Sampling method. The default is `'random'`.
+        transform : bool, optional
+            Transformation via an isoprobabilistic transformation method. The
+            default is `False`.
+
+        Returns
+        -------
+        samples: array of shape (n_samples, n_params)
+            Generated samples from defined model input object.
+
+        """
+        try:
+            samples = chaospy.generate_samples(
+                int(n_samples), domain=self.origJDist, rule=sampling_method
+                )
+        except:
+            samples = self.random_sampler(int(n_samples)).T
+
+        return samples.T
+
+
+            
+    # -------------------------------------------------------------------------
+    def generate_ED(self, n_samples, transform=False,
+                    max_pce_deg=None):
+        """
+        Generates experimental designs (training set) with the given method.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested training points.
+        sampling_method : str, optional
+            Sampling method. The default is `'random'`.
+        transform : bool, optional
+            Isoprobabilistic transformation. The default is `False`.
+        max_pce_deg : int, optional
+            Maximum PCE polynomial degree. The default is `None`.
+            
+        Returns
+        -------
+        None
+
+        """
+        if n_samples <0:
+            raise ValueError('A negative number of samples cannot be created. Please provide positive n_samples')
+        n_samples = int(n_samples)
+        
+        if not hasattr(self, 'n_init_samples'):
+            self.n_init_samples = n_samples
+
+        # Generate the samples based on requested method
+        self.init_param_space(max_pce_deg)
+
+        sampling_method = self.sampling_method
+        # Pass user-defined samples as ED
+        if sampling_method == 'user':
+            if not hasattr(self, 'X'):
+                raise AttributeError('User-defined sampling cannot proceed as no samples provided. Please add them to this class as attribute X')
+            if not self.X.ndim == 2:
+                raise AttributeError('The provided samples shuld have 2 dimensions')
+            samples = self.X
+            self.n_samples = len(samples)
+
+        # Sample the distribution of parameters
+        elif self.input_data_given:
+            # Case II: Input values are directly given by the user.
+
+            if sampling_method == 'random':
+                samples = self.random_sampler(n_samples)
+
+            elif sampling_method == 'PCM' or \
+                    sampling_method == 'LSCM':
+                samples = self.pcm_sampler(n_samples, max_pce_deg)
+
+            else:
+                # Create ExpDesign in the actual space using chaospy
+                try:
+                    samples = chaospy.generate_samples(n_samples,
+                                                       domain=self.JDist,
+                                                       rule=sampling_method).T
+                except:
+                    samples = self.JDist.resample(n_samples).T
+
+        elif not self.input_data_given:
+            # Case I = User passed known distributions
+            samples = chaospy.generate_samples(n_samples, domain=self.JDist,
+                                               rule=sampling_method).T
+
+        self.X = samples
+            
+    def read_from_file(self, out_names):
+        """
+        Reads in the ExpDesign from a provided h5py file and saves the results.
+
+        Parameters
+        ----------
+        out_names : list of strings
+            The keys that are in the outputs (y) saved in the provided file.
+
+        Returns
+        -------
+        None.
+
+        """
+        if self.hdf5_file == None:
+            raise AttributeError('ExpDesign cannot be read in, please provide hdf5 file first')
+
+        # Read hdf5 file
+        f = h5py.File(self.hdf5_file, 'r+')
+
+        # Read EDX and pass it to ExpDesign object
+        try:
+            self.X = np.array(f["EDX/New_init_"])
+        except KeyError:
+            self.X = np.array(f["EDX/init_"])
+
+        # Update number of initial samples
+        self.n_init_samples = self.X.shape[0]
+
+        # Read EDX and pass it to ExpDesign object
+        self.Y = {}
+
+        # Extract x values
+        try:
+            self.Y["x_values"] = dict()
+            for varIdx, var in enumerate(out_names):
+                x = np.array(f[f"x_values/{var}"])
+                self.Y["x_values"][var] = x
+        except KeyError:
+            self.Y["x_values"] = np.array(f["x_values"])
+
+        # Store the output
+        for varIdx, var in enumerate(out_names):
+            try:
+                y = np.array(f[f"EDY/{var}/New_init_"])
+            except KeyError:
+                y = np.array(f[f"EDY/{var}/init_"])
+            self.Y[var] = y
+        f.close()
+        print(f'Experimental Design is read in from file {self.hdf5_file}')
+        print('')
+        
+    
+
+    # -------------------------------------------------------------------------
+    def random_sampler(self, n_samples, max_deg = None):
+        """
+        Samples the given raw data randomly.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested samples.
+            
+        max_deg : int, optional
+            Maximum degree. The default is `None`.
+            This will be used to run init_param_space, if it has not been done
+            until now.
+
+        Returns
+        -------
+        samples: array of shape (n_samples, n_params)
+            The sampling locations in the input space.
+
+        """
+        if not hasattr(self, 'raw_data'):
+            self.init_param_space(max_deg)
+        else:
+            if np.array(self.raw_data).ndim !=2:
+                raise AttributeError('The given raw data for sampling should have two dimensions')
+        samples = np.zeros((n_samples, self.ndim))
+        sample_size = self.raw_data.shape[1]
+
+        # Use a combination of raw data
+        if n_samples < sample_size:
+            for pa_idx in range(self.ndim):
+                # draw random indices
+                rand_idx = np.random.randint(0, sample_size, n_samples)
+                # store the raw data with given random indices
+                samples[:, pa_idx] = self.raw_data[pa_idx, rand_idx]
+        else:
+            try:
+                samples = self.JDist.resample(int(n_samples)).T
+            except AttributeError:
+                samples = self.JDist.sample(int(n_samples)).T
+            # Check if all samples are in the bound_tuples
+            for idx, param_set in enumerate(samples):
+                if not check_ranges(param_set, self.bound_tuples):
+                    try:
+                        proposed_sample = chaospy.generate_samples(
+                            1, domain=self.JDist, rule='random').T[0]
+                    except:
+                        proposed_sample = self.JDist.resample(1).T[0]
+                    while not check_ranges(proposed_sample,
+                                                 self.bound_tuples):
+                        try:
+                            proposed_sample = chaospy.generate_samples(
+                                1, domain=self.JDist, rule='random').T[0]
+                        except:
+                            proposed_sample = self.JDist.resample(1).T[0]
+                    samples[idx] = proposed_sample
+
+        return samples
+
+    # -------------------------------------------------------------------------
+    def pcm_sampler(self, n_samples, max_deg):
+        """
+        Generates collocation points based on the root of the polynomial
+        degrees.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested samples.
+        max_deg : int
+            Maximum degree defined by user. Will also be used to run 
+            init_param_space if that has not been done beforehand.
+
+        Returns
+        -------
+        opt_col_points: array of shape (n_samples, n_params)
+            Collocation points.
+
+        """
+        
+        if not hasattr(self, 'raw_data'):
+            self.init_param_space(max_deg)
+
+        raw_data = self.raw_data
+
+        # Guess the closest degree to self.n_samples
+        def M_uptoMax(deg):
+            result = []
+            for d in range(1, deg+1):
+                result.append(math.factorial(self.ndim+d) //
+                              (math.factorial(self.ndim) * math.factorial(d)))
+            return np.array(result)
+        #print(M_uptoMax(max_deg))
+        #print(np.where(M_uptoMax(max_deg) > n_samples)[0])
+
+        guess_Deg = np.where(M_uptoMax(max_deg) > n_samples)[0][0]
+
+        c_points = np.zeros((guess_Deg+1, self.ndim))
+
+        def PolynomialPa(parIdx):
+            return apoly_construction(self.raw_data[parIdx], max_deg)
+
+        for i in range(self.ndim):
+            poly_coeffs = PolynomialPa(i)[guess_Deg+1][::-1]
+            c_points[:, i] = np.trim_zeros(np.roots(poly_coeffs))
+
+        #  Construction of optimal integration points
+        Prod = itertools.product(np.arange(1, guess_Deg+2), repeat=self.ndim)
+        sort_dig_unique_combos = np.array(list(filter(lambda x: x, Prod)))
+
+        # Ranking relatively mean
+        Temp = np.empty(shape=[0, guess_Deg+1])
+        for j in range(self.ndim):
+            s = abs(c_points[:, j]-np.mean(raw_data[j]))
+            Temp = np.append(Temp, [s], axis=0)
+        temp = Temp.T
+
+        index_CP = np.sort(temp, axis=0)
+        sort_cpoints = np.empty((0, guess_Deg+1))
+
+        for j in range(self.ndim):
+            #print(index_CP[:, j])
+            sort_cp = c_points[index_CP[:, j], j]
+            sort_cpoints = np.vstack((sort_cpoints, sort_cp))
+
+        # Mapping of Combination to Cpoint Combination
+        sort_unique_combos = np.empty(shape=[0, self.ndim])
+        for i in range(len(sort_dig_unique_combos)):
+            sort_un_comb = []
+            for j in range(self.ndim):
+                SortUC = sort_cpoints[j, sort_dig_unique_combos[i, j]-1]
+                sort_un_comb.append(SortUC)
+                sort_uni_comb = np.asarray(sort_un_comb)
+            sort_unique_combos = np.vstack((sort_unique_combos, sort_uni_comb))
+
+        # Output the collocation points
+        if self.sampling_method.lower() == 'lscm':
+            opt_col_points = sort_unique_combos
+        else:
+            opt_col_points = sort_unique_combos[0:self.n_samples]
+
+        return opt_col_points
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/exploration.py b/examples/analytical-function/bayesvalidrox/surrogate_models/exploration.py
new file mode 100644
index 0000000000000000000000000000000000000000..6abb652f145fadb410ecf8f987142e8ceb544a41
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/exploration.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Exploration for sequential training of metamodels
+"""
+
+import numpy as np
+from scipy.spatial import distance
+
+
+class Exploration:
+    """
+    Created based on the Surrogate Modeling Toolbox (SUMO) [1].
+
+    [1] Gorissen, D., Couckuyt, I., Demeester, P., Dhaene, T. and Crombecq, K.,
+        2010. A surrogate modeling and adaptive sampling toolbox for computer
+        based design. Journal of machine learning research.-Cambridge, Mass.,
+        11, pp.2051-2055. sumo@sumo.intec.ugent.be - http://sumo.intec.ugent.be
+
+    Attributes
+    ----------
+    ExpDesign : obj
+        ExpDesign object.
+    n_candidate : int
+        Number of candidate samples.
+    mc_criterion : str
+        Selection crieterion. The default is `'mc-intersite-proj-th'`. Another
+        option is `'mc-intersite-proj'`.
+    w : int
+        Number of random points in the domain for each sample of the
+        training set.
+    """
+
+    def __init__(self, ExpDesign, n_candidate,
+                 mc_criterion='mc-intersite-proj-th'):
+        self.ExpDesign = ExpDesign
+        self.n_candidate = n_candidate
+        self.mc_criterion = mc_criterion
+        self.w = 100
+
+    def get_exploration_samples(self):
+        """
+        This function generates candidates to be selected as new design and
+        their associated exploration scores.
+
+        Returns
+        -------
+        all_candidates : array of shape (n_candidate, n_params)
+            A list of samples.
+        exploration_scores: arrays of shape (n_candidate)
+            Exploration scores.
+        """
+        explore_method = self.ExpDesign.explore_method
+
+        print("\n")
+        print(f' The {explore_method}-Method is selected as the exploration '
+              'method.')
+        print("\n")
+
+        if explore_method == 'Voronoi':
+            # Generate samples using the Voronoi method
+            all_candidates, exploration_scores = self.get_vornoi_samples()
+        else:
+            # Generate samples using the MC method
+            all_candidates, exploration_scores = self.get_mc_samples()
+
+        return all_candidates, exploration_scores
+
+    # -------------------------------------------------------------------------
+    def get_vornoi_samples(self):
+        """
+        This function generates samples based on voronoi cells and their
+        corresponding scores
+
+        Returns
+        -------
+        new_samples : array of shape (n_candidate, n_params)
+            A list of samples.
+        exploration_scores: arrays of shape (n_candidate)
+            Exploration scores.
+        """
+
+        mc_criterion = self.mc_criterion
+        n_candidate = self.n_candidate
+        # Get the Old ExpDesign #samples
+        old_ED_X = self.ExpDesign.X
+        ndim = old_ED_X.shape[1]
+
+        # calculate error #averageErrors
+        error_voronoi, all_candidates = self.approximate_voronoi(
+            self.w, old_ED_X
+            )
+
+        # Pick the best candidate point in the voronoi cell
+        # for each best sample
+        selected_samples = np.empty((0, ndim))
+        bad_samples = []
+
+        for index in range(len(error_voronoi)):
+
+            # get candidate new samples from voronoi tesselation
+            candidates = self.closest_points[index]
+
+            # get total number of candidates
+            n_new_samples = candidates.shape[0]
+
+            # still no candidate samples around this one, skip it!
+            if n_new_samples == 0:
+                print('The following sample has been skipped because there '
+                      'were no candidate samples around it...')
+                print(old_ED_X[index])
+                bad_samples.append(index)
+                continue
+
+            # find candidate that is farthest away from any existing sample
+            max_min_distance = 0
+            best_candidate = 0
+            min_intersite_dist = np.zeros((n_new_samples))
+            min_projected_dist = np.zeros((n_new_samples))
+
+            for j in range(n_new_samples):
+
+                new_samples = np.vstack((old_ED_X, selected_samples))
+
+                # find min distorted distance from all other samples
+                euclidean_dist = self._build_dist_matrix_point(
+                    new_samples, candidates[j], do_sqrt=True)
+                min_euclidean_dist = np.min(euclidean_dist)
+                min_intersite_dist[j] = min_euclidean_dist
+
+                # Check if this is the maximum minimum distance from all other
+                # samples
+                if min_euclidean_dist >= max_min_distance:
+                    max_min_distance = min_euclidean_dist
+                    best_candidate = j
+
+                # Projected distance
+                projected_dist = distance.cdist(
+                    new_samples, [candidates[j]], 'chebyshev')
+                min_projected_dist[j] = np.min(projected_dist)
+
+            if mc_criterion == 'mc-intersite-proj':
+                weight_euclidean_dist = 0.5 * ((n_new_samples+1)**(1/ndim) - 1)
+                weight_projected_dist = 0.5 * (n_new_samples+1)
+                total_dist_scores = weight_euclidean_dist * min_intersite_dist
+                total_dist_scores += weight_projected_dist * min_projected_dist
+
+            elif mc_criterion == 'mc-intersite-proj-th':
+                alpha = 0.5  # chosen (tradeoff)
+                d_min = 2 * alpha / n_new_samples
+                if any(min_projected_dist < d_min):
+                    candidates = np.delete(
+                        candidates, [min_projected_dist < d_min], axis=0
+                        )
+                    total_dist_scores = np.delete(
+                        min_intersite_dist, [min_projected_dist < d_min],
+                        axis=0
+                        )
+                else:
+                    total_dist_scores = min_intersite_dist
+            else:
+                raise NameError(
+                    'The MC-Criterion you requested is not available.'
+                    )
+
+            # Add the best candidate to the list of new samples
+            best_candidate = np.argsort(total_dist_scores)[::-1][:n_candidate]
+            selected_samples = np.vstack(
+                (selected_samples, candidates[best_candidate])
+                )
+
+        self.new_samples = selected_samples
+        self.exploration_scores = np.delete(error_voronoi, bad_samples, axis=0)
+
+        return self.new_samples, self.exploration_scores
+
+    # -------------------------------------------------------------------------
+    def get_mc_samples(self, all_candidates=None):
+        """
+        This function generates random samples based on Global Monte Carlo
+        methods and their corresponding scores, based on [1].
+
+        [1] Crombecq, K., Laermans, E. and Dhaene, T., 2011. Efficient
+            space-filling and non-collapsing sequential design strategies for
+            simulation-based modeling. European Journal of Operational Research
+            , 214(3), pp.683-696.
+            DOI: https://doi.org/10.1016/j.ejor.2011.05.032
+
+        Implemented methods to compute scores:
+            1) mc-intersite-proj
+            2) mc-intersite-proj-th
+
+        Arguments
+        ---------
+        all_candidates : array, optional
+            Samples to compute the scores for. The default is `None`. In this
+            case, samples will be generated by defined model input marginals.
+
+        Returns
+        -------
+        new_samples : array of shape (n_candidate, n_params)
+            A list of samples.
+        exploration_scores: arrays of shape (n_candidate)
+            Exploration scores.
+        """
+        explore_method = self.ExpDesign.explore_method
+        mc_criterion = self.mc_criterion
+        if all_candidates is None:
+            n_candidate = self.n_candidate
+        else:
+            n_candidate = all_candidates.shape[0]
+
+        # Get the Old ExpDesign #samples
+        old_ED_X = self.ExpDesign.X
+        ndim = old_ED_X.shape[1]
+
+        # ----- Compute the number of random points -----
+        if all_candidates is None:
+            # Generate MC Samples
+            all_candidates = self.ExpDesign.generate_samples(
+                self.n_candidate, explore_method
+                )
+        self.all_candidates = all_candidates
+
+        # initialization
+        min_intersite_dist = np.zeros((n_candidate))
+        min_projected_dist = np.zeros((n_candidate))
+
+        for i, candidate in enumerate(all_candidates):
+
+            # find candidate that is farthest away from any existing sample
+            maxMinDistance = 0
+
+            # find min distorted distance from all other samples
+            euclidean_dist = self._build_dist_matrix_point(
+                old_ED_X, candidate, do_sqrt=True
+                )
+            min_euclidean_dist = np.min(euclidean_dist)
+            min_intersite_dist[i] = min_euclidean_dist
+
+            # Check if this is the maximum minimum distance from all other
+            # samples
+            if min_euclidean_dist >= maxMinDistance:
+                maxMinDistance = min_euclidean_dist
+
+            # Projected distance
+            projected_dist = self._build_dist_matrix_point(
+                old_ED_X, candidate, 'chebyshev'
+                )
+            min_projected_dist[i] = np.min(projected_dist)
+
+        if mc_criterion == 'mc-intersite-proj':
+            weight_euclidean_dist = ((n_candidate+1)**(1/ndim) - 1) * 0.5
+            weight_projected_dist = (n_candidate+1) * 0.5
+            total_dist_scores = weight_euclidean_dist * min_intersite_dist
+            total_dist_scores += weight_projected_dist * min_projected_dist
+
+        elif mc_criterion == 'mc-intersite-proj-th':
+            alpha = 0.5  # chosen (tradeoff)
+            d_min = 2 * alpha / n_candidate
+            if any(min_projected_dist < d_min):
+                all_candidates = np.delete(
+                    all_candidates, [min_projected_dist < d_min], axis=0
+                    )
+                total_dist_scores = np.delete(
+                    min_intersite_dist, [min_projected_dist < d_min], axis=0
+                    )
+            else:
+                total_dist_scores = min_intersite_dist
+        else:
+            raise NameError('The MC-Criterion you requested is not available.')
+
+        self.new_samples = all_candidates
+        self.exploration_scores = total_dist_scores
+        self.exploration_scores /= np.nansum(total_dist_scores)
+
+        return self.new_samples, self.exploration_scores
+
+    # -------------------------------------------------------------------------
+    def approximate_voronoi(self, w, samples):
+        """
+        An approximate (monte carlo) version of Matlab's voronoi command.
+
+        Arguments
+        ---------
+        samples : array
+            Old experimental design to be used as center points for voronoi
+            cells.
+
+        Returns
+        -------
+        areas : array
+            An approximation of the voronoi cells' areas.
+        all_candidates: list of arrays
+            A list of samples in each voronoi cell.
+        """
+        n_samples = samples.shape[0]
+        ndim = samples.shape[1]
+
+        # Compute the number of random points
+        n_points = w * samples.shape[0]
+        # Generate w random points in the domain for each sample
+        points = self.ExpDesign.generate_samples(n_points, 'random')
+        self.all_candidates = points
+
+        # Calculate the nearest sample to each point
+        self.areas = np.zeros((n_samples))
+        self.closest_points = [np.empty((0, ndim)) for i in range(n_samples)]
+
+        # Compute the minimum distance from all the samples of old_ED_X for
+        # each test point
+        for idx in range(n_points):
+            # calculate the minimum distance
+            distances = self._build_dist_matrix_point(
+                samples, points[idx], do_sqrt=True
+                )
+            closest_sample = np.argmin(distances)
+
+            # Add to the voronoi list of the closest sample
+            self.areas[closest_sample] = self.areas[closest_sample] + 1
+            prev_closest_points = self.closest_points[closest_sample]
+            self.closest_points[closest_sample] = np.vstack(
+                (prev_closest_points, points[idx])
+                )
+
+        # Divide by the amount of points to get the estimated volume of each
+        # voronoi cell
+        self.areas /= n_points
+
+        self.perc = np.max(self.areas * 100)
+
+        self.errors = self.areas
+
+        return self.areas, self.all_candidates
+
+    # -------------------------------------------------------------------------
+    def _build_dist_matrix_point(self, samples, point, method='euclidean',
+                                 do_sqrt=False):
+        """
+        Calculates the intersite distance of all points in samples from point.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params)
+            The old experimental design.
+        point : array
+            A candidate point.
+        method : str
+            Distance method.
+        do_sqrt : bool, optional
+            Whether to return distances or squared distances. The default is
+            `False`.
+
+        Returns
+        -------
+        distances : array
+            Distances.
+
+        """
+        distances = distance.cdist(samples, np.array([point]), method)
+
+        # do square root?
+        if do_sqrt:
+            return distances
+        else:
+            return distances**2
+
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/glexindex.py b/examples/analytical-function/bayesvalidrox/surrogate_models/glexindex.py
new file mode 100644
index 0000000000000000000000000000000000000000..90877331ec121750e7f81e32a4b69edbc9a110ba
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/glexindex.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Multi indices for monomial exponents.
+Credit: Jonathan Feinberg
+https://github.com/jonathf/numpoly/blob/master/numpoly/utils/glexindex.py
+"""
+
+import numpy
+import numpy.typing
+
+
+def glexindex(start, stop=None, dimensions=1, cross_truncation=1.,
+              graded=False, reverse=False):
+    """
+    Generate graded lexicographical multi-indices for the monomial exponents.
+    Args:
+        start (Union[int, numpy.ndarray]):
+            The lower order of the indices. If array of int, counts as lower
+            bound for each axis.
+        stop (Union[int, numpy.ndarray, None]):
+            The maximum shape included. If omitted: stop <- start; start <- 0
+            If int is provided, set as largest total order. If array of int,
+            set as upper bound for each axis.
+        dimensions (int):
+            The number of dimensions in the expansion.
+        cross_truncation (float, Tuple[float, float]):
+            Use hyperbolic cross truncation scheme to reduce the number of
+            terms in expansion. If two values are provided, first is low bound
+            truncation, while the latter upper bound. If only one value, upper
+            bound is assumed.
+        graded (bool):
+            Graded sorting, meaning the indices are always sorted by the index
+            sum. E.g. ``(2, 2, 2)`` has a sum of 6, and will therefore be
+            consider larger than both ``(3, 1, 1)`` and ``(1, 1, 3)``.
+        reverse (bool):
+            Reversed lexicographical sorting meaning that ``(1, 3)`` is
+            considered smaller than ``(3, 1)``, instead of the opposite.
+    Returns:
+        list:
+            Order list of indices.
+    Examples:
+        >>> numpoly.glexindex(4).tolist()
+        [[0], [1], [2], [3]]
+        >>> numpoly.glexindex(2, dimensions=2).tolist()
+        [[0, 0], [1, 0], [0, 1]]
+        >>> numpoly.glexindex(start=2, stop=3, dimensions=2).tolist()
+        [[2, 0], [1, 1], [0, 2]]
+        >>> numpoly.glexindex([1, 2, 3]).tolist()
+        [[0, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 2]]
+        >>> numpoly.glexindex([1, 2, 3], cross_truncation=numpy.inf).tolist()
+        [[0, 0, 0], [0, 1, 0], [0, 0, 1], [0, 1, 1], [0, 0, 2], [0, 1, 2]]
+    """
+    if stop is None:
+        start, stop = 0, start
+    start = numpy.array(start, dtype=int).flatten()
+    stop = numpy.array(stop, dtype=int).flatten()
+    start, stop, _ = numpy.broadcast_arrays(start, stop, numpy.empty(dimensions))
+
+    cross_truncation = cross_truncation*numpy.ones(2)
+    
+    # Moved here from _glexindex
+    bound = stop.max()
+    dimensions = len(start)
+    start = numpy.clip(start, a_min=0, a_max=None)
+    dtype = numpy.uint8 if bound < 256 else numpy.uint16
+    range_ = numpy.arange(bound, dtype=dtype)
+    indices = range_[:, numpy.newaxis]
+
+    for idx in range(dimensions-1):
+
+        # Truncate at each step to keep memory usage low
+        if idx:
+            indices = indices[cross_truncate(indices, bound-1, cross_truncation[1])]
+
+        # Repeats the current set of indices.
+        # e.g. [0,1,2] -> [0,1,2,0,1,2,...,0,1,2]
+        indices = numpy.tile(indices, (bound, 1))
+
+        # Stretches ranges over the new dimension.
+        # e.g. [0,1,2] -> [0,0,...,0,1,1,...,1,2,2,...,2]
+        front = range_.repeat(len(indices)//bound)[:, numpy.newaxis]
+
+        # Puts them two together.
+        indices = numpy.column_stack((front, indices))
+
+    # Complete the truncation scheme
+    if dimensions == 1:
+        indices = indices[(indices >= start) & (indices < bound)]
+    else:
+        lower = cross_truncate(indices, start-1, cross_truncation[0])
+        upper = cross_truncate(indices, stop-1, cross_truncation[1])
+        indices = indices[lower ^ upper]
+
+    indices = numpy.array(indices, dtype=int).reshape(-1, dimensions)
+    if indices.size:
+        # moved here from glexsort
+        keys = indices.T
+        keys_ = numpy.atleast_2d(keys)
+        if reverse:
+            keys_ = keys_[::-1]
+    
+        indices_sort = numpy.array(numpy.lexsort(keys_))
+        if graded:
+            indices_sort = indices_sort[numpy.argsort(
+                numpy.sum(keys_[:, indices_sort], axis=0))].T
+        
+        indices = indices[indices_sort]
+    return indices
+
+def cross_truncate(indices, bound, norm):
+    r"""
+    Truncate of indices using L_p norm.
+    .. math:
+        L_p(x) = \sum_i |x_i/b_i|^p ^{1/p} \leq 1
+    where :math:`b_i` are bounds that each :math:`x_i` should follow.
+    Args:
+        indices (Sequence[int]):
+            Indices to be truncated.
+        bound (int, Sequence[int]):
+            The bound function for witch the indices can not be larger than.
+        norm (float, Sequence[float]):
+            The `p` in the `L_p`-norm. Support includes both `L_0` and `L_inf`.
+    Returns:
+        Boolean indices to ``indices`` with True for each index where the
+        truncation criteria holds.
+    Examples:
+        >>> indices = numpy.array(numpy.mgrid[:10, :10]).reshape(2, -1).T
+        >>> indices[cross_truncate(indices, 2, norm=0)].T
+        array([[0, 0, 0, 1, 2],
+               [0, 1, 2, 0, 0]])
+        >>> indices[cross_truncate(indices, 2, norm=1)].T
+        array([[0, 0, 0, 1, 1, 2],
+               [0, 1, 2, 0, 1, 0]])
+        >>> indices[cross_truncate(indices, [0, 1], norm=1)].T
+        array([[0, 0],
+               [0, 1]])
+    """
+    assert norm >= 0, "negative L_p norm not allowed"
+    bound = numpy.asfarray(bound).flatten()*numpy.ones(indices.shape[1])
+
+    if numpy.any(bound < 0):
+        return numpy.zeros((len(indices),), dtype=bool)
+
+    if numpy.any(bound == 0):
+        out = numpy.all(indices[:, bound == 0] == 0, axis=-1)
+        if numpy.any(bound):
+            out &= cross_truncate(indices[:, bound != 0], bound[bound != 0], norm=norm)
+        return out
+
+    if norm == 0:
+        out = numpy.sum(indices > 0, axis=-1) <= 1
+        out[numpy.any(indices > bound, axis=-1)] = False
+    elif norm == numpy.inf:
+        out = numpy.max(indices/bound, axis=-1) <= 1
+    else:
+        out = numpy.sum((indices/bound)**norm, axis=-1)**(1./norm) <= 1
+
+    assert numpy.all(out[numpy.all(indices == 0, axis=-1)])
+
+    return out
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/input_space.py b/examples/analytical-function/bayesvalidrox/surrogate_models/input_space.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e010d66f2933ec243bad756d8f2c5454808d802
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/input_space.py
@@ -0,0 +1,398 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Input space built from set prior distributions
+"""
+
+import numpy as np
+import chaospy
+import scipy.stats as st
+
+
+class InputSpace:
+    """
+    This class generates the input space for the metamodel from the
+    distributions provided using the `Input` object.
+
+    Attributes
+    ----------
+    Input : obj
+        Input object containing the parameter marginals, i.e. name,
+        distribution type and distribution parameters or available raw data.
+    meta_Model_type : str
+        Type of the meta_Model_type.
+
+    """
+
+    def __init__(self, Input, meta_Model_type='pce'):
+        self.InputObj = Input
+        self.meta_Model_type = meta_Model_type
+        
+        # Other 
+        self.apce = None
+        self.ndim = None
+        
+        # Init 
+        self.check_valid_inputs()
+        
+        
+    def check_valid_inputs(self)-> None:
+        """
+        Check if the given InputObj is valid to use for further calculations:
+            Has some Marginals
+            Marginals have valid priors
+            All Marginals given as the same type (samples vs dist)
+
+        Returns
+        -------
+        None
+
+        """
+        Inputs = self.InputObj
+        self.ndim = len(Inputs.Marginals)
+        
+        # Check if PCE or aPCE metamodel is selected.
+        # TODO: test also for 'pce'??
+        if self.meta_Model_type.lower() == 'apce':
+            self.apce = True
+        else:
+            self.apce = False
+
+        # check if marginals given 
+        if not self.ndim >=1:
+            raise AssertionError('Cannot build distributions if no marginals are given')
+            
+        # check that each marginal is valid
+        for marginals in Inputs.Marginals:
+            if len(marginals.input_data) == 0:
+                if marginals.dist_type == None:
+                    raise AssertionError('Not all marginals were provided priors')
+                    break
+            if np.array(marginals.input_data).shape[0] and (marginals.dist_type != None):
+                raise AssertionError('Both samples and distribution type are given. Please choose only one.')
+                break
+                
+        # Check if input is given as dist or input_data.
+        self.input_data_given = -1
+        for marg in Inputs.Marginals:
+            #print(self.input_data_given)
+            size = np.array(marg.input_data).shape[0]
+            #print(f'Size: {size}')
+            if size and abs(self.input_data_given) !=1:
+                self.input_data_given = 2
+                break
+            if (not size) and self.input_data_given > 0:
+                self.input_data_given = 2
+                break
+            if not size:
+                self.input_data_given = 0
+            if size:
+                self.input_data_given = 1
+                
+        if self.input_data_given == 2:
+            raise AssertionError('Distributions cannot be built as the priors have different types')
+            
+    
+        # Get the bounds if input_data are directly defined by user:
+        if self.input_data_given:
+            for i in range(self.ndim):
+                low_bound = np.min(Inputs.Marginals[i].input_data)
+                up_bound = np.max(Inputs.Marginals[i].input_data)
+                Inputs.Marginals[i].parameters = [low_bound, up_bound]
+
+  
+
+    # -------------------------------------------------------------------------
+    def init_param_space(self, max_deg=None):
+        """
+        Initializes parameter space.
+
+        Parameters
+        ----------
+        max_deg : int, optional
+            Maximum degree. The default is `None`.
+
+        Creates
+        -------
+        raw_data : array of shape (n_params, n_samples)
+            Raw data.
+        bound_tuples : list of tuples
+            A list containing lower and upper bounds of parameters.
+
+        """
+        # Recheck all before running!
+        self.check_valid_inputs()
+        
+        Inputs = self.InputObj
+        ndim = self.ndim
+        rosenblatt_flag = Inputs.Rosenblatt
+        mc_size = 50000
+
+        # Save parameter names
+        self.par_names = []
+        for parIdx in range(ndim):
+            self.par_names.append(Inputs.Marginals[parIdx].name)
+
+        # Create a multivariate probability distribution
+        # TODO: change this to make max_deg obligatory? at least in some specific cases?
+        if max_deg is not None:
+            JDist, poly_types = self.build_polytypes(rosenblatt=rosenblatt_flag)
+            self.JDist, self.poly_types = JDist, poly_types
+
+        if self.input_data_given:
+            self.MCSize = len(Inputs.Marginals[0].input_data)
+            self.raw_data = np.zeros((ndim, self.MCSize))
+
+            for parIdx in range(ndim):
+                # Save parameter names
+                try:
+                    self.raw_data[parIdx] = np.array(
+                        Inputs.Marginals[parIdx].input_data)
+                except:
+                    self.raw_data[parIdx] = self.JDist[parIdx].sample(mc_size)
+
+        else:
+            # Generate random samples based on parameter distributions
+            self.raw_data = chaospy.generate_samples(mc_size,
+                                                     domain=self.JDist)
+
+        # Extract moments
+        for parIdx in range(ndim):
+            mu = np.mean(self.raw_data[parIdx])
+            std = np.std(self.raw_data[parIdx])
+            self.InputObj.Marginals[parIdx].moments = [mu, std]
+
+        # Generate the bounds based on given inputs for marginals
+        bound_tuples = []
+        for i in range(ndim):
+            if Inputs.Marginals[i].dist_type == 'unif':
+                low_bound = Inputs.Marginals[i].parameters[0]
+                up_bound = Inputs.Marginals[i].parameters[1]
+            else:
+                low_bound = np.min(self.raw_data[i])
+                up_bound = np.max(self.raw_data[i])
+
+            bound_tuples.append((low_bound, up_bound))
+
+        self.bound_tuples = tuple(bound_tuples)
+
+    # -------------------------------------------------------------------------
+    def build_polytypes(self, rosenblatt):
+        """
+        Creates the polynomial types to be passed to univ_basis_vals method of
+        the MetaModel object.
+
+        Parameters
+        ----------
+        rosenblatt : bool
+            Rosenblatt transformation flag.
+
+        Returns
+        -------
+        orig_space_dist : object
+            A chaospy JDist object or a gaussian_kde object.
+        poly_types : list
+            List of polynomial types for the parameters.
+
+        """
+        Inputs = self.InputObj
+        
+        all_data = []
+        all_dist_types = []
+        orig_joints = []
+        poly_types = []
+        
+        for parIdx in range(self.ndim):
+
+            if Inputs.Marginals[parIdx].dist_type is None:
+                data = Inputs.Marginals[parIdx].input_data
+                all_data.append(data)
+                dist_type = None
+            else:
+                dist_type = Inputs.Marginals[parIdx].dist_type
+                params = Inputs.Marginals[parIdx].parameters
+
+            if rosenblatt:
+                polytype = 'hermite'
+                dist = chaospy.Normal()
+
+            elif dist_type is None:
+                polytype = 'arbitrary'
+                dist = None
+
+            elif 'unif' in dist_type.lower():
+                polytype = 'legendre'
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Uniform(lower=params[0], upper=params[1])
+
+            elif 'norm' in dist_type.lower() and \
+                 'log' not in dist_type.lower():
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                polytype = 'hermite'
+                dist = chaospy.Normal(mu=params[0], sigma=params[1])
+
+            elif 'gamma' in dist_type.lower():
+                polytype = 'laguerre'
+                if not np.array(params).shape[0]>=3:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Gamma(shape=params[0],
+                                     scale=params[1],
+                                     shift=params[2])
+
+            elif 'beta' in dist_type.lower():
+                if not np.array(params).shape[0]>=4:
+                    raise AssertionError('Distribution has too few parameters!')
+                polytype = 'jacobi'
+                dist = chaospy.Beta(alpha=params[0], beta=params[1],
+                                    lower=params[2], upper=params[3])
+
+            elif 'lognorm' in dist_type.lower():
+                polytype = 'hermite'
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                mu = np.log(params[0]**2/np.sqrt(params[0]**2 + params[1]**2))
+                sigma = np.sqrt(np.log(1 + params[1]**2 / params[0]**2))
+                dist = chaospy.LogNormal(mu, sigma)
+                # dist = chaospy.LogNormal(mu=params[0], sigma=params[1])
+
+            elif 'expon' in dist_type.lower():
+                polytype = 'exponential'
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Exponential(scale=params[0], shift=params[1])
+
+            elif 'weibull' in dist_type.lower():
+                polytype = 'weibull'
+                if not np.array(params).shape[0]>=3:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Weibull(shape=params[0], scale=params[1],
+                                       shift=params[2])
+
+            else:
+                message = (f"DistType {dist_type} for parameter"
+                           f"{parIdx+1} is not available.")
+                raise ValueError(message)
+
+            if self.input_data_given or self.apce:
+                polytype = 'arbitrary'
+
+            # Store dists and poly_types
+            orig_joints.append(dist)
+            poly_types.append(polytype)
+            all_dist_types.append(dist_type)
+
+        # Prepare final output to return
+        if None in all_dist_types:
+            # Naive approach: Fit a gaussian kernel to the provided data
+            Data = np.asarray(all_data)
+            try:
+                orig_space_dist = st.gaussian_kde(Data)
+            except:
+                raise ValueError('The samples provided to the Marginals should be 1D only')
+            self.prior_space = orig_space_dist
+        else:
+            orig_space_dist = chaospy.J(*orig_joints)
+            try:
+                self.prior_space = st.gaussian_kde(orig_space_dist.sample(10000))
+            except:
+                raise ValueError('Parameter values are not valid, please set differently')
+
+        return orig_space_dist, poly_types
+
+    # -------------------------------------------------------------------------
+    def transform(self, X, params=None, method=None):
+        """
+        Transforms the samples via either a Rosenblatt or an isoprobabilistic
+        transformation.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples,n_params)
+            Samples to be transformed.
+        method : string
+            If transformation method is 'user' transform X, else just pass X.
+
+        Returns
+        -------
+        tr_X: array of shape (n_samples,n_params)
+            Transformed samples.
+
+        """
+        # Check for built JDist
+        if not hasattr(self, 'JDist'):
+            raise AttributeError('Call function init_param_space first to create JDist')
+            
+        # Check if X is 2d
+        if X.ndim != 2:
+            raise AttributeError('X should have two dimensions')
+            
+        # Check if size of X matches Marginals
+        if X.shape[1]!= self.ndim:
+            raise AttributeError('The second dimension of X should be the same size as the number of marginals in the InputObj')
+        
+        if self.InputObj.Rosenblatt:
+            self.origJDist, _ = self.build_polytypes(False)
+            if method == 'user':
+                tr_X = self.JDist.inv(self.origJDist.fwd(X.T)).T
+            else:
+                # Inverse to original spcace -- generate sample ED
+                tr_X = self.origJDist.inv(self.JDist.fwd(X.T)).T
+        else:
+            # Transform samples via an isoprobabilistic transformation
+            n_samples, n_params = X.shape
+            Inputs = self.InputObj
+            origJDist = self.JDist
+            poly_types = self.poly_types
+
+            disttypes = []
+            for par_i in range(n_params):
+                disttypes.append(Inputs.Marginals[par_i].dist_type)
+
+            # Pass non-transformed X, if arbitrary PCE is selected.
+            if None in disttypes or self.input_data_given or self.apce:
+                return X
+
+            cdfx = np.zeros((X.shape))
+            tr_X = np.zeros((X.shape))
+
+            for par_i in range(n_params):
+
+                # Extract the parameters of the original space
+                disttype = disttypes[par_i]
+                if disttype is not None:
+                    dist = origJDist[par_i]
+                else:
+                    dist = None
+                polytype = poly_types[par_i]
+                cdf = np.vectorize(lambda x: dist.cdf(x))
+
+                # Extract the parameters of the transformation space based on
+                # polyType
+                if polytype == 'legendre' or disttype == 'uniform':
+                    # Generate Y_Dists based
+                    params_Y = [-1, 1]
+                    dist_Y = st.uniform(loc=params_Y[0],
+                                        scale=params_Y[1]-params_Y[0])
+                    inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x))
+
+                elif polytype == 'hermite' or disttype == 'norm':
+                    params_Y = [0, 1]
+                    dist_Y = st.norm(loc=params_Y[0], scale=params_Y[1])
+                    inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x))
+
+                elif polytype == 'laguerre' or disttype == 'gamma':
+                    if params == None:
+                        raise AttributeError('Additional parameters have to be set for the gamma distribution!')
+                    params_Y = [1, params[1]]
+                    dist_Y = st.gamma(loc=params_Y[0], scale=params_Y[1])
+                    inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x))
+
+                # Compute CDF_x(X)
+                cdfx[:, par_i] = cdf(X[:, par_i])
+
+                # Compute invCDF_y(cdfx)
+                tr_X[:, par_i] = inv_cdf(cdfx[:, par_i])
+
+        return tr_X
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/inputs.py b/examples/analytical-function/bayesvalidrox/surrogate_models/inputs.py
new file mode 100644
index 0000000000000000000000000000000000000000..094e1066fe008e37288e44750524c5a1370bd7a2
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/inputs.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Inputs and related marginal distributions
+"""
+
+class Input:
+    """
+    A class to define the uncertain input parameters.
+
+    Attributes
+    ----------
+    Marginals : obj
+        Marginal objects. See `inputs.Marginal`.
+    Rosenblatt : bool
+        If Rossenblatt transformation is required for the dependent input
+        parameters.
+
+    Examples
+    -------
+    Marginals can be defined as following:
+
+    >>> Inputs.add_marginals()
+    >>> Inputs.Marginals[0].name = 'X_1'
+    >>> Inputs.Marginals[0].dist_type = 'uniform'
+    >>> Inputs.Marginals[0].parameters = [-5, 5]
+
+    If there is no common data is avaliable, the input data can be given
+    as following:
+
+    >>> Inputs.add_marginals()
+    >>> Inputs.Marginals[0].name = 'X_1'
+    >>> Inputs.Marginals[0].input_data = input_data
+    """
+    poly_coeffs_flag = True
+
+    def __init__(self):
+        self.Marginals = []
+        self.Rosenblatt = False
+
+    def add_marginals(self):
+        """
+        Adds a new Marginal object to the input object.
+
+        Returns
+        -------
+        None.
+
+        """
+        self.Marginals.append(Marginal())
+
+
+# Nested class
+class Marginal:
+    """
+    An object containing the specifications of the marginals for each uncertain
+    parameter.
+
+    Attributes
+    ----------
+    name : string
+        Name of the parameter. The default is `'$x_1$'`.
+    dist_type : string
+        Name of the distribution. The default is `None`.
+    parameters : list
+        List of the parameters corresponding to the distribution type. The
+        default is `None`.
+    input_data : array
+        Available input data. The default is `[]`.
+    moments : list
+        List of the moments.
+    """
+
+    def __init__(self):
+        self.name = '$x_1$'
+        self.dist_type = None
+        self.parameters = None
+        self.input_data = []
+        self.moments = None
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/meta_model_engine.py b/examples/analytical-function/bayesvalidrox/surrogate_models/meta_model_engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..71c0244216b0c87a22174a3ad2043a4c0a80efab
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/meta_model_engine.py
@@ -0,0 +1,2195 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jan 28 09:21:18 2022
+
+@author: farid
+"""
+import numpy as np
+from scipy import stats, signal, linalg, sparse
+from scipy.spatial import distance
+from copy import deepcopy, copy
+from tqdm import tqdm
+import scipy.optimize as opt
+from sklearn.metrics import mean_squared_error
+import multiprocessing
+import matplotlib.pyplot as plt
+import sys
+import os
+import gc
+import seaborn as sns
+from joblib import Parallel, delayed
+
+import bayesvalidrox
+from .exploration import Exploration
+from bayesvalidrox.bayes_inference.bayes_inference import BayesInference
+from bayesvalidrox.bayes_inference.discrepancy import Discrepancy
+import pandas as pd
+
+
+class MetaModelEngine():
+    """ Sequential experimental design
+    This class provieds method for trainig the meta-model in an iterative
+    manners.
+    The main method to execute the task is `train_seq_design`, which
+      recieves a model object and returns the trained metamodel.
+    """
+
+    def __init__(self, meta_model_opts):
+        self.MetaModel = meta_model_opts
+
+    # -------------------------------------------------------------------------
+    def run(self):
+
+        Model = self.MetaModel.ModelObj
+        self.MetaModel.n_params = len(self.MetaModel.input_obj.Marginals)
+        self.MetaModel.ExpDesignFlag = 'normal'
+        # --- Prepare pce degree ---
+        if self.MetaModel.meta_model_type.lower() == 'pce':
+            if type(self.MetaModel.pce_deg) is not np.ndarray:
+                self.MetaModel.pce_deg = np.array(self.MetaModel.pce_deg)
+
+        if self.MetaModel.ExpDesign.method == 'normal':
+            self.MetaModel.ExpDesignFlag = 'normal'
+            self.MetaModel.train_norm_design(parallel = False)
+
+        elif self.MetaModel.ExpDesign.method == 'sequential':
+            self.train_seq_design()
+        else:
+            raise Exception("The method for experimental design you requested"
+                            " has not been implemented yet.")
+
+        # Zip the model run directories
+        if self.MetaModel.ModelObj.link_type.lower() == 'pylink' and\
+           self.MetaModel.ExpDesign.sampling_method.lower() != 'user':
+            Model.zip_subdirs(Model.name, f'{Model.name}_')
+
+    # -------------------------------------------------------------------------
+    def train_seq_design(self):
+        """
+        Starts the adaptive sequential design for refining the surrogate model
+        by selecting training points in a sequential manner.
+
+        Returns
+        -------
+        MetaModel : object
+            Meta model object.
+
+        """
+        # Set model to have shorter call 
+        Model = self.MetaModel.ModelObj
+        # MetaModel = self.MetaModel
+        self.Model = Model
+
+        # Initialization
+        self.MetaModel.SeqModifiedLOO = {}
+        self.MetaModel.seqValidError = {}
+        self.MetaModel.SeqBME = {}
+        self.MetaModel.SeqKLD = {}
+        self.MetaModel.SeqDistHellinger = {}
+        self.MetaModel.seqRMSEMean = {}
+        self.MetaModel.seqRMSEStd = {}
+        self.MetaModel.seqMinDist = []
+
+        # Determine the metamodel type
+        if self.MetaModel.meta_model_type.lower() != 'gpe':
+            pce = True
+        else:
+            pce = False
+        # If given, use mc reference data
+        mc_ref = True if bool(Model.mc_reference) else False
+        if mc_ref:
+            Model.read_mc_reference()
+
+        # if valid_samples not defined, do so now
+        if not hasattr(self.MetaModel, 'valid_samples'):
+            self.MetaModel.valid_samples = []
+            self.MetaModel.valid_model_runs = []
+            self.MetaModel.valid_likelihoods = []
+
+        # Get the parameters
+        max_n_samples = self.MetaModel.ExpDesign.n_max_samples
+        mod_LOO_threshold = self.MetaModel.ExpDesign.mod_LOO_threshold
+        n_canddidate = self.MetaModel.ExpDesign.n_canddidate
+        post_snapshot = self.MetaModel.ExpDesign.post_snapshot
+        n_replication = self.MetaModel.ExpDesign.n_replication
+        util_func = self.MetaModel.ExpDesign.util_func
+        output_name = Model.Output.names
+        validError = None
+        # Handle if only one UtilityFunctions is provided
+        if not isinstance(util_func, list):
+            util_func = [self.MetaModel.ExpDesign.util_func]
+
+        # Read observations or MCReference
+        if len(Model.observations) != 0 or Model.meas_file is not None:
+            self.observations = Model.read_observation()
+            obs_data = self.observations
+        else:
+            obs_data = []
+            TotalSigma2 = {}
+            
+        # TODO: ---------- Initial self.MetaModel ----------
+        # First run MetaModel on non-sequential design
+        self.MetaModel.train_norm_design(parallel = False)
+        initMetaModel = deepcopy(self.MetaModel)
+
+        # Validation error if validation set is provided. - use as initial errors
+        if self.MetaModel.valid_model_runs:
+            init_rmse, init_valid_error = self.__validError(initMetaModel)
+            init_valid_error = list(init_valid_error.values())
+        else:
+            init_rmse = None
+
+        # Check if discrepancy is provided
+        if len(obs_data) != 0 and hasattr(self.MetaModel, 'Discrepancy'):
+            TotalSigma2 = self.MetaModel.Discrepancy.parameters
+
+            # Calculate the initial BME
+            out = self.__BME_Calculator(
+                initMetaModel, obs_data, TotalSigma2, init_rmse)
+            init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out
+            print(f"\nInitial BME: {init_BME:.2f}")
+            print(f"Initial KLD: {init_KLD:.2f}")
+
+            # Posterior snapshot (initial)
+            if post_snapshot:
+                parNames = self.MetaModel.ExpDesign.par_names
+                print('Posterior snapshot (initial) is being plotted...')
+                self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init')
+
+        # Check the convergence of the Mean & Std
+        if mc_ref and pce:
+            init_rmse_mean, init_rmse_std = self.__error_Mean_Std()
+            print(f"Initial Mean and Std error: {init_rmse_mean:.2f},"
+                  f" {init_rmse_std:.2f}")
+
+        # Read the initial experimental design
+        # TODO: this sequential, or the non-sequential samples??
+        Xinit = initMetaModel.ExpDesign.X
+        init_n_samples = len(initMetaModel.ExpDesign.X)
+        initYprev = initMetaModel.ModelOutputDict
+        initLCerror = initMetaModel.LCerror
+        n_itrs = max_n_samples - init_n_samples
+
+        # Read the initial ModifiedLOO
+        if pce:
+            Scores_all, varExpDesignY = [], []
+            for out_name in output_name:
+                y = self.MetaModel.ExpDesign.Y[out_name]
+                Scores_all.append(list(
+                    self.MetaModel.score_dict['b_1'][out_name].values()))
+                if self.MetaModel.dim_red_method.lower() == 'pca':
+                    pca = self.MetaModel.pca['b_1'][out_name]
+                    components = pca.transform(y)
+                    varExpDesignY.append(np.var(components, axis=0))
+                else:
+                    varExpDesignY.append(np.var(y, axis=0))
+
+            Scores = [item for sublist in Scores_all for item in sublist]
+            weights = [item for sublist in varExpDesignY for item in sublist]
+            init_mod_LOO = [np.average([1-score for score in Scores],
+                                       weights=weights)]
+
+        prevMetaModel_dict = {}
+        # Replicate the sequential design
+        for repIdx in range(n_replication):                     # TODO: what does this do?
+            print(f'\n>>>> Replication: {repIdx+1}<<<<')
+
+            # To avoid changes ub original aPCE object
+            self.MetaModel.ExpDesign.X = Xinit
+            self.MetaModel.ExpDesign.Y = initYprev
+            self.MetaModel.LCerror = initLCerror
+
+            for util_f in util_func:                            # TODO: recheck choices for this
+                print(f'\n>>>> Utility Function: {util_f} <<<<')
+                # To avoid changes ub original aPCE object
+                self.MetaModel.ExpDesign.X = Xinit
+                self.MetaModel.ExpDesign.Y = initYprev
+                self.MetaModel.LCerror = initLCerror
+
+                # Set the experimental design
+                Xprev = Xinit
+                total_n_samples = init_n_samples
+                Yprev = initYprev
+
+                Xfull = []
+                Yfull = []
+
+                # Store the initial ModifiedLOO
+                if pce:
+                    print("\nInitial ModifiedLOO:", init_mod_LOO)
+                    SeqModifiedLOO = np.array(init_mod_LOO)
+
+                if len(self.MetaModel.valid_model_runs) != 0:
+                    SeqValidError = np.array(init_valid_error)
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    SeqBME = np.array([init_BME])
+                    SeqKLD = np.array([init_KLD])
+                    SeqDistHellinger = np.array([init_dist_hellinger])
+
+                if mc_ref and pce:
+                    seqRMSEMean = np.array([init_rmse_mean])
+                    seqRMSEStd = np.array([init_rmse_std])
+
+                # ------- Start Sequential Experimental Design -------
+                postcnt = 1
+                for itr_no in range(1, n_itrs+1):
+                    print(f'\n>>>> Iteration number {itr_no} <<<<')
+
+                    # Save the metamodel prediction before updating
+                    prevMetaModel_dict[itr_no] = deepcopy(self.MetaModel)           # Write last MetaModel here
+                    if itr_no > 1:
+                        pc_model = prevMetaModel_dict[itr_no-1]                     
+                        self._y_hat_prev, _ = pc_model.eval_metamodel(              # What's the use of this here??
+                            samples=Xfull[-1].reshape(1, -1))
+                        del prevMetaModel_dict[itr_no-1]                            # Delete second to last metamodel here?
+
+                    # Optimal Bayesian Design
+                    self.MetaModel.ExpDesignFlag = 'sequential'
+                    Xnew, updatedPrior = self.opt_SeqDesign(TotalSigma2,            # TODO: check in this!!
+                                                            n_canddidate,
+                                                            util_f)
+                    S = np.min(distance.cdist(Xinit, Xnew, 'euclidean'))
+                    self.MetaModel.seqMinDist.append(S)
+                    print(f"\nmin Dist from OldExpDesign: {S:2f}")
+                    print("\n")
+
+                    # Evaluate the full model response at the new sample
+                    Ynew, _ = Model.run_model_parallel(
+                        Xnew, prevRun_No=total_n_samples
+                        )
+                    total_n_samples += Xnew.shape[0]
+
+                    # ------ Plot the surrogate model vs Origninal Model ------
+                    if hasattr(self.MetaModel, 'adapt_verbose') and \
+                       self.MetaModel.adapt_verbose:
+                        from .adaptPlot import adaptPlot
+                        y_hat, std_hat = self.MetaModel.eval_metamodel(
+                            samples=Xnew
+                            )
+                        adaptPlot(
+                            self.MetaModel, Ynew, y_hat, std_hat,
+                            plotED=False
+                            )
+
+                    # -------- Retrain the surrogate model -------
+                    # Extend new experimental design
+                    Xfull = np.vstack((Xprev, Xnew))
+
+                    # Updating experimental design Y
+                    for out_name in output_name:
+                        Yfull = np.vstack((Yprev[out_name], Ynew[out_name]))
+                        self.MetaModel.ModelOutputDict[out_name] = Yfull
+
+                    # Pass new design to the metamodel object
+                    self.MetaModel.ExpDesign.sampling_method = 'user'
+                    self.MetaModel.ExpDesign.X = Xfull
+                    self.MetaModel.ExpDesign.Y = self.MetaModel.ModelOutputDict
+
+                    # Save the Experimental Design for next iteration
+                    Xprev = Xfull
+                    Yprev = self.MetaModel.ModelOutputDict
+
+                    # Pass the new prior as the input
+                    self.MetaModel.input_obj.poly_coeffs_flag = False
+                    if updatedPrior is not None:
+                        self.MetaModel.input_obj.poly_coeffs_flag = True
+                        print("updatedPrior:", updatedPrior.shape)
+                        # Arbitrary polynomial chaos
+                        for i in range(updatedPrior.shape[1]):
+                            self.MetaModel.input_obj.Marginals[i].dist_type = None
+                            x = updatedPrior[:, i]
+                            self.MetaModel.input_obj.Marginals[i].raw_data = x
+
+                    # Train the surrogate model for new ExpDesign
+                    self.MetaModel.train_norm_design(parallel=False)
+
+                    # -------- Evaluate the retrained surrogate model -------
+                    # Extract Modified LOO from Output
+                    if pce:
+                        Scores_all, varExpDesignY = [], []
+                        for out_name in output_name:
+                            y = self.MetaModel.ExpDesign.Y[out_name]
+                            Scores_all.append(list(
+                                self.MetaModel.score_dict['b_1'][out_name].values()))
+                            if self.MetaModel.dim_red_method.lower() == 'pca':
+                                pca = self.MetaModel.pca['b_1'][out_name]
+                                components = pca.transform(y)
+                                varExpDesignY.append(np.var(components,
+                                                            axis=0))
+                            else:
+                                varExpDesignY.append(np.var(y, axis=0))
+                        Scores = [item for sublist in Scores_all for item
+                                  in sublist]
+                        weights = [item for sublist in varExpDesignY for item
+                                   in sublist]
+                        ModifiedLOO = [np.average(
+                            [1-score for score in Scores], weights=weights)]
+
+                        print('\n')
+                        print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO)
+                        print('\n')
+
+                    # Compute the validation error
+                    if self.MetaModel.valid_model_runs:
+                        rmse, validError = self.__validError(self.MetaModel)
+                        ValidError = list(validError.values())
+                    else:
+                        rmse = None
+
+                    # Store updated ModifiedLOO
+                    if pce:
+                        SeqModifiedLOO = np.vstack(
+                            (SeqModifiedLOO, ModifiedLOO))
+                        if len(self.MetaModel.valid_model_runs) != 0:
+                            SeqValidError = np.vstack(
+                                (SeqValidError, ValidError))
+                    # -------- Caclulation of BME as accuracy metric -------
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        # Calculate the initial BME
+                        out = self.__BME_Calculator(self.MetaModel, obs_data,
+                                                    TotalSigma2, rmse)
+                        BME, KLD, Posterior, likes, DistHellinger = out
+                        print('\n')
+                        print(f"Updated BME: {BME:.2f}")
+                        print(f"Updated KLD: {KLD:.2f}")
+                        print('\n')
+
+                        # Plot some snapshots of the posterior
+                        step_snapshot = self.MetaModel.ExpDesign.step_snapshot
+                        if post_snapshot and postcnt % step_snapshot == 0:
+                            parNames = self.MetaModel.ExpDesign.par_names
+                            print('Posterior snapshot is being plotted...')
+                            self.__posteriorPlot(Posterior, parNames,
+                                                 f'SeqPosterior_{postcnt}')
+                        postcnt += 1
+
+                    # Check the convergence of the Mean&Std
+                    if mc_ref and pce:
+                        print('\n')
+                        RMSE_Mean, RMSE_std = self.__error_Mean_Std()
+                        print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, "
+                              f"{RMSE_std:.2f}")
+                        print('\n')
+
+                    # Store the updated BME & KLD
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        SeqBME = np.vstack((SeqBME, BME))
+                        SeqKLD = np.vstack((SeqKLD, KLD))
+                        SeqDistHellinger = np.vstack((SeqDistHellinger,
+                                                      DistHellinger))
+                    if mc_ref and pce:
+                        seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean))
+                        seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std))
+
+                    if pce and any(LOO < mod_LOO_threshold
+                                   for LOO in ModifiedLOO):
+                        break
+
+                    # Clean up
+                    if len(obs_data) != 0:
+                        del out
+                    print()
+                    print('-'*50)
+                    print()
+
+                # Store updated ModifiedLOO and BME in dictonary
+                strKey = f'{util_f}_rep_{repIdx+1}'
+                if pce:
+                    self.MetaModel.SeqModifiedLOO[strKey] = SeqModifiedLOO
+                if len(self.MetaModel.valid_model_runs) != 0:
+                    self.MetaModel.seqValidError[strKey] = SeqValidError
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    self.MetaModel.SeqBME[strKey] = SeqBME
+                    self.MetaModel.SeqKLD[strKey] = SeqKLD
+                if hasattr(self.MetaModel, 'valid_likelihoods') and \
+                   self.MetaModel.valid_likelihoods:
+                    self.MetaModel.SeqDistHellinger[strKey] = SeqDistHellinger
+                if mc_ref and pce:
+                    self.MetaModel.seqRMSEMean[strKey] = seqRMSEMean
+                    self.MetaModel.seqRMSEStd[strKey] = seqRMSEStd
+
+        # return self.MetaModel
+
+    # -------------------------------------------------------------------------
+    def util_VarBasedDesign(self, X_can, index, util_func='Entropy'):
+        """
+        Computes the exploitation scores based on:
+        active learning MacKay(ALM) and active learning Cohn (ALC)
+        Paper: Sequential Design with Mutual Information for Computer
+        Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas
+        (2016)
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        index : int
+            Model output index.
+        UtilMethod : string, optional
+            Exploitation utility function. The default is 'Entropy'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+        MetaModel = self.MetaModel
+        ED_X = MetaModel.ExpDesign.X
+        out_dict_y = MetaModel.ExpDesign.Y
+        out_names = MetaModel.ModelObj.Output.names
+
+        # Run the Metamodel for the candidate
+        X_can = X_can.reshape(1, -1)
+        Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can)
+
+        if util_func.lower() == 'alm':
+            # ----- Entropy/MMSE/active learning MacKay(ALM)  -----
+            # Compute perdiction variance of the old model
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            varPCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                varPCE[KeyIdx] = np.max(canPredVar[key], axis=1)
+            score = np.max(varPCE, axis=0)
+
+        elif util_func.lower() == 'eigf':
+            # ----- Expected Improvement for Global fit -----
+            # Find closest EDX to the candidate
+            distances = distance.cdist(ED_X, X_can, 'euclidean')
+            index = np.argmin(distances)
+
+            # Compute perdiction error and variance of the old model
+            predError = {key: Y_PC_can[key] for key in out_names}
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            # Compute perdiction error and variance of the old model
+            # Eq (5) from Liu et al.(2018)
+            EIGF_PCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                residual = predError[key] - out_dict_y[key][int(index)]
+                var = canPredVar[key]
+                EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1)
+            score = np.max(EIGF_PCE, axis=0)
+
+        return -1 * score   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianActiveDesign(self, y_hat, std, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian active design criterion (var).
+
+        It is based on the following paper:
+        Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak.
+        "Bayesian3 active learning for the gaussian process emulator using
+        information theory." Entropy 22, no. 8 (2020): 890.
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            BAL design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # Get the data
+        obs_data = self.observations
+        n_obs = self.Model.n_obs
+        mc_size = 10000
+
+        # Sample a distribution for a normal dist
+        # with Y_mean_can as the mean and Y_std_can as std.
+        Y_MC, std_MC = {}, {}
+        logPriorLikelihoods = np.zeros((mc_size))
+        for key in list(y_hat):
+            cov = np.diag(std[key]**2)
+            rv = stats.multivariate_normal(mean=y_hat[key], cov=cov)
+            Y_MC[key] = rv.rvs(size=mc_size)
+            logPriorLikelihoods += rv.logpdf(Y_MC[key])
+            std_MC[key] = np.zeros((mc_size, y_hat[key].shape[0]))
+
+        #  Likelihood computation (Comparison of data and simulation
+        #  results via PCE with candidate design)
+        likelihoods = self.__normpdf(Y_MC, std_MC, obs_data, sigma2Dict)
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, mc_size)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(likelihoods), dtype=np.longdouble)
+
+        # Posterior-based expectation of likelihoods
+        postLikelihoods = likelihoods[accepted]
+        postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+        # Utility function Eq.2 in Ref. (2)
+        # Posterior covariance matrix after observing data y
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # TODO: Calculate the correction factor for BME
+            # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can,
+            #                                      ObservationData, sigma2Dict)
+            # BME += BMECorrFactor
+            # Haun et al implementation
+            # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+            U_J_d = postExpLikelihoods - logBME
+
+        # Marginal log likelihood
+        elif var == 'BME':
+            U_J_d = np.nanmean(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            logBME = np.log(np.nanmean(likelihoods))
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # Bayesian information criterion
+        elif var == 'BIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxL = np.nanmax(likelihoods)
+            U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams
+
+        # Akaike information criterion
+        elif var == 'AIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxlogL = np.log(np.nanmax(likelihoods))
+            AIC = -2 * maxlogL + 2 * nModelParams
+            # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1)
+            penTerm = 0
+            U_J_d = 1*(AIC + penTerm)
+
+        # Deviance information criterion
+        elif var == 'DIC':
+            # D_theta_bar = np.mean(-2 * Likelihoods)
+            N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0]))
+            Likelihoods_theta_mean = self.__normpdf(
+                y_hat, std, obs_data, sigma2Dict
+                )
+            DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p
+
+            U_J_d = DIC
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Handle inf and NaN (replace by zero)
+        if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf:
+            U_J_d = 0.0
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def update_metamodel(self, MetaModel, output, y_hat_can, univ_p_val, index,
+                         new_pca=False):
+        BasisIndices = MetaModel.basis_dict[output]["y_"+str(index+1)]
+        clf_poly = MetaModel.clf_poly[output]["y_"+str(index+1)]
+        Mn = clf_poly.coef_
+        Sn = clf_poly.sigma_
+        beta = clf_poly.alpha_
+        active = clf_poly.active_
+        Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val)
+
+        Sn_new_inv = np.linalg.inv(Sn)
+        Sn_new_inv += beta * np.dot(Psi[:, active].T, Psi[:, active])
+        Sn_new = np.linalg.inv(Sn_new_inv)
+
+        Mn_new = np.dot(Sn_new_inv, Mn[active]).reshape(-1, 1)
+        Mn_new += beta * np.dot(Psi[:, active].T, y_hat_can)
+        Mn_new = np.dot(Sn_new, Mn_new).flatten()
+
+        # Compute the old and new moments of PCEs
+        mean_old = Mn[0]
+        mean_new = Mn_new[0]
+        std_old = np.sqrt(np.sum(np.square(Mn[1:])))
+        std_new = np.sqrt(np.sum(np.square(Mn_new[1:])))
+
+        # Back transformation if PCA is selected.
+        if MetaModel.dim_red_method.lower() == 'pca':
+            old_pca = MetaModel.pca[output]
+            mean_old = old_pca.mean_[index]
+            mean_old += np.sum(mean_old * old_pca.components_[:, index])
+            std_old = np.sqrt(np.sum(std_old**2 *
+                                     old_pca.components_[:, index]**2))
+            mean_new = new_pca.mean_[index]
+            mean_new += np.sum(mean_new * new_pca.components_[:, index])
+            std_new = np.sqrt(np.sum(std_new**2 *
+                                     new_pca.components_[:, index]**2))
+            # print(f"mean_old: {mean_old:.2f} mean_new: {mean_new:.2f}")
+            # print(f"std_old: {std_old:.2f} std_new: {std_new:.2f}")
+        # Store the old and new moments of PCEs
+        results = {
+            'mean_old': mean_old,
+            'mean_new': mean_new,
+            'std_old': std_old,
+            'std_new': std_new
+            }
+        return results
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign_old(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        Model = self.Model
+        MetaModel = deepcopy(self.MetaModel)
+        old_EDY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels using the candidate design
+        Y_PC_can, Y_std_can = self.MetaModel.eval_metamodel(
+            samples=np.array([X_can])
+            )
+
+        # Generate y from posterior predictive
+        m_size = 100
+        y_hat_samples = {}
+        for idx, key in enumerate(Model.Output.names):
+            means, stds = Y_PC_can[key][0], Y_std_can[key][0]
+            y_hat_samples[key] = np.random.multivariate_normal(
+                means, np.diag(stds), m_size)
+
+        # Create the SparseBayes-based PCE metamodel:
+        MetaModel.input_obj.poly_coeffs_flag = False
+        univ_p_val = self.MetaModel.univ_basis_vals(X_can)
+        G_n_m_all = np.zeros((m_size, len(Model.Output.names), Model.n_obs))
+
+        for i in range(m_size):
+            for idx, key in enumerate(Model.Output.names):
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    # Equal number of components
+                    new_outputs = np.vstack(
+                        (old_EDY[key], y_hat_samples[key][i])
+                        )
+                    new_pca, _ = MetaModel.pca_transformation(new_outputs)
+                    target = new_pca.transform(
+                        y_hat_samples[key][i].reshape(1, -1)
+                        )[0]
+                else:
+                    new_pca, target = False, y_hat_samples[key][i]
+
+                for j in range(len(target)):
+
+                    # Update surrogate
+                    result = self.update_metamodel(
+                        MetaModel, key, target[j], univ_p_val, j, new_pca)
+
+                    # Compute Expected Information Gain (Eq. 39)
+                    G_n_m = np.log(result['std_old']/result['std_new']) - 1./2
+                    G_n_m += result['std_new']**2 / (2*result['std_old']**2)
+                    G_n_m += (result['mean_new'] - result['mean_old'])**2 /\
+                        (2*result['std_old']**2)
+
+                    G_n_m_all[i, idx, j] = G_n_m
+
+        U_J_d = G_n_m_all.mean(axis=(1, 2)).mean()
+        return -1 * U_J_d
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        MetaModel = self.MetaModel
+        out_names = MetaModel.ModelObj.Output.names
+        if X_can.ndim == 1:
+            X_can = X_can.reshape(1, -1)
+
+        # Compute the mean and std based on the MetaModel
+        # pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+        if var == 'ALC':
+            Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+        oldExpDesignY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can)
+        PCE_Model_can = deepcopy(MetaModel)
+        # Add the candidate to the ExpDesign
+        NewExpDesignX = np.vstack((oldExpDesignX, X_can))
+
+        NewExpDesignY = {}
+        for key in oldExpDesignY.keys():
+            NewExpDesignY[key] = np.vstack(
+                (oldExpDesignY[key], Y_PC_can[key])
+                )
+
+        PCE_Model_can.ExpDesign.sampling_method = 'user'
+        PCE_Model_can.ExpDesign.X = NewExpDesignX
+        PCE_Model_can.ModelOutputDict = NewExpDesignY
+        PCE_Model_can.ExpDesign.Y = NewExpDesignY
+
+        # Train the model for the observed data using x_can
+        PCE_Model_can.input_obj.poly_coeffs_flag = False
+        PCE_Model_can.train_norm_design(parallel=False)
+
+        # Set the ExpDesign to its original values
+        PCE_Model_can.ExpDesign.X = oldExpDesignX
+        PCE_Model_can.ModelOutputDict = oldExpDesignY
+        PCE_Model_can.ExpDesign.Y = oldExpDesignY
+
+        if var.lower() == 'mi':
+            # Mutual information based on Krause et al
+            # Adapted from Beck & Guillas (MICE) paper
+            _, std_PC_can = PCE_Model_can.eval_metamodel(samples=X_can)
+            std_can = {key: std_PC_can[key] for key in out_names}
+
+            std_old = {key: Y_std_can[key] for key in out_names}
+
+            varPCE = np.zeros((len(out_names)))
+            for i, key in enumerate(out_names):
+                varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2)
+            score = np.mean(varPCE)
+
+            return -1 * score
+
+        elif var.lower() == 'alc':
+            # Active learning based on Gramyc and Lee
+            # Adaptive design and analysis of supercomputer experiments Techno-
+            # metrics, 51 (2009), pp. 130–145.
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC_can, Y_MC_std_can = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Compute the score
+            score = []
+            for i, key in enumerate(out_names):
+                pce_var = Y_MC_std_can[key]**2
+                pce_var_can = Y_MC_std[key]**2
+                score.append(np.mean(pce_var-pce_var_can, axis=0))
+            score = np.mean(score)
+
+            return -1 * score
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = X_MC.shape[0]
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Enriching Monte Carlo samples if need be
+            if ESS != 0:
+                X_MC = self.MetaModel.ExpDesign.generate_samples(
+                    MCsize, 'random'
+                    )
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and simulation
+            # results via PCE with candidate design)
+            likelihoods = self.__normpdf(
+                Y_MC, std_MC, self.observations, sigma2Dict
+                )
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                print("--- increasing MC size---")
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # -------------------- Utility functions --------------------
+        # Utility function Eq.2 in Ref. (2)
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods, dtype=np.longdouble))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Haun et al implementation
+            U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME)
+
+            # U_J_d = np.sum(G_n_m_all)
+            # Ryan et al (2014) implementation
+            # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0])
+            # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME
+
+            # U_J_d = postExpLikelihoods - logBME
+
+        # Marginal likelihood
+        elif var == 'BME':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+            U_J_d = logBME
+
+        # Bayes risk likelihood
+        elif var == 'BayesRisk':
+
+            U_J_d = -1 * np.var(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postLikelihoods /= np.nansum(likelihoods[accepted])
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Posterior-based expectation of prior densities
+            postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # D-Posterior-precision
+        elif var == 'DPP':
+            X_Posterior = X_MC[accepted]
+            # covariance of the posterior parameters
+            U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior)))
+
+        # A-Posterior-precision
+        elif var == 'APP':
+            X_Posterior = X_MC[accepted]
+            # trace of the posterior parameters
+            U_J_d = -np.log(np.trace(np.cov(X_Posterior)))
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def subdomain(self, Bounds, n_new_samples):
+        """
+        Divides a domain defined by Bounds into sub domains.
+
+        Parameters
+        ----------
+        Bounds : list of tuples
+            List of lower and upper bounds.
+        n_new_samples : TYPE
+            DESCRIPTION.
+
+        Returns
+        -------
+        Subdomains : TYPE
+            DESCRIPTION.
+
+        """
+        n_params = self.MetaModel.n_params
+        n_subdomains = n_new_samples + 1
+        LinSpace = np.zeros((n_params, n_subdomains))
+
+        for i in range(n_params):
+            LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1],
+                                      num=n_subdomains)
+        Subdomains = []
+        for k in range(n_subdomains-1):
+            mylist = []
+            for i in range(n_params):
+                mylist.append((LinSpace[i, k+0], LinSpace[i, k+1]))
+            Subdomains.append(tuple(mylist))
+
+        return Subdomains
+
+    # -------------------------------------------------------------------------
+    def run_util_func(self, method, candidates, index, sigma2Dict=None,
+                      var=None, X_MC=None):
+        """
+        Runs the utility function based on the given method.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        candidates : array of shape (n_samples, n_params)
+            All candidate parameter sets.
+        index : int
+            ExpDesign index.
+        sigma2Dict : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        var : string, optional
+            Utility function. The default is None.
+        X_MC : TYPE, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        index : TYPE
+            DESCRIPTION.
+        List
+            Scores.
+
+        """
+
+        if method.lower() == 'varoptdesign':
+            # U_J_d = self.util_VarBasedDesign(candidates, index, var)
+            U_J_d = np.zeros((candidates.shape[0]))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="varoptdesign"):
+                U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var)
+
+        elif method.lower() == 'bayesactdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            # Evaluate all candidates
+            y_can, std_can = self.MetaModel.eval_metamodel(samples=candidates)
+            # loop through candidates
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="BAL Design"):
+                y_hat = {key: items[idx] for key, items in y_can.items()}
+                std = {key: items[idx] for key, items in std_can.items()}
+                U_J_d[idx] = self.util_BayesianActiveDesign(
+                    y_hat, std, sigma2Dict, var)
+
+        elif method.lower() == 'bayesoptdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict,
+                                                      var)
+        return (index, -1 * U_J_d)
+
+    # -------------------------------------------------------------------------
+    def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No,
+                       verbose=False):
+        """
+        Exploration algorithim to find the optimum parameter space.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        Bounds : list of tuples
+            List of lower and upper boundaries of parameters.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        Run_No : int
+            Run number.
+        verbose : bool, optional
+            Print out a summary. The default is False.
+
+        Returns
+        -------
+        Run_No : int
+            Run number.
+        array
+            Optimial candidate.
+
+        """
+
+        Model = self.Model
+        max_func_itr = self.MetaModel.ExpDesign.max_func_itr
+
+        if method == 'VarOptDesign':
+            Res_Global = opt.dual_annealing(self.util_VarBasedDesign,
+                                            bounds=Bounds,
+                                            args=(Model, var),
+                                            maxfun=max_func_itr)
+
+        elif method == 'BayesOptDesign':
+            Res_Global = opt.dual_annealing(self.util_BayesianDesign,
+                                            bounds=Bounds,
+                                            args=(Model, sigma2Dict, var),
+                                            maxfun=max_func_itr)
+
+        if verbose:
+            print(f"global minimum: xmin = {Res_Global.x}, "
+                  f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}")
+
+        return (Run_No, Res_Global.x)
+
+    # -------------------------------------------------------------------------
+    def tradoff_weights(self, tradeoff_scheme, old_EDX, old_EDY):
+        """
+        Calculates weights for exploration scores based on the requested
+        scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`.
+
+        `None`: No exploration.
+        `equal`: Same weights for exploration and exploitation scores.
+        `epsilon-decreasing`: Start with more exploration and increase the
+            influence of exploitation along the way with a exponential decay
+            function
+        `adaptive`: An adaptive method based on:
+            Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling
+            approach for Kriging metamodeling by maximizing expected prediction
+            error." Computers & Chemical Engineering 106 (2017): 171-182.
+
+        Parameters
+        ----------
+        tradeoff_scheme : string
+            Trade-off scheme for exloration and exploitation scores.
+        old_EDX : array (n_samples, n_params)
+            Old experimental design (training points).
+        old_EDY : dict
+            Old model responses (targets).
+
+        Returns
+        -------
+        exploration_weight : float
+            Exploration weight.
+        exploitation_weight: float
+            Exploitation weight.
+
+        """
+        if tradeoff_scheme is None:
+            exploration_weight = 0
+
+        elif tradeoff_scheme == 'equal':
+            exploration_weight = 0.5
+
+        elif tradeoff_scheme == 'epsilon-decreasing':
+            # epsilon-decreasing scheme
+            # Start with more exploration and increase the influence of
+            # exploitation along the way with a exponential decay function
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8)
+            exploration_weight = signal.exponential(n_max_samples-initNSamples,
+                                                    0, tau2, False)[itrNumber]
+
+        elif tradeoff_scheme == 'adaptive':
+
+            # Extract itrNumber
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            if itrNumber == 0:
+                exploration_weight = 0.5
+            else:
+                # New adaptive trade-off according to Liu et al. (2017)
+                # Mean squared error for last design point
+                last_EDX = old_EDX[-1].reshape(1, -1)
+                lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX)
+                pce_y = np.array(list(lastPCEY.values()))[:, 0]
+                y = np.array(list(old_EDY.values()))[:, -1, :]
+                mseError = mean_squared_error(pce_y, y)
+
+                # Mean squared CV - error for last design point
+                pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0]
+                mseCVError = mean_squared_error(pce_y_prev, y)
+
+                exploration_weight = min([0.5*mseError/mseCVError, 1])
+
+        # Exploitation weight
+        exploitation_weight = 1 - exploration_weight
+
+        return exploration_weight, exploitation_weight
+
+    # -------------------------------------------------------------------------
+    def opt_SeqDesign(self, sigma2, n_candidates=5, var='DKL'):
+        """
+        Runs optimal sequential design.
+
+        Parameters
+        ----------
+        sigma2 : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        n_candidates : int, optional
+            Number of candidate samples. The default is 5.
+        var : string, optional
+            Utility function. The default is None.
+
+        Raises
+        ------
+        NameError
+            Wrong utility function.
+
+        Returns
+        -------
+        Xnew : array (n_samples, n_params)
+            Selected new training point(s).
+        """
+
+        # Initialization
+        MetaModel = self.MetaModel
+        Bounds = MetaModel.bound_tuples
+        n_new_samples = MetaModel.ExpDesign.n_new_samples
+        explore_method = MetaModel.ExpDesign.explore_method
+        exploit_method = MetaModel.ExpDesign.exploit_method
+        n_cand_groups = MetaModel.ExpDesign.n_cand_groups
+        tradeoff_scheme = MetaModel.ExpDesign.tradeoff_scheme
+
+        old_EDX = MetaModel.ExpDesign.X
+        old_EDY = MetaModel.ExpDesign.Y.copy()
+        ndim = MetaModel.ExpDesign.X.shape[1]
+        OutputNames = MetaModel.ModelObj.Output.names
+
+        # -----------------------------------------
+        # ----------- CUSTOMIZED METHODS ----------
+        # -----------------------------------------
+        # Utility function exploit_method provided by user
+        if exploit_method.lower() == 'user':
+
+            Xnew, filteredSamples = MetaModel.ExpDesign.ExploitFunction(self)
+
+            print("\n")
+            print("\nXnew:\n", Xnew)
+
+            return Xnew, filteredSamples
+
+        # -----------------------------------------
+        # ---------- EXPLORATION METHODS ----------
+        # -----------------------------------------
+        if explore_method == 'dual annealing':
+            # ------- EXPLORATION: OPTIMIZATION -------
+            import time
+            start_time = time.time()
+
+            # Divide the domain to subdomains
+            args = []
+            subdomains = self.subdomain(Bounds, n_new_samples)
+            for i in range(n_new_samples):
+                args.append((exploit_method, subdomains[i], sigma2, var, i))
+
+            # Multiprocessing
+            pool = multiprocessing.Pool(multiprocessing.cpu_count())
+
+            # With Pool.starmap_async()
+            results = pool.starmap_async(self.dual_annealing, args).get()
+
+            # Close the pool
+            pool.close()
+
+            Xnew = np.array([results[i][1] for i in range(n_new_samples)])
+
+            print("\nXnew:\n", Xnew)
+
+            elapsed_time = time.time() - start_time
+            print("\n")
+            print(f"elapsed_time: {round(elapsed_time,2)} sec.")
+            print('-'*20)
+
+        elif explore_method == 'LOOCV':
+            # -----------------------------------------------------------------
+            # TODO: LOOCV model construnction based on Feng et al. (2020)
+            # 'LOOCV':
+            # Initilize the ExploitScore array
+
+            # Generate random samples
+            allCandidates = MetaModel.ExpDesign.generate_samples(n_candidates,
+                                                                'random')
+
+            # Construct error model based on LCerror
+            errorModel = MetaModel.create_ModelError(old_EDX, self.LCerror)
+            self.errorModel.append(copy(errorModel))
+
+            # Evaluate the error models for allCandidates
+            eLCAllCands, _ = errorModel.eval_errormodel(allCandidates)
+            # Select the maximum as the representative error
+            eLCAllCands = np.dstack(eLCAllCands.values())
+            eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0]
+
+            # Normalize the error w.r.t the maximum error
+            scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates)
+
+        else:
+            # ------- EXPLORATION: SPACE-FILLING DESIGN -------
+            # Generate candidate samples from Exploration class
+            explore = Exploration(MetaModel, n_candidates)
+            explore.w = 100  # * ndim #500
+            # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+            explore.mc_criterion = 'mc-intersite-proj'
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+
+            # Temp: ---- Plot all candidates -----
+            if ndim == 2:
+                def plotter(points, allCandidates, Method,
+                            scoreExploration=None):
+                    if Method == 'Voronoi':
+                        from scipy.spatial import Voronoi, voronoi_plot_2d
+                        vor = Voronoi(points)
+                        fig = voronoi_plot_2d(vor)
+                        ax1 = fig.axes[0]
+                    else:
+                        fig = plt.figure()
+                        ax1 = fig.add_subplot(111)
+                    ax1.scatter(points[:, 0], points[:, 1], s=10, c='r',
+                                marker="s", label='Old Design Points')
+                    ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10,
+                                c='b', marker="o", label='Design candidates')
+                    for i in range(points.shape[0]):
+                        txt = 'p'+str(i+1)
+                        ax1.annotate(txt, (points[i, 0], points[i, 1]))
+                    if scoreExploration is not None:
+                        for i in range(allCandidates.shape[0]):
+                            txt = str(round(scoreExploration[i], 5))
+                            ax1.annotate(txt, (allCandidates[i, 0],
+                                               allCandidates[i, 1]))
+
+                    plt.xlim(self.bound_tuples[0])
+                    plt.ylim(self.bound_tuples[1])
+                    # plt.show()
+                    plt.legend(loc='upper left')
+
+        # -----------------------------------------
+        # --------- EXPLOITATION METHODS ----------
+        # -----------------------------------------
+        if exploit_method == 'BayesOptDesign' or\
+           exploit_method == 'BayesActDesign':
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\n Exploration weight={explore_w:0.3f} "
+                  f"Exploitation weight={exploit_w:0.3f}\n")
+
+            # ------- EXPLOITATION: BayesOptDesign & ActiveLearning -------
+            if explore_w != 1.0:
+
+                # Create a sample pool for rejection sampling
+                MCsize = 15000
+                X_MC = MetaModel.ExpDesign.generate_samples(MCsize, 'random')
+                candidates = MetaModel.ExpDesign.generate_samples(
+                    MetaModel.ExpDesign.max_func_itr, 'latin_hypercube')
+
+                # Split the candidates in groups for multiprocessing
+                split_cand = np.array_split(
+                    candidates, n_cand_groups, axis=0
+                    )
+
+                results = Parallel(n_jobs=-1, backend='multiprocessing')(
+                        delayed(self.run_util_func)(
+                            exploit_method, split_cand[i], i, sigma2, var, X_MC)
+                        for i in range(n_cand_groups))
+                # out = map(self.run_util_func,
+                #           [exploit_method]*n_cand_groups,
+                #           split_cand,
+                #           range(n_cand_groups),
+                #           [sigma2] * n_cand_groups,
+                #           [var] * n_cand_groups,
+                #           [X_MC] * n_cand_groups
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                U_J_d = np.concatenate([results[NofE][1] for NofE in
+                                        range(n_cand_groups)])
+
+                # Check if all scores are inf
+                if np.isinf(U_J_d).all() or np.isnan(U_J_d).all():
+                    U_J_d = np.ones(len(U_J_d))
+
+                # Get the expected value (mean) of the Utility score
+                # for each cell
+                if explore_method == 'Voronoi':
+                    U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1)
+
+                # create surrogate model for U_J_d
+                # from sklearn.preprocessing import MinMaxScaler
+                # # Take care of inf entries
+                # good_indices = [i for i, arr in enumerate(U_J_d)
+                #                 if np.isfinite(arr).all()]
+                # scaler = MinMaxScaler()
+                # X_S = scaler.fit_transform(candidates[good_indices])
+                # gp = MetaModel.gaussian_process_emulator(
+                #     X_S, U_J_d[good_indices], autoSelect=False
+                #     )
+                # U_J_d = gp.predict(scaler.transform(allCandidates))
+
+                # Normalize U_J_d
+                norm_U_J_d = U_J_d / np.sum(U_J_d)
+            else:
+                norm_U_J_d = np.zeros((len(scoreExploration)))
+
+            # ------- Calculate Total score -------
+            # ------- Trade off between EXPLORATION & EXPLOITATION -------
+            # Accumulate the samples
+            # TODO: added this, recheck!!
+            finalCandidates = np.concatenate((allCandidates, candidates), axis = 0)   
+            finalCandidates = np.unique(finalCandidates, axis = 0)
+            
+            #self.allCandidates = allCandidates
+            #self.candidates = candidates
+            #self.norm_U_J_d = norm_U_J_d
+            #self.exploit_w = exploit_w
+            #self.scoreExploration = scoreExploration
+            
+            # Total score
+            #totalScore = exploit_w * norm_U_J_d
+            #totalScore += explore_w * scoreExploration
+            
+            # TODO: changed this from the above to take into account both exploration and exploitation samples without duplicates
+            totalScore = np.zeros(finalCandidates.shape[0])
+            #self.totalScore = totalScore
+            
+            for cand_idx in range(finalCandidates.shape[0]):
+                # find candidate indices
+                idx1 = np.where(allCandidates == finalCandidates[cand_idx])[0]
+                idx2 = np.where(candidates == finalCandidates[cand_idx])[0]
+                #print(f'Candidate number {cand_idx}')
+                #print(finalCandidates[cand_idx])
+                #print(f'Idx1: {idx1}, Idx2: {idx2}')
+                
+                # exploration 
+                if idx1 != []:
+                    idx1 = idx1[0]
+                    #print(f'Values1: {allCandidates[idx1]}')
+                    totalScore[cand_idx] += explore_w * scoreExploration[idx1]
+                    
+                # exploitation
+                if idx2 != []:
+                    idx2 = idx2[0]
+                    #print(f'Values1: {candidates[idx2]}')
+                    totalScore[cand_idx] += exploit_w * norm_U_J_d[idx2]
+                
+
+            # temp: Plot
+            # dim = self.ExpDesign.X.shape[1]
+            # if dim == 2:
+            #     plotter(self.ExpDesign.X, allCandidates, explore_method)
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            # select the requested number of samples
+            if explore_method == 'Voronoi':
+                Xnew = np.zeros((n_new_samples, ndim))
+                for i, idx in enumerate(bestIdx):
+                    X_can = explore.closestPoints[idx]
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+            else:
+                # TODO: changed this from allCandiates to full set of candidates - still not changed for e.g. 'Voronoi'
+                Xnew = finalCandidates[sorted_idxtotalScore[:n_new_samples]]          # here candidates(exploitation) vs allCandidates (exploration)!!
+
+        elif exploit_method == 'VarOptDesign':
+            # ------- EXPLOITATION: VarOptDesign -------
+            UtilMethod = var
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\nweightExploration={explore_w:0.3f} "
+                  f"weightExploitation={exploit_w:0.3f}")
+
+            # Generate candidate samples from Exploration class
+            nMeasurement = old_EDY[OutputNames[0]].shape[1]
+
+            # Find sensitive region
+            if UtilMethod == 'LOOCV':
+                LCerror = MetaModel.LCerror
+                allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames),
+                                           nMeasurement))
+                for y_idx, y_key in enumerate(OutputNames):
+                    for idx, key in enumerate(LCerror[y_key].keys()):
+                        allModifiedLOO[:, y_idx, idx] = abs(
+                            LCerror[y_key][key])
+
+                ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1)
+
+            elif UtilMethod in ['EIGF', 'ALM']:
+                # ----- All other in  ['EIGF', 'ALM'] -----
+                # Initilize the ExploitScore array
+                ExploitScore = np.zeros((len(old_EDX), len(OutputNames)))
+
+                # Split the candidates in groups for multiprocessing
+                if explore_method != 'Voronoi':
+                    split_cand = np.array_split(allCandidates,
+                                                n_cand_groups,
+                                                axis=0)
+                    goodSampleIdx = range(n_cand_groups)
+                else:
+                    # Find indices of the Vornoi cells with samples
+                    goodSampleIdx = []
+                    for idx in range(len(explore.closest_points)):
+                        if len(explore.closest_points[idx]) != 0:
+                            goodSampleIdx.append(idx)
+                    split_cand = explore.closest_points
+
+                # Split the candidates in groups for multiprocessing
+                args = []
+                for index in goodSampleIdx:
+                    args.append((exploit_method, split_cand[index], index,
+                                 sigma2, var))
+
+                # Multiprocessing
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.run_util_func, args).get()
+
+                # Close the pool
+                pool.close()
+                # out = map(self.run_util_func,
+                #           [exploit_method]*len(goodSampleIdx),
+                #           split_cand,
+                #           range(len(goodSampleIdx)),
+                #           [sigma2] * len(goodSampleIdx),
+                #           [var] * len(goodSampleIdx)
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                if explore_method == 'Voronoi':
+                    ExploitScore = [np.mean(results[k][1]) for k in
+                                    range(len(goodSampleIdx))]
+                else:
+                    ExploitScore = np.concatenate(
+                        [results[k][1] for k in range(len(goodSampleIdx))])
+
+            else:
+                raise NameError('The requested utility function is not '
+                                'available.')
+
+            # print("ExploitScore:\n", ExploitScore)
+
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            # Total score
+            # Normalize U_J_d
+            ExploitScore = ExploitScore / np.sum(ExploitScore)
+            totalScore = exploit_w * ExploitScore
+            totalScore += explore_w * scoreExploration
+
+            temp = totalScore.copy()
+            sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            Xnew = np.zeros((n_new_samples, ndim))
+            if explore_method != 'Voronoi':
+                Xnew = allCandidates[bestIdx]
+            else:
+                for i, idx in enumerate(bestIdx.flatten()):
+                    X_can = explore.closest_points[idx]
+                    # plotter(self.ExpDesign.X, X_can, explore_method,
+                    # scoreExploration=None)
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+
+        elif exploit_method == 'alphabetic':
+            # ------- EXPLOITATION: ALPHABETIC -------
+            Xnew = self.util_AlphOptDesign(allCandidates, var)
+
+        elif exploit_method == 'Space-filling':
+            # ------- EXPLOITATION: SPACE-FILLING -------
+            totalScore = scoreExploration
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+
+            # select the requested number of samples
+            Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        else:
+            raise NameError('The requested design method is not available.')
+
+        print("\n")
+        print("\nRun No. {}:".format(old_EDX.shape[0]+1))
+        print("Xnew:\n", Xnew)
+
+        return Xnew, None
+
+    # -------------------------------------------------------------------------
+    def util_AlphOptDesign(self, candidates, var='D-Opt'):
+        """
+        Enriches the Experimental design with the requested alphabetic
+        criterion based on exploring the space with number of sampling points.
+
+        Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos
+        expansion: A review of sampling strategies., Computer Methods in
+        Applied Mechanics and Engineering, 332, 382-407.
+
+        Arguments
+        ---------
+        NCandidate : int
+            Number of candidate points to be searched
+
+        var : string
+            Alphabetic optimality criterion
+
+        Returns
+        -------
+        X_new : array of shape (1, n_params)
+            The new sampling location in the input space.
+        """
+        MetaModelOrig = self
+        Model = self.Model
+        n_new_samples = MetaModelOrig.ExpDesign.n_new_samples
+        NCandidate = candidates.shape[0]
+
+        # TODO: Loop over outputs
+        OutputName = Model.Output.names[0]
+
+        # To avoid changes ub original aPCE object
+        MetaModel = deepcopy(MetaModelOrig)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+
+        # TODO: Only one psi can be selected.
+        # Suggestion: Go for the one with the highest LOO error
+        Scores = list(MetaModel.score_dict[OutputName].values())
+        ModifiedLOO = [1-score for score in Scores]
+        outIdx = np.argmax(ModifiedLOO)
+
+        # Initialize Phi to save the criterion's values
+        Phi = np.zeros((NCandidate))
+
+        BasisIndices = MetaModelOrig.basis_dict[OutputName]["y_"+str(outIdx+1)]
+        P = len(BasisIndices)
+
+        # ------ Old Psi ------------
+        univ_p_val = MetaModelOrig.univ_basis_vals(oldExpDesignX)
+        Psi = MetaModelOrig.create_psi(BasisIndices, univ_p_val)
+
+        # ------ New candidates (Psi_c) ------------
+        # Assemble Psi_c
+        univ_p_val_c = self.univ_basis_vals(candidates)
+        Psi_c = self.create_psi(BasisIndices, univ_p_val_c)
+
+        for idx in range(NCandidate):
+
+            # Include the new row to the original Psi
+            Psi_cand = np.vstack((Psi, Psi_c[idx]))
+
+            # Information matrix
+            PsiTPsi = np.dot(Psi_cand.T, Psi_cand)
+            M = PsiTPsi / (len(oldExpDesignX)+1)
+
+            if np.linalg.cond(PsiTPsi) > 1e-12 \
+               and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon:
+                # faster
+                invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray())
+            else:
+                # stabler
+                invM = np.linalg.pinv(M)
+
+            # ---------- Calculate optimality criterion ----------
+            # Optimality criteria according to Section 4.5.1 in Ref.
+
+            # D-Opt
+            if var == 'D-Opt':
+                Phi[idx] = (np.linalg.det(invM)) ** (1/P)
+
+            # A-Opt
+            elif var == 'A-Opt':
+                Phi[idx] = np.trace(invM)
+
+            # K-Opt
+            elif var == 'K-Opt':
+                Phi[idx] = np.linalg.cond(M)
+
+            else:
+                raise Exception('The optimality criterion you requested has '
+                      'not been implemented yet!')
+
+        # find an optimal point subset to add to the initial design
+        # by minimization of the Phi
+        sorted_idxtotalScore = np.argsort(Phi)
+
+        # select the requested number of samples
+        Xnew = candidates[sorted_idxtotalScore[:n_new_samples]]
+
+        return Xnew
+
+    # -------------------------------------------------------------------------
+    def __normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s,
+                  rmse=None):
+
+        Model = self.Model
+        likelihoods = 1.0
+
+        # Loop over the outputs
+        for idx, out in enumerate(Model.Output.names):
+
+            # (Meta)Model Output
+            nsamples, nout = y_hat_pce[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values
+
+            # Surrogate error if valid dataset is given.
+            if rmse is not None:
+                tot_sigma2s += rmse[out]**2
+            else:
+                tot_sigma2s += np.mean(std_pce[out])**2
+
+            likelihoods *= stats.multivariate_normal.pdf(
+                y_hat_pce[out], data, np.diag(tot_sigma2s),
+                allow_singular=True)
+
+        self.Likelihoods = likelihoods
+
+        return likelihoods
+
+    # -------------------------------------------------------------------------
+    def __corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = MetaModel.ExpDesign.X  # valid_samples
+        model_outputs = MetaModel.ExpDesign.Y  # valid_model_runs
+        Model = MetaModel.ModelObj
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = Model.Output.names
+
+        # TODO: Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                # covMatrix = np.diag(stdOutputRS[out][i]**2)
+                covMatrix = np.diag((y_m-y_m_hat)**2)
+                covMatrix = np.diag(
+                    np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2
+                    )
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += self.__logpdf(
+                    y_m_hat, data, covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += self.__logpdf(y_m_hat, y_m, covMatrix)
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.exp(logLik_model+logLik_data)
+
+        return np.log(np.mean(weights))
+
+    # -------------------------------------------------------------------------
+    def __logpdf(self, x, mean, cov):
+        """
+        computes the likelihood based on a multivariate normal distribution.
+
+        Parameters
+        ----------
+        x : TYPE
+            DESCRIPTION.
+        mean : array_like
+            Observation data.
+        cov : 2d array
+            Covariance matrix of the distribution.
+
+        Returns
+        -------
+        log_lik : float
+            Log likelihood.
+
+        """
+        n = len(mean)
+        L = linalg.cholesky(cov, lower=True)
+        beta = np.sum(np.log(np.diag(L)))
+        dev = x - mean
+        alpha = dev.dot(linalg.cho_solve((L, True), dev))
+        log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+
+        return log_lik
+
+    # -------------------------------------------------------------------------
+    def __posteriorPlot(self, posterior, par_names, key):
+
+        # Initialization
+        newpath = (r'Outputs_SeqPosteriorComparison/posterior')
+        os.makedirs(newpath, exist_ok=True)
+
+        bound_tuples = self.MetaModel.bound_tuples
+        n_params = len(par_names)
+        font_size = 40
+        if n_params == 2:
+
+            figPosterior, ax = plt.subplots(figsize=(15, 15))
+
+            sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1],
+                        fill=True, ax=ax, cmap=plt.cm.jet,
+                        clip=bound_tuples)
+            # Axis labels
+            plt.xlabel(par_names[0], fontsize=font_size)
+            plt.ylabel(par_names[1], fontsize=font_size)
+
+            # Set axis limit
+            plt.xlim(bound_tuples[0])
+            plt.ylim(bound_tuples[1])
+
+            # Increase font size
+            plt.xticks(fontsize=font_size)
+            plt.yticks(fontsize=font_size)
+
+            # Switch off the grids
+            plt.grid(False)
+
+        else:
+            import corner
+            figPosterior = corner.corner(posterior, labels=par_names,
+                                         title_fmt='.2e', show_titles=True,
+                                         title_kwargs={"fontsize": 12})
+
+        figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight')
+        plt.close()
+
+        # Save the posterior as .npy
+        np.save(f'./{newpath}/{key}.npy', posterior)
+
+        return figPosterior
+
+    # -------------------------------------------------------------------------
+    def __hellinger_distance(self, P, Q):
+        """
+        Hellinger distance between two continuous distributions.
+
+        The maximum distance 1 is achieved when P assigns probability zero to
+        every set to which Q assigns a positive probability, and vice versa.
+        0 (identical) and 1 (maximally different)
+
+        Parameters
+        ----------
+        P : array
+            Reference likelihood.
+        Q : array
+            Estimated likelihood.
+
+        Returns
+        -------
+        float
+            Hellinger distance of two distributions.
+
+        """
+        mu1 = P.mean()
+        Sigma1 = np.std(P)
+
+        mu2 = Q.mean()
+        Sigma2 = np.std(Q)
+
+        term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2))
+
+        term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2))
+
+        H_squared = 1 - term1 * term2
+
+        return np.sqrt(H_squared)
+
+    # -------------------------------------------------------------------------
+    def __BME_Calculator(self, MetaModel, obs_data, sigma2Dict, rmse=None):
+        """
+        This function computes the Bayesian model evidence (BME) via Monte
+        Carlo integration.
+
+        """
+        # Initializations
+        if hasattr(MetaModel, 'valid_likelihoods'):
+            valid_likelihoods = MetaModel.valid_likelihoods
+        else:
+            valid_likelihoods = []
+
+        post_snapshot = MetaModel.ExpDesign.post_snapshot
+        #print(f'post_snapshot: {post_snapshot}')
+        if post_snapshot or len(valid_likelihoods) != 0:
+            newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref')
+            os.makedirs(newpath, exist_ok=True)
+
+        SamplingMethod = 'random'
+        MCsize = 10000
+        ESS = 0
+
+        # Estimation of the integral via Monte Varlo integration
+        while (ESS > MCsize) or (ESS < 1):
+
+            # Generate samples for Monte Carlo simulation
+            X_MC = MetaModel.ExpDesign.generate_samples(
+                MCsize, SamplingMethod
+                )
+
+            # Monte Carlo simulation for the candidate design
+            Y_MC, std_MC = MetaModel.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and
+            # simulation results via PCE with candidate design)
+            Likelihoods = self.__normpdf(
+                Y_MC, std_MC, obs_data, sigma2Dict, rmse
+                )
+
+            # Check the Effective Sample Size (1000<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if (ESS > MCsize) or (ESS < 1):
+                print(f'ESS={ESS} MC size should be larger.')
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+        X_Posterior = X_MC[accepted]
+
+        # ------------------------------------------------------------
+        # --- Kullback-Leibler Divergence & Information Entropy ------
+        # ------------------------------------------------------------
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(Likelihoods))
+
+        # TODO: Correction factor
+        # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME)
+
+        # Posterior-based expectation of likelihoods
+        postExpLikelihoods = np.mean(np.log(Likelihoods[accepted]))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(
+            np.log(MetaModel.ExpDesign.JDist.pdf(X_Posterior.T))
+            )
+
+        # Calculate Kullback-Leibler Divergence
+        # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+        KLD = postExpLikelihoods - logBME
+
+        # Information Entropy based on Entropy paper Eq. 38
+        infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+        # If post_snapshot is True, plot likelihood vs refrence
+        if post_snapshot or valid_likelihoods:
+            # Hellinger distance
+            #print('arrived here')
+            #print(np.array(valid_likelihoods))
+            valid_likelihoods = np.array(valid_likelihoods)
+            #valid_likelihoods = np.array(valid_likelihoods)
+            ref_like = np.log(valid_likelihoods[(valid_likelihoods > 0)])
+            est_like = np.log(Likelihoods[Likelihoods > 0])
+            distHellinger = self.__hellinger_distance(ref_like, est_like)
+
+            idx = len([name for name in os.listdir(newpath) if 'Likelihoods_'
+                       in name and os.path.isfile(os.path.join(newpath, name))])
+            fig, ax = plt.subplots()
+            try:
+                sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]),
+                            shade=True, color="g", label='Ref. Likelihood')
+                sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True,
+                            color="b", label='Likelihood with PCE')
+            except:
+                pass
+
+            text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}"
+            "\n DKL={KLD:.3f}"
+
+            plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat',
+                                                 edgecolor='black',
+                                                 boxstyle='round,pad=1'),
+                     transform=ax.transAxes)
+
+            fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf',
+                        bbox_inches='tight')
+            plt.close()
+
+        else:
+            distHellinger = 0.0
+
+        # Bayesian inference with Emulator only for 2D problem
+        if post_snapshot and MetaModel.n_params == 2 and not idx % 5:
+            BayesOpts = BayesInference(MetaModel)
+            BayesOpts.emulator = True
+            BayesOpts.plot_post_pred = False
+
+            # Select the inference method
+            import emcee
+            BayesOpts.inference_method = "MCMC"
+            # Set the MCMC parameters passed to self.mcmc_params
+            BayesOpts.mcmc_params = {
+                'n_steps': 1e5,
+                'n_walkers': 30,
+                'moves': emcee.moves.KDEMove(),
+                'verbose': False
+                }
+
+            # ----- Define the discrepancy model -------
+            obs_data = pd.DataFrame(obs_data, columns=self.Model.Output.names)
+            BayesOpts.measurement_error = obs_data
+
+            # # -- (Option B) --
+            DiscrepancyOpts = Discrepancy('')
+            DiscrepancyOpts.type = 'Gaussian'
+            DiscrepancyOpts.parameters = obs_data**2
+            BayesOpts.Discrepancy = DiscrepancyOpts
+            # Start the calibration/inference
+            Bayes_PCE = BayesOpts.create_inference()
+            X_Posterior = Bayes_PCE.posterior_df.values
+
+        return (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+
+    # -------------------------------------------------------------------------
+    def __validError(self, MetaModel):
+
+        # MetaModel = self.MetaModel
+        Model = MetaModel.ModelObj
+        OutputName = Model.Output.names
+
+        # Extract the original model with the generated samples
+        valid_samples = MetaModel.valid_samples
+        valid_model_runs = MetaModel.valid_model_runs
+
+        # Run the PCE model with the generated samples
+        valid_PCE_runs, _ = MetaModel.eval_metamodel(samples=valid_samples)
+
+        rms_error = {}
+        valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in OutputName:
+            rms_error[key] = mean_squared_error(
+                valid_model_runs[key], valid_PCE_runs[key],
+                multioutput='raw_values',
+                sample_weight=None,
+                squared=False)
+            # Validation error
+            valid_error[key] = (rms_error[key]**2)
+            valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Updated Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(rms_error[key],
+                                             valid_error[key]))))
+
+        return rms_error, valid_error
+
+    # -------------------------------------------------------------------------
+    def __error_Mean_Std(self):
+
+        MetaModel = self.MetaModel
+        # Extract the mean and std provided by user
+        df_MCReference = MetaModel.ModelObj.mc_reference
+
+        # Compute the mean and std based on the MetaModel
+        pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+
+        # Compute the root mean squared error
+        for output in MetaModel.ModelObj.Output.names:
+
+            # Compute the error between mean and std of MetaModel and OrigModel
+            RMSE_Mean = mean_squared_error(
+                df_MCReference['mean'], pce_means[output], squared=False
+                )
+            RMSE_std = mean_squared_error(
+                df_MCReference['std'], pce_means[output], squared=False
+                )
+
+        return RMSE_Mean, RMSE_std
+
+    # -------------------------------------------------------------------------
+    def _compute_pce_moments(self, MetaModel):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moment (mean) of the surrogate.
+        pce_stds: dict
+            The second moment (standard deviation) of the surrogate.
+
+        """
+        outputs = MetaModel.ModelObj.Output.names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(MetaModel.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    PCA = MetaModel.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.inverse_transform(pce_mean)
+                    stds[output] = PCA.inverse_transform(np.sqrt(pce_var))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+        return pce_means, pce_stds
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py b/examples/analytical-function/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py
new file mode 100644
index 0000000000000000000000000000000000000000..96ef9c1d50b10b587ad0846d41733fc7f1cedfe8
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py
@@ -0,0 +1,366 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jul 15 14:08:59 2022
+
+@author: farid
+"""
+import numpy as np
+from sklearn.base import RegressorMixin
+from sklearn.linear_model._base import LinearModel
+from sklearn.utils import check_X_y
+
+
+def corr(x, y):
+    return abs(x.dot(y))/np.sqrt((x**2).sum())
+
+
+class OrthogonalMatchingPursuit(LinearModel, RegressorMixin):
+    '''
+    Regression with Orthogonal Matching Pursuit [1].
+
+    Parameters
+    ----------
+    fit_intercept : boolean, optional (DEFAULT = True)
+        whether to calculate the intercept for this model. If set
+        to false, no intercept will be used in calculations
+        (e.g. data is expected to be already centered).
+
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied; else, it may be overwritten.
+
+    verbose : boolean, optional (DEFAULT = FALSE)
+        Verbose mode when fitting the model
+
+    Attributes
+    ----------
+    coef_ : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+
+    active_ : array, dtype = np.bool, shape = (n_features)
+       True for non-zero coefficients, False otherwise
+
+    References
+    ----------
+    [1] Pati, Y., Rezaiifar, R., Krishnaprasad, P. (1993). Orthogonal matching
+        pursuit: recursive function approximation with application to wavelet
+        decomposition. Proceedings of 27th Asilomar Conference on Signals,
+        Systems and Computers, 40-44.
+    '''
+
+    def __init__(self, fit_intercept=True, normalize=False, copy_X=True,
+                 verbose=False):
+        self.fit_intercept   = fit_intercept
+        self.normalize       = normalize
+        self.copy_X          = copy_X
+        self.verbose         = verbose
+
+    def _preprocess_data(self, X, y):
+        """Center and scale data.
+        Centers data to have mean zero along axis 0. If fit_intercept=False or
+        if the X is a sparse matrix, no centering is done, but normalization
+        can still be applied. The function returns the statistics necessary to
+        reconstruct the input data, which are X_offset, y_offset, X_scale, such
+        that the output
+            X = (X - X_offset) / X_scale
+        X_scale is the L2 norm of X - X_offset.
+        """
+
+        if self.copy_X:
+            X = X.copy(order='K')
+
+        y = np.asarray(y, dtype=X.dtype)
+
+        if self.fit_intercept:
+            X_offset = np.average(X, axis=0)
+            X -= X_offset
+            if self.normalize:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+                std = np.sqrt(np.sum(X**2, axis=0)/(len(X)-1))
+                X_scale[std != 0] = std[std != 0]
+                X /= X_scale
+            else:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            y_offset = np.mean(y)
+            y = y - y_offset
+        else:
+            X_offset = np.zeros(X.shape[1], dtype=X.dtype)
+            X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            if y.ndim == 1:
+                y_offset = X.dtype.type(0)
+            else:
+                y_offset = np.zeros(y.shape[1], dtype=X.dtype)
+
+        return X, y, X_offset, y_offset, X_scale
+
+    def fit(self, X, y):
+        '''
+        Fits Regression with Orthogonal Matching Pursuit Algorithm.
+
+        Parameters
+        -----------
+        X: {array-like, sparse matrix} of size (n_samples, n_features)
+           Training data, matrix of explanatory variables
+
+        y: array-like of size [n_samples, n_features]
+           Target values
+
+        Returns
+        -------
+        self : object
+            Returns self.
+        '''
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+
+        X, y, X_mean, y_mean, X_std = self._preprocess_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean = y_mean
+        self._x_std = X_std
+
+        # Normalize columns of Psi, so that each column has norm = 1
+        norm_X = np.linalg.norm(X, axis=0)
+        X_norm = X/norm_X
+
+        # Initialize residual vector to full model response and normalize
+        R = y
+        norm_y = np.sqrt(np.dot(y, y))
+        r = y/norm_y
+
+        # Check for constant regressors
+        const_indices = np.where(~np.diff(X, axis=0).any(axis=0))[0]
+        bool_const = not const_indices
+
+        # Start regression using OPM algorithm
+        precision = 0        # Set precision criterion to precision of program
+        early_stop = True
+        cond_early = True    # Initialize condition for early stop
+        ind = []
+        iindx = []           # index of selected columns
+        indtot = np.arange(n_features)  # Full index set for remaining columns
+        kmax = min(n_samples, n_features)  # Maximum number of iterations
+        LOO = np.PINF * np.ones(kmax)  # Store LOO error at each iteration
+        LOOmin = np.PINF               # Initialize minimum value of LOO
+        coeff = np.zeros((n_features, kmax))
+        count = 0
+        k = 0.1                # Percentage of iteration history for early stop
+
+        # Begin iteration over regressors set (Matrix X)
+        while (np.linalg.norm(R) > precision) and (count <= kmax-1) and \
+              ((cond_early or early_stop) ^ ~cond_early):
+
+            # Update index set of columns yet to select
+            if count != 0:
+                indtot = np.delete(indtot, iindx)
+
+            # Find column of X that is most correlated with residual
+            h = abs(np.dot(r, X_norm))
+            iindx = np.argmax(h[indtot])
+            indx = indtot[iindx]
+
+            # initialize with the constant regressor, if it exists in the basis
+            if (count == 0) and bool_const:
+                # overwrite values for iindx and indx
+                iindx = const_indices[0]
+                indx = indtot[iindx]
+
+            # Invert the information matrix at the first iteration, later only
+            # update its value on the basis of the previously inverted one,
+            if count == 0:
+                M = 1 / np.dot(X[:, indx], X[:, indx])
+            else:
+                x = np.dot(X[:, ind].T, X[:, indx])
+                r = np.dot(X[:, indx], X[:, indx])
+                M = self.blockwise_inverse(M, x, x.T, r)
+
+            # Add newly found index to the selected indexes set
+            ind.append(indx)
+
+            # Select regressors subset (Projection subspace)
+            Xpro = X[:, ind]
+
+            # Obtain coefficient by performing OLS
+            TT = np.dot(y, Xpro)
+            beta = np.dot(M, TT)
+            coeff[ind, count] = beta
+
+            # Compute LOO error
+            LOO[count] = self.loo_error(Xpro, M, y, beta)
+
+            # Compute new residual due to new projection
+            R = y - np.dot(Xpro, beta)
+
+            # Normalize residual
+            norm_R = np.sqrt(np.dot(R, R))
+            r = R / norm_R
+
+            # Update counters and early-stop criterions
+            countinf = max(0, int(count-k*kmax))
+            LOOmin = min(LOOmin, LOO[count])
+
+            if count == 0:
+                cond_early = (LOO[0] <= LOOmin)
+            else:
+                cond_early = (min(LOO[countinf:count+1]) <= LOOmin)
+
+            if self.verbose:
+                print(f'Iteration: {count+1}, mod. LOOCV error : '
+                      f'{LOO[count]:.2e}')
+
+            # Update counter
+            count += 1
+
+        # Select projection with smallest cross-validation error
+        countmin = np.argmin(LOO[:-1])
+        self.coef_ = coeff[:, countmin]
+        self.active = coeff[:, countmin] != 0.0
+
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+
+        return self
+
+    def predict(self, X):
+        '''
+        Computes predictive distribution for test set.
+
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+
+        Returns
+        -------
+        y_hat: numpy array of size (n_samples_test,)
+               Estimated values of targets on test set (i.e. mean of
+               predictive distribution)
+        '''
+
+        y_hat = np.dot(X, self.coef_) + self.intercept_
+
+        return y_hat
+
+    def loo_error(self, psi, inv_inf_matrix, y, coeffs):
+        """
+        Calculates the corrected LOO error for regression on regressor
+        matrix `psi` that generated the coefficients based on [1] and [2].
+
+        [1] Blatman, G., 2009. Adaptive sparse polynomial chaos expansions for
+            uncertainty propagation and sensitivity analysis (Doctoral
+            dissertation, Clermont-Ferrand 2).
+
+        [2] Blatman, G. and Sudret, B., 2011. Adaptive sparse polynomial chaos
+            expansion based on least angle regression. Journal of computational
+            Physics, 230(6), pp.2345-2367.
+
+        Parameters
+        ----------
+        psi : array of shape (n_samples, n_feature)
+            Orthogonal bases evaluated at the samples.
+        inv_inf_matrix : array
+            Inverse of the information matrix.
+        y : array of shape (n_samples, )
+            Targets.
+        coeffs : array
+            Computed regresssor cofficients.
+
+        Returns
+        -------
+        loo_error : float
+            Modified LOOCV error.
+
+        """
+
+        # NrEvaluation (Size of experimental design)
+        N, P = psi.shape
+
+        # h factor (the full matrix is not calculated explicitly,
+        # only the trace is, to save memory)
+        PsiM = np.dot(psi, inv_inf_matrix)
+
+        h = np.sum(np.multiply(PsiM, psi), axis=1, dtype=np.longdouble)
+
+        # ------ Calculate Error Loocv for each measurement point ----
+        # Residuals
+        residual = np.dot(psi, coeffs) - y
+
+        # Variance
+        varY = np.var(y)
+
+        if varY == 0:
+            norm_emp_error = 0
+            loo_error = 0
+        else:
+            norm_emp_error = np.mean(residual**2)/varY
+
+            loo_error = np.mean(np.square(residual / (1-h))) / varY
+
+            # if there are NaNs, just return an infinite LOO error (this
+            # happens, e.g., when a strongly underdetermined problem is solved)
+            if np.isnan(loo_error):
+                loo_error = np.inf
+
+        # Corrected Error for over-determined system
+        tr_M = np.trace(np.atleast_2d(inv_inf_matrix))
+        if tr_M < 0 or abs(tr_M) > 1e6:
+            tr_M = np.trace(np.linalg.pinv(np.dot(psi.T, psi)))
+
+        # Over-determined system of Equation
+        if N > P:
+            T_factor = N/(N-P) * (1 + tr_M)
+
+        # Under-determined system of Equation
+        else:
+            T_factor = np.inf
+
+        loo_error *= T_factor
+
+        return loo_error
+
+    def blockwise_inverse(self, Ainv, B, C, D):
+        """
+        non-singular square matrix M defined as M = [[A B]; [C D]] .
+        B, C and D can have any dimension, provided their combination defines
+        a square matrix M.
+
+        Parameters
+        ----------
+        Ainv : float or array
+            inverse of the square-submatrix A.
+        B : float or array
+            Information matrix with all new regressor.
+        C : float or array
+            Transpose of B.
+        D : float or array
+            Information matrix with all selected regressors.
+
+        Returns
+        -------
+        M : array
+            Inverse of the information matrix.
+
+        """
+        if np.isscalar(D):
+            # Inverse of D
+            Dinv = 1/D
+            # Schur complement
+            SCinv = 1/(D - np.dot(C, np.dot(Ainv, B[:, None])))[0]
+        else:
+            # Inverse of D
+            Dinv = np.linalg.solve(D, np.eye(D.shape))
+            # Schur complement
+            SCinv = np.linalg.solve((D - C*Ainv*B), np.eye(D.shape))
+
+        T1 = np.dot(Ainv, np.dot(B[:, None], SCinv))
+        T2 = np.dot(C, Ainv)
+
+        # Assemble the inverse matrix
+        M = np.vstack((
+            np.hstack((Ainv+T1*T2, -T1)),
+            np.hstack((-(SCinv)*T2, SCinv))
+            ))
+        return M
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/reg_fast_ard.py b/examples/analytical-function/bayesvalidrox/surrogate_models/reg_fast_ard.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6883a3edd6d247c219b8be328f5206b75780fbb
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/reg_fast_ard.py
@@ -0,0 +1,475 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 24 19:41:45 2020
+
+@author: farid
+"""
+import numpy as np
+from scipy.linalg import solve_triangular
+from numpy.linalg import LinAlgError
+from sklearn.base import RegressorMixin
+from sklearn.linear_model._base import LinearModel
+import warnings
+from sklearn.utils import check_X_y
+from scipy.linalg import pinvh
+
+
+def update_precisions(Q,S,q,s,A,active,tol,n_samples,clf_bias):
+    '''
+    Selects one feature to be added/recomputed/deleted to model based on
+    effect it will have on value of log marginal likelihood.
+    '''
+    # initialise vector holding changes in log marginal likelihood
+    deltaL = np.zeros(Q.shape[0])
+
+    # identify features that can be added , recomputed and deleted in model
+    theta        =  q**2 - s
+    add          =  (theta > 0) * (active == False)
+    recompute    =  (theta > 0) * (active == True)
+    delete       = ~(add + recompute)
+
+    # compute sparsity & quality parameters corresponding to features in
+    # three groups identified above
+    Qadd,Sadd      = Q[add], S[add]
+    Qrec,Srec,Arec = Q[recompute], S[recompute], A[recompute]
+    Qdel,Sdel,Adel = Q[delete], S[delete], A[delete]
+
+    # compute new alpha's (precision parameters) for features that are
+    # currently in model and will be recomputed
+    Anew           = s[recompute]**2/ ( theta[recompute] + np.finfo(np.float32).eps)
+    delta_alpha    = (1./Anew - 1./Arec)
+
+    # compute change in log marginal likelihood
+    deltaL[add]       = ( Qadd**2 - Sadd ) / Sadd + np.log(Sadd/Qadd**2 )
+    deltaL[recompute] = Qrec**2 / (Srec + 1. / delta_alpha) - np.log(1 + Srec*delta_alpha)
+    deltaL[delete]    = Qdel**2 / (Sdel - Adel) - np.log(1 - Sdel / Adel)
+    deltaL            = deltaL  / n_samples
+
+    # find feature which caused largest change in likelihood
+    feature_index = np.argmax(deltaL)
+
+    # no deletions or additions
+    same_features  = np.sum( theta[~recompute] > 0) == 0
+
+    # changes in precision for features already in model is below threshold
+    no_delta       = np.sum( abs( Anew - Arec ) > tol ) == 0
+    # if same_features: print(abs( Anew - Arec ))
+    # print("same_features = {} no_delta = {}".format(same_features,no_delta))
+    # check convergence: if no features to add or delete and small change in
+    #                    precision for current features then terminate
+    converged = False
+    if same_features and no_delta:
+        converged = True
+        return [A,converged]
+
+    # if not converged update precision parameter of weights and return
+    if theta[feature_index] > 0:
+        A[feature_index] = s[feature_index]**2 / theta[feature_index]
+        if active[feature_index] == False:
+            active[feature_index] = True
+    else:
+        # at least two active features
+        if active[feature_index] == True and np.sum(active) >= 2:
+            # do not remove bias term in classification
+            # (in regression it is factored in through centering)
+            if not (feature_index == 0 and clf_bias):
+                active[feature_index] = False
+                A[feature_index]      = np.PINF
+
+    return [A,converged]
+
+
+class RegressionFastARD(LinearModel, RegressorMixin):
+    '''
+    Regression with Automatic Relevance Determination (Fast Version uses
+    Sparse Bayesian Learning)
+    https://github.com/AmazaspShumik/sklearn-bayes/blob/master/skbayes/rvm_ard_models/fast_rvm.py
+
+    Parameters
+    ----------
+    n_iter: int, optional (DEFAULT = 100)
+        Maximum number of iterations
+
+    start: list, optional (DEFAULT = None)
+        Initial selected features.
+
+    tol: float, optional (DEFAULT = 1e-3)
+        If absolute change in precision parameter for weights is below threshold
+        algorithm terminates.
+
+    fit_intercept : boolean, optional (DEFAULT = True)
+        whether to calculate the intercept for this model. If set
+        to false, no intercept will be used in calculations
+        (e.g. data is expected to be already centered).
+
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied; else, it may be overwritten.
+
+    compute_score : bool, default=False
+        If True, compute the log marginal likelihood at each iteration of the
+        optimization.
+
+    verbose : boolean, optional (DEFAULT = FALSE)
+        Verbose mode when fitting the model
+
+    Attributes
+    ----------
+    coef_ : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+
+    alpha_ : float
+       estimated precision of the noise
+
+    active_ : array, dtype = np.bool, shape = (n_features)
+       True for non-zero coefficients, False otherwise
+
+    lambda_ : array, shape = (n_features)
+       estimated precisions of the coefficients
+
+    sigma_ : array, shape = (n_features, n_features)
+        estimated covariance matrix of the weights, computed only
+        for non-zero coefficients
+
+    scores_ : array-like of shape (n_iter_+1,)
+        If computed_score is True, value of the log marginal likelihood (to be
+        maximized) at each iteration of the optimization.
+
+    References
+    ----------
+    [1] Fast marginal likelihood maximisation for sparse Bayesian models
+    (Tipping & Faul 2003) (http://www.miketipping.com/papers/met-fastsbl.pdf)
+    [2] Analysis of sparse Bayesian learning (Tipping & Faul 2001)
+        (http://www.miketipping.com/abstracts.htm#Faul:NIPS01)
+    '''
+
+    def __init__(self, n_iter=300, start=None, tol=1e-3, fit_intercept=True,
+                 normalize=False, copy_X=True, compute_score=False, verbose=False):
+        self.n_iter          = n_iter
+        self.start           = start
+        self.tol             = tol
+        self.scores_         = list()
+        self.fit_intercept   = fit_intercept
+        self.normalize       = normalize
+        self.copy_X          = copy_X
+        self.compute_score   = compute_score
+        self.verbose         = verbose
+
+    def _preprocess_data(self, X, y):
+        """Center and scale data.
+        Centers data to have mean zero along axis 0. If fit_intercept=False or
+        if the X is a sparse matrix, no centering is done, but normalization
+        can still be applied. The function returns the statistics necessary to
+        reconstruct the input data, which are X_offset, y_offset, X_scale, such
+        that the output
+            X = (X - X_offset) / X_scale
+        X_scale is the L2 norm of X - X_offset.
+        """
+
+        if self.copy_X:
+            X = X.copy(order='K')
+
+        y = np.asarray(y, dtype=X.dtype)
+
+        if self.fit_intercept:
+            X_offset = np.average(X, axis=0)
+            X -= X_offset
+            if self.normalize:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+                std = np.sqrt(np.sum(X**2, axis=0)/(len(X)-1))
+                X_scale[std != 0] = std[std != 0]
+                X /= X_scale
+            else:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            y_offset = np.mean(y)
+            y = y - y_offset
+        else:
+            X_offset = np.zeros(X.shape[1], dtype=X.dtype)
+            X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            if y.ndim == 1:
+                y_offset = X.dtype.type(0)
+            else:
+                y_offset = np.zeros(y.shape[1], dtype=X.dtype)
+
+        return X, y, X_offset, y_offset, X_scale
+
+    def fit(self, X, y):
+        '''
+        Fits ARD Regression with Sequential Sparse Bayes Algorithm.
+
+        Parameters
+        -----------
+        X: {array-like, sparse matrix} of size (n_samples, n_features)
+           Training data, matrix of explanatory variables
+
+        y: array-like of size [n_samples, n_features]
+           Target values
+
+        Returns
+        -------
+        self : object
+            Returns self.
+        '''
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+
+        X, y, X_mean, y_mean, X_std = self._preprocess_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean = y_mean
+        self._x_std = X_std
+
+        #  precompute X'*Y , X'*X for faster iterations & allocate memory for
+        #  sparsity & quality vectors
+        XY = np.dot(X.T, y)
+        XX = np.dot(X.T, X)
+        XXd = np.diag(XX)
+
+        #  initialise precision of noise & and coefficients
+        var_y = np.var(y)
+
+        # check that variance is non zero !!!
+        if var_y == 0:
+            beta = 1e-2
+            self.var_y = True
+        else:
+            beta = 1. / np.var(y)
+            self.var_y = False
+
+        A = np.PINF * np.ones(n_features)
+        active = np.zeros(n_features, dtype=np.bool)
+
+        if self.start is not None and not hasattr(self, 'active_'):
+            start = self.start
+            # start from a given start basis vector
+            proj = XY**2 / XXd
+            active[start] = True
+            A[start] = XXd[start]/(proj[start] - var_y)
+
+        else:
+            # in case of almost perfect multicollinearity between some features
+            # start from feature 0
+            if np.sum(XXd - X_mean**2 < np.finfo(np.float32).eps) > 0:
+                A[0] = np.finfo(np.float16).eps
+                active[0] = True
+
+            else:
+                # start from a single basis vector with largest projection on
+                # targets
+                proj = XY**2 / XXd
+                start = np.argmax(proj)
+                active[start] = True
+                A[start] = XXd[start]/(proj[start] - var_y +
+                                       np.finfo(np.float32).eps)
+
+        warning_flag = 0
+        scores_ = []
+        for i in range(self.n_iter):
+            # Handle variance zero
+            if self.var_y:
+                A[0] = y_mean
+                active[0] = True
+                converged = True
+                break
+
+            XXa = XX[active, :][:, active]
+            XYa = XY[active]
+            Aa = A[active]
+
+            # mean & covariance of posterior distribution
+            Mn, Ri, cholesky = self._posterior_dist(Aa, beta, XXa, XYa)
+            if cholesky:
+                Sdiag = np.sum(Ri**2, 0)
+            else:
+                Sdiag = np.copy(np.diag(Ri))
+                warning_flag += 1
+
+            # raise warning in case cholesky fails
+            if warning_flag == 1:
+                warnings.warn(("Cholesky decomposition failed! Algorithm uses "
+                               "pinvh, which is significantly slower. If you "
+                               "use RVR it is advised to change parameters of "
+                               "the kernel!"))
+
+            # compute quality & sparsity parameters
+            s, q, S, Q = self._sparsity_quality(XX, XXd, XY, XYa, Aa, Ri,
+                                                active, beta, cholesky)
+
+            # update precision parameter for noise distribution
+            rss = np.sum((y - np.dot(X[:, active], Mn))**2)
+
+            # if near perfect fit , then terminate
+            if (rss / n_samples/var_y) < self.tol:
+                warnings.warn('Early termination due to near perfect fit')
+                converged = True
+                break
+            beta = n_samples - np.sum(active) + np.sum(Aa * Sdiag)
+            beta /= rss
+            # beta /= (rss + np.finfo(np.float32).eps)
+
+            # update precision parameters of coefficients
+            A, converged = update_precisions(Q, S, q, s, A, active, self.tol,
+                                             n_samples, False)
+
+            if self.compute_score:
+                scores_.append(self.log_marginal_like(XXa, XYa, Aa, beta))
+
+            if self.verbose:
+                print(('Iteration: {0}, number of features '
+                       'in the model: {1}').format(i, np.sum(active)))
+
+            if converged or i == self.n_iter - 1:
+                if converged and self.verbose:
+                    print('Algorithm converged!')
+                break
+
+        # after last update of alpha & beta update parameters
+        # of posterior distribution
+        XXa, XYa, Aa = XX[active, :][:, active], XY[active], A[active]
+        Mn, Sn, cholesky = self._posterior_dist(Aa, beta, XXa, XYa, True)
+        self.coef_ = np.zeros(n_features)
+        self.coef_[active] = Mn
+        self.sigma_ = Sn
+        self.active_ = active
+        self.lambda_ = A
+        self.alpha_ = beta
+        self.converged = converged
+        if self.compute_score:
+            self.scores_ = np.array(scores_)
+
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+        return self
+
+    def log_marginal_like(self, XXa, XYa, Aa, beta):
+        """Computes the log of the marginal likelihood."""
+        N, M = XXa.shape
+        A = np.diag(Aa)
+
+        Mn, sigma_, cholesky = self._posterior_dist(Aa, beta, XXa, XYa,
+                                                    full_covar=True)
+
+        C = sigma_ + np.dot(np.dot(XXa.T, np.linalg.pinv(A)), XXa)
+
+        score = np.dot(np.dot(XYa.T, np.linalg.pinv(C)), XYa) +\
+            np.log(np.linalg.det(C)) + N * np.log(2 * np.pi)
+
+        return -0.5 * score
+
+    def predict(self, X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance based on
+        Ref.[1] Section 3.3.2.
+
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of
+                    predictive distribution)
+
+                var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+        References
+        ----------
+        [1] Bishop, C. M. (2006). Pattern recognition and machine learning.
+        springer.
+        '''
+
+        y_hat = np.dot(X, self.coef_) + self.intercept_
+
+        if return_std:
+            # Handle the zero variance case
+            if self.var_y:
+                return y_hat, np.zeros_like(y_hat)
+
+            if self.normalize:
+                X -= self._x_mean_[self.active_]
+                X /= self._x_std[self.active_]
+            var_hat = 1./self.alpha_
+            var_hat += np.sum(X.dot(self.sigma_) * X, axis=1)
+            std_hat = np.sqrt(var_hat)
+            return y_hat, std_hat
+        else:
+            return y_hat
+
+    def _posterior_dist(self, A, beta, XX, XY, full_covar=False):
+        '''
+        Calculates mean and covariance matrix of posterior distribution
+        of coefficients.
+        '''
+        # compute precision matrix for active features
+        Sinv = beta * XX
+        np.fill_diagonal(Sinv, np.diag(Sinv) + A)
+        cholesky = True
+
+        # try cholesky, if it fails go back to pinvh
+        try:
+            # find posterior mean : R*R.T*mean = beta*X.T*Y
+            # solve(R*z = beta*X.T*Y) =>find z=> solve(R.T*mean = z)=>find mean
+            R = np.linalg.cholesky(Sinv)
+            Z = solve_triangular(R, beta*XY, check_finite=True, lower=True)
+            Mn = solve_triangular(R.T, Z, check_finite=True, lower=False)
+
+            # invert lower triangular matrix from cholesky decomposition
+            Ri = solve_triangular(R, np.eye(A.shape[0]), check_finite=False,
+                                  lower=True)
+            if full_covar:
+                Sn = np.dot(Ri.T, Ri)
+                return Mn, Sn, cholesky
+            else:
+                return Mn, Ri, cholesky
+        except LinAlgError:
+            cholesky = False
+            Sn = pinvh(Sinv)
+            Mn = beta*np.dot(Sinv, XY)
+            return Mn, Sn, cholesky
+
+    def _sparsity_quality(self, XX, XXd, XY, XYa, Aa, Ri, active, beta, cholesky):
+        '''
+        Calculates sparsity and quality parameters for each feature
+
+        Theoretical Note:
+        -----------------
+        Here we used Woodbury Identity for inverting covariance matrix
+        of target distribution
+        C    = 1/beta + 1/alpha * X' * X
+        C^-1 = beta - beta^2 * X * Sn * X'
+        '''
+        bxy = beta*XY
+        bxx = beta*XXd
+        if cholesky:
+            # here Ri is inverse of lower triangular matrix obtained from
+            # cholesky decomp
+            xxr = np.dot(XX[:, active], Ri.T)
+            rxy = np.dot(Ri, XYa)
+            S = bxx - beta**2 * np.sum(xxr**2, axis=1)
+            Q = bxy - beta**2 * np.dot(xxr, rxy)
+        else:
+            # here Ri is covariance matrix
+            XXa = XX[:, active]
+            XS = np.dot(XXa, Ri)
+            S = bxx - beta**2 * np.sum(XS*XXa, 1)
+            Q = bxy - beta**2 * np.dot(XS, XYa)
+        # Use following:
+        # (EQ 1) q = A*Q/(A - S) ; s = A*S/(A-S)
+        # so if A = np.PINF q = Q, s = S
+        qi = np.copy(Q)
+        si = np.copy(S)
+        # If A is not np.PINF, then it should be 'active' feature => use (EQ 1)
+        Qa, Sa = Q[active], S[active]
+        qi[active] = Aa * Qa / (Aa - Sa)
+        si[active] = Aa * Sa / (Aa - Sa)
+
+        return [si, qi, S, Q]
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/reg_fast_laplace.py b/examples/analytical-function/bayesvalidrox/surrogate_models/reg_fast_laplace.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fdcb5cf6e93c396d32eae2b0aad87a194a9cba4
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/reg_fast_laplace.py
@@ -0,0 +1,452 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import numpy as np
+from sklearn.utils import as_float_array
+from sklearn.model_selection import KFold
+
+
+class RegressionFastLaplace():
+    '''
+    Sparse regression with Bayesian Compressive Sensing as described in Alg. 1
+    (Fast Laplace) of Ref.[1], which updated formulas from [2].
+
+    sigma2: noise precision (sigma^2)
+    nu fixed to 0
+
+    uqlab/lib/uq_regression/BCS/uq_bsc.m
+
+    Parameters
+    ----------
+    n_iter: int, optional (DEFAULT = 1000)
+        Maximum number of iterations
+
+    tol: float, optional (DEFAULT = 1e-7)
+        If absolute change in precision parameter for weights is below
+        threshold algorithm terminates.
+
+    fit_intercept : boolean, optional (DEFAULT = True)
+        whether to calculate the intercept for this model. If set
+        to false, no intercept will be used in calculations
+        (e.g. data is expected to be already centered).
+
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied; else, it may be overwritten.
+
+    verbose : boolean, optional (DEFAULT = FALSE)
+        Verbose mode when fitting the model
+
+    Attributes
+    ----------
+    coef_ : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+
+    alpha_ : float
+       estimated precision of the noise
+
+    active_ : array, dtype = np.bool, shape = (n_features)
+       True for non-zero coefficients, False otherwise
+
+    lambda_ : array, shape = (n_features)
+       estimated precisions of the coefficients
+
+    sigma_ : array, shape = (n_features, n_features)
+        estimated covariance matrix of the weights, computed only
+        for non-zero coefficients
+
+    References
+    ----------
+    [1] Babacan, S. D., Molina, R., & Katsaggelos, A. K. (2009). Bayesian
+        compressive sensing using Laplace priors. IEEE Transactions on image
+        processing, 19(1), 53-63.
+    [2] Fast marginal likelihood maximisation for sparse Bayesian models
+        (Tipping & Faul 2003).
+        (http://www.miketipping.com/papers/met-fastsbl.pdf)
+    '''
+
+    def __init__(self, n_iter=1000, n_Kfold=10, tol=1e-7, fit_intercept=False,
+                 bias_term=True, copy_X=True, verbose=False):
+        self.n_iter = n_iter
+        self.n_Kfold = n_Kfold
+        self.tol = tol
+        self.fit_intercept = fit_intercept
+        self.bias_term = bias_term
+        self.copy_X = copy_X
+        self.verbose = verbose
+
+    def _center_data(self, X, y):
+        ''' Centers data'''
+        X = as_float_array(X, copy = self.copy_X)
+
+        # normalisation should be done in preprocessing!
+        X_std = np.ones(X.shape[1], dtype=X.dtype)
+        if self.fit_intercept:
+            X_mean = np.average(X, axis=0)
+            y_mean = np.average(y, axis=0)
+            X -= X_mean
+            y -= y_mean
+        else:
+            X_mean = np.zeros(X.shape[1], dtype=X.dtype)
+            y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype)
+        return X, y, X_mean, y_mean, X_std
+
+    def fit(self, X, y):
+
+        k_fold = KFold(n_splits=self.n_Kfold)
+
+        varY = np.var(y, ddof=1) if np.var(y, ddof=1) != 0 else 1.0
+        sigma2s = len(y)*varY*(10**np.linspace(-16, -1, self.n_Kfold))
+
+        errors = np.zeros((len(sigma2s), self.n_Kfold))
+        for s, sigma2 in enumerate(sigma2s):
+            for k, (train, test) in enumerate(k_fold.split(X, y)):
+                self.fit_(X[train], y[train], sigma2)
+                errors[s, k] = np.linalg.norm(
+                    y[test] - self.predict(X[test])
+                    )**2/len(test)
+
+        KfCVerror = np.sum(errors, axis=1)/self.n_Kfold/varY
+        i_minCV = np.argmin(KfCVerror)
+
+        self.kfoldCVerror = np.min(KfCVerror)
+
+        return self.fit_(X, y, sigma2s[i_minCV])
+
+    def fit_(self, X, y, sigma2):
+
+        N, P = X.shape
+        # n_samples, n_features = X.shape
+
+        X, y, X_mean, y_mean, X_std = self._center_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean = y_mean
+        self._x_std = X_std
+
+        # check that variance is non zero !!!
+        if np.var(y) == 0:
+            self.var_y = True
+        else:
+            self.var_y = False
+        beta = 1./sigma2
+
+        #  precompute X'*Y , X'*X for faster iterations & allocate memory for
+        #  sparsity & quality vectors X=Psi
+        PsiTY = np.dot(X.T, y)
+        PsiTPsi = np.dot(X.T, X)
+        XXd = np.diag(PsiTPsi)
+
+        # initialize with constant regressor, or if that one does not exist,
+        # with the one that has the largest correlation with Y
+        ind_global_to_local = np.zeros(P, dtype=np.int32)
+
+        # identify constant regressors
+        constidx = np.where(~np.diff(X, axis=0).all(axis=0))[0]
+
+        if self.bias_term and constidx.size != 0:
+            ind_start = constidx[0]
+            ind_global_to_local[ind_start] = True
+        else:
+            # start from a single basis vector with largest projection on
+            # targets
+            proj = np.divide(np.square(PsiTY), XXd)
+            ind_start = np.argmax(proj)
+            ind_global_to_local[ind_start] = True
+
+        num_active = 1
+        active_indices = [ind_start]
+        deleted_indices = []
+        bcs_path = [ind_start]
+        gamma = np.zeros(P)
+        # for the initial value of gamma(ind_start), use the RVM formula
+        #   gamma = (q^2 - s) / (s^2)
+        # and the fact that initially s = S = beta*Psi_i'*Psi_i and q = Q =
+        # beta*Psi_i'*Y
+        gamma[ind_start] = np.square(PsiTY[ind_start])
+        gamma[ind_start] -= sigma2 * PsiTPsi[ind_start, ind_start]
+        gamma[ind_start] /= np.square(PsiTPsi[ind_start, ind_start])
+
+        Sigma = 1. / (beta * PsiTPsi[ind_start, ind_start]
+                      + 1./gamma[ind_start])
+
+        mu = Sigma * PsiTY[ind_start] * beta
+        tmp1 = beta * PsiTPsi[ind_start]
+        S = beta * np.diag(PsiTPsi).T - Sigma * np.square(tmp1)
+        Q = beta * PsiTY.T - mu*(tmp1)
+
+        tmp2 = np.ones(P)  # alternative computation for the initial s,q
+        q0tilde = PsiTY[ind_start]
+        s0tilde = PsiTPsi[ind_start, ind_start]
+        tmp2[ind_start] = s0tilde / (q0tilde**2) / beta
+        s = np.divide(S, tmp2)
+        q = np.divide(Q, tmp2)
+        Lambda = 2*(num_active - 1) / np.sum(gamma)
+
+        Delta_L_max = []
+        for i in range(self.n_iter):
+            # Handle variance zero
+            if self.var_y:
+                mu = np.mean(y)
+                break
+
+            if self.verbose:
+                print('    lambda = {0:.6e}\n'.format(Lambda))
+
+            # Calculate the potential updated value of each gamma[i]
+            if Lambda == 0.0:  # RVM
+                gamma_potential = np.multiply((
+                    (q**2 - s) > Lambda),
+                    np.divide(q**2 - s, s**2)
+                    )
+            else:
+                a = Lambda * s**2
+                b = s**2 + 2*Lambda*s
+                c = Lambda + s - q**2
+                gamma_potential = np.multiply(
+                    (c < 0), np.divide(
+                        -b + np.sqrt(b**2 - 4*np.multiply(a, c)), 2*a)
+                    )
+
+            l_gamma = - np.log(np.absolute(1 + np.multiply(gamma, s)))
+            l_gamma += np.divide(np.multiply(q**2, gamma),
+                                 (1 + np.multiply(gamma, s)))
+            l_gamma -= Lambda*gamma  # omitted the factor 1/2
+
+            # Contribution of each updated gamma(i) to L(gamma)
+            l_gamma_potential = - np.log(
+                np.absolute(1 + np.multiply(gamma_potential, s))
+                )
+            l_gamma_potential += np.divide(
+                np.multiply(q**2, gamma_potential),
+                (1 + np.multiply(gamma_potential, s))
+                )
+            # omitted the factor 1/2
+            l_gamma_potential -= Lambda*gamma_potential
+
+            # Check how L(gamma) would change if we replaced gamma(i) by the
+            # updated gamma_potential(i), for each i separately
+            Delta_L_potential = l_gamma_potential - l_gamma
+
+            # deleted indices should not be chosen again
+            if len(deleted_indices) != 0:
+                values = -np.inf * np.ones(len(deleted_indices))
+                Delta_L_potential[deleted_indices] = values
+
+            Delta_L_max.append(np.nanmax(Delta_L_potential))
+            ind_L_max = np.nanargmax(Delta_L_potential)
+
+            # in case there is only 1 regressor in the model and it would now
+            # be deleted
+            if len(active_indices) == 1 and ind_L_max == active_indices[0] \
+               and gamma_potential[ind_L_max] == 0.0:
+                Delta_L_potential[ind_L_max] = -np.inf
+                Delta_L_max[i] = np.max(Delta_L_potential)
+                ind_L_max = np.argmax(Delta_L_potential)
+
+            # If L did not change significantly anymore, break
+            if Delta_L_max[i] <= 0.0 or\
+                    (i > 0 and all(np.absolute(Delta_L_max[i-1:])
+                                   < sum(Delta_L_max)*self.tol)) or \
+                    (i > 0 and all(np.diff(bcs_path)[i-1:] == 0.0)):
+                if self.verbose:
+                    print('Increase in L: {0:.6e} (eta = {1:.3e})\
+                          -- break\n'.format(Delta_L_max[i], self.tol))
+                break
+
+            # Print information
+            if self.verbose:
+                print('    Delta L = {0:.6e} \n'.format(Delta_L_max[i]))
+
+            what_changed = int(gamma[ind_L_max] == 0.0)
+            what_changed -= int(gamma_potential[ind_L_max] == 0.0)
+
+            # Print information
+            if self.verbose:
+                if what_changed < 0:
+                    print(f'{i+1} - Remove regressor #{ind_L_max+1}..\n')
+                elif what_changed == 0:
+                    print(f'{i+1} - Recompute regressor #{ind_L_max+1}..\n')
+                else:
+                    print(f'{i+1} - Add regressor #{ind_L_max+1}..\n')
+
+            # --- Update all quantities ----
+            if what_changed == 1:
+                # adding a regressor
+
+                # update gamma
+                gamma[ind_L_max] = gamma_potential[ind_L_max]
+
+                Sigma_ii = 1.0 / (1.0/gamma[ind_L_max] + S[ind_L_max])
+                try:
+                    x_i = np.matmul(
+                        Sigma, PsiTPsi[active_indices, ind_L_max].reshape(-1, 1)
+                        )
+                except ValueError:
+                    x_i = Sigma * PsiTPsi[active_indices, ind_L_max]
+                tmp_1 = - (beta * Sigma_ii) * x_i
+                Sigma = np.vstack(
+                    (np.hstack(((beta**2 * Sigma_ii) * np.dot(x_i, x_i.T)
+                                + Sigma, tmp_1)), np.append(tmp_1.T, Sigma_ii))
+                    )
+                mu_i = Sigma_ii * Q[ind_L_max]
+                mu = np.vstack((mu - (beta * mu_i) * x_i, mu_i))
+
+                tmp2_1 = PsiTPsi[:, ind_L_max] - beta * np.squeeze(
+                    np.matmul(PsiTPsi[:, active_indices], x_i)
+                    )
+                if i == 0:
+                    tmp2_1[0] /= 2
+                tmp2 = beta * tmp2_1.T
+                S = S - Sigma_ii * np.square(tmp2)
+                Q = Q - mu_i * tmp2
+
+                num_active += 1
+                ind_global_to_local[ind_L_max] = num_active
+                active_indices.append(ind_L_max)
+                bcs_path.append(ind_L_max)
+
+            elif what_changed == 0:
+                # recomputation
+                # zero if regressor has not been chosen yet
+                if not ind_global_to_local[ind_L_max]:
+                    raise Exception('Cannot recompute index{0} -- not yet\
+                                    part of the model!'.format(ind_L_max))
+                Sigma = np.atleast_2d(Sigma)
+                mu = np.atleast_2d(mu)
+                gamma_i_new = gamma_potential[ind_L_max]
+                gamma_i_old = gamma[ind_L_max]
+                # update gamma
+                gamma[ind_L_max] = gamma_potential[ind_L_max]
+
+                # index of regressor in Sigma
+                local_ind = ind_global_to_local[ind_L_max]-1
+
+                kappa_i = (1.0/gamma_i_new - 1.0/gamma_i_old)
+                kappa_i = 1.0 / kappa_i
+                kappa_i += Sigma[local_ind, local_ind]
+                kappa_i = 1 / kappa_i
+                Sigma_i_col = Sigma[:, local_ind]
+
+                Sigma = Sigma - kappa_i * (Sigma_i_col * Sigma_i_col.T)
+                mu_i = mu[local_ind]
+                mu = mu - (kappa_i * mu_i) * Sigma_i_col[:, None]
+
+                tmp1 = beta * np.dot(
+                    Sigma_i_col.reshape(1, -1), PsiTPsi[active_indices])[0]
+                S = S + kappa_i * np.square(tmp1)
+                Q = Q + (kappa_i * mu_i) * tmp1
+
+                # no change in active_indices or ind_global_to_local
+                bcs_path.append(ind_L_max + 0.1)
+
+            elif what_changed == -1:
+                gamma[ind_L_max] = 0
+
+                # index of regressor in Sigma
+                local_ind = ind_global_to_local[ind_L_max]-1
+
+                Sigma_ii_inv = 1. / Sigma[local_ind, local_ind]
+                Sigma_i_col = Sigma[:, local_ind]
+
+                Sigma = Sigma - Sigma_ii_inv * (Sigma_i_col * Sigma_i_col.T)
+
+                Sigma = np.delete(
+                    np.delete(Sigma, local_ind, axis=0), local_ind, axis=1)
+
+                mu = mu - (mu[local_ind] * Sigma_ii_inv) * Sigma_i_col[:, None]
+                mu = np.delete(mu, local_ind, axis=0)
+
+                tmp1 = beta * np.dot(Sigma_i_col, PsiTPsi[active_indices])
+                S = S + Sigma_ii_inv * np.square(tmp1)
+                Q = Q + (mu_i * Sigma_ii_inv) * tmp1
+
+                num_active -= 1
+                ind_global_to_local[ind_L_max] = 0.0
+                v = ind_global_to_local[ind_global_to_local > local_ind] - 1
+                ind_global_to_local[ind_global_to_local > local_ind] = v
+                del active_indices[local_ind]
+                deleted_indices.append(ind_L_max)
+                # and therefore ineligible
+                bcs_path.append(-ind_L_max)
+
+            # same for all three cases
+            tmp3 = 1 - np.multiply(gamma, S)
+            s = np.divide(S, tmp3)
+            q = np.divide(Q, tmp3)
+
+            # Update lambda
+            Lambda = 2*(num_active - 1) / np.sum(gamma)
+
+        # Prepare the result object
+        self.coef_ = np.zeros(P)
+        self.coef_[active_indices] = np.squeeze(mu)
+        self.sigma_ = Sigma
+        self.active_ = active_indices
+        self.gamma = gamma
+        self.Lambda = Lambda
+        self.beta = beta
+        self.bcs_path = bcs_path
+
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+
+        return self
+
+    def predict(self, X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance based on
+        Ref.[1] Section 3.3.2.
+
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of
+                    predictive distribution)
+
+                var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+
+        References
+        ----------
+        [1] Bishop, C. M. (2006). Pattern recognition and machine learning.
+        springer.
+        '''
+        y_hat = np.dot(X, self.coef_) + self.intercept_
+
+        if return_std:
+            # Handle the zero variance case
+            if self.var_y:
+                return y_hat, np.zeros_like(y_hat)
+
+            var_hat = 1./self.beta
+            var_hat += np.sum(X.dot(self.sigma_) * X, axis=1)
+            std_hat = np.sqrt(var_hat)
+            return y_hat, std_hat
+        else:
+            return y_hat
+
+# l2norm = 0.0
+# for idx in range(10):
+#     sigma2 = np.genfromtxt('./test/sigma2_{0}.csv'.format(idx+1), delimiter=',')
+#     Psi_train = np.genfromtxt('./test/Psi_train_{0}.csv'.format(idx+1), delimiter=',')
+#     Y_train = np.genfromtxt('./test/Y_train_{0}.csv'.format(idx+1))
+#     Psi_test = np.genfromtxt('./test/Psi_test_{0}.csv'.format(idx+1), delimiter=',')
+#     Y_test = np.genfromtxt('./test/Y_test_{0}.csv'.format(idx+1))
+
+#     clf = RegressionFastLaplace(verbose=True)
+#     clf.fit_(Psi_train, Y_train, sigma2)
+#     coeffs_fold = np.genfromtxt('./test/coeffs_fold_{0}.csv'.format(idx+1))
+#     print("coeffs error: {0:.4g}".format(np.linalg.norm(clf.coef_ - coeffs_fold)))
+#     l2norm += np.linalg.norm(Y_test - clf.predict(Psi_test))**2/len(Y_test)
+#     print("l2norm error: {0:.4g}".format(l2norm))
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/sequential_design.py b/examples/analytical-function/bayesvalidrox/surrogate_models/sequential_design.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc81dcd4529ca0708dfba47385aef4415992eb3e
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/sequential_design.py
@@ -0,0 +1,2187 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jan 28 09:21:18 2022
+
+@author: farid
+"""
+import numpy as np
+from scipy import stats, signal, linalg, sparse
+from scipy.spatial import distance
+from copy import deepcopy, copy
+from tqdm import tqdm
+import scipy.optimize as opt
+from sklearn.metrics import mean_squared_error
+import multiprocessing
+import matplotlib.pyplot as plt
+import sys
+import os
+import gc
+import seaborn as sns
+from joblib import Parallel, delayed
+import resource
+from .exploration import Exploration
+
+
+class SeqDesign():
+    """ Sequential experimental design
+    This class provieds method for trainig the meta-model in an iterative
+    manners.
+    The main method to execute the task is `train_seq_design`, which
+      recieves a model object and returns the trained metamodel.
+    """
+
+    # -------------------------------------------------------------------------
+    def train_seq_design(self, MetaModel):
+        """
+        Starts the adaptive sequential design for refining the surrogate model
+        by selecting training points in a sequential manner.
+
+        Parameters
+        ----------
+        Model : object
+            An object containing all model specifications.
+
+        Returns
+        -------
+        MetaModel : object
+            Meta model object.
+
+        """
+        # MetaModel = self
+        Model = MetaModel.ModelObj
+        self.MetaModel = MetaModel
+        self.Model = Model
+
+        # Initialization
+        MetaModel.SeqModifiedLOO = {}
+        MetaModel.seqValidError = {}
+        MetaModel.SeqBME = {}
+        MetaModel.SeqKLD = {}
+        MetaModel.SeqDistHellinger = {}
+        MetaModel.seqRMSEMean = {}
+        MetaModel.seqRMSEStd = {}
+        MetaModel.seqMinDist = []
+        pce = True if MetaModel.meta_model_type.lower() != 'gpe' else False
+        mc_ref = True if bool(Model.mc_reference) else False
+        if mc_ref:
+            Model.read_mc_reference()
+
+        if not hasattr(MetaModel, 'valid_likelihoods'):
+            MetaModel.valid_samples = []
+            MetaModel.valid_model_runs = []
+            MetaModel.valid_likelihoods = []
+
+        # Get the parameters
+        max_n_samples = MetaModel.ExpDesign.n_max_samples
+        mod_LOO_threshold = MetaModel.ExpDesign.mod_LOO_threshold
+        n_canddidate = MetaModel.ExpDesign.n_canddidate
+        post_snapshot = MetaModel.ExpDesign.post_snapshot
+        n_replication = MetaModel.ExpDesign.n_replication
+        util_func = MetaModel.ExpDesign.util_func
+        output_name = Model.Output.names
+        validError = None
+        # Handle if only one UtilityFunctions is provided
+        if not isinstance(util_func, list):
+            util_func = [MetaModel.ExpDesign.util_func]
+
+        # Read observations or MCReference
+        if len(Model.observations) != 0 or Model.meas_file is not None:
+            self.observations = Model.read_observation()
+            obs_data = self.observations
+        else:
+            obs_data = []
+            TotalSigma2 = {}
+        # ---------- Initial MetaModel ----------
+        initMetaModel = deepcopy(MetaModel)
+
+        # Validation error if validation set is provided.
+        if len(MetaModel.valid_model_runs) != 0:
+            init_rmse, init_valid_error = self.__validError(initMetaModel)
+            init_valid_error = list(init_valid_error.values())
+        else:
+            init_rmse = None
+
+        # Check if discrepancy is provided
+        if len(obs_data) != 0 and hasattr(MetaModel, 'Discrepancy'):
+            TotalSigma2 = MetaModel.Discrepancy.parameters
+
+            # Calculate the initial BME
+            out = self.__BME_Calculator(
+                initMetaModel, obs_data, TotalSigma2, init_rmse)
+            init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out
+            print(f"\nInitial BME: {init_BME:.2f}")
+            print(f"Initial KLD: {init_KLD:.2f}")
+
+            # Posterior snapshot (initial)
+            if post_snapshot:
+                parNames = MetaModel.ExpDesign.par_names
+                print('Posterior snapshot (initial) is being plotted...')
+                self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init')
+
+        # Check the convergence of the Mean & Std
+        if mc_ref and pce:
+            init_rmse_mean, init_rmse_std = self.__error_Mean_Std()
+            print(f"Initial Mean and Std error: {init_rmse_mean},"
+                  f" {init_rmse_std}")
+
+        # Read the initial experimental design
+        Xinit = initMetaModel.ExpDesign.X
+        init_n_samples = len(MetaModel.ExpDesign.X)
+        initYprev = initMetaModel.ModelOutputDict
+        initLCerror = initMetaModel.LCerror
+        n_itrs = max_n_samples - init_n_samples
+
+        # Read the initial ModifiedLOO
+        if pce:
+            Scores_all, varExpDesignY = [], []
+            for out_name in output_name:
+                y = initMetaModel.ExpDesign.Y[out_name]
+                Scores_all.append(list(
+                    initMetaModel.score_dict['b_1'][out_name].values()))
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    pca = MetaModel.pca['b_1'][out_name]
+                    components = pca.transform(y)
+                    varExpDesignY.append(np.var(components, axis=0))
+                else:
+                    varExpDesignY.append(np.var(y, axis=0))
+
+            Scores = [item for sublist in Scores_all for item in sublist]
+            weights = [item for sublist in varExpDesignY for item in sublist]
+            init_mod_LOO = [np.average([1-score for score in Scores],
+                                       weights=weights)]
+
+        prevMetaModel_dict = {}
+        # Replicate the sequential design
+        for repIdx in range(n_replication):
+            print(f'\n>>>> Replication: {repIdx+1}<<<<')
+
+            # To avoid changes ub original aPCE object
+            MetaModel.ExpDesign.X = Xinit
+            MetaModel.ExpDesign.Y = initYprev
+            MetaModel.LCerror = initLCerror
+
+            for util_f in util_func:
+                print(f'\n>>>> Utility Function: {util_f} <<<<')
+                # To avoid changes ub original aPCE object
+                MetaModel.ExpDesign.X = Xinit
+                MetaModel.ExpDesign.Y = initYprev
+                MetaModel.LCerror = initLCerror
+
+                # Set the experimental design
+                Xprev = Xinit
+                total_n_samples = init_n_samples
+                Yprev = initYprev
+
+                Xfull = []
+                Yfull = []
+
+                # Store the initial ModifiedLOO
+                if pce:
+                    print("\nInitial ModifiedLOO:", init_mod_LOO)
+                    SeqModifiedLOO = np.array(init_mod_LOO)
+
+                if len(MetaModel.valid_model_runs) != 0:
+                    SeqValidError = np.array(init_valid_error)
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    SeqBME = np.array([init_BME])
+                    SeqKLD = np.array([init_KLD])
+                    SeqDistHellinger = np.array([init_dist_hellinger])
+
+                if mc_ref and pce:
+                    seqRMSEMean = np.array([init_rmse_mean])
+                    seqRMSEStd = np.array([init_rmse_std])
+
+                # ------- Start Sequential Experimental Design -------
+                postcnt = 1
+                for itr_no in range(1, n_itrs+1):
+                    print(f'\n>>>> Iteration number {itr_no} <<<<')
+
+                    # Save the metamodel prediction before updating
+                    prevMetaModel_dict[itr_no] = deepcopy(MetaModel)
+                    if itr_no > 1:
+                        pc_model = prevMetaModel_dict[itr_no-1]
+                        self._y_hat_prev, _ = pc_model.eval_metamodel(
+                            samples=Xfull[-1].reshape(1, -1))
+
+                    # Optimal Bayesian Design
+                    m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    MetaModel.ExpDesignFlag = 'sequential'
+                    Xnew, updatedPrior = self.opt_SeqDesign(TotalSigma2,
+                                                            n_canddidate,
+                                                            util_f)
+                    m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    S = np.min(distance.cdist(Xinit, Xnew, 'euclidean'))
+                    MetaModel.seqMinDist.append(S)
+                    print(f"\nmin Dist from OldExpDesign: {S:2f}")
+                    print("\n")
+
+                    # Evaluate the full model response at the new sample
+                    Ynew, _ = Model.run_model_parallel(
+                        Xnew, prevRun_No=total_n_samples
+                        )
+                    total_n_samples += Xnew.shape[0]
+                    # ------ Plot the surrogate model vs Origninal Model ------
+                    if hasattr(MetaModel, 'adapt_verbose') and \
+                       MetaModel.adapt_verbose:
+                        from .adaptPlot import adaptPlot
+                        y_hat, std_hat = MetaModel.eval_metamodel(samples=Xnew)
+                        adaptPlot(MetaModel, Ynew, y_hat, std_hat, plotED=False)
+
+                    # -------- Retrain the surrogate model -------
+                    # Extend new experimental design
+                    Xfull = np.vstack((Xprev, Xnew))
+
+                    # Updating experimental design Y
+                    for out_name in output_name:
+                        Yfull = np.vstack((Yprev[out_name], Ynew[out_name]))
+                        MetaModel.ModelOutputDict[out_name] = Yfull
+
+                    # Pass new design to the metamodel object
+                    MetaModel.ExpDesign.sampling_method = 'user'
+                    MetaModel.ExpDesign.X = Xfull
+                    MetaModel.ExpDesign.Y = MetaModel.ModelOutputDict
+
+                    # Save the Experimental Design for next iteration
+                    Xprev = Xfull
+                    Yprev = MetaModel.ModelOutputDict
+
+                    # Pass the new prior as the input
+                    MetaModel.input_obj.poly_coeffs_flag = False
+                    if updatedPrior is not None:
+                        MetaModel.input_obj.poly_coeffs_flag = True
+                        print("updatedPrior:", updatedPrior.shape)
+                        # Arbitrary polynomial chaos
+                        for i in range(updatedPrior.shape[1]):
+                            MetaModel.input_obj.Marginals[i].dist_type = None
+                            x = updatedPrior[:, i]
+                            MetaModel.input_obj.Marginals[i].raw_data = x
+
+                    # Train the surrogate model for new ExpDesign
+                    MetaModel.train_norm_design(parallel=False)
+                    m_3 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+
+                    # -------- Evaluate the retrained surrogate model -------
+                    # Extract Modified LOO from Output
+                    if pce:
+                        Scores_all, varExpDesignY = [], []
+                        for out_name in output_name:
+                            y = MetaModel.ExpDesign.Y[out_name]
+                            Scores_all.append(list(
+                                MetaModel.score_dict['b_1'][out_name].values()))
+                            if MetaModel.dim_red_method.lower() == 'pca':
+                                pca = MetaModel.pca['b_1'][out_name]
+                                components = pca.transform(y)
+                                varExpDesignY.append(np.var(components,
+                                                            axis=0))
+                            else:
+                                varExpDesignY.append(np.var(y, axis=0))
+                        Scores = [item for sublist in Scores_all for item
+                                  in sublist]
+                        weights = [item for sublist in varExpDesignY for item
+                                   in sublist]
+                        ModifiedLOO = [np.average(
+                            [1-score for score in Scores], weights=weights)]
+
+                        print('\n')
+                        print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO)
+                        print('\n')
+
+                    # Compute the validation error
+                    if len(MetaModel.valid_model_runs) != 0:
+                        rmse, validError = self.__validError(MetaModel)
+                        ValidError = list(validError.values())
+                    else:
+                        rmse = None
+
+                    # Store updated ModifiedLOO
+                    if pce:
+                        SeqModifiedLOO = np.vstack(
+                            (SeqModifiedLOO, ModifiedLOO))
+                        if len(MetaModel.valid_model_runs) != 0:
+                            SeqValidError = np.vstack(
+                                (SeqValidError, ValidError))
+                    m_4 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    # -------- Caclulation of BME as accuracy metric -------
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        # Calculate the initial BME
+                        out = self.__BME_Calculator(MetaModel, obs_data,
+                                                    TotalSigma2, rmse)
+                        BME, KLD, Posterior, likes, DistHellinger = out
+                        print('\n')
+                        print(f"Updated BME: {BME:.2f}")
+                        print(f"Updated KLD: {KLD:.2f}")
+                        print('\n')
+
+                        # Plot some snapshots of the posterior
+                        step_snapshot = MetaModel.ExpDesign.step_snapshot
+                        if post_snapshot and postcnt % step_snapshot == 0:
+                            parNames = MetaModel.ExpDesign.par_names
+                            print('Posterior snapshot is being plotted...')
+                            self.__posteriorPlot(Posterior, parNames,
+                                                 f'SeqPosterior_{postcnt}')
+                        postcnt += 1
+                    m_5 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+
+                    # Check the convergence of the Mean&Std
+                    if mc_ref and pce:
+                        print('\n')
+                        RMSE_Mean, RMSE_std = self.__error_Mean_Std()
+                        print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, "
+                              f"{RMSE_std:.2f}")
+                        print('\n')
+
+                    # Store the updated BME & KLD
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        SeqBME = np.vstack((SeqBME, BME))
+                        SeqKLD = np.vstack((SeqKLD, KLD))
+                        SeqDistHellinger = np.vstack((SeqDistHellinger,
+                                                      DistHellinger))
+                    if mc_ref and pce:
+                        seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean))
+                        seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std))
+
+                    if pce and any(LOO < mod_LOO_threshold
+                                   for LOO in ModifiedLOO):
+                        break
+
+                    print(f"Memory itr {itr_no}: I: {m_2-m_1:.2f} MB")
+                    print(f"Memory itr {itr_no}: II: {m_3-m_2:.2f} MB")
+                    print(f"Memory itr {itr_no}: III: {m_4-m_3:.2f} MB")
+                    print(f"Memory itr {itr_no}: IV: {m_5-m_4:.2f} MB")
+                    m_6 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    print(f"Memory itr {itr_no}: total: {m_6:.2f} MB")
+
+                    # Clean up
+                    if len(obs_data) != 0:
+                        del out
+                    gc.collect()
+                    print()
+                    print('-'*50)
+                    print()
+
+                # Store updated ModifiedLOO and BME in dictonary
+                strKey = f'{util_f}_rep_{repIdx+1}'
+                if pce:
+                    MetaModel.SeqModifiedLOO[strKey] = SeqModifiedLOO
+                if len(MetaModel.valid_model_runs) != 0:
+                    MetaModel.seqValidError[strKey] = SeqValidError
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    MetaModel.SeqBME[strKey] = SeqBME
+                    MetaModel.SeqKLD[strKey] = SeqKLD
+                if len(MetaModel.valid_likelihoods) != 0:
+                    MetaModel.SeqDistHellinger[strKey] = SeqDistHellinger
+                if mc_ref and pce:
+                    MetaModel.seqRMSEMean[strKey] = seqRMSEMean
+                    MetaModel.seqRMSEStd[strKey] = seqRMSEStd
+
+        return MetaModel
+
+    # -------------------------------------------------------------------------
+    def util_VarBasedDesign(self, X_can, index, util_func='Entropy'):
+        """
+        Computes the exploitation scores based on:
+        active learning MacKay(ALM) and active learning Cohn (ALC)
+        Paper: Sequential Design with Mutual Information for Computer
+        Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas
+        (2016)
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        index : int
+            Model output index.
+        UtilMethod : string, optional
+            Exploitation utility function. The default is 'Entropy'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+        MetaModel = self.MetaModel
+        ED_X = MetaModel.ExpDesign.X
+        out_dict_y = MetaModel.ExpDesign.Y
+        out_names = MetaModel.ModelObj.Output.names
+
+        # Run the Metamodel for the candidate
+        X_can = X_can.reshape(1, -1)
+        Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can)
+
+        if util_func.lower() == 'alm':
+            # ----- Entropy/MMSE/active learning MacKay(ALM)  -----
+            # Compute perdiction variance of the old model
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            varPCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                varPCE[KeyIdx] = np.max(canPredVar[key], axis=1)
+            score = np.max(varPCE, axis=0)
+
+        elif util_func.lower() == 'eigf':
+            # ----- Expected Improvement for Global fit -----
+            # Find closest EDX to the candidate
+            distances = distance.cdist(ED_X, X_can, 'euclidean')
+            index = np.argmin(distances)
+
+            # Compute perdiction error and variance of the old model
+            predError = {key: Y_PC_can[key] for key in out_names}
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            # Compute perdiction error and variance of the old model
+            # Eq (5) from Liu et al.(2018)
+            EIGF_PCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                residual = predError[key] - out_dict_y[key][int(index)]
+                var = canPredVar[key]
+                EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1)
+            score = np.max(EIGF_PCE, axis=0)
+
+        return -1 * score   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianActiveDesign(self, X_can, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian active design criterion (var).
+
+        It is based on the following paper:
+        Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak.
+        "Bayesian3 active learning for the gaussian process emulator using
+        information theory." Entropy 22, no. 8 (2020): 890.
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            BAL design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_mean_can, Y_std_can = self.MetaModel.eval_metamodel(
+            samples=np.array([X_can])
+            )
+
+        # Get the data
+        obs_data = self.observations
+        n_obs = self.Model.n_obs
+        # TODO: Analytical DKL
+        # Sample a distribution for a normal dist
+        # with Y_mean_can as the mean and Y_std_can as std.
+
+        # priorMean, priorSigma2, Obs = np.empty((0)),np.empty((0)),np.empty((0))
+
+        # for key in list(Y_mean_can):
+        #     # concatenate the measurement error
+        #     Obs = np.hstack((Obs,ObservationData[key]))
+
+        #     # concatenate the mean and variance of prior predictive
+        #     means, stds = Y_mean_can[key][0], Y_std_can[key][0]
+        #     priorMean = np.hstack((priorSigma2,means))
+        #     priorSigma2 = np.hstack((priorSigma2,stds**2))
+
+        # # Covariance Matrix of prior
+        # covPrior = np.zeros((priorSigma2.shape[0], priorSigma2.shape[0]), float)
+        # np.fill_diagonal(covPrior, priorSigma2)
+
+        # # Covariance Matrix of Likelihood
+        # covLikelihood = np.zeros((sigma2Dict.shape[0], sigma2Dict.shape[0]), float)
+        # np.fill_diagonal(covLikelihood, sigma2Dict)
+
+        # # Calculate moments of the posterior (Analytical derivation)
+        # n = priorSigma2.shape[0]
+        # covPost = np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))),covLikelihood/n)
+
+        # meanPost = np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))) , Obs) + \
+        #             np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))),
+        #                     priorMean/n)
+        # # Compute DKL from prior to posterior
+        # term1 = np.trace(np.dot(np.linalg.inv(covPrior),covPost))
+        # deltaMean = priorMean-meanPost
+        # term2 = np.dot(np.dot(deltaMean,np.linalg.inv(covPrior)),deltaMean[:,None])
+        # term3 = np.log(np.linalg.det(covPrior)/np.linalg.det(covPost))
+        # DKL = 0.5 * (term1 + term2 - n + term3)[0]
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = 20000
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Sample a distribution for a normal dist
+            # with Y_mean_can as the mean and Y_std_can as std.
+            Y_MC, std_MC = {}, {}
+            logPriorLikelihoods = np.zeros((MCsize))
+            for key in list(Y_mean_can):
+                means, stds = Y_mean_can[key][0], Y_std_can[key][0]
+                # cov = np.zeros((means.shape[0], means.shape[0]), float)
+                # np.fill_diagonal(cov, stds**2)
+
+                Y_MC[key] = np.zeros((MCsize, n_obs))
+                logsamples = np.zeros((MCsize, n_obs))
+                for i in range(n_obs):
+                    NormalDensity = stats.norm(means[i], stds[i])
+                    Y_MC[key][:, i] = NormalDensity.rvs(MCsize)
+                    logsamples[:, i] = NormalDensity.logpdf(Y_MC[key][:, i])
+
+                logPriorLikelihoods = np.sum(logsamples, axis=1)
+                std_MC[key] = np.zeros((MCsize, means.shape[0]))
+
+            #  Likelihood computation (Comparison of data and simulation
+            #  results via PCE with candidate design)
+            likelihoods = self.__normpdf(Y_MC, std_MC, obs_data, sigma2Dict)
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.nansum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(likelihoods))
+
+        # Posterior-based expectation of likelihoods
+        postLikelihoods = likelihoods[accepted]
+        postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+        # Utility function Eq.2 in Ref. (2)
+        # Posterior covariance matrix after observing data y
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # TODO: Calculate the correction factor for BME
+            # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can,
+            #                                      ObservationData, sigma2Dict)
+            # BME += BMECorrFactor
+            # Haun et al implementation
+            # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+            U_J_d = postExpLikelihoods - logBME
+
+        # Marginal log likelihood
+        elif var == 'BME':
+            U_J_d = logBME
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            logBME = np.log(np.nanmean(likelihoods))
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # Bayesian information criterion
+        elif var == 'BIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxL = np.nanmax(likelihoods)
+            U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams
+
+        # Akaike information criterion
+        elif var == 'AIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxlogL = np.log(np.nanmax(likelihoods))
+            AIC = -2 * maxlogL + 2 * nModelParams
+            # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1)
+            penTerm = 0
+            U_J_d = 1*(AIC + penTerm)
+
+        # Deviance information criterion
+        elif var == 'DIC':
+            # D_theta_bar = np.mean(-2 * Likelihoods)
+            N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0]))
+            Likelihoods_theta_mean = self.__normpdf(
+                Y_mean_can, Y_std_can, obs_data, sigma2Dict
+                )
+            DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p
+
+            U_J_d = DIC
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Handle inf and NaN (replace by zero)
+        if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf:
+            U_J_d = 0.0
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+        gc.collect(generation=2)
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def update_metamodel(self, MetaModel, output, y_hat_can, univ_p_val, index,
+                         new_pca=False):
+        BasisIndices = MetaModel.basis_dict[output]["y_"+str(index+1)]
+        clf_poly = MetaModel.clf_poly[output]["y_"+str(index+1)]
+        Mn = clf_poly.coef_
+        Sn = clf_poly.sigma_
+        beta = clf_poly.alpha_
+        active = clf_poly.active_
+        Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val)
+
+        Sn_new_inv = np.linalg.inv(Sn)
+        Sn_new_inv += beta * np.dot(Psi[:, active].T, Psi[:, active])
+        Sn_new = np.linalg.inv(Sn_new_inv)
+
+        Mn_new = np.dot(Sn_new_inv, Mn[active]).reshape(-1, 1)
+        Mn_new += beta * np.dot(Psi[:, active].T, y_hat_can)
+        Mn_new = np.dot(Sn_new, Mn_new).flatten()
+
+        # Compute the old and new moments of PCEs
+        mean_old = Mn[0]
+        mean_new = Mn_new[0]
+        std_old = np.sqrt(np.sum(np.square(Mn[1:])))
+        std_new = np.sqrt(np.sum(np.square(Mn_new[1:])))
+
+        # Back transformation if PCA is selected.
+        if MetaModel.dim_red_method.lower() == 'pca':
+            old_pca = MetaModel.pca[output]
+            mean_old = old_pca.mean_[index]
+            mean_old += np.sum(mean_old * old_pca.components_[:, index])
+            std_old = np.sqrt(np.sum(std_old**2 *
+                                     old_pca.components_[:, index]**2))
+            mean_new = new_pca.mean_[index]
+            mean_new += np.sum(mean_new * new_pca.components_[:, index])
+            std_new = np.sqrt(np.sum(std_new**2 *
+                                     new_pca.components_[:, index]**2))
+            # print(f"mean_old: {mean_old:.2f} mean_new: {mean_new:.2f}")
+            # print(f"std_old: {std_old:.2f} std_new: {std_new:.2f}")
+        # Store the old and new moments of PCEs
+        results = {
+            'mean_old': mean_old,
+            'mean_new': mean_new,
+            'std_old': std_old,
+            'std_new': std_new
+            }
+        return results
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign_old(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        Model = self.Model
+        MetaModel = deepcopy(self.MetaModel)
+        old_EDY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels using the candidate design
+        Y_PC_can, Y_std_can = self.MetaModel.eval_metamodel(
+            samples=np.array([X_can])
+            )
+
+        # Generate y from posterior predictive
+        m_size = 100
+        y_hat_samples = {}
+        for idx, key in enumerate(Model.Output.names):
+            means, stds = Y_PC_can[key][0], Y_std_can[key][0]
+            y_hat_samples[key] = np.random.multivariate_normal(
+                means, np.diag(stds), m_size)
+
+        # Create the SparseBayes-based PCE metamodel:
+        MetaModel.input_obj.poly_coeffs_flag = False
+        univ_p_val = self.MetaModel.univ_basis_vals(X_can)
+        G_n_m_all = np.zeros((m_size, len(Model.Output.names), Model.n_obs))
+
+        for i in range(m_size):
+            for idx, key in enumerate(Model.Output.names):
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    # Equal number of components
+                    new_outputs = np.vstack(
+                        (old_EDY[key], y_hat_samples[key][i])
+                        )
+                    new_pca, _ = MetaModel.pca_transformation(new_outputs)
+                    target = new_pca.transform(
+                        y_hat_samples[key][i].reshape(1, -1)
+                        )[0]
+                else:
+                    new_pca, target = False, y_hat_samples[key][i]
+
+                for j in range(len(target)):
+
+                    # Update surrogate
+                    result = self.update_metamodel(
+                        MetaModel, key, target[j], univ_p_val, j, new_pca)
+
+                    # Compute Expected Information Gain (Eq. 39)
+                    G_n_m = np.log(result['std_old']/result['std_new']) - 1./2
+                    G_n_m += result['std_new']**2 / (2*result['std_old']**2)
+                    G_n_m += (result['mean_new'] - result['mean_old'])**2 /\
+                        (2*result['std_old']**2)
+
+                    G_n_m_all[i, idx, j] = G_n_m
+
+        U_J_d = G_n_m_all.mean(axis=(1, 2)).mean()
+        return -1 * U_J_d
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        Model = self.Model
+        MetaModel = deepcopy(self.MetaModel)
+        out_names = MetaModel.ModelObj.Output.names
+        if X_can.ndim == 1:
+            X_can = X_can.reshape(1, -1)
+
+        # Compute the mean and std based on the MetaModel
+        # pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+        if var == 'ALC':
+            Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+        oldExpDesignY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can)
+
+        # Add all suggestion as new ExpDesign
+        NewExpDesignX = np.vstack((oldExpDesignX, X_can))
+
+        NewExpDesignY = {}
+        for key in oldExpDesignY.keys():
+            try:
+                NewExpDesignY[key] = np.vstack((oldExpDesignY[key],
+                                                Y_PC_can[key]))
+            except:
+                NewExpDesignY[key] = oldExpDesignY[key]
+
+        MetaModel.ExpDesign.sampling_method = 'user'
+        MetaModel.ExpDesign.X = NewExpDesignX
+        MetaModel.ExpDesign.Y = NewExpDesignY
+
+        # Train the model for the observed data using x_can
+        MetaModel.input_obj.poly_coeffs_flag = False
+        MetaModel.train_norm_design(parallel=False)
+        PCE_Model_can = MetaModel
+
+        if var.lower() == 'mi':
+            # Mutual information based on Krause et al
+            # Adapted from Beck & Guillas (MICE) paper
+            _, std_PC_can = PCE_Model_can.eval_metamodel(samples=X_can)
+            std_can = {key: std_PC_can[key] for key in out_names}
+
+            std_old = {key: Y_std_can[key] for key in out_names}
+
+            varPCE = np.zeros((len(out_names)))
+            for i, key in enumerate(out_names):
+                varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2)
+            score = np.mean(varPCE)
+
+            return -1 * score
+
+        elif var.lower() == 'alc':
+            # Active learning based on Gramyc and Lee
+            # Adaptive design and analysis of supercomputer experiments Techno-
+            # metrics, 51 (2009), pp. 130–145.
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC_can, Y_MC_std_can = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Compute the score
+            score = []
+            for i, key in enumerate(out_names):
+                pce_var = Y_MC_std_can[key]**2
+                pce_var_can = Y_MC_std[key]**2
+                score.append(np.mean(pce_var-pce_var_can, axis=0))
+            score = np.mean(score)
+
+            return -1 * score
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = X_MC.shape[0]
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Enriching Monte Carlo samples if need be
+            if ESS != 0:
+                X_MC = self.MetaModel.ExpDesign.generate_samples(
+                    MCsize, 'random'
+                    )
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and simulation
+            # results via PCE with candidate design)
+            likelihoods = self.__normpdf(
+                Y_MC, std_MC, self.observations, sigma2Dict
+                )
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                print("--- increasing MC size---")
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # -------------------- Utility functions --------------------
+        # Utility function Eq.2 in Ref. (2)
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods, dtype=np.float128))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Haun et al implementation
+            U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME)
+
+            # U_J_d = np.sum(G_n_m_all)
+            # Ryan et al (2014) implementation
+            # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0])
+            # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME
+
+            # U_J_d = postExpLikelihoods - logBME
+
+        # Marginal likelihood
+        elif var == 'BME':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+            U_J_d = logBME
+
+        # Bayes risk likelihood
+        elif var == 'BayesRisk':
+
+            U_J_d = -1 * np.var(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted] / np.nansum(likelihoods[accepted])
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Posterior-based expectation of prior densities
+            postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # D-Posterior-precision
+        elif var == 'DPP':
+            X_Posterior = X_MC[accepted]
+            # covariance of the posterior parameters
+            U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior)))
+
+        # A-Posterior-precision
+        elif var == 'APP':
+            X_Posterior = X_MC[accepted]
+            # trace of the posterior parameters
+            U_J_d = -np.log(np.trace(np.cov(X_Posterior)))
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+        gc.collect(generation=2)
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def subdomain(self, Bounds, n_new_samples):
+        """
+        Divides a domain defined by Bounds into sub domains.
+
+        Parameters
+        ----------
+        Bounds : list of tuples
+            List of lower and upper bounds.
+        n_new_samples : TYPE
+            DESCRIPTION.
+
+        Returns
+        -------
+        Subdomains : TYPE
+            DESCRIPTION.
+
+        """
+        n_params = self.MetaModel.n_params
+        n_subdomains = n_new_samples + 1
+        LinSpace = np.zeros((n_params, n_subdomains))
+
+        for i in range(n_params):
+            LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1],
+                                      num=n_subdomains)
+        Subdomains = []
+        for k in range(n_subdomains-1):
+            mylist = []
+            for i in range(n_params):
+                mylist.append((LinSpace[i, k+0], LinSpace[i, k+1]))
+            Subdomains.append(tuple(mylist))
+
+        return Subdomains
+
+    # -------------------------------------------------------------------------
+    def run_util_func(self, method, candidates, index, sigma2Dict=None,
+                      var=None, X_MC=None):
+        """
+        Runs the utility function based on the given method.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        candidates : array of shape (n_samples, n_params)
+            All candidate parameter sets.
+        index : int
+            ExpDesign index.
+        sigma2Dict : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        var : string, optional
+            Utility function. The default is None.
+        X_MC : TYPE, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        index : TYPE
+            DESCRIPTION.
+        List
+            Scores.
+
+        """
+
+        if method.lower() == 'varoptdesign':
+            # U_J_d = self.util_VarBasedDesign(candidates, index, var)
+            U_J_d = np.zeros((candidates.shape[0]))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="varoptdesign"):
+                U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var)
+
+        elif method.lower() == 'bayesactdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianActiveDesign(X_can, sigma2Dict,
+                                                            var)
+        elif method.lower() == 'bayesoptdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict,
+                                                      var)
+        return (index, -1 * U_J_d)
+
+    # -------------------------------------------------------------------------
+    def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No,
+                       verbose=False):
+        """
+        Exploration algorithim to find the optimum parameter space.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        Bounds : list of tuples
+            List of lower and upper boundaries of parameters.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        Run_No : int
+            Run number.
+        verbose : bool, optional
+            Print out a summary. The default is False.
+
+        Returns
+        -------
+        Run_No : int
+            Run number.
+        array
+            Optimial candidate.
+
+        """
+
+        Model = self.Model
+        max_func_itr = self.MetaModel.ExpDesign.max_func_itr
+
+        if method == 'VarOptDesign':
+            Res_Global = opt.dual_annealing(self.util_VarBasedDesign,
+                                            bounds=Bounds,
+                                            args=(Model, var),
+                                            maxfun=max_func_itr)
+
+        elif method == 'BayesOptDesign':
+            Res_Global = opt.dual_annealing(self.util_BayesianDesign,
+                                            bounds=Bounds,
+                                            args=(Model, sigma2Dict, var),
+                                            maxfun=max_func_itr)
+
+        if verbose:
+            print(f"global minimum: xmin = {Res_Global.x}, "
+                  f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}")
+
+        return (Run_No, Res_Global.x)
+
+    # -------------------------------------------------------------------------
+    def tradoff_weights(self, tradeoff_scheme, old_EDX, old_EDY):
+        """
+        Calculates weights for exploration scores based on the requested
+        scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`.
+
+        `None`: No exploration.
+        `equal`: Same weights for exploration and exploitation scores.
+        `epsilon-decreasing`: Start with more exploration and increase the
+            influence of exploitation along the way with a exponential decay
+            function
+        `adaptive`: An adaptive method based on:
+            Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling
+            approach for Kriging metamodeling by maximizing expected prediction
+            error." Computers & Chemical Engineering 106 (2017): 171-182.
+
+        Parameters
+        ----------
+        tradeoff_scheme : string
+            Trade-off scheme for exloration and exploitation scores.
+        old_EDX : array (n_samples, n_params)
+            Old experimental design (training points).
+        old_EDY : dict
+            Old model responses (targets).
+
+        Returns
+        -------
+        exploration_weight : float
+            Exploration weight.
+        exploitation_weight: float
+            Exploitation weight.
+
+        """
+        if tradeoff_scheme is None:
+            exploration_weight = 0
+
+        elif tradeoff_scheme == 'equal':
+            exploration_weight = 0.5
+
+        elif tradeoff_scheme == 'epsilon-decreasing':
+            # epsilon-decreasing scheme
+            # Start with more exploration and increase the influence of
+            # exploitation along the way with a exponential decay function
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8)
+            exploration_weight = signal.exponential(n_max_samples-initNSamples,
+                                                    0, tau2, False)[itrNumber]
+
+        elif tradeoff_scheme == 'adaptive':
+
+            # Extract itrNumber
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            if itrNumber == 0:
+                exploration_weight = 0.5
+            else:
+                # New adaptive trade-off according to Liu et al. (2017)
+                # Mean squared error for last design point
+                last_EDX = old_EDX[-1].reshape(1, -1)
+                lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX)
+                pce_y = np.array(list(lastPCEY.values()))[:, 0]
+                y = np.array(list(old_EDY.values()))[:, -1, :]
+                mseError = mean_squared_error(pce_y, y)
+
+                # Mean squared CV - error for last design point
+                pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0]
+                mseCVError = mean_squared_error(pce_y_prev, y)
+
+                exploration_weight = min([0.5*mseError/mseCVError, 1])
+
+        # Exploitation weight
+        exploitation_weight = 1 - exploration_weight
+
+        return exploration_weight, exploitation_weight
+
+    # -------------------------------------------------------------------------
+    def opt_SeqDesign(self, sigma2, n_candidates=5, var='DKL'):
+        """
+        Runs optimal sequential design.
+
+        Parameters
+        ----------
+        sigma2 : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        n_candidates : int, optional
+            Number of candidate samples. The default is 5.
+        var : string, optional
+            Utility function. The default is None.
+
+        Raises
+        ------
+        NameError
+            Wrong utility function.
+
+        Returns
+        -------
+        Xnew : array (n_samples, n_params)
+            Selected new training point(s).
+        """
+
+        # Initialization
+        MetaModel = self.MetaModel
+        Bounds = MetaModel.bound_tuples
+        n_new_samples = MetaModel.ExpDesign.n_new_samples
+        explore_method = MetaModel.ExpDesign.explore_method
+        exploit_method = MetaModel.ExpDesign.exploit_method
+        n_cand_groups = MetaModel.ExpDesign.n_cand_groups
+        tradeoff_scheme = MetaModel.ExpDesign.tradeoff_scheme
+
+        old_EDX = MetaModel.ExpDesign.X
+        old_EDY = MetaModel.ExpDesign.Y.copy()
+        ndim = MetaModel.ExpDesign.X.shape[1]
+        OutputNames = MetaModel.ModelObj.Output.names
+
+        # -----------------------------------------
+        # ----------- CUSTOMIZED METHODS ----------
+        # -----------------------------------------
+        # Utility function exploit_method provided by user
+        if exploit_method.lower() == 'user':
+
+            Xnew, filteredSamples = MetaModel.ExpDesign.ExploitFunction(self)
+
+            print("\n")
+            print("\nXnew:\n", Xnew)
+
+            return Xnew, filteredSamples
+
+        # -----------------------------------------
+        # ---------- EXPLORATION METHODS ----------
+        # -----------------------------------------
+        if explore_method == 'dual annealing':
+            # ------- EXPLORATION: OPTIMIZATION -------
+            import time
+            start_time = time.time()
+
+            # Divide the domain to subdomains
+            args = []
+            subdomains = self.subdomain(Bounds, n_new_samples)
+            for i in range(n_new_samples):
+                args.append((exploit_method, subdomains[i], sigma2, var, i))
+
+            # Multiprocessing
+            pool = multiprocessing.Pool(multiprocessing.cpu_count())
+
+            # With Pool.starmap_async()
+            results = pool.starmap_async(self.dual_annealing, args).get()
+
+            # Close the pool
+            pool.close()
+
+            Xnew = np.array([results[i][1] for i in range(n_new_samples)])
+
+            print("\nXnew:\n", Xnew)
+
+            elapsed_time = time.time() - start_time
+            print("\n")
+            print(f"elapsed_time: {round(elapsed_time,2)} sec.")
+            print('-'*20)
+
+        elif explore_method == 'LOOCV':
+            # -----------------------------------------------------------------
+            # TODO: LOOCV model construnction based on Feng et al. (2020)
+            # 'LOOCV':
+            # Initilize the ExploitScore array
+
+            # Generate random samples
+            allCandidates = MetaModel.ExpDesign.generate_samples(n_candidates,
+                                                                'random')
+
+            # Construct error model based on LCerror
+            errorModel = MetaModel.create_ModelError(old_EDX, self.LCerror)
+            self.errorModel.append(copy(errorModel))
+
+            # Evaluate the error models for allCandidates
+            eLCAllCands, _ = errorModel.eval_errormodel(allCandidates)
+            # Select the maximum as the representative error
+            eLCAllCands = np.dstack(eLCAllCands.values())
+            eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0]
+
+            # Normalize the error w.r.t the maximum error
+            scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates)
+
+        else:
+            # ------- EXPLORATION: SPACE-FILLING DESIGN -------
+            # Generate candidate samples from Exploration class
+            explore = Exploration(MetaModel, n_candidates)
+            explore.w = 100  # * ndim #500
+            # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+            explore.mc_criterion = 'mc-intersite-proj'
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+
+            # Temp: ---- Plot all candidates -----
+            if ndim == 2:
+                def plotter(points, allCandidates, Method,
+                            scoreExploration=None):
+                    if Method == 'Voronoi':
+                        from scipy.spatial import Voronoi, voronoi_plot_2d
+                        vor = Voronoi(points)
+                        fig = voronoi_plot_2d(vor)
+                        ax1 = fig.axes[0]
+                    else:
+                        fig = plt.figure()
+                        ax1 = fig.add_subplot(111)
+                    ax1.scatter(points[:, 0], points[:, 1], s=10, c='r',
+                                marker="s", label='Old Design Points')
+                    ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10,
+                                c='b', marker="o", label='Design candidates')
+                    for i in range(points.shape[0]):
+                        txt = 'p'+str(i+1)
+                        ax1.annotate(txt, (points[i, 0], points[i, 1]))
+                    if scoreExploration is not None:
+                        for i in range(allCandidates.shape[0]):
+                            txt = str(round(scoreExploration[i], 5))
+                            ax1.annotate(txt, (allCandidates[i, 0],
+                                               allCandidates[i, 1]))
+
+                    plt.xlim(self.bound_tuples[0])
+                    plt.ylim(self.bound_tuples[1])
+                    # plt.show()
+                    plt.legend(loc='upper left')
+
+        # -----------------------------------------
+        # --------- EXPLOITATION METHODS ----------
+        # -----------------------------------------
+        if exploit_method == 'BayesOptDesign' or\
+           exploit_method == 'BayesActDesign':
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\n Exploration weight={explore_w:0.3f} "
+                  f"Exploitation weight={exploit_w:0.3f}\n")
+
+            # ------- EXPLOITATION: BayesOptDesign & ActiveLearning -------
+            if explore_w != 1.0:
+
+                # Create a sample pool for rejection sampling
+                MCsize = 15000
+                X_MC = MetaModel.ExpDesign.generate_samples(MCsize, 'random')
+                candidates = MetaModel.ExpDesign.generate_samples(
+                    MetaModel.ExpDesign.max_func_itr, 'latin_hypercube')
+
+                # Split the candidates in groups for multiprocessing
+                split_cand = np.array_split(
+                    candidates, n_cand_groups, axis=0
+                    )
+
+                results = Parallel(n_jobs=-1, backend='threading')(
+                        delayed(self.run_util_func)(
+                            exploit_method, split_cand[i], i, sigma2, var, X_MC)
+                        for i in range(n_cand_groups))
+                # out = map(self.run_util_func,
+                #           [exploit_method]*n_cand_groups,
+                #           split_cand,
+                #           range(n_cand_groups),
+                #           [sigma2] * n_cand_groups,
+                #           [var] * n_cand_groups,
+                #           [X_MC] * n_cand_groups
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                U_J_d = np.concatenate([results[NofE][1] for NofE in
+                                        range(n_cand_groups)])
+
+                # Check if all scores are inf
+                if np.isinf(U_J_d).all() or np.isnan(U_J_d).all():
+                    U_J_d = np.ones(len(U_J_d))
+
+                # Get the expected value (mean) of the Utility score
+                # for each cell
+                if explore_method == 'Voronoi':
+                    U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1)
+
+                # create surrogate model for U_J_d
+                from sklearn.preprocessing import MinMaxScaler
+                # Take care of inf entries
+                good_indices = [i for i, arr in enumerate(U_J_d)
+                                if np.isfinite(arr).all()]
+                scaler = MinMaxScaler()
+                X_S = scaler.fit_transform(candidates[good_indices])
+                gp = MetaModel.gaussian_process_emulator(
+                    X_S, U_J_d[good_indices], autoSelect=True
+                    )
+                U_J_d = gp.predict(scaler.transform(allCandidates))
+
+                # Normalize U_J_d
+                norm_U_J_d = U_J_d / np.sum(U_J_d)
+                print("norm_U_J_d:\n", norm_U_J_d)
+            else:
+                norm_U_J_d = np.zeros((len(scoreExploration)))
+
+            # ------- Calculate Total score -------
+            # ------- Trade off between EXPLORATION & EXPLOITATION -------
+            # Total score
+            totalScore = exploit_w * norm_U_J_d
+            totalScore += explore_w * scoreExploration
+
+            # temp: Plot
+            # dim = self.ExpDesign.X.shape[1]
+            # if dim == 2:
+            #     plotter(self.ExpDesign.X, allCandidates, explore_method)
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            # select the requested number of samples
+            if explore_method == 'Voronoi':
+                Xnew = np.zeros((n_new_samples, ndim))
+                for i, idx in enumerate(bestIdx):
+                    X_can = explore.closestPoints[idx]
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+            else:
+                Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        elif exploit_method == 'VarOptDesign':
+            # ------- EXPLOITATION: VarOptDesign -------
+            UtilMethod = var
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\nweightExploration={explore_w:0.3f} "
+                  f"weightExploitation={exploit_w:0.3f}")
+
+            # Generate candidate samples from Exploration class
+            nMeasurement = old_EDY[OutputNames[0]].shape[1]
+
+            # Find sensitive region
+            if UtilMethod == 'LOOCV':
+                LCerror = MetaModel.LCerror
+                allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames),
+                                           nMeasurement))
+                for y_idx, y_key in enumerate(OutputNames):
+                    for idx, key in enumerate(LCerror[y_key].keys()):
+                        allModifiedLOO[:, y_idx, idx] = abs(
+                            LCerror[y_key][key])
+
+                ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1)
+
+            elif UtilMethod in ['EIGF', 'ALM']:
+                # ----- All other in  ['EIGF', 'ALM'] -----
+                # Initilize the ExploitScore array
+                ExploitScore = np.zeros((len(old_EDX), len(OutputNames)))
+
+                # Split the candidates in groups for multiprocessing
+                if explore_method != 'Voronoi':
+                    split_cand = np.array_split(allCandidates,
+                                                n_cand_groups,
+                                                axis=0)
+                    goodSampleIdx = range(n_cand_groups)
+                else:
+                    # Find indices of the Vornoi cells with samples
+                    goodSampleIdx = []
+                    for idx in range(len(explore.closest_points)):
+                        if len(explore.closest_points[idx]) != 0:
+                            goodSampleIdx.append(idx)
+                    split_cand = explore.closest_points
+
+                # Split the candidates in groups for multiprocessing
+                args = []
+                for index in goodSampleIdx:
+                    args.append((exploit_method, split_cand[index], index,
+                                 sigma2, var))
+
+                # Multiprocessing
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.run_util_func, args).get()
+
+                # Close the pool
+                pool.close()
+                # out = map(self.run_util_func,
+                #           [exploit_method]*len(goodSampleIdx),
+                #           split_cand,
+                #           range(len(goodSampleIdx)),
+                #           [sigma2] * len(goodSampleIdx),
+                #           [var] * len(goodSampleIdx)
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                if explore_method == 'Voronoi':
+                    ExploitScore = [np.mean(results[k][1]) for k in
+                                    range(len(goodSampleIdx))]
+                else:
+                    ExploitScore = np.concatenate(
+                        [results[k][1] for k in range(len(goodSampleIdx))])
+
+            else:
+                raise NameError('The requested utility function is not '
+                                'available.')
+
+            # print("ExploitScore:\n", ExploitScore)
+
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            # Total score
+            # Normalize U_J_d
+            ExploitScore = ExploitScore / np.sum(ExploitScore)
+            totalScore = exploit_w * ExploitScore
+            totalScore += explore_w * scoreExploration
+
+            temp = totalScore.copy()
+            sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            Xnew = np.zeros((n_new_samples, ndim))
+            if explore_method != 'Voronoi':
+                Xnew = allCandidates[bestIdx]
+            else:
+                for i, idx in enumerate(bestIdx.flatten()):
+                    X_can = explore.closest_points[idx]
+                    # plotter(self.ExpDesign.X, X_can, explore_method,
+                    # scoreExploration=None)
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+
+        elif exploit_method == 'alphabetic':
+            # ------- EXPLOITATION: ALPHABETIC -------
+            Xnew = self.util_AlphOptDesign(allCandidates, var)
+
+        elif exploit_method == 'Space-filling':
+            # ------- EXPLOITATION: SPACE-FILLING -------
+            totalScore = scoreExploration
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+
+            # select the requested number of samples
+            Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        else:
+            raise NameError('The requested design method is not available.')
+
+        print("\n")
+        print("\nRun No. {}:".format(old_EDX.shape[0]+1))
+        print("Xnew:\n", Xnew)
+        gc.collect()
+
+        return Xnew, None
+
+    # -------------------------------------------------------------------------
+    def util_AlphOptDesign(self, candidates, var='D-Opt'):
+        """
+        Enriches the Experimental design with the requested alphabetic
+        criterion based on exploring the space with number of sampling points.
+
+        Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos
+        expansion: A review of sampling strategies., Computer Methods in
+        Applied Mechanics and Engineering, 332, 382-407.
+
+        Arguments
+        ---------
+        NCandidate : int
+            Number of candidate points to be searched
+
+        var : string
+            Alphabetic optimality criterion
+
+        Returns
+        -------
+        X_new : array of shape (1, n_params)
+            The new sampling location in the input space.
+        """
+        MetaModelOrig = self
+        Model = self.Model
+        n_new_samples = MetaModelOrig.ExpDesign.n_new_samples
+        NCandidate = candidates.shape[0]
+
+        # TODO: Loop over outputs
+        OutputName = Model.Output.names[0]
+
+        # To avoid changes ub original aPCE object
+        MetaModel = deepcopy(MetaModelOrig)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+
+        # TODO: Only one psi can be selected.
+        # Suggestion: Go for the one with the highest LOO error
+        Scores = list(MetaModel.score_dict[OutputName].values())
+        ModifiedLOO = [1-score for score in Scores]
+        outIdx = np.argmax(ModifiedLOO)
+
+        # Initialize Phi to save the criterion's values
+        Phi = np.zeros((NCandidate))
+
+        BasisIndices = MetaModelOrig.basis_dict[OutputName]["y_"+str(outIdx+1)]
+        P = len(BasisIndices)
+
+        # ------ Old Psi ------------
+        univ_p_val = MetaModelOrig.univ_basis_vals(oldExpDesignX)
+        Psi = MetaModelOrig.create_psi(BasisIndices, univ_p_val)
+
+        # ------ New candidates (Psi_c) ------------
+        # Assemble Psi_c
+        univ_p_val_c = self.univ_basis_vals(candidates)
+        Psi_c = self.create_psi(BasisIndices, univ_p_val_c)
+
+        for idx in range(NCandidate):
+
+            # Include the new row to the original Psi
+            Psi_cand = np.vstack((Psi, Psi_c[idx]))
+
+            # Information matrix
+            PsiTPsi = np.dot(Psi_cand.T, Psi_cand)
+            M = PsiTPsi / (len(oldExpDesignX)+1)
+
+            if np.linalg.cond(PsiTPsi) > 1e-12 \
+               and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon:
+                # faster
+                invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray())
+            else:
+                # stabler
+                invM = np.linalg.pinv(M)
+
+            # ---------- Calculate optimality criterion ----------
+            # Optimality criteria according to Section 4.5.1 in Ref.
+
+            # D-Opt
+            if var == 'D-Opt':
+                Phi[idx] = (np.linalg.det(invM)) ** (1/P)
+
+            # A-Opt
+            elif var == 'A-Opt':
+                Phi[idx] = np.trace(invM)
+
+            # K-Opt
+            elif var == 'K-Opt':
+                Phi[idx] = np.linalg.cond(M)
+
+            else:
+                raise Exception('The optimality criterion you requested has '
+                      'not been implemented yet!')
+
+        # find an optimal point subset to add to the initial design
+        # by minimization of the Phi
+        sorted_idxtotalScore = np.argsort(Phi)
+
+        # select the requested number of samples
+        Xnew = candidates[sorted_idxtotalScore[:n_new_samples]]
+
+        return Xnew
+
+    # -------------------------------------------------------------------------
+    def __normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s,
+                  rmse=None):
+
+        Model = self.Model
+        likelihoods = 1.0
+
+        # Loop over the outputs
+        for idx, out in enumerate(Model.Output.names):
+
+            # (Meta)Model Output
+            nsamples, nout = y_hat_pce[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values
+
+            # Surrogate error if valid dataset is given.
+            if rmse is not None:
+                tot_sigma2s += rmse[out]**2
+
+            likelihoods *= stats.multivariate_normal.pdf(
+                y_hat_pce[out], data, np.diag(tot_sigma2s),
+                allow_singular=True)
+        self.Likelihoods = likelihoods
+
+        return likelihoods
+
+    # -------------------------------------------------------------------------
+    def __corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = MetaModel.ExpDesign.X  # valid_samples
+        model_outputs = MetaModel.ExpDesign.Y  # valid_model_runs
+        Model = MetaModel.ModelObj
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = Model.Output.names
+
+        # TODO: Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                # covMatrix = np.diag(stdOutputRS[out][i]**2)
+                covMatrix = np.diag((y_m-y_m_hat)**2)
+                covMatrix = np.diag(
+                    np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2
+                    )
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += self.__logpdf(
+                    y_m_hat, data, covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += self.__logpdf(y_m_hat, y_m, covMatrix)
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.exp(logLik_model+logLik_data)
+
+        return np.log(np.mean(weights))
+
+    # -------------------------------------------------------------------------
+    def __logpdf(self, x, mean, cov):
+        """
+        computes the likelihood based on a multivariate normal distribution.
+
+        Parameters
+        ----------
+        x : TYPE
+            DESCRIPTION.
+        mean : array_like
+            Observation data.
+        cov : 2d array
+            Covariance matrix of the distribution.
+
+        Returns
+        -------
+        log_lik : float
+            Log likelihood.
+
+        """
+        n = len(mean)
+        L = linalg.cholesky(cov, lower=True)
+        beta = np.sum(np.log(np.diag(L)))
+        dev = x - mean
+        alpha = dev.dot(linalg.cho_solve((L, True), dev))
+        log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+
+        return log_lik
+
+    # -------------------------------------------------------------------------
+    def __posteriorPlot(self, posterior, par_names, key):
+
+        # Initialization
+        newpath = (r'Outputs_SeqPosteriorComparison/posterior')
+        os.makedirs(newpath, exist_ok=True)
+
+        bound_tuples = self.MetaModel.bound_tuples
+        n_params = len(par_names)
+        font_size = 40
+        if n_params == 2:
+
+            figPosterior, ax = plt.subplots(figsize=(15, 15))
+
+            sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1],
+                        fill=True, ax=ax, cmap=plt.cm.jet,
+                        clip=bound_tuples)
+            # Axis labels
+            plt.xlabel(par_names[0], fontsize=font_size)
+            plt.ylabel(par_names[1], fontsize=font_size)
+
+            # Set axis limit
+            plt.xlim(bound_tuples[0])
+            plt.ylim(bound_tuples[1])
+
+            # Increase font size
+            plt.xticks(fontsize=font_size)
+            plt.yticks(fontsize=font_size)
+
+            # Switch off the grids
+            plt.grid(False)
+
+        else:
+            import corner
+            figPosterior = corner.corner(posterior, labels=par_names,
+                                         title_fmt='.2e', show_titles=True,
+                                         title_kwargs={"fontsize": 12})
+
+        figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight')
+        plt.close()
+
+        # Save the posterior as .npy
+        np.save(f'./{newpath}/{key}.npy', posterior)
+
+        return figPosterior
+
+    # -------------------------------------------------------------------------
+    def __hellinger_distance(self, P, Q):
+        """
+        Hellinger distance between two continuous distributions.
+
+        The maximum distance 1 is achieved when P assigns probability zero to
+        every set to which Q assigns a positive probability, and vice versa.
+        0 (identical) and 1 (maximally different)
+
+        Parameters
+        ----------
+        P : array
+            Reference likelihood.
+        Q : array
+            Estimated likelihood.
+
+        Returns
+        -------
+        float
+            Hellinger distance of two distributions.
+
+        """
+        mu1 = P.mean()
+        Sigma1 = np.std(P)
+
+        mu2 = Q.mean()
+        Sigma2 = np.std(Q)
+
+        term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2))
+
+        term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2))
+
+        H_squared = 1 - term1 * term2
+
+        return np.sqrt(H_squared)
+
+    # -------------------------------------------------------------------------
+    def __BME_Calculator(self, MetaModel, obs_data, sigma2Dict, rmse=None):
+        """
+        This function computes the Bayesian model evidence (BME) via Monte
+        Carlo integration.
+
+        """
+        # Initializations
+        valid_likelihoods = MetaModel.valid_likelihoods
+
+        post_snapshot = MetaModel.ExpDesign.post_snapshot
+        if post_snapshot or len(valid_likelihoods) != 0:
+            newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref')
+            os.makedirs(newpath, exist_ok=True)
+
+        SamplingMethod = 'random'
+        MCsize = 10000
+        ESS = 0
+
+        # Estimation of the integral via Monte Varlo integration
+        while (ESS > MCsize) or (ESS < 1):
+
+            # Generate samples for Monte Carlo simulation
+            X_MC = MetaModel.ExpDesign.generate_samples(
+                MCsize, SamplingMethod
+                )
+
+            # Monte Carlo simulation for the candidate design
+            m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+            Y_MC, std_MC = MetaModel.eval_metamodel(samples=X_MC)
+            m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+            print(f"\nMemory eval_metamodel in BME: {m_2-m_1:.2f} MB")
+
+            # Likelihood computation (Comparison of data and
+            # simulation results via PCE with candidate design)
+            Likelihoods = self.__normpdf(
+                Y_MC, std_MC, obs_data, sigma2Dict, rmse
+                )
+
+            # Check the Effective Sample Size (1000<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if (ESS > MCsize) or (ESS < 1):
+                print(f'ESS={ESS} MC size should be larger.')
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+        X_Posterior = X_MC[accepted]
+
+        # ------------------------------------------------------------
+        # --- Kullback-Leibler Divergence & Information Entropy ------
+        # ------------------------------------------------------------
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(Likelihoods))
+
+        # TODO: Correction factor
+        # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME)
+
+        # Posterior-based expectation of likelihoods
+        postExpLikelihoods = np.mean(np.log(Likelihoods[accepted]))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(
+            np.log(MetaModel.ExpDesign.JDist.pdf(X_Posterior.T))
+            )
+
+        # Calculate Kullback-Leibler Divergence
+        # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+        KLD = postExpLikelihoods - logBME
+
+        # Information Entropy based on Entropy paper Eq. 38
+        infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+        # If post_snapshot is True, plot likelihood vs refrence
+        if post_snapshot or len(valid_likelihoods) != 0:
+            # Hellinger distance
+            ref_like = np.log(valid_likelihoods[valid_likelihoods > 0])
+            est_like = np.log(Likelihoods[Likelihoods > 0])
+            distHellinger = self.__hellinger_distance(ref_like, est_like)
+
+            idx = len([name for name in os.listdir(newpath) if 'Likelihoods_'
+                       in name and os.path.isfile(os.path.join(newpath, name))])
+            fig, ax = plt.subplots()
+            try:
+                sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]),
+                            shade=True, color="g", label='Ref. Likelihood')
+                sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True,
+                            color="b", label='Likelihood with PCE')
+            except:
+                pass
+
+            text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}"
+            "\n DKL={KLD:.3f}"
+
+            plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat',
+                                                 edgecolor='black',
+                                                 boxstyle='round,pad=1'),
+                     transform=ax.transAxes)
+
+            fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf',
+                        bbox_inches='tight')
+            plt.close()
+
+        else:
+            distHellinger = 0.0
+
+        # Bayesian inference with Emulator only for 2D problem
+        if post_snapshot and MetaModel.n_params == 2 and not idx % 5:
+            from bayes_inference.bayes_inference import BayesInference
+            from bayes_inference.discrepancy import Discrepancy
+            import pandas as pd
+            BayesOpts = BayesInference(MetaModel)
+            BayesOpts.emulator = True
+            BayesOpts.plot_post_pred = False
+
+            # Select the inference method
+            import emcee
+            BayesOpts.inference_method = "MCMC"
+            # Set the MCMC parameters passed to self.mcmc_params
+            BayesOpts.mcmc_params = {
+                'n_steps': 1e5,
+                'n_walkers': 30,
+                'moves': emcee.moves.KDEMove(),
+                'verbose': False
+                }
+
+            # ----- Define the discrepancy model -------
+            obs_data = pd.DataFrame(obs_data, columns=self.Model.Output.names)
+            BayesOpts.measurement_error = obs_data
+
+            # # -- (Option B) --
+            DiscrepancyOpts = Discrepancy('')
+            DiscrepancyOpts.type = 'Gaussian'
+            DiscrepancyOpts.parameters = obs_data**2
+            BayesOpts.Discrepancy = DiscrepancyOpts
+            # Start the calibration/inference
+            Bayes_PCE = BayesOpts.create_inference()
+            X_Posterior = Bayes_PCE.posterior_df.values
+
+        # Clean up
+        del Y_MC, std_MC
+        gc.collect()
+
+        return (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+
+    # -------------------------------------------------------------------------
+    def __validError(self, MetaModel):
+
+        # MetaModel = self.MetaModel
+        Model = MetaModel.ModelObj
+        OutputName = Model.Output.names
+
+        # Extract the original model with the generated samples
+        valid_samples = MetaModel.valid_samples
+        valid_model_runs = MetaModel.valid_model_runs
+
+        # Run the PCE model with the generated samples
+        m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+        valid_PCE_runs, valid_PCE_std = MetaModel.eval_metamodel(samples=valid_samples)
+        m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+        print(f"\nMemory eval_metamodel: {m_2-m_1:.2f} MB")
+
+        rms_error = {}
+        valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in OutputName:
+            rms_error[key] = mean_squared_error(
+                valid_model_runs[key], valid_PCE_runs[key],
+                multioutput='raw_values',
+                sample_weight=None,
+                squared=False)
+
+            # Validation error
+            valid_error[key] = (rms_error[key]**2)
+            valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Updated Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(rms_error[key],
+                                             valid_error[key]))))
+
+        return rms_error, valid_error
+
+    # -------------------------------------------------------------------------
+    def __error_Mean_Std(self):
+
+        MetaModel = self.MetaModel
+        # Extract the mean and std provided by user
+        df_MCReference = MetaModel.ModelObj.mc_reference
+
+        # Compute the mean and std based on the MetaModel
+        pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+
+        # Compute the root mean squared error
+        for output in MetaModel.ModelObj.Output.names:
+
+            # Compute the error between mean and std of MetaModel and OrigModel
+            RMSE_Mean = mean_squared_error(
+                df_MCReference['mean'], pce_means[output], squared=False
+                )
+            RMSE_std = mean_squared_error(
+                df_MCReference['std'], pce_means[output], squared=False
+                )
+
+        return RMSE_Mean, RMSE_std
+
+    # -------------------------------------------------------------------------
+    def _compute_pce_moments(self, MetaModel):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moment (mean) of the surrogate.
+        pce_stds: dict
+            The second moment (standard deviation) of the surrogate.
+
+        """
+        outputs = MetaModel.ModelObj.Output.names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(MetaModel.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    PCA = MetaModel.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.mean_ + np.dot(
+                        pce_mean, PCA.components_)
+                    stds[output] = np.sqrt(np.dot(pce_var,
+                                                  PCA.components_**2))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+        return pce_means, pce_stds
diff --git a/examples/analytical-function/bayesvalidrox/surrogate_models/surrogate_models.py b/examples/analytical-function/bayesvalidrox/surrogate_models/surrogate_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..e318dfc32a22a1b02efc4ee8d3ccbaa4b2b190f3
--- /dev/null
+++ b/examples/analytical-function/bayesvalidrox/surrogate_models/surrogate_models.py
@@ -0,0 +1,1581 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Implementation of metamodel as either PC, aPC or GPE
+"""
+
+import warnings
+import numpy as np
+import math
+import h5py
+import matplotlib.pyplot as plt
+from sklearn.preprocessing import MinMaxScaler
+import scipy as sp
+from scipy.optimize import minimize, NonlinearConstraint, LinearConstraint
+from tqdm import tqdm
+from sklearn.decomposition import PCA as sklearnPCA
+import sklearn.linear_model as lm
+from sklearn.gaussian_process import GaussianProcessRegressor
+import sklearn.gaussian_process.kernels as kernels
+import os
+from joblib import Parallel, delayed
+import copy
+
+from .input_space import InputSpace
+from .glexindex import glexindex
+from .eval_rec_rule import eval_univ_basis
+from .reg_fast_ard import RegressionFastARD
+from .reg_fast_laplace import RegressionFastLaplace
+from .orthogonal_matching_pursuit import OrthogonalMatchingPursuit
+from .bayes_linear import VBLinearRegression, EBLinearRegression
+from .apoly_construction import apoly_construction
+warnings.filterwarnings("ignore")
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class MetaModel():
+    """
+    Meta (surrogate) model
+
+    This class trains a surrogate model. It accepts an input object (input_obj)
+    containing the specification of the distributions for uncertain parameters
+    and a model object with instructions on how to run the computational model.
+
+    Attributes
+    ----------
+    input_obj : obj
+        Input object with the information on the model input parameters.
+    meta_model_type : str
+        Surrogate model types. Three surrogate model types are supported:
+        polynomial chaos expansion (`PCE`), arbitrary PCE (`aPCE`) and
+        Gaussian process regression (`GPE`). Default is PCE.
+    pce_reg_method : str
+        PCE regression method to compute the coefficients. The following
+        regression methods are available:
+
+        1. OLS: Ordinary Least Square method
+        2. BRR: Bayesian Ridge Regression
+        3. LARS: Least angle regression
+        4. ARD: Bayesian ARD Regression
+        5. FastARD: Fast Bayesian ARD Regression
+        6. VBL: Variational Bayesian Learning
+        7. EBL: Emperical Bayesian Learning
+        Default is `OLS`.
+    bootstrap_method : str
+        Bootstraping method. Options are `'normal'` and `'fast'`. The default
+        is `'fast'`. It means that in each iteration except the first one, only
+        the coefficent are recalculated with the ordinary least square method.
+    n_bootstrap_itrs : int
+        Number of iterations for the bootstrap sampling. The default is `1`.
+    pce_deg : int or list of int
+        Polynomial degree(s). If a list is given, an adaptive algorithm is used
+        to find the best degree with the lowest Leave-One-Out cross-validation
+        (LOO) error (or the highest score=1-LOO). Default is `1`.
+    pce_q_norm : float
+        Hyperbolic (or q-norm) truncation for multi-indices of multivariate
+        polynomials. Default is `1.0`.
+    dim_red_method : str
+        Dimensionality reduction method for the output space. The available
+        method is based on principal component analysis (PCA). The Default is
+        `'no'`. There are two ways to select number of components: use
+        percentage of the explainable variance threshold (between 0 and 100)
+        (Option A) or direct prescription of components' number (Option B):
+
+            >>> MetaModelOpts.dim_red_method = 'PCA'
+            >>> MetaModelOpts.var_pca_threshold = 99.999  # Option A
+            >>> MetaModelOpts.n_pca_components = 12 # Option B
+    apply_constraints : bool
+        If set to true constraints will be applied during training. 
+        In this case the training uses OLS. In this version the constraints 
+        need to be set explicitly in this class.
+
+    verbose : bool
+        Prints summary of the regression results. Default is `False`.
+
+    Note
+    -------
+    To define the sampling methods and the training set, an experimental design
+    instance shall be defined. This can be done by:
+
+    >>> MetaModelOpts.add_InputSpace()
+
+    Two experimental design schemes are supported: one-shot (`normal`) and
+    adaptive sequential (`sequential`) designs.
+    For experimental design refer to `InputSpace`.
+
+    """
+
+    def __init__(self, input_obj, meta_model_type='PCE',
+                 pce_reg_method='OLS', bootstrap_method='fast',
+                 n_bootstrap_itrs=1, pce_deg=1, pce_q_norm=1.0,
+                 dim_red_method='no', apply_constraints = False, 
+                 verbose=False):
+
+        self.input_obj = input_obj
+        self.meta_model_type = meta_model_type
+        self.pce_reg_method = pce_reg_method
+        self.bootstrap_method = bootstrap_method
+        self.n_bootstrap_itrs = n_bootstrap_itrs
+        self.pce_deg = pce_deg
+        self.pce_q_norm = pce_q_norm
+        self.dim_red_method = dim_red_method
+        self.apply_constraints = apply_constraints
+        self.verbose = verbose
+ 
+    def build_metamodel(self, n_init_samples = None) -> None:
+        """
+        Builds the parts for the metamodel (polynomes,...) that are neede before fitting.
+
+        Returns
+        -------
+        None
+            DESCRIPTION.
+
+        """
+        
+        # Generate general warnings
+        if self.apply_constraints or self.pce_reg_method.lower() == 'ols':
+            print('There are no estimations of surrogate uncertainty available'
+                  ' for the chosen regression options. This might lead to issues'
+                  ' in later steps.')
+        
+        # Add InputSpace to MetaModel if it does not have any
+        if not hasattr(self, 'InputSpace'):
+            self.InputSpace = InputSpace(self.input_obj)
+            self.InputSpace.n_init_samples = n_init_samples
+            self.InputSpace.init_param_space(np.max(self.pce_deg))
+            
+        self.ndim = self.InputSpace.ndim
+        
+        if not hasattr(self, 'CollocationPoints'):
+            raise AttributeError('Please provide samples to the metamodel before building it.')
+            
+        # Transform input samples
+        # TODO: this is probably not yet correct! Make 'method' variable
+        self.CollocationPoints = self.InputSpace.transform(self.CollocationPoints, method='user') 
+
+        
+        self.n_params = len(self.input_obj.Marginals)
+        
+        # Generate polynomials
+        if self.meta_model_type.lower() != 'gpe':
+            self.generate_polynomials(np.max(self.pce_deg))
+
+        # Initialize the nested dictionaries
+        if self.meta_model_type.lower() == 'gpe':
+            self.gp_poly = self.auto_vivification()
+            self.x_scaler = self.auto_vivification()
+            self.LCerror = self.auto_vivification()
+        else:
+            self.deg_dict = self.auto_vivification()
+            self.q_norm_dict = self.auto_vivification()
+            self.coeffs_dict = self.auto_vivification()
+            self.basis_dict = self.auto_vivification()
+            self.score_dict = self.auto_vivification()
+            self.clf_poly = self.auto_vivification()
+            self.LCerror = self.auto_vivification()
+        if self.dim_red_method.lower() == 'pca':
+            self.pca = self.auto_vivification()
+
+        # Define an array containing the degrees
+        self.CollocationPoints = np.array(self.CollocationPoints)
+        self.n_samples, ndim = self.CollocationPoints.shape
+        if self.ndim != ndim:
+            raise AttributeError('The given samples do not match the given number of priors. The samples should be a 2D array of size (#samples, #priors)')
+            
+        self.deg_array = self.__select_degree(ndim, self.n_samples)
+
+        # Generate all basis indices
+        self.allBasisIndices = self.auto_vivification()
+        for deg in self.deg_array:
+            keys = self.allBasisIndices.keys()
+            if deg not in np.fromiter(keys, dtype=float):
+                # Generate the polynomial basis indices
+                for qidx, q in enumerate(self.pce_q_norm):
+                    basis_indices = glexindex(start=0, stop=deg+1,
+                                              dimensions=self.n_params,
+                                              cross_truncation=q,
+                                              reverse=False, graded=True)
+                    self.allBasisIndices[str(deg)][str(q)] = basis_indices
+
+        
+        
+    def fit(self, X, y, parallel = True, verbose = False):
+        """
+        Fits the surrogate to the given data (samples X, outputs y).
+        Note here that the samples X should be the transformed samples provided
+        by the experimental design if the transformation is used there.
+
+        Parameters
+        ----------
+        X : 2D list or np.array of shape (#samples, #dim)
+            The parameter value combinations that the model was evaluated at.
+        y : dict of 2D lists or arrays of shape (#samples, #timesteps)
+            The respective model evaluations.
+
+        Returns
+        -------
+        None.
+
+        """
+#        print(X)
+#        print(X.shape)
+#        print(y)
+#        print(y['Z'].shape)
+        X = np.array(X)
+        for key in y.keys():
+            y_val = np.array(y[key])
+            if y_val.ndim !=2:
+                raise ValueError('The given outputs y should be 2D')
+            y[key] = np.array(y[key])
+        
+        # Output names are the same as the keys in y
+        self.out_names = list(y.keys())
+        
+        # Build the MetaModel on the static samples
+        self.CollocationPoints = X
+        
+        # TODO: other option: rebuild every time
+        if not hasattr(self, 'deg_array'):
+            self.build_metamodel(n_init_samples = X.shape[1])
+            
+        # Evaluate the univariate polynomials on InputSpace
+        if self.meta_model_type.lower() != 'gpe':
+           self.univ_p_val = self.univ_basis_vals(self.CollocationPoints)
+        
+        # --- Loop through data points and fit the surrogate ---
+        if verbose:
+            print(f"\n>>>> Training the {self.meta_model_type} metamodel "
+                  "started. <<<<<<\n")
+
+        # --- Bootstrap sampling ---
+        # Correct number of bootstrap if PCA transformation is required.
+        if self.dim_red_method.lower() == 'pca' and self.n_bootstrap_itrs == 1:
+            self.n_bootstrap_itrs = 100
+
+        # Check if fast version (update coeffs with OLS) is selected.
+        if self.bootstrap_method.lower() == 'fast':
+            fast_bootstrap = True
+            first_out = {}
+            n_comp_dict = {}
+        else:
+            fast_bootstrap = False
+
+        # Prepare tqdm iteration maessage
+        if verbose and self.n_bootstrap_itrs > 1:
+            enum_obj = tqdm(range(self.n_bootstrap_itrs),
+                            total=self.n_bootstrap_itrs,
+                            desc="Bootstrapping the metamodel",
+                            ascii=True)
+        else:
+            enum_obj = range(self.n_bootstrap_itrs)
+
+        # Loop over the bootstrap iterations
+        for b_i in enum_obj:
+            if b_i > 0:
+                b_indices = np.random.randint(self.n_samples, size=self.n_samples)
+            else:
+                b_indices = np.arange(len(X))
+
+            X_train_b = X[b_indices]
+
+            if verbose and self.n_bootstrap_itrs == 1:
+                items = tqdm(y.items(), desc="Fitting regression")
+            else:
+                items = y.items()
+
+            # For loop over the components/outputs
+            for key, Output in items:
+
+                # Dimensionality reduction with PCA, if specified
+                if self.dim_red_method.lower() == 'pca':
+
+                    # Use the stored n_comp for fast bootsrtrapping
+                    if fast_bootstrap and b_i > 0:
+                        self.n_pca_components = n_comp_dict[key]
+
+                    # Start transformation
+                    pca, target, n_comp = self.pca_transformation(
+                        Output[b_indices], verbose=False
+                        )
+                    self.pca[f'b_{b_i+1}'][key] = pca
+                    # Store the number of components for fast bootsrtrapping
+                    if fast_bootstrap and b_i == 0:
+                        n_comp_dict[key] = n_comp
+                else:
+                    #print(b_indices)
+                    target = Output[b_indices]
+
+                # Parallel fit regression
+                if self.meta_model_type.lower() == 'gpe':
+                    # Prepare the input matrix
+                    scaler = MinMaxScaler()
+                    X_S = scaler.fit_transform(X_train_b)
+
+                    self.x_scaler[f'b_{b_i+1}'][key] = scaler
+                    if parallel:
+                        out = Parallel(n_jobs=-1, backend='multiprocessing')(
+                            delayed(self.gaussian_process_emulator)(
+                                X_S, target[:, idx]) for idx in
+                            range(target.shape[1]))
+                    else:
+                        results = map(self.gaussian_process_emulator,
+                                      [X_train_b]*target.shape[1],
+                                      [target[:, idx] for idx in
+                                       range(target.shape[1])]
+                                      )
+                        out = list(results)
+
+                    for idx in range(target.shape[1]):
+                        self.gp_poly[f'b_{b_i+1}'][key][f"y_{idx+1}"] = out[idx]
+
+                else:
+                    self.univ_p_val = self.univ_p_val[b_indices]
+                    if parallel and (not fast_bootstrap or b_i == 0):
+                        out = Parallel(n_jobs=-1, backend='multiprocessing')(
+                            delayed(self.adaptive_regression)(X_train_b,
+                                                              target[:, idx],
+                                                              idx)
+                            for idx in range(target.shape[1]))
+                    elif not parallel and (not fast_bootstrap or b_i == 0):
+                        results = map(self.adaptive_regression,
+                                      [X_train_b]*target.shape[1],
+                                      [target[:, idx] for idx in
+                                       range(target.shape[1])],
+                                      range(target.shape[1]))
+                        out = list(results)
+
+                    # Store the first out dictionary
+                    if fast_bootstrap and b_i == 0:
+                        first_out[key] = copy.deepcopy(out)
+
+                    if b_i > 0 and fast_bootstrap:
+
+                        # fast bootstrap
+                        out = self.update_pce_coeffs(
+                            X_train_b, target, first_out[key])
+
+                    for i in range(target.shape[1]):
+                        # Create a dict to pass the variables
+                        self.deg_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['degree']
+                        self.q_norm_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['qnorm']
+                        self.coeffs_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['coeffs']
+                        self.basis_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['multi_indices']
+                        self.score_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['LOOCVScore']
+                        self.clf_poly[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['clf_poly']
+                        #self.LCerror[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['LCerror']
+
+        if verbose:
+            print(f"\n>>>> Training the {self.meta_model_type} metamodel"
+                  " sucessfully completed. <<<<<<\n")
+
+    # -------------------------------------------------------------------------
+    def update_pce_coeffs(self, X, y, out_dict = None):
+        """
+        Updates the PCE coefficents using only the ordinary least square method
+        for the fast version of the bootstrapping.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples, n_params)
+            Training set.
+        y : array of shape (n_samples, n_outs)
+            The (transformed) model responses.
+        out_dict : dict
+            The training output dictionary of the first iteration, i.e.
+            the surrogate model for the original experimental design.
+
+        Returns
+        -------
+        final_out_dict : dict
+            The updated training output dictionary.
+
+        """
+        # Make a copy
+        final_out_dict = copy.deepcopy(out_dict)
+
+        # Loop over the points
+        for i in range(y.shape[1]):
+
+                    
+            # Extract nonzero basis indices
+            nnz_idx = np.nonzero(out_dict[i]['coeffs'])[0]
+            if len(nnz_idx) != 0:
+                basis_indices = out_dict[i]['multi_indices']
+
+                # Evaluate the multivariate polynomials on CollocationPoints
+                psi = self.create_psi(basis_indices, self.univ_p_val)
+
+                # Calulate the cofficients of surrogate model
+                updated_out = self.regression(
+                    psi, y[:, i], basis_indices, reg_method='OLS',
+                    sparsity=False
+                    )
+
+                # Update coeffs in out_dict
+                final_out_dict[i]['coeffs'][nnz_idx] = updated_out['coeffs']
+
+        return final_out_dict
+
+    # -------------------------------------------------------------------------
+    def add_InputSpace(self):
+        """
+        Instanciates experimental design object.
+
+        Returns
+        -------
+        None.
+
+        """
+        self.InputSpace = InputSpace(self.input_obj,
+                                    meta_Model_type=self.meta_model_type)
+
+    # -------------------------------------------------------------------------
+    def univ_basis_vals(self, samples, n_max=None):
+        """
+        Evaluates univariate regressors along input directions.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params)
+            Samples.
+        n_max : int, optional
+            Maximum polynomial degree. The default is `None`.
+
+        Returns
+        -------
+        univ_basis: array of shape (n_samples, n_params, n_max+1)
+            All univariate regressors up to n_max.
+        """
+        # Extract information
+        poly_types = self.InputSpace.poly_types
+        if samples.ndim != 2:
+            samples = samples.reshape(1, len(samples))
+        n_max = np.max(self.pce_deg) if n_max is None else n_max
+
+        # Extract poly coeffs
+        if self.InputSpace.input_data_given or self.InputSpace.apce:
+            apolycoeffs = self.polycoeffs
+        else:
+            apolycoeffs = None
+
+        # Evaluate univariate basis
+        univ_basis = eval_univ_basis(samples, n_max, poly_types, apolycoeffs)
+
+        return univ_basis
+
+    # -------------------------------------------------------------------------
+    def create_psi(self, basis_indices, univ_p_val):
+        """
+        This function assemble the design matrix Psi from the given basis index
+        set INDICES and the univariate polynomial evaluations univ_p_val.
+
+        Parameters
+        ----------
+        basis_indices : array of shape (n_terms, n_params)
+            Multi-indices of multivariate polynomials.
+        univ_p_val : array of (n_samples, n_params, n_max+1)
+            All univariate regressors up to `n_max`.
+
+        Raises
+        ------
+        ValueError
+            n_terms in arguments do not match.
+
+        Returns
+        -------
+        psi : array of shape (n_samples, n_terms)
+            Multivariate regressors.
+
+        """
+        # Check if BasisIndices is a sparse matrix
+        sparsity = sp.sparse.issparse(basis_indices)
+        if sparsity:
+            basis_indices = basis_indices.toarray()
+
+        # Initialization and consistency checks
+        # number of input variables
+        n_params = univ_p_val.shape[1]
+
+        # Size of the experimental design
+        n_samples = univ_p_val.shape[0]
+
+        # number of basis terms
+        n_terms = basis_indices.shape[0]
+
+        # check that the variables have consistent sizes
+        if n_params != basis_indices.shape[1]:
+            raise ValueError(
+                f"The shapes of basis_indices ({basis_indices.shape[1]}) and "
+                f"univ_p_val ({n_params}) don't match!!"
+                )
+
+        # Preallocate the Psi matrix for performance
+        psi = np.ones((n_samples, n_terms))
+        # Assemble the Psi matrix
+        for m in range(basis_indices.shape[1]):
+            aa = np.where(basis_indices[:, m] > 0)[0]
+            try:
+                basisIdx = basis_indices[aa, m]
+                bb = univ_p_val[:, m, basisIdx].reshape(psi[:, aa].shape)
+                psi[:, aa] = np.multiply(psi[:, aa], bb)
+            except ValueError as err:
+                raise err
+        return psi
+
+    # -------------------------------------------------------------------------
+    def regression(self, X, y, basis_indices, reg_method=None, sparsity=True):
+        """
+        Fit regression using the regression method provided.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples, n_features)
+            Training vector, where n_samples is the number of samples and
+            n_features is the number of features.
+        y : array of shape (n_samples,)
+            Target values.
+        basis_indices : array of shape (n_terms, n_params)
+            Multi-indices of multivariate polynomials.
+        reg_method : str, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        return_out_dict : Dict
+            Fitted estimator, spareMulti-Index, sparseX and coefficients.
+
+        """
+        if reg_method is None:
+            reg_method = self.pce_reg_method
+
+        bias_term = self.dim_red_method.lower() != 'pca'
+
+        compute_score = True if self.verbose else False
+
+        #  inverse of the observed variance of the data
+        if np.var(y) != 0:
+            Lambda = 1 / np.var(y)
+        else:
+            Lambda = 1e-6
+
+        # Bayes sparse adaptive aPCE
+        if reg_method.lower() == 'ols':
+            clf_poly = lm.LinearRegression(fit_intercept=False)
+        elif reg_method.lower() == 'brr':
+            clf_poly = lm.BayesianRidge(n_iter=1000, tol=1e-7,
+                                        fit_intercept=False,
+                                        #normalize=True,
+                                        compute_score=compute_score,
+                                        alpha_1=1e-04, alpha_2=1e-04,
+                                        lambda_1=Lambda, lambda_2=Lambda)
+            clf_poly.converged = True
+
+        elif reg_method.lower() == 'ard':
+            if X.shape[0]<2:
+                raise ValueError('Regression with ARD can only be performed for more than 2 samples')
+            clf_poly = lm.ARDRegression(fit_intercept=False,
+                                        #normalize=True,
+                                        compute_score=compute_score,
+                                        n_iter=1000, tol=0.0001,
+                                        alpha_1=1e-3, alpha_2=1e-3,
+                                        lambda_1=Lambda, lambda_2=Lambda)
+
+        elif reg_method.lower() == 'fastard':
+            clf_poly = RegressionFastARD(fit_intercept=False,
+                                         normalize=True,
+                                         compute_score=compute_score,
+                                         n_iter=300, tol=1e-10)
+
+        elif reg_method.lower() == 'bcs':
+            if X.shape[0]<10:
+                raise ValueError('Regression with BCS can only be performed for more than 10 samples')
+            clf_poly = RegressionFastLaplace(fit_intercept=False,
+                                         bias_term=bias_term,
+                                         n_iter=1000, tol=1e-7)
+
+        elif reg_method.lower() == 'lars':
+            if X.shape[0]<10:
+                raise ValueError('Regression with LARS can only be performed for more than 5 samples')
+            clf_poly = lm.LassoLarsCV(fit_intercept=False)
+
+        elif reg_method.lower() == 'sgdr':
+            clf_poly = lm.SGDRegressor(fit_intercept=False,
+                                       max_iter=5000, tol=1e-7)
+
+        elif reg_method.lower() == 'omp':
+            clf_poly = OrthogonalMatchingPursuit(fit_intercept=False)
+
+        elif reg_method.lower() == 'vbl':
+            clf_poly = VBLinearRegression(fit_intercept=False)
+
+        elif reg_method.lower() == 'ebl':
+            clf_poly = EBLinearRegression(optimizer='em')
+            
+        
+        # Training with constraints automatically uses L2
+        if self.apply_constraints:       
+            # TODO: set the constraints here
+            # Define the nonlin. constraint     
+            nlc = NonlinearConstraint(lambda x: np.matmul(X,x),-1,1.1)
+            self.nlc = nlc
+            
+            fun = lambda x: (np.linalg.norm(np.matmul(X, x)-y, ord = 2))**2
+            if self.init_type =='zeros':
+                res = minimize(fun, np.zeros(X.shape[1]), method = 'trust-constr', constraints  = self.nlc) 
+            if self.init_type == 'nonpi':
+                clf_poly.fit(X, y)
+                coeff = clf_poly.coef_
+                res = minimize(fun, coeff, method = 'trust-constr', constraints  = self.nlc)
+            
+            coeff = np.array(res.x)
+            clf_poly.coef_ = coeff
+            clf_poly.X = X
+            clf_poly.y = y
+            clf_poly.intercept_ = 0
+            
+        # Training without constraints uses chosen regression method
+        else:
+            clf_poly.fit(X, y)
+
+        # Select the nonzero entries of coefficients
+        if sparsity:
+            nnz_idx = np.nonzero(clf_poly.coef_)[0]
+        else:
+            nnz_idx = np.arange(clf_poly.coef_.shape[0])
+
+        # This is for the case where all outputs are zero, thereby
+        # all coefficients are zero
+        if (y == 0).all():
+            nnz_idx = np.insert(np.nonzero(clf_poly.coef_)[0], 0, 0)
+
+        sparse_basis_indices = basis_indices[nnz_idx]
+        sparse_X = X[:, nnz_idx]
+        coeffs = clf_poly.coef_[nnz_idx]
+        clf_poly.coef_ = coeffs
+
+        # Create a dict to pass the outputs
+        return_out_dict = dict()
+        return_out_dict['clf_poly'] = clf_poly
+        return_out_dict['spareMulti-Index'] = sparse_basis_indices
+        return_out_dict['sparePsi'] = sparse_X
+        return_out_dict['coeffs'] = coeffs
+        return return_out_dict
+    
+    # -------------------------------------------------------------------------
+    def create_psi(self, basis_indices, univ_p_val):
+        """
+        This function assemble the design matrix Psi from the given basis index
+        set INDICES and the univariate polynomial evaluations univ_p_val.
+
+        Parameters
+        ----------
+        basis_indices : array of shape (n_terms, n_params)
+            Multi-indices of multivariate polynomials.
+        univ_p_val : array of (n_samples, n_params, n_max+1)
+            All univariate regressors up to `n_max`.
+
+        Raises
+        ------
+        ValueError
+            n_terms in arguments do not match.
+
+        Returns
+        -------
+        psi : array of shape (n_samples, n_terms)
+            Multivariate regressors.
+
+        """
+        # Check if BasisIndices is a sparse matrix
+        sparsity = sp.sparse.issparse(basis_indices)
+        if sparsity:
+            basis_indices = basis_indices.toarray()
+
+        # Initialization and consistency checks
+        # number of input variables
+        n_params = univ_p_val.shape[1]
+
+        # Size of the experimental design
+        n_samples = univ_p_val.shape[0]
+
+        # number of basis terms
+        n_terms = basis_indices.shape[0]
+
+        # check that the variables have consistent sizes
+        if n_params != basis_indices.shape[1]:
+            raise ValueError(
+                f"The shapes of basis_indices ({basis_indices.shape[1]}) and "
+                f"univ_p_val ({n_params}) don't match!!"
+                )
+
+        # Preallocate the Psi matrix for performance
+        psi = np.ones((n_samples, n_terms))
+        # Assemble the Psi matrix
+        for m in range(basis_indices.shape[1]):
+            aa = np.where(basis_indices[:, m] > 0)[0]
+            try:
+                basisIdx = basis_indices[aa, m]
+                bb = univ_p_val[:, m, basisIdx].reshape(psi[:, aa].shape)
+                psi[:, aa] = np.multiply(psi[:, aa], bb)
+            except ValueError as err:
+                raise err
+        return psi
+
+    # --------------------------------------------------------------------------------------------------------
+    def adaptive_regression(self, ED_X, ED_Y, varIdx, verbose=False):
+        """
+        Adaptively fits the PCE model by comparing the scores of different
+        degrees and q-norm.
+
+        Parameters
+        ----------
+        ED_X : array of shape (n_samples, n_params)
+            Experimental design.
+        ED_Y : array of shape (n_samples,)
+            Target values, i.e. simulation results for the Experimental design.
+        varIdx : int
+            Index of the output.
+        verbose : bool, optional
+            Print out summary. The default is False.
+
+        Returns
+        -------
+        returnVars : Dict
+            Fitted estimator, best degree, best q-norm, LOOCVScore and
+            coefficients.
+
+        """
+
+        n_samples, n_params = ED_X.shape
+        # Initialization
+        qAllCoeffs, AllCoeffs = {}, {}
+        qAllIndices_Sparse, AllIndices_Sparse = {}, {}
+        qAllclf_poly, Allclf_poly = {}, {}
+        qAllnTerms, AllnTerms = {}, {}
+        qAllLCerror, AllLCerror = {}, {}
+
+        # Extract degree array and qnorm array
+        deg_array = np.array([*self.allBasisIndices], dtype=int)
+        qnorm = [*self.allBasisIndices[str(int(deg_array[0]))]]
+
+        # Some options for EarlyStop
+        errorIncreases = False
+        # Stop degree, if LOO error does not decrease n_checks_degree times
+        n_checks_degree = 3
+        # Stop qNorm, if criterion isn't fulfilled n_checks_qNorm times
+        n_checks_qNorm = 2
+        nqnorms = len(qnorm)
+        qNormEarlyStop = True
+        if nqnorms < n_checks_qNorm+1:
+            qNormEarlyStop = False
+
+        # =====================================================================
+        # basis adaptive polynomial chaos: repeat the calculation by increasing
+        # polynomial degree until the highest accuracy is reached
+        # =====================================================================
+        # For each degree check all q-norms and choose the best one
+        scores = -np.inf * np.ones(deg_array.shape[0])
+        qNormScores = -np.inf * np.ones(nqnorms)
+
+        for degIdx, deg in enumerate(deg_array):
+
+            for qidx, q in enumerate(qnorm):
+
+                # Extract the polynomial basis indices from the pool of
+                # allBasisIndices
+                BasisIndices = self.allBasisIndices[str(deg)][str(q)]
+
+                # Assemble the Psi matrix
+                Psi = self.create_psi(BasisIndices, self.univ_p_val)
+
+                # Calulate the cofficients of the meta model
+                outs = self.regression(Psi, ED_Y, BasisIndices)
+
+                # Calculate and save the score of LOOCV
+                score, LCerror = self.corr_loocv_error(outs['clf_poly'],
+                                                       outs['sparePsi'],
+                                                       outs['coeffs'],
+                                                       ED_Y)
+
+                # Check the convergence of noise for FastARD
+                if self.pce_reg_method == 'FastARD' and \
+                   outs['clf_poly'].alpha_ < np.finfo(np.float32).eps:
+                    score = -np.inf
+
+                qNormScores[qidx] = score
+                qAllCoeffs[str(qidx+1)] = outs['coeffs']
+                qAllIndices_Sparse[str(qidx+1)] = outs['spareMulti-Index']
+                qAllclf_poly[str(qidx+1)] = outs['clf_poly']
+                qAllnTerms[str(qidx+1)] = BasisIndices.shape[0]
+                qAllLCerror[str(qidx+1)] = LCerror
+
+                # EarlyStop check
+                # if there are at least n_checks_qNorm entries after the
+                # best one, we stop
+                if qNormEarlyStop and \
+                   sum(np.isfinite(qNormScores)) > n_checks_qNorm:
+                    # If the error has increased the last two iterations, stop!
+                    qNormScores_nonInf = qNormScores[np.isfinite(qNormScores)]
+                    deltas = np.sign(np.diff(qNormScores_nonInf))
+                    if sum(deltas[-n_checks_qNorm+1:]) == 2:
+                        # stop the q-norm loop here
+                        break
+                if np.var(ED_Y) == 0:
+                    break
+
+            # Store the score in the scores list
+            best_q = np.nanargmax(qNormScores)
+            scores[degIdx] = qNormScores[best_q]
+
+            AllCoeffs[str(degIdx+1)] = qAllCoeffs[str(best_q+1)]
+            AllIndices_Sparse[str(degIdx+1)] = qAllIndices_Sparse[str(best_q+1)]
+            Allclf_poly[str(degIdx+1)] = qAllclf_poly[str(best_q+1)]
+            AllnTerms[str(degIdx+1)] = qAllnTerms[str(best_q+1)]
+            AllLCerror[str(degIdx+1)] = qAllLCerror[str(best_q+1)]
+
+            # Check the direction of the error (on average):
+            # if it increases consistently stop the iterations
+            if len(scores[scores != -np.inf]) > n_checks_degree:
+                scores_nonInf = scores[scores != -np.inf]
+                ss = np.sign(scores_nonInf - np.max(scores_nonInf))
+                # ss<0 error decreasing
+                errorIncreases = np.sum(np.sum(ss[-2:])) <= -1*n_checks_degree
+
+            if errorIncreases:
+                break
+
+            # Check only one degree, if target matrix has zero variance
+            if np.var(ED_Y) == 0:
+                break
+
+        # ------------------ Summary of results ------------------
+        # Select the one with the best score and save the necessary outputs
+        best_deg = np.nanargmax(scores)+1
+        coeffs = AllCoeffs[str(best_deg)]
+        basis_indices = AllIndices_Sparse[str(best_deg)]
+        clf_poly = Allclf_poly[str(best_deg)]
+        LOOCVScore = np.nanmax(scores)
+        P = AllnTerms[str(best_deg)]
+        LCerror = AllLCerror[str(best_deg)]
+        degree = deg_array[np.nanargmax(scores)]
+        qnorm = float(qnorm[best_q])
+
+        # ------------------ Print out Summary of results ------------------
+        if self.verbose:
+            # Create PSI_Sparse by removing redundent terms
+            nnz_idx = np.nonzero(coeffs)[0]
+            BasisIndices_Sparse = basis_indices[nnz_idx]
+
+            print(f'Output variable {varIdx+1}:')
+            print('The estimation of PCE coefficients converged at polynomial '
+                  f'degree {deg_array[best_deg-1]} with '
+                  f'{len(BasisIndices_Sparse)} terms (Sparsity index = '
+                  f'{round(len(BasisIndices_Sparse)/P, 3)}).')
+
+            print(f'Final ModLOO error estimate: {1-max(scores):.3e}')
+            print('\n'+'-'*50)
+
+        if verbose:
+            print('='*50)
+            print(' '*10 + ' Summary of results ')
+            print('='*50)
+
+            print("Scores:\n", scores)
+            print("Degree of best score:", self.deg_array[best_deg-1])
+            print("No. of terms:", len(basis_indices))
+            print("Sparsity index:", round(len(basis_indices)/P, 3))
+            print("Best Indices:\n", basis_indices)
+
+            if self.pce_reg_method in ['BRR', 'ARD']:
+                fig, ax = plt.subplots(figsize=(12, 10))
+                plt.title("Marginal log-likelihood")
+                plt.plot(clf_poly.scores_, color='navy', linewidth=2)
+                plt.ylabel("Score")
+                plt.xlabel("Iterations")
+                if self.pce_reg_method.lower() == 'bbr':
+                    text = f"$\\alpha={clf_poly.alpha_:.1f}$\n"
+                    f"$\\lambda={clf_poly.lambda_:.3f}$\n"
+                    f"$L={clf_poly.scores_[-1]:.1f}$"
+                else:
+                    text = f"$\\alpha={clf_poly.alpha_:.1f}$\n$"
+                    f"\\L={clf_poly.scores_[-1]:.1f}$"
+
+                plt.text(0.75, 0.5, text, fontsize=18, transform=ax.transAxes)
+                plt.show()
+            print('='*80)
+
+        # Create a dict to pass the outputs
+        returnVars = dict()
+        returnVars['clf_poly'] = clf_poly
+        returnVars['degree'] = degree
+        returnVars['qnorm'] = qnorm
+        returnVars['coeffs'] = coeffs
+        returnVars['multi_indices'] = basis_indices
+        returnVars['LOOCVScore'] = LOOCVScore
+        returnVars['LCerror'] = LCerror
+
+        return returnVars
+
+    # -------------------------------------------------------------------------
+    def corr_loocv_error(self, clf, psi, coeffs, y):
+        """
+        Calculates the corrected LOO error for regression on regressor
+        matrix `psi` that generated the coefficients based on [1] and [2].
+
+        [1] Blatman, G., 2009. Adaptive sparse polynomial chaos expansions for
+            uncertainty propagation and sensitivity analysis (Doctoral
+            dissertation, Clermont-Ferrand 2).
+
+        [2] Blatman, G. and Sudret, B., 2011. Adaptive sparse polynomial chaos
+            expansion based on least angle regression. Journal of computational
+            Physics, 230(6), pp.2345-2367.
+
+        Parameters
+        ----------
+        clf : object
+            Fitted estimator.
+        psi : array of shape (n_samples, n_features)
+            The multivariate orthogonal polynomials (regressor).
+        coeffs : array-like of shape (n_features,)
+            Estimated cofficients.
+        y : array of shape (n_samples,)
+            Target values.
+
+        Returns
+        -------
+        R_2 : float
+            LOOCV Validation score (1-LOOCV erro).
+        residual : array of shape (n_samples,)
+            Residual values (y - predicted targets).
+
+        """
+        psi = np.array(psi, dtype=float)
+
+        # Create PSI_Sparse by removing redundent terms
+        nnz_idx = np.nonzero(coeffs)[0]
+        if len(nnz_idx) == 0:
+            nnz_idx = [0]
+        psi_sparse = psi[:, nnz_idx]
+
+        # NrCoeffs of aPCEs
+        P = len(nnz_idx)
+        # NrEvaluation (Size of experimental design)
+        N = psi.shape[0]
+
+        # Build the projection matrix
+        PsiTPsi = np.dot(psi_sparse.T, psi_sparse)
+
+        if np.linalg.cond(PsiTPsi) > 1e-12: #and \
+           # np.linalg.cond(PsiTPsi) < 1/sys.float_info.epsilon:
+            # faster
+            try:
+                M = sp.linalg.solve(PsiTPsi,
+                                sp.sparse.eye(PsiTPsi.shape[0]).toarray())
+            except:
+                raise AttributeError('There are too few samples for the corrected loo-cv error. Fit surrogate on at least as many samples as parameters to use this')
+        else:
+            # stabler
+            M = np.linalg.pinv(PsiTPsi)
+
+        # h factor (the full matrix is not calculated explicitly,
+        # only the trace is, to save memory)
+        PsiM = np.dot(psi_sparse, M)
+
+        h = np.sum(np.multiply(PsiM, psi_sparse), axis=1, dtype=np.longdouble)#float128)
+
+        # ------ Calculate Error Loocv for each measurement point ----
+        # Residuals
+        try:
+            residual = clf.predict(psi) - y
+        except:
+            residual = np.dot(psi, coeffs) - y
+
+        # Variance
+        var_y = np.var(y)
+
+        if var_y == 0:
+            norm_emp_error = 0
+            loo_error = 0
+            LCerror = np.zeros((y.shape))
+            return 1-loo_error, LCerror
+        else:
+            norm_emp_error = np.mean(residual**2)/var_y
+
+            # LCerror = np.divide(residual, (1-h))
+            LCerror = residual / (1-h)
+            loo_error = np.mean(np.square(LCerror)) / var_y
+            # if there are NaNs, just return an infinite LOO error (this
+            # happens, e.g., when a strongly underdetermined problem is solved)
+            if np.isnan(loo_error):
+                loo_error = np.inf
+
+        # Corrected Error for over-determined system
+        tr_M = np.trace(M)
+        if tr_M < 0 or abs(tr_M) > 1e6:
+            tr_M = np.trace(np.linalg.pinv(np.dot(psi.T, psi)))
+
+        # Over-determined system of Equation
+        if N > P:
+            T_factor = N/(N-P) * (1 + tr_M)
+
+        # Under-determined system of Equation
+        else:
+            T_factor = np.inf
+
+        corrected_loo_error = loo_error * T_factor
+
+        R_2 = 1 - corrected_loo_error
+
+        return R_2, LCerror
+
+    # -------------------------------------------------------------------------
+    def pca_transformation(self, target, verbose=False):
+        """
+        Transforms the targets (outputs) via Principal Component Analysis
+
+        Parameters
+        ----------
+        target : array of shape (n_samples,)
+            Target values.
+
+        Returns
+        -------
+        pca : obj
+            Fitted sklearnPCA object.
+        OutputMatrix : array of shape (n_samples,)
+            Transformed target values.
+        n_pca_components : int
+            Number of selected principal components.
+
+        """
+        # Transform via Principal Component Analysis
+        if hasattr(self, 'var_pca_threshold'):
+            var_pca_threshold = self.var_pca_threshold
+        else:
+            var_pca_threshold = 100.0
+        n_samples, n_features = target.shape
+
+        if hasattr(self, 'n_pca_components'):
+            n_pca_components = self.n_pca_components
+        else:
+            # Instantiate and fit sklearnPCA object
+            covar_matrix = sklearnPCA(n_components=None)
+            covar_matrix.fit(target)
+            var = np.cumsum(np.round(covar_matrix.explained_variance_ratio_,
+                                     decimals=5)*100)
+            # Find the number of components to explain self.varPCAThreshold of
+            # variance
+            try:
+                n_components = np.where(var >= var_pca_threshold)[0][0] + 1
+            except IndexError:
+                n_components = min(n_samples, n_features)
+
+            n_pca_components = min(n_samples, n_features, n_components)
+
+        # Print out a report
+        if verbose:
+            print()
+            print('-' * 50)
+            print(f"PCA transformation is performed with {n_pca_components}"
+                  " components.")
+            print('-' * 50)
+            print()
+
+        # Fit and transform with the selected number of components
+        pca = sklearnPCA(n_components=n_pca_components, svd_solver='arpack')
+        scaled_target = pca.fit_transform(target)
+
+        return pca, scaled_target, n_pca_components
+
+    # -------------------------------------------------------------------------
+    def gaussian_process_emulator(self, X, y, nug_term=None, autoSelect=False,
+                                  varIdx=None):
+        """
+        Fits a Gaussian Process Emulator to the target given the training
+         points.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples, n_params)
+            Training points.
+        y : array of shape (n_samples,)
+            Target values.
+        nug_term : float, optional
+            Nugget term. The default is None, i.e. variance of y.
+        autoSelect : bool, optional
+            Loop over some kernels and select the best. The default is False.
+        varIdx : int, optional
+            The index number. The default is None.
+
+        Returns
+        -------
+        gp : object
+            Fitted estimator.
+
+        """
+
+        nug_term = nug_term if nug_term else np.var(y)
+
+        Kernels = [nug_term * kernels.RBF(length_scale=1.0,
+                                          length_scale_bounds=(1e-25, 1e15)),
+                   nug_term * kernels.RationalQuadratic(length_scale=0.2,
+                                                        alpha=1.0),
+                   nug_term * kernels.Matern(length_scale=1.0,
+                                             length_scale_bounds=(1e-15, 1e5),
+                                             nu=1.5)]
+
+        # Automatic selection of the kernel
+        if autoSelect:
+            gp = {}
+            BME = []
+            for i, kernel in enumerate(Kernels):
+                gp[i] = GaussianProcessRegressor(kernel=kernel,
+                                                 n_restarts_optimizer=3,
+                                                 normalize_y=False)
+
+                # Fit to data using Maximum Likelihood Estimation
+                gp[i].fit(X, y)
+
+                # Store the MLE as BME score
+                BME.append(gp[i].log_marginal_likelihood())
+
+            gp = gp[np.argmax(BME)]
+
+        else:
+            gp = GaussianProcessRegressor(kernel=Kernels[0],
+                                          n_restarts_optimizer=3,
+                                          normalize_y=False)
+            gp.fit(X, y)
+
+        # Compute score
+        if varIdx is not None:
+            Score = gp.score(X, y)
+            print('-'*50)
+            print(f'Output variable {varIdx}:')
+            print('The estimation of GPE coefficients converged,')
+            print(f'with the R^2 score: {Score:.3f}')
+            print('-'*50)
+
+        return gp
+
+    # -------------------------------------------------------------------------
+    def eval_metamodel(self, samples):
+        """
+        Evaluates meta-model at the requested samples. One can also generate
+        nsamples.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Samples to evaluate meta-model at. The default is None.
+        nsamples : int, optional
+            Number of samples to generate, if no `samples` is provided. The
+            default is None.
+        sampling_method : str, optional
+            Type of sampling, if no `samples` is provided. The default is
+            'random'.
+        return_samples : bool, optional
+            Retun samples, if no `samples` is provided. The default is False.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean of the predictions.
+        std_pred : dict
+            Standard deviatioon of the predictions.
+        """
+        # Transform into np array - can also be given as list
+        samples = np.array(samples)
+        
+        # Transform samples to the independent space
+        samples = self.InputSpace.transform(
+            samples,
+            method='user'
+            )
+        # Compute univariate bases for the given samples
+        if self.meta_model_type.lower() != 'gpe':
+            univ_p_val = self.univ_basis_vals(
+                samples,
+                n_max=np.max(self.pce_deg)
+                )
+
+        mean_pred_b = {}
+        std_pred_b = {}
+        # Loop over bootstrap iterations
+        for b_i in range(self.n_bootstrap_itrs):
+
+            # Extract model dictionary
+            if self.meta_model_type.lower() == 'gpe':
+                model_dict = self.gp_poly[f'b_{b_i+1}']
+            else:
+                model_dict = self.coeffs_dict[f'b_{b_i+1}']
+
+            # Loop over outputs
+            mean_pred = {}
+            std_pred = {}
+            for output, values in model_dict.items():
+
+                mean = np.empty((len(samples), len(values)))
+                std = np.empty((len(samples), len(values)))
+                idx = 0
+                for in_key, InIdxValues in values.items():
+
+                    # Prediction with GPE
+                    if self.meta_model_type.lower() == 'gpe':
+                        X_T = self.x_scaler[f'b_{b_i+1}'][output].transform(samples)
+                        gp = self.gp_poly[f'b_{b_i+1}'][output][in_key]
+                        y_mean, y_std = gp.predict(X_T, return_std=True)
+
+                    else:
+                        # Prediction with PCE
+                        # Assemble Psi matrix
+                        basis = self.basis_dict[f'b_{b_i+1}'][output][in_key]
+                        psi = self.create_psi(basis, univ_p_val)
+
+                        # Prediction
+                        if self.bootstrap_method != 'fast' or b_i == 0:
+                            # with error bar, i.e. use clf_poly
+                            clf_poly = self.clf_poly[f'b_{b_i+1}'][output][in_key]
+                            try:
+                                y_mean, y_std = clf_poly.predict(
+                                    psi, return_std=True
+                                    )
+                            except TypeError:
+                                y_mean = clf_poly.predict(psi)
+                                y_std = np.zeros_like(y_mean)
+                        else:
+                            # without error bar
+                            coeffs = self.coeffs_dict[f'b_{b_i+1}'][output][in_key]
+                            y_mean = np.dot(psi, coeffs)
+                            y_std = np.zeros_like(y_mean)
+
+                    mean[:, idx] = y_mean
+                    std[:, idx] = y_std
+                    idx += 1
+
+                # Save predictions for each output
+                if self.dim_red_method.lower() == 'pca':
+                    PCA = self.pca[f'b_{b_i+1}'][output]
+                    mean_pred[output] = PCA.inverse_transform(mean)
+                    std_pred[output] = np.zeros(mean.shape)
+                else:
+                    mean_pred[output] = mean
+                    std_pred[output] = std
+
+            # Save predictions for each bootstrap iteration
+            mean_pred_b[b_i] = mean_pred
+            std_pred_b[b_i] = std_pred
+
+        # Change the order of nesting
+        mean_pred_all = {}
+        for i in sorted(mean_pred_b):
+            for k, v in mean_pred_b[i].items():
+                if k not in mean_pred_all:
+                    mean_pred_all[k] = [None] * len(mean_pred_b)
+                mean_pred_all[k][i] = v
+
+        # Compute the moments of predictions over the predictions
+        for output in self.out_names:
+            # Only use bootstraps with finite values
+            finite_rows = np.isfinite(
+                mean_pred_all[output]).all(axis=2).all(axis=1)
+            outs = np.asarray(mean_pred_all[output])[finite_rows]
+            # Compute mean
+            mean_pred[output] = np.mean(outs, axis=0)
+            # Compute standard deviation
+            if self.n_bootstrap_itrs > 1:
+                std_pred[output] = np.std(outs, axis=0)
+            else:
+                std_pred[output] = std_pred_b[b_i][output]
+
+        return mean_pred, std_pred
+
+    # -------------------------------------------------------------------------
+    def create_model_error(self, X, y, Model, name='Calib'):
+        """
+        Fits a GPE-based model error.
+
+        Parameters
+        ----------
+        X : array of shape (n_outputs, n_inputs)
+            Input array. It can contain any forcing inputs or coordinates of
+             extracted data.
+        y : array of shape (n_outputs,)
+            The model response for the MAP parameter set.
+        name : str, optional
+            Calibration or validation. The default is `'Calib'`.
+
+        Returns
+        -------
+        self: object
+            Self object.
+
+        """
+        outputNames = self.out_names
+        self.errorRegMethod = 'GPE'
+        self.errorclf_poly = self.auto_vivification()
+        self.errorScale = self.auto_vivification()
+
+        # Read data
+        # TODO: do this call outside the metamodel
+        MeasuredData = Model.read_observation(case=name)
+
+        # Fitting GPR based bias model
+        for out in outputNames:
+            nan_idx = ~np.isnan(MeasuredData[out])
+            # Select data
+            try:
+                data = MeasuredData[out].values[nan_idx]
+            except AttributeError:
+                data = MeasuredData[out][nan_idx]
+
+            # Prepare the input matrix
+            scaler = MinMaxScaler()
+            delta = data  # - y[out][0]
+            BiasInputs = np.hstack((X[out], y[out].reshape(-1, 1)))
+            X_S = scaler.fit_transform(BiasInputs)
+            gp = self.gaussian_process_emulator(X_S, delta)
+
+            self.errorScale[out]["y_1"] = scaler
+            self.errorclf_poly[out]["y_1"] = gp
+
+        return self
+
+    # -------------------------------------------------------------------------
+    def eval_model_error(self, X, y_pred):
+        """
+        Evaluates the error model.
+
+        Parameters
+        ----------
+        X : array
+            Inputs.
+        y_pred : dict
+            Predictions.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean predition of the GPE-based error model.
+        std_pred : dict
+            standard deviation of the GPE-based error model.
+
+        """
+        mean_pred = {}
+        std_pred = {}
+
+        for Outkey, ValuesDict in self.errorclf_poly.items():
+
+            pred_mean = np.zeros_like(y_pred[Outkey])
+            pred_std = np.zeros_like(y_pred[Outkey])
+
+            for Inkey, InIdxValues in ValuesDict.items():
+
+                gp = self.errorclf_poly[Outkey][Inkey]
+                scaler = self.errorScale[Outkey][Inkey]
+
+                # Transform Samples using scaler
+                for j, pred in enumerate(y_pred[Outkey]):
+                    BiasInputs = np.hstack((X[Outkey], pred.reshape(-1, 1)))
+                    Samples_S = scaler.transform(BiasInputs)
+                    y_hat, y_std = gp.predict(Samples_S, return_std=True)
+                    pred_mean[j] = y_hat
+                    pred_std[j] = y_std
+                    # pred_mean[j] += pred
+
+            mean_pred[Outkey] = pred_mean
+            std_pred[Outkey] = pred_std
+
+        return mean_pred, std_pred
+
+    # -------------------------------------------------------------------------
+    class auto_vivification(dict):
+        """
+        Implementation of perl's AutoVivification feature.
+
+        Source: https://stackoverflow.com/a/651879/18082457
+        """
+
+        def __getitem__(self, item):
+            try:
+                return dict.__getitem__(self, item)
+            except KeyError:
+                value = self[item] = type(self)()
+                return value
+
+    # -------------------------------------------------------------------------
+    def copy_meta_model_opts(self):
+        """
+        This method is a convinient function to copy the metamodel options.
+
+        Returns
+        -------
+        new_MetaModelOpts : object
+            The copied object.
+
+        """
+        # TODO: what properties should be moved to the new object?
+        new_MetaModelOpts = copy.deepcopy(self)
+        new_MetaModelOpts.input_obj = self.input_obj#InputObj
+        new_MetaModelOpts.InputSpace = self.InputSpace
+        #new_MetaModelOpts.InputSpace.meta_Model = 'aPCE'
+        #new_MetaModelOpts.InputSpace.InputObj = self.input_obj
+        #new_MetaModelOpts.InputSpace.ndim = len(self.input_obj.Marginals)
+        new_MetaModelOpts.n_params = len(self.input_obj.Marginals)
+        #new_MetaModelOpts.InputSpace.hdf5_file = None
+
+        return new_MetaModelOpts
+
+    # -------------------------------------------------------------------------
+    def __select_degree(self, ndim, n_samples):
+        """
+        Selects degree based on the number of samples and parameters in the
+        sequential design.
+
+        Parameters
+        ----------
+        ndim : int
+            Dimension of the parameter space.
+        n_samples : int
+            Number of samples.
+
+        Returns
+        -------
+        deg_array: array
+            Array containing the arrays.
+
+        """
+        # Define the deg_array
+        max_deg = np.max(self.pce_deg)
+        min_Deg = np.min(self.pce_deg)
+        
+        # TODO: remove the options for sequential?
+        #nitr = n_samples - self.InputSpace.n_init_samples
+
+        # Check q-norm
+        if not np.isscalar(self.pce_q_norm):
+            self.pce_q_norm = np.array(self.pce_q_norm)
+        else:
+            self.pce_q_norm = np.array([self.pce_q_norm])
+
+        def M_uptoMax(maxDeg):
+            n_combo = np.zeros(maxDeg)
+            for i, d in enumerate(range(1, maxDeg+1)):
+                n_combo[i] = math.factorial(ndim+d)
+                n_combo[i] /= math.factorial(ndim) * math.factorial(d)
+            return n_combo
+
+        deg_new = max_deg
+        #d = nitr if nitr != 0 and self.n_params > 5 else 1
+        # d = 1
+        # min_index = np.argmin(abs(M_uptoMax(max_deg)-ndim*n_samples*d))
+        # deg_new = range(1, max_deg+1)[min_index]
+
+        if deg_new > min_Deg and self.pce_reg_method.lower() != 'fastard':
+            deg_array = np.arange(min_Deg, deg_new+1)
+        else:
+            deg_array = np.array([deg_new])
+
+        return deg_array
+
+    def generate_polynomials(self, max_deg=None):
+        # Check for InputSpace
+        if not hasattr(self, 'InputSpace'):
+            raise AttributeError('Generate or add InputSpace before generating polynomials')
+            
+        ndim = self.InputSpace.ndim
+        # Create orthogonal polynomial coefficients if necessary
+        if (self.meta_model_type.lower()!='gpe') and max_deg is not None:# and self.input_obj.poly_coeffs_flag:
+            self.polycoeffs = {}
+            for parIdx in tqdm(range(ndim), ascii=True,
+                               desc="Computing orth. polynomial coeffs"):
+                poly_coeffs = apoly_construction(
+                    self.InputSpace.raw_data[parIdx],
+                    max_deg
+                    )
+                self.polycoeffs[f'p_{parIdx+1}'] = poly_coeffs
+        else:
+            raise AttributeError('MetaModel cannot generate polynomials in the given scenario!')
+
+    # -------------------------------------------------------------------------
+    def _compute_pce_moments(self):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moment (mean) of the surrogate.
+        pce_stds: dict
+            The second moment (standard deviation) of the surrogate.
+
+        """
+        
+        # Check if its truly a pce-surrogate
+        if self.meta_model_type.lower() == 'gpe':
+            raise AttributeError('Moments can only be computed for pce-type surrogates')
+        
+        outputs = self.out_names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(self.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = self.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = self.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = self.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if self.dim_red_method.lower() == 'pca':
+                    PCA = self.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.inverse_transform(pce_mean)
+                    stds[output] = PCA.inverse_transform(np.sqrt(pce_var))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+        return pce_means, pce_stds
diff --git a/examples/analytical-function/example_analytical_function.py b/examples/analytical-function/example_analytical_function.py
index 52e7731b576c42cdd29a705865f0a0389f812654..9ec23f8cdaae003c33bac4cfe75e9a1a4d82bc42 100644
--- a/examples/analytical-function/example_analytical_function.py
+++ b/examples/analytical-function/example_analytical_function.py
@@ -141,7 +141,7 @@ if __name__ == "__main__":
 
     # One-shot (normal) or Sequential Adaptive (sequential) Design
     ExpDesign.method = 'sequential'
-    ExpDesign.n_init_samples = 100#3*ndim
+    ExpDesign.n_init_samples = 140#00#3*ndim
 
     # Sampling methods
     # 1) random 2) latin_hypercube 3) sobol 4) halton 5) hammersley
@@ -271,7 +271,7 @@ if __name__ == "__main__":
     # BayesOpts.bootstrap_noise = 100
 
     # Bayesian cross validation
-    # BayesOpts.bayes_loocv = True
+    BayesOpts.bayes_loocv = True  # TODO: test what this does
 
     # Select the inference method
     import emcee
diff --git a/examples/model-comparison/bayesvalidrox/__init__.py b/examples/model-comparison/bayesvalidrox/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..8e865af80652b8dd29203c2c85f8d1c717e335bc
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/__init__.py
@@ -0,0 +1,25 @@
+# -*- coding: utf-8 -*-
+__version__ = "0.0.5"
+
+from .pylink.pylink import PyLinkForwardModel
+from .surrogate_models.surrogate_models import MetaModel
+#from .surrogate_models.meta_model_engine import MetaModelEngine
+from .surrogate_models.engine import Engine
+from .surrogate_models.inputs import Input
+from .post_processing.post_processing import PostProcessing
+from .bayes_inference.bayes_inference import BayesInference
+from .bayes_inference.bayes_model_comparison import BayesModelComparison
+from .bayes_inference.discrepancy import Discrepancy
+
+__all__ = [
+    "__version__",
+    "PyLinkForwardModel",
+    "Input",
+    "Discrepancy",
+    "MetaModel",
+    #"MetaModelEngine",
+    "Engine",
+    "PostProcessing",
+    "BayesInference",
+    "BayesModelComparison"
+    ]
diff --git a/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..beaab3c798a63fcfbc361982388fdf10830a787e
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b1f65d567bf46826c3f2f76b1c730caf924b8f43
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..d5ebaedb4c8b77b5d7dbd0a6945f09079d8b10e4
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__init__.py b/examples/model-comparison/bayesvalidrox/bayes_inference/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..df8d935680f96ab487cf087866e8bfd504762945
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/bayes_inference/__init__.py
@@ -0,0 +1,9 @@
+# -*- coding: utf-8 -*-
+
+from .bayes_inference import BayesInference
+from .mcmc import MCMC
+
+__all__ = [
+    "BayesInference",
+    "MCMC"
+    ]
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..430b9885a8c8bd658da24bbc4ac1a6a0a74f69e6
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..72c63a98588c54dfec12536a99537cfa3a67e0cf
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..287257c1ca9b3f3a6d7e176e006ad432f8c685bf
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e89dfb5e6b3a873ac2f40dcc2084aa52caaedcd6
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..88adfbcabb95e11d86659db42240c5fa87390c2e
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a37330680c29bbfdba5a1bfd98041dda24957604
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_inference.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..57322b839ff7d50ea32d3b36ce011c09cf91e232
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f8cf9ea94d01b774b2d634e4e0caa06ef9bf6843
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b58f792ac36504c702205afc34228600f9bbba77
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/bayes_model_comparison.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bc71688a4cae4f74ec3a67838fca659881c520c9
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5301d0c093a62cd323cbd18cdf99d5eed93ad76c
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f154800917aeaef9c499abf13d47fcca6ffc639b
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/discrepancy.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1d2122246d54ec5697803cbce28900e077b2306a
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6177da43e7573677803782f2472399b0cb79a672
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b9fe4689524895c2149048d489b22b08e85026df
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/bayes_inference/__pycache__/mcmc.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/bayes_inference.py b/examples/model-comparison/bayesvalidrox/bayes_inference/bayes_inference.py
new file mode 100644
index 0000000000000000000000000000000000000000..1898a8ae619597d92bc355ac4249f57019f0aed7
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/bayes_inference/bayes_inference.py
@@ -0,0 +1,1532 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import os
+import copy
+import pandas as pd
+from tqdm import tqdm
+from scipy import stats
+import scipy.linalg as spla
+import joblib
+import seaborn as sns
+import corner
+import h5py
+import multiprocessing
+import gc
+from sklearn.metrics import mean_squared_error, r2_score
+from sklearn import preprocessing
+from matplotlib.patches import Patch
+import matplotlib.lines as mlines
+from matplotlib.backends.backend_pdf import PdfPages
+import matplotlib.pylab as plt
+
+from .mcmc import MCMC
+
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class BayesInference:
+    """
+    A class to perform Bayesian Analysis.
+
+
+    Attributes
+    ----------
+    MetaModel : obj
+        Meta model object.
+    discrepancy : obj
+        The discrepancy object for the sigma2s, i.e. the diagonal entries
+        of the variance matrix for a multivariate normal likelihood.
+    name : str, optional
+        The type of analysis, either calibration (`Calib`) or validation
+        (`Valid`). The default is `'Calib'`.
+    emulator : bool, optional
+        Analysis with emulator (MetaModel). The default is `True`.
+    bootstrap : bool, optional
+        Bootstrap the analysis. The default is `False`.
+    req_outputs : list, optional
+        The list of requested output to be used for the analysis.
+        The default is `None`. If None, all the defined outputs for the model
+        object is used.
+    selected_indices : dict, optional
+        A dictionary with the selected indices of each model output. The
+        default is `None`. If `None`, all measurement points are used in the
+        analysis.
+    samples : array of shape (n_samples, n_params), optional
+        The samples to be used in the analysis. The default is `None`. If
+        None the samples are drawn from the probablistic input parameter
+        object of the MetaModel object.
+    n_samples : int, optional
+        Number of samples to be used in the analysis. The default is `500000`.
+        If samples is not `None`, this argument will be assigned based on the
+        number of samples given.
+    measured_data : dict, optional
+        A dictionary containing the observation data. The default is `None`.
+        if `None`, the observation defined in the Model object of the
+        MetaModel is used.
+    inference_method : str, optional
+        A method for approximating the posterior distribution in the Bayesian
+        inference step. The default is `'rejection'`, which stands for
+        rejection sampling. A Markov Chain Monte Carlo sampler can be simply
+        selected by passing `'MCMC'`.
+    mcmc_params : dict, optional
+        A dictionary with args required for the Bayesian inference with
+        `MCMC`. The default is `None`.
+
+        Pass the mcmc_params like the following:
+
+            >>> mcmc_params:{
+                'init_samples': None,  # initial samples
+                'n_walkers': 100,  # number of walkers (chain)
+                'n_steps': 100000,  # number of maximum steps
+                'n_burn': 200,  # number of burn-in steps
+                'moves': None,  # Moves for the emcee sampler
+                'multiprocessing': False,  # multiprocessing
+                'verbose': False # verbosity
+                }
+        The items shown above are the default values. If any parmeter is
+        not defined, the default value will be assigned to it.
+    bayes_loocv : bool, optional
+        Bayesian Leave-one-out Cross Validation. The default is `False`. If
+        `True`, the LOOCV procedure is used to estimate the bayesian Model
+        Evidence (BME).
+    n_bootstrap_itrs : int, optional
+        Number of bootstrap iteration. The default is `1`. If bayes_loocv is
+        `True`, this is qualt to the total length of the observation data
+        set.
+    perturbed_data : array of shape (n_bootstrap_itrs, n_obs), optional
+        User defined perturbed data. The default is `[]`.
+    bootstrap_noise : float, optional
+        A noise level to perturb the data set. The default is `0.05`.
+    just_analysis : bool, optional
+        Justifiability analysis. The default is False.
+    valid_metrics : list, optional
+        List of the validation metrics. The following metrics are supported:
+
+        1. log_BME : logarithm of the Bayesian model evidence
+        2. KLD : Kullback-Leibler Divergence
+        3. inf_entropy: Information entropy
+        The default is `['log_BME']`.
+    plot_post_pred : bool, optional
+        Plot posterior predictive plots. The default is `True`.
+    plot_map_pred : bool, optional
+        Plot the model outputs vs the metamodel predictions for the maximum
+        a posteriori (defined as `max_a_posteriori`) parameter set. The
+        default is `False`.
+    max_a_posteriori : str, optional
+        Maximum a posteriori. `'mean'` and `'mode'` are available. The default
+        is `'mean'`.
+    corner_title_fmt : str, optional
+        Title format for the posterior distribution plot with python
+        package `corner`. The default is `'.2e'`.
+
+    """
+
+    def __init__(self, engine, MetaModel = None, discrepancy=None, emulator=True,
+                 name='Calib', bootstrap=False, req_outputs=None,
+                 selected_indices=None, samples=None, n_samples=100000,
+                 measured_data=None, inference_method='rejection',
+                 mcmc_params=None, bayes_loocv=False, n_bootstrap_itrs=1,
+                 perturbed_data=[], bootstrap_noise=0.05, just_analysis=False,
+                 valid_metrics=['BME'], plot_post_pred=True,
+                 plot_map_pred=False, max_a_posteriori='mean',
+                 corner_title_fmt='.2e'):
+
+        self.engine = engine
+        self.MetaModel = engine.MetaModel
+        self.Discrepancy = discrepancy
+        self.emulator = emulator
+        self.name = name
+        self.bootstrap = bootstrap
+        self.req_outputs = req_outputs
+        self.selected_indices = selected_indices
+        self.samples = samples
+        self.n_samples = n_samples
+        self.measured_data = measured_data
+        self.inference_method = inference_method
+        self.mcmc_params = mcmc_params
+        self.perturbed_data = perturbed_data
+        self.bayes_loocv = bayes_loocv
+        self.n_bootstrap_itrs = n_bootstrap_itrs
+        self.bootstrap_noise = bootstrap_noise
+        self.just_analysis = just_analysis
+        self.valid_metrics = valid_metrics
+        self.plot_post_pred = plot_post_pred
+        self.plot_map_pred = plot_map_pred
+        self.max_a_posteriori = max_a_posteriori
+        self.corner_title_fmt = corner_title_fmt
+
+    # -------------------------------------------------------------------------
+    def create_inference(self):
+        """
+        Starts the inference.
+
+        Returns
+        -------
+        BayesInference : obj
+            The Bayes inference object.
+
+        """
+
+        # Set some variables
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+        n_params = MetaModel.n_params
+        output_names = Model.Output.names
+        par_names = self.engine.ExpDesign.par_names
+
+        # If the prior is set by the user, take it.
+        if self.samples is None:
+            self.samples = self.engine.ExpDesign.generate_samples(
+                self.n_samples, 'random')
+        else:
+            try:
+                samples = self.samples.values
+            except AttributeError:
+                samples = self.samples
+
+            # Take care of an additional Sigma2s
+            self.samples = samples[:, :n_params]
+
+            # Update number of samples
+            self.n_samples = self.samples.shape[0]
+
+        # ---------- Preparation of observation data ----------
+        # Read observation data and perturb it if requested.
+        if self.measured_data is None:
+            self.measured_data = Model.read_observation(case=self.name)
+        # Convert measured_data to a data frame
+        if not isinstance(self.measured_data, pd.DataFrame):
+            self.measured_data = pd.DataFrame(self.measured_data)
+
+        # Extract the total number of measurement points
+        if self.name.lower() == 'calib':
+            self.n_tot_measurement = Model.n_obs
+        else:
+            self.n_tot_measurement = Model.n_obs_valid
+
+        # Find measurement error (if not given) for post predictive plot
+        if not hasattr(self, 'measurement_error'):
+            if isinstance(self.Discrepancy, dict):
+                Disc = self.Discrepancy['known']
+            else:
+                Disc = self.Discrepancy
+            if isinstance(Disc.parameters, dict):
+                self.measurement_error = {k: np.sqrt(Disc.parameters[k]) for k
+                                          in Disc.parameters.keys()}
+            else:
+                try:
+                    self.measurement_error = np.sqrt(Disc.parameters)
+                except TypeError:
+                    pass
+
+        # ---------- Preparation of variance for covariance matrix ----------
+        # Independent and identically distributed
+        total_sigma2 = dict()
+        opt_sigma_flag = isinstance(self.Discrepancy, dict)
+        opt_sigma = None
+        for key_idx, key in enumerate(output_names):
+
+            # Find opt_sigma
+            if opt_sigma_flag and opt_sigma is None:
+                # Option A: known error with unknown bias term
+                opt_sigma = 'A'
+                known_discrepancy = self.Discrepancy['known']
+                self.Discrepancy = self.Discrepancy['infer']
+                sigma2 = np.array(known_discrepancy.parameters[key])
+
+            elif opt_sigma == 'A' or self.Discrepancy.parameters is not None:
+                # Option B: The sigma2 is known (no bias term)
+                if opt_sigma == 'A':
+                    sigma2 = np.array(known_discrepancy.parameters[key])
+                else:
+                    opt_sigma = 'B'
+                    sigma2 = np.array(self.Discrepancy.parameters[key])
+
+            elif not isinstance(self.Discrepancy.InputDisc, str):
+                # Option C: The sigma2 is unknown (bias term including error)
+                opt_sigma = 'C'
+                self.Discrepancy.opt_sigma = opt_sigma
+                n_measurement = self.measured_data[key].values.shape
+                sigma2 = np.zeros((n_measurement[0]))
+
+            total_sigma2[key] = sigma2
+
+            self.Discrepancy.opt_sigma = opt_sigma
+            self.Discrepancy.total_sigma2 = total_sigma2
+
+        # If inferred sigma2s obtained from e.g. calibration are given
+        try:
+            self.sigma2s = self.Discrepancy.get_sample(self.n_samples)
+        except:
+            pass
+
+        # ---------------- Bootstrap & TOM --------------------
+        if self.bootstrap or self.bayes_loocv or self.just_analysis:
+            if len(self.perturbed_data) == 0:
+                # zero mean noise Adding some noise to the observation function
+                self.perturbed_data = self._perturb_data(
+                    self.measured_data, output_names
+                    )
+            else:
+                self.n_bootstrap_itrs = len(self.perturbed_data)
+
+            # -------- Model Discrepancy -----------
+            if hasattr(self, 'error_model') and self.error_model \
+               and self.name.lower() != 'calib':
+                # Select posterior mean as MAP
+                MAP_theta = self.samples.mean(axis=0).reshape((1, n_params))
+                # MAP_theta = stats.mode(self.samples,axis=0)[0]
+
+                # Evaluate the (meta-)model at the MAP
+                y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=MAP_theta)
+
+                # Train a GPR meta-model using MAP
+                self.error_MetaModel = MetaModel.create_model_error(
+                    self.bias_inputs, y_MAP, Name=self.name
+                    )
+
+            # -----------------------------------------------------
+            # ----- Loop over the perturbed observation data ------
+            # -----------------------------------------------------
+            # Initilize arrays
+            logLikelihoods = np.zeros((self.n_samples, self.n_bootstrap_itrs),
+                                      dtype=np.float16)
+            BME_Corr = np.zeros((self.n_bootstrap_itrs))
+            log_BME = np.zeros((self.n_bootstrap_itrs))
+            KLD = np.zeros((self.n_bootstrap_itrs))
+            inf_entropy = np.zeros((self.n_bootstrap_itrs))
+
+            # Compute the prior predtions
+            # Evaluate the MetaModel
+            if self.emulator:
+                y_hat, y_std = MetaModel.eval_metamodel(samples=self.samples)
+                self.__mean_pce_prior_pred = y_hat
+                self._std_pce_prior_pred = y_std
+
+                # Correct the predictions with Model discrepancy
+                if hasattr(self, 'error_model') and self.error_model:
+                    y_hat_corr, y_std = self.error_MetaModel.eval_model_error(
+                        self.bias_inputs, self.__mean_pce_prior_pred
+                        )
+                    self.__mean_pce_prior_pred = y_hat_corr
+                    self._std_pce_prior_pred = y_std
+
+                # Surrogate model's error using RMSE of test data
+                if hasattr(MetaModel, 'rmse'):
+                    surrError = MetaModel.rmse
+                else:
+                    surrError = None
+
+            else:
+                # Evaluate the original model
+                self.__model_prior_pred = self._eval_model(
+                    samples=self.samples, key='PriorPred'
+                    )
+                surrError = None
+
+            # Start the likelihood-BME computations for the perturbed data
+            for itr_idx, data in tqdm(
+                    enumerate(self.perturbed_data),
+                    total=self.n_bootstrap_itrs,
+                    desc="Bootstrapping the BME calculations", ascii=True
+                    ):
+
+                # ---------------- Likelihood calculation ----------------
+                if self.emulator:
+                    model_evals = self.__mean_pce_prior_pred
+                else:
+                    model_evals = self.__model_prior_pred
+
+                # Leave one out
+                if self.bayes_loocv or self.just_analysis:
+                    self.selected_indices = np.nonzero(data)[0]
+
+                # Prepare data dataframe
+                nobs = list(self.measured_data.count().values[1:])
+                numbers = list(np.cumsum(nobs))
+                indices = list(zip([0] + numbers, numbers))
+                data_dict = {
+                    output_names[i]: data[j:k] for i, (j, k) in
+                    enumerate(indices)
+                    }
+                #print(output_names)
+                #print(indices)
+                #print(numbers)
+                #print(nobs)
+                #print(self.measured_data)
+                #for i, (j, k) in enumerate(indices):
+                #    print(i,j,k)
+                #print(data)
+                #print(data_dict)
+                #stop
+
+                # Unknown sigma2
+                if opt_sigma == 'C' or hasattr(self, 'sigma2s'):
+                    logLikelihoods[:, itr_idx] = self.normpdf(
+                        model_evals, data_dict, total_sigma2,
+                        sigma2=self.sigma2s, std=surrError
+                        )
+                else:
+                    # known sigma2
+                    logLikelihoods[:, itr_idx] = self.normpdf(
+                        model_evals, data_dict, total_sigma2,
+                        std=surrError
+                        )
+
+                # ---------------- BME Calculations ----------------
+                # BME (log)
+                log_BME[itr_idx] = np.log(
+                    np.nanmean(np.exp(logLikelihoods[:, itr_idx],
+                                      dtype=np.longdouble))#float128))
+                    )
+
+                # BME correction when using Emulator
+                if self.emulator:
+                    BME_Corr[itr_idx] = self.__corr_factor_BME(
+                        data_dict, total_sigma2, log_BME[itr_idx]
+                        )
+
+                # Rejection Step
+                if 'kld' in list(map(str.lower, self.valid_metrics)) and\
+                   'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+                    # Random numbers between 0 and 1
+                    unif = np.random.rand(1, self.n_samples)[0]
+
+                    # Reject the poorly performed prior
+                    Likelihoods = np.exp(logLikelihoods[:, itr_idx],
+                                         dtype=np.float64)
+                    accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+                    posterior = self.samples[accepted]
+
+                    # Posterior-based expectation of likelihoods
+                    postExpLikelihoods = np.mean(
+                        logLikelihoods[:, itr_idx][accepted]
+                        )
+
+                    # Calculate Kullback-Leibler Divergence
+                    KLD[itr_idx] = postExpLikelihoods - log_BME[itr_idx]
+
+                # Posterior-based expectation of prior densities
+                if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+                    n_thread = int(0.875 * multiprocessing.cpu_count())
+                    with multiprocessing.Pool(n_thread) as p:
+                        postExpPrior = np.mean(np.concatenate(
+                            p.map(
+                                self.engine.ExpDesign.JDist.pdf,
+                                np.array_split(posterior.T, n_thread, axis=1))
+                            )
+                            )
+                    # Information Entropy based on Entropy paper Eq. 38
+                    inf_entropy[itr_idx] = log_BME[itr_idx] - postExpPrior - \
+                        postExpLikelihoods
+
+                # Clear memory
+                gc.collect(generation=2)
+
+            # ---------- Store metrics for perturbed data set ----------------
+            # Likelihoods (Size: n_samples, n_bootstrap_itr)
+            self.log_likes = logLikelihoods
+
+            # BME (log), KLD, infEntropy (Size: 1,n_bootstrap_itr)
+            self.log_BME = log_BME
+
+            # BMECorrFactor (log) (Size: 1,n_bootstrap_itr)
+            if self.emulator:
+                self.log_BME_corr_factor = BME_Corr
+
+            if 'kld' in list(map(str.lower, self.valid_metrics)):
+                self.KLD = KLD
+            if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+                self.inf_entropy = inf_entropy
+
+            # BME = BME + BMECorrFactor
+            if self.emulator:
+                self.log_BME += self.log_BME_corr_factor
+
+        # ---------------- Parameter Bayesian inference ----------------
+        if self.inference_method.lower() == 'mcmc':
+            # Instantiate the MCMC object
+            MCMC_Obj = MCMC(self)
+            self.posterior_df = MCMC_Obj.run_sampler(
+                self.measured_data, total_sigma2
+                )
+
+        elif self.name.lower() == 'valid':
+            # Convert to a dataframe if samples are provided after calibration.
+            self.posterior_df = pd.DataFrame(self.samples, columns=par_names)
+
+        else:
+            # Rejection sampling
+            self.posterior_df = self._rejection_sampling()
+
+        # Provide posterior's summary
+        print('\n')
+        print('-'*15 + 'Posterior summary' + '-'*15)
+        pd.options.display.max_columns = None
+        pd.options.display.max_rows = None
+        print(self.posterior_df.describe())
+        print('-'*50)
+
+        # -------- Model Discrepancy -----------
+        if hasattr(self, 'error_model') and self.error_model \
+           and self.name.lower() == 'calib':
+            if self.inference_method.lower() == 'mcmc':
+                self.error_MetaModel = MCMC_Obj.error_MetaModel
+            else:
+                # Select posterior mean as MAP
+                if opt_sigma == "B":
+                    posterior_df = self.posterior_df.values
+                else:
+                    posterior_df = self.posterior_df.values[:, :-Model.n_outputs]
+
+                # Select posterior mean as Maximum a posteriori
+                map_theta = posterior_df.mean(axis=0).reshape((1, n_params))
+                # map_theta = stats.mode(Posterior_df,axis=0)[0]
+
+                # Evaluate the (meta-)model at the MAP
+                y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=map_theta)
+
+                # Train a GPR meta-model using MAP
+                self.error_MetaModel = MetaModel.create_model_error(
+                    self.bias_inputs, y_MAP, Name=self.name
+                    )
+
+        # -------- Posterior perdictive -----------
+        self._posterior_predictive()
+
+        # -----------------------------------------------------
+        # ------------------ Visualization --------------------
+        # -----------------------------------------------------
+        # Create Output directory, if it doesn't exist already.
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        os.makedirs(out_dir, exist_ok=True)
+
+        # -------- Posteior parameters --------
+        if opt_sigma != "B":
+            par_names.extend(
+                [self.Discrepancy.InputDisc.Marginals[i].name for i
+                 in range(len(self.Discrepancy.InputDisc.Marginals))]
+                )
+        # Pot with corner
+        figPosterior = corner.corner(self.posterior_df.to_numpy(),
+                                     labels=par_names,
+                                     quantiles=[0.15, 0.5, 0.85],
+                                     show_titles=True,
+                                     title_fmt=self.corner_title_fmt,
+                                     labelpad=0.2,
+                                     use_math_text=True,
+                                     title_kwargs={"fontsize": 28},
+                                     plot_datapoints=False,
+                                     plot_density=False,
+                                     fill_contours=True,
+                                     smooth=0.5,
+                                     smooth1d=0.5)
+
+        # Loop over axes and set x limits
+        if opt_sigma == "B":
+            axes = np.array(figPosterior.axes).reshape(
+                (len(par_names), len(par_names))
+                )
+            for yi in range(len(par_names)):
+                ax = axes[yi, yi]
+                ax.set_xlim(self.engine.ExpDesign.bound_tuples[yi])
+                for xi in range(yi):
+                    ax = axes[yi, xi]
+                    ax.set_xlim(self.engine.ExpDesign.bound_tuples[xi])
+        plt.close()
+
+        # Turn off gridlines
+        for ax in figPosterior.axes:
+            ax.grid(False)
+
+        if self.emulator:
+            plotname = f'/Posterior_Dist_{Model.name}_emulator'
+        else:
+            plotname = f'/Posterior_Dist_{Model.name}'
+
+        figPosterior.set_size_inches((24, 16))
+        figPosterior.savefig(f'./{out_dir}{plotname}.pdf',
+                             bbox_inches='tight')
+
+        # -------- Plot MAP --------
+        if self.plot_map_pred:
+            self._plot_max_a_posteriori()
+
+        # -------- Plot log_BME dist --------
+        if self.bootstrap:
+
+            # Computing the TOM performance
+            self.log_BME_tom = stats.chi2.rvs(
+                self.n_tot_measurement, size=self.log_BME.shape[0]
+                )
+
+            fig, ax = plt.subplots()
+            sns.kdeplot(self.log_BME_tom, ax=ax, color="green", shade=True)
+            sns.kdeplot(
+                self.log_BME, ax=ax, color="blue", shade=True,
+                label='Model BME')
+
+            ax.set_xlabel('log$_{10}$(BME)')
+            ax.set_ylabel('Probability density')
+
+            legend_elements = [
+                Patch(facecolor='green', edgecolor='green', label='TOM BME'),
+                Patch(facecolor='blue', edgecolor='blue', label='Model BME')
+                ]
+            ax.legend(handles=legend_elements)
+
+            if self.emulator:
+                plotname = f'/BME_hist_{Model.name}_emulator'
+            else:
+                plotname = f'/BME_hist_{Model.name}'
+
+            plt.savefig(f'./{out_dir}{plotname}.pdf', bbox_inches='tight')
+            plt.show()
+            plt.close()
+
+        # -------- Posteior perdictives --------
+        if self.plot_post_pred:
+            # Plot the posterior predictive
+            self._plot_post_predictive()
+
+        return self
+
+    # -------------------------------------------------------------------------
+    def _perturb_data(self, data, output_names):
+        """
+        Returns an array with n_bootstrap_itrs rowsof perturbed data.
+        The first row includes the original observation data.
+        If `self.bayes_loocv` is True, a 2d-array will be returned with
+        repeated rows and zero diagonal entries.
+
+        Parameters
+        ----------
+        data : pandas DataFrame
+            Observation data.
+        output_names : list
+            List of the output names.
+
+        Returns
+        -------
+        final_data : array
+            Perturbed data set.
+
+        """
+        noise_level = self.bootstrap_noise
+        obs_data = data[output_names].values
+        n_measurement, n_outs = obs_data.shape
+        self.n_tot_measurement = obs_data[~np.isnan(obs_data)].shape[0]
+        # Number of bootstrap iterations
+        if self.bayes_loocv:
+            self.n_bootstrap_itrs = self.n_tot_measurement
+
+        # Pass loocv dataset
+        if self.bayes_loocv:
+            obs = obs_data.T[~np.isnan(obs_data.T)]
+            final_data = np.repeat(np.atleast_2d(obs), self.n_bootstrap_itrs,
+                                   axis=0)
+            np.fill_diagonal(final_data, 0)
+            return final_data
+
+        else:
+            final_data = np.zeros(
+                (self.n_bootstrap_itrs, self.n_tot_measurement)
+                )
+            final_data[0] = obs_data.T[~np.isnan(obs_data.T)]
+            for itrIdx in range(1, self.n_bootstrap_itrs):
+                data = np.zeros((n_measurement, n_outs))
+                for idx in range(len(output_names)):
+                    std = np.nanstd(obs_data[:, idx])
+                    if std == 0:
+                        std = 0.001
+                    noise = std * noise_level
+                    data[:, idx] = np.add(
+                        obs_data[:, idx],
+                        np.random.normal(0, 1, obs_data.shape[0]) * noise,
+                    )
+
+                final_data[itrIdx] = data.T[~np.isnan(data.T)]
+
+            return final_data
+
+    # -------------------------------------------------------------------------
+    def _logpdf(self, x, mean, cov):
+        """
+        computes the likelihood based on a multivariate normal distribution.
+
+        Parameters
+        ----------
+        x : TYPE
+            DESCRIPTION.
+        mean : array_like
+            Observation data.
+        cov : 2d array
+            Covariance matrix of the distribution.
+
+        Returns
+        -------
+        log_lik : float
+            Log likelihood.
+
+        """
+        n = len(mean)
+        L = spla.cholesky(cov, lower=True)
+        beta = np.sum(np.log(np.diag(L)))
+        dev = x - mean
+        alpha = dev.dot(spla.cho_solve((L, True), dev))
+        log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+        return log_lik
+
+    # -------------------------------------------------------------------------
+    def _eval_model(self, samples=None, key='MAP'):
+        """
+        Evaluates Forward Model.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Parameter sets. The default is None.
+        key : str, optional
+            Key string to be passed to the run_model_parallel method.
+            The default is 'MAP'.
+
+        Returns
+        -------
+        model_outputs : dict
+            Model outputs.
+
+        """
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+
+        if samples is None:
+            self.samples = self.engine.ExpDesign.generate_samples(
+                self.n_samples, 'random')
+        else:
+            self.samples = samples
+            self.n_samples = len(samples)
+
+        model_outputs, _ = Model.run_model_parallel(
+            self.samples, key_str=key+self.name)
+
+        # Clean up
+        # Zip the subdirectories
+        try:
+            dir_name = f'{Model.name}MAP{self.name}'
+            key = dir_name + '_'
+            Model.zip_subdirs(dir_name, key)
+        except:
+            pass
+
+        return model_outputs
+
+    # -------------------------------------------------------------------------
+    def _kernel_rbf(self, X, hyperparameters):
+        """
+        Isotropic squared exponential kernel.
+
+        Higher l values lead to smoother functions and therefore to coarser
+        approximations of the training data. Lower l values make functions
+        more wiggly with wide uncertainty regions between training data points.
+
+        sigma_f controls the marginal variance of b(x)
+
+        Parameters
+        ----------
+        X : ndarray of shape (n_samples_X, n_features)
+
+        hyperparameters : Dict
+            Lambda characteristic length
+            sigma_f controls the marginal variance of b(x)
+            sigma_0 unresolvable error nugget term, interpreted as random
+                    error that cannot be attributed to measurement error.
+        Returns
+        -------
+        var_cov_matrix : ndarray of shape (n_samples_X,n_samples_X)
+            Kernel k(X, X).
+
+        """
+        from sklearn.gaussian_process.kernels import RBF
+        min_max_scaler = preprocessing.MinMaxScaler()
+        X_minmax = min_max_scaler.fit_transform(X)
+
+        nparams = len(hyperparameters)
+        # characteristic length (0,1]
+        Lambda = hyperparameters[0]
+        # sigma_f controls the marginal variance of b(x)
+        sigma2_f = hyperparameters[1]
+
+        # cov_matrix = sigma2_f*rbf_kernel(X_minmax, gamma = 1/Lambda**2)
+
+        rbf = RBF(length_scale=Lambda)
+        cov_matrix = sigma2_f * rbf(X_minmax)
+        if nparams > 2:
+            # (unresolvable error) nugget term that is interpreted as random
+            # error that cannot be attributed to measurement error.
+            sigma2_0 = hyperparameters[2:]
+            for i, j in np.ndindex(cov_matrix.shape):
+                cov_matrix[i, j] += np.sum(sigma2_0) if i == j else 0
+
+        return cov_matrix
+
+    # -------------------------------------------------------------------------
+    def normpdf(self, outputs, obs_data, total_sigma2s, sigma2=None, std=None):
+        """
+        Calculates the likelihood of simulation outputs compared with
+        observation data.
+
+        Parameters
+        ----------
+        outputs : dict
+            A dictionary containing the simulation outputs as array of shape
+            (n_samples, n_measurement) for each model output.
+        obs_data : dict
+            A dictionary/dataframe containing the observation data.
+        total_sigma2s : dict
+            A dictionary with known values of the covariance diagonal entries,
+            a.k.a sigma^2.
+        sigma2 : array, optional
+            An array of the sigma^2 samples, when the covariance diagonal
+            entries are unknown and are being jointly inferred. The default is
+            None.
+        std : dict, optional
+            A dictionary containing the root mean squared error as array of
+            shape (n_samples, n_measurement) for each model output. The default
+            is None.
+
+        Returns
+        -------
+        logLik : array of shape (n_samples)
+            Likelihoods.
+
+        """
+        Model = self.engine.Model
+        logLik = 0.0
+
+        # Extract the requested model outputs for likelihood calulation
+        if self.req_outputs is None:
+            req_outputs = Model.Output.names
+        else:
+            req_outputs = list(self.req_outputs)
+
+        # Loop over the outputs
+        for idx, out in enumerate(req_outputs):
+
+            # (Meta)Model Output
+            nsamples, nout = outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Add the std of the PCE is chosen as emulator.
+            if self.emulator:
+                if std is not None:
+                    tot_sigma2s += std[out]**2
+
+            # Covariance Matrix
+            covMatrix = np.diag(tot_sigma2s)
+
+            # Select the data points to compare
+            try:
+                indices = self.selected_indices[out]
+            except:
+                indices = list(range(nout))
+            covMatrix = np.diag(covMatrix[indices, indices])
+
+            # If sigma2 is not given, use given total_sigma2s
+            if sigma2 is None:
+                logLik += stats.multivariate_normal.logpdf(
+                    outputs[out][:, indices], data[indices], covMatrix)
+                continue
+
+            # Loop over each run/sample and calculate logLikelihood
+            logliks = np.zeros(nsamples)
+            for s_idx in range(nsamples):
+
+                # Simulation run
+                tot_outputs = outputs[out]
+
+                # Covariance Matrix
+                covMatrix = np.diag(tot_sigma2s)
+
+                if sigma2 is not None:
+                    # Check the type error term
+                    if hasattr(self, 'bias_inputs') and \
+                       not hasattr(self, 'error_model'):
+                        # Infer a Bias model usig Gaussian Process Regression
+                        bias_inputs = np.hstack(
+                            (self.bias_inputs[out],
+                             tot_outputs[s_idx].reshape(-1, 1)))
+
+                        params = sigma2[s_idx, idx*3:(idx+1)*3]
+                        covMatrix = self._kernel_rbf(bias_inputs, params)
+                    else:
+                        # Infer equal sigma2s
+                        try:
+                            sigma_2 = sigma2[s_idx, idx]
+                        except TypeError:
+                            sigma_2 = 0.0
+
+                        covMatrix += sigma_2 * np.eye(nout)
+                        # covMatrix = np.diag(sigma2 * total_sigma2s)
+
+                # Select the data points to compare
+                try:
+                    indices = self.selected_indices[out]
+                except:
+                    indices = list(range(nout))
+                covMatrix = np.diag(covMatrix[indices, indices])
+
+                # Compute loglikelihood
+                logliks[s_idx] = self._logpdf(
+                    tot_outputs[s_idx, indices], data[indices], covMatrix
+                    )
+
+            logLik += logliks
+        return logLik
+
+    # -------------------------------------------------------------------------
+    def _corr_factor_BME_old(self, Data, total_sigma2s, posterior):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        OrigModelOutput = self.engine.ExpDesign.Y
+        Model = self.engine.Model
+
+        # Posterior with guassian-likelihood
+        postDist = stats.gaussian_kde(posterior.T)
+
+        # Remove NaN
+        Data = Data[~np.isnan(Data)]
+        total_sigma2s = total_sigma2s[~np.isnan(total_sigma2s)]
+
+        # Covariance Matrix
+        covMatrix = np.diag(total_sigma2s[:self.n_tot_measurement])
+
+        # Extract the requested model outputs for likelihood calulation
+        if self.req_outputs is None:
+            OutputType = Model.Output.names
+        else:
+            OutputType = list(self.req_outputs)
+
+        # SampleSize = OrigModelOutput[OutputType[0]].shape[0]
+
+
+        # Flatten the OutputType for OrigModel
+        TotalOutputs = np.concatenate([OrigModelOutput[x] for x in OutputType], 1)
+
+        NrofBayesSamples = self.n_samples
+        # Evaluate MetaModel on the experimental design
+        Samples = self.engine.ExpDesign.X
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=Samples)
+
+        # Reset the NrofSamples to NrofBayesSamples
+        self.n_samples = NrofBayesSamples
+
+        # Flatten the OutputType for MetaModel
+        TotalPCEOutputs = np.concatenate([OutputRS[x] for x in OutputRS], 1)
+        TotalPCEstdOutputRS= np.concatenate([stdOutputRS[x] for x in stdOutputRS], 1)
+
+        logweight = 0
+        for i, sample in enumerate(Samples):
+            # Compute likelilhood output vs RS
+            covMatrix = np.diag(TotalPCEstdOutputRS[i]**2)
+            logLik = self._logpdf(TotalOutputs[i], TotalPCEOutputs[i], covMatrix)
+            # Compute posterior likelihood of the collocation points
+            logpostLik = np.log(postDist.pdf(sample[:, None]))[0]
+            if logpostLik != -np.inf:
+                logweight += logLik + logpostLik
+        return logweight
+
+    # -------------------------------------------------------------------------
+    def __corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = self.engine.ExpDesign.X
+        model_outputs = self.engine.ExpDesign.Y
+        Model = self.engine.Model
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = Model.Output.names
+
+        # Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                covMatrix = np.eye(len(y_m)) * 1/(2*np.pi)
+
+                # Select the data points to compare
+                try:
+                    indices = self.selected_indices[out]
+                except:
+                    indices = list(range(nout))
+                covMatrix = np.diag(covMatrix[indices, indices])
+                covMatrix_data = np.diag(covMatrix_data[indices, indices])
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += self._logpdf(
+                    y_m_hat[indices], data[indices],
+                    covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += self._logpdf(
+                    y_m_hat[indices], y_m[indices],
+                    covMatrix
+                    )
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.mean(np.exp(logLik_model+logLik_data))
+
+        return np.log(weights)
+
+    # -------------------------------------------------------------------------
+    def _rejection_sampling(self):
+        """
+        Performs rejection sampling to update the prior distribution on the
+        input parameters.
+
+        Returns
+        -------
+        posterior : pandas.dataframe
+            Posterior samples of the input parameters.
+
+        """
+
+        MetaModel = self.MetaModel
+        try:
+            sigma2_prior = self.Discrepancy.sigma2_prior
+        except:
+            sigma2_prior = None
+
+        # Check if the discrepancy is defined as a distribution:
+        samples = self.samples
+
+        if sigma2_prior is not None:
+            samples = np.hstack((samples, sigma2_prior))
+
+        # Take the first column of Likelihoods (Observation data without noise)
+        if self.just_analysis or self.bayes_loocv:
+            index = self.n_tot_measurement-1
+            likelihoods = np.exp(self.log_likes[:, index], dtype=np.longdouble)#np.float128)
+        else:
+            likelihoods = np.exp(self.log_likes[:, 0], dtype=np.longdouble)#np.float128)
+
+        n_samples = len(likelihoods)
+        norm_ikelihoods = likelihoods / np.max(likelihoods)
+
+        # Normalize based on min if all Likelihoods are zero
+        if all(likelihoods == 0.0):
+            likelihoods = self.log_likes[:, 0]
+            norm_ikelihoods = likelihoods / np.min(likelihoods)
+
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, n_samples)[0]
+
+        # Reject the poorly performed prior
+        accepted_samples = samples[norm_ikelihoods >= unif]
+
+        # Output the Posterior
+        par_names = self.engine.ExpDesign.par_names
+        if sigma2_prior is not None:
+            for name in self.Discrepancy.name:
+                par_names.append(name)
+
+        return pd.DataFrame(accepted_samples, columns=sigma2_prior)
+
+    # -------------------------------------------------------------------------
+    def _posterior_predictive(self):
+        """
+        Stores the prior- and posterior predictive samples, i.e. model
+        evaluations using the samples, into hdf5 files.
+
+        priorPredictive.hdf5 : Prior predictive samples.
+        postPredictive_wo_noise.hdf5 : Posterior predictive samples without
+        the additive noise.
+        postPredictive.hdf5 : Posterior predictive samples with the additive
+        noise.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+
+        # Make a directory to save the prior/posterior predictive
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        os.makedirs(out_dir, exist_ok=True)
+
+        # Read observation data and perturb it if requested
+        if self.measured_data is None:
+            self.measured_data = Model.read_observation(case=self.name)
+
+        if not isinstance(self.measured_data, pd.DataFrame):
+            self.measured_data = pd.DataFrame(self.measured_data)
+
+        # X_values
+        x_values = self.engine.ExpDesign.x_values
+
+        try:
+            sigma2_prior = self.Discrepancy.sigma2_prior
+        except:
+            sigma2_prior = None
+
+        # Extract posterior samples
+        posterior_df = self.posterior_df
+
+        # Take care of the sigma2
+        if sigma2_prior is not None:
+            try:
+                sigma2s = posterior_df[self.Discrepancy.name].values
+                posterior_df = posterior_df.drop(
+                    labels=self.Discrepancy.name, axis=1
+                    )
+            except:
+                sigma2s = self.sigma2s
+
+        # Posterior predictive
+        if self.emulator:
+            if self.inference_method == 'rejection':
+                prior_pred = self.__mean_pce_prior_pred
+            if self.name.lower() != 'calib':
+                post_pred = self.__mean_pce_prior_pred
+                post_pred_std = self._std_pce_prior_pred
+            else:
+                post_pred, post_pred_std = MetaModel.eval_metamodel(
+                    samples=posterior_df.values
+                    )
+
+        else:
+            if self.inference_method == 'rejection':
+                prior_pred = self.__model_prior_pred
+            if self.name.lower() != 'calib':
+                post_pred = self.__mean_pce_prior_pred,
+                post_pred_std = self._std_pce_prior_pred
+            else:
+                post_pred = self._eval_model(
+                    samples=posterior_df.values, key='PostPred'
+                    )
+        # Correct the predictions with Model discrepancy
+        if hasattr(self, 'error_model') and self.error_model:
+            y_hat, y_std = self.error_MetaModel.eval_model_error(
+                self.bias_inputs, post_pred
+                )
+            post_pred, post_pred_std = y_hat, y_std
+
+        # Add discrepancy from likelihood Sample to the current posterior runs
+        total_sigma2 = self.Discrepancy.total_sigma2
+        post_pred_withnoise = copy.deepcopy(post_pred)
+        for varIdx, var in enumerate(Model.Output.names):
+            for i in range(len(post_pred[var])):
+                pred = post_pred[var][i]
+
+                # Known sigma2s
+                clean_sigma2 = total_sigma2[var][~np.isnan(total_sigma2[var])]
+                tot_sigma2 = clean_sigma2[:len(pred)]
+                cov = np.diag(tot_sigma2)
+
+                # Check the type error term
+                if sigma2_prior is not None:
+                    # Inferred sigma2s
+                    if hasattr(self, 'bias_inputs') and \
+                       not hasattr(self, 'error_model'):
+                        # TODO: Infer a Bias model usig GPR
+                        bias_inputs = np.hstack((
+                            self.bias_inputs[var], pred.reshape(-1, 1)))
+                        params = sigma2s[i, varIdx*3:(varIdx+1)*3]
+                        cov = self._kernel_rbf(bias_inputs, params)
+                    else:
+                        # Infer equal sigma2s
+                        try:
+                            sigma2 = sigma2s[i, varIdx]
+                        except TypeError:
+                            sigma2 = 0.0
+
+                        # Convert biasSigma2s to a covMatrix
+                        cov += sigma2 * np.eye(len(pred))
+
+                if self.emulator:
+                    if hasattr(MetaModel, 'rmse') and \
+                       MetaModel.rmse is not None:
+                        stdPCE = MetaModel.rmse[var]
+                    else:
+                        stdPCE = post_pred_std[var][i]
+                    # Expected value of variance (Assump: i.i.d stds)
+                    cov += np.diag(stdPCE**2)
+
+                # Sample a multivariate normal distribution with mean of
+                # prediction and variance of cov
+                post_pred_withnoise[var][i] = np.random.multivariate_normal(
+                    pred, cov, 1
+                    )
+
+        # ----- Prior Predictive -----
+        if self.inference_method.lower() == 'rejection':
+            # Create hdf5 metadata
+            hdf5file = f'{out_dir}/priorPredictive.hdf5'
+            hdf5_exist = os.path.exists(hdf5file)
+            if hdf5_exist:
+                os.remove(hdf5file)
+            file = h5py.File(hdf5file, 'a')
+
+            # Store x_values
+            if type(x_values) is dict:
+                grp_x_values = file.create_group("x_values/")
+                for varIdx, var in enumerate(Model.Output.names):
+                    grp_x_values.create_dataset(var, data=x_values[var])
+            else:
+                file.create_dataset("x_values", data=x_values)
+
+            # Store posterior predictive
+            grpY = file.create_group("EDY/")
+            for varIdx, var in enumerate(Model.Output.names):
+                grpY.create_dataset(var, data=prior_pred[var])
+
+        # ----- Posterior Predictive only model evaluations -----
+        # Create hdf5 metadata
+        hdf5file = out_dir+'/postPredictive_wo_noise.hdf5'
+        hdf5_exist = os.path.exists(hdf5file)
+        if hdf5_exist:
+            os.remove(hdf5file)
+        file = h5py.File(hdf5file, 'a')
+
+        # Store x_values
+        if type(x_values) is dict:
+            grp_x_values = file.create_group("x_values/")
+            for varIdx, var in enumerate(Model.Output.names):
+                grp_x_values.create_dataset(var, data=x_values[var])
+        else:
+            file.create_dataset("x_values", data=x_values)
+
+        # Store posterior predictive
+        grpY = file.create_group("EDY/")
+        for varIdx, var in enumerate(Model.Output.names):
+            grpY.create_dataset(var, data=post_pred[var])
+
+        # ----- Posterior Predictive with noise -----
+        # Create hdf5 metadata
+        hdf5file = out_dir+'/postPredictive.hdf5'
+        hdf5_exist = os.path.exists(hdf5file)
+        if hdf5_exist:
+            os.remove(hdf5file)
+        file = h5py.File(hdf5file, 'a')
+
+        # Store x_values
+        if type(x_values) is dict:
+            grp_x_values = file.create_group("x_values/")
+            for varIdx, var in enumerate(Model.Output.names):
+                grp_x_values.create_dataset(var, data=x_values[var])
+        else:
+            file.create_dataset("x_values", data=x_values)
+
+        # Store posterior predictive
+        grpY = file.create_group("EDY/")
+        for varIdx, var in enumerate(Model.Output.names):
+            grpY.create_dataset(var, data=post_pred_withnoise[var])
+
+        return
+
+    # -------------------------------------------------------------------------
+    def _plot_max_a_posteriori(self):
+        """
+        Plots the response of the model output against that of the metamodel at
+        the maximum a posteriori sample (mean or mode of posterior.)
+
+        Returns
+        -------
+        None.
+
+        """
+
+        MetaModel = self.MetaModel
+        Model = self.engine.Model
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        opt_sigma = self.Discrepancy.opt_sigma
+
+        # -------- Find MAP and run MetaModel and origModel --------
+        # Compute the MAP
+        if self.max_a_posteriori.lower() == 'mean':
+            if opt_sigma == "B":
+                Posterior_df = self.posterior_df.values
+            else:
+                Posterior_df = self.posterior_df.values[:, :-Model.n_outputs]
+            map_theta = Posterior_df.mean(axis=0).reshape(
+                (1, MetaModel.n_params))
+        else:
+            map_theta = stats.mode(Posterior_df.values, axis=0)[0]
+        # Prin report
+        print("\nPoint estimator:\n", map_theta[0])
+
+        # Run the models for MAP
+        # MetaModel
+        map_metamodel_mean, map_metamodel_std = MetaModel.eval_metamodel(
+            samples=map_theta)
+        self.map_metamodel_mean = map_metamodel_mean
+        self.map_metamodel_std = map_metamodel_std
+
+        # origModel
+        map_orig_model = self._eval_model(samples=map_theta)
+        self.map_orig_model = map_orig_model
+
+        # Extract slicing index
+        x_values = map_orig_model['x_values']
+
+        # List of markers and colors
+        Color = ['k', 'b', 'g', 'r']
+        Marker = 'x'
+
+        # Create a PdfPages object
+        pdf = PdfPages(f'./{out_dir}MAP_PCE_vs_Model_{self.name}.pdf')
+        fig = plt.figure()
+        for i, key in enumerate(Model.Output.names):
+
+            y_val = map_orig_model[key]
+            y_pce_val = map_metamodel_mean[key]
+            y_pce_val_std = map_metamodel_std[key]
+
+            plt.plot(x_values, y_val, color=Color[i], marker=Marker,
+                     lw=2.0, label='$Y_{MAP}^{M}$')
+
+            plt.plot(
+                x_values, y_pce_val[i], color=Color[i], lw=2.0,
+                marker=Marker, linestyle='--', label='$Y_{MAP}^{PCE}$'
+                )
+            # plot the confidence interval
+            plt.fill_between(
+                x_values, y_pce_val[i] - 1.96*y_pce_val_std[i],
+                y_pce_val[i] + 1.96*y_pce_val_std[i],
+                color=Color[i], alpha=0.15
+                )
+
+            # Calculate the adjusted R_squared and RMSE
+            R2 = r2_score(y_pce_val.reshape(-1, 1), y_val.reshape(-1, 1))
+            rmse = np.sqrt(mean_squared_error(y_pce_val, y_val))
+
+            plt.ylabel(key)
+            plt.xlabel("Time [s]")
+            plt.title(f'Model vs MetaModel {key}')
+
+            ax = fig.axes[0]
+            leg = ax.legend(loc='best', frameon=True)
+            fig.canvas.draw()
+            p = leg.get_window_extent().inverse_transformed(ax.transAxes)
+            ax.text(
+                p.p0[1]-0.05, p.p1[1]-0.25,
+                f'RMSE = {rmse:.3f}\n$R^2$ = {R2:.3f}',
+                transform=ax.transAxes, color='black',
+                bbox=dict(facecolor='none', edgecolor='black',
+                          boxstyle='round,pad=1'))
+
+            plt.show()
+
+            # save the current figure
+            pdf.savefig(fig, bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+
+        pdf.close()
+
+    # -------------------------------------------------------------------------
+    def _plot_post_predictive(self):
+        """
+        Plots the posterior predictives against the observation data.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        Model = self.engine.Model
+        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        # Plot the posterior predictive
+        for out_idx, out_name in enumerate(Model.Output.names):
+            fig, ax = plt.subplots()
+            with sns.axes_style("ticks"):
+                x_key = list(self.measured_data)[0]
+
+                # --- Read prior and posterior predictive ---
+                if self.inference_method == 'rejection' and \
+                   self.name.lower() != 'valid':
+                    #  --- Prior ---
+                    # Load posterior predictive
+                    f = h5py.File(
+                        f'{out_dir}/priorPredictive.hdf5', 'r+')
+
+                    try:
+                        x_coords = np.array(f[f"x_values/{out_name}"])
+                    except:
+                        x_coords = np.array(f["x_values"])
+
+                    X_values = np.repeat(x_coords, 10000)
+
+                    prior_pred_df = {}
+                    prior_pred_df[x_key] = X_values
+                    prior_pred_df[out_name] = np.array(
+                        f[f"EDY/{out_name}"])[:10000].flatten('F')
+                    prior_pred_df = pd.DataFrame(prior_pred_df)
+
+                    tags_post = ['prior'] * len(prior_pred_df)
+                    prior_pred_df.insert(
+                        len(prior_pred_df.columns), "Tags", tags_post,
+                        True)
+                    f.close()
+
+                    # --- Posterior ---
+                    f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+')
+
+                    X_values = np.repeat(
+                        x_coords, np.array(f[f"EDY/{out_name}"]).shape[0])
+
+                    post_pred_df = {}
+                    post_pred_df[x_key] = X_values
+                    post_pred_df[out_name] = np.array(
+                        f[f"EDY/{out_name}"]).flatten('F')
+
+                    post_pred_df = pd.DataFrame(post_pred_df)
+
+                    tags_post = ['posterior'] * len(post_pred_df)
+                    post_pred_df.insert(
+                        len(post_pred_df.columns), "Tags", tags_post, True)
+                    f.close()
+                    # Concatenate two dataframes based on x_values
+                    frames = [prior_pred_df, post_pred_df]
+                    all_pred_df = pd.concat(frames)
+
+                    # --- Plot posterior predictive ---
+                    sns.violinplot(
+                        x_key, y=out_name, data=all_pred_df, hue="Tags",
+                        legend=False, ax=ax, split=True, inner=None,
+                        color=".8")
+
+                    # --- Plot Data ---
+                    # Find the x,y coordinates for each point
+                    x_coords = np.arange(x_coords.shape[0])
+                    first_header = list(self.measured_data)[0]
+                    obs_data = self.measured_data.round({first_header: 6})
+                    sns.pointplot(
+                        x=first_header, y=out_name, color='g', markers='x',
+                        linestyles='', capsize=16, data=obs_data, ax=ax)
+
+                    ax.errorbar(
+                        x_coords, obs_data[out_name].values,
+                        yerr=1.96*self.measurement_error[out_name],
+                        ecolor='g', fmt=' ', zorder=-1)
+
+                    # Add labels to the legend
+                    handles, labels = ax.get_legend_handles_labels()
+                    labels.append('Data')
+
+                    data_marker = mlines.Line2D(
+                        [], [], color='lime', marker='+', linestyle='None',
+                        markersize=10)
+                    handles.append(data_marker)
+
+                    # Add legend
+                    ax.legend(handles=handles, labels=labels, loc='best',
+                              fontsize='large', frameon=True)
+
+                else:
+                    # Load posterior predictive
+                    f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+')
+
+                    try:
+                        x_coords = np.array(f[f"x_values/{out_name}"])
+                    except:
+                        x_coords = np.array(f["x_values"])
+
+                    mu = np.mean(np.array(f[f"EDY/{out_name}"]), axis=0)
+                    std = np.std(np.array(f[f"EDY/{out_name}"]), axis=0)
+
+                    # --- Plot posterior predictive ---
+                    plt.plot(
+                        x_coords, mu, marker='o', color='b',
+                        label='Mean Post. Predictive')
+                    plt.fill_between(
+                        x_coords, mu-1.96*std, mu+1.96*std, color='b',
+                        alpha=0.15)
+
+                    # --- Plot Data ---
+                    ax.plot(
+                        x_coords, self.measured_data[out_name].values,
+                        'ko', label='data', markeredgecolor='w')
+
+                    # --- Plot ExpDesign ---
+                    orig_ED_Y = self.engine.ExpDesign.Y[out_name]
+                    for output in orig_ED_Y:
+                        plt.plot(
+                            x_coords, output, color='grey', alpha=0.15
+                            )
+
+                    # Add labels for axes
+                    plt.xlabel('Time [s]')
+                    plt.ylabel(out_name)
+
+                    # Add labels to the legend
+                    handles, labels = ax.get_legend_handles_labels()
+
+                    patch = Patch(color='b', alpha=0.15)
+                    handles.insert(1, patch)
+                    labels.insert(1, '95 $\\%$ CI')
+
+                    # Add legend
+                    ax.legend(handles=handles, labels=labels, loc='best',
+                              frameon=True)
+
+                # Save figure in pdf format
+                if self.emulator:
+                    plotname = f'/Post_Prior_Perd_{Model.name}_emulator'
+                else:
+                    plotname = f'/Post_Prior_Perd_{Model.name}'
+
+                fig.savefig(f'./{out_dir}{plotname}_{out_name}.pdf',
+                            bbox_inches='tight')
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/bayes_model_comparison.py b/examples/model-comparison/bayesvalidrox/bayes_inference/bayes_model_comparison.py
new file mode 100644
index 0000000000000000000000000000000000000000..828613556e90ec0c529b91f2592eec148c98136b
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/bayes_inference/bayes_model_comparison.py
@@ -0,0 +1,654 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import os
+from scipy import stats
+import seaborn as sns
+import matplotlib.patches as patches
+import matplotlib.colors as mcolors
+import matplotlib.pylab as plt
+from .bayes_inference import BayesInference
+
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class BayesModelComparison:
+    """
+    A class to perform Bayesian Analysis.
+
+
+    Attributes
+    ----------
+    justifiability : bool, optional
+        Whether to perform the justifiability analysis. The default is
+        `True`.
+    perturbed_data : array of shape (n_bootstrap_itrs, n_obs), optional
+        User defined perturbed data. The default is `None`.
+    n_bootstarp : int
+        Number of bootstrap iteration. The default is `1000`.
+    data_noise_level : float
+        A noise level to perturb the data set. The default is `0.01`.
+    just_n_meas : int
+        Number of measurements considered for visualization of the
+        justifiability results.
+
+    """
+
+    def __init__(self, justifiability=True, perturbed_data=None,
+                 n_bootstarp=1000, data_noise_level=0.01, just_n_meas=2):
+
+        self.justifiability = justifiability
+        self.perturbed_data = perturbed_data
+        self.n_bootstarp = n_bootstarp
+        self.data_noise_level = data_noise_level
+        self.just_n_meas = just_n_meas
+
+    # --------------------------------------------------------------------------
+    def create_model_comparison(self, model_dict, opts_dict):
+        """
+        Starts the two-stage model comparison.
+        Stage I: Compare models using Bayes factors.
+        Stage II: Compare models via justifiability analysis.
+
+        Parameters
+        ----------
+        model_dict : dict
+            A dictionary including the metamodels.
+        opts_dict : dict
+            A dictionary given the `BayesInference` options.
+
+            Example:
+
+                >>> opts_bootstrap = {
+                    "bootstrap": True,
+                    "n_samples": 10000,
+                    "Discrepancy": DiscrepancyOpts,
+                    "emulator": True,
+                    "plot_post_pred": True
+                    }
+
+        Returns
+        -------
+        output : dict
+            A dictionary containing the objects and the model weights for the
+            comparison using Bayes factors and justifiability analysis.
+
+        """
+
+        # Bayes factor
+        bayes_dict_bf, model_weights_dict_bf = self.compare_models(
+            model_dict, opts_dict
+            )
+
+        output = {
+            'Bayes objects BF': bayes_dict_bf,
+            'Model weights BF': model_weights_dict_bf
+            }
+
+        # Justifiability analysis
+        if self.justifiability:
+            bayes_dict_ja, model_weights_dict_ja = self.compare_models(
+                model_dict, opts_dict, justifiability=True
+                )
+
+            output['Bayes objects JA'] = bayes_dict_ja
+            output['Model weights JA'] = model_weights_dict_ja
+
+        return output
+
+    # --------------------------------------------------------------------------
+    def compare_models(self, model_dict, opts_dict, justifiability=False):
+        """
+        Passes the options to instantiates the BayesInference class for each
+        model and passes the options from `opts_dict`. Then, it starts the
+        computations.
+        It also creates a folder and saves the diagrams, e.g., Bayes factor
+        plot, confusion matrix, etc.
+
+        Parameters
+        ----------
+        model_dict : dict
+            A dictionary including the metamodels.
+        opts_dict : dict
+            A dictionary given the `BayesInference` options.
+        justifiability : bool, optional
+            Whether to perform the justifiability analysis. The default is
+            `False`.
+
+        Returns
+        -------
+        bayes_dict : dict
+            A dictionary with `BayesInference` objects.
+        model_weights_dict : dict
+            A dictionary containing the model weights.
+
+        """
+
+        if not isinstance(model_dict, dict):
+            raise Exception("To run model comparsion, you need to pass a "
+                            "dictionary of models.")
+
+        # Extract model names
+        self.model_names = [*model_dict]
+
+        # Compute total number of the measurement points
+        Engine = list(model_dict.items())[0][1]
+        Engine.Model.read_observation()
+        self.n_meas = Engine.Model.n_obs
+
+        # ----- Generate data -----
+        # Find n_bootstrap
+        if self.perturbed_data is None:
+            n_bootstarp = self.n_bootstarp
+        else:
+            n_bootstarp = self.perturbed_data.shape[0]
+
+        # Create dataset
+        justData = self.generate_dataset(
+            model_dict, justifiability, n_bootstarp=n_bootstarp)
+
+        # Run create Interface for each model
+        bayes_dict = {}
+        for model in model_dict.keys():
+            print("-"*20)
+            print("Bayesian inference of {}.\n".format(model))
+
+            BayesOpts = BayesInference(model_dict[model])
+
+            # Set BayesInference options
+            for key, value in opts_dict.items():
+                if key in BayesOpts.__dict__.keys():
+                    if key == "Discrepancy" and isinstance(value, dict):
+                        setattr(BayesOpts, key, value[model])
+                    else:
+                        setattr(BayesOpts, key, value)
+
+            # Pass justifiability data as perturbed data
+            BayesOpts.perturbed_data = justData
+            BayesOpts.just_analysis = justifiability
+
+            bayes_dict[model] = BayesOpts.create_inference()
+            print("-"*20)
+
+        # Compute model weights
+        BME_Dict = dict()
+        for modelName, bayesObj in bayes_dict.items():
+            BME_Dict[modelName] = np.exp(bayesObj.log_BME, dtype=np.longdouble)#float128)
+
+        # BME correction in BayesInference class
+        model_weights = self.cal_model_weight(
+            BME_Dict, justifiability, n_bootstarp=n_bootstarp)
+
+        # Plot model weights
+        if justifiability:
+            model_names = self.model_names
+            model_names.insert(0, 'Observation')
+
+            # Split the model weights and save in a dict
+            list_ModelWeights = np.split(
+                model_weights, model_weights.shape[1]/self.n_meas, axis=1)
+            model_weights_dict = {key: weights for key, weights in
+                                  zip(model_names, list_ModelWeights)}
+
+            #self.plot_just_analysis(model_weights_dict)
+        else:
+            # Create box plot for model weights
+            self.plot_model_weights(model_weights, 'model_weights')
+
+            # Create kde plot for bayes factors
+            self.plot_bayes_factor(BME_Dict, 'kde_plot')
+
+            # Store model weights in a dict
+            model_weights_dict = {key: weights for key, weights in
+                                  zip(self.model_names, model_weights)}
+
+        return bayes_dict, model_weights_dict
+
+    # -------------------------------------------------------------------------
+    def generate_dataset(self, model_dict, justifiability=False,
+                         n_bootstarp=1):
+        """
+        Generates the perturbed data set for the Bayes factor calculations and
+        the data set for the justifiability analysis.
+
+        Parameters
+        ----------
+        model_dict : dict
+            A dictionary including the metamodels.
+        bool, optional
+            Whether to perform the justifiability analysis. The default is
+            `False`.
+        n_bootstarp : int, optional
+            Number of bootstrap iterations. The default is `1`.
+
+        Returns
+        -------
+        all_just_data: array
+            Created data set.
+
+        """
+        # Compute some variables
+        all_just_data = []
+        Engine = list(model_dict.items())[0][1]
+        out_names = Engine.Model.Output.names
+
+        # Perturb observations for Bayes Factor
+        if self.perturbed_data is None:
+            self.perturbed_data = self.__perturb_data(
+                    Engine.Model.observations, out_names, n_bootstarp,
+                    noise_level=self.data_noise_level)
+
+        # Only for Bayes Factor
+        if not justifiability:
+            return self.perturbed_data
+
+        # Evaluate metamodel
+        runs = {}
+        for key, metaModel in model_dict.items():
+            y_hat, _ = metaModel.eval_metamodel(nsamples=n_bootstarp)
+            runs[key] = y_hat
+
+        # Generate data
+        for i in range(n_bootstarp):
+            y_data = self.perturbed_data[i].reshape(1, -1)
+            justData = np.tril(np.repeat(y_data, y_data.shape[1], axis=0))
+            # Use surrogate runs for data-generating process
+            for key, metaModel in model_dict.items():
+                model_data = np.array(
+                    [runs[key][out][i] for out in out_names]).reshape(y_data.shape)
+                justData = np.vstack((
+                    justData,
+                    np.tril(np.repeat(model_data, model_data.shape[1], axis=0))
+                    ))
+            # Save in a list
+            all_just_data.append(justData)
+
+        # Squeeze the array
+        all_just_data = np.array(all_just_data).transpose(1, 0, 2).reshape(
+            -1, np.array(all_just_data).shape[2]
+            )
+
+        return all_just_data
+
+    # -------------------------------------------------------------------------
+    def __perturb_data(self, data, output_names, n_bootstrap, noise_level):
+        """
+        Returns an array with n_bootstrap_itrs rowsof perturbed data.
+        The first row includes the original observation data.
+        If `self.bayes_loocv` is True, a 2d-array will be returned with
+        repeated rows and zero diagonal entries.
+
+        Parameters
+        ----------
+        data : pandas DataFrame
+            Observation data.
+        output_names : list
+            List of the output names.
+
+        Returns
+        -------
+        final_data : array
+            Perturbed data set.
+
+        """
+        obs_data = data[output_names].values
+        n_measurement, n_outs = obs_data.shape
+        n_tot_measurement = obs_data[~np.isnan(obs_data)].shape[0]
+        final_data = np.zeros(
+            (n_bootstrap, n_tot_measurement)
+            )
+        final_data[0] = obs_data.T[~np.isnan(obs_data.T)]
+        for itrIdx in range(1, n_bootstrap):
+            data = np.zeros((n_measurement, n_outs))
+            for idx in range(len(output_names)):
+                std = np.nanstd(obs_data[:, idx])
+                if std == 0:
+                    std = 0.001
+                noise = std * noise_level
+                data[:, idx] = np.add(
+                    obs_data[:, idx],
+                    np.random.normal(0, 1, obs_data.shape[0]) * noise,
+                )
+
+            final_data[itrIdx] = data.T[~np.isnan(data.T)]
+
+        return final_data
+
+    # -------------------------------------------------------------------------
+    def cal_model_weight(self, BME_Dict, justifiability=False, n_bootstarp=1):
+        """
+        Normalize the BME (Asumption: Model Prior weights are equal for models)
+
+        Parameters
+        ----------
+        BME_Dict : dict
+            A dictionary containing the BME values.
+
+        Returns
+        -------
+        model_weights : array
+            Model weights.
+
+        """
+        # Stack the BME values for all models
+        all_BME = np.vstack(list(BME_Dict.values()))
+
+        if justifiability:
+            # Compute expected log_BME for justifiabiliy analysis
+            all_BME = all_BME.reshape(
+                all_BME.shape[0], -1, n_bootstarp).mean(axis=2)
+
+        # Model weights
+        model_weights = np.divide(all_BME, np.nansum(all_BME, axis=0))
+
+        return model_weights
+
+    # -------------------------------------------------------------------------
+    def plot_just_analysis(self, model_weights_dict):
+        """
+        Visualizes the confusion matrix and the model wights for the
+        justifiability analysis.
+
+        Parameters
+        ----------
+        model_weights_dict : dict
+            Model weights.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        directory = 'Outputs_Comparison/'
+        os.makedirs(directory, exist_ok=True)
+        Color = [*mcolors.TABLEAU_COLORS]
+        names = [*model_weights_dict]
+
+        model_names = [model.replace('_', '$-$') for model in self.model_names]
+        for name in names:
+            fig, ax = plt.subplots()
+            for i, model in enumerate(model_names[1:]):
+                plt.plot(list(range(1, self.n_meas+1)),
+                         model_weights_dict[name][i],
+                         color=Color[i], marker='o',
+                         ms=10, linewidth=2, label=model
+                         )
+
+            plt.title(f"Data generated by: {name.replace('_', '$-$')}")
+            plt.ylabel("Weights")
+            plt.xlabel("No. of measurement points")
+            ax.set_xticks(list(range(1, self.n_meas+1)))
+            plt.legend(loc="best")
+            fig.savefig(
+                f'{directory}modelWeights_{name}.svg', bbox_inches='tight'
+                )
+            plt.close()
+
+        # Confusion matrix for some measurement points
+        epsilon = 1 if self.just_n_meas != 1 else 0
+        for index in range(0, self.n_meas+epsilon, self.just_n_meas):
+            weights = np.array(
+                [model_weights_dict[key][:, index] for key in model_weights_dict]
+                )
+            g = sns.heatmap(
+                weights.T, annot=True, cmap='Blues', xticklabels=model_names,
+                yticklabels=model_names[1:], annot_kws={"size": 24}
+                )
+
+            # x axis on top
+            g.xaxis.tick_top()
+            g.xaxis.set_label_position('top')
+            g.set_xlabel(r"\textbf{Data generated by:}", labelpad=15)
+            g.set_ylabel(r"\textbf{Model weight for:}", labelpad=15)
+            g.figure.savefig(
+                f"{directory}confusionMatrix_ND_{index+1}.pdf",
+                bbox_inches='tight'
+                )
+            plt.close()
+
+    # -------------------------------------------------------------------------
+    def plot_model_weights(self, model_weights, plot_name):
+        """
+        Visualizes the model weights resulting from BMS via the observation
+        data.
+
+        Parameters
+        ----------
+        model_weights : array
+            Model weights.
+        plot_name : str
+            Plot name.
+
+        Returns
+        -------
+        None.
+
+        """
+        font_size = 40
+        # mkdir for plots
+        directory = 'Outputs_Comparison/'
+        os.makedirs(directory, exist_ok=True)
+
+        # Create figure
+        fig, ax = plt.subplots()
+
+        # Filter data using np.isnan
+        mask = ~np.isnan(model_weights.T)
+        filtered_data = [d[m] for d, m in zip(model_weights, mask.T)]
+
+        # Create the boxplot
+        bp = ax.boxplot(filtered_data, patch_artist=True, showfliers=False)
+
+        # change outline color, fill color and linewidth of the boxes
+        for box in bp['boxes']:
+            # change outline color
+            box.set(color='#7570b3', linewidth=4)
+            # change fill color
+            box.set(facecolor='#1b9e77')
+
+        # change color and linewidth of the whiskers
+        for whisker in bp['whiskers']:
+            whisker.set(color='#7570b3', linewidth=2)
+
+        # change color and linewidth of the caps
+        for cap in bp['caps']:
+            cap.set(color='#7570b3', linewidth=2)
+
+        # change color and linewidth of the medians
+        for median in bp['medians']:
+            median.set(color='#b2df8a', linewidth=2)
+
+        # change the style of fliers and their fill
+        # for flier in bp['fliers']:
+        #     flier.set(marker='o', color='#e7298a', alpha=0.75)
+
+        # Custom x-axis labels
+        model_names = [model.replace('_', '$-$') for model in self.model_names]
+        ax.set_xticklabels(model_names)
+
+        ax.set_ylabel('Weight', fontsize=font_size)
+
+        # Title
+        plt.title('Posterior Model Weights')
+
+        # Set y lim
+        ax.set_ylim((-0.05, 1.05))
+
+        # Set size of the ticks
+        for t in ax.get_xticklabels():
+            t.set_fontsize(font_size)
+        for t in ax.get_yticklabels():
+            t.set_fontsize(font_size)
+
+        # Save the figure
+        fig.savefig(
+            f'./{directory}{plot_name}.pdf', bbox_inches='tight'
+            )
+
+        plt.close()
+
+    # -------------------------------------------------------------------------
+    def plot_bayes_factor(self, BME_Dict, plot_name=''):
+        """
+        Plots the Bayes factor distibutions in a :math:`N_m \\times N_m`
+        matrix, where :math:`N_m` is the number of the models.
+
+        Parameters
+        ----------
+        BME_Dict : dict
+            A dictionary containing the BME values of the models.
+        plot_name : str, optional
+            Plot name. The default is ''.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        font_size = 40
+
+        # mkdir for plots
+        directory = 'Outputs_Comparison/'
+        os.makedirs(directory, exist_ok=True)
+
+        Colors = ["blue", "green", "gray", "brown"]
+
+        model_names = list(BME_Dict.keys())
+        nModels = len(model_names)
+
+        # Plots
+        fig, axes = plt.subplots(
+            nrows=nModels, ncols=nModels, sharex=True, sharey=True
+            )
+
+        for i, key_i in enumerate(model_names):
+
+            for j, key_j in enumerate(model_names):
+                ax = axes[i, j]
+                # Set size of the ticks
+                for t in ax.get_xticklabels():
+                    t.set_fontsize(font_size)
+                for t in ax.get_yticklabels():
+                    t.set_fontsize(font_size)
+
+                if j != i:
+
+                    # Null hypothesis: key_j is the better model
+                    BayesFactor = np.log10(
+                        np.divide(BME_Dict[key_i], BME_Dict[key_j])
+                        )
+
+                    # sns.kdeplot(BayesFactor, ax=ax, color=Colors[i], shade=True)
+                    # sns.histplot(BayesFactor, ax=ax, stat="probability",
+                    #              kde=True, element='step',
+                    #              color=Colors[j])
+
+                    # taken from seaborn's source code (utils.py and
+                    # distributions.py)
+                    def seaborn_kde_support(data, bw, gridsize, cut, clip):
+                        if clip is None:
+                            clip = (-np.inf, np.inf)
+                        support_min = max(data.min() - bw * cut, clip[0])
+                        support_max = min(data.max() + bw * cut, clip[1])
+                        return np.linspace(support_min, support_max, gridsize)
+
+                    kde_estim = stats.gaussian_kde(
+                        BayesFactor, bw_method='scott'
+                        )
+
+                    # manual linearization of data
+                    # linearized = np.linspace(
+                    #     quotient.min(), quotient.max(), num=500)
+
+                    # or better: mimic seaborn's internal stuff
+                    bw = kde_estim.scotts_factor() * np.std(BayesFactor)
+                    linearized = seaborn_kde_support(
+                        BayesFactor, bw, 100, 3, None)
+
+                    # computes values of the estimated function on the
+                    # estimated linearized inputs
+                    Z = kde_estim.evaluate(linearized)
+
+                    # https://stackoverflow.com/questions/29661574/normalize-
+                    # numpy-array-columns-in-python
+                    def normalize(x):
+                        return (x - x.min(0)) / x.ptp(0)
+
+                    # normalize so it is between 0;1
+                    Z2 = normalize(Z)
+                    ax.plot(linearized, Z2, "-", color=Colors[i], linewidth=4)
+                    ax.fill_between(
+                        linearized, 0, Z2, color=Colors[i], alpha=0.25
+                        )
+
+                    # Draw BF significant levels according to Jeffreys 1961
+                    # Strong evidence for both models
+                    ax.axvline(
+                        x=np.log10(3), ymin=0, linewidth=4, color='dimgrey'
+                        )
+                    # Strong evidence for one model
+                    ax.axvline(
+                        x=np.log10(10), ymin=0, linewidth=4, color='orange'
+                        )
+                    # Decisive evidence for one model
+                    ax.axvline(
+                        x=np.log10(100), ymin=0, linewidth=4, color='r'
+                        )
+
+                    # legend
+                    BF_label = key_i.replace('_', '$-$') + \
+                        '/' + key_j.replace('_', '$-$')
+                    legend_elements = [
+                        patches.Patch(facecolor=Colors[i], edgecolor=Colors[i],
+                                      label=f'BF({BF_label})')
+                        ]
+                    ax.legend(
+                        loc='upper left', handles=legend_elements,
+                        fontsize=font_size-(nModels+1)*5
+                        )
+
+                elif j == i:
+                    # build a rectangle in axes coords
+                    left, width = 0, 1
+                    bottom, height = 0, 1
+
+                    # axes coordinates are 0,0 is bottom left and 1,1 is upper
+                    # right
+                    p = patches.Rectangle(
+                        (left, bottom), width, height, color='white',
+                        fill=True, transform=ax.transAxes, clip_on=False
+                        )
+                    ax.grid(False)
+                    ax.add_patch(p)
+                    # ax.text(0.5*(left+right), 0.5*(bottom+top), key_i,
+                    fsize = font_size+20 if nModels < 4 else font_size
+                    ax.text(0.5, 0.5, key_i.replace('_', '$-$'),
+                            horizontalalignment='center',
+                            verticalalignment='center',
+                            fontsize=fsize, color=Colors[i],
+                            transform=ax.transAxes)
+
+        # Defining custom 'ylim' values.
+        custom_ylim = (0, 1.05)
+
+        # Setting the values for all axes.
+        plt.setp(axes, ylim=custom_ylim)
+
+        # set labels
+        for i in range(nModels):
+            axes[-1, i].set_xlabel('log$_{10}$(BF)', fontsize=font_size)
+            axes[i, 0].set_ylabel('Probability', fontsize=font_size)
+
+        # Adjust subplots
+        plt.subplots_adjust(wspace=0.2, hspace=0.1)
+
+        plt.savefig(
+            f'./{directory}Bayes_Factor{plot_name}.pdf', bbox_inches='tight'
+            )
+
+        plt.close()
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/discrepancy.py b/examples/model-comparison/bayesvalidrox/bayes_inference/discrepancy.py
new file mode 100644
index 0000000000000000000000000000000000000000..fff32a2500ae20b3667c7b0ec2cc85c1da614688
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/bayes_inference/discrepancy.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import scipy.stats as stats
+from bayesvalidrox.surrogate_models.exp_designs import ExpDesigns
+
+
+class Discrepancy:
+    """
+    Discrepancy class for Bayesian inference method.
+    We define the reference or reality to be equal to what we can model and a
+    descripancy term \\( \\epsilon \\). We consider the followin format:
+
+    $$\\textbf{y}_{\\text{reality}} = \\mathcal{M}(\\theta) + \\epsilon,$$
+
+    where \\( \\epsilon \\in R^{N_{out}} \\) represents the the effects of
+    measurement error and model inaccuracy. For simplicity, it can be defined
+    as an additive Gaussian disrepancy with zeromean and given covariance
+    matrix \\( \\Sigma \\):
+
+    $$\\epsilon \\sim \\mathcal{N}(\\epsilon|0, \\Sigma). $$
+
+    In the context of model inversion or calibration, an observation point
+    \\( \\textbf{y}_i \\in \\mathcal{y} \\) is a realization of a Gaussian
+    distribution with mean value of \\(\\mathcal{M}(\\theta) \\) and covariance
+    matrix of \\( \\Sigma \\).
+
+    $$ p(\\textbf{y}|\\theta) = \\mathcal{N}(\\textbf{y}|\\mathcal{M}
+                                             (\\theta))$$
+
+    The following options are available:
+
+    * Option A: With known redidual covariance matrix \\(\\Sigma\\) for
+    independent measurements.
+
+    * Option B: With unknown redidual covariance matrix \\(\\Sigma\\),
+    paramethrized as \\(\\Sigma(\\theta_{\\epsilon})=\\sigma^2 \\textbf{I}_
+    {N_{out}}\\) with unknown residual variances \\(\\sigma^2\\).
+    This term will be jointly infer with the uncertain input parameters. For
+    the inversion, you need to define a prior marginal via `Input` class. Note
+    that \\(\\sigma^2\\) is only a single scalar multiplier for the diagonal
+    entries of the covariance matrix \\(\\Sigma\\).
+
+    Attributes
+    ----------
+    InputDisc : obj
+        Input object. When the \\(\\sigma^2\\) is expected to be inferred
+        jointly with the parameters (`Option B`).If multiple output groups are
+        defined by `Model.Output.names`, each model output needs to have.
+        a prior marginal using the `Input` class. The default is `''`.
+    disc_type : str
+        Type of the noise definition. `'Gaussian'` is only supported so far.
+    parameters : dict or pandas.DataFrame
+        Known residual variance \\(\\sigma^2\\), i.e. diagonal entry of the
+        covariance matrix of the multivariate normal likelihood in case of
+        `Option A`.
+
+    """
+
+    def __init__(self, InputDisc='', disc_type='Gaussian', parameters=None):
+        self.InputDisc = InputDisc
+        self.disc_type = disc_type
+        self.parameters = parameters
+
+    # -------------------------------------------------------------------------
+    def get_sample(self, n_samples):
+        """
+        Generate samples for the \\(\\sigma^2\\), i.e. the diagonal entries of
+        the variance-covariance matrix in the multivariate normal distribution.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of samples (parameter sets).
+
+        Returns
+        -------
+        sigma2_prior: array of shape (n_samples, n_params)
+            \\(\\sigma^2\\) samples.
+
+        """
+        self.n_samples = n_samples # TODO: not used again in here - needed from the outside?
+        
+        if self.InputDisc == '':
+            raise AttributeError('Cannot create new samples, please provide input distributions')
+        
+        # Create and store BoundTuples
+        self.ExpDesign = ExpDesigns(self.InputDisc)
+        self.ExpDesign.sampling_method = 'random'
+        self.ExpDesign.generate_ED(
+            n_samples, max_pce_deg=1
+            )
+        # TODO: need to recheck the following line
+        # This used to simply be the return from the call above
+        self.sigma2_prior = self.ExpDesign.X
+
+        # Naive approach: Fit a gaussian kernel to the provided data
+        self.ExpDesign.JDist = stats.gaussian_kde(self.ExpDesign.raw_data)
+
+        # Save the names of sigmas
+        if len(self.InputDisc.Marginals) != 0:
+            self.name = []
+            for Marginalidx in range(len(self.InputDisc.Marginals)):
+                self.name.append(self.InputDisc.Marginals[Marginalidx].name)
+
+        return self.sigma2_prior
diff --git a/examples/model-comparison/bayesvalidrox/bayes_inference/mcmc.py b/examples/model-comparison/bayesvalidrox/bayes_inference/mcmc.py
new file mode 100644
index 0000000000000000000000000000000000000000..fe22a152f117aab7023bfe6592ce3a48bb0b3aec
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/bayes_inference/mcmc.py
@@ -0,0 +1,909 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import os
+import numpy as np
+import emcee
+import pandas as pd
+import matplotlib.pyplot as plt
+from matplotlib.backends.backend_pdf import PdfPages
+import multiprocessing
+import scipy.stats as st
+from scipy.linalg import cholesky as chol
+import warnings
+import shutil
+os.environ["OMP_NUM_THREADS"] = "1"
+
+
+class MCMC:
+    """
+    A class for bayesian inference via a Markov-Chain Monte-Carlo (MCMC)
+    Sampler to approximate the posterior distribution of the Bayes theorem:
+    $$p(\\theta|\\mathcal{y}) = \\frac{p(\\mathcal{y}|\\theta) p(\\theta)}
+                                         {p(\\mathcal{y})}.$$
+
+    This class make inference with emcee package [1] using an Affine Invariant
+    Ensemble sampler (AIES) [2].
+
+    [1] Foreman-Mackey, D., Hogg, D.W., Lang, D. and Goodman, J., 2013.emcee:
+        the MCMC hammer. Publications of the Astronomical Society of the
+        Pacific, 125(925), p.306. https://emcee.readthedocs.io/en/stable/
+
+    [2] Goodman, J. and Weare, J., 2010. Ensemble samplers with affine
+        invariance. Communications in applied mathematics and computational
+        science, 5(1), pp.65-80.
+
+
+    Attributes
+    ----------
+    BayesOpts : obj
+        Bayes object.
+    """
+
+    def __init__(self, BayesOpts):
+
+        self.BayesOpts = BayesOpts
+
+    def run_sampler(self, observation, total_sigma2):
+
+        BayesObj = self.BayesOpts
+        MetaModel = BayesObj.engine.MetaModel
+        Model = BayesObj.engine.Model
+        Discrepancy = self.BayesOpts.Discrepancy
+        n_cpus = Model.n_cpus
+        priorDist = BayesObj.engine.ExpDesign.JDist
+        ndim = MetaModel.n_params
+        self.counter = 0
+        output_dir = f'Outputs_Bayes_{Model.name}_{self.BayesOpts.name}'
+        if not os.path.exists(output_dir):
+            os.makedirs(output_dir)
+
+        self.observation = observation
+        self.total_sigma2 = total_sigma2
+
+        # Unpack mcmc parameters given to BayesObj.mcmc_params
+        self.initsamples = None
+        self.nwalkers = 100
+        self.nburn = 200
+        self.nsteps = 100000
+        self.moves = None
+        self.mp = False
+        self.verbose = False
+
+        # Extract initial samples
+        if 'init_samples' in BayesObj.mcmc_params:
+            self.initsamples = BayesObj.mcmc_params['init_samples']
+            if isinstance(self.initsamples, pd.DataFrame):
+                self.initsamples = self.initsamples.values
+
+        # Extract number of steps per walker
+        if 'n_steps' in BayesObj.mcmc_params:
+            self.nsteps = int(BayesObj.mcmc_params['n_steps'])
+        # Extract number of walkers (chains)
+        if 'n_walkers' in BayesObj.mcmc_params:
+            self.nwalkers = int(BayesObj.mcmc_params['n_walkers'])
+        # Extract moves
+        if 'moves' in BayesObj.mcmc_params:
+            self.moves = BayesObj.mcmc_params['moves']
+        # Extract multiprocessing
+        if 'multiprocessing' in BayesObj.mcmc_params:
+            self.mp = BayesObj.mcmc_params['multiprocessing']
+        # Extract verbose
+        if 'verbose' in BayesObj.mcmc_params:
+            self.verbose = BayesObj.mcmc_params['verbose']
+
+        # Set initial samples
+        np.random.seed(0)
+        if self.initsamples is None:
+            try:
+                initsamples = priorDist.sample(self.nwalkers).T
+            except:
+                # when aPCE selected - gaussian kernel distribution
+                inputSamples = MetaModel.ExpDesign.raw_data.T
+                random_indices = np.random.choice(
+                    len(inputSamples), size=self.nwalkers, replace=False
+                    )
+                initsamples = inputSamples[random_indices]
+
+        else:
+            if self.initsamples.ndim == 1:
+                # When MAL is given.
+                theta = self.initsamples
+                initsamples = [theta + 1e-1*np.multiply(
+                    np.random.randn(ndim), theta) for i in
+                               range(self.nwalkers)]
+            else:
+                # Pick samples based on a uniform dist between min and max of
+                # each dim
+                initsamples = np.zeros((self.nwalkers, ndim))
+                bound_tuples = []
+                for idx_dim in range(ndim):
+                    lower = np.min(self.initsamples[:, idx_dim])
+                    upper = np.max(self.initsamples[:, idx_dim])
+                    bound_tuples.append((lower, upper))
+                    dist = st.uniform(loc=lower, scale=upper-lower)
+                    initsamples[:, idx_dim] = dist.rvs(size=self.nwalkers)
+
+                # Update lower and upper
+                MetaModel.ExpDesign.bound_tuples = bound_tuples
+
+        # Check if sigma^2 needs to be inferred
+        if Discrepancy.opt_sigma != 'B':
+            sigma2_samples = Discrepancy.get_sample(self.nwalkers)
+
+            # Update initsamples
+            initsamples = np.hstack((initsamples, sigma2_samples))
+
+            # Update ndim
+            ndim = initsamples.shape[1]
+
+            # Discrepancy bound
+            disc_bound_tuple = Discrepancy.ExpDesign.bound_tuples
+
+            # Update bound_tuples
+            BayesObj.engine.ExpDesign.bound_tuples += disc_bound_tuple
+
+        print("\n>>>> Bayesian inference with MCMC for "
+              f"{self.BayesOpts.name} started. <<<<<<")
+
+        # Set up the backend
+        filename = f"{output_dir}/emcee_sampler.h5"
+        backend = emcee.backends.HDFBackend(filename)
+        # Clear the backend in case the file already exists
+        backend.reset(self.nwalkers, ndim)
+
+        # Define emcee sampler
+        # Here we'll set up the computation. emcee combines multiple "walkers",
+        # each of which is its own MCMC chain. The number of trace results will
+        # be nwalkers * nsteps.
+        if self.mp:
+            # Run in parallel
+            if n_cpus is None:
+                n_cpus = multiprocessing.cpu_count()
+
+            with multiprocessing.Pool(n_cpus) as pool:
+                sampler = emcee.EnsembleSampler(
+                    self.nwalkers, ndim, self.log_posterior, moves=self.moves,
+                    pool=pool, backend=backend
+                    )
+
+                # Check if a burn-in phase is needed!
+                if self.initsamples is None:
+                    # Burn-in
+                    print("\n Burn-in period is starting:")
+                    pos = sampler.run_mcmc(
+                        initsamples, self.nburn, progress=True
+                        )
+
+                    # Reset sampler
+                    sampler.reset()
+                    pos = pos.coords
+                else:
+                    pos = initsamples
+
+                # Production run
+                print("\n Production run is starting:")
+                pos, prob, state = sampler.run_mcmc(
+                    pos, self.nsteps, progress=True
+                    )
+
+        else:
+            # Run in series and monitor the convergence
+            sampler = emcee.EnsembleSampler(
+                self.nwalkers, ndim, self.log_posterior, moves=self.moves,
+                backend=backend, vectorize=True
+                )
+
+            # Check if a burn-in phase is needed!
+            if self.initsamples is None:
+                # Burn-in
+                print("\n Burn-in period is starting:")
+                pos = sampler.run_mcmc(
+                    initsamples, self.nburn, progress=True
+                    )
+
+                # Reset sampler
+                sampler.reset()
+                pos = pos.coords
+            else:
+                pos = initsamples
+
+            # Production run
+            print("\n Production run is starting:")
+
+            # Track how the average autocorrelation time estimate changes
+            autocorrIdx = 0
+            autocorr = np.empty(self.nsteps)
+            tauold = np.inf
+            autocorreverynsteps = 50
+
+            # sample step by step using the generator sampler.sample
+            for sample in sampler.sample(pos,
+                                         iterations=self.nsteps,
+                                         tune=True,
+                                         progress=True):
+
+                # only check convergence every autocorreverynsteps steps
+                if sampler.iteration % autocorreverynsteps:
+                    continue
+
+                # Train model discrepancy/error
+                if hasattr(BayesObj, 'errorModel') and BayesObj.errorModel \
+                   and not sampler.iteration % 3 * autocorreverynsteps:
+                    try:
+                        self.error_MetaModel = self.train_error_model(sampler)
+                    except:
+                        pass
+
+                # Print the current mean acceptance fraction
+                if self.verbose:
+                    print("\nStep: {}".format(sampler.iteration))
+                    acc_fr = np.mean(sampler.acceptance_fraction)
+                    print(f"Mean acceptance fraction: {acc_fr:.3f}")
+
+                # compute the autocorrelation time so far
+                # using tol=0 means that we'll always get an estimate even if
+                # it isn't trustworthy
+                tau = sampler.get_autocorr_time(tol=0)
+                # average over walkers
+                autocorr[autocorrIdx] = np.nanmean(tau)
+                autocorrIdx += 1
+
+                # output current autocorrelation estimate
+                if self.verbose:
+                    print(f"Mean autocorr. time estimate: {np.nanmean(tau):.3f}")
+                    list_gr = np.round(self.gelman_rubin(sampler.chain), 3)
+                    print("Gelman-Rubin Test*: ", list_gr)
+
+                # check convergence
+                converged = np.all(tau*autocorreverynsteps < sampler.iteration)
+                converged &= np.all(np.abs(tauold - tau) / tau < 0.01)
+                converged &= np.all(self.gelman_rubin(sampler.chain) < 1.1)
+
+                if converged:
+                    break
+                tauold = tau
+
+        # Posterior diagnostics
+        try:
+            tau = sampler.get_autocorr_time(tol=0)
+        except emcee.autocorr.AutocorrError:
+            tau = 5
+
+        if all(np.isnan(tau)):
+            tau = 5
+
+        burnin = int(2*np.nanmax(tau))
+        thin = int(0.5*np.nanmin(tau)) if int(0.5*np.nanmin(tau)) != 0 else 1
+        finalsamples = sampler.get_chain(discard=burnin, flat=True, thin=thin)
+        acc_fr = np.nanmean(sampler.acceptance_fraction)
+        list_gr = np.round(self.gelman_rubin(sampler.chain[:, burnin:]), 3)
+
+        # Print summary
+        print('\n')
+        print('-'*15 + 'Posterior diagnostics' + '-'*15)
+        print(f"Mean auto-correlation time: {np.nanmean(tau):.3f}")
+        print(f"Thin: {thin}")
+        print(f"Burn-in: {burnin}")
+        print(f"Flat chain shape: {finalsamples.shape}")
+        print(f"Mean acceptance fraction*: {acc_fr:.3f}")
+        print("Gelman-Rubin Test**: ", list_gr)
+
+        print("\n* This value must lay between 0.234 and 0.5.")
+        print("** These values must be smaller than 1.1.")
+        print('-'*50)
+
+        print(f"\n>>>> Bayesian inference with MCMC for {self.BayesOpts.name} "
+              "successfully completed. <<<<<<\n")
+
+        # Extract parameter names and their prior ranges
+        par_names = self.BayesOpts.engine.ExpDesign.par_names
+
+        if Discrepancy.opt_sigma != 'B':
+            for i in range(len(Discrepancy.InputDisc.Marginals)):
+                par_names.append(Discrepancy.InputDisc.Marginals[i].name)
+
+        params_range = self.BayesOpts.engine.ExpDesign.bound_tuples
+
+        # Plot traces
+        if self.verbose and self.nsteps < 10000:
+            pdf = PdfPages(output_dir+'/traceplots.pdf')
+            fig = plt.figure()
+            for parIdx in range(ndim):
+                # Set up the axes with gridspec
+                fig = plt.figure()
+                grid = plt.GridSpec(4, 4, hspace=0.2, wspace=0.2)
+                main_ax = fig.add_subplot(grid[:-1, :3])
+                y_hist = fig.add_subplot(grid[:-1, -1], xticklabels=[],
+                                         sharey=main_ax)
+
+                for i in range(self.nwalkers):
+                    samples = sampler.chain[i, :, parIdx]
+                    main_ax.plot(samples, '-')
+
+                    # histogram on the attached axes
+                    y_hist.hist(samples[burnin:], 40, histtype='stepfilled',
+                                orientation='horizontal', color='gray')
+
+                main_ax.set_ylim(params_range[parIdx])
+                main_ax.set_title('traceplot for ' + par_names[parIdx])
+                main_ax.set_xlabel('step number')
+
+                # save the current figure
+                pdf.savefig(fig, bbox_inches='tight')
+
+                # Destroy the current plot
+                plt.clf()
+
+            pdf.close()
+
+        # plot development of autocorrelation estimate
+        if not self.mp:
+            fig1 = plt.figure()
+            steps = autocorreverynsteps*np.arange(1, autocorrIdx+1)
+            taus = autocorr[:autocorrIdx]
+            plt.plot(steps, steps / autocorreverynsteps, "--k")
+            plt.plot(steps, taus)
+            plt.xlim(0, steps.max())
+            plt.ylim(0, np.nanmax(taus)+0.1*(np.nanmax(taus)-np.nanmin(taus)))
+            plt.xlabel("number of steps")
+            plt.ylabel(r"mean $\hat{\tau}$")
+            fig1.savefig(f"{output_dir}/autocorrelation_time.pdf",
+                         bbox_inches='tight')
+
+        # logml_dict = self.marginal_llk_emcee(sampler, self.nburn, logp=None,
+        # maxiter=5000)
+        # print('\nThe Bridge Sampling Estimation is "
+        #       f"{logml_dict['logml']:.5f}.')
+
+        # # Posterior-based expectation of posterior probablity
+        # postExpPostLikelihoods = np.mean(sampler.get_log_prob(flat=True)
+        # [self.nburn*self.nwalkers:])
+
+        # # Posterior-based expectation of prior densities
+        # postExpPrior = np.mean(self.log_prior(emcee_trace.T))
+
+        # # Posterior-based expectation of likelihoods
+        # postExpLikelihoods_emcee = postExpPostLikelihoods - postExpPrior
+
+        # # Calculate Kullback-Leibler Divergence
+        # KLD_emcee = postExpLikelihoods_emcee - logml_dict['logml']
+        # print("Kullback-Leibler divergence: %.5f"%KLD_emcee)
+
+        # # Information Entropy based on Entropy paper Eq. 38
+        # infEntropy_emcee = logml_dict['logml'] - postExpPrior -
+        #                    postExpLikelihoods_emcee
+        # print("Information Entropy: %.5f" %infEntropy_emcee)
+
+        Posterior_df = pd.DataFrame(finalsamples, columns=par_names)
+
+        return Posterior_df
+
+    # -------------------------------------------------------------------------
+    def log_prior(self, theta):
+        """
+        Calculates the log prior likelihood \\( p(\\theta)\\) for the given
+        parameter set(s) \\( \\theta \\).
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter sets, i.e. proposals of MCMC chains.
+
+        Returns
+        -------
+        logprior: float or array of shape n_samples
+            Log prior likelihood. If theta has only one row, a single value is
+            returned otherwise an array.
+
+        """
+
+        MetaModel = self.BayesOpts.MetaModel
+        Discrepancy = self.BayesOpts.Discrepancy
+
+        # Find the number of sigma2 parameters
+        if Discrepancy.opt_sigma != 'B':
+            disc_bound_tuples = Discrepancy.ExpDesign.bound_tuples
+            disc_marginals = Discrepancy.ExpDesign.InputObj.Marginals
+            disc_prior_space = Discrepancy.ExpDesign.prior_space
+            n_sigma2 = len(disc_bound_tuples)
+        else:
+            n_sigma2 = -len(theta)
+        prior_dist = self.BayesOpts.engine.ExpDesign.prior_space
+        params_range = self.BayesOpts.engine.ExpDesign.bound_tuples
+        theta = theta if theta.ndim != 1 else theta.reshape((1, -1))
+        nsamples = theta.shape[0]
+        logprior = -np.inf*np.ones(nsamples)
+
+        for i in range(nsamples):
+            # Check if the sample is within the parameters' range
+            if self._check_ranges(theta[i], params_range):
+                # Check if all dists are uniform, if yes priors are equal.
+                if all(MetaModel.input_obj.Marginals[i].dist_type == 'uniform'
+                       for i in range(MetaModel.n_params)):
+                    logprior[i] = 0.0
+                else:
+                    logprior[i] = np.log(
+                        prior_dist.pdf(theta[i, :-n_sigma2].T)
+                        )
+
+                # Check if bias term needs to be inferred
+                if Discrepancy.opt_sigma != 'B':
+                    if self._check_ranges(theta[i, -n_sigma2:],
+                                          disc_bound_tuples):
+                        if all('unif' in disc_marginals[i].dist_type for i in
+                               range(Discrepancy.ExpDesign.ndim)):
+                            logprior[i] = 0.0
+                        else:
+                            logprior[i] += np.log(
+                                disc_prior_space.pdf(theta[i, -n_sigma2:])
+                                )
+
+        if nsamples == 1:
+            return logprior[0]
+        else:
+            return logprior
+
+    # -------------------------------------------------------------------------
+    def log_likelihood(self, theta):
+        """
+        Computes likelihood \\( p(\\mathcal{Y}|\\theta)\\) of the performance
+        of the (meta-)model in reproducing the observation data.
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter set, i.e. proposals of the MCMC chains.
+
+        Returns
+        -------
+        log_like : array of shape (n_samples)
+            Log likelihood.
+
+        """
+
+        BayesOpts = self.BayesOpts
+        MetaModel = BayesOpts.MetaModel
+        Discrepancy = self.BayesOpts.Discrepancy
+
+        # Find the number of sigma2 parameters
+        if Discrepancy.opt_sigma != 'B':
+            disc_bound_tuples = Discrepancy.ExpDesign.bound_tuples
+            n_sigma2 = len(disc_bound_tuples)
+        else:
+            n_sigma2 = -len(theta)
+        # Check if bias term needs to be inferred
+        if Discrepancy.opt_sigma != 'B':
+            sigma2 = theta[:, -n_sigma2:]
+            theta = theta[:, :-n_sigma2]
+        else:
+            sigma2 = None
+        theta = theta if theta.ndim != 1 else theta.reshape((1, -1))
+
+        # Evaluate Model/MetaModel at theta
+        mean_pred, BayesOpts._std_pce_prior_pred = self.eval_model(theta)
+
+        # Surrogate model's error using RMSE of test data
+        surrError = MetaModel.rmse if hasattr(MetaModel, 'rmse') else None
+
+        # Likelihood
+        log_like = BayesOpts.normpdf(
+            mean_pred, self.observation, self.total_sigma2, sigma2,
+            std=surrError
+            )
+        return log_like
+
+    # -------------------------------------------------------------------------
+    def log_posterior(self, theta):
+        """
+        Computes the posterior likelihood \\(p(\\theta| \\mathcal{Y})\\) for
+        the given parameterset.
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter set, i.e. proposals of the MCMC chains.
+
+        Returns
+        -------
+        log_like : array of shape (n_samples)
+            Log posterior likelihood.
+
+        """
+
+        nsamples = 1 if theta.ndim == 1 else theta.shape[0]
+
+        if nsamples == 1:
+            if self.log_prior(theta) == -np.inf:
+                return -np.inf
+            else:
+                # Compute log prior
+                log_prior = self.log_prior(theta)
+                # Compute log Likelihood
+                log_likelihood = self.log_likelihood(theta)
+
+                return log_prior + log_likelihood
+        else:
+            # Compute log prior
+            log_prior = self.log_prior(theta)
+
+            # Initialize log_likelihood
+            log_likelihood = -np.inf*np.ones(nsamples)
+
+            # find the indices for -inf sets
+            non_inf_idx = np.where(log_prior != -np.inf)[0]
+
+            # Compute loLikelihoods
+            if non_inf_idx.size != 0:
+                log_likelihood[non_inf_idx] = self.log_likelihood(
+                    theta[non_inf_idx]
+                    )
+
+            return log_prior + log_likelihood
+
+    # -------------------------------------------------------------------------
+    def eval_model(self, theta):
+        """
+        Evaluates the (meta-) model at the given theta.
+
+        Parameters
+        ----------
+        theta : array of shape (n_samples, n_params)
+            Parameter set, i.e. proposals of the MCMC chains.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean model prediction.
+        std_pred : dict
+            Std of model prediction.
+
+        """
+
+        BayesObj = self.BayesOpts
+        MetaModel = BayesObj.MetaModel
+        Model = BayesObj.engine.Model
+
+        if BayesObj.emulator:
+            # Evaluate the MetaModel
+            mean_pred, std_pred = MetaModel.eval_metamodel(samples=theta)
+        else:
+            # Evaluate the origModel
+            mean_pred, std_pred = dict(), dict()
+
+            model_outs, _ = Model.run_model_parallel(
+                theta, prevRun_No=self.counter,
+                key_str='_MCMC', mp=False, verbose=False)
+
+            # Save outputs in respective dicts
+            for varIdx, var in enumerate(Model.Output.names):
+                mean_pred[var] = model_outs[var]
+                std_pred[var] = np.zeros((mean_pred[var].shape))
+
+            # Remove the folder
+            if Model.link_type.lower() != 'function':
+                shutil.rmtree(f"{Model.name}_MCMC_{self.counter+1}")
+
+            # Add one to the counter
+            self.counter += 1
+
+        if hasattr(self, 'error_MetaModel') and BayesObj.error_model:
+            meanPred, stdPred = self.error_MetaModel.eval_model_error(
+                BayesObj.BiasInputs, mean_pred
+                )
+
+        return mean_pred, std_pred
+
+    # -------------------------------------------------------------------------
+    def train_error_model(self, sampler):
+        """
+        Trains an error model using a Gaussian Process Regression.
+
+        Parameters
+        ----------
+        sampler : obj
+            emcee sampler.
+
+        Returns
+        -------
+        error_MetaModel : obj
+            A error model.
+
+        """
+        BayesObj = self.BayesOpts
+        MetaModel = BayesObj.MetaModel
+
+        # Prepare the poster samples
+        try:
+            tau = sampler.get_autocorr_time(tol=0)
+        except emcee.autocorr.AutocorrError:
+            tau = 5
+
+        if all(np.isnan(tau)):
+            tau = 5
+
+        burnin = int(2*np.nanmax(tau))
+        thin = int(0.5*np.nanmin(tau)) if int(0.5*np.nanmin(tau)) != 0 else 1
+        finalsamples = sampler.get_chain(discard=burnin, flat=True, thin=thin)
+        posterior = finalsamples[:, :MetaModel.n_params]
+
+        # Select posterior mean as MAP
+        map_theta = posterior.mean(axis=0).reshape((1, MetaModel.n_params))
+        # MAP_theta = st.mode(Posterior_df,axis=0)[0]
+
+        # Evaluate the (meta-)model at the MAP
+        y_map, y_std_map = MetaModel.eval_metamodel(samples=map_theta)
+
+        # Train a GPR meta-model using MAP
+        error_MetaModel = MetaModel.create_model_error(
+            BayesObj.BiasInputs, y_map, name='Calib')
+
+        return error_MetaModel
+
+    # -------------------------------------------------------------------------
+    def gelman_rubin(self, chain, return_var=False):
+        """
+        The potential scale reduction factor (PSRF) defined by the variance
+        within one chain, W, with the variance between chains B.
+        Both variances are combined in a weighted sum to obtain an estimate of
+        the variance of a parameter \\( \\theta \\).The square root of the
+        ratio of this estimates variance to the within chain variance is called
+        the potential scale reduction.
+        For a well converged chain it should approach 1. Values greater than
+        1.1 typically indicate that the chains have not yet fully converged.
+
+        Source: http://joergdietrich.github.io/emcee-convergence.html
+
+        https://github.com/jwalton3141/jwalton3141.github.io/blob/master/assets/posts/ESS/rwmh.py
+
+        Parameters
+        ----------
+        chain : array (n_walkers, n_steps, n_params)
+            The emcee ensamples.
+
+        Returns
+        -------
+        R_hat : float
+            The Gelman-Robin values.
+
+        """
+        m_chains, n_iters = chain.shape[:2]
+
+        # Calculate between-chain variance
+        θb = np.mean(chain, axis=1)
+        θbb = np.mean(θb, axis=0)
+        B_over_n = ((θbb - θb)**2).sum(axis=0)
+        B_over_n /= (m_chains - 1)
+
+        # Calculate within-chain variances
+        ssq = np.var(chain, axis=1, ddof=1)
+        W = np.mean(ssq, axis=0)
+
+        # (over) estimate of variance
+        var_θ = W * (n_iters - 1) / n_iters + B_over_n
+
+        if return_var:
+            return var_θ
+        else:
+            # The square root of the ratio of this estimates variance to the
+            # within chain variance
+            R_hat = np.sqrt(var_θ / W)
+            return R_hat
+
+    # -------------------------------------------------------------------------
+    def marginal_llk_emcee(self, sampler, nburn=None, logp=None, maxiter=1000):
+        """
+        The Bridge Sampling Estimator of the Marginal Likelihood based on
+        https://gist.github.com/junpenglao/4d2669d69ddfe1d788318264cdcf0583
+
+        Parameters
+        ----------
+        sampler : TYPE
+            MultiTrace, result of MCMC run.
+        nburn : int, optional
+            Number of burn-in step. The default is None.
+        logp : TYPE, optional
+            Model Log-probability function. The default is None.
+        maxiter : int, optional
+            Maximum number of iterations. The default is 1000.
+
+        Returns
+        -------
+        marg_llk : dict
+            Estimated Marginal log-Likelihood.
+
+        """
+        r0, tol1, tol2 = 0.5, 1e-10, 1e-4
+
+        if logp is None:
+            logp = sampler.log_prob_fn
+
+        # Split the samples into two parts
+        # Use the first 50% for fiting the proposal distribution
+        # and the second 50% in the iterative scheme.
+        if nburn is None:
+            mtrace = sampler.chain
+        else:
+            mtrace = sampler.chain[:, nburn:, :]
+
+        nchain, len_trace, nrofVars = mtrace.shape
+
+        N1_ = len_trace // 2
+        N1 = N1_*nchain
+        N2 = len_trace*nchain - N1
+
+        samples_4_fit = np.zeros((nrofVars, N1))
+        samples_4_iter = np.zeros((nrofVars, N2))
+        effective_n = np.zeros((nrofVars))
+
+        # matrix with already transformed samples
+        for var in range(nrofVars):
+
+            # for fitting the proposal
+            x = mtrace[:, :N1_, var]
+
+            samples_4_fit[var, :] = x.flatten()
+            # for the iterative scheme
+            x2 = mtrace[:, N1_:, var]
+            samples_4_iter[var, :] = x2.flatten()
+
+            # effective sample size of samples_4_iter, scalar
+            effective_n[var] = self._my_ESS(x2)
+
+        # median effective sample size (scalar)
+        neff = np.median(effective_n)
+
+        # get mean & covariance matrix and generate samples from proposal
+        m = np.mean(samples_4_fit, axis=1)
+        V = np.cov(samples_4_fit)
+        L = chol(V, lower=True)
+
+        # Draw N2 samples from the proposal distribution
+        gen_samples = m[:, None] + np.dot(
+            L, st.norm.rvs(0, 1, size=samples_4_iter.shape)
+            )
+
+        # Evaluate proposal distribution for posterior & generated samples
+        q12 = st.multivariate_normal.logpdf(samples_4_iter.T, m, V)
+        q22 = st.multivariate_normal.logpdf(gen_samples.T, m, V)
+
+        # Evaluate unnormalized posterior for posterior & generated samples
+        q11 = logp(samples_4_iter.T)
+        q21 = logp(gen_samples.T)
+
+        # Run iterative scheme:
+        tmp = self._iterative_scheme(
+            N1, N2, q11, q12, q21, q22, r0, neff, tol1, maxiter, 'r'
+            )
+        if ~np.isfinite(tmp['logml']):
+            warnings.warn(
+                "Logml could not be estimated within maxiter, rerunning with "
+                "adjusted starting value. Estimate might be more variable than"
+                " usual.")
+            # use geometric mean as starting value
+            r0_2 = np.sqrt(tmp['r_vals'][-2]*tmp['r_vals'][-1])
+            tmp = self._iterative_scheme(
+                q11, q12, q21, q22, r0_2, neff, tol2, maxiter, 'logml'
+                )
+
+        marg_llk = dict(
+            logml=tmp['logml'], niter=tmp['niter'], method="normal",
+            q11=q11, q12=q12, q21=q21, q22=q22
+            )
+        return marg_llk
+
+    # -------------------------------------------------------------------------
+    def _iterative_scheme(self, N1, N2, q11, q12, q21, q22, r0, neff, tol,
+                          maxiter, criterion):
+        """
+        Iterative scheme as proposed in Meng and Wong (1996) to estimate the
+        marginal likelihood
+
+        """
+        l1 = q11 - q12
+        l2 = q21 - q22
+        # To increase numerical stability,
+        # subtracting the median of l1 from l1 & l2 later
+        lstar = np.median(l1)
+        s1 = neff/(neff + N2)
+        s2 = N2/(neff + N2)
+        r = r0
+        r_vals = [r]
+        logml = np.log(r) + lstar
+        criterion_val = 1 + tol
+
+        i = 0
+        while (i <= maxiter) & (criterion_val > tol):
+            rold = r
+            logmlold = logml
+            numi = np.exp(l2 - lstar)/(s1 * np.exp(l2 - lstar) + s2 * r)
+            deni = 1/(s1 * np.exp(l1 - lstar) + s2 * r)
+            if np.sum(~np.isfinite(numi))+np.sum(~np.isfinite(deni)) > 0:
+                warnings.warn(
+                    """Infinite value in iterative scheme, returning NaN.
+                     Try rerunning with more samples.""")
+            r = (N1/N2) * np.sum(numi)/np.sum(deni)
+            r_vals.append(r)
+            logml = np.log(r) + lstar
+            i += 1
+            if criterion == 'r':
+                criterion_val = np.abs((r - rold)/r)
+            elif criterion == 'logml':
+                criterion_val = np.abs((logml - logmlold)/logml)
+
+        if i >= maxiter:
+            return dict(logml=np.NaN, niter=i, r_vals=np.asarray(r_vals))
+        else:
+            return dict(logml=logml, niter=i)
+
+    # -------------------------------------------------------------------------
+    def _my_ESS(self, x):
+        """
+        Compute the effective sample size of estimand of interest.
+        Vectorised implementation.
+        https://github.com/jwalton3141/jwalton3141.github.io/blob/master/assets/posts/ESS/rwmh.py
+
+
+        Parameters
+        ----------
+        x : array of shape (n_walkers, n_steps)
+            MCMC Samples.
+
+        Returns
+        -------
+        int
+            Effective sample size.
+
+        """
+        m_chains, n_iters = x.shape
+
+        def variogram(t):
+            variogram = ((x[:, t:] - x[:, :(n_iters - t)])**2).sum()
+            variogram /= (m_chains * (n_iters - t))
+            return variogram
+
+        post_var = self.gelman_rubin(x, return_var=True)
+
+        t = 1
+        rho = np.ones(n_iters)
+        negative_autocorr = False
+
+        # Iterate until the sum of consecutive estimates of autocorrelation is
+        # negative
+        while not negative_autocorr and (t < n_iters):
+            rho[t] = 1 - variogram(t) / (2 * post_var)
+
+            if not t % 2:
+                negative_autocorr = sum(rho[t-1:t+1]) < 0
+
+            t += 1
+
+        return int(m_chains*n_iters / (1 + 2*rho[1:t].sum()))
+
+    # -------------------------------------------------------------------------
+    def _check_ranges(self, theta, ranges):
+        """
+        This function checks if theta lies in the given ranges.
+
+        Parameters
+        ----------
+        theta : array
+            Proposed parameter set.
+        ranges : nested list
+            List of the praremeter ranges.
+
+        Returns
+        -------
+        c : bool
+            If it lies in the given range, it return True else False.
+
+        """
+        c = True
+        # traverse in the list1
+        for i, bounds in enumerate(ranges):
+            x = theta[i]
+            # condition check
+            if x < bounds[0] or x > bounds[1]:
+                c = False
+                return c
+        return c
diff --git a/examples/model-comparison/bayesvalidrox/bayesvalidrox.mplstyle b/examples/model-comparison/bayesvalidrox/bayesvalidrox.mplstyle
new file mode 100644
index 0000000000000000000000000000000000000000..1f31c01f24597de0e0be741be4d3a706c4213a6c
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/bayesvalidrox.mplstyle
@@ -0,0 +1,16 @@
+figure.titlesize : 30
+axes.titlesize : 30
+axes.labelsize : 30
+axes.linewidth : 3
+axes.grid : True
+lines.linewidth : 3
+lines.markersize : 10
+xtick.labelsize : 30
+ytick.labelsize : 30
+legend.fontsize : 30
+font.family : serif
+font.serif : Arial
+font.size : 30
+text.usetex : True
+grid.linestyle : -
+figure.figsize : 24, 16
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/__init__.py b/examples/model-comparison/bayesvalidrox/post_processing/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..81c9825420b6ed3f027fb3c141be8af05a89f695
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/post_processing/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from .post_processing import PostProcessing
+
+__all__ = [
+    "PostProcessing"
+    ]
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c8590a242166b2e8d40de7ee2eece71980bd1571
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e84acd550fed2f7af8a071adf99001f44547bdf6
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..58a0eb24635d0b97a14d13708e616de6a0659976
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0924d8afac04d4fe82ebe791bc55a8ae48d7c117
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..94ffbf6f3da6b2b15cdf648a10ce9edb82d90834
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..312575d7655db85df423489051f494f3dce62692
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/post_processing/__pycache__/post_processing.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/post_processing/post_processing.py b/examples/model-comparison/bayesvalidrox/post_processing/post_processing.py
new file mode 100644
index 0000000000000000000000000000000000000000..6520a40f9f2393798f6b8abac026b9ed38fe33ca
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/post_processing/post_processing.py
@@ -0,0 +1,1338 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import numpy as np
+import math
+import os
+from itertools import combinations, cycle
+import pandas as pd
+import scipy.stats as stats
+from sklearn.linear_model import LinearRegression
+from sklearn.metrics import mean_squared_error, r2_score
+import matplotlib.pyplot as plt
+import matplotlib.ticker as ticker
+from matplotlib.offsetbox import AnchoredText
+from matplotlib.patches import Patch
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class PostProcessing:
+    """
+    This class provides many helper functions to post-process the trained
+    meta-model.
+
+    Attributes
+    ----------
+    MetaModel : obj
+        MetaModel object to do postprocessing on.
+    name : str
+        Type of the anaylsis. The default is `'calib'`. If a validation is
+        expected to be performed change this to `'valid'`.
+    """
+
+    def __init__(self, engine, name='calib'):
+        self.engine = engine
+        self.MetaModel = engine.MetaModel
+        self.ExpDesign = engine.ExpDesign
+        self.ModelObj = engine.Model
+        self.name = name
+
+    # -------------------------------------------------------------------------
+    def plot_moments(self, xlabel='Time [s]', plot_type=None):
+        """
+        Plots the moments in a pdf format in the directory
+        `Outputs_PostProcessing`.
+
+        Parameters
+        ----------
+        xlabel : str, optional
+            String to be displayed as x-label. The default is `'Time [s]'`.
+        plot_type : str, optional
+            Options: bar or line. The default is `None`.
+
+        Returns
+        -------
+        pce_means: dict
+            Mean of the model outputs.
+        pce_means: dict
+            Standard deviation of the model outputs.
+
+        """
+
+        bar_plot = True if plot_type == 'bar' else False
+        meta_model_type = self.MetaModel.meta_model_type
+        Model = self.ModelObj
+
+        # Read Monte-Carlo reference
+        self.mc_reference = Model.read_observation('mc_ref')
+
+        # Set the x values
+        x_values_orig = self.engine.ExpDesign.x_values
+
+        # Compute the moments with the PCEModel object
+        self.pce_means, self.pce_stds = self.compute_pce_moments()
+
+        # Get the variables
+        out_names = Model.Output.names
+
+        # Open a pdf for the plots
+        newpath = (f'Outputs_PostProcessing_{self.name}/')
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # Plot the best fit line, set the linewidth (lw), color and
+        # transparency (alpha) of the line
+        for key in out_names:
+            fig, ax = plt.subplots(nrows=1, ncols=2)
+
+            # Extract mean and std
+            mean_data = self.pce_means[key]
+            std_data = self.pce_stds[key]
+
+            # Extract a list of x values
+            if type(x_values_orig) is dict:
+                x = x_values_orig[key]
+            else:
+                x = x_values_orig
+
+            # Plot: bar plot or line plot
+            if bar_plot:
+                ax[0].bar(list(map(str, x)), mean_data, color='b',
+                          width=0.25)
+                ax[1].bar(list(map(str, x)), std_data, color='b',
+                          width=0.25)
+                ax[0].legend(labels=[meta_model_type])
+                ax[1].legend(labels=[meta_model_type])
+            else:
+                ax[0].plot(x, mean_data, lw=3, color='k', marker='x',
+                           label=meta_model_type)
+                ax[1].plot(x, std_data, lw=3, color='k', marker='x',
+                           label=meta_model_type)
+
+            if self.mc_reference is not None:
+                if bar_plot:
+                    ax[0].bar(list(map(str, x)), self.mc_reference['mean'],
+                              color='r', width=0.25)
+                    ax[1].bar(list(map(str, x)), self.mc_reference['std'],
+                              color='r', width=0.25)
+                    ax[0].legend(labels=[meta_model_type])
+                    ax[1].legend(labels=[meta_model_type])
+                else:
+                    ax[0].plot(x, self.mc_reference['mean'], lw=3, marker='x',
+                               color='r', label='Ref.')
+                    ax[1].plot(x, self.mc_reference['std'], lw=3, marker='x',
+                               color='r', label='Ref.')
+
+            # Label the axes and provide a title
+            ax[0].set_xlabel(xlabel)
+            ax[1].set_xlabel(xlabel)
+            ax[0].set_ylabel(key)
+            ax[1].set_ylabel(key)
+
+            # Provide a title
+            ax[0].set_title('Mean of ' + key)
+            ax[1].set_title('Std of ' + key)
+
+            if not bar_plot:
+                ax[0].legend(loc='best')
+                ax[1].legend(loc='best')
+
+            plt.tight_layout()
+
+            # save the current figure
+            fig.savefig(
+                f'./{newpath}Mean_Std_PCE_{key}.pdf',
+                bbox_inches='tight'
+                )
+
+        return self.pce_means, self.pce_stds
+
+    # -------------------------------------------------------------------------
+    def valid_metamodel(self, n_samples=1, samples=None, model_out_dict=None,
+                        x_axis='Time [s]'):
+        """
+        Evaluates and plots the meta model and the PCEModel outputs for the
+        given number of samples or the given samples.
+
+        Parameters
+        ----------
+        n_samples : int, optional
+            Number of samples to be evaluated. The default is 1.
+        samples : array of shape (n_samples, n_params), optional
+            Samples to be evaluated. The default is None.
+        model_out_dict: dict
+            The model runs using the samples provided.
+        x_axis : str, optional
+            Label of x axis. The default is `'Time [s]'`.
+
+        Returns
+        -------
+        None.
+
+        """
+        MetaModel = self.MetaModel
+        Model = self.ModelObj
+
+        if samples is None:
+            self.n_samples = n_samples
+            samples = self._get_sample()
+        else:
+            self.n_samples = samples.shape[0]
+
+        # Extract x_values
+        x_values = self.engine.ExpDesign.x_values
+
+        if model_out_dict is not None:
+            self.model_out_dict = model_out_dict
+        else:
+            self.model_out_dict = self._eval_model(samples, key_str='valid')
+        self.pce_out_mean, self.pce_out_std = MetaModel.eval_metamodel(samples)
+
+        try:
+            key = Model.Output.names[1]
+        except IndexError:
+            key = Model.Output.names[0]
+
+        n_obs = self.model_out_dict[key].shape[1]
+
+        if n_obs == 1:
+            self._plot_validation()
+        else:
+            self._plot_validation_multi(x_values=x_values, x_axis=x_axis)
+
+    # -------------------------------------------------------------------------
+    def check_accuracy(self, n_samples=None, samples=None, outputs=None):
+        """
+        Checks accuracy of the metamodel by computing the root mean square
+        error and validation error for all outputs.
+
+        Parameters
+        ----------
+        n_samples : int, optional
+            Number of samples. The default is None.
+        samples : array of shape (n_samples, n_params), optional
+            Parameter sets to be checked. The default is None.
+        outputs : dict, optional
+            Output dictionary with model outputs for all given output types in
+            `Model.Output.names`. The default is None.
+
+        Raises
+        ------
+        Exception
+            When neither n_samples nor samples are provided.
+
+        Returns
+        -------
+        rmse: dict
+            Root mean squared error for each output.
+        valid_error : dict
+            Validation error for each output.
+
+        """
+        MetaModel = self.MetaModel
+        Model = self.ModelObj
+
+        # Set the number of samples
+        if n_samples:
+            self.n_samples = n_samples
+        elif samples is not None:
+            self.n_samples = samples.shape[0]
+        else:
+            raise Exception("Please provide either samples or pass the number"
+                            " of samples!")
+
+        # Generate random samples if necessary
+        Samples = self._get_sample() if samples is None else samples
+
+        # Run the original model with the generated samples
+        if outputs is None:
+            outputs = self._eval_model(Samples, key_str='validSet')
+
+        # Run the PCE model with the generated samples
+        pce_outputs, _ = MetaModel.eval_metamodel(samples=Samples)
+
+        self.rmse = {}
+        self.valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in Model.Output.names:
+            # Root mena square
+            self.rmse[key] = mean_squared_error(outputs[key], pce_outputs[key],
+                                                squared=False,
+                                                multioutput='raw_values')
+            # Validation error
+            self.valid_error[key] = (self.rmse[key]**2) / \
+                np.var(outputs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(self.rmse[key],
+                                             self.valid_error[key]))))
+        # Save error dicts in PCEModel object
+        self.MetaModel.rmse = self.rmse
+        self.MetaModel.valid_error = self.valid_error
+
+        return
+
+    # -------------------------------------------------------------------------
+    def plot_seq_design_diagnostics(self, ref_BME_KLD=None):
+        """
+        Plots the Bayesian Model Evidence (BME) and Kullback-Leibler divergence
+        (KLD) for the sequential design.
+
+        Parameters
+        ----------
+        ref_BME_KLD : array, optional
+            Reference BME and KLD . The default is `None`.
+
+        Returns
+        -------
+        None.
+
+        """
+        engine = self.engine
+        PCEModel = self.MetaModel
+        n_init_samples = engine.ExpDesign.n_init_samples
+        n_total_samples = engine.ExpDesign.X.shape[0]
+
+        newpath = f'Outputs_PostProcessing_{self.name}/seq_design_diagnostics/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        plotList = ['Modified LOO error', 'Validation error', 'KLD', 'BME',
+                    'RMSEMean', 'RMSEStd', 'Hellinger distance']
+        seqList = [engine.SeqModifiedLOO, engine.seqValidError,
+                   engine.SeqKLD, engine.SeqBME, engine.seqRMSEMean,
+                   engine.seqRMSEStd, engine.SeqDistHellinger]
+
+        markers = ('x', 'o', 'd', '*', '+')
+        colors = ('k', 'darkgreen', 'b', 'navy', 'darkred')
+
+        # Plot the evolution of the diagnostic criteria of the
+        # Sequential Experimental Design.
+        for plotidx, plot in enumerate(plotList):
+            fig, ax = plt.subplots()
+            seq_dict = seqList[plotidx]
+            name_util = list(seq_dict.keys())
+
+            if len(name_util) == 0:
+                continue
+
+            # Box plot when Replications have been detected.
+            if any(int(name.split("rep_", 1)[1]) > 1 for name in name_util):
+                # Extract the values from dict
+                sorted_seq_opt = {}
+                # Number of replications
+                n_reps = engine.ExpDesign.n_replication
+
+                # Get the list of utility function names
+                # Handle if only one UtilityFunction is provided
+                if not isinstance(engine.ExpDesign.util_func, list):
+                    util_funcs = [engine.ExpDesign.util_func]
+                else:
+                    util_funcs = engine.ExpDesign.util_func
+
+                for util in util_funcs:
+                    sortedSeq = {}
+                    # min number of runs available from reps
+                    n_runs = min([seq_dict[f'{util}_rep_{i+1}'].shape[0]
+                                 for i in range(n_reps)])
+
+                    for runIdx in range(n_runs):
+                        values = []
+                        for key in seq_dict.keys():
+                            if util in key:
+                                values.append(seq_dict[key][runIdx].mean())
+                        sortedSeq['SeqItr_'+str(runIdx)] = np.array(values)
+                    sorted_seq_opt[util] = sortedSeq
+
+                # BoxPlot
+                def draw_plot(data, labels, edge_color, fill_color, idx):
+                    pos = labels - (idx-1)
+                    bp = plt.boxplot(data, positions=pos, labels=labels,
+                                     patch_artist=True, sym='', widths=0.75)
+                    elements = ['boxes', 'whiskers', 'fliers', 'means',
+                                'medians', 'caps']
+                    for element in elements:
+                        plt.setp(bp[element], color=edge_color[idx])
+
+                    for patch in bp['boxes']:
+                        patch.set(facecolor=fill_color[idx])
+
+                if engine.ExpDesign.n_new_samples != 1:
+                    step1 = engine.ExpDesign.n_new_samples
+                    step2 = 1
+                else:
+                    step1 = 5
+                    step2 = 5
+                edge_color = ['red', 'blue', 'green']
+                fill_color = ['tan', 'cyan', 'lightgreen']
+                plot_label = plot
+                # Plot for different Utility Functions
+                for idx, util in enumerate(util_funcs):
+                    all_errors = np.empty((n_reps, 0))
+
+                    for key in list(sorted_seq_opt[util].keys()):
+                        errors = sorted_seq_opt.get(util, {}).get(key)[:, None]
+                        all_errors = np.hstack((all_errors, errors))
+
+                    # Special cases for BME and KLD
+                    if plot == 'KLD' or plot == 'BME':
+                        # BME convergence if refBME is provided
+                        if ref_BME_KLD is not None:
+                            if plot == 'BME':
+                                refValue = ref_BME_KLD[0]
+                                plot_label = r'BME/BME$^{Ref.}$'
+                            if plot == 'KLD':
+                                refValue = ref_BME_KLD[1]
+                                plot_label = '$D_{KL}[p(\\theta|y_*),p(\\theta)]'\
+                                    ' / D_{KL}^{Ref.}[p(\\theta|y_*), '\
+                                    'p(\\theta)]$'
+
+                            # Difference between BME/KLD and the ref. values
+                            all_errors = np.divide(all_errors,
+                                                   np.full((all_errors.shape),
+                                                           refValue))
+
+                            # Plot baseline for zero, i.e. no difference
+                            plt.axhline(y=1.0, xmin=0, xmax=1, c='green',
+                                        ls='--', lw=2)
+
+                    # Plot each UtilFuncs
+                    labels = np.arange(n_init_samples, n_total_samples+1, step1)
+                    draw_plot(all_errors[:, ::step2], labels, edge_color,
+                              fill_color, idx)
+
+                plt.xticks(labels, labels)
+                # Set the major and minor locators
+                ax.xaxis.set_major_locator(ticker.AutoLocator())
+                ax.xaxis.set_minor_locator(ticker.AutoMinorLocator())
+                ax.xaxis.grid(True, which='major', linestyle='-')
+                ax.xaxis.grid(True, which='minor', linestyle='--')
+
+                # Legend
+                legend_elements = []
+                for idx, util in enumerate(util_funcs):
+                    legend_elements.append(Patch(facecolor=fill_color[idx],
+                                                 edgecolor=edge_color[idx],
+                                                 label=util))
+                plt.legend(handles=legend_elements[::-1], loc='best')
+
+                if plot != 'BME' and plot != 'KLD':
+                    plt.yscale('log')
+                plt.autoscale(True)
+                plt.xlabel('\\# of training samples')
+                plt.ylabel(plot_label)
+                plt.title(plot)
+
+                # save the current figure
+                plot_name = plot.replace(' ', '_')
+                fig.savefig(
+                    f'./{newpath}/seq_{plot_name}.pdf',
+                    bbox_inches='tight'
+                    )
+                # Destroy the current plot
+                plt.clf()
+                # Save arrays into files
+                f = open(f'./{newpath}/seq_{plot_name}.txt', 'w')
+                f.write(str(sorted_seq_opt))
+                f.close()
+            else:
+                for idx, name in enumerate(name_util):
+                    seq_values = seq_dict[name]
+                    if engine.ExpDesign.n_new_samples != 1:
+                        step = engine.ExpDesign.n_new_samples
+                    else:
+                        step = 1
+                    x_idx = np.arange(n_init_samples, n_total_samples+1, step)
+                    if n_total_samples not in x_idx:
+                        x_idx = np.hstack((x_idx, n_total_samples))
+
+                    if plot == 'KLD' or plot == 'BME':
+                        # BME convergence if refBME is provided
+                        if ref_BME_KLD is not None:
+                            if plot == 'BME':
+                                refValue = ref_BME_KLD[0]
+                                plot_label = r'BME/BME$^{Ref.}$'
+                            if plot == 'KLD':
+                                refValue = ref_BME_KLD[1]
+                                plot_label = '$D_{KL}[p(\\theta|y_*),p(\\theta)]'\
+                                    ' / D_{KL}^{Ref.}[p(\\theta|y_*), '\
+                                    'p(\\theta)]$'
+
+                            # Difference between BME/KLD and the ref. values
+                            values = np.divide(seq_values,
+                                               np.full((seq_values.shape),
+                                                       refValue))
+
+                            # Plot baseline for zero, i.e. no difference
+                            plt.axhline(y=1.0, xmin=0, xmax=1, c='green',
+                                        ls='--', lw=2)
+
+                            # Set the limits
+                            plt.ylim([1e-1, 1e1])
+
+                            # Create the plots
+                            plt.semilogy(x_idx, values, marker=markers[idx],
+                                         color=colors[idx], ls='--', lw=2,
+                                         label=name.split("_rep", 1)[0])
+                        else:
+                            plot_label = plot
+
+                            # Create the plots
+                            plt.plot(x_idx, seq_values, marker=markers[idx],
+                                     color=colors[idx], ls='--', lw=2,
+                                     label=name.split("_rep", 1)[0])
+
+                    else:
+                        plot_label = plot
+                        seq_values = np.nan_to_num(seq_values)
+
+                        # Plot the error evolution for each output
+                        plt.semilogy(x_idx, seq_values.mean(axis=1),
+                                     marker=markers[idx], ls='--', lw=2,
+                                     color=colors[idx],
+                                     label=name.split("_rep", 1)[0])
+
+                # Set the major and minor locators
+                ax.xaxis.set_major_locator(ticker.AutoLocator())
+                ax.xaxis.set_minor_locator(ticker.AutoMinorLocator())
+                ax.xaxis.grid(True, which='major', linestyle='-')
+                ax.xaxis.grid(True, which='minor', linestyle='--')
+
+                ax.tick_params(axis='both', which='major', direction='in',
+                               width=3, length=10)
+                ax.tick_params(axis='both', which='minor', direction='in',
+                               width=2, length=8)
+                plt.xlabel('Number of runs')
+                plt.ylabel(plot_label)
+                plt.title(plot)
+                plt.legend(frameon=True)
+
+                # save the current figure
+                plot_name = plot.replace(' ', '_')
+                fig.savefig(
+                    f'./{newpath}/seq_{plot_name}.pdf',
+                    bbox_inches='tight'
+                    )
+                # Destroy the current plot
+                plt.clf()
+
+                # ---------------- Saving arrays into files ---------------
+                np.save(f'./{newpath}/seq_{plot_name}.npy', seq_values)
+
+        return
+
+    # -------------------------------------------------------------------------
+    def sobol_indices(self, xlabel='Time [s]', plot_type=None):
+        """
+        Provides Sobol indices as a sensitivity measure to infer the importance
+        of the input parameters. See Eq. 27 in [1] for more details. For the
+        case with Principal component analysis refer to [2].
+
+        [1] Global sensitivity analysis: A flexible and efficient framework
+        with an example from stochastic hydrogeology S. Oladyshkin, F.P.
+        de Barros, W. Nowak  https://doi.org/10.1016/j.advwatres.2011.11.001
+
+        [2] Nagel, J.B., Rieckermann, J. and Sudret, B., 2020. Principal
+        component analysis and sparse polynomial chaos expansions for global
+        sensitivity analysis and model calibration: Application to urban
+        drainage simulation. Reliability Engineering & System Safety, 195,
+        p.106737.
+
+        Parameters
+        ----------
+        xlabel : str, optional
+            Label of the x-axis. The default is `'Time [s]'`.
+        plot_type : str, optional
+            Plot type. The default is `None`. This corresponds to line plot.
+            Bar chart can be selected by `bar`.
+
+        Returns
+        -------
+        sobol_cell: dict
+            Sobol indices.
+        total_sobol: dict
+            Total Sobol indices.
+
+        """
+        # Extract the necessary variables
+        PCEModel = self.MetaModel
+        basis_dict = PCEModel.basis_dict
+        coeffs_dict = PCEModel.coeffs_dict
+        n_params = PCEModel.n_params
+        max_order = np.max(PCEModel.pce_deg)
+        sobol_cell_b = {}
+        total_sobol_b = {}
+        cov_Z_p_q = np.zeros((n_params))
+
+        for b_i in range(PCEModel.n_bootstrap_itrs):
+
+            sobol_cell_, total_sobol_ = {}, {}
+
+            for output in self.ModelObj.Output.names:
+
+                n_meas_points = len(coeffs_dict[f'b_{b_i+1}'][output])
+
+                # Initialize the (cell) array containing the (total) Sobol indices.
+                sobol_array = dict.fromkeys(range(1, max_order+1), [])
+                sobol_cell_array = dict.fromkeys(range(1, max_order+1), [])
+
+                for i_order in range(1, max_order+1):
+                    n_comb = math.comb(n_params, i_order)
+
+                    sobol_cell_array[i_order] = np.zeros((n_comb, n_meas_points))
+
+                total_sobol_array = np.zeros((n_params, n_meas_points))
+
+                # Initialize the cell to store the names of the variables
+                TotalVariance = np.zeros((n_meas_points))
+                # Loop over all measurement points and calculate sobol indices
+                for pIdx in range(n_meas_points):
+
+                    # Extract the basis indices (alpha) and coefficients
+                    Basis = basis_dict[f'b_{b_i+1}'][output][f'y_{pIdx+1}']
+
+                    try:
+                        clf_poly = PCEModel.clf_poly[f'b_{b_i+1}'][output][f'y_{pIdx+1}']
+                        PCECoeffs = clf_poly.coef_
+                    except:
+                        PCECoeffs = coeffs_dict[f'b_{b_i+1}'][output][f'y_{pIdx+1}']
+
+                    # Compute total variance
+                    TotalVariance[pIdx] = np.sum(np.square(PCECoeffs[1:]))
+
+                    nzidx = np.where(PCECoeffs != 0)[0]
+                    # Set all the Sobol indices equal to zero in the presence of a
+                    # null output.
+                    if len(nzidx) == 0:
+                        # This is buggy.
+                        for i_order in range(1, max_order+1):
+                            sobol_cell_array[i_order][:, pIdx] = 0
+
+                    # Otherwise compute them by summing well-chosen coefficients
+                    else:
+                        nz_basis = Basis[nzidx]
+                        for i_order in range(1, max_order+1):
+                            idx = np.where(np.sum(nz_basis > 0, axis=1) == i_order)
+                            subbasis = nz_basis[idx]
+                            Z = np.array(list(combinations(range(n_params), i_order)))
+
+                            for q in range(Z.shape[0]):
+                                Zq = Z[q]
+                                subsubbasis = subbasis[:, Zq]
+                                subidx = np.prod(subsubbasis, axis=1) > 0
+                                sum_ind = nzidx[idx[0][subidx]]
+                                if TotalVariance[pIdx] == 0.0:
+                                    sobol_cell_array[i_order][q, pIdx] = 0.0
+                                else:
+                                    sobol = np.sum(np.square(PCECoeffs[sum_ind]))
+                                    sobol /= TotalVariance[pIdx]
+                                    sobol_cell_array[i_order][q, pIdx] = sobol
+
+                        # Compute the TOTAL Sobol indices.
+                        for ParIdx in range(n_params):
+                            idx = nz_basis[:, ParIdx] > 0
+                            sum_ind = nzidx[idx]
+
+                            if TotalVariance[pIdx] == 0.0:
+                                total_sobol_array[ParIdx, pIdx] = 0.0
+                            else:
+                                sobol = np.sum(np.square(PCECoeffs[sum_ind]))
+                                sobol /= TotalVariance[pIdx]
+                                total_sobol_array[ParIdx, pIdx] = sobol
+
+                    # ----- if PCA selected: Compute covariance -----
+                    if PCEModel.dim_red_method.lower() == 'pca':
+                        # Extract the basis indices (alpha) and coefficients for
+                        # next component
+                        if pIdx < n_meas_points-1:
+                            nextBasis = basis_dict[f'b_{b_i+1}'][output][f'y_{pIdx+2}']
+                            if PCEModel.bootstrap_method != 'fast' or b_i == 0:
+                                clf_poly = PCEModel.clf_poly[f'b_{b_i+1}'][output][f'y_{pIdx+2}']
+                                nextPCECoeffs = clf_poly.coef_
+                            else:
+                                nextPCECoeffs = coeffs_dict[f'b_{b_i+1}'][output][f'y_{pIdx+2}']
+
+                            # Choose the common non-zero basis
+                            mask = (Basis[:, None] == nextBasis).all(-1).any(-1)
+                            n_mask = (nextBasis[:, None] == Basis).all(-1).any(-1)
+
+                            # Compute the covariance in Eq 17.
+                            for ParIdx in range(n_params):
+                                idx = (mask) & (Basis[:, ParIdx] > 0)
+                                n_idx = (n_mask) & (nextBasis[:, ParIdx] > 0)
+                                try:
+                                    cov_Z_p_q[ParIdx] += np.sum(np.dot(
+                                        PCECoeffs[idx], nextPCECoeffs[n_idx])
+                                        )
+                                except:
+                                    pass
+
+                # Compute the sobol indices according to Ref. 2
+                if PCEModel.dim_red_method.lower() == 'pca':
+                    n_c_points = self.engine.ExpDesign.Y[output].shape[1]
+                    PCA = PCEModel.pca[f'b_{b_i+1}'][output]
+                    compPCA = PCA.components_
+                    nComp = compPCA.shape[0]
+                    var_Z_p = PCA.explained_variance_
+
+                    # Extract the sobol index of the components
+                    for i_order in range(1, max_order+1):
+                        n_comb = math.comb(n_params, i_order)
+                        sobol_array[i_order] = np.zeros((n_comb, n_c_points))
+                        Z = np.array(list(combinations(range(n_params), i_order)))
+
+                        # Loop over parameters
+                        for q in range(Z.shape[0]):
+                            S_Z_i = sobol_cell_array[i_order][q]
+
+                            for tIdx in range(n_c_points):
+                                var_Y_t = np.var(
+                                    self.engine.ExpDesign.Y[output][:, tIdx])
+                                if var_Y_t == 0.0:
+                                    term1, term2 = 0.0, 0.0
+                                else:
+                                    # Eq. 17
+                                    term1 = 0.0
+                                    for i in range(nComp):
+                                        a = S_Z_i[i] * var_Z_p[i]
+                                        a *= compPCA[i, tIdx]**2
+                                        term1 += a
+
+                                    # TODO: Term 2
+                                    # term2 = 0.0
+                                    # for i in range(nComp-1):
+                                    #     term2 += cov_Z_p_q[q] * compPCA[i, tIdx]
+                                    #     term2 *= compPCA[i+1, tIdx]
+                                    # term2 *= 2
+
+                                sobol_array[i_order][q, tIdx] = term1 #+ term2
+
+                                # Devide over total output variance Eq. 18
+                                sobol_array[i_order][q, tIdx] /= var_Y_t
+
+                    # Compute the TOTAL Sobol indices.
+                    total_sobol = np.zeros((n_params, n_c_points))
+                    for ParIdx in range(n_params):
+                        S_Z_i = total_sobol_array[ParIdx]
+
+                        for tIdx in range(n_c_points):
+                            var_Y_t = np.var(self.engine.ExpDesign.Y[output][:, tIdx])
+                            if var_Y_t == 0.0:
+                                term1, term2 = 0.0, 0.0
+                            else:
+                                term1 = 0
+                                for i in range(nComp):
+                                    term1 += S_Z_i[i] * var_Z_p[i] * \
+                                        (compPCA[i, tIdx]**2)
+
+                                # Term 2
+                                term2 = 0
+                                for i in range(nComp-1):
+                                    term2 += cov_Z_p_q[ParIdx] * compPCA[i, tIdx] \
+                                        * compPCA[i+1, tIdx]
+                                term2 *= 2
+
+                            total_sobol[ParIdx, tIdx] = term1 #+ term2
+
+                            # Devide over total output variance Eq. 18
+                            total_sobol[ParIdx, tIdx] /= var_Y_t
+
+                    sobol_cell_[output] = sobol_array
+                    total_sobol_[output] = total_sobol
+                else:
+                    sobol_cell_[output] = sobol_cell_array
+                    total_sobol_[output] = total_sobol_array
+
+            # Save for each bootsrtap iteration
+            sobol_cell_b[b_i] = sobol_cell_
+            total_sobol_b[b_i] = total_sobol_
+
+        # Average total sobol indices
+        total_sobol_all = {}
+        for i in sorted(total_sobol_b):
+            for k, v in total_sobol_b[i].items():
+                if k not in total_sobol_all:
+                    total_sobol_all[k] = [None] * len(total_sobol_b)
+                total_sobol_all[k][i] = v
+
+        self.total_sobol = {}
+        for output in self.ModelObj.Output.names:
+            self.total_sobol[output] = np.mean(total_sobol_all[output], axis=0)
+
+        # ---------------- Plot -----------------------
+        par_names = self.engine.ExpDesign.par_names
+        x_values_orig = self.engine.ExpDesign.x_values
+
+        newpath = (f'Outputs_PostProcessing_{self.name}/')
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        fig = plt.figure()
+
+        for outIdx, output in enumerate(self.ModelObj.Output.names):
+
+            # Extract total Sobol indices
+            total_sobol = self.total_sobol[output]
+
+            # Compute quantiles
+            q_5 = np.quantile(total_sobol_all[output], q=0.05, axis=0)
+            q_97_5 = np.quantile(total_sobol_all[output], q=0.975, axis=0)
+
+            # Extract a list of x values
+            if type(x_values_orig) is dict:
+                x = x_values_orig[output]
+            else:
+                x = x_values_orig
+
+            if plot_type == 'bar':
+                ax = fig.add_axes([0, 0, 1, 1])
+                dict1 = {xlabel: x}
+                dict2 = {param: sobolIndices for param, sobolIndices
+                         in zip(par_names, total_sobol)}
+
+                df = pd.DataFrame({**dict1, **dict2})
+                df.plot(x=xlabel, y=par_names, kind="bar", ax=ax, rot=0,
+                        colormap='Dark2', yerr=q_97_5-q_5)
+                ax.set_ylabel('Total Sobol indices, $S^T$')
+
+            else:
+                for i, sobolIndices in enumerate(total_sobol):
+                    plt.plot(x, sobolIndices, label=par_names[i],
+                             marker='x', lw=2.5)
+                    plt.fill_between(x, q_5[i], q_97_5[i], alpha=0.15)
+
+                plt.ylabel('Total Sobol indices, $S^T$')
+                plt.xlabel(xlabel)
+
+            plt.title(f'Sensitivity analysis of {output}')
+            if plot_type != 'bar':
+                plt.legend(loc='best', frameon=True)
+
+            # Save indices
+            np.savetxt(f'./{newpath}totalsobol_' +
+                       output.replace('/', '_') + '.csv',
+                       total_sobol.T, delimiter=',',
+                       header=','.join(par_names), comments='')
+
+            # save the current figure
+            fig.savefig(
+                f'./{newpath}Sobol_indices_{output}.pdf',
+                bbox_inches='tight'
+                )
+
+            # Destroy the current plot
+            plt.clf()
+
+        return self.total_sobol
+
+    # -------------------------------------------------------------------------
+    def check_reg_quality(self, n_samples=1000, samples=None):
+        """
+        Checks the quality of the metamodel for single output models based on:
+        https://towardsdatascience.com/how-do-you-check-the-quality-of-your-regression-model-in-python-fa61759ff685
+
+
+        Parameters
+        ----------
+        n_samples : int, optional
+            Number of parameter sets to use for the check. The default is 1000.
+        samples : array of shape (n_samples, n_params), optional
+            Parameter sets to use for the check. The default is None.
+
+        Returns
+        -------
+        None.
+
+        """
+        MetaModel = self.MetaModel
+
+        if samples is None:
+            self.n_samples = n_samples
+            samples = self._get_sample()
+        else:
+            self.n_samples = samples.shape[0]
+
+        # Evaluate the original and the surrogate model
+        y_val = self._eval_model(samples, key_str='valid')
+        y_pce_val, _ = MetaModel.eval_metamodel(samples=samples)
+
+        # Open a pdf for the plots
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # Fit the data(train the model)
+        for key in y_pce_val.keys():
+
+            y_pce_val_ = y_pce_val[key]
+            y_val_ = y_val[key]
+            residuals = y_val_ - y_pce_val_
+
+            # ------ Residuals vs. predicting variables ------
+            # Check the assumptions of linearity and independence
+            fig1 = plt.figure()
+            for i, par in enumerate(self.engine.ExpDesign.par_names):
+                plt.title(f"{key}: Residuals vs. {par}")
+                plt.scatter(
+                    x=samples[:, i], y=residuals, color='blue', edgecolor='k')
+                plt.grid(True)
+                xmin, xmax = min(samples[:, i]), max(samples[:, i])
+                plt.hlines(y=0, xmin=xmin*0.9, xmax=xmax*1.1, color='red',
+                           lw=3, linestyle='--')
+                plt.xlabel(par)
+                plt.ylabel('Residuals')
+                plt.show()
+
+                # save the current figure
+                fig1.savefig(f'./{newpath}/Residuals_vs_Par_{i+1}.pdf',
+                             bbox_inches='tight')
+                # Destroy the current plot
+                plt.clf()
+
+            # ------ Fitted vs. residuals ------
+            # Check the assumptions of linearity and independence
+            fig2 = plt.figure()
+            plt.title(f"{key}: Residuals vs. fitted values")
+            plt.scatter(x=y_pce_val_, y=residuals, color='blue', edgecolor='k')
+            plt.grid(True)
+            xmin, xmax = min(y_val_), max(y_val_)
+            plt.hlines(y=0, xmin=xmin*0.9, xmax=xmax*1.1, color='red', lw=3,
+                       linestyle='--')
+            plt.xlabel(key)
+            plt.ylabel('Residuals')
+            plt.show()
+
+            # save the current figure
+            fig2.savefig(f'./{newpath}/Fitted_vs_Residuals.pdf',
+                         bbox_inches='tight')
+            # Destroy the current plot
+            plt.clf()
+
+            # ------ Histogram of normalized residuals ------
+            fig3 = plt.figure()
+            resid_pearson = residuals / (max(residuals)-min(residuals))
+            plt.hist(resid_pearson, bins=20, edgecolor='k')
+            plt.ylabel('Count')
+            plt.xlabel('Normalized residuals')
+            plt.title(f"{key}: Histogram of normalized residuals")
+
+            # Normality (Shapiro-Wilk) test of the residuals
+            ax = plt.gca()
+            _, p = stats.shapiro(residuals)
+            if p < 0.01:
+                annText = "The residuals seem to come from a Gaussian Process."
+            else:
+                annText = "The normality assumption may not hold."
+            at = AnchoredText(annText, prop=dict(size=30), frameon=True,
+                              loc='upper left')
+            at.patch.set_boxstyle("round,pad=0.,rounding_size=0.2")
+            ax.add_artist(at)
+
+            plt.show()
+
+            # save the current figure
+            fig3.savefig(f'./{newpath}/Hist_NormResiduals.pdf',
+                         bbox_inches='tight')
+            # Destroy the current plot
+            plt.clf()
+
+            # ------ Q-Q plot of the normalized residuals ------
+            plt.figure()
+            stats.probplot(residuals[:, 0], plot=plt)
+            plt.xticks()
+            plt.yticks()
+            plt.xlabel("Theoretical quantiles")
+            plt.ylabel("Sample quantiles")
+            plt.title(f"{key}: Q-Q plot of normalized residuals")
+            plt.grid(True)
+            plt.show()
+
+            # save the current figure
+            plt.savefig(f'./{newpath}/QQPlot_NormResiduals.pdf',
+                        bbox_inches='tight')
+            # Destroy the current plot
+            plt.clf()
+
+    # -------------------------------------------------------------------------
+    def eval_pce_model_3d(self):
+
+        self.n_samples = 1000
+
+        PCEModel = self.MetaModel
+        Model = self.ModelObj
+        n_samples = self.n_samples
+
+        # Create 3D-Grid
+        # TODO: Make it general
+        x = np.linspace(-5, 10, n_samples)
+        y = np.linspace(0, 15, n_samples)
+
+        X, Y = np.meshgrid(x, y)
+        PCE_Z = np.zeros((self.n_samples, self.n_samples))
+        Model_Z = np.zeros((self.n_samples, self.n_samples))
+
+        for idxMesh in range(self.n_samples):
+            sample_mesh = np.vstack((X[:, idxMesh], Y[:, idxMesh])).T
+
+            univ_p_val = PCEModel.univ_basis_vals(sample_mesh)
+
+            for Outkey, ValuesDict in PCEModel.coeffs_dict.items():
+
+                pce_out_mean = np.zeros((len(sample_mesh), len(ValuesDict)))
+                pce_out_std = np.zeros((len(sample_mesh), len(ValuesDict)))
+                model_outs = np.zeros((len(sample_mesh), len(ValuesDict)))
+
+                for Inkey, InIdxValues in ValuesDict.items():
+                    idx = int(Inkey.split('_')[1]) - 1
+                    basis_deg_ind = PCEModel.basis_dict[Outkey][Inkey]
+                    clf_poly = PCEModel.clf_poly[Outkey][Inkey]
+
+                    PSI_Val = PCEModel.create_psi(basis_deg_ind, univ_p_val)
+
+                    # Perdiction with error bar
+                    y_mean, y_std = clf_poly.predict(PSI_Val, return_std=True)
+
+                    pce_out_mean[:, idx] = y_mean
+                    pce_out_std[:, idx] = y_std
+
+                    # Model evaluation
+                    model_out_dict, _ = Model.run_model_parallel(sample_mesh,
+                                                                 key_str='Valid3D')
+                    model_outs[:, idx] = model_out_dict[Outkey].T
+
+                PCE_Z[:, idxMesh] = y_mean
+                Model_Z[:, idxMesh] = model_outs[:, 0]
+
+        # ---------------- 3D plot for PCEModel -----------------------
+        fig_PCE = plt.figure()
+        ax = plt.axes(projection='3d')
+        ax.plot_surface(X, Y, PCE_Z, rstride=1, cstride=1,
+                        cmap='viridis', edgecolor='none')
+        ax.set_title('PCEModel')
+        ax.set_xlabel('$x_1$')
+        ax.set_ylabel('$x_2$')
+        ax.set_zlabel('$f(x_1,x_2)$')
+
+        plt.grid()
+        plt.show()
+
+        #  Saving the figure
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # save the figure to file
+        fig_PCE.savefig(f'./{newpath}/3DPlot_PCEModel.pdf',
+                        bbox_inches='tight')
+        plt.close(fig_PCE)
+
+        # ---------------- 3D plot for Model -----------------------
+        fig_Model = plt.figure()
+        ax = plt.axes(projection='3d')
+        ax.plot_surface(X, Y, PCE_Z, rstride=1, cstride=1,
+                        cmap='viridis', edgecolor='none')
+        ax.set_title('Model')
+        ax.set_xlabel('$x_1$')
+        ax.set_ylabel('$x_2$')
+        ax.set_zlabel('$f(x_1,x_2)$')
+
+        plt.grid()
+        plt.show()
+
+        # Save the figure
+        fig_Model.savefig(f'./{newpath}/3DPlot_Model.pdf',
+                          bbox_inches='tight')
+        plt.close(fig_Model)
+
+        return
+
+    # -------------------------------------------------------------------------
+    def compute_pce_moments(self):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moments (mean) of outpust.
+        pce_means: dict
+            The first moments (mean) of outpust.
+
+        """
+
+        MetaModel = self.MetaModel
+        outputs = self.ModelObj.Output.names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(MetaModel.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    PCA = MetaModel.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.inverse_transform(pce_mean)
+                    stds[output] = np.sqrt(np.dot(pce_var,
+                                                  PCA.components_**2))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+            # Print a report table
+            print("\n>>>>> Moments of {} <<<<<".format(output))
+            print("\nIndex  |  Mean   |  Std. deviation")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(pce_means[output],
+                                             pce_stds[output]))))
+        print('-'*40)
+
+        return pce_means, pce_stds
+
+    # -------------------------------------------------------------------------
+    def _get_sample(self, n_samples=None):
+        """
+        Generates random samples taken from the input parameter space.
+
+        Returns
+        -------
+        samples : array of shape (n_samples, n_params)
+            Generated samples.
+
+        """
+        if n_samples is None:
+            n_samples = self.n_samples
+        self.samples = self.ExpDesign.generate_samples(
+            n_samples,
+            sampling_method='random')
+        return self.samples
+
+    # -------------------------------------------------------------------------
+    def _eval_model(self, samples=None, key_str='Valid'):
+        """
+        Evaluates Forward Model for the given number of self.samples or given
+        samples.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Samples to evaluate the model at. The default is None.
+        key_str : str, optional
+            Key string pass to the model. The default is 'Valid'.
+
+        Returns
+        -------
+        model_outs : dict
+            Dictionary of results.
+
+        """
+        Model = self.ModelObj
+
+        if samples is None:
+            samples = self._get_sample()
+            self.samples = samples
+        else:
+            self.n_samples = len(samples)
+
+        model_outs, _ = Model.run_model_parallel(samples, key_str=key_str)
+
+        return model_outs
+
+    # -------------------------------------------------------------------------
+    def _plot_validation(self):
+        """
+        Plots outputs for visual comparison of metamodel outputs with that of
+        the (full) original model.
+
+        Returns
+        -------
+        None.
+
+        """
+        PCEModel = self.MetaModel
+
+        # get the samples
+        x_val = self.samples
+        y_pce_val = self.pce_out_mean
+        y_val = self.model_out_dict
+
+        # Open a pdf for the plots
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        fig = plt.figure()
+        # Fit the data(train the model)
+        for key in y_pce_val.keys():
+
+            y_pce_val_ = y_pce_val[key]
+            y_val_ = y_val[key]
+
+            regression_model = LinearRegression()
+            regression_model.fit(y_pce_val_, y_val_)
+
+            # Predict
+            x_new = np.linspace(np.min(y_pce_val_), np.max(y_val_), 100)
+            y_predicted = regression_model.predict(x_new[:, np.newaxis])
+
+            plt.scatter(y_pce_val_, y_val_, color='gold', linewidth=2)
+            plt.plot(x_new, y_predicted, color='k')
+
+            # Calculate the adjusted R_squared and RMSE
+            # the total number of explanatory variables in the model
+            # (not including the constant term)
+            length_list = []
+            for key, value in PCEModel.coeffs_dict['b_1'][key].items():
+                length_list.append(len(value))
+            n_predictors = min(length_list)
+            n_samples = x_val.shape[0]
+
+            R2 = r2_score(y_pce_val_, y_val_)
+            AdjR2 = 1 - (1 - R2) * (n_samples - 1) / \
+                (n_samples - n_predictors - 1)
+            rmse = mean_squared_error(y_pce_val_, y_val_, squared=False)
+
+            plt.annotate(f'RMSE = {rmse:.3f}\n Adjusted $R^2$ = {AdjR2:.3f}',
+                         xy=(0.05, 0.85), xycoords='axes fraction')
+
+            plt.ylabel("Original Model")
+            plt.xlabel("PCE Model")
+            plt.grid()
+            plt.show()
+
+            # save the current figure
+            plot_name = key.replace(' ', '_')
+            fig.savefig(f'./{newpath}/Model_vs_PCEModel_{plot_name}.pdf',
+                        bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+
+    # -------------------------------------------------------------------------
+    def _plot_validation_multi(self, x_values=[], x_axis="x [m]"):
+        """
+        Plots outputs for visual comparison of metamodel outputs with that of
+        the (full) multioutput original model
+
+        Parameters
+        ----------
+        x_values : list or array, optional
+            List of x values. The default is [].
+        x_axis : str, optional
+            Label of the x axis. The default is "x [m]".
+
+        Returns
+        -------
+        None.
+
+        """
+        Model = self.ModelObj
+
+        newpath = f'Outputs_PostProcessing_{self.name}/'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # List of markers and colors
+        color = cycle((['b', 'g', 'r', 'y', 'k']))
+        marker = cycle(('x', 'd', '+', 'o', '*'))
+
+        fig = plt.figure()
+        # Plot the model vs PCE model
+        for keyIdx, key in enumerate(Model.Output.names):
+
+            y_pce_val = self.pce_out_mean[key]
+            y_pce_val_std = self.pce_out_std[key]
+            y_val = self.model_out_dict[key]
+            try:
+                x = self.model_out_dict['x_values'][key]
+            except (TypeError, IndexError):
+                x = x_values
+
+            for idx in range(y_val.shape[0]):
+                Color = next(color)
+                Marker = next(marker)
+
+                plt.plot(x, y_val[idx], color=Color, marker=Marker,
+                         label='$Y_{%s}^M$'%(idx+1))
+
+                plt.plot(x, y_pce_val[idx], color=Color, marker=Marker,
+                         linestyle='--',
+                         label='$Y_{%s}^{PCE}$'%(idx+1))
+                plt.fill_between(x, y_pce_val[idx]-1.96*y_pce_val_std[idx],
+                                 y_pce_val[idx]+1.96*y_pce_val_std[idx],
+                                 color=Color, alpha=0.15)
+
+            # Calculate the RMSE
+            rmse = mean_squared_error(y_pce_val, y_val, squared=False)
+            R2 = r2_score(y_pce_val[idx].reshape(-1, 1),
+                          y_val[idx].reshape(-1, 1))
+
+            plt.annotate(f'RMSE = {rmse:.3f}\n $R^2$ = {R2:.3f}',
+                         xy=(0.85, 0.1), xycoords='axes fraction')
+
+            plt.ylabel(key)
+            plt.xlabel(x_axis)
+            plt.legend(loc='best')
+            plt.grid()
+
+            # save the current figure
+            plot_name = key.replace(' ', '_')
+            fig.savefig(f'./{newpath}/Model_vs_PCEModel_{plot_name}.pdf',
+                        bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+
+        # Zip the subdirectories
+        Model.zip_subdirs(f'{Model.name}valid', f'{Model.name}valid_')
diff --git a/examples/model-comparison/bayesvalidrox/pylink/__init__.py b/examples/model-comparison/bayesvalidrox/pylink/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..4bd81739faf43956324b30f6d8e5365b29d55677
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/pylink/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from .pylink import PyLinkForwardModel
+
+__all__ = [
+    "PyLinkForwardModel"
+    ]
diff --git a/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5b7c1b3926506fb279b856f55ca6120df31b8888
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1947ad354716d0293953761f0d35193f706cedc1
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0bbb522855ad250ad55bca46123c0f5023076291
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6ae7c14b35b60388e38fcbd3af64d04771a947c
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0f0f850911d36dce30d3a9e6f59478e5216044c8
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..746c82eb52be2e437c61bd201433f9d38b8ab177
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/pylink/__pycache__/pylink.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/pylink/pylink.py b/examples/model-comparison/bayesvalidrox/pylink/pylink.py
new file mode 100644
index 0000000000000000000000000000000000000000..227a51ab38cd834e7e85f6193d83563c7ed3437a
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/pylink/pylink.py
@@ -0,0 +1,803 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Calls to the model and evaluations
+"""
+
+from dataclasses import dataclass
+
+import os
+import shutil
+import h5py
+import numpy as np
+import time
+import zipfile
+import pandas as pd
+import multiprocessing
+from functools import partial
+import tqdm
+
+#from multiprocessing import get_context
+from multiprocess import get_context
+
+
+
+def within_range(out, minout, maxout):
+    """
+    Checks if all the values in out lie between minout and maxout
+
+    Parameters
+    ----------
+    out : array or list
+        Data to check against range
+    minout : int
+        Lower bound of the range
+    maxout : int
+        Upper bound of the range
+
+    Returns
+    -------
+    inside : bool
+        True if all values in out are in the specified range
+
+    """
+    try:
+        out = np.array(out)
+    except:
+        raise AttributeError('The given values should be a 1D array, but are not')
+    if out.ndim != 1:
+            raise AttributeError('The given values should be a 1D array, but are not')
+        
+    if minout > maxout:
+        raise ValueError('The lower and upper bounds do not form a valid range, they might be switched')
+    
+    inside = False
+    if (out > minout).all() and (out < maxout).all():
+        inside = True
+    return inside
+
+
+class PyLinkForwardModel(object):
+    """
+    A forward model binder
+
+    This calss serves as a code wrapper. This wrapper allows the execution of
+    a third-party software/solver within the scope of BayesValidRox.
+
+    Attributes
+    ----------
+    link_type : str
+        The type of the wrapper. The default is `'pylink'`. This runs the
+        third-party software or an executable using a shell command with given
+        input files.
+        Second option is `'function'` which assumed that model can be run using
+        a function written separately in a Python script.
+    name : str
+        Name of the model.
+    py_file : str
+        Python file name without `.py` extension to be run for the `'function'`
+        wrapper. Note that the name of the python file and that of the function
+        must be simillar. This function must recieve the parameters in an array
+        of shape `(n_samples, n_params)` and returns a dictionary with the
+        x_values and output arrays for given output names.
+    func_args : dict
+        Additional arguments for the python file. The default is `{}`.
+    shell_command : str
+        Shell command to be executed for the `'pylink'` wrapper.
+    input_file : str or list
+        The input file to be passed to the `'pylink'` wrapper.
+    input_template : str or list
+        A template input file to be passed to the `'pylink'` wrapper. This file
+        must be a copy of `input_file` with `<Xi>` place holder for the input
+        parameters defined using `inputs` class, with i being the number of
+        parameter. The file name ending should include `.tpl` before the actual
+        extension of the input file, for example, `params.tpl.input`.
+    aux_file : str or list
+        The list of auxiliary files needed for the `'pylink'` wrapper.
+    exe_path : str
+        Execution path if you wish to run the model for the `'pylink'` wrapper
+        in another directory. The default is `None`, which corresponds to the
+        currecnt working directory.
+    output_file_names : list of str
+        List of the name of the model output text files for the `'pylink'`
+        wrapper.
+    output_names : list of str
+        List of the model outputs to be used for the analysis.
+    output_parser : str
+        Name of the model parser file (without `.py` extension) that recieves
+        the `output_file_names` and returns a 2d-array with the first row being
+        the x_values, e.g. x coordinates or time and the rest of raws pass the
+        simulation output for each model output defined in `output_names`. Note
+        that again here the name of the file and that of the function must be
+        the same.
+    multi_process: bool
+        Whether the model runs to be executed in parallel for the `'pylink'`
+        wrapper. The default is `True`.
+    n_cpus: int
+        The number of cpus to be used for the parallel model execution for the
+        `'pylink'` wrapper. The default is `None`, which corresponds to all
+        available cpus.
+    meas_file : str
+        The name of the measurement text-based file. This file must contain
+        x_values as the first column and one column for each model output. The
+        default is `None`. Only needed for the Bayesian Inference.
+    meas_file_valid : str
+        The name of the measurement text-based file for the validation. The
+        default is `None`. Only needed for the validation with Bayesian
+        Inference.
+    mc_ref_file : str
+        The name of the text file for the Monte-Carlo reference (mean and
+        standard deviation) values. It must contain `x_values` as the first
+        column, `mean` as the second column and `std` as the third. It can be
+        used to compare the estimated moments using meta-model in the post-
+        processing step. This is only available for one output.
+    obs_dict : dict
+        A dictionary containing the measurement text-based file. It must
+        contain `x_values` as the first item and one item for each model output
+        . The default is `{}`. Only needed for the Bayesian Inference.
+    obs_dict_valid : dict
+        A dictionary containing the validation measurement text-based file. It
+        must contain `x_values` as the first item and one item for each model
+        output. The default is `{}`.
+    mc_ref_dict : dict
+        A dictionary containing the Monte-Carlo reference (mean and standard
+        deviation) values. It must contain `x_values` as the first item and
+        `mean` as the second item and `std` as the third. The default is `{}`.
+        This is only available for one output.
+    """
+
+    # Nested class
+    @dataclass
+    class OutputData(object):
+        parser: str = ""
+        names: list = None
+        file_names: list = None
+
+    def __init__(self, link_type='pylink', name=None, py_file=None,
+                 func_args={}, shell_command='', input_file=None,
+                 input_template=None, aux_file=None, exe_path='',
+                 output_file_names=[], output_names=[], output_parser='',
+                 multi_process=True, n_cpus=None, meas_file=None,
+                 meas_file_valid=None, mc_ref_file=None, obs_dict={},
+                 obs_dict_valid={}, mc_ref_dict={}):
+        self.link_type = link_type
+        self.name = name
+        self.shell_command = shell_command
+        self.py_file = py_file
+        self.func_args = func_args
+        self.input_file = input_file
+        self.input_template = input_template
+        self.aux_file = aux_file
+        self.exe_path = exe_path
+        self.multi_process = multi_process
+        self.n_cpus = n_cpus
+        self.Output = self.OutputData(
+            parser=output_parser,
+            names=output_names,
+            file_names=output_file_names,
+        )
+        self.n_outputs = len(self.Output.names)
+        self.meas_file = meas_file
+        self.meas_file_valid = meas_file_valid
+        self.mc_ref_file = mc_ref_file
+        self.observations = obs_dict
+        self.observations_valid = obs_dict_valid
+        self.mc_reference = mc_ref_dict
+
+    # -------------------------------------------------------------------------
+    def read_observation(self, case='calib'):
+        """
+        Reads/prepare the observation/measurement data for
+        calibration.
+        
+        Parameters
+        ----------
+        case : str
+            The type of observation to read in. Can be either 'calib',
+            'valid' or 'mc_ref'
+
+        Returns
+        -------
+        DataFrame
+            A dataframe with the calibration data.
+
+        """
+        # TOOD: check that what is read in/transformed matches the expected form of data/reference
+        if case.lower() == 'calib':
+            if isinstance(self.observations, dict) and bool(self.observations):
+                self.observations = pd.DataFrame.from_dict(self.observations)
+            elif self.meas_file is not None:
+                file_path = os.path.join(os.getcwd(), self.meas_file)
+                self.observations = pd.read_csv(file_path, delimiter=',')
+            elif isinstance(self.observations, pd.DataFrame):
+                self.observations = self.observations
+            else:
+                raise Exception("Please provide the observation data as a "
+                                "dictionary via observations attribute or pass"
+                                " the csv-file path to MeasurementFile "
+                                "attribute")
+            # Compute the number of observation
+            self.n_obs = self.observations[self.Output.names].notnull().sum().values.sum()
+            return self.observations
+            
+        elif case.lower() == 'valid':
+            if isinstance(self.observations_valid, dict) and \
+              bool(self.observations_valid):
+                self.observations_valid = pd.DataFrame.from_dict(self.observations_valid)
+            elif self.meas_file_valid is not None:
+                file_path = os.path.join(os.getcwd(), self.meas_file_valid)
+                self.observations_valid = pd.read_csv(file_path, delimiter=',')
+            elif isinstance(self.observations_valid, pd.DataFrame):
+                self.observations_valid = self.observations_valid
+            else:
+                raise Exception("Please provide the observation data as a "
+                                "dictionary via observations attribute or pass"
+                                " the csv-file path to MeasurementFile "
+                                "attribute")
+            # Compute the number of observation
+            self.n_obs_valid = self.observations_valid[self.Output.names].notnull().sum().values.sum()
+            return self.observations_valid
+                
+        elif case.lower() == 'mc_ref':
+            if self.mc_ref_file is None and \
+               isinstance(self.mc_reference, pd.DataFrame):
+                return self.mc_reference
+            elif isinstance(self.mc_reference, dict) and bool(self.mc_reference):
+                self.mc_reference = pd.DataFrame.from_dict(self.mc_reference)
+            elif self.mc_ref_file is not None:
+                file_path = os.path.join(os.getcwd(), self.mc_ref_file)
+                self.mc_reference = pd.read_csv(file_path, delimiter=',')
+            else:
+                self.mc_reference = None
+            return self.mc_reference
+
+
+    # -------------------------------------------------------------------------
+    def read_output(self):
+        """
+        Reads the the parser output file and returns it as an
+         executable function. It is required when the models returns the
+         simulation outputs in csv files.
+
+        Returns
+        -------
+        Output : func
+            Output parser function.
+
+        """
+        output_func_name = self.Output.parser
+
+        output_func = getattr(__import__(output_func_name), output_func_name)
+
+        file_names = []
+        for File in self.Output.file_names:
+            file_names.append(os.path.join(self.exe_path, File))
+        try:
+            output = output_func(self.name, file_names)
+        except TypeError:
+            output = output_func(file_names)
+        return output
+
+    # -------------------------------------------------------------------------
+    def update_input_params(self, new_input_file, param_set):
+        """
+        Finds this pattern with <X1> in the new_input_file and replace it with
+         the new value from the array param_sets.
+
+        Parameters
+        ----------
+        new_input_file : list
+            List of the input files with the adapted names.
+        param_set : array of shape (n_params)
+            Parameter set.
+
+        Returns
+        -------
+        None.
+
+        """
+        NofPa = param_set.shape[0]
+        text_to_search_list = [f'<X{i+1}>' for i in range(NofPa)]
+
+        for filename in new_input_file:
+            # Read in the file
+            with open(filename, 'r') as file:
+                filedata = file.read()
+
+            # Replace the target string
+            for text_to_search, params in zip(text_to_search_list, param_set):
+                filedata = filedata.replace(text_to_search, f'{params:0.4e}')
+
+            # Write the file out again
+            with open(filename, 'w') as file:
+                file.write(filedata)
+
+    # -------------------------------------------------------------------------
+    def run_command(self, command, output_file_names):
+        """
+        Runs the execution command given by the user to run the given model.
+        It checks if the output files have been generated. If yes, the jobe is
+        done and it extracts and returns the requested output(s). Otherwise,
+        it executes the command again.
+
+        Parameters
+        ----------
+        command : str
+            The shell command to be executed.
+        output_file_names : list
+            Name of the output file names.
+
+        Returns
+        -------
+        simulation_outputs : array of shape (n_obs, n_outputs)
+            Simulation outputs.
+
+        """
+
+        # Check if simulation is finished
+        while True:
+            time.sleep(3)
+            files = os.listdir(".")
+            if all(elem in files for elem in output_file_names):
+                break
+            else:
+                # Run command
+                Process = os.system(f'./../{command}')
+                if Process != 0:
+                    print('\nMessage 1:')
+                    print(f'\tIf the value of \'{Process}\' is a non-zero value'
+                          ', then compilation problems occur \n' % Process)          
+        os.chdir("..")
+
+        # Read the output
+        simulation_outputs = self.read_output()
+
+        return simulation_outputs
+
+    # -------------------------------------------------------------------------
+    def run_forwardmodel(self, xx):
+        """
+        This function creates subdirectory for the current run and copies the
+        necessary files to this directory and renames them. Next, it executes
+        the given command.
+
+        Parameters
+        ----------
+        xx : tuple
+            A tuple including parameter set, simulation number and key string.
+
+        Returns
+        -------
+        output : array of shape (n_outputs+1, n_obs)
+            An array passed by the output paraser containing the x_values as
+            the first row and the simulations results stored in the the rest of
+            the array.
+
+        """
+        c_points, run_no, key_str = xx
+
+        # Handle if only one imput file is provided
+        if not isinstance(self.input_template, list):
+            self.input_template = [self.input_template]
+        if not isinstance(self.input_file, list):
+            self.input_file = [self.input_file]
+
+        new_input_file = []
+        # Loop over the InputTemplates:
+        for in_temp in self.input_template:
+            if '/' in in_temp:
+                in_temp = in_temp.split('/')[-1]
+            new_input_file.append(in_temp.split('.tpl')[0] + key_str +
+                                  f"_{run_no+1}" + in_temp.split('.tpl')[1])
+
+        # Create directories
+        newpath = self.name + key_str + f'_{run_no+1}'
+        if not os.path.exists(newpath):
+            os.makedirs(newpath)
+
+        # Copy the necessary files to the directories
+        print(self.input_template)
+        for in_temp in self.input_template:
+            # Input file(s) of the model
+            shutil.copy2(in_temp, newpath)
+        # Auxiliary file
+        if self.aux_file is not None:
+            shutil.copy2(self.aux_file, newpath)  # Auxiliary file
+
+        # Rename the Inputfile and/or auxiliary file
+        os.chdir(newpath)
+        for input_tem, input_file in zip(self.input_template, new_input_file):
+            if '/' in input_tem:
+                input_tem = input_tem.split('/')[-1]
+            os.rename(input_tem, input_file)
+
+        # Update the parametrs in Input file
+        self.update_input_params(new_input_file, c_points)
+
+        # Update the user defined command and the execution path
+        try:
+            new_command = self.shell_command.replace(self.input_file[0],
+                                                     new_input_file[0])
+            new_command = new_command.replace(self.input_file[1],
+                                              new_input_file[1])
+        except:
+            new_command = self.shell_command.replace(self.input_file[0],
+                                                     new_input_file[0])
+        # Set the exe path if not provided
+        if not bool(self.exe_path):
+            self.exe_path = os.getcwd()
+
+        # Run the model
+        print(new_command)
+        output = self.run_command(new_command, self.Output.file_names)
+
+        return output
+
+    # -------------------------------------------------------------------------
+    def run_model_parallel(self, c_points, prevRun_No=0, key_str='',
+                           mp=True, verbose=True):
+        """
+        Runs model simulations. If mp is true (default), then the simulations
+         are started in parallel.
+
+        Parameters
+        ----------
+        c_points : array of shape (n_samples, n_params)
+            Collocation points (training set).
+        prevRun_No : int, optional
+            Previous run number, in case the sequential design is selected.
+            The default is `0`.
+        key_str : str, optional
+            A descriptive string for validation runs. The default is `''`.
+        mp : bool, optional
+            Multiprocessing. The default is `True`.
+        verbose: bool, optional
+            Verbosity. The default is `True`.
+
+        Returns
+        -------
+        all_outputs : dict
+            A dictionary with x values (time step or point id) and all outputs.
+            Each key contains an array of the shape `(n_samples, n_obs)`.
+        new_c_points : array
+            Updated collocation points (training set). If a simulation does not
+            executed successfully, the parameter set is removed.
+
+        """
+
+        # Initilization
+        n_c_points = len(c_points)
+        all_outputs = {}
+        
+        # If the link type is UM-Bridge, then no parallel needs to be started from here
+        if self.link_type.lower() == 'umbridge':
+            import umbridge 
+            if not hasattr(self, 'x_values'):
+                raise AttributeError('For model type `umbridge` the attribute `x_values` needs to be set for the model!')
+            # Init model
+            #model = umbridge.HTTPModel('http://localhost:4242', 'forward')
+            self.model = umbridge.HTTPModel(self.host, 'forward') # TODO: is this always forward?
+            Function = self.uMBridge_model
+
+        # Extract the function
+        if self.link_type.lower() == 'function':
+            # Prepare the function
+            Function = getattr(__import__(self.py_file), self.py_file)
+        # ---------------------------------------------------------------
+        # -------------- Multiprocessing with Pool Class ----------------
+        # ---------------------------------------------------------------
+        # Start a pool with the number of CPUs
+        if self.n_cpus is None:
+            n_cpus = multiprocessing.cpu_count()
+        else:
+            n_cpus = self.n_cpus
+
+        # Run forward model
+        if n_c_points == 1 or not mp:
+            if n_c_points== 1:
+                if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                    group_results = Function(c_points, **self.func_args)
+                else:
+                    group_results = self.run_forwardmodel(
+                        (c_points[0], prevRun_No, key_str)
+                        )
+            else:
+                for i in range(c_points.shape[0]):
+                    if i == 0:
+                        if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                            group_results = Function(np.array([c_points[0]]), **self.func_args)
+                        else:
+                            group_results = self.run_forwardmodel(
+                                (c_points[0], prevRun_No, key_str)
+                                )
+                        for key in group_results:
+                            if key != 'x_values':
+                                group_results[key] = [group_results[key]]
+                    else: 
+                        if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                            res = Function(np.array([c_points[i]]), **self.func_args)
+                        else:
+                            res = self.run_forwardmodel(
+                                (c_points[i], prevRun_No, key_str)
+                                )
+                        for key in res:
+                            if key != 'x_values':
+                                group_results[key].append(res[key])
+        
+                for key in group_results:
+                    if key != 'x_values':
+                        group_results[key]= np.array(group_results[key])
+
+        elif self.multi_process or mp:
+            with get_context('spawn').Pool(n_cpus) as p:
+            #with multiprocessing.Pool(n_cpus) as p:
+                
+                if self.link_type.lower() == 'function' or self.link_type.lower() == 'umbridge':
+                    imap_var = p.imap(partial(Function, **self.func_args),
+                                      c_points[:, np.newaxis])
+                else:
+                    args = zip(c_points,
+                               [prevRun_No+i for i in range(n_c_points)],
+                               [key_str]*n_c_points)
+                    imap_var = p.imap(self.run_forwardmodel, args)
+
+                if verbose:
+                    desc = f'Running forward model {key_str}'
+                    group_results = list(tqdm.tqdm(imap_var, total=n_c_points,
+                                                   desc=desc))
+                else:
+                    group_results = list(imap_var)
+
+        # Check for NaN
+        for var_i, var in enumerate(self.Output.names):
+            # If results are given as one dictionary
+            if isinstance(group_results, dict):
+                Outputs = np.asarray(group_results[var])
+            # If results are given as list of dictionaries
+            elif isinstance(group_results, list):
+                Outputs = np.asarray([item[var] for item in group_results],
+                                     dtype=np.float64)
+            NaN_idx = np.unique(np.argwhere(np.isnan(Outputs))[:, 0])
+            new_c_points = np.delete(c_points, NaN_idx, axis=0)
+            all_outputs[var] = np.atleast_2d(
+                np.delete(Outputs, NaN_idx, axis=0)
+                )
+
+        # Print the collocation points whose simulations crashed
+        if len(NaN_idx) != 0:
+            print('\n')
+            print('*'*20)
+            print("\nThe following parameter sets have been removed:\n",
+                  c_points[NaN_idx])
+            print("\n")
+            print('*'*20)
+
+        # Save time steps or x-values
+        if isinstance(group_results, dict):
+            all_outputs["x_values"] = group_results["x_values"]
+        elif any(isinstance(i, dict) for i in group_results):
+            all_outputs["x_values"] = group_results[0]["x_values"]
+
+        # Store simulations in a hdf5 file
+        self._store_simulations(
+            c_points, all_outputs, NaN_idx, key_str, prevRun_No
+            )
+
+        return all_outputs, new_c_points
+    
+    def uMBridge_model(self, params):
+        """
+        Function that calls a UMBridge model and transforms its output into the 
+        shape expected for the surrogate.
+    
+        Parameters
+        ----------
+        params : 2d np.array, shape (#samples, #params)
+            The parameter values for which the model is run.
+    
+        Returns
+        -------
+        dict
+            The transformed model outputs.
+    
+        """
+        # Run the model
+        #out = np.array(model(np.ndarray.tolist(params), {'level':0}))
+        out = np.array(self.model(np.ndarray.tolist(params), self.modelparams))
+        
+        # Sort into dict
+        out_dict = {}
+        cnt = 0
+        for key in self.Output.names:
+        #    # If needed resort into single-value outputs
+        #    if self.output_type == 'single-valued':
+        #        if out.shape[1]>1:  # TODO: this doesn't fully seem correct??
+        #            for i in range(out[:,key]): # TODO: this doesn't fully seem correct??
+        #                new_key = key+str(i)
+        #                if new_key not in self.Output.names:
+        #                    self.Output.names.append(new_key)
+        #                    if i == 0:
+        #                        self.Ouptut.names.remove(key)
+        #                out_dict[new_key] = out[:,cnt,i] # TODO: not sure about this, need to test
+        #        else: 
+        #            out_dict[key] = out[:,cnt]
+        #            
+        #        
+        #    else:
+            out_dict[key] = out[:,cnt]
+            cnt += 1
+        
+            
+        ## TODO: how to deal with the x-values?
+        #if self.output_type == 'single-valued':
+        #    out_dict['x_values'] = [0]
+        #else:
+        #    out_dict['x_values'] = np.arange(0,out[:,0].shape[0],1)
+        out_dict['x_values'] = self.x_values
+        
+        #return {'T1':out[:,0], 'T2':out[:,1], 'H1':out[:,2], 'H2':out[:,3], 
+       #         'x_values':[0]}
+        return out_dict
+
+    # -------------------------------------------------------------------------
+    def _store_simulations(self, c_points, all_outputs, NaN_idx, key_str,
+                           prevRun_No):
+        """
+        
+
+        Parameters
+        ----------
+        c_points : TYPE
+            DESCRIPTION.
+        all_outputs : TYPE
+            DESCRIPTION.
+        NaN_idx : TYPE
+            DESCRIPTION.
+        key_str : TYPE
+            DESCRIPTION.
+        prevRun_No : TYPE
+            DESCRIPTION.
+
+        Returns
+        -------
+        None.
+
+        """
+
+        # Create hdf5 metadata
+        if key_str == '':
+            hdf5file = f'ExpDesign_{self.name}.hdf5'
+        else:
+            hdf5file = f'ValidSet_{self.name}.hdf5'
+        hdf5_exist = os.path.exists(hdf5file)
+        file = h5py.File(hdf5file, 'a')
+
+        # ---------- Save time steps or x-values ----------
+        if not hdf5_exist:
+            if type(all_outputs["x_values"]) is dict:
+                grp_x_values = file.create_group("x_values/")
+                for varIdx, var in enumerate(self.Output.names):
+                    grp_x_values.create_dataset(
+                        var, data=all_outputs["x_values"][var]
+                        )
+            else:
+                file.create_dataset("x_values", data=all_outputs["x_values"])
+
+        # ---------- Save outputs ----------
+        for varIdx, var in enumerate(self.Output.names):
+            if not hdf5_exist:
+                grpY = file.create_group("EDY/"+var)
+            else:
+                grpY = file.get("EDY/"+var)
+
+            if prevRun_No == 0 and key_str == '':
+                grpY.create_dataset(f'init_{key_str}', data=all_outputs[var])
+            else:
+                try:
+                    oldEDY = np.array(file[f'EDY/{var}/adaptive_{key_str}'])
+                    del file[f'EDY/{var}/adaptive_{key_str}']
+                    data = np.vstack((oldEDY, all_outputs[var]))
+                except KeyError:
+                    data = all_outputs[var]
+                grpY.create_dataset('adaptive_'+key_str, data=data)
+
+            if prevRun_No == 0 and key_str == '':
+                grpY.create_dataset(f"New_init_{key_str}",
+                                    data=all_outputs[var])
+            else:
+                try:
+                    name = f'EDY/{var}/New_adaptive_{key_str}'
+                    oldEDY = np.array(file[name])
+                    del file[f'EDY/{var}/New_adaptive_{key_str}']
+                    data = np.vstack((oldEDY, all_outputs[var]))
+                except KeyError:
+                    data = all_outputs[var]
+                grpY.create_dataset(f'New_adaptive_{key_str}', data=data)
+
+        # ---------- Save CollocationPoints ----------
+        new_c_points = np.delete(c_points, NaN_idx, axis=0)
+        grpX = file.create_group("EDX") if not hdf5_exist else file.get("EDX")
+        if prevRun_No == 0 and key_str == '':
+            grpX.create_dataset("init_"+key_str, data=c_points)
+            if len(NaN_idx) != 0:
+                grpX.create_dataset("New_init_"+key_str, data=new_c_points)
+
+        else:
+            try:
+                name = f'EDX/adaptive_{key_str}'
+                oldCollocationPoints = np.array(file[name])
+                del file[f'EDX/adaptive_{key_str}']
+                data = np.vstack((oldCollocationPoints, new_c_points))
+            except KeyError:
+                data = new_c_points
+            grpX.create_dataset('adaptive_'+key_str, data=data)
+
+            if len(NaN_idx) != 0:
+                try:
+                    name = f'EDX/New_adaptive_{key_str}'
+                    oldCollocationPoints = np.array(file[name])
+                    del file[f'EDX/New_adaptive_{key_str}']
+                    data = np.vstack((oldCollocationPoints, new_c_points))
+                except KeyError:
+                    data = new_c_points
+                grpX.create_dataset('New_adaptive_'+key_str, data=data)
+
+        # Close h5py file
+        file.close()
+
+    # -------------------------------------------------------------------------
+    def zip_subdirs(self, dir_name, key):
+        """
+        Zips all the files containing the key(word).
+
+        Parameters
+        ----------
+        dir_name : str
+            Directory name.
+        key : str
+            Keyword to search for.
+
+        Returns
+        -------
+        None.
+
+        """
+        # setup file paths variable
+        dir_list = []
+        file_paths = []
+
+        # Read all directory, subdirectories and file lists
+        dir_path = os.getcwd()
+
+        for root, directories, files in os.walk(dir_path):
+            for directory in directories:
+                # Create the full filepath by using os module.
+                if key in directory:
+                    folderPath = os.path.join(dir_path, directory)
+                    dir_list.append(folderPath)
+
+        # Loop over the identified directories to store the file paths
+        for direct_name in dir_list:
+            for root, directories, files in os.walk(direct_name):
+                for filename in files:
+                    # Create the full filepath by using os module.
+                    filePath = os.path.join(root, filename)
+                    file_paths.append('.'+filePath.split(dir_path)[1])
+
+        # writing files to a zipfile
+        if len(file_paths) != 0:
+            zip_file = zipfile.ZipFile(dir_name+'.zip', 'w')
+            with zip_file:
+                # writing each file one by one
+                for file in file_paths:
+                    zip_file.write(file)
+
+            file_paths = [path for path in os.listdir('.') if key in path]
+
+            for path in file_paths:
+                shutil.rmtree(path)
+
+            print("\n")
+            print(f'{dir_name}.zip has been created successfully!\n')
+
+        return
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__init__.py b/examples/model-comparison/bayesvalidrox/surrogate_models/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..70bfb20f570464c2907a0a4128f4ed99b6c13736
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/__init__.py
@@ -0,0 +1,7 @@
+# -*- coding: utf-8 -*-
+
+from .surrogate_models import MetaModel
+
+__all__ = [
+    "MetaModel"
+    ]
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8c10c82287a57ba1e3b4dd428962e57cdfbc5c58
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4b73e63a3096fbc9afc41bae35a3fcc1d7851166
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f1a3fcc2eed66172304cd27ab5fe111ca0198bf5
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/__init__.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/adaptPlot.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/adaptPlot.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2854217e56fecb2456011a91a984951fed9cbcbb
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/adaptPlot.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ffab8b4f67e52a3128aa8740301f958a0d72c502
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5d2b8aa7d552e4f4afc87a25caf36ab034f31486
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..82737a42dd7351d06b703b3da838031ba95979da
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/apoly_construction.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..92d0cc0e7a0a07123fdfbc2c777d1b9281a43344
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..56cfc2006febc94fdf721712929918499cd46491
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..063355b16a397fb5fd89d38daa0d3ca5a8506766
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/bayes_linear.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/engine.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/engine.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0a98675fd4b4b1172c75b81b88112c0e39880261
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/engine.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..cbfe4d97e8e83ebc276e45ba6e84f514784f1d0b
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1d789b4515f89e02f006dafd2c9d85b8e7bea110
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..27878248cbdaa2abf0af9d51d061aa6e2db86f43
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/eval_rec_rule.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fc01f0f01b8bc70d438a3317b87d304883456f15
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a392bbec968c4728540bfbd2470d6cd4efb67faa
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e7e0226dc1e28c09b9cd09a610599007b2267e3c
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs_.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs_.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..076580dd7fa9e11559ef202903d44e00e25b8a26
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exp_designs_.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..af7431ce432969f095d1c07f429b8129cb5a2def
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9a435919a7163073324fbaf96093a1aeb0b6387b
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..98964152a4ea29f85f061ea6ec7daa3df7487230
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/exploration.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..16a383b994853cdb226f7b7fb291cdbef789e1f7
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7ffc6990e1d99e9ad0a3c41c689e313d7f680d8e
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c3f24cf9cb753b59f9e226f828a7c3598ac65a9a
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/glexindex.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/input_space.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/input_space.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..2ff32cef7757bc4a20197f768e3cbfe819f9d428
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/input_space.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ed2b0f6f101965fb42fe059ec79e8084a4b3a9bf
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..615d2fe8b67cb014059c4fab5f06344ca0878adf
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b0c91ff3182adb1245aa7f20656e8e94336a438c
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/inputs.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/loss_function.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/loss_function.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a1e26394365e683dbec6da95fbe223ebcee10ef4
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/loss_function.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7ebbfad9c34b1f9e6c819ea7cf7852af65591444
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b2b3f8f40181637b13ee1156ffa8a03c1ffce8b3
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bd6eb8fcd459fd95ff1b85514f996344b6e4880c
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/meta_model_engine.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..543416416ef052c2402c2e9a97976dc4aab22866
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9e909177d8f6cb2a1722e871da4bc92ebfea493c
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9f1ec9479869fec42f363e7f46e07dba2f1c6be5
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/orthogonal_matching_pursuit.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..97ed55acc1e800a138ddf489ccea709a8b28f634
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4ab80e19a7070efd8d180d21c3b492cc333d73dd
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0843cdf8bd820d9cccfdcde3a1193f3f416ccc10
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_ard.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..30079dce4bc04802324f720381b872c1a2f64018
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..9f089d06b16a5698dee5f706d007dfa13916a42c
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..7a3bbc05003eab405d52a934e83053ae50080d25
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/reg_fast_laplace.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/sequential_design.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/sequential_design.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..af35ff42f1ea1e8ee19d476f7b51c19199513cde
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/sequential_design.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-310.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-310.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b856dfef2c2658af8ecc6b1ab85b99c499a39705
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-310.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..569685a009aea33d28959c005328402624029ef7
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-39.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-39.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..f44a774b4165e6ff769d8db2f2c13c4dd0cbe8b3
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/surrogate_models.cpython-39.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/wrapper.cpython-311.pyc b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/wrapper.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a6d61c979e164a0fd590b3f925c2d1ac68adb4fc
Binary files /dev/null and b/examples/model-comparison/bayesvalidrox/surrogate_models/__pycache__/wrapper.cpython-311.pyc differ
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/adaptPlot.py b/examples/model-comparison/bayesvalidrox/surrogate_models/adaptPlot.py
new file mode 100644
index 0000000000000000000000000000000000000000..102f0373c1086ba4420ada2fb2fc723b78bbd53f
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/adaptPlot.py
@@ -0,0 +1,109 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Thu Aug 13 13:46:24 2020
+
+@author: farid
+"""
+import os
+from sklearn.metrics import mean_squared_error, r2_score
+from itertools import cycle
+from matplotlib.backends.backend_pdf import PdfPages
+import matplotlib.pyplot as plt
+
+
+def adaptPlot(PCEModel, Y_Val, Y_PC_Val, Y_PC_Val_std, x_values=[],
+              plotED=False, SaveFig=True):
+
+    NrofSamples = PCEModel.ExpDesign.n_new_samples
+    initNSamples = PCEModel.ExpDesign.n_init_samples
+    itrNr = 1 + (PCEModel.ExpDesign.X.shape[0] - initNSamples)//NrofSamples
+
+    oldEDY = PCEModel.ExpDesign.Y
+
+    if SaveFig:
+        newpath = 'adaptivePlots'
+        os.makedirs(newpath, exist_ok=True)
+
+        # create a PdfPages object
+        pdf = PdfPages(f'./{newpath}/Model_vs_PCEModel_itr_{itrNr}.pdf')
+
+    # List of markers and colors
+    color = cycle((['b', 'g', 'r', 'y', 'k']))
+    marker = cycle(('x', 'd', '+', 'o', '*'))
+
+    OutNames = list(Y_Val.keys())
+    x_axis = 'Time [s]'
+
+    if len(OutNames) == 1:
+        OutNames.insert(0, x_axis)
+    try:
+        x_values = Y_Val['x_values']
+    except KeyError:
+        x_values = x_values
+
+    fig = plt.figure(figsize=(24, 16))
+
+    # Plot the model vs PCE model
+    for keyIdx, key in enumerate(PCEModel.ModelObj.Output.names):
+        Y_PC_Val_ = Y_PC_Val[key]
+        Y_PC_Val_std_ = Y_PC_Val_std[key]
+        Y_Val_ = Y_Val[key]
+        if Y_Val_.ndim == 1:
+            Y_Val_ = Y_Val_.reshape(1, -1)
+        old_EDY = oldEDY[key]
+        if isinstance(x_values, dict):
+            x = x_values[key]
+        else:
+            x = x_values
+
+        for idx, y in enumerate(Y_Val_):
+            Color = next(color)
+            Marker = next(marker)
+
+            plt.plot(
+                x, y, color=Color, marker=Marker,
+                lw=2.0, label='$Y_{%s}^{M}$'%(idx+itrNr)
+                )
+
+            plt.plot(
+                x, Y_PC_Val_[idx], color=Color, marker=Marker,
+                lw=2.0, linestyle='--', label='$Y_{%s}^{PCE}$'%(idx+itrNr)
+                )
+            plt.fill_between(
+                x, Y_PC_Val_[idx]-1.96*Y_PC_Val_std_[idx],
+                Y_PC_Val_[idx]+1.96*Y_PC_Val_std_[idx], color=Color,
+                alpha=0.15
+                )
+
+            if plotED:
+                for output in old_EDY:
+                    plt.plot(x, output, color='grey', alpha=0.1)
+
+        # Calculate the RMSE
+        RMSE = mean_squared_error(Y_PC_Val_, Y_Val_, squared=False)
+        R2 = r2_score(Y_PC_Val_.reshape(-1, 1), Y_Val_.reshape(-1, 1))
+
+        plt.ylabel(key)
+        plt.xlabel(x_axis)
+        plt.title(key)
+
+        ax = fig.axes[0]
+        ax.legend(loc='best', frameon=True)
+        fig.canvas.draw()
+        ax.text(0.65, 0.85,
+                f'RMSE = {round(RMSE, 3)}\n$R^2$ = {round(R2, 3)}',
+                transform=ax.transAxes, color='black',
+                bbox=dict(facecolor='none',
+                          edgecolor='black',
+                          boxstyle='round,pad=1')
+                )
+        plt.grid()
+
+        if SaveFig:
+            # save the current figure
+            pdf.savefig(fig, bbox_inches='tight')
+
+            # Destroy the current plot
+            plt.clf()
+    pdf.close()
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/apoly_construction.py b/examples/model-comparison/bayesvalidrox/surrogate_models/apoly_construction.py
new file mode 100644
index 0000000000000000000000000000000000000000..40830fe8aaa94248df4828c0c49bd4d23e755abd
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/apoly_construction.py
@@ -0,0 +1,124 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import numpy as np
+
+
+def apoly_construction(Data, degree):
+    """
+    Construction of Data-driven Orthonormal Polynomial Basis
+    Author: Dr.-Ing. habil. Sergey Oladyshkin
+    Department of Stochastic Simulation and Safety Research for Hydrosystems
+    Institute for Modelling Hydraulic and Environmental Systems
+    Universitaet Stuttgart, Pfaffenwaldring 5a, 70569 Stuttgart
+    E-mail: Sergey.Oladyshkin@iws.uni-stuttgart.de
+    http://www.iws-ls3.uni-stuttgart.de
+    The current script is based on definition of arbitrary polynomial chaos
+    expansion (aPC), which is presented in the following manuscript:
+    Oladyshkin, S. and W. Nowak. Data-driven uncertainty quantification using
+    the arbitrary polynomial chaos expansion. Reliability Engineering & System
+    Safety, Elsevier, V. 106, P.  179-190, 2012.
+    DOI: 10.1016/j.ress.2012.05.002.
+
+    Parameters
+    ----------
+    Data : array
+        Raw data.
+    degree : int
+        Maximum polynomial degree.
+
+    Returns
+    -------
+    Polynomial : array
+        The coefficients of the univariate orthonormal polynomials.
+
+    """
+    if Data.ndim !=1:
+        raise AttributeError('Data should be a 1D array')
+
+    # Initialization
+    dd = degree + 1
+    nsamples = len(Data)
+
+    # Forward linear transformation (Avoiding numerical issues)
+    MeanOfData = np.mean(Data)
+    Data = Data/MeanOfData
+
+    # Compute raw moments of input data
+    raw_moments = [np.sum(np.power(Data, p))/nsamples for p in range(2*dd+2)]
+
+    # Main Loop for Polynomial with degree up to dd
+    PolyCoeff_NonNorm = np.empty((0, 1))
+    Polynomial = np.zeros((dd+1, dd+1))
+
+    for degree in range(dd+1):
+        Mm = np.zeros((degree+1, degree+1))
+        Vc = np.zeros((degree+1))
+
+        # Define Moments Matrix Mm
+        for i in range(degree+1):
+            for j in range(degree+1):
+                if (i < degree):
+                    Mm[i, j] = raw_moments[i+j]
+
+                elif (i == degree) and (j == degree):
+                    Mm[i, j] = 1
+
+            # Numerical Optimization for Matrix Solver
+            Mm[i] = Mm[i] / max(abs(Mm[i]))
+
+        # Defenition of Right Hand side ortogonality conditions: Vc
+        for i in range(degree+1):
+            Vc[i] = 1 if i == degree else 0
+
+        # Solution: Coefficients of Non-Normal Orthogonal Polynomial: Vp Eq.(4)
+        try:
+            Vp = np.linalg.solve(Mm, Vc)
+        except:
+            inv_Mm = np.linalg.pinv(Mm)
+            Vp = np.dot(inv_Mm, Vc.T)
+
+        if degree == 0:
+            PolyCoeff_NonNorm = np.append(PolyCoeff_NonNorm, Vp)
+
+        if degree != 0:
+            if degree == 1:
+                zero = [0]
+            else:
+                zero = np.zeros((degree, 1))
+            PolyCoeff_NonNorm = np.hstack((PolyCoeff_NonNorm, zero))
+
+            PolyCoeff_NonNorm = np.vstack((PolyCoeff_NonNorm, Vp))
+
+        if 100*abs(sum(abs(np.dot(Mm, Vp)) - abs(Vc))) > 0.5:
+            print('\n---> Attention: Computational Error too high !')
+            print('\n---> Problem: Convergence of Linear Solver')
+
+        # Original Numerical Normalization of Coefficients with Norm and
+        # orthonormal Basis computation Matrix Storrage
+        # Note: Polynomial(i,j) correspont to coefficient number "j-1"
+        # of polynomial degree "i-1"
+        P_norm = 0
+        for i in range(nsamples):
+            Poly = 0
+            for k in range(degree+1):
+                if degree == 0:
+                    Poly += PolyCoeff_NonNorm[k] * (Data[i]**k)
+                else:
+                    Poly += PolyCoeff_NonNorm[degree, k] * (Data[i]**k)
+
+            P_norm += Poly**2 / nsamples
+
+        P_norm = np.sqrt(P_norm)
+
+        for k in range(degree+1):
+            if degree == 0:
+                Polynomial[degree, k] = PolyCoeff_NonNorm[k]/P_norm
+            else:
+                Polynomial[degree, k] = PolyCoeff_NonNorm[degree, k]/P_norm
+
+    # Backward linear transformation to the real data space
+    Data *= MeanOfData
+    for k in range(len(Polynomial)):
+        Polynomial[:, k] = Polynomial[:, k] / (MeanOfData**(k))
+
+    return Polynomial
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/bayes_linear.py b/examples/model-comparison/bayesvalidrox/surrogate_models/bayes_linear.py
new file mode 100644
index 0000000000000000000000000000000000000000..3bd827ac0ecc5b3a38116b21767e8a8799593b24
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/bayes_linear.py
@@ -0,0 +1,523 @@
+import numpy as np
+from sklearn.base import RegressorMixin
+from sklearn.linear_model._base import LinearModel
+from sklearn.utils import check_X_y, check_array, as_float_array
+from sklearn.utils.validation import check_is_fitted
+from scipy.linalg import svd
+import warnings
+from sklearn.preprocessing import normalize as f_normalize
+
+
+
+class BayesianLinearRegression(RegressorMixin,LinearModel):
+    '''
+    Superclass for Empirical Bayes and Variational Bayes implementations of 
+    Bayesian Linear Regression Model
+    '''
+    def __init__(self, n_iter, tol, fit_intercept,copy_X, verbose):
+        self.n_iter        = n_iter
+        self.fit_intercept = fit_intercept
+        self.copy_X        = copy_X
+        self.verbose       = verbose
+        self.tol           = tol
+        
+        
+    def _check_convergence(self, mu, mu_old):
+        '''
+        Checks convergence of algorithm using changes in mean of posterior
+        distribution of weights
+        '''
+        return np.sum(abs(mu-mu_old)>self.tol) == 0
+        
+        
+    def _center_data(self,X,y):
+        ''' Centers data'''
+        X     = as_float_array(X,copy = self.copy_X)
+        # normalisation should be done in preprocessing!
+        X_std = np.ones(X.shape[1], dtype = X.dtype)
+        if self.fit_intercept:
+            X_mean = np.average(X,axis = 0)
+            y_mean = np.average(y,axis = 0)
+            X     -= X_mean
+            y      = y - y_mean
+        else:
+            X_mean = np.zeros(X.shape[1],dtype = X.dtype)
+            y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype)
+        return X,y, X_mean, y_mean, X_std
+        
+        
+    def predict_dist(self,X):
+        '''
+        Calculates  mean and variance of predictive distribution for each data 
+        point of test set.(Note predictive distribution for each data point is 
+        Gaussian, therefore it is uniquely determined by mean and variance)                    
+                    
+        Parameters
+        ----------
+        x: array-like of size (n_test_samples, n_features)
+            Set of features for which corresponding responses should be predicted
+
+        Returns
+        -------
+        :list of two numpy arrays [mu_pred, var_pred]
+        
+            mu_pred : numpy array of size (n_test_samples,)
+                      Mean of predictive distribution
+                      
+            var_pred: numpy array of size (n_test_samples,)
+                      Variance of predictive distribution        
+        '''
+        # Note check_array and check_is_fitted are done within self._decision_function(X)
+        mu_pred     = self._decision_function(X)
+        data_noise  = 1./self.beta_
+        model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1)
+        var_pred    =  data_noise + model_noise
+        return [mu_pred,var_pred]
+    
+        
+        
+
+class EBLinearRegression(BayesianLinearRegression):
+    '''
+    Bayesian Regression with type II maximum likelihood (Empirical Bayes)
+    
+    Parameters:
+    -----------  
+    n_iter: int, optional (DEFAULT = 300)
+       Maximum number of iterations
+         
+    tol: float, optional (DEFAULT = 1e-3)
+       Threshold for convergence
+       
+    optimizer: str, optional (DEFAULT = 'fp')
+       Method for optimization , either Expectation Maximization or 
+       Fixed Point Gull-MacKay {'em','fp'}. Fixed point iterations are
+       faster, but can be numerically unstable (especially in case of near perfect fit).
+       
+    fit_intercept: bool, optional (DEFAULT = True)
+       If True includes bias term in model
+       
+    perfect_fit_tol: float (DEAFAULT = 1e-5)
+       Prevents overflow of precision parameters (this is smallest value RSS can have).
+       ( !!! Note if using EM instead of fixed-point, try smaller values
+       of perfect_fit_tol, for better estimates of variance of predictive distribution )
+
+    alpha: float (DEFAULT = 1)
+       Initial value of precision paramter for coefficients ( by default we define 
+       very broad distribution )
+       
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied, otherwise will be 
+        
+    verbose: bool, optional (Default = False)
+       If True at each iteration progress report is printed out
+    
+    Attributes
+    ----------
+    coef_  : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+        
+    intercept_: float
+        Value of bias term (if fit_intercept is False, then intercept_ = 0)
+        
+    alpha_ : float
+        Estimated precision of coefficients
+       
+    beta_  : float 
+        Estimated precision of noise
+        
+    eigvals_ : array, shape = (n_features, )
+        Eigenvalues of covariance matrix (from posterior distribution of weights)
+        
+    eigvecs_ : array, shape = (n_features, n_featues)
+        Eigenvectors of covariance matrix (from posterior distribution of weights)
+
+    '''
+    
+    def __init__(self,n_iter = 300, tol = 1e-3, optimizer = 'fp', fit_intercept = True,
+                 normalize=True, perfect_fit_tol = 1e-6, alpha = 1, copy_X = True, verbose = False):
+        super(EBLinearRegression,self).__init__(n_iter, tol, fit_intercept, copy_X, verbose)
+        if optimizer not in ['em','fp']:
+            raise ValueError('Optimizer can be either "em" or "fp" ')
+        self.optimizer     =  optimizer 
+        self.alpha         =  alpha 
+        self.perfect_fit   =  False
+        self.normalize     = True
+        self.scores_       =  [np.NINF]
+        self.perfect_fit_tol = perfect_fit_tol
+    
+    def _check_convergence(self, mu, mu_old):
+        '''
+        Checks convergence of algorithm using changes in mean of posterior
+        distribution of weights
+        '''
+        return np.sum(abs(mu-mu_old)>self.tol) == 0
+        
+        
+    def _center_data(self,X,y):
+        ''' Centers data'''
+        X     = as_float_array(X,copy = self.copy_X)
+        # normalisation should be done in preprocessing!
+        X_std = np.ones(X.shape[1], dtype = X.dtype)
+        if self.fit_intercept:
+            X_mean = np.average(X, axis=0)
+            X -= X_mean
+            if self.normalize:
+                X, X_std = f_normalize(X, axis=0, copy=False,
+                                         return_norm=True)
+            else:
+                X_std = np.ones(X.shape[1], dtype=X.dtype)
+            y_mean = np.average(y, axis=0)
+            y = y - y_mean
+        else:
+            X_mean = np.zeros(X.shape[1],dtype = X.dtype)
+            y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype)
+        return X,y, X_mean, y_mean, X_std
+            
+    def fit(self, X, y):
+        '''
+        Fits Bayesian Linear Regression using Empirical Bayes
+        
+        Parameters
+        ----------
+        X: array-like of size [n_samples,n_features]
+           Matrix of explanatory variables (should not include bias term)
+       
+        y: array-like of size [n_features]
+           Vector of dependent variables.
+           
+        Returns
+        -------
+        object: self
+          self
+    
+        '''
+        # preprocess data
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+        X, y, X_mean, y_mean, X_std = self._center_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean  = y_mean
+        self._x_std   = X_std
+        
+        #  precision of noise & and coefficients
+        alpha   =  self.alpha
+        var_y  = np.var(y)
+        # check that variance is non zero !!!
+        if var_y == 0 :
+            beta = 1e-2
+        else:
+            beta = 1. / np.var(y)
+
+        # to speed all further computations save svd decomposition and reuse it later
+        u,d,vt   = svd(X, full_matrices = False)
+        Uy      = np.dot(u.T,y)
+        dsq     = d**2
+        mu      = 0
+    
+        for i in range(self.n_iter):
+            
+            # find mean for posterior of w ( for EM this is E-step)
+            mu_old  =  mu
+            if n_samples > n_features:
+                 mu =  vt.T *  d/(dsq+alpha/beta) 
+            else:
+                 # clever use of SVD here , faster for large n_features
+                 mu =  u * 1./(dsq + alpha/beta)
+                 mu =  np.dot(X.T,mu)
+            mu =  np.dot(mu,Uy)
+
+            # precompute errors, since both methods use it in estimation
+            error   = y - np.dot(X,mu)
+            sqdErr  = np.sum(error**2)
+            
+            if sqdErr / n_samples < self.perfect_fit_tol:
+                self.perfect_fit = True
+                warnings.warn( ('Almost perfect fit!!! Estimated values of variance '
+                                'for predictive distribution are computed using only RSS'))
+                break
+            
+            if self.optimizer == "fp":           
+                gamma      =  np.sum(beta*dsq/(beta*dsq + alpha))
+                # use updated mu and gamma parameters to update alpha and beta
+                # !!! made computation numerically stable for perfect fit case
+                alpha      =   gamma  / (np.sum(mu**2) + np.finfo(np.float32).eps )
+                beta       =  ( n_samples - gamma ) / (sqdErr + np.finfo(np.float32).eps )
+            else:             
+                # M-step, update parameters alpha and beta to maximize ML TYPE II
+                eigvals    = 1. / (beta * dsq + alpha)
+                alpha      = n_features / ( np.sum(mu**2) + np.sum(1/eigvals) )
+                beta       = n_samples / ( sqdErr + np.sum(dsq/eigvals) )
+
+            # if converged or exceeded maximum number of iterations => terminate
+            converged = self._check_convergence(mu_old,mu)
+            if self.verbose:
+                print( "Iteration {0} completed".format(i) )
+                if converged is True:
+                    print("Algorithm converged after {0} iterations".format(i))
+            if converged or i==self.n_iter -1:
+                break
+        eigvals       = 1./(beta * dsq + alpha)
+        self.coef_    = beta*np.dot(vt.T*d*eigvals ,Uy)
+        self._set_intercept(X_mean,y_mean,X_std)
+        self.beta_    = beta
+        self.alpha_   = alpha
+        self.eigvals_ = eigvals
+        self.eigvecs_ = vt.T
+        
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+
+        return self
+    
+    def predict(self,X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance.
+        
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+           
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+        
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of predictive
+                    distribution)
+           
+             var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+        '''
+        y_hat     = np.dot(X,self.coef_) + self.intercept_
+        
+        if return_std:
+            if self.normalize:
+                X   = (X - self._x_mean_) / self._x_std
+            data_noise  = 1./self.beta_
+            model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1)
+            var_pred    =  data_noise + model_noise
+            std_hat = np.sqrt(var_pred)
+            return y_hat, std_hat
+        else:
+            return y_hat
+            
+            
+# ==============================  VBLR  =========================================
+
+def gamma_mean(a,b):
+    '''
+    Computes mean of gamma distribution
+    
+    Parameters
+    ----------
+    a: float
+      Shape parameter of Gamma distribution
+    
+    b: float
+      Rate parameter of Gamma distribution
+      
+    Returns
+    -------
+    : float
+      Mean of Gamma distribution
+    '''
+    return float(a) / b 
+    
+
+
+class VBLinearRegression(BayesianLinearRegression):
+    '''
+    Implements Bayesian Linear Regression using mean-field approximation.
+    Assumes gamma prior on precision parameters of coefficients and noise.
+
+    Parameters:
+    -----------
+    n_iter: int, optional (DEFAULT = 100)
+       Maximum number of iterations for KL minimization
+
+    tol: float, optional (DEFAULT = 1e-3)
+       Convergence threshold
+       
+    fit_intercept: bool, optional (DEFAULT = True)
+       If True will use bias term in model fitting
+
+    a: float, optional (Default = 1e-4)
+       Shape parameter of Gamma prior for precision of coefficients
+       
+    b: float, optional (Default = 1e-4)
+       Rate parameter of Gamma prior for precision coefficients
+       
+    c: float, optional (Default = 1e-4)
+       Shape parameter of  Gamma prior for precision of noise
+       
+    d: float, optional (Default = 1e-4)
+       Rate parameter of  Gamma prior for precision of noise
+       
+    verbose: bool, optional (Default = False)
+       If True at each iteration progress report is printed out
+       
+    Attributes
+    ----------
+    coef_  : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+        
+    intercept_: float
+        Value of bias term (if fit_intercept is False, then intercept_ = 0)
+        
+    alpha_ : float
+        Mean of precision of coefficients
+       
+    beta_  : float 
+        Mean of precision of noise
+
+    eigvals_ : array, shape = (n_features, )
+        Eigenvalues of covariance matrix (from posterior distribution of weights)
+        
+    eigvecs_ : array, shape = (n_features, n_featues)
+        Eigenvectors of covariance matrix (from posterior distribution of weights)
+
+    '''
+    
+    def __init__(self, n_iter = 100, tol =1e-4, fit_intercept = True, 
+                 a = 1e-4, b = 1e-4, c = 1e-4, d = 1e-4, copy_X = True,
+                 verbose = False):
+        super(VBLinearRegression,self).__init__(n_iter, tol, fit_intercept, copy_X,
+                                                verbose)
+        self.a,self.b   =  a, b
+        self.c,self.d   =  c, d
+
+        
+    def fit(self,X,y):
+        '''
+        Fits Variational Bayesian Linear Regression Model
+        
+        Parameters
+        ----------
+        X: array-like of size [n_samples,n_features]
+           Matrix of explanatory variables (should not include bias term)
+       
+        Y: array-like of size [n_features]
+           Vector of dependent variables.
+           
+        Returns
+        -------
+        object: self
+          self
+        '''
+        # preprocess data
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+        X, y, X_mean, y_mean, X_std = self._center_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean  = y_mean
+        self._x_std   = X_std
+        
+        # SVD decomposition, done once , reused at each iteration
+        u,D,vt = svd(X, full_matrices = False)
+        dsq    = D**2
+        UY     = np.dot(u.T,y)
+        
+        # some parameters of Gamma distribution have closed form solution
+        a      = self.a + 0.5 * n_features
+        c      = self.c + 0.5 * n_samples
+        b,d    = self.b,  self.d
+        
+        # initial mean of posterior for coefficients
+        mu     = 0
+                
+        for i in range(self.n_iter):
+            
+            # update parameters of distribution Q(weights)
+            e_beta       = gamma_mean(c,d)
+            e_alpha      = gamma_mean(a,b)
+            mu_old       = np.copy(mu)
+            mu,eigvals   = self._posterior_weights(e_beta,e_alpha,UY,dsq,u,vt,D,X)
+            
+            # update parameters of distribution Q(precision of weights) 
+            b            = self.b + 0.5*( np.sum(mu**2) + np.sum(eigvals))
+            
+            # update parameters of distribution Q(precision of likelihood)
+            sqderr       = np.sum((y - np.dot(X,mu))**2)
+            xsx          = np.sum(dsq*eigvals)
+            d            = self.d + 0.5*(sqderr + xsx)
+ 
+            # check convergence 
+            converged = self._check_convergence(mu,mu_old)
+            if self.verbose is True:
+                print("Iteration {0} is completed".format(i))
+                if converged is True:
+                    print("Algorithm converged after {0} iterations".format(i))
+               
+            # terminate if convergence or maximum number of iterations are achieved
+            if converged or i==(self.n_iter-1):
+                break
+            
+        # save necessary parameters    
+        self.beta_   = gamma_mean(c,d)
+        self.alpha_  = gamma_mean(a,b)
+        self.coef_, self.eigvals_ = self._posterior_weights(self.beta_, self.alpha_, UY,
+                                                            dsq, u, vt, D, X)
+        self._set_intercept(X_mean,y_mean,X_std)
+        self.eigvecs_ = vt.T
+        return self
+        
+
+    def _posterior_weights(self, e_beta, e_alpha, UY, dsq, u, vt, d, X):
+        '''
+        Calculates parameters of approximate posterior distribution 
+        of weights
+        '''
+        # eigenvalues of covariance matrix
+        sigma = 1./ (e_beta*dsq + e_alpha)
+        
+        # mean of approximate posterior distribution
+        n_samples, n_features = X.shape
+        if n_samples > n_features:
+             mu =  vt.T *  d/(dsq + e_alpha/e_beta)# + np.finfo(np.float64).eps) 
+        else:
+             mu =  u * 1./(dsq + e_alpha/e_beta)# + np.finfo(np.float64).eps)
+             mu =  np.dot(X.T,mu)
+        mu =  np.dot(mu,UY)
+        return mu,sigma
+        
+    def predict(self,X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance.
+        
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+           
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+        
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of predictive
+                    distribution)
+           
+             var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+        '''
+        x         = (X - self._x_mean_) / self._x_std
+        y_hat     = np.dot(x,self.coef_) + self._y_mean
+        
+        if return_std:
+            data_noise  = 1./self.beta_
+            model_noise = np.sum(np.dot(X,self.eigvecs_)**2 * self.eigvals_,1)
+            var_pred    =  data_noise + model_noise
+            std_hat = np.sqrt(var_pred)
+            return y_hat, std_hat
+        else:
+            return y_hat
\ No newline at end of file
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/engine.py b/examples/model-comparison/bayesvalidrox/surrogate_models/engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..42307d4770d4ae23a40107dfea64057aac682c23
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/engine.py
@@ -0,0 +1,2225 @@
+# -*- coding: utf-8 -*-
+"""
+Engine to train the surrogate
+
+"""
+import copy
+from copy import deepcopy, copy
+import h5py
+import joblib
+import numpy as np
+import os
+
+from scipy import stats, signal, linalg, sparse
+from scipy.spatial import distance
+from tqdm import tqdm
+import scipy.optimize as opt
+from sklearn.metrics import mean_squared_error
+import multiprocessing
+import matplotlib.pyplot as plt
+import pandas as pd
+import sys
+import seaborn as sns
+from joblib import Parallel, delayed
+
+
+from bayesvalidrox.bayes_inference.bayes_inference import BayesInference
+from bayesvalidrox.bayes_inference.discrepancy import Discrepancy
+from .exploration import Exploration
+import pathlib
+
+#from .inputs import Input
+#from .exp_designs import ExpDesigns
+#from .surrogate_models import MetaModel
+#from bayesvalidrox.post_processing.post_processing import PostProcessing
+
+def hellinger_distance(P, Q):
+    """
+    Hellinger distance between two continuous distributions.
+
+    The maximum distance 1 is achieved when P assigns probability zero to
+    every set to which Q assigns a positive probability, and vice versa.
+    0 (identical) and 1 (maximally different)
+
+    Parameters
+    ----------
+    P : array
+        Reference likelihood.
+    Q : array
+        Estimated likelihood.
+
+    Returns
+    -------
+    float
+        Hellinger distance of two distributions.
+
+    """
+    P = np.array(P)
+    Q= np.array(Q)
+    
+    mu1 = P.mean()
+    Sigma1 = np.std(P)
+
+    mu2 = Q.mean()
+    Sigma2 = np.std(Q)
+
+    term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2))
+
+    term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2))
+
+    H_squared = 1 - term1 * term2
+
+    return np.sqrt(H_squared)
+
+
+def logpdf(x, mean, cov):
+    """
+    Computes the likelihood based on a multivariate normal distribution.
+
+    Parameters
+    ----------
+    x : TYPE
+        DESCRIPTION.
+    mean : array_like
+        Observation data.
+    cov : 2d array
+        Covariance matrix of the distribution.
+
+    Returns
+    -------
+    log_lik : float
+        Log likelihood.
+
+    """
+    n = len(mean)
+    L = linalg.cholesky(cov, lower=True)
+    beta = np.sum(np.log(np.diag(L)))
+    dev = x - mean
+    alpha = dev.dot(linalg.cho_solve((L, True), dev))
+    log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+
+    return log_lik
+
+def subdomain(Bounds, n_new_samples):
+    """
+    Divides a domain defined by Bounds into sub domains.
+
+    Parameters
+    ----------
+    Bounds : list of tuples
+        List of lower and upper bounds.
+    n_new_samples : int
+        Number of samples to divide the domain for.
+    n_params : int
+        The number of params to build the subdomains for
+
+    Returns
+    -------
+    Subdomains : List of tuples of tuples
+        Each tuple of tuples divides one set of bounds into n_new_samples parts.
+
+    """
+    n_params = len(Bounds)
+    n_subdomains = n_new_samples + 1
+    LinSpace = np.zeros((n_params, n_subdomains))
+
+    for i in range(n_params):
+        LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1],
+                                  num=n_subdomains)
+    Subdomains = []
+    for k in range(n_subdomains-1):
+        mylist = []
+        for i in range(n_params):
+            mylist.append((LinSpace[i, k+0], LinSpace[i, k+1]))
+        Subdomains.append(tuple(mylist))
+
+    return Subdomains
+
+class Engine():
+    
+    
+    def __init__(self, MetaMod, Model, ExpDes):
+        self.MetaModel = MetaMod
+        self.Model = Model
+        self.ExpDesign = ExpDes
+        self.parallel = False
+        
+    def start_engine(self) -> None:
+        """
+        Do all the preparations that need to be run before the actual training
+
+        Returns
+        -------
+        None
+
+        """
+        self.out_names = self.Model.Output.names
+        self.MetaModel.out_names = self.out_names
+        
+        
+    def train_normal(self, parallel = False, verbose = False, save = False) -> None:
+        """
+        Trains surrogate on static samples only.
+        Samples are taken from the experimental design and the specified 
+        model is run on them.
+        Alternatively the samples can be read in from a provided hdf5 file.
+        
+
+        Returns
+        -------
+        None
+
+        """
+            
+        ExpDesign = self.ExpDesign
+        MetaModel = self.MetaModel
+        
+        # Read ExpDesign (training and targets) from the provided hdf5
+        if ExpDesign.hdf5_file is not None:
+            # TODO: need to run 'generate_ED' as well after this or not?
+            ExpDesign.read_from_file(self.out_names)
+        else:
+            # Check if an old hdf5 file exists: if yes, rename it
+            hdf5file = f'ExpDesign_{self.Model.name}.hdf5'
+            if os.path.exists(hdf5file):
+           #     os.rename(hdf5file, 'old_'+hdf5file)
+                file = pathlib.Path(hdf5file)
+                file.unlink()
+
+        # Prepare X samples 
+        # For training the surrogate use ExpDesign.X_tr, ExpDesign.X is for the model to run on 
+        ExpDesign.generate_ED(ExpDesign.n_init_samples,
+                                              transform=True,
+                                              max_pce_deg=np.max(MetaModel.pce_deg))
+        
+        # Run simulations at X 
+        if not hasattr(ExpDesign, 'Y') or ExpDesign.Y is None:
+            print('\n Now the forward model needs to be run!\n')
+            ED_Y, up_ED_X = self.Model.run_model_parallel(ExpDesign.X, mp = parallel)
+            ExpDesign.Y = ED_Y
+        else:
+            # Check if a dict has been passed.
+            if not type(ExpDesign.Y) is dict:
+                raise Exception('Please provide either a dictionary or a hdf5'
+                                'file to ExpDesign.hdf5_file argument.')
+                
+        # Separate output dict and x-values
+        if 'x_values' in ExpDesign.Y:
+            ExpDesign.x_values = ExpDesign.Y['x_values']
+            del ExpDesign.Y['x_values']
+        else:
+            print('No x_values are given, this might lead to issues during PostProcessing')
+        
+        
+        # Fit the surrogate
+        MetaModel.fit(ExpDesign.X, ExpDesign.Y, parallel, verbose)
+        
+        # Save what there is to save
+        if save:
+            # Save surrogate
+            with open(f'surrogates/surrogate_{self.Model.name}.pk1', 'wb') as output:
+                joblib.dump(MetaModel, output, 2)
+                    
+            # Zip the model run directories
+            if self.Model.link_type.lower() == 'pylink' and\
+               self.ExpDesign.sampling_method.lower() != 'user':
+                self.Model.zip_subdirs(self.Model.name, f'{self.Model.name}_')
+                
+            
+    def train_sequential(self, parallel = False, verbose = False) -> None:
+        """
+        Train the surrogate in a sequential manner.
+        First build and train evereything on the static samples, then iterate
+        choosing more samples and refitting the surrogate on them.
+
+        Returns
+        -------
+        None
+
+        """
+        #self.train_normal(parallel, verbose)
+        self.parallel = parallel
+        self.train_seq_design(parallel, verbose)
+        
+        
+    # -------------------------------------------------------------------------
+    def eval_metamodel(self, samples=None, nsamples=None,
+                       sampling_method='random', return_samples=False):
+        """
+        Evaluates meta-model at the requested samples. One can also generate
+        nsamples.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Samples to evaluate meta-model at. The default is None.
+        nsamples : int, optional
+            Number of samples to generate, if no `samples` is provided. The
+            default is None.
+        sampling_method : str, optional
+            Type of sampling, if no `samples` is provided. The default is
+            'random'.
+        return_samples : bool, optional
+            Retun samples, if no `samples` is provided. The default is False.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean of the predictions.
+        std_pred : dict
+            Standard deviatioon of the predictions.
+        """
+        # Generate or transform (if need be) samples
+        if samples is None:
+            # Generate
+            samples = self.ExpDesign.generate_samples(
+                nsamples,
+                sampling_method
+                )
+
+        # Transformation to other space is to be done in the MetaModel
+        # TODO: sort the transformations better
+        mean_pred, std_pred = self.MetaModel.eval_metamodel(samples)
+
+        if return_samples:
+            return mean_pred, std_pred, samples
+        else:
+            return mean_pred, std_pred
+        
+        
+    # -------------------------------------------------------------------------
+    def train_seq_design(self, parallel = False, verbose = False):
+        """
+        Starts the adaptive sequential design for refining the surrogate model
+        by selecting training points in a sequential manner.
+
+        Returns
+        -------
+        MetaModel : object
+            Meta model object.
+
+        """
+        self.parallel = parallel
+        
+        # Initialization
+        self.SeqModifiedLOO = {}
+        self.seqValidError = {}
+        self.SeqBME = {}
+        self.SeqKLD = {}
+        self.SeqDistHellinger = {}
+        self.seqRMSEMean = {}
+        self.seqRMSEStd = {}
+        self.seqMinDist = []
+        
+        if not hasattr(self.MetaModel, 'valid_samples'):
+            self.ExpDesign.valid_samples = []
+            self.ExpDesign.valid_model_runs = []
+            self.valid_likelihoods = []
+        
+        validError = None
+
+
+        # Determine the metamodel type
+        if self.MetaModel.meta_model_type.lower() != 'gpe':
+            pce = True
+        else:
+            pce = False
+        mc_ref = True if bool(self.Model.mc_reference) else False
+        if mc_ref:
+            self.Model.read_observation('mc_ref')
+
+        # Get the parameters
+        max_n_samples = self.ExpDesign.n_max_samples
+        mod_LOO_threshold = self.ExpDesign.mod_LOO_threshold
+        n_canddidate = self.ExpDesign.n_canddidate
+        post_snapshot = self.ExpDesign.post_snapshot
+        n_replication = self.ExpDesign.n_replication
+        util_func = self.ExpDesign.util_func
+        output_name = self.out_names
+        
+        # Handle if only one UtilityFunctions is provided
+        if not isinstance(util_func, list):
+            util_func = [self.ExpDesign.util_func]
+
+        # Read observations or MCReference
+        # TODO: recheck the logic in this if statement
+        if (len(self.Model.observations) != 0 or self.Model.meas_file is not None) and hasattr(self.MetaModel, 'Discrepancy'):
+            self.observations = self.Model.read_observation()
+            obs_data = self.observations
+        else:
+            obs_data = []
+            # TODO: TotalSigma2 not defined if not in this else???
+            # TODO: no self.observations if in here
+            TotalSigma2 = {}
+            
+        # ---------- Initial self.MetaModel ----------
+        self.train_normal(parallel = parallel, verbose=verbose)
+        
+        initMetaModel = deepcopy(self.MetaModel)
+
+        # Validation error if validation set is provided.
+        if self.ExpDesign.valid_model_runs:
+            init_rmse, init_valid_error = self._validError(initMetaModel)
+            init_valid_error = list(init_valid_error.values())
+        else:
+            init_rmse = None
+
+        # Check if discrepancy is provided
+        if len(obs_data) != 0 and hasattr(self.MetaModel, 'Discrepancy'):
+            TotalSigma2 = self.MetaModel.Discrepancy.parameters
+
+            # Calculate the initial BME
+            out = self._BME_Calculator(
+                obs_data, TotalSigma2, init_rmse)
+            init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out
+            print(f"\nInitial BME: {init_BME:.2f}")
+            print(f"Initial KLD: {init_KLD:.2f}")
+
+            # Posterior snapshot (initial)
+            if post_snapshot:
+                parNames = self.ExpDesign.par_names
+                print('Posterior snapshot (initial) is being plotted...')
+                self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init')
+
+        # Check the convergence of the Mean & Std
+        if mc_ref and pce:
+            init_rmse_mean, init_rmse_std = self._error_Mean_Std()
+            print(f"Initial Mean and Std error: {init_rmse_mean:.2f},"
+                  f" {init_rmse_std:.2f}")
+
+        # Read the initial experimental design
+        Xinit = self.ExpDesign.X
+        init_n_samples = len(self.ExpDesign.X)
+        initYprev = self.ExpDesign.Y#initMetaModel.ModelOutputDict
+        #self.MetaModel.ModelOutputDict = self.ExpDesign.Y
+        initLCerror = initMetaModel.LCerror
+        n_itrs = max_n_samples - init_n_samples
+
+        ## Get some initial statistics
+        # Read the initial ModifiedLOO
+        if pce:
+            Scores_all, varExpDesignY = [], []
+            for out_name in output_name:
+                y = self.ExpDesign.Y[out_name]
+                Scores_all.append(list(
+                    self.MetaModel.score_dict['b_1'][out_name].values()))
+                if self.MetaModel.dim_red_method.lower() == 'pca':
+                    pca = self.MetaModel.pca['b_1'][out_name]
+                    components = pca.transform(y)
+                    varExpDesignY.append(np.var(components, axis=0))
+                else:
+                    varExpDesignY.append(np.var(y, axis=0))
+
+            Scores = [item for sublist in Scores_all for item in sublist]
+            weights = [item for sublist in varExpDesignY for item in sublist]
+            init_mod_LOO = [np.average([1-score for score in Scores],
+                                       weights=weights)]
+
+        prevMetaModel_dict = {}
+        #prevExpDesign_dict = {}
+        # Can run sequential design multiple times for comparison
+        for repIdx in range(n_replication):
+            print(f'\n>>>> Replication: {repIdx+1}<<<<')
+
+            # util_func: the function to use inside the type of exploitation
+            for util_f in util_func:
+                print(f'\n>>>> Utility Function: {util_f} <<<<')
+                # To avoid changes ub original aPCE object
+                self.ExpDesign.X = Xinit
+                self.ExpDesign.Y = initYprev
+                self.ExpDesign.LCerror = initLCerror
+
+                # Set the experimental design
+                Xprev = Xinit
+                total_n_samples = init_n_samples
+                Yprev = initYprev
+
+                Xfull = []
+                Yfull = []
+
+                # Store the initial ModifiedLOO
+                if pce:
+                    print("\nInitial ModifiedLOO:", init_mod_LOO)
+                    SeqModifiedLOO = np.array(init_mod_LOO)
+
+                if len(self.ExpDesign.valid_model_runs) != 0:
+                    SeqValidError = np.array(init_valid_error)
+
+                # Check if data is provided
+                if len(obs_data) != 0 and hasattr(self.MetaModel, 'Discrepancy'):
+                    SeqBME = np.array([init_BME])
+                    SeqKLD = np.array([init_KLD])
+                    SeqDistHellinger = np.array([init_dist_hellinger])
+
+                if mc_ref and pce:
+                    seqRMSEMean = np.array([init_rmse_mean])
+                    seqRMSEStd = np.array([init_rmse_std])
+
+                # ------- Start Sequential Experimental Design -------
+                postcnt = 1
+                for itr_no in range(1, n_itrs+1):
+                    print(f'\n>>>> Iteration number {itr_no} <<<<')
+
+                    # Save the metamodel prediction before updating
+                    prevMetaModel_dict[itr_no] = deepcopy(self.MetaModel)
+                    #prevExpDesign_dict[itr_no] = deepcopy(self.ExpDesign)
+                    if itr_no > 1:
+                        pc_model = prevMetaModel_dict[itr_no-1]
+                        self._y_hat_prev, _ = pc_model.eval_metamodel(
+                            samples=Xfull[-1].reshape(1, -1))
+                        del prevMetaModel_dict[itr_no-1]
+
+                    # Optimal Bayesian Design
+                    #self.MetaModel.ExpDesignFlag = 'sequential'
+                    Xnew, updatedPrior = self.choose_next_sample(TotalSigma2,
+                                                            n_canddidate,
+                                                            util_f)
+                    S = np.min(distance.cdist(Xinit, Xnew, 'euclidean'))
+                    self.seqMinDist.append(S)
+                    print(f"\nmin Dist from OldExpDesign: {S:2f}")
+                    print("\n")
+
+                    # Evaluate the full model response at the new sample
+                    Ynew, _ = self.Model.run_model_parallel(
+                        Xnew, prevRun_No=total_n_samples
+                        )
+                    total_n_samples += Xnew.shape[0]
+
+                    # ------ Plot the surrogate model vs Origninal Model ------
+                    if hasattr(self.ExpDesign, 'adapt_verbose') and \
+                       self.ExpDesign.adapt_verbose:
+                        from .adaptPlot import adaptPlot
+                        y_hat, std_hat = self.MetaModel.eval_metamodel(
+                            samples=Xnew
+                            )
+                        adaptPlot(
+                            self.MetaModel, Ynew, y_hat, std_hat,
+                            plotED=False
+                            )
+
+                    # -------- Retrain the surrogate model -------
+                    # Extend new experimental design
+                    Xfull = np.vstack((Xprev, Xnew))
+
+                    # Updating experimental design Y
+                    for out_name in output_name:
+                        Yfull = np.vstack((Yprev[out_name], Ynew[out_name]))
+                        self.ExpDesign.Y[out_name] = Yfull
+
+                    # Pass new design to the metamodel object
+                    self.ExpDesign.sampling_method = 'user'
+                    self.ExpDesign.X = Xfull
+                    #self.ExpDesign.Y = self.MetaModel.ModelOutputDict
+
+                    # Save the Experimental Design for next iteration
+                    Xprev = Xfull
+                    Yprev = self.ExpDesign.Y 
+
+                    # Pass the new prior as the input
+                    # TODO: another look at this - no difference apc to pce to gpe?
+                    self.MetaModel.input_obj.poly_coeffs_flag = False
+                    if updatedPrior is not None:
+                        self.MetaModel.input_obj.poly_coeffs_flag = True
+                        print("updatedPrior:", updatedPrior.shape)
+                        # Arbitrary polynomial chaos
+                        for i in range(updatedPrior.shape[1]):
+                            self.MetaModel.input_obj.Marginals[i].dist_type = None
+                            x = updatedPrior[:, i]
+                            self.MetaModel.input_obj.Marginals[i].raw_data = x
+
+                    # Train the surrogate model for new ExpDesign
+                    self.train_normal(parallel=False)
+
+                    # -------- Evaluate the retrained surrogate model -------
+                    # Extract Modified LOO from Output
+                    if pce:
+                        Scores_all, varExpDesignY = [], []
+                        for out_name in output_name:
+                            y = self.ExpDesign.Y[out_name]
+                            Scores_all.append(list(
+                                self.MetaModel.score_dict['b_1'][out_name].values()))
+                            if self.MetaModel.dim_red_method.lower() == 'pca':
+                                pca = self.MetaModel.pca['b_1'][out_name]
+                                components = pca.transform(y)
+                                varExpDesignY.append(np.var(components,
+                                                            axis=0))
+                            else:
+                                varExpDesignY.append(np.var(y, axis=0))
+                        Scores = [item for sublist in Scores_all for item
+                                  in sublist]
+                        weights = [item for sublist in varExpDesignY for item
+                                   in sublist]
+                        ModifiedLOO = [np.average(
+                            [1-score for score in Scores], weights=weights)]
+
+                        print('\n')
+                        print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO)
+                        print('\n')
+
+                    # Compute the validation error
+                    if self.ExpDesign.valid_model_runs:
+                        rmse, validError = self._validError(self.MetaModel)
+                        ValidError = list(validError.values())
+                    else:
+                        rmse = None
+
+                    # Store updated ModifiedLOO
+                    if pce:
+                        SeqModifiedLOO = np.vstack(
+                            (SeqModifiedLOO, ModifiedLOO))
+                        if len(self.ExpDesign.valid_model_runs) != 0:
+                            SeqValidError = np.vstack(
+                                (SeqValidError, ValidError))
+                    # -------- Caclulation of BME as accuracy metric -------
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        # Calculate the initial BME
+                        out = self._BME_Calculator(obs_data, TotalSigma2, rmse)
+                        BME, KLD, Posterior, likes, DistHellinger = out
+                        print('\n')
+                        print(f"Updated BME: {BME:.2f}")
+                        print(f"Updated KLD: {KLD:.2f}")
+                        print('\n')
+
+                        # Plot some snapshots of the posterior
+                        step_snapshot = self.ExpDesign.step_snapshot
+                        if post_snapshot and postcnt % step_snapshot == 0:
+                            parNames = self.ExpDesign.par_names
+                            print('Posterior snapshot is being plotted...')
+                            self.__posteriorPlot(Posterior, parNames,
+                                                 f'SeqPosterior_{postcnt}')
+                        postcnt += 1
+
+                    # Check the convergence of the Mean&Std
+                    if mc_ref and pce:
+                        print('\n')
+                        RMSE_Mean, RMSE_std = self._error_Mean_Std()
+                        print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, "
+                              f"{RMSE_std:.2f}")
+                        print('\n')
+
+                    # Store the updated BME & KLD
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        SeqBME = np.vstack((SeqBME, BME))
+                        SeqKLD = np.vstack((SeqKLD, KLD))
+                        SeqDistHellinger = np.vstack((SeqDistHellinger,
+                                                      DistHellinger))
+                    if mc_ref and pce:
+                        seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean))
+                        seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std))
+
+                    if pce and any(LOO < mod_LOO_threshold
+                                   for LOO in ModifiedLOO):
+                        break
+
+                    # Clean up
+                    if len(obs_data) != 0:
+                        del out
+                    print()
+                    print('-'*50)
+                    print()
+
+                # Store updated ModifiedLOO and BME in dictonary
+                strKey = f'{util_f}_rep_{repIdx+1}'
+                if pce:
+                    self.SeqModifiedLOO[strKey] = SeqModifiedLOO
+                if len(self.ExpDesign.valid_model_runs) != 0:
+                    self.seqValidError[strKey] = SeqValidError
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    self.SeqBME[strKey] = SeqBME
+                    self.SeqKLD[strKey] = SeqKLD
+                if hasattr(self.MetaModel, 'valid_likelihoods') and \
+                   self.valid_likelihoods:
+                    self.SeqDistHellinger[strKey] = SeqDistHellinger
+                if mc_ref and pce:
+                    self.seqRMSEMean[strKey] = seqRMSEMean
+                    self.seqRMSEStd[strKey] = seqRMSEStd
+
+        # return self.MetaModel
+
+    # -------------------------------------------------------------------------
+    def util_VarBasedDesign(self, X_can, index, util_func='Entropy'):
+        """
+        Computes the exploitation scores based on:
+        active learning MacKay(ALM) and active learning Cohn (ALC)
+        Paper: Sequential Design with Mutual Information for Computer
+        Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas
+        (2016)
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        index : int
+            Model output index.
+        UtilMethod : string, optional
+            Exploitation utility function. The default is 'Entropy'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+        MetaModel = self.MetaModel
+        ED_X = self.ExpDesign.X
+        out_dict_y = self.ExpDesign.Y
+        out_names = self.out_names
+
+        # Run the Metamodel for the candidate
+        X_can = X_can.reshape(1, -1)
+        Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can)
+
+        if util_func.lower() == 'alm':
+            # ----- Entropy/MMSE/active learning MacKay(ALM)  -----
+            # Compute perdiction variance of the old model
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            varPCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                varPCE[KeyIdx] = np.max(canPredVar[key], axis=1)
+            score = np.max(varPCE, axis=0)
+
+        elif util_func.lower() == 'eigf':
+            # ----- Expected Improvement for Global fit -----
+            # Find closest EDX to the candidate
+            distances = distance.cdist(ED_X, X_can, 'euclidean')
+            index = np.argmin(distances)
+
+            # Compute perdiction error and variance of the old model
+            predError = {key: Y_PC_can[key] for key in out_names}
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            # Compute perdiction error and variance of the old model
+            # Eq (5) from Liu et al.(2018)
+            EIGF_PCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                residual = predError[key] - out_dict_y[key][int(index)]
+                var = canPredVar[key]
+                EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1)
+            score = np.max(EIGF_PCE, axis=0)
+
+        return -1 * score   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianActiveDesign(self, y_hat, std, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian active design criterion (var).
+
+        It is based on the following paper:
+        Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak.
+        "Bayesian3 active learning for the gaussian process emulator using
+        information theory." Entropy 22, no. 8 (2020): 890.
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            BAL design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # Get the data
+        obs_data = self.observations
+        # TODO: this should be optimizable to be calculated explicitly
+        if hasattr(self.Model, 'n_obs'):
+            n_obs = self.Model.n_obs
+        else:
+            n_obs = self.n_obs
+        mc_size = 10000
+
+        # Sample a distribution for a normal dist
+        # with Y_mean_can as the mean and Y_std_can as std.
+        Y_MC, std_MC = {}, {}
+        logPriorLikelihoods = np.zeros((mc_size))
+       # print(y_hat)
+       # print(list[y_hat])
+        for key in list(y_hat):
+            cov = np.diag(std[key]**2)
+           # print(y_hat[key], cov)
+            # TODO: added the allow_singular = True here
+            rv = stats.multivariate_normal(mean=y_hat[key], cov=cov,)
+            Y_MC[key] = rv.rvs(size=mc_size)
+            logPriorLikelihoods += rv.logpdf(Y_MC[key])
+            std_MC[key] = np.zeros((mc_size, y_hat[key].shape[0]))
+
+        #  Likelihood computation (Comparison of data and simulation
+        #  results via PCE with candidate design)
+        likelihoods = self._normpdf(Y_MC, std_MC, obs_data, sigma2Dict)
+        
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, mc_size)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(likelihoods), dtype=np.longdouble)#float128)
+
+        # Posterior-based expectation of likelihoods
+        postLikelihoods = likelihoods[accepted]
+        postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+        # Utility function Eq.2 in Ref. (2)
+        # Posterior covariance matrix after observing data y
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # TODO: Calculate the correction factor for BME
+            # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can,
+            #                                      ObservationData, sigma2Dict)
+            # BME += BMECorrFactor
+            # Haun et al implementation
+            # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+            U_J_d = postExpLikelihoods - logBME
+
+        # Marginal log likelihood
+        elif var == 'BME':
+            U_J_d = np.nanmean(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            logBME = np.log(np.nanmean(likelihoods))
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # Bayesian information criterion
+        elif var == 'BIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxL = np.nanmax(likelihoods)
+            U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams
+
+        # Akaike information criterion
+        elif var == 'AIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxlogL = np.log(np.nanmax(likelihoods))
+            AIC = -2 * maxlogL + 2 * nModelParams
+            # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1)
+            penTerm = 0
+            U_J_d = 1*(AIC + penTerm)
+
+        # Deviance information criterion
+        elif var == 'DIC':
+            # D_theta_bar = np.mean(-2 * Likelihoods)
+            N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0]))
+            Likelihoods_theta_mean = self._normpdf(
+                y_hat, std, obs_data, sigma2Dict
+                )
+            DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p
+
+            U_J_d = DIC
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Handle inf and NaN (replace by zero)
+        if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf:
+            U_J_d = 0.0
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        MetaModel = self.MetaModel
+        out_names = self.out_names
+        if X_can.ndim == 1:
+            X_can = X_can.reshape(1, -1)
+
+        # Compute the mean and std based on the MetaModel
+        # pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+        if var == 'ALC':
+            Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC)
+
+        # Old Experimental design
+        oldExpDesignX = self.ExpDesign.X
+        oldExpDesignY = self.ExpDesign.Y
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can)
+        PCE_Model_can = deepcopy(MetaModel)
+        engine_can = deepcopy(self)
+        # Add the candidate to the ExpDesign
+        NewExpDesignX = np.vstack((oldExpDesignX, X_can))
+
+        NewExpDesignY = {}
+        for key in oldExpDesignY.keys():
+            NewExpDesignY[key] = np.vstack(
+                (oldExpDesignY[key], Y_PC_can[key])
+                )
+
+        engine_can.ExpDesign.sampling_method = 'user'
+        engine_can.ExpDesign.X = NewExpDesignX
+        #engine_can.ModelOutputDict = NewExpDesignY
+        engine_can.ExpDesign.Y = NewExpDesignY
+
+        # Train the model for the observed data using x_can
+        engine_can.MetaModel.input_obj.poly_coeffs_flag = False
+        engine_can.start_engine()
+        engine_can.train_normal(parallel=False)
+        engine_can.MetaModel.fit(NewExpDesignX, NewExpDesignY)
+#        engine_can.train_norm_design(parallel=False)
+
+        # Set the ExpDesign to its original values
+        engine_can.ExpDesign.X = oldExpDesignX
+        engine_can.ModelOutputDict = oldExpDesignY
+        engine_can.ExpDesign.Y = oldExpDesignY
+
+        if var.lower() == 'mi':
+            # Mutual information based on Krause et al
+            # Adapted from Beck & Guillas (MICE) paper
+            _, std_PC_can = engine_can.MetaModel.eval_metamodel(samples=X_can)
+            std_can = {key: std_PC_can[key] for key in out_names}
+
+            std_old = {key: Y_std_can[key] for key in out_names}
+
+            varPCE = np.zeros((len(out_names)))
+            for i, key in enumerate(out_names):
+                varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2)
+            score = np.mean(varPCE)
+
+            return -1 * score
+
+        elif var.lower() == 'alc':
+            # Active learning based on Gramyc and Lee
+            # Adaptive design and analysis of supercomputer experiments Techno-
+            # metrics, 51 (2009), pp. 130–145.
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC_can, Y_MC_std_can = engine_can.MetaModel.eval_metamodel(samples=X_MC)
+
+            # Compute the score
+            score = []
+            for i, key in enumerate(out_names):
+                pce_var = Y_MC_std_can[key]**2
+                pce_var_can = Y_MC_std[key]**2
+                score.append(np.mean(pce_var-pce_var_can, axis=0))
+            score = np.mean(score)
+
+            return -1 * score
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = X_MC.shape[0]
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Enriching Monte Carlo samples if need be
+            if ESS != 0:
+                X_MC = self.ExpDesign.generate_samples(
+                    MCsize, 'random'
+                    )
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and simulation
+            # results via PCE with candidate design)
+            likelihoods = self._normpdf(
+                Y_MC, std_MC, self.observations, sigma2Dict
+                )
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                print("--- increasing MC size---")
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # -------------------- Utility functions --------------------
+        # Utility function Eq.2 in Ref. (2)
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods, dtype=np.longdouble))#float128))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Haun et al implementation
+            U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME)
+
+            # U_J_d = np.sum(G_n_m_all)
+            # Ryan et al (2014) implementation
+            # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0])
+            # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME
+
+            # U_J_d = postExpLikelihoods - logBME
+
+        # Marginal likelihood
+        elif var == 'BME':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+            U_J_d = logBME
+
+        # Bayes risk likelihood
+        elif var == 'BayesRisk':
+
+            U_J_d = -1 * np.var(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postLikelihoods /= np.nansum(likelihoods[accepted])
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Posterior-based expectation of prior densities
+            postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # D-Posterior-precision
+        elif var == 'DPP':
+            X_Posterior = X_MC[accepted]
+            # covariance of the posterior parameters
+            U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior)))
+
+        # A-Posterior-precision
+        elif var == 'APP':
+            X_Posterior = X_MC[accepted]
+            # trace of the posterior parameters
+            U_J_d = -np.log(np.trace(np.cov(X_Posterior)))
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+
+    # -------------------------------------------------------------------------
+    def run_util_func(self, method, candidates, index, sigma2Dict=None,
+                      var=None, X_MC=None):
+        """
+        Runs the utility function based on the given method.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        candidates : array of shape (n_samples, n_params)
+            All candidate parameter sets.
+        index : int
+            ExpDesign index.
+        sigma2Dict : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        var : string, optional
+            Utility function. The default is None.
+        X_MC : TYPE, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        index : TYPE
+            DESCRIPTION.
+        List
+            Scores.
+
+        """
+
+        if method.lower() == 'varoptdesign':
+            # U_J_d = self.util_VarBasedDesign(candidates, index, var)
+            U_J_d = np.zeros((candidates.shape[0]))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="varoptdesign"):
+                U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var)
+
+        elif method.lower() == 'bayesactdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            # Evaluate all candidates
+            y_can, std_can = self.MetaModel.eval_metamodel(samples=candidates)
+            # loop through candidates
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="BAL Design"):
+                y_hat = {key: items[idx] for key, items in y_can.items()}
+                std = {key: items[idx] for key, items in std_can.items()}
+                
+               # print(y_hat)
+               # print(std)
+                U_J_d[idx] = self.util_BayesianActiveDesign(
+                    y_hat, std, sigma2Dict, var)
+
+        elif method.lower() == 'bayesoptdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict,
+                                                      var)
+        return (index, -1 * U_J_d)
+
+    # -------------------------------------------------------------------------
+    def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No,
+                       verbose=False):
+        """
+        Exploration algorithm to find the optimum parameter space.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        Bounds : list of tuples
+            List of lower and upper boundaries of parameters.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        Run_No : int
+            Run number.
+        verbose : bool, optional
+            Print out a summary. The default is False.
+
+        Returns
+        -------
+        Run_No : int
+            Run number.
+        array
+            Optimial candidate.
+
+        """
+
+        Model = self.Model
+        max_func_itr = self.ExpDesign.max_func_itr
+
+        if method == 'VarOptDesign':
+            Res_Global = opt.dual_annealing(self.util_VarBasedDesign,
+                                            bounds=Bounds,
+                                            args=(Model, var),
+                                            maxfun=max_func_itr)
+
+        elif method == 'BayesOptDesign':
+            Res_Global = opt.dual_annealing(self.util_BayesianDesign,
+                                            bounds=Bounds,
+                                            args=(Model, sigma2Dict, var),
+                                            maxfun=max_func_itr)
+
+        if verbose:
+            print(f"Global minimum: xmin = {Res_Global.x}, "
+                  f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}")
+
+        return (Run_No, Res_Global.x)
+
+    # -------------------------------------------------------------------------
+    def tradeoff_weights(self, tradeoff_scheme, old_EDX, old_EDY):
+        """
+        Calculates weights for exploration scores based on the requested
+        scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`.
+
+        `None`: No exploration.
+        `equal`: Same weights for exploration and exploitation scores.
+        `epsilon-decreasing`: Start with more exploration and increase the
+            influence of exploitation along the way with a exponential decay
+            function
+        `adaptive`: An adaptive method based on:
+            Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling
+            approach for Kriging metamodeling by maximizing expected prediction
+            error." Computers & Chemical Engineering 106 (2017): 171-182.
+
+        Parameters
+        ----------
+        tradeoff_scheme : string
+            Trade-off scheme for exloration and exploitation scores.
+        old_EDX : array (n_samples, n_params)
+            Old experimental design (training points).
+        old_EDY : dict
+            Old model responses (targets).
+
+        Returns
+        -------
+        exploration_weight : float
+            Exploration weight.
+        exploitation_weight: float
+            Exploitation weight.
+
+        """
+        if tradeoff_scheme is None:
+            exploration_weight = 0
+
+        elif tradeoff_scheme == 'equal':
+            exploration_weight = 0.5
+
+        elif tradeoff_scheme == 'epsilon-decreasing':
+            # epsilon-decreasing scheme
+            # Start with more exploration and increase the influence of
+            # exploitation along the way with a exponential decay function
+            initNSamples = self.ExpDesign.n_init_samples
+            n_max_samples = self.ExpDesign.n_max_samples
+
+            itrNumber = (self.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.ExpDesign.n_new_samples
+
+            tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8)
+            exploration_weight = signal.exponential(n_max_samples-initNSamples,
+                                                    0, tau2, False)[itrNumber]
+
+        elif tradeoff_scheme == 'adaptive':
+
+            # Extract itrNumber
+            initNSamples = self.ExpDesign.n_init_samples
+            n_max_samples = self.ExpDesign.n_max_samples
+            itrNumber = (self.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.ExpDesign.n_new_samples
+
+            if itrNumber == 0:
+                exploration_weight = 0.5
+            else:
+                # New adaptive trade-off according to Liu et al. (2017)
+                # Mean squared error for last design point
+                last_EDX = old_EDX[-1].reshape(1, -1)
+                lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX)
+                pce_y = np.array(list(lastPCEY.values()))[:, 0]
+                y = np.array(list(old_EDY.values()))[:, -1, :]
+                mseError = mean_squared_error(pce_y, y)
+
+                # Mean squared CV - error for last design point
+                pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0]
+                mseCVError = mean_squared_error(pce_y_prev, y)
+
+                exploration_weight = min([0.5*mseError/mseCVError, 1])
+
+        # Exploitation weight
+        exploitation_weight = 1 - exploration_weight
+
+        return exploration_weight, exploitation_weight
+
+    # -------------------------------------------------------------------------
+    def choose_next_sample(self, sigma2=None, n_candidates=5, var='DKL'):
+        """
+        Runs optimal sequential design.
+
+        Parameters
+        ----------
+        sigma2 : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        n_candidates : int, optional
+            Number of candidate samples. The default is 5.
+        var : string, optional
+            Utility function. The default is None. # TODO: default is set to DKL, not none
+
+        Raises
+        ------
+        NameError
+            Wrong utility function.
+
+        Returns
+        -------
+        Xnew : array (n_samples, n_params)
+            Selected new training point(s).
+        """
+
+        # Initialization
+        Bounds = self.ExpDesign.bound_tuples
+        n_new_samples = self.ExpDesign.n_new_samples
+        explore_method = self.ExpDesign.explore_method
+        exploit_method = self.ExpDesign.exploit_method
+        n_cand_groups = self.ExpDesign.n_cand_groups
+        tradeoff_scheme = self.ExpDesign.tradeoff_scheme
+
+        old_EDX = self.ExpDesign.X
+        old_EDY = self.ExpDesign.Y.copy()
+        ndim = self.ExpDesign.X.shape[1]
+        OutputNames = self.out_names
+
+        # -----------------------------------------
+        # ----------- CUSTOMIZED METHODS ----------
+        # -----------------------------------------
+        # Utility function exploit_method provided by user
+        if exploit_method.lower() == 'user':
+            if not hasattr(self.ExpDesign, 'ExploitFunction'):
+                raise AttributeError('Function `ExploitFunction` not given to the ExpDesign, thus cannor run user-defined sequential scheme')
+            # TODO: syntax does not fully match the rest - can test this??
+            Xnew, filteredSamples = self.ExpDesign.ExploitFunction(self)
+
+            print("\n")
+            print("\nXnew:\n", Xnew)
+
+            return Xnew, filteredSamples
+
+
+        # Dual-Annealing works differently from the rest, so deal with this first
+        # Here exploration and exploitation are performed simulataneously
+        if explore_method == 'dual annealing':
+            # ------- EXPLORATION: OPTIMIZATION -------
+            import time
+            start_time = time.time()
+
+            # Divide the domain to subdomains
+            subdomains = subdomain(Bounds, n_new_samples)
+
+            # Multiprocessing
+            if self.parallel:
+                args = []
+                for i in range(n_new_samples):
+                    args.append((exploit_method, subdomains[i], sigma2, var, i))
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.dual_annealing, args).get()
+
+                # Close the pool
+                pool.close()
+            # Without multiprocessing
+            else:
+                results = []
+                for i in range(n_new_samples):
+                    results.append(self.dual_annealing(exploit_method, subdomains[i], sigma2, var, i))
+                    
+            # New sample
+            Xnew = np.array([results[i][1] for i in range(n_new_samples)])
+            print("\nXnew:\n", Xnew)
+
+            # Computational cost
+            elapsed_time = time.time() - start_time
+            print("\n")
+            print(f"Elapsed_time: {round(elapsed_time,2)} sec.")
+            print('-'*20)
+            
+            return Xnew, None
+        
+        # Generate needed Exploration class
+        explore = Exploration(self.ExpDesign, n_candidates)
+        explore.w = 100  # * ndim #500  # TODO: where does this value come from?
+        
+        # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+        explore.mc_criterion = 'mc-intersite-proj'
+        
+        # Generate the candidate samples
+        # TODO: here use the sampling method provided by the expdesign?
+        sampling_method = self.ExpDesign.sampling_method
+        
+        # TODO: changed this from 'random' for LOOCV
+        if explore_method == 'LOOCV':
+            allCandidates = self.ExpDesign.generate_samples(n_candidates,
+                                                            sampling_method)
+        else:
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+        
+        # -----------------------------------------
+        # ---------- EXPLORATION METHODS ----------
+        # -----------------------------------------
+        if explore_method == 'LOOCV':
+            # -----------------------------------------------------------------
+            # TODO: LOOCV model construnction based on Feng et al. (2020)
+            # 'LOOCV':
+            # Initilize the ExploitScore array
+
+            # Generate random samples
+            allCandidates = self.ExpDesign.generate_samples(n_candidates,
+                                                                'random')
+
+            # Construct error model based on LCerror
+            errorModel = self.MetaModel.create_ModelError(old_EDX, self.LCerror)
+            self.errorModel.append(copy(errorModel))
+
+            # Evaluate the error models for allCandidates
+            eLCAllCands, _ = errorModel.eval_errormodel(allCandidates)
+            # Select the maximum as the representative error
+            eLCAllCands = np.dstack(eLCAllCands.values())
+            eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0]
+
+            # Normalize the error w.r.t the maximum error
+            scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates)
+
+        else:
+            # ------- EXPLORATION: SPACE-FILLING DESIGN -------
+            # Generate candidate samples from Exploration class
+            explore = Exploration(self.ExpDesign, n_candidates)
+            explore.w = 100  # * ndim #500
+            # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+            explore.mc_criterion = 'mc-intersite-proj'
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+
+            # Temp: ---- Plot all candidates -----
+            if ndim == 2:
+                def plotter(points, allCandidates, Method,
+                            scoreExploration=None):
+                    if Method == 'Voronoi':
+                        from scipy.spatial import Voronoi, voronoi_plot_2d
+                        vor = Voronoi(points)
+                        fig = voronoi_plot_2d(vor)
+                        ax1 = fig.axes[0]
+                    else:
+                        fig = plt.figure()
+                        ax1 = fig.add_subplot(111)
+                    ax1.scatter(points[:, 0], points[:, 1], s=10, c='r',
+                                marker="s", label='Old Design Points')
+                    ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10,
+                                c='b', marker="o", label='Design candidates')
+                    for i in range(points.shape[0]):
+                        txt = 'p'+str(i+1)
+                        ax1.annotate(txt, (points[i, 0], points[i, 1]))
+                    if scoreExploration is not None:
+                        for i in range(allCandidates.shape[0]):
+                            txt = str(round(scoreExploration[i], 5))
+                            ax1.annotate(txt, (allCandidates[i, 0],
+                                               allCandidates[i, 1]))
+
+                    plt.xlim(self.bound_tuples[0])
+                    plt.ylim(self.bound_tuples[1])
+                    # plt.show()
+                    plt.legend(loc='upper left')
+
+        # -----------------------------------------
+        # --------- EXPLOITATION METHODS ----------
+        # -----------------------------------------
+        if exploit_method == 'BayesOptDesign' or\
+           exploit_method == 'BayesActDesign':
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradeoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\n Exploration weight={explore_w:0.3f} "
+                  f"Exploitation weight={exploit_w:0.3f}\n")
+
+            # ------- EXPLOITATION: BayesOptDesign & ActiveLearning -------
+            if explore_w != 1.0:
+                # Check if all needed properties are set
+                if not hasattr(self.ExpDesign, 'max_func_itr'):
+                    raise AttributeError('max_func_itr not given to the experimental design')
+
+                # Create a sample pool for rejection sampling
+                MCsize = 15000
+                X_MC = self.ExpDesign.generate_samples(MCsize, 'random')
+                candidates = self.ExpDesign.generate_samples(
+                    n_candidates, 'latin_hypercube')
+
+                # Split the candidates in groups for multiprocessing
+                split_cand = np.array_split(
+                    candidates, n_cand_groups, axis=0
+                    )
+               # print(candidates)
+               # print(split_cand)
+                if self.parallel:
+                    results = Parallel(n_jobs=-1, backend='multiprocessing')(
+                        delayed(self.run_util_func)(
+                            exploit_method, split_cand[i], i, sigma2, var, X_MC)
+                        for i in range(n_cand_groups)) 
+                else:
+                    results = []
+                    for i in range(n_cand_groups):
+                        results.append(self.run_util_func(exploit_method, split_cand[i], i, sigma2, var, X_MC))
+                        
+                # Retrieve the results and append them
+                U_J_d = np.concatenate([results[NofE][1] for NofE in
+                                        range(n_cand_groups)])
+
+                # Check if all scores are inf
+                if np.isinf(U_J_d).all() or np.isnan(U_J_d).all():
+                    U_J_d = np.ones(len(U_J_d))
+
+                # Get the expected value (mean) of the Utility score
+                # for each cell
+                if explore_method == 'Voronoi':
+                    U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1)
+
+                # Normalize U_J_d
+                norm_U_J_d = U_J_d / np.sum(U_J_d)
+            else:
+                norm_U_J_d = np.zeros((len(scoreExploration)))
+
+            # ------- Calculate Total score -------
+            # ------- Trade off between EXPLORATION & EXPLOITATION -------
+            # Accumulate the samples
+            finalCandidates = np.concatenate((allCandidates, candidates), axis = 0)   
+            finalCandidates = np.unique(finalCandidates, axis = 0)
+            
+            # Calculations take into account both exploration and exploitation 
+            # samples without duplicates
+            totalScore = np.zeros(finalCandidates.shape[0])
+            #self.totalScore = totalScore
+            
+            for cand_idx in range(finalCandidates.shape[0]):
+                # find candidate indices
+                idx1 = np.where(allCandidates == finalCandidates[cand_idx])[0]
+                idx2 = np.where(candidates == finalCandidates[cand_idx])[0]
+                
+                # exploration 
+                if idx1 != []:
+                    idx1 = idx1[0]
+                    totalScore[cand_idx] += explore_w * scoreExploration[idx1]
+                    
+                # exploitation
+                if idx2 != []:
+                    idx2 = idx2[0]
+                    totalScore[cand_idx] += exploit_w * norm_U_J_d[idx2]
+                
+
+            # Total score
+            totalScore = exploit_w * norm_U_J_d
+            totalScore += explore_w * scoreExploration
+
+            # temp: Plot
+            # dim = self.ExpDesign.X.shape[1]
+            # if dim == 2:
+            #     plotter(self.ExpDesign.X, allCandidates, explore_method)
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            # select the requested number of samples
+            if explore_method == 'Voronoi':
+                Xnew = np.zeros((n_new_samples, ndim))
+                for i, idx in enumerate(bestIdx):
+                    X_can = explore.closestPoints[idx]
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+            else:
+                # Changed this from allCandiates to full set of candidates 
+                # TODO: still not changed for e.g. 'Voronoi'
+                Xnew = finalCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+
+        elif exploit_method == 'VarOptDesign':
+            # ------- EXPLOITATION: VarOptDesign -------
+            UtilMethod = var
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradeoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\nweightExploration={explore_w:0.3f} "
+                  f"weightExploitation={exploit_w:0.3f}")
+
+            # Generate candidate samples from Exploration class
+            nMeasurement = old_EDY[OutputNames[0]].shape[1]
+            
+           # print(UtilMethod)
+            
+            # Find sensitive region
+            if UtilMethod == 'LOOCV':
+                LCerror = self.MetaModel.LCerror
+                allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames),
+                                           nMeasurement))
+                for y_idx, y_key in enumerate(OutputNames):
+                    for idx, key in enumerate(LCerror[y_key].keys()):
+                        allModifiedLOO[:, y_idx, idx] = abs(
+                            LCerror[y_key][key])
+
+                ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1)
+               # print(allModifiedLOO.shape)
+
+            elif UtilMethod in ['EIGF', 'ALM']:
+                # ----- All other in  ['EIGF', 'ALM'] -----
+                # Initilize the ExploitScore array
+                ExploitScore = np.zeros((len(old_EDX), len(OutputNames)))
+
+                # Split the candidates in groups for multiprocessing
+                if explore_method != 'Voronoi':
+                    split_cand = np.array_split(allCandidates,
+                                                n_cand_groups,
+                                                axis=0)
+                    goodSampleIdx = range(n_cand_groups)
+                else:
+                    # Find indices of the Vornoi cells with samples
+                    goodSampleIdx = []
+                    for idx in range(len(explore.closest_points)):
+                        if len(explore.closest_points[idx]) != 0:
+                            goodSampleIdx.append(idx)
+                    split_cand = explore.closest_points
+
+                # Split the candidates in groups for multiprocessing
+                args = []
+                for index in goodSampleIdx:
+                    args.append((exploit_method, split_cand[index], index,
+                                 sigma2, var))
+
+                # Multiprocessing
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.run_util_func, args).get()
+
+                # Close the pool
+                pool.close()
+
+                # Retrieve the results and append them
+                if explore_method == 'Voronoi':
+                    ExploitScore = [np.mean(results[k][1]) for k in
+                                    range(len(goodSampleIdx))]
+                else:
+                    ExploitScore = np.concatenate(
+                        [results[k][1] for k in range(len(goodSampleIdx))])
+
+            else:
+                raise NameError('The requested utility function is not '
+                                'available.')
+
+            # print("ExploitScore:\n", ExploitScore)
+
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            # Total score
+            # Normalize U_J_d
+            ExploitScore = ExploitScore / np.sum(ExploitScore)
+            totalScore = exploit_w * ExploitScore
+           # print(totalScore.shape)
+           # print(explore_w)
+           # print(scoreExploration.shape)
+            totalScore += explore_w * scoreExploration
+
+            temp = totalScore.copy()
+            sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            Xnew = np.zeros((n_new_samples, ndim))
+            if explore_method != 'Voronoi':
+                Xnew = allCandidates[bestIdx]
+            else:
+                for i, idx in enumerate(bestIdx.flatten()):
+                    X_can = explore.closest_points[idx]
+                    # plotter(self.ExpDesign.X, X_can, explore_method,
+                    # scoreExploration=None)
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+
+        elif exploit_method == 'alphabetic':
+            # ------- EXPLOITATION: ALPHABETIC -------
+            Xnew = self.util_AlphOptDesign(allCandidates, var)
+
+        elif exploit_method == 'Space-filling':
+            # ------- EXPLOITATION: SPACE-FILLING -------
+            totalScore = scoreExploration
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+
+            # select the requested number of samples
+            Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        else:
+            raise NameError('The requested design method is not available.')
+
+        print("\n")
+        print("\nRun No. {}:".format(old_EDX.shape[0]+1))
+        print("Xnew:\n", Xnew)
+
+        # TODO: why does it also return None?
+        return Xnew, None
+
+    # -------------------------------------------------------------------------
+    def util_AlphOptDesign(self, candidates, var='D-Opt'):
+        """
+        Enriches the Experimental design with the requested alphabetic
+        criterion based on exploring the space with number of sampling points.
+
+        Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos
+        expansion: A review of sampling strategies., Computer Methods in
+        Applied Mechanics and Engineering, 332, 382-407.
+
+        Arguments
+        ---------
+        NCandidate : int
+            Number of candidate points to be searched
+
+        var : string
+            Alphabetic optimality criterion
+
+        Returns
+        -------
+        X_new : array of shape (1, n_params)
+            The new sampling location in the input space.
+        """
+        MetaModelOrig = self # TODO: this doesn't fully seem correct?
+        n_new_samples = MetaModelOrig.ExpDesign.n_new_samples
+        NCandidate = candidates.shape[0]
+
+        # TODO: Loop over outputs
+        OutputName = self.out_names[0]
+
+        # To avoid changes ub original aPCE object
+        MetaModel = deepcopy(MetaModelOrig)
+
+        # Old Experimental design
+        oldExpDesignX = self.ExpDesign.X
+
+        # TODO: Only one psi can be selected.
+        # Suggestion: Go for the one with the highest LOO error
+        # TODO: this is just a patch, need to look at again!
+        Scores = list(self.MetaModel.score_dict['b_1'][OutputName].values())
+        #print(Scores)
+        #print(self.MetaModel.score_dict)
+        #print(self.MetaModel.score_dict.values())
+        #print(self.MetaModel.score_dict['b_1'].values())
+        #print(self.MetaModel.score_dict['b_1'][OutputName].values())
+        ModifiedLOO = [1-score for score in Scores]
+        outIdx = np.argmax(ModifiedLOO)
+
+        # Initialize Phi to save the criterion's values
+        Phi = np.zeros((NCandidate))
+
+        # TODO: also patched here
+        BasisIndices = self.MetaModel.basis_dict['b_1'][OutputName]["y_"+str(outIdx+1)]
+        P = len(BasisIndices)
+
+        # ------ Old Psi ------------
+        univ_p_val = self.MetaModel.univ_basis_vals(oldExpDesignX)
+        Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val)
+
+        # ------ New candidates (Psi_c) ------------
+        # Assemble Psi_c
+        univ_p_val_c = self.MetaModel.univ_basis_vals(candidates)
+        Psi_c = self.MetaModel.create_psi(BasisIndices, univ_p_val_c)
+
+        for idx in range(NCandidate):
+
+            # Include the new row to the original Psi
+            Psi_cand = np.vstack((Psi, Psi_c[idx]))
+
+            # Information matrix
+            PsiTPsi = np.dot(Psi_cand.T, Psi_cand)
+            M = PsiTPsi / (len(oldExpDesignX)+1)
+
+            if np.linalg.cond(PsiTPsi) > 1e-12 \
+               and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon:
+                # faster
+                invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray())
+            else:
+                # stabler
+                invM = np.linalg.pinv(M)
+
+            # ---------- Calculate optimality criterion ----------
+            # Optimality criteria according to Section 4.5.1 in Ref.
+
+            # D-Opt
+            if var.lower() == 'd-opt':
+                Phi[idx] = (np.linalg.det(invM)) ** (1/P)
+
+            # A-Opt
+            elif var.lower() == 'a-opt':
+                Phi[idx] = np.trace(invM)
+
+            # K-Opt
+            elif var.lower() == 'k-opt':
+                Phi[idx] = np.linalg.cond(M)
+
+            else:
+               # print(var.lower())
+                raise Exception('The optimality criterion you requested has '
+                      'not been implemented yet!')
+
+        # find an optimal point subset to add to the initial design
+        # by minimization of the Phi
+        sorted_idxtotalScore = np.argsort(Phi)
+
+        # select the requested number of samples
+        Xnew = candidates[sorted_idxtotalScore[:n_new_samples]]
+
+        return Xnew
+
+    # -------------------------------------------------------------------------
+    def _normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s,
+                  rmse=None):
+        """
+        Calculated gaussian likelihood for given y+std based on given obs+sigma
+        # TODO: is this understanding correct?
+        
+        Parameters
+        ----------
+        y_hat_pce : dict of 2d np arrays
+            Mean output of the surrogate.
+        std_pce : dict of 2d np arrays
+            Standard deviation output of the surrogate.
+        obs_data : dict of 1d np arrays
+            Observed data.
+        total_sigma2s : pandas dataframe, matches obs_data
+            Estimated uncertainty for the observed data.
+        rmse : dict, optional
+            RMSE values from validation of the surrogate. The default is None.
+
+        Returns
+        -------
+        likelihoods : dict of float
+            The likelihood for each surrogate eval in y_hat_pce compared to the
+            observations (?).
+
+        """
+
+        likelihoods = 1.0
+
+        # Loop over the outputs
+        for idx, out in enumerate(self.out_names):
+
+            # (Meta)Model Output
+           # print(y_hat_pce[out])
+            nsamples, nout = y_hat_pce[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values
+
+            # Surrogate error if valid dataset is given.
+            if rmse is not None:
+                tot_sigma2s += rmse[out]**2
+            else:
+                tot_sigma2s += np.mean(std_pce[out])**2
+
+            likelihoods *= stats.multivariate_normal.pdf(
+                y_hat_pce[out], data, np.diag(tot_sigma2s),
+                allow_singular=True)
+
+        # TODO: remove this here
+        self.Likelihoods = likelihoods
+
+        return likelihoods
+
+    # -------------------------------------------------------------------------
+    def _corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = self.ExpDesign.X  # valid_samples
+        model_outputs = self.ExpDesign.Y  # valid_model_runs
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = self.out_names
+
+        # TODO: Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                # covMatrix = np.diag(stdOutputRS[out][i]**2)
+                covMatrix = np.diag((y_m-y_m_hat)**2)
+                covMatrix = np.diag(
+                    np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2
+                    )
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += logpdf(
+                    y_m_hat, data, covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += logpdf(y_m_hat, y_m, covMatrix)
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.exp(logLik_model+logLik_data)
+
+        return np.log(np.mean(weights))
+
+    # -------------------------------------------------------------------------
+    def _posteriorPlot(self, posterior, par_names, key):
+        """
+        Plot the posterior of a specific key as a corner plot
+
+        Parameters
+        ----------
+        posterior : 2d np.array
+            Samples of the posterior.
+        par_names : list of strings
+            List of the parameter names.
+        key : string
+            Output key that this posterior belongs to.
+
+        Returns
+        -------
+        figPosterior : corner.corner
+            Plot of the posterior.
+
+        """
+
+        # Initialization
+        newpath = (r'Outputs_SeqPosteriorComparison/posterior')
+        os.makedirs(newpath, exist_ok=True)
+
+        bound_tuples = self.ExpDesign.bound_tuples
+        n_params = len(par_names)
+        font_size = 40
+        if n_params == 2:
+
+            figPosterior, ax = plt.subplots(figsize=(15, 15))
+
+            sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1],
+                        fill=True, ax=ax, cmap=plt.cm.jet,
+                        clip=bound_tuples)
+            # Axis labels
+            plt.xlabel(par_names[0], fontsize=font_size)
+            plt.ylabel(par_names[1], fontsize=font_size)
+
+            # Set axis limit
+            plt.xlim(bound_tuples[0])
+            plt.ylim(bound_tuples[1])
+
+            # Increase font size
+            plt.xticks(fontsize=font_size)
+            plt.yticks(fontsize=font_size)
+
+            # Switch off the grids
+            plt.grid(False)
+
+        else:
+            import corner
+            figPosterior = corner.corner(posterior, labels=par_names,
+                                         title_fmt='.2e', show_titles=True,
+                                         title_kwargs={"fontsize": 12})
+
+        figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight')
+        plt.close()
+
+        # Save the posterior as .npy
+        np.save(f'./{newpath}/{key}.npy', posterior)
+
+        return figPosterior
+
+    
+    # -------------------------------------------------------------------------
+    def _BME_Calculator(self, obs_data, sigma2Dict, rmse=None):
+        """
+        This function computes the Bayesian model evidence (BME) via Monte
+        Carlo integration.
+
+        Parameters
+        ----------
+        obs_data : dict of 1d np arrays
+            Observed data.
+        sigma2Dict : pandas dataframe, matches obs_data
+            Estimated uncertainty for the observed data.
+        rmse : dict of floats, optional
+            RMSE values for each output-key. The dafault is None.
+
+        Returns
+        -------
+        (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+        
+        """
+        # Initializations
+        if hasattr(self, 'valid_likelihoods'):
+            valid_likelihoods = self.valid_likelihoods
+        else:
+            valid_likelihoods = []
+        valid_likelihoods = np.array(valid_likelihoods)
+
+        post_snapshot = self.ExpDesign.post_snapshot
+        if post_snapshot or valid_likelihoods.shape[0] != 0:
+            newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref')
+            os.makedirs(newpath, exist_ok=True)
+
+        SamplingMethod = 'random'
+        MCsize = 10000
+        ESS = 0
+
+        # Estimation of the integral via Monte Varlo integration
+        while (ESS > MCsize) or (ESS < 1):
+
+            # Generate samples for Monte Carlo simulation
+            X_MC = self.ExpDesign.generate_samples(
+                MCsize, SamplingMethod
+                )
+
+            # Monte Carlo simulation for the candidate design
+            Y_MC, std_MC = self.MetaModel.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and
+            # simulation results via PCE with candidate design)
+            Likelihoods = self._normpdf(
+                Y_MC, std_MC, obs_data, sigma2Dict, rmse
+                )
+
+            # Check the Effective Sample Size (1000<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if (ESS > MCsize) or (ESS < 1):
+                print(f'ESS={ESS} MC size should be larger.')
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+        X_Posterior = X_MC[accepted]
+
+        # ------------------------------------------------------------
+        # --- Kullback-Leibler Divergence & Information Entropy ------
+        # ------------------------------------------------------------
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(Likelihoods))
+
+        # TODO: Correction factor
+        # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME)
+
+        # Posterior-based expectation of likelihoods
+        postExpLikelihoods = np.mean(np.log(Likelihoods[accepted]))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(
+            np.log(self.ExpDesign.JDist.pdf(X_Posterior.T))
+            )
+
+        # Calculate Kullback-Leibler Divergence
+        # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+        KLD = postExpLikelihoods - logBME
+
+        # Information Entropy based on Entropy paper Eq. 38
+        infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+        # If post_snapshot is True, plot likelihood vs refrence
+        if post_snapshot or valid_likelihoods:
+            # Hellinger distance
+            valid_likelihoods = np.array(valid_likelihoods)
+            ref_like = np.log(valid_likelihoods[(valid_likelihoods > 0)])
+            est_like = np.log(Likelihoods[Likelihoods > 0])
+            distHellinger = hellinger_distance(ref_like, est_like)
+            
+            idx = len([name for name in os.listdir(newpath) if 'Likelihoods_'
+                       in name and os.path.isfile(os.path.join(newpath, name))])
+            
+            fig, ax = plt.subplots()
+            try:
+                sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]),
+                            shade=True, color="g", label='Ref. Likelihood')
+                sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True,
+                            color="b", label='Likelihood with PCE')
+            except:
+                pass
+
+            text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}"
+            "\n DKL={KLD:.3f}"
+
+            plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat',
+                                                 edgecolor='black',
+                                                 boxstyle='round,pad=1'),
+                     transform=ax.transAxes)
+
+            fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf',
+                        bbox_inches='tight')
+            plt.close()
+
+        else:
+            distHellinger = 0.0
+
+        # Bayesian inference with Emulator only for 2D problem
+        if post_snapshot and self.MetaModel.n_params == 2 and not idx % 5:
+            BayesOpts = BayesInference(self)
+
+            BayesOpts.emulator = True
+            BayesOpts.plot_post_pred = False
+
+            # Select the inference method
+            import emcee
+            BayesOpts.inference_method = "MCMC"
+            # Set the MCMC parameters passed to self.mcmc_params
+            BayesOpts.mcmc_params = {
+                'n_steps': 1e5,
+                'n_walkers': 30,
+                'moves': emcee.moves.KDEMove(),
+                'verbose': False
+                }
+
+            # ----- Define the discrepancy model -------
+            # TODO: check with Farid if this first line is how it should be
+            BayesOpts.measured_data = obs_data
+            obs_data = pd.DataFrame(obs_data, columns=self.out_names)
+            BayesOpts.measurement_error = obs_data
+            # TODO: shouldn't the uncertainty be sigma2Dict instead of obs_data?
+
+            # # -- (Option B) --
+            DiscrepancyOpts = Discrepancy('')
+            DiscrepancyOpts.type = 'Gaussian'
+            DiscrepancyOpts.parameters = obs_data**2
+            BayesOpts.Discrepancy = DiscrepancyOpts
+            # Start the calibration/inference
+            Bayes_PCE = BayesOpts.create_inference()
+            X_Posterior = Bayes_PCE.posterior_df.values
+
+        return (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+
+    # -------------------------------------------------------------------------
+    def _validError(self):
+        """
+        Evaluate the metamodel on the validation samples and calculate the
+        error against the corresponding model runs
+
+        Returns
+        -------
+        rms_error : dict
+            RMSE for each validation run.
+        valid_error : dict
+            Normed (?)RMSE for each validation run.
+
+        """
+        # Extract the original model with the generated samples
+        valid_model_runs = self.ExpDesign.valid_model_runs
+
+        # Run the PCE model with the generated samples
+        valid_PCE_runs, _ = self.MetaModel.eval_metamodel(samples=self.ExpDesign.valid_samples)
+
+        rms_error = {}
+        valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in self.out_names:
+            rms_error[key] = mean_squared_error(
+                valid_model_runs[key], valid_PCE_runs[key],
+                multioutput='raw_values',
+                sample_weight=None,
+                squared=False)
+            # Validation error
+            valid_error[key] = (rms_error[key]**2)
+            valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Updated Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(rms_error[key],
+                                             valid_error[key]))))
+
+        return rms_error, valid_error
+
+    # -------------------------------------------------------------------------
+    def _error_Mean_Std(self):
+        """
+        Calculates the error in the overall mean and std approximation of the
+        surrogate against the mc-reference provided to the model.
+        This can only be applied to metamodels of polynomial type
+
+        Returns
+        -------
+        RMSE_Mean : float
+            RMSE of the means 
+        RMSE_std : float
+            RMSE of the standard deviations
+
+        """
+        # Compute the mean and std based on the MetaModel
+        pce_means, pce_stds = self.MetaModel._compute_pce_moments()
+
+        # Compute the root mean squared error
+        for output in self.out_names:
+
+            # Compute the error between mean and std of MetaModel and OrigModel
+            RMSE_Mean = mean_squared_error(
+                self.Model.mc_reference['mean'], pce_means[output], squared=False
+                )
+            RMSE_std = mean_squared_error(
+                self.Model.mc_reference['std'], pce_stds[output], squared=False
+                )
+
+        return RMSE_Mean, RMSE_std
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/eval_rec_rule.py b/examples/model-comparison/bayesvalidrox/surrogate_models/eval_rec_rule.py
new file mode 100644
index 0000000000000000000000000000000000000000..b583c7eb2ec58d55d19b34130812730d21a12368
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/eval_rec_rule.py
@@ -0,0 +1,197 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+
+
+Based on the implementation in UQLab [1].
+
+References:
+1. S. Marelli, and B. Sudret, UQLab: A framework for uncertainty quantification
+in Matlab, Proc. 2nd Int. Conf. on Vulnerability, Risk Analysis and Management
+(ICVRAM2014), Liverpool, United Kingdom, 2014, 2554-2563.
+
+2. S. Marelli, N. Lüthen, B. Sudret, UQLab user manual – Polynomial chaos
+expansions, Report # UQLab-V1.4-104, Chair of Risk, Safety and Uncertainty
+Quantification, ETH Zurich, Switzerland, 2021.
+
+Author: Farid Mohammadi, M.Sc.
+E-Mail: farid.mohammadi@iws.uni-stuttgart.de
+Department of Hydromechanics and Modelling of Hydrosystems (LH2)
+Institute for Modelling Hydraulic and Environmental Systems (IWS), University
+of Stuttgart, www.iws.uni-stuttgart.de/lh2/
+Pfaffenwaldring 61
+70569 Stuttgart
+
+Created on Fri Jan 14 2022
+"""
+import numpy as np
+from numpy.polynomial.polynomial import polyval
+
+
+def poly_rec_coeffs(n_max, poly_type, params=None):
+    """
+    Computes the recurrence coefficients for classical Wiener-Askey orthogonal
+    polynomials.
+
+    Parameters
+    ----------
+    n_max : int
+        Maximum polynomial degree.
+    poly_type : string
+        Polynomial type.
+    params : list, optional
+        Parameters required for `laguerre` poly type. The default is None.
+
+    Returns
+    -------
+    AB : dict
+        The 3 term recursive coefficients and the applicable ranges.
+
+    """
+
+    if poly_type == 'legendre':
+
+        def an(n):
+            return np.zeros((n+1, 1))
+
+        def sqrt_bn(n):
+            sq_bn = np.zeros((n+1, 1))
+            sq_bn[0, 0] = 1
+            for i in range(1, n+1):
+                sq_bn[i, 0] = np.sqrt(1./(4-i**-2))
+            return sq_bn
+
+        bounds = [-1, 1]
+
+    elif poly_type == 'hermite':
+
+        def an(n):
+            return np.zeros((n+1, 1))
+
+        def sqrt_bn(n):
+            sq_bn = np.zeros((n+1, 1))
+            sq_bn[0, 0] = 1
+            for i in range(1, n+1):
+                sq_bn[i, 0] = np.sqrt(i)
+            return sq_bn
+
+        bounds = [-np.inf, np.inf]
+
+    elif poly_type == 'laguerre':
+
+        def an(n):
+            a = np.zeros((n+1, 1))
+            for i in range(1, n+1):
+                a[i] = 2*n + params[1]
+            return a
+
+        def sqrt_bn(n):
+            sq_bn = np.zeros((n+1, 1))
+            sq_bn[0, 0] = 1
+            for i in range(1, n+1):
+                sq_bn[i, 0] = -np.sqrt(i * (i+params[1]-1))
+            return sq_bn
+
+        bounds = [0, np.inf]
+
+    AB = {'alpha_beta': np.concatenate((an(n_max), sqrt_bn(n_max)), axis=1),
+          'bounds': bounds}
+
+    return AB
+
+
+def eval_rec_rule(x, max_deg, poly_type):
+    """
+    Evaluates the polynomial that corresponds to the Jacobi matrix defined
+    from the AB.
+
+    Parameters
+    ----------
+    x : array (n_samples)
+        Points where the polynomials are evaluated.
+    max_deg : int
+        Maximum degree.
+    poly_type : string
+        Polynomial type.
+
+    Returns
+    -------
+    values : array of shape (n_samples, max_deg+1)
+        Polynomials corresponding to the Jacobi matrix.
+
+    """
+    AB = poly_rec_coeffs(max_deg, poly_type)
+    AB = AB['alpha_beta']
+
+    values = np.zeros((len(x), AB.shape[0]+1))
+    values[:, 1] = 1 / AB[0, 1]
+
+    for k in range(AB.shape[0]-1):
+        values[:, k+2] = np.multiply((x - AB[k, 0]), values[:, k+1]) - \
+                         np.multiply(values[:, k], AB[k, 1])
+        values[:, k+2] = np.divide(values[:, k+2], AB[k+1, 1])
+    return values[:, 1:]
+
+
+def eval_rec_rule_arbitrary(x, max_deg, poly_coeffs):
+    """
+    Evaluates the polynomial at sample array x.
+
+    Parameters
+    ----------
+    x : array (n_samples)
+        Points where the polynomials are evaluated.
+    max_deg : int
+        Maximum degree.
+    poly_coeffs : dict
+        Polynomial coefficients computed based on moments.
+
+    Returns
+    -------
+    values : array of shape (n_samples, max_deg+1)
+        Univariate Polynomials evaluated at samples.
+
+    """
+    values = np.zeros((len(x), max_deg+1))
+
+    for deg in range(max_deg+1):
+        values[:, deg] = polyval(x, poly_coeffs[deg]).T
+
+    return values
+
+
+def eval_univ_basis(x, max_deg, poly_types, apoly_coeffs=None):
+    """
+    Evaluates univariate regressors along input directions.
+
+    Parameters
+    ----------
+    x : array of shape (n_samples, n_params)
+        Training samples.
+    max_deg : int
+        Maximum polynomial degree.
+    poly_types : list of strings
+        List of polynomial types for all parameters.
+    apoly_coeffs : dict , optional
+        Polynomial coefficients computed based on moments. The default is None.
+
+    Returns
+    -------
+    univ_vals : array of shape (n_samples, n_params, max_deg+1)
+        Univariate polynomials for all degrees and parameters evaluated at x.
+
+    """
+    # Initilize the output array
+    n_samples, n_params = x.shape
+    univ_vals = np.zeros((n_samples, n_params, max_deg+1))
+
+    for i in range(n_params):
+
+        if poly_types[i] == 'arbitrary':
+            polycoeffs = apoly_coeffs[f'p_{i+1}']
+            univ_vals[:, i] = eval_rec_rule_arbitrary(x[:, i], max_deg,
+                                                      polycoeffs)
+        else:
+            univ_vals[:, i] = eval_rec_rule(x[:, i], max_deg, poly_types[i])
+
+    return univ_vals
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/exp_designs.py b/examples/model-comparison/bayesvalidrox/surrogate_models/exp_designs.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa03fe17d96fb2c1f19546b7b72fb2fd6dd1c13a
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/exp_designs.py
@@ -0,0 +1,479 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Experimental design with associated sampling methods
+"""
+
+import numpy as np
+import math
+import itertools
+import chaospy
+import scipy.stats as st
+from tqdm import tqdm
+import h5py
+import os
+
+from .apoly_construction import apoly_construction
+from .input_space import InputSpace
+
+# -------------------------------------------------------------------------
+def check_ranges(theta, ranges):
+    """
+    This function checks if theta lies in the given ranges.
+
+    Parameters
+    ----------
+    theta : array
+        Proposed parameter set.
+    ranges : nested list
+        List of the praremeter ranges.
+
+    Returns
+    -------
+    c : bool
+        If it lies in the given range, it return True else False.
+
+    """
+    c = True
+    # traverse in the list1
+    for i, bounds in enumerate(ranges):
+        x = theta[i]
+        # condition check
+        if x < bounds[0] or x > bounds[1]:
+            c = False
+            return c
+    return c
+
+
+class ExpDesigns(InputSpace):
+    """
+    This class generates samples from the prescribed marginals for the model
+    parameters using the `Input` object.
+
+    Attributes
+    ----------
+    Input : obj
+        Input object containing the parameter marginals, i.e. name,
+        distribution type and distribution parameters or available raw data.
+    meta_Model_type : str
+        Type of the meta_Model_type.
+    sampling_method : str
+        Name of the sampling method for the experimental design. The following
+        sampling method are supported:
+
+        * random
+        * latin_hypercube
+        * sobol
+        * halton
+        * hammersley
+        * chebyshev(FT)
+        * grid(FT)
+        * user
+    hdf5_file : str
+        Name of the hdf5 file that contains the experimental design.
+    n_new_samples : int
+        Number of (initial) training points.
+    n_max_samples : int
+        Number of maximum training points.
+    mod_LOO_threshold : float
+        The modified leave-one-out cross validation threshold where the
+        sequential design stops.
+    tradeoff_scheme : str
+        Trade-off scheme to assign weights to the exploration and exploitation
+        scores in the sequential design.
+    n_canddidate : int
+        Number of candidate training sets to calculate the scores for.
+    explore_method : str
+        Type of the exploration method for the sequential design. The following
+        methods are supported:
+
+        * Voronoi
+        * random
+        * latin_hypercube
+        * LOOCV
+        * dual annealing
+    exploit_method : str
+        Type of the exploitation method for the sequential design. The
+        following methods are supported:
+
+        * BayesOptDesign
+        * BayesActDesign
+        * VarOptDesign
+        * alphabetic
+        * Space-filling
+    util_func : str or list
+        The utility function to be specified for the `exploit_method`. For the
+        available utility functions see Note section.
+    n_cand_groups : int
+        Number of candidate groups. Each group of candidate training sets will
+        be evaulated separately in parallel.
+    n_replication : int
+        Number of replications. Only for comparison. The default is 1.
+    post_snapshot : int
+        Whether to plot the posterior in the sequential design. The default is
+        `True`.
+    step_snapshot : int
+        The number of steps to plot the posterior in the sequential design. The
+        default is 1.
+    max_a_post : list or array
+        Maximum a posteriori of the posterior distribution, if known. The
+        default is `[]`.
+    adapt_verbose : bool
+        Whether to plot the model response vs that of metamodel for the new
+        trining point in the sequential design.
+
+    Note
+    ----------
+    The following utiliy functions for the **exploitation** methods are
+    supported:
+
+    #### BayesOptDesign (when data is available)
+    - DKL (Kullback-Leibler Divergence)
+    - DPP (D-Posterior-percision)
+    - APP (A-Posterior-percision)
+
+    #### VarBasedOptDesign -> when data is not available
+    - Entropy (Entropy/MMSE/active learning)
+    - EIGF (Expected Improvement for Global fit)
+    - LOOCV (Leave-one-out Cross Validation)
+
+    #### alphabetic
+    - D-Opt (D-Optimality)
+    - A-Opt (A-Optimality)
+    - K-Opt (K-Optimality)
+    """
+
+    def __init__(self, Input, meta_Model_type='pce',
+                 sampling_method='random', hdf5_file=None,
+                 n_new_samples=1, n_max_samples=None, mod_LOO_threshold=1e-16,
+                 tradeoff_scheme=None, n_canddidate=1, explore_method='random',
+                 exploit_method='Space-filling', util_func='Space-filling',
+                 n_cand_groups=4, n_replication=1, post_snapshot=False,
+                 step_snapshot=1, max_a_post=[], adapt_verbose=False, max_func_itr=1):
+
+        self.InputObj = Input
+        self.meta_Model_type = meta_Model_type
+        self.sampling_method = sampling_method
+        self.hdf5_file = hdf5_file
+        self.n_new_samples = n_new_samples
+        self.n_max_samples = n_max_samples
+        self.mod_LOO_threshold = mod_LOO_threshold
+        self.explore_method = explore_method
+        self.exploit_method = exploit_method
+        self.util_func = util_func
+        self.tradeoff_scheme = tradeoff_scheme
+        self.n_canddidate = n_canddidate
+        self.n_cand_groups = n_cand_groups
+        self.n_replication = n_replication
+        self.post_snapshot = post_snapshot
+        self.step_snapshot = step_snapshot
+        self.max_a_post = max_a_post
+        self.adapt_verbose = adapt_verbose
+        self.max_func_itr = max_func_itr
+        
+        # Other 
+        self.apce = None
+        self.ndim = None
+        
+        # Init 
+        self.check_valid_inputs()
+        
+    # -------------------------------------------------------------------------
+    def generate_samples(self, n_samples, sampling_method='random',
+                         transform=False):
+        """
+        Generates samples with given sampling method
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested samples.
+        sampling_method : str, optional
+            Sampling method. The default is `'random'`.
+        transform : bool, optional
+            Transformation via an isoprobabilistic transformation method. The
+            default is `False`.
+
+        Returns
+        -------
+        samples: array of shape (n_samples, n_params)
+            Generated samples from defined model input object.
+
+        """
+        try:
+            samples = chaospy.generate_samples(
+                int(n_samples), domain=self.origJDist, rule=sampling_method
+                )
+        except:
+            samples = self.random_sampler(int(n_samples)).T
+
+        return samples.T
+
+
+            
+    # -------------------------------------------------------------------------
+    def generate_ED(self, n_samples, transform=False,
+                    max_pce_deg=None):
+        """
+        Generates experimental designs (training set) with the given method.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested training points.
+        sampling_method : str, optional
+            Sampling method. The default is `'random'`.
+        transform : bool, optional
+            Isoprobabilistic transformation. The default is `False`.
+        max_pce_deg : int, optional
+            Maximum PCE polynomial degree. The default is `None`.
+            
+        Returns
+        -------
+        None
+
+        """
+        if n_samples <0:
+            raise ValueError('A negative number of samples cannot be created. Please provide positive n_samples')
+        n_samples = int(n_samples)
+        
+        if not hasattr(self, 'n_init_samples'):
+            self.n_init_samples = n_samples
+
+        # Generate the samples based on requested method
+        self.init_param_space(max_pce_deg)
+
+        sampling_method = self.sampling_method
+        # Pass user-defined samples as ED
+        if sampling_method == 'user':
+            if not hasattr(self, 'X'):
+                raise AttributeError('User-defined sampling cannot proceed as no samples provided. Please add them to this class as attribute X')
+            if not self.X.ndim == 2:
+                raise AttributeError('The provided samples shuld have 2 dimensions')
+            samples = self.X
+            self.n_samples = len(samples)
+
+        # Sample the distribution of parameters
+        elif self.input_data_given:
+            # Case II: Input values are directly given by the user.
+
+            if sampling_method == 'random':
+                samples = self.random_sampler(n_samples)
+
+            elif sampling_method == 'PCM' or \
+                    sampling_method == 'LSCM':
+                samples = self.pcm_sampler(n_samples, max_pce_deg)
+
+            else:
+                # Create ExpDesign in the actual space using chaospy
+                try:
+                    samples = chaospy.generate_samples(n_samples,
+                                                       domain=self.JDist,
+                                                       rule=sampling_method).T
+                except:
+                    samples = self.JDist.resample(n_samples).T
+
+        elif not self.input_data_given:
+            # Case I = User passed known distributions
+            samples = chaospy.generate_samples(n_samples, domain=self.JDist,
+                                               rule=sampling_method).T
+
+        self.X = samples
+            
+    def read_from_file(self, out_names):
+        """
+        Reads in the ExpDesign from a provided h5py file and saves the results.
+
+        Parameters
+        ----------
+        out_names : list of strings
+            The keys that are in the outputs (y) saved in the provided file.
+
+        Returns
+        -------
+        None.
+
+        """
+        if self.hdf5_file == None:
+            raise AttributeError('ExpDesign cannot be read in, please provide hdf5 file first')
+
+        # Read hdf5 file
+        f = h5py.File(self.hdf5_file, 'r+')
+
+        # Read EDX and pass it to ExpDesign object
+        try:
+            self.X = np.array(f["EDX/New_init_"])
+        except KeyError:
+            self.X = np.array(f["EDX/init_"])
+
+        # Update number of initial samples
+        self.n_init_samples = self.X.shape[0]
+
+        # Read EDX and pass it to ExpDesign object
+        self.Y = {}
+
+        # Extract x values
+        try:
+            self.Y["x_values"] = dict()
+            for varIdx, var in enumerate(out_names):
+                x = np.array(f[f"x_values/{var}"])
+                self.Y["x_values"][var] = x
+        except KeyError:
+            self.Y["x_values"] = np.array(f["x_values"])
+
+        # Store the output
+        for varIdx, var in enumerate(out_names):
+            try:
+                y = np.array(f[f"EDY/{var}/New_init_"])
+            except KeyError:
+                y = np.array(f[f"EDY/{var}/init_"])
+            self.Y[var] = y
+        f.close()
+        print(f'Experimental Design is read in from file {self.hdf5_file}')
+        print('')
+        
+    
+
+    # -------------------------------------------------------------------------
+    def random_sampler(self, n_samples, max_deg = None):
+        """
+        Samples the given raw data randomly.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested samples.
+            
+        max_deg : int, optional
+            Maximum degree. The default is `None`.
+            This will be used to run init_param_space, if it has not been done
+            until now.
+
+        Returns
+        -------
+        samples: array of shape (n_samples, n_params)
+            The sampling locations in the input space.
+
+        """
+        if not hasattr(self, 'raw_data'):
+            self.init_param_space(max_deg)
+        else:
+            if np.array(self.raw_data).ndim !=2:
+                raise AttributeError('The given raw data for sampling should have two dimensions')
+        samples = np.zeros((n_samples, self.ndim))
+        sample_size = self.raw_data.shape[1]
+
+        # Use a combination of raw data
+        if n_samples < sample_size:
+            for pa_idx in range(self.ndim):
+                # draw random indices
+                rand_idx = np.random.randint(0, sample_size, n_samples)
+                # store the raw data with given random indices
+                samples[:, pa_idx] = self.raw_data[pa_idx, rand_idx]
+        else:
+            try:
+                samples = self.JDist.resample(int(n_samples)).T
+            except AttributeError:
+                samples = self.JDist.sample(int(n_samples)).T
+            # Check if all samples are in the bound_tuples
+            for idx, param_set in enumerate(samples):
+                if not check_ranges(param_set, self.bound_tuples):
+                    try:
+                        proposed_sample = chaospy.generate_samples(
+                            1, domain=self.JDist, rule='random').T[0]
+                    except:
+                        proposed_sample = self.JDist.resample(1).T[0]
+                    while not check_ranges(proposed_sample,
+                                                 self.bound_tuples):
+                        try:
+                            proposed_sample = chaospy.generate_samples(
+                                1, domain=self.JDist, rule='random').T[0]
+                        except:
+                            proposed_sample = self.JDist.resample(1).T[0]
+                    samples[idx] = proposed_sample
+
+        return samples
+
+    # -------------------------------------------------------------------------
+    def pcm_sampler(self, n_samples, max_deg):
+        """
+        Generates collocation points based on the root of the polynomial
+        degrees.
+
+        Parameters
+        ----------
+        n_samples : int
+            Number of requested samples.
+        max_deg : int
+            Maximum degree defined by user. Will also be used to run 
+            init_param_space if that has not been done beforehand.
+
+        Returns
+        -------
+        opt_col_points: array of shape (n_samples, n_params)
+            Collocation points.
+
+        """
+        
+        if not hasattr(self, 'raw_data'):
+            self.init_param_space(max_deg)
+
+        raw_data = self.raw_data
+
+        # Guess the closest degree to self.n_samples
+        def M_uptoMax(deg):
+            result = []
+            for d in range(1, deg+1):
+                result.append(math.factorial(self.ndim+d) //
+                              (math.factorial(self.ndim) * math.factorial(d)))
+            return np.array(result)
+        #print(M_uptoMax(max_deg))
+        #print(np.where(M_uptoMax(max_deg) > n_samples)[0])
+
+        guess_Deg = np.where(M_uptoMax(max_deg) > n_samples)[0][0]
+
+        c_points = np.zeros((guess_Deg+1, self.ndim))
+
+        def PolynomialPa(parIdx):
+            return apoly_construction(self.raw_data[parIdx], max_deg)
+
+        for i in range(self.ndim):
+            poly_coeffs = PolynomialPa(i)[guess_Deg+1][::-1]
+            c_points[:, i] = np.trim_zeros(np.roots(poly_coeffs))
+
+        #  Construction of optimal integration points
+        Prod = itertools.product(np.arange(1, guess_Deg+2), repeat=self.ndim)
+        sort_dig_unique_combos = np.array(list(filter(lambda x: x, Prod)))
+
+        # Ranking relatively mean
+        Temp = np.empty(shape=[0, guess_Deg+1])
+        for j in range(self.ndim):
+            s = abs(c_points[:, j]-np.mean(raw_data[j]))
+            Temp = np.append(Temp, [s], axis=0)
+        temp = Temp.T
+
+        index_CP = np.sort(temp, axis=0)
+        sort_cpoints = np.empty((0, guess_Deg+1))
+
+        for j in range(self.ndim):
+            #print(index_CP[:, j])
+            sort_cp = c_points[index_CP[:, j], j]
+            sort_cpoints = np.vstack((sort_cpoints, sort_cp))
+
+        # Mapping of Combination to Cpoint Combination
+        sort_unique_combos = np.empty(shape=[0, self.ndim])
+        for i in range(len(sort_dig_unique_combos)):
+            sort_un_comb = []
+            for j in range(self.ndim):
+                SortUC = sort_cpoints[j, sort_dig_unique_combos[i, j]-1]
+                sort_un_comb.append(SortUC)
+                sort_uni_comb = np.asarray(sort_un_comb)
+            sort_unique_combos = np.vstack((sort_unique_combos, sort_uni_comb))
+
+        # Output the collocation points
+        if self.sampling_method.lower() == 'lscm':
+            opt_col_points = sort_unique_combos
+        else:
+            opt_col_points = sort_unique_combos[0:self.n_samples]
+
+        return opt_col_points
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/exploration.py b/examples/model-comparison/bayesvalidrox/surrogate_models/exploration.py
new file mode 100644
index 0000000000000000000000000000000000000000..6abb652f145fadb410ecf8f987142e8ceb544a41
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/exploration.py
@@ -0,0 +1,367 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Exploration for sequential training of metamodels
+"""
+
+import numpy as np
+from scipy.spatial import distance
+
+
+class Exploration:
+    """
+    Created based on the Surrogate Modeling Toolbox (SUMO) [1].
+
+    [1] Gorissen, D., Couckuyt, I., Demeester, P., Dhaene, T. and Crombecq, K.,
+        2010. A surrogate modeling and adaptive sampling toolbox for computer
+        based design. Journal of machine learning research.-Cambridge, Mass.,
+        11, pp.2051-2055. sumo@sumo.intec.ugent.be - http://sumo.intec.ugent.be
+
+    Attributes
+    ----------
+    ExpDesign : obj
+        ExpDesign object.
+    n_candidate : int
+        Number of candidate samples.
+    mc_criterion : str
+        Selection crieterion. The default is `'mc-intersite-proj-th'`. Another
+        option is `'mc-intersite-proj'`.
+    w : int
+        Number of random points in the domain for each sample of the
+        training set.
+    """
+
+    def __init__(self, ExpDesign, n_candidate,
+                 mc_criterion='mc-intersite-proj-th'):
+        self.ExpDesign = ExpDesign
+        self.n_candidate = n_candidate
+        self.mc_criterion = mc_criterion
+        self.w = 100
+
+    def get_exploration_samples(self):
+        """
+        This function generates candidates to be selected as new design and
+        their associated exploration scores.
+
+        Returns
+        -------
+        all_candidates : array of shape (n_candidate, n_params)
+            A list of samples.
+        exploration_scores: arrays of shape (n_candidate)
+            Exploration scores.
+        """
+        explore_method = self.ExpDesign.explore_method
+
+        print("\n")
+        print(f' The {explore_method}-Method is selected as the exploration '
+              'method.')
+        print("\n")
+
+        if explore_method == 'Voronoi':
+            # Generate samples using the Voronoi method
+            all_candidates, exploration_scores = self.get_vornoi_samples()
+        else:
+            # Generate samples using the MC method
+            all_candidates, exploration_scores = self.get_mc_samples()
+
+        return all_candidates, exploration_scores
+
+    # -------------------------------------------------------------------------
+    def get_vornoi_samples(self):
+        """
+        This function generates samples based on voronoi cells and their
+        corresponding scores
+
+        Returns
+        -------
+        new_samples : array of shape (n_candidate, n_params)
+            A list of samples.
+        exploration_scores: arrays of shape (n_candidate)
+            Exploration scores.
+        """
+
+        mc_criterion = self.mc_criterion
+        n_candidate = self.n_candidate
+        # Get the Old ExpDesign #samples
+        old_ED_X = self.ExpDesign.X
+        ndim = old_ED_X.shape[1]
+
+        # calculate error #averageErrors
+        error_voronoi, all_candidates = self.approximate_voronoi(
+            self.w, old_ED_X
+            )
+
+        # Pick the best candidate point in the voronoi cell
+        # for each best sample
+        selected_samples = np.empty((0, ndim))
+        bad_samples = []
+
+        for index in range(len(error_voronoi)):
+
+            # get candidate new samples from voronoi tesselation
+            candidates = self.closest_points[index]
+
+            # get total number of candidates
+            n_new_samples = candidates.shape[0]
+
+            # still no candidate samples around this one, skip it!
+            if n_new_samples == 0:
+                print('The following sample has been skipped because there '
+                      'were no candidate samples around it...')
+                print(old_ED_X[index])
+                bad_samples.append(index)
+                continue
+
+            # find candidate that is farthest away from any existing sample
+            max_min_distance = 0
+            best_candidate = 0
+            min_intersite_dist = np.zeros((n_new_samples))
+            min_projected_dist = np.zeros((n_new_samples))
+
+            for j in range(n_new_samples):
+
+                new_samples = np.vstack((old_ED_X, selected_samples))
+
+                # find min distorted distance from all other samples
+                euclidean_dist = self._build_dist_matrix_point(
+                    new_samples, candidates[j], do_sqrt=True)
+                min_euclidean_dist = np.min(euclidean_dist)
+                min_intersite_dist[j] = min_euclidean_dist
+
+                # Check if this is the maximum minimum distance from all other
+                # samples
+                if min_euclidean_dist >= max_min_distance:
+                    max_min_distance = min_euclidean_dist
+                    best_candidate = j
+
+                # Projected distance
+                projected_dist = distance.cdist(
+                    new_samples, [candidates[j]], 'chebyshev')
+                min_projected_dist[j] = np.min(projected_dist)
+
+            if mc_criterion == 'mc-intersite-proj':
+                weight_euclidean_dist = 0.5 * ((n_new_samples+1)**(1/ndim) - 1)
+                weight_projected_dist = 0.5 * (n_new_samples+1)
+                total_dist_scores = weight_euclidean_dist * min_intersite_dist
+                total_dist_scores += weight_projected_dist * min_projected_dist
+
+            elif mc_criterion == 'mc-intersite-proj-th':
+                alpha = 0.5  # chosen (tradeoff)
+                d_min = 2 * alpha / n_new_samples
+                if any(min_projected_dist < d_min):
+                    candidates = np.delete(
+                        candidates, [min_projected_dist < d_min], axis=0
+                        )
+                    total_dist_scores = np.delete(
+                        min_intersite_dist, [min_projected_dist < d_min],
+                        axis=0
+                        )
+                else:
+                    total_dist_scores = min_intersite_dist
+            else:
+                raise NameError(
+                    'The MC-Criterion you requested is not available.'
+                    )
+
+            # Add the best candidate to the list of new samples
+            best_candidate = np.argsort(total_dist_scores)[::-1][:n_candidate]
+            selected_samples = np.vstack(
+                (selected_samples, candidates[best_candidate])
+                )
+
+        self.new_samples = selected_samples
+        self.exploration_scores = np.delete(error_voronoi, bad_samples, axis=0)
+
+        return self.new_samples, self.exploration_scores
+
+    # -------------------------------------------------------------------------
+    def get_mc_samples(self, all_candidates=None):
+        """
+        This function generates random samples based on Global Monte Carlo
+        methods and their corresponding scores, based on [1].
+
+        [1] Crombecq, K., Laermans, E. and Dhaene, T., 2011. Efficient
+            space-filling and non-collapsing sequential design strategies for
+            simulation-based modeling. European Journal of Operational Research
+            , 214(3), pp.683-696.
+            DOI: https://doi.org/10.1016/j.ejor.2011.05.032
+
+        Implemented methods to compute scores:
+            1) mc-intersite-proj
+            2) mc-intersite-proj-th
+
+        Arguments
+        ---------
+        all_candidates : array, optional
+            Samples to compute the scores for. The default is `None`. In this
+            case, samples will be generated by defined model input marginals.
+
+        Returns
+        -------
+        new_samples : array of shape (n_candidate, n_params)
+            A list of samples.
+        exploration_scores: arrays of shape (n_candidate)
+            Exploration scores.
+        """
+        explore_method = self.ExpDesign.explore_method
+        mc_criterion = self.mc_criterion
+        if all_candidates is None:
+            n_candidate = self.n_candidate
+        else:
+            n_candidate = all_candidates.shape[0]
+
+        # Get the Old ExpDesign #samples
+        old_ED_X = self.ExpDesign.X
+        ndim = old_ED_X.shape[1]
+
+        # ----- Compute the number of random points -----
+        if all_candidates is None:
+            # Generate MC Samples
+            all_candidates = self.ExpDesign.generate_samples(
+                self.n_candidate, explore_method
+                )
+        self.all_candidates = all_candidates
+
+        # initialization
+        min_intersite_dist = np.zeros((n_candidate))
+        min_projected_dist = np.zeros((n_candidate))
+
+        for i, candidate in enumerate(all_candidates):
+
+            # find candidate that is farthest away from any existing sample
+            maxMinDistance = 0
+
+            # find min distorted distance from all other samples
+            euclidean_dist = self._build_dist_matrix_point(
+                old_ED_X, candidate, do_sqrt=True
+                )
+            min_euclidean_dist = np.min(euclidean_dist)
+            min_intersite_dist[i] = min_euclidean_dist
+
+            # Check if this is the maximum minimum distance from all other
+            # samples
+            if min_euclidean_dist >= maxMinDistance:
+                maxMinDistance = min_euclidean_dist
+
+            # Projected distance
+            projected_dist = self._build_dist_matrix_point(
+                old_ED_X, candidate, 'chebyshev'
+                )
+            min_projected_dist[i] = np.min(projected_dist)
+
+        if mc_criterion == 'mc-intersite-proj':
+            weight_euclidean_dist = ((n_candidate+1)**(1/ndim) - 1) * 0.5
+            weight_projected_dist = (n_candidate+1) * 0.5
+            total_dist_scores = weight_euclidean_dist * min_intersite_dist
+            total_dist_scores += weight_projected_dist * min_projected_dist
+
+        elif mc_criterion == 'mc-intersite-proj-th':
+            alpha = 0.5  # chosen (tradeoff)
+            d_min = 2 * alpha / n_candidate
+            if any(min_projected_dist < d_min):
+                all_candidates = np.delete(
+                    all_candidates, [min_projected_dist < d_min], axis=0
+                    )
+                total_dist_scores = np.delete(
+                    min_intersite_dist, [min_projected_dist < d_min], axis=0
+                    )
+            else:
+                total_dist_scores = min_intersite_dist
+        else:
+            raise NameError('The MC-Criterion you requested is not available.')
+
+        self.new_samples = all_candidates
+        self.exploration_scores = total_dist_scores
+        self.exploration_scores /= np.nansum(total_dist_scores)
+
+        return self.new_samples, self.exploration_scores
+
+    # -------------------------------------------------------------------------
+    def approximate_voronoi(self, w, samples):
+        """
+        An approximate (monte carlo) version of Matlab's voronoi command.
+
+        Arguments
+        ---------
+        samples : array
+            Old experimental design to be used as center points for voronoi
+            cells.
+
+        Returns
+        -------
+        areas : array
+            An approximation of the voronoi cells' areas.
+        all_candidates: list of arrays
+            A list of samples in each voronoi cell.
+        """
+        n_samples = samples.shape[0]
+        ndim = samples.shape[1]
+
+        # Compute the number of random points
+        n_points = w * samples.shape[0]
+        # Generate w random points in the domain for each sample
+        points = self.ExpDesign.generate_samples(n_points, 'random')
+        self.all_candidates = points
+
+        # Calculate the nearest sample to each point
+        self.areas = np.zeros((n_samples))
+        self.closest_points = [np.empty((0, ndim)) for i in range(n_samples)]
+
+        # Compute the minimum distance from all the samples of old_ED_X for
+        # each test point
+        for idx in range(n_points):
+            # calculate the minimum distance
+            distances = self._build_dist_matrix_point(
+                samples, points[idx], do_sqrt=True
+                )
+            closest_sample = np.argmin(distances)
+
+            # Add to the voronoi list of the closest sample
+            self.areas[closest_sample] = self.areas[closest_sample] + 1
+            prev_closest_points = self.closest_points[closest_sample]
+            self.closest_points[closest_sample] = np.vstack(
+                (prev_closest_points, points[idx])
+                )
+
+        # Divide by the amount of points to get the estimated volume of each
+        # voronoi cell
+        self.areas /= n_points
+
+        self.perc = np.max(self.areas * 100)
+
+        self.errors = self.areas
+
+        return self.areas, self.all_candidates
+
+    # -------------------------------------------------------------------------
+    def _build_dist_matrix_point(self, samples, point, method='euclidean',
+                                 do_sqrt=False):
+        """
+        Calculates the intersite distance of all points in samples from point.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params)
+            The old experimental design.
+        point : array
+            A candidate point.
+        method : str
+            Distance method.
+        do_sqrt : bool, optional
+            Whether to return distances or squared distances. The default is
+            `False`.
+
+        Returns
+        -------
+        distances : array
+            Distances.
+
+        """
+        distances = distance.cdist(samples, np.array([point]), method)
+
+        # do square root?
+        if do_sqrt:
+            return distances
+        else:
+            return distances**2
+
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/glexindex.py b/examples/model-comparison/bayesvalidrox/surrogate_models/glexindex.py
new file mode 100644
index 0000000000000000000000000000000000000000..90877331ec121750e7f81e32a4b69edbc9a110ba
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/glexindex.py
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Multi indices for monomial exponents.
+Credit: Jonathan Feinberg
+https://github.com/jonathf/numpoly/blob/master/numpoly/utils/glexindex.py
+"""
+
+import numpy
+import numpy.typing
+
+
+def glexindex(start, stop=None, dimensions=1, cross_truncation=1.,
+              graded=False, reverse=False):
+    """
+    Generate graded lexicographical multi-indices for the monomial exponents.
+    Args:
+        start (Union[int, numpy.ndarray]):
+            The lower order of the indices. If array of int, counts as lower
+            bound for each axis.
+        stop (Union[int, numpy.ndarray, None]):
+            The maximum shape included. If omitted: stop <- start; start <- 0
+            If int is provided, set as largest total order. If array of int,
+            set as upper bound for each axis.
+        dimensions (int):
+            The number of dimensions in the expansion.
+        cross_truncation (float, Tuple[float, float]):
+            Use hyperbolic cross truncation scheme to reduce the number of
+            terms in expansion. If two values are provided, first is low bound
+            truncation, while the latter upper bound. If only one value, upper
+            bound is assumed.
+        graded (bool):
+            Graded sorting, meaning the indices are always sorted by the index
+            sum. E.g. ``(2, 2, 2)`` has a sum of 6, and will therefore be
+            consider larger than both ``(3, 1, 1)`` and ``(1, 1, 3)``.
+        reverse (bool):
+            Reversed lexicographical sorting meaning that ``(1, 3)`` is
+            considered smaller than ``(3, 1)``, instead of the opposite.
+    Returns:
+        list:
+            Order list of indices.
+    Examples:
+        >>> numpoly.glexindex(4).tolist()
+        [[0], [1], [2], [3]]
+        >>> numpoly.glexindex(2, dimensions=2).tolist()
+        [[0, 0], [1, 0], [0, 1]]
+        >>> numpoly.glexindex(start=2, stop=3, dimensions=2).tolist()
+        [[2, 0], [1, 1], [0, 2]]
+        >>> numpoly.glexindex([1, 2, 3]).tolist()
+        [[0, 0, 0], [0, 1, 0], [0, 0, 1], [0, 0, 2]]
+        >>> numpoly.glexindex([1, 2, 3], cross_truncation=numpy.inf).tolist()
+        [[0, 0, 0], [0, 1, 0], [0, 0, 1], [0, 1, 1], [0, 0, 2], [0, 1, 2]]
+    """
+    if stop is None:
+        start, stop = 0, start
+    start = numpy.array(start, dtype=int).flatten()
+    stop = numpy.array(stop, dtype=int).flatten()
+    start, stop, _ = numpy.broadcast_arrays(start, stop, numpy.empty(dimensions))
+
+    cross_truncation = cross_truncation*numpy.ones(2)
+    
+    # Moved here from _glexindex
+    bound = stop.max()
+    dimensions = len(start)
+    start = numpy.clip(start, a_min=0, a_max=None)
+    dtype = numpy.uint8 if bound < 256 else numpy.uint16
+    range_ = numpy.arange(bound, dtype=dtype)
+    indices = range_[:, numpy.newaxis]
+
+    for idx in range(dimensions-1):
+
+        # Truncate at each step to keep memory usage low
+        if idx:
+            indices = indices[cross_truncate(indices, bound-1, cross_truncation[1])]
+
+        # Repeats the current set of indices.
+        # e.g. [0,1,2] -> [0,1,2,0,1,2,...,0,1,2]
+        indices = numpy.tile(indices, (bound, 1))
+
+        # Stretches ranges over the new dimension.
+        # e.g. [0,1,2] -> [0,0,...,0,1,1,...,1,2,2,...,2]
+        front = range_.repeat(len(indices)//bound)[:, numpy.newaxis]
+
+        # Puts them two together.
+        indices = numpy.column_stack((front, indices))
+
+    # Complete the truncation scheme
+    if dimensions == 1:
+        indices = indices[(indices >= start) & (indices < bound)]
+    else:
+        lower = cross_truncate(indices, start-1, cross_truncation[0])
+        upper = cross_truncate(indices, stop-1, cross_truncation[1])
+        indices = indices[lower ^ upper]
+
+    indices = numpy.array(indices, dtype=int).reshape(-1, dimensions)
+    if indices.size:
+        # moved here from glexsort
+        keys = indices.T
+        keys_ = numpy.atleast_2d(keys)
+        if reverse:
+            keys_ = keys_[::-1]
+    
+        indices_sort = numpy.array(numpy.lexsort(keys_))
+        if graded:
+            indices_sort = indices_sort[numpy.argsort(
+                numpy.sum(keys_[:, indices_sort], axis=0))].T
+        
+        indices = indices[indices_sort]
+    return indices
+
+def cross_truncate(indices, bound, norm):
+    r"""
+    Truncate of indices using L_p norm.
+    .. math:
+        L_p(x) = \sum_i |x_i/b_i|^p ^{1/p} \leq 1
+    where :math:`b_i` are bounds that each :math:`x_i` should follow.
+    Args:
+        indices (Sequence[int]):
+            Indices to be truncated.
+        bound (int, Sequence[int]):
+            The bound function for witch the indices can not be larger than.
+        norm (float, Sequence[float]):
+            The `p` in the `L_p`-norm. Support includes both `L_0` and `L_inf`.
+    Returns:
+        Boolean indices to ``indices`` with True for each index where the
+        truncation criteria holds.
+    Examples:
+        >>> indices = numpy.array(numpy.mgrid[:10, :10]).reshape(2, -1).T
+        >>> indices[cross_truncate(indices, 2, norm=0)].T
+        array([[0, 0, 0, 1, 2],
+               [0, 1, 2, 0, 0]])
+        >>> indices[cross_truncate(indices, 2, norm=1)].T
+        array([[0, 0, 0, 1, 1, 2],
+               [0, 1, 2, 0, 1, 0]])
+        >>> indices[cross_truncate(indices, [0, 1], norm=1)].T
+        array([[0, 0],
+               [0, 1]])
+    """
+    assert norm >= 0, "negative L_p norm not allowed"
+    bound = numpy.asfarray(bound).flatten()*numpy.ones(indices.shape[1])
+
+    if numpy.any(bound < 0):
+        return numpy.zeros((len(indices),), dtype=bool)
+
+    if numpy.any(bound == 0):
+        out = numpy.all(indices[:, bound == 0] == 0, axis=-1)
+        if numpy.any(bound):
+            out &= cross_truncate(indices[:, bound != 0], bound[bound != 0], norm=norm)
+        return out
+
+    if norm == 0:
+        out = numpy.sum(indices > 0, axis=-1) <= 1
+        out[numpy.any(indices > bound, axis=-1)] = False
+    elif norm == numpy.inf:
+        out = numpy.max(indices/bound, axis=-1) <= 1
+    else:
+        out = numpy.sum((indices/bound)**norm, axis=-1)**(1./norm) <= 1
+
+    assert numpy.all(out[numpy.all(indices == 0, axis=-1)])
+
+    return out
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/input_space.py b/examples/model-comparison/bayesvalidrox/surrogate_models/input_space.py
new file mode 100644
index 0000000000000000000000000000000000000000..4e010d66f2933ec243bad756d8f2c5454808d802
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/input_space.py
@@ -0,0 +1,398 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Input space built from set prior distributions
+"""
+
+import numpy as np
+import chaospy
+import scipy.stats as st
+
+
+class InputSpace:
+    """
+    This class generates the input space for the metamodel from the
+    distributions provided using the `Input` object.
+
+    Attributes
+    ----------
+    Input : obj
+        Input object containing the parameter marginals, i.e. name,
+        distribution type and distribution parameters or available raw data.
+    meta_Model_type : str
+        Type of the meta_Model_type.
+
+    """
+
+    def __init__(self, Input, meta_Model_type='pce'):
+        self.InputObj = Input
+        self.meta_Model_type = meta_Model_type
+        
+        # Other 
+        self.apce = None
+        self.ndim = None
+        
+        # Init 
+        self.check_valid_inputs()
+        
+        
+    def check_valid_inputs(self)-> None:
+        """
+        Check if the given InputObj is valid to use for further calculations:
+            Has some Marginals
+            Marginals have valid priors
+            All Marginals given as the same type (samples vs dist)
+
+        Returns
+        -------
+        None
+
+        """
+        Inputs = self.InputObj
+        self.ndim = len(Inputs.Marginals)
+        
+        # Check if PCE or aPCE metamodel is selected.
+        # TODO: test also for 'pce'??
+        if self.meta_Model_type.lower() == 'apce':
+            self.apce = True
+        else:
+            self.apce = False
+
+        # check if marginals given 
+        if not self.ndim >=1:
+            raise AssertionError('Cannot build distributions if no marginals are given')
+            
+        # check that each marginal is valid
+        for marginals in Inputs.Marginals:
+            if len(marginals.input_data) == 0:
+                if marginals.dist_type == None:
+                    raise AssertionError('Not all marginals were provided priors')
+                    break
+            if np.array(marginals.input_data).shape[0] and (marginals.dist_type != None):
+                raise AssertionError('Both samples and distribution type are given. Please choose only one.')
+                break
+                
+        # Check if input is given as dist or input_data.
+        self.input_data_given = -1
+        for marg in Inputs.Marginals:
+            #print(self.input_data_given)
+            size = np.array(marg.input_data).shape[0]
+            #print(f'Size: {size}')
+            if size and abs(self.input_data_given) !=1:
+                self.input_data_given = 2
+                break
+            if (not size) and self.input_data_given > 0:
+                self.input_data_given = 2
+                break
+            if not size:
+                self.input_data_given = 0
+            if size:
+                self.input_data_given = 1
+                
+        if self.input_data_given == 2:
+            raise AssertionError('Distributions cannot be built as the priors have different types')
+            
+    
+        # Get the bounds if input_data are directly defined by user:
+        if self.input_data_given:
+            for i in range(self.ndim):
+                low_bound = np.min(Inputs.Marginals[i].input_data)
+                up_bound = np.max(Inputs.Marginals[i].input_data)
+                Inputs.Marginals[i].parameters = [low_bound, up_bound]
+
+  
+
+    # -------------------------------------------------------------------------
+    def init_param_space(self, max_deg=None):
+        """
+        Initializes parameter space.
+
+        Parameters
+        ----------
+        max_deg : int, optional
+            Maximum degree. The default is `None`.
+
+        Creates
+        -------
+        raw_data : array of shape (n_params, n_samples)
+            Raw data.
+        bound_tuples : list of tuples
+            A list containing lower and upper bounds of parameters.
+
+        """
+        # Recheck all before running!
+        self.check_valid_inputs()
+        
+        Inputs = self.InputObj
+        ndim = self.ndim
+        rosenblatt_flag = Inputs.Rosenblatt
+        mc_size = 50000
+
+        # Save parameter names
+        self.par_names = []
+        for parIdx in range(ndim):
+            self.par_names.append(Inputs.Marginals[parIdx].name)
+
+        # Create a multivariate probability distribution
+        # TODO: change this to make max_deg obligatory? at least in some specific cases?
+        if max_deg is not None:
+            JDist, poly_types = self.build_polytypes(rosenblatt=rosenblatt_flag)
+            self.JDist, self.poly_types = JDist, poly_types
+
+        if self.input_data_given:
+            self.MCSize = len(Inputs.Marginals[0].input_data)
+            self.raw_data = np.zeros((ndim, self.MCSize))
+
+            for parIdx in range(ndim):
+                # Save parameter names
+                try:
+                    self.raw_data[parIdx] = np.array(
+                        Inputs.Marginals[parIdx].input_data)
+                except:
+                    self.raw_data[parIdx] = self.JDist[parIdx].sample(mc_size)
+
+        else:
+            # Generate random samples based on parameter distributions
+            self.raw_data = chaospy.generate_samples(mc_size,
+                                                     domain=self.JDist)
+
+        # Extract moments
+        for parIdx in range(ndim):
+            mu = np.mean(self.raw_data[parIdx])
+            std = np.std(self.raw_data[parIdx])
+            self.InputObj.Marginals[parIdx].moments = [mu, std]
+
+        # Generate the bounds based on given inputs for marginals
+        bound_tuples = []
+        for i in range(ndim):
+            if Inputs.Marginals[i].dist_type == 'unif':
+                low_bound = Inputs.Marginals[i].parameters[0]
+                up_bound = Inputs.Marginals[i].parameters[1]
+            else:
+                low_bound = np.min(self.raw_data[i])
+                up_bound = np.max(self.raw_data[i])
+
+            bound_tuples.append((low_bound, up_bound))
+
+        self.bound_tuples = tuple(bound_tuples)
+
+    # -------------------------------------------------------------------------
+    def build_polytypes(self, rosenblatt):
+        """
+        Creates the polynomial types to be passed to univ_basis_vals method of
+        the MetaModel object.
+
+        Parameters
+        ----------
+        rosenblatt : bool
+            Rosenblatt transformation flag.
+
+        Returns
+        -------
+        orig_space_dist : object
+            A chaospy JDist object or a gaussian_kde object.
+        poly_types : list
+            List of polynomial types for the parameters.
+
+        """
+        Inputs = self.InputObj
+        
+        all_data = []
+        all_dist_types = []
+        orig_joints = []
+        poly_types = []
+        
+        for parIdx in range(self.ndim):
+
+            if Inputs.Marginals[parIdx].dist_type is None:
+                data = Inputs.Marginals[parIdx].input_data
+                all_data.append(data)
+                dist_type = None
+            else:
+                dist_type = Inputs.Marginals[parIdx].dist_type
+                params = Inputs.Marginals[parIdx].parameters
+
+            if rosenblatt:
+                polytype = 'hermite'
+                dist = chaospy.Normal()
+
+            elif dist_type is None:
+                polytype = 'arbitrary'
+                dist = None
+
+            elif 'unif' in dist_type.lower():
+                polytype = 'legendre'
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Uniform(lower=params[0], upper=params[1])
+
+            elif 'norm' in dist_type.lower() and \
+                 'log' not in dist_type.lower():
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                polytype = 'hermite'
+                dist = chaospy.Normal(mu=params[0], sigma=params[1])
+
+            elif 'gamma' in dist_type.lower():
+                polytype = 'laguerre'
+                if not np.array(params).shape[0]>=3:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Gamma(shape=params[0],
+                                     scale=params[1],
+                                     shift=params[2])
+
+            elif 'beta' in dist_type.lower():
+                if not np.array(params).shape[0]>=4:
+                    raise AssertionError('Distribution has too few parameters!')
+                polytype = 'jacobi'
+                dist = chaospy.Beta(alpha=params[0], beta=params[1],
+                                    lower=params[2], upper=params[3])
+
+            elif 'lognorm' in dist_type.lower():
+                polytype = 'hermite'
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                mu = np.log(params[0]**2/np.sqrt(params[0]**2 + params[1]**2))
+                sigma = np.sqrt(np.log(1 + params[1]**2 / params[0]**2))
+                dist = chaospy.LogNormal(mu, sigma)
+                # dist = chaospy.LogNormal(mu=params[0], sigma=params[1])
+
+            elif 'expon' in dist_type.lower():
+                polytype = 'exponential'
+                if not np.array(params).shape[0]>=2:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Exponential(scale=params[0], shift=params[1])
+
+            elif 'weibull' in dist_type.lower():
+                polytype = 'weibull'
+                if not np.array(params).shape[0]>=3:
+                    raise AssertionError('Distribution has too few parameters!')
+                dist = chaospy.Weibull(shape=params[0], scale=params[1],
+                                       shift=params[2])
+
+            else:
+                message = (f"DistType {dist_type} for parameter"
+                           f"{parIdx+1} is not available.")
+                raise ValueError(message)
+
+            if self.input_data_given or self.apce:
+                polytype = 'arbitrary'
+
+            # Store dists and poly_types
+            orig_joints.append(dist)
+            poly_types.append(polytype)
+            all_dist_types.append(dist_type)
+
+        # Prepare final output to return
+        if None in all_dist_types:
+            # Naive approach: Fit a gaussian kernel to the provided data
+            Data = np.asarray(all_data)
+            try:
+                orig_space_dist = st.gaussian_kde(Data)
+            except:
+                raise ValueError('The samples provided to the Marginals should be 1D only')
+            self.prior_space = orig_space_dist
+        else:
+            orig_space_dist = chaospy.J(*orig_joints)
+            try:
+                self.prior_space = st.gaussian_kde(orig_space_dist.sample(10000))
+            except:
+                raise ValueError('Parameter values are not valid, please set differently')
+
+        return orig_space_dist, poly_types
+
+    # -------------------------------------------------------------------------
+    def transform(self, X, params=None, method=None):
+        """
+        Transforms the samples via either a Rosenblatt or an isoprobabilistic
+        transformation.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples,n_params)
+            Samples to be transformed.
+        method : string
+            If transformation method is 'user' transform X, else just pass X.
+
+        Returns
+        -------
+        tr_X: array of shape (n_samples,n_params)
+            Transformed samples.
+
+        """
+        # Check for built JDist
+        if not hasattr(self, 'JDist'):
+            raise AttributeError('Call function init_param_space first to create JDist')
+            
+        # Check if X is 2d
+        if X.ndim != 2:
+            raise AttributeError('X should have two dimensions')
+            
+        # Check if size of X matches Marginals
+        if X.shape[1]!= self.ndim:
+            raise AttributeError('The second dimension of X should be the same size as the number of marginals in the InputObj')
+        
+        if self.InputObj.Rosenblatt:
+            self.origJDist, _ = self.build_polytypes(False)
+            if method == 'user':
+                tr_X = self.JDist.inv(self.origJDist.fwd(X.T)).T
+            else:
+                # Inverse to original spcace -- generate sample ED
+                tr_X = self.origJDist.inv(self.JDist.fwd(X.T)).T
+        else:
+            # Transform samples via an isoprobabilistic transformation
+            n_samples, n_params = X.shape
+            Inputs = self.InputObj
+            origJDist = self.JDist
+            poly_types = self.poly_types
+
+            disttypes = []
+            for par_i in range(n_params):
+                disttypes.append(Inputs.Marginals[par_i].dist_type)
+
+            # Pass non-transformed X, if arbitrary PCE is selected.
+            if None in disttypes or self.input_data_given or self.apce:
+                return X
+
+            cdfx = np.zeros((X.shape))
+            tr_X = np.zeros((X.shape))
+
+            for par_i in range(n_params):
+
+                # Extract the parameters of the original space
+                disttype = disttypes[par_i]
+                if disttype is not None:
+                    dist = origJDist[par_i]
+                else:
+                    dist = None
+                polytype = poly_types[par_i]
+                cdf = np.vectorize(lambda x: dist.cdf(x))
+
+                # Extract the parameters of the transformation space based on
+                # polyType
+                if polytype == 'legendre' or disttype == 'uniform':
+                    # Generate Y_Dists based
+                    params_Y = [-1, 1]
+                    dist_Y = st.uniform(loc=params_Y[0],
+                                        scale=params_Y[1]-params_Y[0])
+                    inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x))
+
+                elif polytype == 'hermite' or disttype == 'norm':
+                    params_Y = [0, 1]
+                    dist_Y = st.norm(loc=params_Y[0], scale=params_Y[1])
+                    inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x))
+
+                elif polytype == 'laguerre' or disttype == 'gamma':
+                    if params == None:
+                        raise AttributeError('Additional parameters have to be set for the gamma distribution!')
+                    params_Y = [1, params[1]]
+                    dist_Y = st.gamma(loc=params_Y[0], scale=params_Y[1])
+                    inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x))
+
+                # Compute CDF_x(X)
+                cdfx[:, par_i] = cdf(X[:, par_i])
+
+                # Compute invCDF_y(cdfx)
+                tr_X[:, par_i] = inv_cdf(cdfx[:, par_i])
+
+        return tr_X
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/inputs.py b/examples/model-comparison/bayesvalidrox/surrogate_models/inputs.py
new file mode 100644
index 0000000000000000000000000000000000000000..094e1066fe008e37288e44750524c5a1370bd7a2
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/inputs.py
@@ -0,0 +1,79 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Inputs and related marginal distributions
+"""
+
+class Input:
+    """
+    A class to define the uncertain input parameters.
+
+    Attributes
+    ----------
+    Marginals : obj
+        Marginal objects. See `inputs.Marginal`.
+    Rosenblatt : bool
+        If Rossenblatt transformation is required for the dependent input
+        parameters.
+
+    Examples
+    -------
+    Marginals can be defined as following:
+
+    >>> Inputs.add_marginals()
+    >>> Inputs.Marginals[0].name = 'X_1'
+    >>> Inputs.Marginals[0].dist_type = 'uniform'
+    >>> Inputs.Marginals[0].parameters = [-5, 5]
+
+    If there is no common data is avaliable, the input data can be given
+    as following:
+
+    >>> Inputs.add_marginals()
+    >>> Inputs.Marginals[0].name = 'X_1'
+    >>> Inputs.Marginals[0].input_data = input_data
+    """
+    poly_coeffs_flag = True
+
+    def __init__(self):
+        self.Marginals = []
+        self.Rosenblatt = False
+
+    def add_marginals(self):
+        """
+        Adds a new Marginal object to the input object.
+
+        Returns
+        -------
+        None.
+
+        """
+        self.Marginals.append(Marginal())
+
+
+# Nested class
+class Marginal:
+    """
+    An object containing the specifications of the marginals for each uncertain
+    parameter.
+
+    Attributes
+    ----------
+    name : string
+        Name of the parameter. The default is `'$x_1$'`.
+    dist_type : string
+        Name of the distribution. The default is `None`.
+    parameters : list
+        List of the parameters corresponding to the distribution type. The
+        default is `None`.
+    input_data : array
+        Available input data. The default is `[]`.
+    moments : list
+        List of the moments.
+    """
+
+    def __init__(self):
+        self.name = '$x_1$'
+        self.dist_type = None
+        self.parameters = None
+        self.input_data = []
+        self.moments = None
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py b/examples/model-comparison/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py
new file mode 100644
index 0000000000000000000000000000000000000000..96ef9c1d50b10b587ad0846d41733fc7f1cedfe8
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py
@@ -0,0 +1,366 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jul 15 14:08:59 2022
+
+@author: farid
+"""
+import numpy as np
+from sklearn.base import RegressorMixin
+from sklearn.linear_model._base import LinearModel
+from sklearn.utils import check_X_y
+
+
+def corr(x, y):
+    return abs(x.dot(y))/np.sqrt((x**2).sum())
+
+
+class OrthogonalMatchingPursuit(LinearModel, RegressorMixin):
+    '''
+    Regression with Orthogonal Matching Pursuit [1].
+
+    Parameters
+    ----------
+    fit_intercept : boolean, optional (DEFAULT = True)
+        whether to calculate the intercept for this model. If set
+        to false, no intercept will be used in calculations
+        (e.g. data is expected to be already centered).
+
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied; else, it may be overwritten.
+
+    verbose : boolean, optional (DEFAULT = FALSE)
+        Verbose mode when fitting the model
+
+    Attributes
+    ----------
+    coef_ : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+
+    active_ : array, dtype = np.bool, shape = (n_features)
+       True for non-zero coefficients, False otherwise
+
+    References
+    ----------
+    [1] Pati, Y., Rezaiifar, R., Krishnaprasad, P. (1993). Orthogonal matching
+        pursuit: recursive function approximation with application to wavelet
+        decomposition. Proceedings of 27th Asilomar Conference on Signals,
+        Systems and Computers, 40-44.
+    '''
+
+    def __init__(self, fit_intercept=True, normalize=False, copy_X=True,
+                 verbose=False):
+        self.fit_intercept   = fit_intercept
+        self.normalize       = normalize
+        self.copy_X          = copy_X
+        self.verbose         = verbose
+
+    def _preprocess_data(self, X, y):
+        """Center and scale data.
+        Centers data to have mean zero along axis 0. If fit_intercept=False or
+        if the X is a sparse matrix, no centering is done, but normalization
+        can still be applied. The function returns the statistics necessary to
+        reconstruct the input data, which are X_offset, y_offset, X_scale, such
+        that the output
+            X = (X - X_offset) / X_scale
+        X_scale is the L2 norm of X - X_offset.
+        """
+
+        if self.copy_X:
+            X = X.copy(order='K')
+
+        y = np.asarray(y, dtype=X.dtype)
+
+        if self.fit_intercept:
+            X_offset = np.average(X, axis=0)
+            X -= X_offset
+            if self.normalize:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+                std = np.sqrt(np.sum(X**2, axis=0)/(len(X)-1))
+                X_scale[std != 0] = std[std != 0]
+                X /= X_scale
+            else:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            y_offset = np.mean(y)
+            y = y - y_offset
+        else:
+            X_offset = np.zeros(X.shape[1], dtype=X.dtype)
+            X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            if y.ndim == 1:
+                y_offset = X.dtype.type(0)
+            else:
+                y_offset = np.zeros(y.shape[1], dtype=X.dtype)
+
+        return X, y, X_offset, y_offset, X_scale
+
+    def fit(self, X, y):
+        '''
+        Fits Regression with Orthogonal Matching Pursuit Algorithm.
+
+        Parameters
+        -----------
+        X: {array-like, sparse matrix} of size (n_samples, n_features)
+           Training data, matrix of explanatory variables
+
+        y: array-like of size [n_samples, n_features]
+           Target values
+
+        Returns
+        -------
+        self : object
+            Returns self.
+        '''
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+
+        X, y, X_mean, y_mean, X_std = self._preprocess_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean = y_mean
+        self._x_std = X_std
+
+        # Normalize columns of Psi, so that each column has norm = 1
+        norm_X = np.linalg.norm(X, axis=0)
+        X_norm = X/norm_X
+
+        # Initialize residual vector to full model response and normalize
+        R = y
+        norm_y = np.sqrt(np.dot(y, y))
+        r = y/norm_y
+
+        # Check for constant regressors
+        const_indices = np.where(~np.diff(X, axis=0).any(axis=0))[0]
+        bool_const = not const_indices
+
+        # Start regression using OPM algorithm
+        precision = 0        # Set precision criterion to precision of program
+        early_stop = True
+        cond_early = True    # Initialize condition for early stop
+        ind = []
+        iindx = []           # index of selected columns
+        indtot = np.arange(n_features)  # Full index set for remaining columns
+        kmax = min(n_samples, n_features)  # Maximum number of iterations
+        LOO = np.PINF * np.ones(kmax)  # Store LOO error at each iteration
+        LOOmin = np.PINF               # Initialize minimum value of LOO
+        coeff = np.zeros((n_features, kmax))
+        count = 0
+        k = 0.1                # Percentage of iteration history for early stop
+
+        # Begin iteration over regressors set (Matrix X)
+        while (np.linalg.norm(R) > precision) and (count <= kmax-1) and \
+              ((cond_early or early_stop) ^ ~cond_early):
+
+            # Update index set of columns yet to select
+            if count != 0:
+                indtot = np.delete(indtot, iindx)
+
+            # Find column of X that is most correlated with residual
+            h = abs(np.dot(r, X_norm))
+            iindx = np.argmax(h[indtot])
+            indx = indtot[iindx]
+
+            # initialize with the constant regressor, if it exists in the basis
+            if (count == 0) and bool_const:
+                # overwrite values for iindx and indx
+                iindx = const_indices[0]
+                indx = indtot[iindx]
+
+            # Invert the information matrix at the first iteration, later only
+            # update its value on the basis of the previously inverted one,
+            if count == 0:
+                M = 1 / np.dot(X[:, indx], X[:, indx])
+            else:
+                x = np.dot(X[:, ind].T, X[:, indx])
+                r = np.dot(X[:, indx], X[:, indx])
+                M = self.blockwise_inverse(M, x, x.T, r)
+
+            # Add newly found index to the selected indexes set
+            ind.append(indx)
+
+            # Select regressors subset (Projection subspace)
+            Xpro = X[:, ind]
+
+            # Obtain coefficient by performing OLS
+            TT = np.dot(y, Xpro)
+            beta = np.dot(M, TT)
+            coeff[ind, count] = beta
+
+            # Compute LOO error
+            LOO[count] = self.loo_error(Xpro, M, y, beta)
+
+            # Compute new residual due to new projection
+            R = y - np.dot(Xpro, beta)
+
+            # Normalize residual
+            norm_R = np.sqrt(np.dot(R, R))
+            r = R / norm_R
+
+            # Update counters and early-stop criterions
+            countinf = max(0, int(count-k*kmax))
+            LOOmin = min(LOOmin, LOO[count])
+
+            if count == 0:
+                cond_early = (LOO[0] <= LOOmin)
+            else:
+                cond_early = (min(LOO[countinf:count+1]) <= LOOmin)
+
+            if self.verbose:
+                print(f'Iteration: {count+1}, mod. LOOCV error : '
+                      f'{LOO[count]:.2e}')
+
+            # Update counter
+            count += 1
+
+        # Select projection with smallest cross-validation error
+        countmin = np.argmin(LOO[:-1])
+        self.coef_ = coeff[:, countmin]
+        self.active = coeff[:, countmin] != 0.0
+
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+
+        return self
+
+    def predict(self, X):
+        '''
+        Computes predictive distribution for test set.
+
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+
+        Returns
+        -------
+        y_hat: numpy array of size (n_samples_test,)
+               Estimated values of targets on test set (i.e. mean of
+               predictive distribution)
+        '''
+
+        y_hat = np.dot(X, self.coef_) + self.intercept_
+
+        return y_hat
+
+    def loo_error(self, psi, inv_inf_matrix, y, coeffs):
+        """
+        Calculates the corrected LOO error for regression on regressor
+        matrix `psi` that generated the coefficients based on [1] and [2].
+
+        [1] Blatman, G., 2009. Adaptive sparse polynomial chaos expansions for
+            uncertainty propagation and sensitivity analysis (Doctoral
+            dissertation, Clermont-Ferrand 2).
+
+        [2] Blatman, G. and Sudret, B., 2011. Adaptive sparse polynomial chaos
+            expansion based on least angle regression. Journal of computational
+            Physics, 230(6), pp.2345-2367.
+
+        Parameters
+        ----------
+        psi : array of shape (n_samples, n_feature)
+            Orthogonal bases evaluated at the samples.
+        inv_inf_matrix : array
+            Inverse of the information matrix.
+        y : array of shape (n_samples, )
+            Targets.
+        coeffs : array
+            Computed regresssor cofficients.
+
+        Returns
+        -------
+        loo_error : float
+            Modified LOOCV error.
+
+        """
+
+        # NrEvaluation (Size of experimental design)
+        N, P = psi.shape
+
+        # h factor (the full matrix is not calculated explicitly,
+        # only the trace is, to save memory)
+        PsiM = np.dot(psi, inv_inf_matrix)
+
+        h = np.sum(np.multiply(PsiM, psi), axis=1, dtype=np.longdouble)
+
+        # ------ Calculate Error Loocv for each measurement point ----
+        # Residuals
+        residual = np.dot(psi, coeffs) - y
+
+        # Variance
+        varY = np.var(y)
+
+        if varY == 0:
+            norm_emp_error = 0
+            loo_error = 0
+        else:
+            norm_emp_error = np.mean(residual**2)/varY
+
+            loo_error = np.mean(np.square(residual / (1-h))) / varY
+
+            # if there are NaNs, just return an infinite LOO error (this
+            # happens, e.g., when a strongly underdetermined problem is solved)
+            if np.isnan(loo_error):
+                loo_error = np.inf
+
+        # Corrected Error for over-determined system
+        tr_M = np.trace(np.atleast_2d(inv_inf_matrix))
+        if tr_M < 0 or abs(tr_M) > 1e6:
+            tr_M = np.trace(np.linalg.pinv(np.dot(psi.T, psi)))
+
+        # Over-determined system of Equation
+        if N > P:
+            T_factor = N/(N-P) * (1 + tr_M)
+
+        # Under-determined system of Equation
+        else:
+            T_factor = np.inf
+
+        loo_error *= T_factor
+
+        return loo_error
+
+    def blockwise_inverse(self, Ainv, B, C, D):
+        """
+        non-singular square matrix M defined as M = [[A B]; [C D]] .
+        B, C and D can have any dimension, provided their combination defines
+        a square matrix M.
+
+        Parameters
+        ----------
+        Ainv : float or array
+            inverse of the square-submatrix A.
+        B : float or array
+            Information matrix with all new regressor.
+        C : float or array
+            Transpose of B.
+        D : float or array
+            Information matrix with all selected regressors.
+
+        Returns
+        -------
+        M : array
+            Inverse of the information matrix.
+
+        """
+        if np.isscalar(D):
+            # Inverse of D
+            Dinv = 1/D
+            # Schur complement
+            SCinv = 1/(D - np.dot(C, np.dot(Ainv, B[:, None])))[0]
+        else:
+            # Inverse of D
+            Dinv = np.linalg.solve(D, np.eye(D.shape))
+            # Schur complement
+            SCinv = np.linalg.solve((D - C*Ainv*B), np.eye(D.shape))
+
+        T1 = np.dot(Ainv, np.dot(B[:, None], SCinv))
+        T2 = np.dot(C, Ainv)
+
+        # Assemble the inverse matrix
+        M = np.vstack((
+            np.hstack((Ainv+T1*T2, -T1)),
+            np.hstack((-(SCinv)*T2, SCinv))
+            ))
+        return M
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/reg_fast_ard.py b/examples/model-comparison/bayesvalidrox/surrogate_models/reg_fast_ard.py
new file mode 100644
index 0000000000000000000000000000000000000000..e6883a3edd6d247c219b8be328f5206b75780fbb
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/reg_fast_ard.py
@@ -0,0 +1,475 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Tue Mar 24 19:41:45 2020
+
+@author: farid
+"""
+import numpy as np
+from scipy.linalg import solve_triangular
+from numpy.linalg import LinAlgError
+from sklearn.base import RegressorMixin
+from sklearn.linear_model._base import LinearModel
+import warnings
+from sklearn.utils import check_X_y
+from scipy.linalg import pinvh
+
+
+def update_precisions(Q,S,q,s,A,active,tol,n_samples,clf_bias):
+    '''
+    Selects one feature to be added/recomputed/deleted to model based on
+    effect it will have on value of log marginal likelihood.
+    '''
+    # initialise vector holding changes in log marginal likelihood
+    deltaL = np.zeros(Q.shape[0])
+
+    # identify features that can be added , recomputed and deleted in model
+    theta        =  q**2 - s
+    add          =  (theta > 0) * (active == False)
+    recompute    =  (theta > 0) * (active == True)
+    delete       = ~(add + recompute)
+
+    # compute sparsity & quality parameters corresponding to features in
+    # three groups identified above
+    Qadd,Sadd      = Q[add], S[add]
+    Qrec,Srec,Arec = Q[recompute], S[recompute], A[recompute]
+    Qdel,Sdel,Adel = Q[delete], S[delete], A[delete]
+
+    # compute new alpha's (precision parameters) for features that are
+    # currently in model and will be recomputed
+    Anew           = s[recompute]**2/ ( theta[recompute] + np.finfo(np.float32).eps)
+    delta_alpha    = (1./Anew - 1./Arec)
+
+    # compute change in log marginal likelihood
+    deltaL[add]       = ( Qadd**2 - Sadd ) / Sadd + np.log(Sadd/Qadd**2 )
+    deltaL[recompute] = Qrec**2 / (Srec + 1. / delta_alpha) - np.log(1 + Srec*delta_alpha)
+    deltaL[delete]    = Qdel**2 / (Sdel - Adel) - np.log(1 - Sdel / Adel)
+    deltaL            = deltaL  / n_samples
+
+    # find feature which caused largest change in likelihood
+    feature_index = np.argmax(deltaL)
+
+    # no deletions or additions
+    same_features  = np.sum( theta[~recompute] > 0) == 0
+
+    # changes in precision for features already in model is below threshold
+    no_delta       = np.sum( abs( Anew - Arec ) > tol ) == 0
+    # if same_features: print(abs( Anew - Arec ))
+    # print("same_features = {} no_delta = {}".format(same_features,no_delta))
+    # check convergence: if no features to add or delete and small change in
+    #                    precision for current features then terminate
+    converged = False
+    if same_features and no_delta:
+        converged = True
+        return [A,converged]
+
+    # if not converged update precision parameter of weights and return
+    if theta[feature_index] > 0:
+        A[feature_index] = s[feature_index]**2 / theta[feature_index]
+        if active[feature_index] == False:
+            active[feature_index] = True
+    else:
+        # at least two active features
+        if active[feature_index] == True and np.sum(active) >= 2:
+            # do not remove bias term in classification
+            # (in regression it is factored in through centering)
+            if not (feature_index == 0 and clf_bias):
+                active[feature_index] = False
+                A[feature_index]      = np.PINF
+
+    return [A,converged]
+
+
+class RegressionFastARD(LinearModel, RegressorMixin):
+    '''
+    Regression with Automatic Relevance Determination (Fast Version uses
+    Sparse Bayesian Learning)
+    https://github.com/AmazaspShumik/sklearn-bayes/blob/master/skbayes/rvm_ard_models/fast_rvm.py
+
+    Parameters
+    ----------
+    n_iter: int, optional (DEFAULT = 100)
+        Maximum number of iterations
+
+    start: list, optional (DEFAULT = None)
+        Initial selected features.
+
+    tol: float, optional (DEFAULT = 1e-3)
+        If absolute change in precision parameter for weights is below threshold
+        algorithm terminates.
+
+    fit_intercept : boolean, optional (DEFAULT = True)
+        whether to calculate the intercept for this model. If set
+        to false, no intercept will be used in calculations
+        (e.g. data is expected to be already centered).
+
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied; else, it may be overwritten.
+
+    compute_score : bool, default=False
+        If True, compute the log marginal likelihood at each iteration of the
+        optimization.
+
+    verbose : boolean, optional (DEFAULT = FALSE)
+        Verbose mode when fitting the model
+
+    Attributes
+    ----------
+    coef_ : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+
+    alpha_ : float
+       estimated precision of the noise
+
+    active_ : array, dtype = np.bool, shape = (n_features)
+       True for non-zero coefficients, False otherwise
+
+    lambda_ : array, shape = (n_features)
+       estimated precisions of the coefficients
+
+    sigma_ : array, shape = (n_features, n_features)
+        estimated covariance matrix of the weights, computed only
+        for non-zero coefficients
+
+    scores_ : array-like of shape (n_iter_+1,)
+        If computed_score is True, value of the log marginal likelihood (to be
+        maximized) at each iteration of the optimization.
+
+    References
+    ----------
+    [1] Fast marginal likelihood maximisation for sparse Bayesian models
+    (Tipping & Faul 2003) (http://www.miketipping.com/papers/met-fastsbl.pdf)
+    [2] Analysis of sparse Bayesian learning (Tipping & Faul 2001)
+        (http://www.miketipping.com/abstracts.htm#Faul:NIPS01)
+    '''
+
+    def __init__(self, n_iter=300, start=None, tol=1e-3, fit_intercept=True,
+                 normalize=False, copy_X=True, compute_score=False, verbose=False):
+        self.n_iter          = n_iter
+        self.start           = start
+        self.tol             = tol
+        self.scores_         = list()
+        self.fit_intercept   = fit_intercept
+        self.normalize       = normalize
+        self.copy_X          = copy_X
+        self.compute_score   = compute_score
+        self.verbose         = verbose
+
+    def _preprocess_data(self, X, y):
+        """Center and scale data.
+        Centers data to have mean zero along axis 0. If fit_intercept=False or
+        if the X is a sparse matrix, no centering is done, but normalization
+        can still be applied. The function returns the statistics necessary to
+        reconstruct the input data, which are X_offset, y_offset, X_scale, such
+        that the output
+            X = (X - X_offset) / X_scale
+        X_scale is the L2 norm of X - X_offset.
+        """
+
+        if self.copy_X:
+            X = X.copy(order='K')
+
+        y = np.asarray(y, dtype=X.dtype)
+
+        if self.fit_intercept:
+            X_offset = np.average(X, axis=0)
+            X -= X_offset
+            if self.normalize:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+                std = np.sqrt(np.sum(X**2, axis=0)/(len(X)-1))
+                X_scale[std != 0] = std[std != 0]
+                X /= X_scale
+            else:
+                X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            y_offset = np.mean(y)
+            y = y - y_offset
+        else:
+            X_offset = np.zeros(X.shape[1], dtype=X.dtype)
+            X_scale = np.ones(X.shape[1], dtype=X.dtype)
+            if y.ndim == 1:
+                y_offset = X.dtype.type(0)
+            else:
+                y_offset = np.zeros(y.shape[1], dtype=X.dtype)
+
+        return X, y, X_offset, y_offset, X_scale
+
+    def fit(self, X, y):
+        '''
+        Fits ARD Regression with Sequential Sparse Bayes Algorithm.
+
+        Parameters
+        -----------
+        X: {array-like, sparse matrix} of size (n_samples, n_features)
+           Training data, matrix of explanatory variables
+
+        y: array-like of size [n_samples, n_features]
+           Target values
+
+        Returns
+        -------
+        self : object
+            Returns self.
+        '''
+        X, y = check_X_y(X, y, dtype=np.float64, y_numeric=True)
+        n_samples, n_features = X.shape
+
+        X, y, X_mean, y_mean, X_std = self._preprocess_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean = y_mean
+        self._x_std = X_std
+
+        #  precompute X'*Y , X'*X for faster iterations & allocate memory for
+        #  sparsity & quality vectors
+        XY = np.dot(X.T, y)
+        XX = np.dot(X.T, X)
+        XXd = np.diag(XX)
+
+        #  initialise precision of noise & and coefficients
+        var_y = np.var(y)
+
+        # check that variance is non zero !!!
+        if var_y == 0:
+            beta = 1e-2
+            self.var_y = True
+        else:
+            beta = 1. / np.var(y)
+            self.var_y = False
+
+        A = np.PINF * np.ones(n_features)
+        active = np.zeros(n_features, dtype=np.bool)
+
+        if self.start is not None and not hasattr(self, 'active_'):
+            start = self.start
+            # start from a given start basis vector
+            proj = XY**2 / XXd
+            active[start] = True
+            A[start] = XXd[start]/(proj[start] - var_y)
+
+        else:
+            # in case of almost perfect multicollinearity between some features
+            # start from feature 0
+            if np.sum(XXd - X_mean**2 < np.finfo(np.float32).eps) > 0:
+                A[0] = np.finfo(np.float16).eps
+                active[0] = True
+
+            else:
+                # start from a single basis vector with largest projection on
+                # targets
+                proj = XY**2 / XXd
+                start = np.argmax(proj)
+                active[start] = True
+                A[start] = XXd[start]/(proj[start] - var_y +
+                                       np.finfo(np.float32).eps)
+
+        warning_flag = 0
+        scores_ = []
+        for i in range(self.n_iter):
+            # Handle variance zero
+            if self.var_y:
+                A[0] = y_mean
+                active[0] = True
+                converged = True
+                break
+
+            XXa = XX[active, :][:, active]
+            XYa = XY[active]
+            Aa = A[active]
+
+            # mean & covariance of posterior distribution
+            Mn, Ri, cholesky = self._posterior_dist(Aa, beta, XXa, XYa)
+            if cholesky:
+                Sdiag = np.sum(Ri**2, 0)
+            else:
+                Sdiag = np.copy(np.diag(Ri))
+                warning_flag += 1
+
+            # raise warning in case cholesky fails
+            if warning_flag == 1:
+                warnings.warn(("Cholesky decomposition failed! Algorithm uses "
+                               "pinvh, which is significantly slower. If you "
+                               "use RVR it is advised to change parameters of "
+                               "the kernel!"))
+
+            # compute quality & sparsity parameters
+            s, q, S, Q = self._sparsity_quality(XX, XXd, XY, XYa, Aa, Ri,
+                                                active, beta, cholesky)
+
+            # update precision parameter for noise distribution
+            rss = np.sum((y - np.dot(X[:, active], Mn))**2)
+
+            # if near perfect fit , then terminate
+            if (rss / n_samples/var_y) < self.tol:
+                warnings.warn('Early termination due to near perfect fit')
+                converged = True
+                break
+            beta = n_samples - np.sum(active) + np.sum(Aa * Sdiag)
+            beta /= rss
+            # beta /= (rss + np.finfo(np.float32).eps)
+
+            # update precision parameters of coefficients
+            A, converged = update_precisions(Q, S, q, s, A, active, self.tol,
+                                             n_samples, False)
+
+            if self.compute_score:
+                scores_.append(self.log_marginal_like(XXa, XYa, Aa, beta))
+
+            if self.verbose:
+                print(('Iteration: {0}, number of features '
+                       'in the model: {1}').format(i, np.sum(active)))
+
+            if converged or i == self.n_iter - 1:
+                if converged and self.verbose:
+                    print('Algorithm converged!')
+                break
+
+        # after last update of alpha & beta update parameters
+        # of posterior distribution
+        XXa, XYa, Aa = XX[active, :][:, active], XY[active], A[active]
+        Mn, Sn, cholesky = self._posterior_dist(Aa, beta, XXa, XYa, True)
+        self.coef_ = np.zeros(n_features)
+        self.coef_[active] = Mn
+        self.sigma_ = Sn
+        self.active_ = active
+        self.lambda_ = A
+        self.alpha_ = beta
+        self.converged = converged
+        if self.compute_score:
+            self.scores_ = np.array(scores_)
+
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+        return self
+
+    def log_marginal_like(self, XXa, XYa, Aa, beta):
+        """Computes the log of the marginal likelihood."""
+        N, M = XXa.shape
+        A = np.diag(Aa)
+
+        Mn, sigma_, cholesky = self._posterior_dist(Aa, beta, XXa, XYa,
+                                                    full_covar=True)
+
+        C = sigma_ + np.dot(np.dot(XXa.T, np.linalg.pinv(A)), XXa)
+
+        score = np.dot(np.dot(XYa.T, np.linalg.pinv(C)), XYa) +\
+            np.log(np.linalg.det(C)) + N * np.log(2 * np.pi)
+
+        return -0.5 * score
+
+    def predict(self, X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance based on
+        Ref.[1] Section 3.3.2.
+
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of
+                    predictive distribution)
+
+                var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+        References
+        ----------
+        [1] Bishop, C. M. (2006). Pattern recognition and machine learning.
+        springer.
+        '''
+
+        y_hat = np.dot(X, self.coef_) + self.intercept_
+
+        if return_std:
+            # Handle the zero variance case
+            if self.var_y:
+                return y_hat, np.zeros_like(y_hat)
+
+            if self.normalize:
+                X -= self._x_mean_[self.active_]
+                X /= self._x_std[self.active_]
+            var_hat = 1./self.alpha_
+            var_hat += np.sum(X.dot(self.sigma_) * X, axis=1)
+            std_hat = np.sqrt(var_hat)
+            return y_hat, std_hat
+        else:
+            return y_hat
+
+    def _posterior_dist(self, A, beta, XX, XY, full_covar=False):
+        '''
+        Calculates mean and covariance matrix of posterior distribution
+        of coefficients.
+        '''
+        # compute precision matrix for active features
+        Sinv = beta * XX
+        np.fill_diagonal(Sinv, np.diag(Sinv) + A)
+        cholesky = True
+
+        # try cholesky, if it fails go back to pinvh
+        try:
+            # find posterior mean : R*R.T*mean = beta*X.T*Y
+            # solve(R*z = beta*X.T*Y) =>find z=> solve(R.T*mean = z)=>find mean
+            R = np.linalg.cholesky(Sinv)
+            Z = solve_triangular(R, beta*XY, check_finite=True, lower=True)
+            Mn = solve_triangular(R.T, Z, check_finite=True, lower=False)
+
+            # invert lower triangular matrix from cholesky decomposition
+            Ri = solve_triangular(R, np.eye(A.shape[0]), check_finite=False,
+                                  lower=True)
+            if full_covar:
+                Sn = np.dot(Ri.T, Ri)
+                return Mn, Sn, cholesky
+            else:
+                return Mn, Ri, cholesky
+        except LinAlgError:
+            cholesky = False
+            Sn = pinvh(Sinv)
+            Mn = beta*np.dot(Sinv, XY)
+            return Mn, Sn, cholesky
+
+    def _sparsity_quality(self, XX, XXd, XY, XYa, Aa, Ri, active, beta, cholesky):
+        '''
+        Calculates sparsity and quality parameters for each feature
+
+        Theoretical Note:
+        -----------------
+        Here we used Woodbury Identity for inverting covariance matrix
+        of target distribution
+        C    = 1/beta + 1/alpha * X' * X
+        C^-1 = beta - beta^2 * X * Sn * X'
+        '''
+        bxy = beta*XY
+        bxx = beta*XXd
+        if cholesky:
+            # here Ri is inverse of lower triangular matrix obtained from
+            # cholesky decomp
+            xxr = np.dot(XX[:, active], Ri.T)
+            rxy = np.dot(Ri, XYa)
+            S = bxx - beta**2 * np.sum(xxr**2, axis=1)
+            Q = bxy - beta**2 * np.dot(xxr, rxy)
+        else:
+            # here Ri is covariance matrix
+            XXa = XX[:, active]
+            XS = np.dot(XXa, Ri)
+            S = bxx - beta**2 * np.sum(XS*XXa, 1)
+            Q = bxy - beta**2 * np.dot(XS, XYa)
+        # Use following:
+        # (EQ 1) q = A*Q/(A - S) ; s = A*S/(A-S)
+        # so if A = np.PINF q = Q, s = S
+        qi = np.copy(Q)
+        si = np.copy(S)
+        # If A is not np.PINF, then it should be 'active' feature => use (EQ 1)
+        Qa, Sa = Q[active], S[active]
+        qi[active] = Aa * Qa / (Aa - Sa)
+        si[active] = Aa * Sa / (Aa - Sa)
+
+        return [si, qi, S, Q]
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/reg_fast_laplace.py b/examples/model-comparison/bayesvalidrox/surrogate_models/reg_fast_laplace.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fdcb5cf6e93c396d32eae2b0aad87a194a9cba4
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/reg_fast_laplace.py
@@ -0,0 +1,452 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+import numpy as np
+from sklearn.utils import as_float_array
+from sklearn.model_selection import KFold
+
+
+class RegressionFastLaplace():
+    '''
+    Sparse regression with Bayesian Compressive Sensing as described in Alg. 1
+    (Fast Laplace) of Ref.[1], which updated formulas from [2].
+
+    sigma2: noise precision (sigma^2)
+    nu fixed to 0
+
+    uqlab/lib/uq_regression/BCS/uq_bsc.m
+
+    Parameters
+    ----------
+    n_iter: int, optional (DEFAULT = 1000)
+        Maximum number of iterations
+
+    tol: float, optional (DEFAULT = 1e-7)
+        If absolute change in precision parameter for weights is below
+        threshold algorithm terminates.
+
+    fit_intercept : boolean, optional (DEFAULT = True)
+        whether to calculate the intercept for this model. If set
+        to false, no intercept will be used in calculations
+        (e.g. data is expected to be already centered).
+
+    copy_X : boolean, optional (DEFAULT = True)
+        If True, X will be copied; else, it may be overwritten.
+
+    verbose : boolean, optional (DEFAULT = FALSE)
+        Verbose mode when fitting the model
+
+    Attributes
+    ----------
+    coef_ : array, shape = (n_features)
+        Coefficients of the regression model (mean of posterior distribution)
+
+    alpha_ : float
+       estimated precision of the noise
+
+    active_ : array, dtype = np.bool, shape = (n_features)
+       True for non-zero coefficients, False otherwise
+
+    lambda_ : array, shape = (n_features)
+       estimated precisions of the coefficients
+
+    sigma_ : array, shape = (n_features, n_features)
+        estimated covariance matrix of the weights, computed only
+        for non-zero coefficients
+
+    References
+    ----------
+    [1] Babacan, S. D., Molina, R., & Katsaggelos, A. K. (2009). Bayesian
+        compressive sensing using Laplace priors. IEEE Transactions on image
+        processing, 19(1), 53-63.
+    [2] Fast marginal likelihood maximisation for sparse Bayesian models
+        (Tipping & Faul 2003).
+        (http://www.miketipping.com/papers/met-fastsbl.pdf)
+    '''
+
+    def __init__(self, n_iter=1000, n_Kfold=10, tol=1e-7, fit_intercept=False,
+                 bias_term=True, copy_X=True, verbose=False):
+        self.n_iter = n_iter
+        self.n_Kfold = n_Kfold
+        self.tol = tol
+        self.fit_intercept = fit_intercept
+        self.bias_term = bias_term
+        self.copy_X = copy_X
+        self.verbose = verbose
+
+    def _center_data(self, X, y):
+        ''' Centers data'''
+        X = as_float_array(X, copy = self.copy_X)
+
+        # normalisation should be done in preprocessing!
+        X_std = np.ones(X.shape[1], dtype=X.dtype)
+        if self.fit_intercept:
+            X_mean = np.average(X, axis=0)
+            y_mean = np.average(y, axis=0)
+            X -= X_mean
+            y -= y_mean
+        else:
+            X_mean = np.zeros(X.shape[1], dtype=X.dtype)
+            y_mean = 0. if y.ndim == 1 else np.zeros(y.shape[1], dtype=X.dtype)
+        return X, y, X_mean, y_mean, X_std
+
+    def fit(self, X, y):
+
+        k_fold = KFold(n_splits=self.n_Kfold)
+
+        varY = np.var(y, ddof=1) if np.var(y, ddof=1) != 0 else 1.0
+        sigma2s = len(y)*varY*(10**np.linspace(-16, -1, self.n_Kfold))
+
+        errors = np.zeros((len(sigma2s), self.n_Kfold))
+        for s, sigma2 in enumerate(sigma2s):
+            for k, (train, test) in enumerate(k_fold.split(X, y)):
+                self.fit_(X[train], y[train], sigma2)
+                errors[s, k] = np.linalg.norm(
+                    y[test] - self.predict(X[test])
+                    )**2/len(test)
+
+        KfCVerror = np.sum(errors, axis=1)/self.n_Kfold/varY
+        i_minCV = np.argmin(KfCVerror)
+
+        self.kfoldCVerror = np.min(KfCVerror)
+
+        return self.fit_(X, y, sigma2s[i_minCV])
+
+    def fit_(self, X, y, sigma2):
+
+        N, P = X.shape
+        # n_samples, n_features = X.shape
+
+        X, y, X_mean, y_mean, X_std = self._center_data(X, y)
+        self._x_mean_ = X_mean
+        self._y_mean = y_mean
+        self._x_std = X_std
+
+        # check that variance is non zero !!!
+        if np.var(y) == 0:
+            self.var_y = True
+        else:
+            self.var_y = False
+        beta = 1./sigma2
+
+        #  precompute X'*Y , X'*X for faster iterations & allocate memory for
+        #  sparsity & quality vectors X=Psi
+        PsiTY = np.dot(X.T, y)
+        PsiTPsi = np.dot(X.T, X)
+        XXd = np.diag(PsiTPsi)
+
+        # initialize with constant regressor, or if that one does not exist,
+        # with the one that has the largest correlation with Y
+        ind_global_to_local = np.zeros(P, dtype=np.int32)
+
+        # identify constant regressors
+        constidx = np.where(~np.diff(X, axis=0).all(axis=0))[0]
+
+        if self.bias_term and constidx.size != 0:
+            ind_start = constidx[0]
+            ind_global_to_local[ind_start] = True
+        else:
+            # start from a single basis vector with largest projection on
+            # targets
+            proj = np.divide(np.square(PsiTY), XXd)
+            ind_start = np.argmax(proj)
+            ind_global_to_local[ind_start] = True
+
+        num_active = 1
+        active_indices = [ind_start]
+        deleted_indices = []
+        bcs_path = [ind_start]
+        gamma = np.zeros(P)
+        # for the initial value of gamma(ind_start), use the RVM formula
+        #   gamma = (q^2 - s) / (s^2)
+        # and the fact that initially s = S = beta*Psi_i'*Psi_i and q = Q =
+        # beta*Psi_i'*Y
+        gamma[ind_start] = np.square(PsiTY[ind_start])
+        gamma[ind_start] -= sigma2 * PsiTPsi[ind_start, ind_start]
+        gamma[ind_start] /= np.square(PsiTPsi[ind_start, ind_start])
+
+        Sigma = 1. / (beta * PsiTPsi[ind_start, ind_start]
+                      + 1./gamma[ind_start])
+
+        mu = Sigma * PsiTY[ind_start] * beta
+        tmp1 = beta * PsiTPsi[ind_start]
+        S = beta * np.diag(PsiTPsi).T - Sigma * np.square(tmp1)
+        Q = beta * PsiTY.T - mu*(tmp1)
+
+        tmp2 = np.ones(P)  # alternative computation for the initial s,q
+        q0tilde = PsiTY[ind_start]
+        s0tilde = PsiTPsi[ind_start, ind_start]
+        tmp2[ind_start] = s0tilde / (q0tilde**2) / beta
+        s = np.divide(S, tmp2)
+        q = np.divide(Q, tmp2)
+        Lambda = 2*(num_active - 1) / np.sum(gamma)
+
+        Delta_L_max = []
+        for i in range(self.n_iter):
+            # Handle variance zero
+            if self.var_y:
+                mu = np.mean(y)
+                break
+
+            if self.verbose:
+                print('    lambda = {0:.6e}\n'.format(Lambda))
+
+            # Calculate the potential updated value of each gamma[i]
+            if Lambda == 0.0:  # RVM
+                gamma_potential = np.multiply((
+                    (q**2 - s) > Lambda),
+                    np.divide(q**2 - s, s**2)
+                    )
+            else:
+                a = Lambda * s**2
+                b = s**2 + 2*Lambda*s
+                c = Lambda + s - q**2
+                gamma_potential = np.multiply(
+                    (c < 0), np.divide(
+                        -b + np.sqrt(b**2 - 4*np.multiply(a, c)), 2*a)
+                    )
+
+            l_gamma = - np.log(np.absolute(1 + np.multiply(gamma, s)))
+            l_gamma += np.divide(np.multiply(q**2, gamma),
+                                 (1 + np.multiply(gamma, s)))
+            l_gamma -= Lambda*gamma  # omitted the factor 1/2
+
+            # Contribution of each updated gamma(i) to L(gamma)
+            l_gamma_potential = - np.log(
+                np.absolute(1 + np.multiply(gamma_potential, s))
+                )
+            l_gamma_potential += np.divide(
+                np.multiply(q**2, gamma_potential),
+                (1 + np.multiply(gamma_potential, s))
+                )
+            # omitted the factor 1/2
+            l_gamma_potential -= Lambda*gamma_potential
+
+            # Check how L(gamma) would change if we replaced gamma(i) by the
+            # updated gamma_potential(i), for each i separately
+            Delta_L_potential = l_gamma_potential - l_gamma
+
+            # deleted indices should not be chosen again
+            if len(deleted_indices) != 0:
+                values = -np.inf * np.ones(len(deleted_indices))
+                Delta_L_potential[deleted_indices] = values
+
+            Delta_L_max.append(np.nanmax(Delta_L_potential))
+            ind_L_max = np.nanargmax(Delta_L_potential)
+
+            # in case there is only 1 regressor in the model and it would now
+            # be deleted
+            if len(active_indices) == 1 and ind_L_max == active_indices[0] \
+               and gamma_potential[ind_L_max] == 0.0:
+                Delta_L_potential[ind_L_max] = -np.inf
+                Delta_L_max[i] = np.max(Delta_L_potential)
+                ind_L_max = np.argmax(Delta_L_potential)
+
+            # If L did not change significantly anymore, break
+            if Delta_L_max[i] <= 0.0 or\
+                    (i > 0 and all(np.absolute(Delta_L_max[i-1:])
+                                   < sum(Delta_L_max)*self.tol)) or \
+                    (i > 0 and all(np.diff(bcs_path)[i-1:] == 0.0)):
+                if self.verbose:
+                    print('Increase in L: {0:.6e} (eta = {1:.3e})\
+                          -- break\n'.format(Delta_L_max[i], self.tol))
+                break
+
+            # Print information
+            if self.verbose:
+                print('    Delta L = {0:.6e} \n'.format(Delta_L_max[i]))
+
+            what_changed = int(gamma[ind_L_max] == 0.0)
+            what_changed -= int(gamma_potential[ind_L_max] == 0.0)
+
+            # Print information
+            if self.verbose:
+                if what_changed < 0:
+                    print(f'{i+1} - Remove regressor #{ind_L_max+1}..\n')
+                elif what_changed == 0:
+                    print(f'{i+1} - Recompute regressor #{ind_L_max+1}..\n')
+                else:
+                    print(f'{i+1} - Add regressor #{ind_L_max+1}..\n')
+
+            # --- Update all quantities ----
+            if what_changed == 1:
+                # adding a regressor
+
+                # update gamma
+                gamma[ind_L_max] = gamma_potential[ind_L_max]
+
+                Sigma_ii = 1.0 / (1.0/gamma[ind_L_max] + S[ind_L_max])
+                try:
+                    x_i = np.matmul(
+                        Sigma, PsiTPsi[active_indices, ind_L_max].reshape(-1, 1)
+                        )
+                except ValueError:
+                    x_i = Sigma * PsiTPsi[active_indices, ind_L_max]
+                tmp_1 = - (beta * Sigma_ii) * x_i
+                Sigma = np.vstack(
+                    (np.hstack(((beta**2 * Sigma_ii) * np.dot(x_i, x_i.T)
+                                + Sigma, tmp_1)), np.append(tmp_1.T, Sigma_ii))
+                    )
+                mu_i = Sigma_ii * Q[ind_L_max]
+                mu = np.vstack((mu - (beta * mu_i) * x_i, mu_i))
+
+                tmp2_1 = PsiTPsi[:, ind_L_max] - beta * np.squeeze(
+                    np.matmul(PsiTPsi[:, active_indices], x_i)
+                    )
+                if i == 0:
+                    tmp2_1[0] /= 2
+                tmp2 = beta * tmp2_1.T
+                S = S - Sigma_ii * np.square(tmp2)
+                Q = Q - mu_i * tmp2
+
+                num_active += 1
+                ind_global_to_local[ind_L_max] = num_active
+                active_indices.append(ind_L_max)
+                bcs_path.append(ind_L_max)
+
+            elif what_changed == 0:
+                # recomputation
+                # zero if regressor has not been chosen yet
+                if not ind_global_to_local[ind_L_max]:
+                    raise Exception('Cannot recompute index{0} -- not yet\
+                                    part of the model!'.format(ind_L_max))
+                Sigma = np.atleast_2d(Sigma)
+                mu = np.atleast_2d(mu)
+                gamma_i_new = gamma_potential[ind_L_max]
+                gamma_i_old = gamma[ind_L_max]
+                # update gamma
+                gamma[ind_L_max] = gamma_potential[ind_L_max]
+
+                # index of regressor in Sigma
+                local_ind = ind_global_to_local[ind_L_max]-1
+
+                kappa_i = (1.0/gamma_i_new - 1.0/gamma_i_old)
+                kappa_i = 1.0 / kappa_i
+                kappa_i += Sigma[local_ind, local_ind]
+                kappa_i = 1 / kappa_i
+                Sigma_i_col = Sigma[:, local_ind]
+
+                Sigma = Sigma - kappa_i * (Sigma_i_col * Sigma_i_col.T)
+                mu_i = mu[local_ind]
+                mu = mu - (kappa_i * mu_i) * Sigma_i_col[:, None]
+
+                tmp1 = beta * np.dot(
+                    Sigma_i_col.reshape(1, -1), PsiTPsi[active_indices])[0]
+                S = S + kappa_i * np.square(tmp1)
+                Q = Q + (kappa_i * mu_i) * tmp1
+
+                # no change in active_indices or ind_global_to_local
+                bcs_path.append(ind_L_max + 0.1)
+
+            elif what_changed == -1:
+                gamma[ind_L_max] = 0
+
+                # index of regressor in Sigma
+                local_ind = ind_global_to_local[ind_L_max]-1
+
+                Sigma_ii_inv = 1. / Sigma[local_ind, local_ind]
+                Sigma_i_col = Sigma[:, local_ind]
+
+                Sigma = Sigma - Sigma_ii_inv * (Sigma_i_col * Sigma_i_col.T)
+
+                Sigma = np.delete(
+                    np.delete(Sigma, local_ind, axis=0), local_ind, axis=1)
+
+                mu = mu - (mu[local_ind] * Sigma_ii_inv) * Sigma_i_col[:, None]
+                mu = np.delete(mu, local_ind, axis=0)
+
+                tmp1 = beta * np.dot(Sigma_i_col, PsiTPsi[active_indices])
+                S = S + Sigma_ii_inv * np.square(tmp1)
+                Q = Q + (mu_i * Sigma_ii_inv) * tmp1
+
+                num_active -= 1
+                ind_global_to_local[ind_L_max] = 0.0
+                v = ind_global_to_local[ind_global_to_local > local_ind] - 1
+                ind_global_to_local[ind_global_to_local > local_ind] = v
+                del active_indices[local_ind]
+                deleted_indices.append(ind_L_max)
+                # and therefore ineligible
+                bcs_path.append(-ind_L_max)
+
+            # same for all three cases
+            tmp3 = 1 - np.multiply(gamma, S)
+            s = np.divide(S, tmp3)
+            q = np.divide(Q, tmp3)
+
+            # Update lambda
+            Lambda = 2*(num_active - 1) / np.sum(gamma)
+
+        # Prepare the result object
+        self.coef_ = np.zeros(P)
+        self.coef_[active_indices] = np.squeeze(mu)
+        self.sigma_ = Sigma
+        self.active_ = active_indices
+        self.gamma = gamma
+        self.Lambda = Lambda
+        self.beta = beta
+        self.bcs_path = bcs_path
+
+        # set intercept_
+        if self.fit_intercept:
+            self.coef_ = self.coef_ / X_std
+            self.intercept_ = y_mean - np.dot(X_mean, self.coef_.T)
+        else:
+            self.intercept_ = 0.
+
+        return self
+
+    def predict(self, X, return_std=False):
+        '''
+        Computes predictive distribution for test set.
+        Predictive distribution for each data point is one dimensional
+        Gaussian and therefore is characterised by mean and variance based on
+        Ref.[1] Section 3.3.2.
+
+        Parameters
+        -----------
+        X: {array-like, sparse} (n_samples_test, n_features)
+           Test data, matrix of explanatory variables
+
+        Returns
+        -------
+        : list of length two [y_hat, var_hat]
+
+             y_hat: numpy array of size (n_samples_test,)
+                    Estimated values of targets on test set (i.e. mean of
+                    predictive distribution)
+
+                var_hat: numpy array of size (n_samples_test,)
+                    Variance of predictive distribution
+
+        References
+        ----------
+        [1] Bishop, C. M. (2006). Pattern recognition and machine learning.
+        springer.
+        '''
+        y_hat = np.dot(X, self.coef_) + self.intercept_
+
+        if return_std:
+            # Handle the zero variance case
+            if self.var_y:
+                return y_hat, np.zeros_like(y_hat)
+
+            var_hat = 1./self.beta
+            var_hat += np.sum(X.dot(self.sigma_) * X, axis=1)
+            std_hat = np.sqrt(var_hat)
+            return y_hat, std_hat
+        else:
+            return y_hat
+
+# l2norm = 0.0
+# for idx in range(10):
+#     sigma2 = np.genfromtxt('./test/sigma2_{0}.csv'.format(idx+1), delimiter=',')
+#     Psi_train = np.genfromtxt('./test/Psi_train_{0}.csv'.format(idx+1), delimiter=',')
+#     Y_train = np.genfromtxt('./test/Y_train_{0}.csv'.format(idx+1))
+#     Psi_test = np.genfromtxt('./test/Psi_test_{0}.csv'.format(idx+1), delimiter=',')
+#     Y_test = np.genfromtxt('./test/Y_test_{0}.csv'.format(idx+1))
+
+#     clf = RegressionFastLaplace(verbose=True)
+#     clf.fit_(Psi_train, Y_train, sigma2)
+#     coeffs_fold = np.genfromtxt('./test/coeffs_fold_{0}.csv'.format(idx+1))
+#     print("coeffs error: {0:.4g}".format(np.linalg.norm(clf.coef_ - coeffs_fold)))
+#     l2norm += np.linalg.norm(Y_test - clf.predict(Psi_test))**2/len(Y_test)
+#     print("l2norm error: {0:.4g}".format(l2norm))
diff --git a/examples/model-comparison/bayesvalidrox/surrogate_models/surrogate_models.py b/examples/model-comparison/bayesvalidrox/surrogate_models/surrogate_models.py
new file mode 100644
index 0000000000000000000000000000000000000000..ca902f26bef0c45e8befb72ff67313ef09a77603
--- /dev/null
+++ b/examples/model-comparison/bayesvalidrox/surrogate_models/surrogate_models.py
@@ -0,0 +1,1576 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Implementation of metamodel as either PC, aPC or GPE
+"""
+
+import warnings
+import numpy as np
+import math
+import h5py
+import matplotlib.pyplot as plt
+from sklearn.preprocessing import MinMaxScaler
+import scipy as sp
+from scipy.optimize import minimize, NonlinearConstraint, LinearConstraint
+from tqdm import tqdm
+from sklearn.decomposition import PCA as sklearnPCA
+import sklearn.linear_model as lm
+from sklearn.gaussian_process import GaussianProcessRegressor
+import sklearn.gaussian_process.kernels as kernels
+import os
+from joblib import Parallel, delayed
+import copy
+
+from .input_space import InputSpace
+from .glexindex import glexindex
+from .eval_rec_rule import eval_univ_basis
+from .reg_fast_ard import RegressionFastARD
+from .reg_fast_laplace import RegressionFastLaplace
+from .orthogonal_matching_pursuit import OrthogonalMatchingPursuit
+from .bayes_linear import VBLinearRegression, EBLinearRegression
+from .apoly_construction import apoly_construction
+warnings.filterwarnings("ignore")
+# Load the mplstyle
+plt.style.use(os.path.join(os.path.split(__file__)[0],
+                           '../', 'bayesvalidrox.mplstyle'))
+
+
+class MetaModel():
+    """
+    Meta (surrogate) model
+
+    This class trains a surrogate model. It accepts an input object (input_obj)
+    containing the specification of the distributions for uncertain parameters
+    and a model object with instructions on how to run the computational model.
+
+    Attributes
+    ----------
+    input_obj : obj
+        Input object with the information on the model input parameters.
+    meta_model_type : str
+        Surrogate model types. Three surrogate model types are supported:
+        polynomial chaos expansion (`PCE`), arbitrary PCE (`aPCE`) and
+        Gaussian process regression (`GPE`). Default is PCE.
+    pce_reg_method : str
+        PCE regression method to compute the coefficients. The following
+        regression methods are available:
+
+        1. OLS: Ordinary Least Square method
+        2. BRR: Bayesian Ridge Regression
+        3. LARS: Least angle regression
+        4. ARD: Bayesian ARD Regression
+        5. FastARD: Fast Bayesian ARD Regression
+        6. VBL: Variational Bayesian Learning
+        7. EBL: Emperical Bayesian Learning
+        Default is `OLS`.
+    bootstrap_method : str
+        Bootstraping method. Options are `'normal'` and `'fast'`. The default
+        is `'fast'`. It means that in each iteration except the first one, only
+        the coefficent are recalculated with the ordinary least square method.
+    n_bootstrap_itrs : int
+        Number of iterations for the bootstrap sampling. The default is `1`.
+    pce_deg : int or list of int
+        Polynomial degree(s). If a list is given, an adaptive algorithm is used
+        to find the best degree with the lowest Leave-One-Out cross-validation
+        (LOO) error (or the highest score=1-LOO). Default is `1`.
+    pce_q_norm : float
+        Hyperbolic (or q-norm) truncation for multi-indices of multivariate
+        polynomials. Default is `1.0`.
+    dim_red_method : str
+        Dimensionality reduction method for the output space. The available
+        method is based on principal component analysis (PCA). The Default is
+        `'no'`. There are two ways to select number of components: use
+        percentage of the explainable variance threshold (between 0 and 100)
+        (Option A) or direct prescription of components' number (Option B):
+
+            >>> MetaModelOpts.dim_red_method = 'PCA'
+            >>> MetaModelOpts.var_pca_threshold = 99.999  # Option A
+            >>> MetaModelOpts.n_pca_components = 12 # Option B
+    apply_constraints : bool
+        If set to true constraints will be applied during training. 
+        In this case the training uses OLS. In this version the constraints 
+        need to be set explicitly in this class.
+
+    verbose : bool
+        Prints summary of the regression results. Default is `False`.
+
+    Note
+    -------
+    To define the sampling methods and the training set, an experimental design
+    instance shall be defined. This can be done by:
+
+    >>> MetaModelOpts.add_InputSpace()
+
+    Two experimental design schemes are supported: one-shot (`normal`) and
+    adaptive sequential (`sequential`) designs.
+    For experimental design refer to `InputSpace`.
+
+    """
+
+    def __init__(self, input_obj, meta_model_type='PCE',
+                 pce_reg_method='OLS', bootstrap_method='fast',
+                 n_bootstrap_itrs=1, pce_deg=1, pce_q_norm=1.0,
+                 dim_red_method='no', apply_constraints = False, 
+                 verbose=False):
+
+        self.input_obj = input_obj
+        self.meta_model_type = meta_model_type
+        self.pce_reg_method = pce_reg_method
+        self.bootstrap_method = bootstrap_method
+        self.n_bootstrap_itrs = n_bootstrap_itrs
+        self.pce_deg = pce_deg
+        self.pce_q_norm = pce_q_norm
+        self.dim_red_method = dim_red_method
+        self.apply_constraints = apply_constraints
+        self.verbose = verbose
+ 
+    def build_metamodel(self, n_init_samples = None) -> None:
+        """
+        Builds the parts for the metamodel (polynomes,...) that are neede before fitting.
+
+        Returns
+        -------
+        None
+            DESCRIPTION.
+
+        """
+        
+        # Generate general warnings
+        if self.apply_constraints or self.pce_reg_method.lower() == 'ols':
+            print('There are no estimations of surrogate uncertainty available'
+                  ' for the chosen regression options. This might lead to issues'
+                  ' in later steps.')
+        
+        # Add InputSpace to MetaModel if it does not have any
+        if not hasattr(self, 'InputSpace'):
+            self.InputSpace = InputSpace(self.input_obj)
+            self.InputSpace.n_init_samples = n_init_samples
+            self.InputSpace.init_param_space(np.max(self.pce_deg))
+            
+        self.ndim = self.InputSpace.ndim
+        
+        if not hasattr(self, 'CollocationPoints'):
+            raise AttributeError('Please provide samples to the metamodel before building it.')
+            
+        # Transform input samples
+        # TODO: this is probably not yet correct! Make 'method' variable
+        self.CollocationPoints = self.InputSpace.transform(self.CollocationPoints, method='user') 
+
+        
+        self.n_params = len(self.input_obj.Marginals)
+        
+        # Generate polynomials
+        if self.meta_model_type.lower() != 'gpe':
+            self.generate_polynomials(np.max(self.pce_deg))
+
+        # Initialize the nested dictionaries
+        if self.meta_model_type.lower() == 'gpe':
+            self.gp_poly = self.auto_vivification()
+            self.x_scaler = self.auto_vivification()
+            self.LCerror = self.auto_vivification()
+        else:
+            self.deg_dict = self.auto_vivification()
+            self.q_norm_dict = self.auto_vivification()
+            self.coeffs_dict = self.auto_vivification()
+            self.basis_dict = self.auto_vivification()
+            self.score_dict = self.auto_vivification()
+            self.clf_poly = self.auto_vivification()
+            self.LCerror = self.auto_vivification()
+        if self.dim_red_method.lower() == 'pca':
+            self.pca = self.auto_vivification()
+
+        # Define an array containing the degrees
+        self.CollocationPoints = np.array(self.CollocationPoints)
+        self.n_samples, ndim = self.CollocationPoints.shape
+        if self.ndim != ndim:
+            raise AttributeError('The given samples do not match the given number of priors. The samples should be a 2D array of size (#samples, #priors)')
+            
+        self.deg_array = self.__select_degree(ndim, self.n_samples)
+
+        # Generate all basis indices
+        self.allBasisIndices = self.auto_vivification()
+        for deg in self.deg_array:
+            keys = self.allBasisIndices.keys()
+            if deg not in np.fromiter(keys, dtype=float):
+                # Generate the polynomial basis indices
+                for qidx, q in enumerate(self.pce_q_norm):
+                    basis_indices = glexindex(start=0, stop=deg+1,
+                                              dimensions=self.n_params,
+                                              cross_truncation=q,
+                                              reverse=False, graded=True)
+                    self.allBasisIndices[str(deg)][str(q)] = basis_indices
+
+        
+        
+    def fit(self, X, y, parallel = True, verbose = False):
+        """
+        Fits the surrogate to the given data (samples X, outputs y).
+        Note here that the samples X should be the transformed samples provided
+        by the experimental design if the transformation is used there.
+
+        Parameters
+        ----------
+        X : 2D list or np.array of shape (#samples, #dim)
+            The parameter value combinations that the model was evaluated at.
+        y : dict of 2D lists or arrays of shape (#samples, #timesteps)
+            The respective model evaluations.
+
+        Returns
+        -------
+        None.
+
+        """
+        X = np.array(X)
+        for key in y.keys():
+            y_val = np.array(y[key])
+            if y_val.ndim !=2:
+                raise ValueError('The given outputs y should be 2D')
+            y[key] = np.array(y[key])
+        
+        # Output names are the same as the keys in y
+        self.out_names = list(y.keys())
+        
+        # Build the MetaModel on the static samples
+        self.CollocationPoints = X
+        
+        # TODO: other option: rebuild every time
+        if not hasattr(self, 'deg_array'):
+            self.build_metamodel(n_init_samples = X.shape[1])
+            
+        # Evaluate the univariate polynomials on InputSpace
+        if self.meta_model_type.lower() != 'gpe':
+           self.univ_p_val = self.univ_basis_vals(self.CollocationPoints)
+        
+        # --- Loop through data points and fit the surrogate ---
+        if verbose:
+            print(f"\n>>>> Training the {self.meta_model_type} metamodel "
+                  "started. <<<<<<\n")
+
+        # --- Bootstrap sampling ---
+        # Correct number of bootstrap if PCA transformation is required.
+        if self.dim_red_method.lower() == 'pca' and self.n_bootstrap_itrs == 1:
+            self.n_bootstrap_itrs = 100
+
+        # Check if fast version (update coeffs with OLS) is selected.
+        if self.bootstrap_method.lower() == 'fast':
+            fast_bootstrap = True
+            first_out = {}
+            n_comp_dict = {}
+        else:
+            fast_bootstrap = False
+
+        # Prepare tqdm iteration maessage
+        if verbose and self.n_bootstrap_itrs > 1:
+            enum_obj = tqdm(range(self.n_bootstrap_itrs),
+                            total=self.n_bootstrap_itrs,
+                            desc="Bootstrapping the metamodel",
+                            ascii=True)
+        else:
+            enum_obj = range(self.n_bootstrap_itrs)
+
+        # Loop over the bootstrap iterations
+        for b_i in enum_obj:
+            if b_i > 0:
+                b_indices = np.random.randint(self.n_samples, size=self.n_samples)
+            else:
+                b_indices = np.arange(len(X))
+
+            X_train_b = X[b_indices]
+
+            if verbose and self.n_bootstrap_itrs == 1:
+                items = tqdm(y.items(), desc="Fitting regression")
+            else:
+                items = y.items()
+
+            # For loop over the components/outputs
+            for key, Output in items:
+
+                # Dimensionality reduction with PCA, if specified
+                if self.dim_red_method.lower() == 'pca':
+
+                    # Use the stored n_comp for fast bootsrtrapping
+                    if fast_bootstrap and b_i > 0:
+                        self.n_pca_components = n_comp_dict[key]
+
+                    # Start transformation
+                    pca, target, n_comp = self.pca_transformation(
+                        Output[b_indices], verbose=False
+                        )
+                    self.pca[f'b_{b_i+1}'][key] = pca
+                    # Store the number of components for fast bootsrtrapping
+                    if fast_bootstrap and b_i == 0:
+                        n_comp_dict[key] = n_comp
+                else:
+                    target = Output[b_indices]
+
+                # Parallel fit regression
+                if self.meta_model_type.lower() == 'gpe':
+                    # Prepare the input matrix
+                    scaler = MinMaxScaler()
+                    X_S = scaler.fit_transform(X_train_b)
+
+                    self.x_scaler[f'b_{b_i+1}'][key] = scaler
+                    if parallel:
+                        out = Parallel(n_jobs=-1, backend='multiprocessing')(
+                            delayed(self.gaussian_process_emulator)(
+                                X_S, target[:, idx]) for idx in
+                            range(target.shape[1]))
+                    else:
+                        results = map(self.gaussian_process_emulator,
+                                      [X_train_b]*target.shape[1],
+                                      [target[:, idx] for idx in
+                                       range(target.shape[1])]
+                                      )
+                        out = list(results)
+
+                    for idx in range(target.shape[1]):
+                        self.gp_poly[f'b_{b_i+1}'][key][f"y_{idx+1}"] = out[idx]
+
+                else:
+                    self.univ_p_val = self.univ_p_val[b_indices]
+                    if parallel and (not fast_bootstrap or b_i == 0):
+                        out = Parallel(n_jobs=-1, backend='multiprocessing')(
+                            delayed(self.adaptive_regression)(X_train_b,
+                                                              target[:, idx],
+                                                              idx)
+                            for idx in range(target.shape[1]))
+                    elif not parallel and (not fast_bootstrap or b_i == 0):
+                        results = map(self.adaptive_regression,
+                                      [X_train_b]*target.shape[1],
+                                      [target[:, idx] for idx in
+                                       range(target.shape[1])],
+                                      range(target.shape[1]))
+                        out = list(results)
+
+                    # Store the first out dictionary
+                    if fast_bootstrap and b_i == 0:
+                        first_out[key] = copy.deepcopy(out)
+
+                    if b_i > 0 and fast_bootstrap:
+
+                        # fast bootstrap
+                        out = self.update_pce_coeffs(
+                            X_train_b, target, first_out[key])
+
+                    for i in range(target.shape[1]):
+                        # Create a dict to pass the variables
+                        self.deg_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['degree']
+                        self.q_norm_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['qnorm']
+                        self.coeffs_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['coeffs']
+                        self.basis_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['multi_indices']
+                        self.score_dict[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['LOOCVScore']
+                        self.clf_poly[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['clf_poly']
+                        #self.LCerror[f'b_{b_i+1}'][key][f"y_{i+1}"] = out[i]['LCerror']
+
+        if verbose:
+            print(f"\n>>>> Training the {self.meta_model_type} metamodel"
+                  " sucessfully completed. <<<<<<\n")
+
+    # -------------------------------------------------------------------------
+    def update_pce_coeffs(self, X, y, out_dict = None):
+        """
+        Updates the PCE coefficents using only the ordinary least square method
+        for the fast version of the bootstrapping.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples, n_params)
+            Training set.
+        y : array of shape (n_samples, n_outs)
+            The (transformed) model responses.
+        out_dict : dict
+            The training output dictionary of the first iteration, i.e.
+            the surrogate model for the original experimental design.
+
+        Returns
+        -------
+        final_out_dict : dict
+            The updated training output dictionary.
+
+        """
+        # Make a copy
+        final_out_dict = copy.deepcopy(out_dict)
+
+        # Loop over the points
+        for i in range(y.shape[1]):
+
+                    
+            # Extract nonzero basis indices
+            nnz_idx = np.nonzero(out_dict[i]['coeffs'])[0]
+            if len(nnz_idx) != 0:
+                basis_indices = out_dict[i]['multi_indices']
+
+                # Evaluate the multivariate polynomials on CollocationPoints
+                psi = self.create_psi(basis_indices, self.univ_p_val)
+
+                # Calulate the cofficients of surrogate model
+                updated_out = self.regression(
+                    psi, y[:, i], basis_indices, reg_method='OLS',
+                    sparsity=False
+                    )
+
+                # Update coeffs in out_dict
+                final_out_dict[i]['coeffs'][nnz_idx] = updated_out['coeffs']
+
+        return final_out_dict
+
+    # -------------------------------------------------------------------------
+    def add_InputSpace(self):
+        """
+        Instanciates experimental design object.
+
+        Returns
+        -------
+        None.
+
+        """
+        self.InputSpace = InputSpace(self.input_obj,
+                                    meta_Model_type=self.meta_model_type)
+
+    # -------------------------------------------------------------------------
+    def univ_basis_vals(self, samples, n_max=None):
+        """
+        Evaluates univariate regressors along input directions.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params)
+            Samples.
+        n_max : int, optional
+            Maximum polynomial degree. The default is `None`.
+
+        Returns
+        -------
+        univ_basis: array of shape (n_samples, n_params, n_max+1)
+            All univariate regressors up to n_max.
+        """
+        # Extract information
+        poly_types = self.InputSpace.poly_types
+        if samples.ndim != 2:
+            samples = samples.reshape(1, len(samples))
+        n_max = np.max(self.pce_deg) if n_max is None else n_max
+
+        # Extract poly coeffs
+        if self.InputSpace.input_data_given or self.InputSpace.apce:
+            apolycoeffs = self.polycoeffs
+        else:
+            apolycoeffs = None
+
+        # Evaluate univariate basis
+        univ_basis = eval_univ_basis(samples, n_max, poly_types, apolycoeffs)
+
+        return univ_basis
+
+    # -------------------------------------------------------------------------
+    def create_psi(self, basis_indices, univ_p_val):
+        """
+        This function assemble the design matrix Psi from the given basis index
+        set INDICES and the univariate polynomial evaluations univ_p_val.
+
+        Parameters
+        ----------
+        basis_indices : array of shape (n_terms, n_params)
+            Multi-indices of multivariate polynomials.
+        univ_p_val : array of (n_samples, n_params, n_max+1)
+            All univariate regressors up to `n_max`.
+
+        Raises
+        ------
+        ValueError
+            n_terms in arguments do not match.
+
+        Returns
+        -------
+        psi : array of shape (n_samples, n_terms)
+            Multivariate regressors.
+
+        """
+        # Check if BasisIndices is a sparse matrix
+        sparsity = sp.sparse.issparse(basis_indices)
+        if sparsity:
+            basis_indices = basis_indices.toarray()
+
+        # Initialization and consistency checks
+        # number of input variables
+        n_params = univ_p_val.shape[1]
+
+        # Size of the experimental design
+        n_samples = univ_p_val.shape[0]
+
+        # number of basis terms
+        n_terms = basis_indices.shape[0]
+
+        # check that the variables have consistent sizes
+        if n_params != basis_indices.shape[1]:
+            raise ValueError(
+                f"The shapes of basis_indices ({basis_indices.shape[1]}) and "
+                f"univ_p_val ({n_params}) don't match!!"
+                )
+
+        # Preallocate the Psi matrix for performance
+        psi = np.ones((n_samples, n_terms))
+        # Assemble the Psi matrix
+        for m in range(basis_indices.shape[1]):
+            aa = np.where(basis_indices[:, m] > 0)[0]
+            try:
+                basisIdx = basis_indices[aa, m]
+                bb = univ_p_val[:, m, basisIdx].reshape(psi[:, aa].shape)
+                psi[:, aa] = np.multiply(psi[:, aa], bb)
+            except ValueError as err:
+                raise err
+        return psi
+
+    # -------------------------------------------------------------------------
+    def regression(self, X, y, basis_indices, reg_method=None, sparsity=True):
+        """
+        Fit regression using the regression method provided.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples, n_features)
+            Training vector, where n_samples is the number of samples and
+            n_features is the number of features.
+        y : array of shape (n_samples,)
+            Target values.
+        basis_indices : array of shape (n_terms, n_params)
+            Multi-indices of multivariate polynomials.
+        reg_method : str, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        return_out_dict : Dict
+            Fitted estimator, spareMulti-Index, sparseX and coefficients.
+
+        """
+        if reg_method is None:
+            reg_method = self.pce_reg_method
+
+        bias_term = self.dim_red_method.lower() != 'pca'
+
+        compute_score = True if self.verbose else False
+
+        #  inverse of the observed variance of the data
+        if np.var(y) != 0:
+            Lambda = 1 / np.var(y)
+        else:
+            Lambda = 1e-6
+
+        # Bayes sparse adaptive aPCE
+        if reg_method.lower() == 'ols':
+            clf_poly = lm.LinearRegression(fit_intercept=False)
+        elif reg_method.lower() == 'brr':
+            clf_poly = lm.BayesianRidge(n_iter=1000, tol=1e-7,
+                                        fit_intercept=False,
+                                        #normalize=True,
+                                        compute_score=compute_score,
+                                        alpha_1=1e-04, alpha_2=1e-04,
+                                        lambda_1=Lambda, lambda_2=Lambda)
+            clf_poly.converged = True
+
+        elif reg_method.lower() == 'ard':
+            if X.shape[0]<2:
+                raise ValueError('Regression with ARD can only be performed for more than 2 samples')
+            clf_poly = lm.ARDRegression(fit_intercept=False,
+                                        #normalize=True,
+                                        compute_score=compute_score,
+                                        n_iter=1000, tol=0.0001,
+                                        alpha_1=1e-3, alpha_2=1e-3,
+                                        lambda_1=Lambda, lambda_2=Lambda)
+
+        elif reg_method.lower() == 'fastard':
+            clf_poly = RegressionFastARD(fit_intercept=False,
+                                         normalize=True,
+                                         compute_score=compute_score,
+                                         n_iter=300, tol=1e-10)
+
+        elif reg_method.lower() == 'bcs':
+            if X.shape[0]<10:
+                raise ValueError('Regression with BCS can only be performed for more than 10 samples')
+            clf_poly = RegressionFastLaplace(fit_intercept=False,
+                                         bias_term=bias_term,
+                                         n_iter=1000, tol=1e-7)
+
+        elif reg_method.lower() == 'lars':
+            if X.shape[0]<10:
+                raise ValueError('Regression with LARS can only be performed for more than 5 samples')
+            clf_poly = lm.LassoLarsCV(fit_intercept=False)
+
+        elif reg_method.lower() == 'sgdr':
+            clf_poly = lm.SGDRegressor(fit_intercept=False,
+                                       max_iter=5000, tol=1e-7)
+
+        elif reg_method.lower() == 'omp':
+            clf_poly = OrthogonalMatchingPursuit(fit_intercept=False)
+
+        elif reg_method.lower() == 'vbl':
+            clf_poly = VBLinearRegression(fit_intercept=False)
+
+        elif reg_method.lower() == 'ebl':
+            clf_poly = EBLinearRegression(optimizer='em')
+            
+        
+        # Training with constraints automatically uses L2
+        if self.apply_constraints:       
+            # TODO: set the constraints here
+            # Define the nonlin. constraint     
+            nlc = NonlinearConstraint(lambda x: np.matmul(X,x),-1,1.1)
+            self.nlc = nlc
+            
+            fun = lambda x: (np.linalg.norm(np.matmul(X, x)-y, ord = 2))**2
+            if self.init_type =='zeros':
+                res = minimize(fun, np.zeros(X.shape[1]), method = 'trust-constr', constraints  = self.nlc) 
+            if self.init_type == 'nonpi':
+                clf_poly.fit(X, y)
+                coeff = clf_poly.coef_
+                res = minimize(fun, coeff, method = 'trust-constr', constraints  = self.nlc)
+            
+            coeff = np.array(res.x)
+            clf_poly.coef_ = coeff
+            clf_poly.X = X
+            clf_poly.y = y
+            clf_poly.intercept_ = 0
+            
+        # Training without constraints uses chosen regression method
+        else:
+            clf_poly.fit(X, y)
+
+        # Select the nonzero entries of coefficients
+        if sparsity:
+            nnz_idx = np.nonzero(clf_poly.coef_)[0]
+        else:
+            nnz_idx = np.arange(clf_poly.coef_.shape[0])
+
+        # This is for the case where all outputs are zero, thereby
+        # all coefficients are zero
+        if (y == 0).all():
+            nnz_idx = np.insert(np.nonzero(clf_poly.coef_)[0], 0, 0)
+
+        sparse_basis_indices = basis_indices[nnz_idx]
+        sparse_X = X[:, nnz_idx]
+        coeffs = clf_poly.coef_[nnz_idx]
+        clf_poly.coef_ = coeffs
+
+        # Create a dict to pass the outputs
+        return_out_dict = dict()
+        return_out_dict['clf_poly'] = clf_poly
+        return_out_dict['spareMulti-Index'] = sparse_basis_indices
+        return_out_dict['sparePsi'] = sparse_X
+        return_out_dict['coeffs'] = coeffs
+        return return_out_dict
+    
+    # -------------------------------------------------------------------------
+    def create_psi(self, basis_indices, univ_p_val):
+        """
+        This function assemble the design matrix Psi from the given basis index
+        set INDICES and the univariate polynomial evaluations univ_p_val.
+
+        Parameters
+        ----------
+        basis_indices : array of shape (n_terms, n_params)
+            Multi-indices of multivariate polynomials.
+        univ_p_val : array of (n_samples, n_params, n_max+1)
+            All univariate regressors up to `n_max`.
+
+        Raises
+        ------
+        ValueError
+            n_terms in arguments do not match.
+
+        Returns
+        -------
+        psi : array of shape (n_samples, n_terms)
+            Multivariate regressors.
+
+        """
+        # Check if BasisIndices is a sparse matrix
+        sparsity = sp.sparse.issparse(basis_indices)
+        if sparsity:
+            basis_indices = basis_indices.toarray()
+
+        # Initialization and consistency checks
+        # number of input variables
+        n_params = univ_p_val.shape[1]
+
+        # Size of the experimental design
+        n_samples = univ_p_val.shape[0]
+
+        # number of basis terms
+        n_terms = basis_indices.shape[0]
+
+        # check that the variables have consistent sizes
+        if n_params != basis_indices.shape[1]:
+            raise ValueError(
+                f"The shapes of basis_indices ({basis_indices.shape[1]}) and "
+                f"univ_p_val ({n_params}) don't match!!"
+                )
+
+        # Preallocate the Psi matrix for performance
+        psi = np.ones((n_samples, n_terms))
+        # Assemble the Psi matrix
+        for m in range(basis_indices.shape[1]):
+            aa = np.where(basis_indices[:, m] > 0)[0]
+            try:
+                basisIdx = basis_indices[aa, m]
+                bb = univ_p_val[:, m, basisIdx].reshape(psi[:, aa].shape)
+                psi[:, aa] = np.multiply(psi[:, aa], bb)
+            except ValueError as err:
+                raise err
+        return psi
+
+    # --------------------------------------------------------------------------------------------------------
+    def adaptive_regression(self, ED_X, ED_Y, varIdx, verbose=False):
+        """
+        Adaptively fits the PCE model by comparing the scores of different
+        degrees and q-norm.
+
+        Parameters
+        ----------
+        ED_X : array of shape (n_samples, n_params)
+            Experimental design.
+        ED_Y : array of shape (n_samples,)
+            Target values, i.e. simulation results for the Experimental design.
+        varIdx : int
+            Index of the output.
+        verbose : bool, optional
+            Print out summary. The default is False.
+
+        Returns
+        -------
+        returnVars : Dict
+            Fitted estimator, best degree, best q-norm, LOOCVScore and
+            coefficients.
+
+        """
+
+        n_samples, n_params = ED_X.shape
+        # Initialization
+        qAllCoeffs, AllCoeffs = {}, {}
+        qAllIndices_Sparse, AllIndices_Sparse = {}, {}
+        qAllclf_poly, Allclf_poly = {}, {}
+        qAllnTerms, AllnTerms = {}, {}
+        qAllLCerror, AllLCerror = {}, {}
+
+        # Extract degree array and qnorm array
+        deg_array = np.array([*self.allBasisIndices], dtype=int)
+        qnorm = [*self.allBasisIndices[str(int(deg_array[0]))]]
+
+        # Some options for EarlyStop
+        errorIncreases = False
+        # Stop degree, if LOO error does not decrease n_checks_degree times
+        n_checks_degree = 3
+        # Stop qNorm, if criterion isn't fulfilled n_checks_qNorm times
+        n_checks_qNorm = 2
+        nqnorms = len(qnorm)
+        qNormEarlyStop = True
+        if nqnorms < n_checks_qNorm+1:
+            qNormEarlyStop = False
+
+        # =====================================================================
+        # basis adaptive polynomial chaos: repeat the calculation by increasing
+        # polynomial degree until the highest accuracy is reached
+        # =====================================================================
+        # For each degree check all q-norms and choose the best one
+        scores = -np.inf * np.ones(deg_array.shape[0])
+        qNormScores = -np.inf * np.ones(nqnorms)
+
+        for degIdx, deg in enumerate(deg_array):
+
+            for qidx, q in enumerate(qnorm):
+
+                # Extract the polynomial basis indices from the pool of
+                # allBasisIndices
+                BasisIndices = self.allBasisIndices[str(deg)][str(q)]
+
+                # Assemble the Psi matrix
+                Psi = self.create_psi(BasisIndices, self.univ_p_val)
+
+                # Calulate the cofficients of the meta model
+                outs = self.regression(Psi, ED_Y, BasisIndices)
+
+                # Calculate and save the score of LOOCV
+                score, LCerror = self.corr_loocv_error(outs['clf_poly'],
+                                                       outs['sparePsi'],
+                                                       outs['coeffs'],
+                                                       ED_Y)
+
+                # Check the convergence of noise for FastARD
+                if self.pce_reg_method == 'FastARD' and \
+                   outs['clf_poly'].alpha_ < np.finfo(np.float32).eps:
+                    score = -np.inf
+
+                qNormScores[qidx] = score
+                qAllCoeffs[str(qidx+1)] = outs['coeffs']
+                qAllIndices_Sparse[str(qidx+1)] = outs['spareMulti-Index']
+                qAllclf_poly[str(qidx+1)] = outs['clf_poly']
+                qAllnTerms[str(qidx+1)] = BasisIndices.shape[0]
+                qAllLCerror[str(qidx+1)] = LCerror
+
+                # EarlyStop check
+                # if there are at least n_checks_qNorm entries after the
+                # best one, we stop
+                if qNormEarlyStop and \
+                   sum(np.isfinite(qNormScores)) > n_checks_qNorm:
+                    # If the error has increased the last two iterations, stop!
+                    qNormScores_nonInf = qNormScores[np.isfinite(qNormScores)]
+                    deltas = np.sign(np.diff(qNormScores_nonInf))
+                    if sum(deltas[-n_checks_qNorm+1:]) == 2:
+                        # stop the q-norm loop here
+                        break
+                if np.var(ED_Y) == 0:
+                    break
+
+            # Store the score in the scores list
+            best_q = np.nanargmax(qNormScores)
+            scores[degIdx] = qNormScores[best_q]
+
+            AllCoeffs[str(degIdx+1)] = qAllCoeffs[str(best_q+1)]
+            AllIndices_Sparse[str(degIdx+1)] = qAllIndices_Sparse[str(best_q+1)]
+            Allclf_poly[str(degIdx+1)] = qAllclf_poly[str(best_q+1)]
+            AllnTerms[str(degIdx+1)] = qAllnTerms[str(best_q+1)]
+            AllLCerror[str(degIdx+1)] = qAllLCerror[str(best_q+1)]
+
+            # Check the direction of the error (on average):
+            # if it increases consistently stop the iterations
+            if len(scores[scores != -np.inf]) > n_checks_degree:
+                scores_nonInf = scores[scores != -np.inf]
+                ss = np.sign(scores_nonInf - np.max(scores_nonInf))
+                # ss<0 error decreasing
+                errorIncreases = np.sum(np.sum(ss[-2:])) <= -1*n_checks_degree
+
+            if errorIncreases:
+                break
+
+            # Check only one degree, if target matrix has zero variance
+            if np.var(ED_Y) == 0:
+                break
+
+        # ------------------ Summary of results ------------------
+        # Select the one with the best score and save the necessary outputs
+        best_deg = np.nanargmax(scores)+1
+        coeffs = AllCoeffs[str(best_deg)]
+        basis_indices = AllIndices_Sparse[str(best_deg)]
+        clf_poly = Allclf_poly[str(best_deg)]
+        LOOCVScore = np.nanmax(scores)
+        P = AllnTerms[str(best_deg)]
+        LCerror = AllLCerror[str(best_deg)]
+        degree = deg_array[np.nanargmax(scores)]
+        qnorm = float(qnorm[best_q])
+
+        # ------------------ Print out Summary of results ------------------
+        if self.verbose:
+            # Create PSI_Sparse by removing redundent terms
+            nnz_idx = np.nonzero(coeffs)[0]
+            BasisIndices_Sparse = basis_indices[nnz_idx]
+
+            print(f'Output variable {varIdx+1}:')
+            print('The estimation of PCE coefficients converged at polynomial '
+                  f'degree {deg_array[best_deg-1]} with '
+                  f'{len(BasisIndices_Sparse)} terms (Sparsity index = '
+                  f'{round(len(BasisIndices_Sparse)/P, 3)}).')
+
+            print(f'Final ModLOO error estimate: {1-max(scores):.3e}')
+            print('\n'+'-'*50)
+
+        if verbose:
+            print('='*50)
+            print(' '*10 + ' Summary of results ')
+            print('='*50)
+
+            print("Scores:\n", scores)
+            print("Degree of best score:", self.deg_array[best_deg-1])
+            print("No. of terms:", len(basis_indices))
+            print("Sparsity index:", round(len(basis_indices)/P, 3))
+            print("Best Indices:\n", basis_indices)
+
+            if self.pce_reg_method in ['BRR', 'ARD']:
+                fig, ax = plt.subplots(figsize=(12, 10))
+                plt.title("Marginal log-likelihood")
+                plt.plot(clf_poly.scores_, color='navy', linewidth=2)
+                plt.ylabel("Score")
+                plt.xlabel("Iterations")
+                if self.pce_reg_method.lower() == 'bbr':
+                    text = f"$\\alpha={clf_poly.alpha_:.1f}$\n"
+                    f"$\\lambda={clf_poly.lambda_:.3f}$\n"
+                    f"$L={clf_poly.scores_[-1]:.1f}$"
+                else:
+                    text = f"$\\alpha={clf_poly.alpha_:.1f}$\n$"
+                    f"\\L={clf_poly.scores_[-1]:.1f}$"
+
+                plt.text(0.75, 0.5, text, fontsize=18, transform=ax.transAxes)
+                plt.show()
+            print('='*80)
+
+        # Create a dict to pass the outputs
+        returnVars = dict()
+        returnVars['clf_poly'] = clf_poly
+        returnVars['degree'] = degree
+        returnVars['qnorm'] = qnorm
+        returnVars['coeffs'] = coeffs
+        returnVars['multi_indices'] = basis_indices
+        returnVars['LOOCVScore'] = LOOCVScore
+        returnVars['LCerror'] = LCerror
+
+        return returnVars
+
+    # -------------------------------------------------------------------------
+    def corr_loocv_error(self, clf, psi, coeffs, y):
+        """
+        Calculates the corrected LOO error for regression on regressor
+        matrix `psi` that generated the coefficients based on [1] and [2].
+
+        [1] Blatman, G., 2009. Adaptive sparse polynomial chaos expansions for
+            uncertainty propagation and sensitivity analysis (Doctoral
+            dissertation, Clermont-Ferrand 2).
+
+        [2] Blatman, G. and Sudret, B., 2011. Adaptive sparse polynomial chaos
+            expansion based on least angle regression. Journal of computational
+            Physics, 230(6), pp.2345-2367.
+
+        Parameters
+        ----------
+        clf : object
+            Fitted estimator.
+        psi : array of shape (n_samples, n_features)
+            The multivariate orthogonal polynomials (regressor).
+        coeffs : array-like of shape (n_features,)
+            Estimated cofficients.
+        y : array of shape (n_samples,)
+            Target values.
+
+        Returns
+        -------
+        R_2 : float
+            LOOCV Validation score (1-LOOCV erro).
+        residual : array of shape (n_samples,)
+            Residual values (y - predicted targets).
+
+        """
+        psi = np.array(psi, dtype=float)
+
+        # Create PSI_Sparse by removing redundent terms
+        nnz_idx = np.nonzero(coeffs)[0]
+        if len(nnz_idx) == 0:
+            nnz_idx = [0]
+        psi_sparse = psi[:, nnz_idx]
+
+        # NrCoeffs of aPCEs
+        P = len(nnz_idx)
+        # NrEvaluation (Size of experimental design)
+        N = psi.shape[0]
+
+        # Build the projection matrix
+        PsiTPsi = np.dot(psi_sparse.T, psi_sparse)
+
+        if np.linalg.cond(PsiTPsi) > 1e-12: #and \
+           # np.linalg.cond(PsiTPsi) < 1/sys.float_info.epsilon:
+            # faster
+            try:
+                M = sp.linalg.solve(PsiTPsi,
+                                sp.sparse.eye(PsiTPsi.shape[0]).toarray())
+            except:
+                raise AttributeError('There are too few samples for the corrected loo-cv error. Fit surrogate on at least as many samples as parameters to use this')
+        else:
+            # stabler
+            M = np.linalg.pinv(PsiTPsi)
+
+        # h factor (the full matrix is not calculated explicitly,
+        # only the trace is, to save memory)
+        PsiM = np.dot(psi_sparse, M)
+
+        h = np.sum(np.multiply(PsiM, psi_sparse), axis=1, dtype=np.longdouble)#float128)
+
+        # ------ Calculate Error Loocv for each measurement point ----
+        # Residuals
+        try:
+            residual = clf.predict(psi) - y
+        except:
+            residual = np.dot(psi, coeffs) - y
+
+        # Variance
+        var_y = np.var(y)
+
+        if var_y == 0:
+            norm_emp_error = 0
+            loo_error = 0
+            LCerror = np.zeros((y.shape))
+            return 1-loo_error, LCerror
+        else:
+            norm_emp_error = np.mean(residual**2)/var_y
+
+            # LCerror = np.divide(residual, (1-h))
+            LCerror = residual / (1-h)
+            loo_error = np.mean(np.square(LCerror)) / var_y
+            # if there are NaNs, just return an infinite LOO error (this
+            # happens, e.g., when a strongly underdetermined problem is solved)
+            if np.isnan(loo_error):
+                loo_error = np.inf
+
+        # Corrected Error for over-determined system
+        tr_M = np.trace(M)
+        if tr_M < 0 or abs(tr_M) > 1e6:
+            tr_M = np.trace(np.linalg.pinv(np.dot(psi.T, psi)))
+
+        # Over-determined system of Equation
+        if N > P:
+            T_factor = N/(N-P) * (1 + tr_M)
+
+        # Under-determined system of Equation
+        else:
+            T_factor = np.inf
+
+        corrected_loo_error = loo_error * T_factor
+
+        R_2 = 1 - corrected_loo_error
+
+        return R_2, LCerror
+
+    # -------------------------------------------------------------------------
+    def pca_transformation(self, target, verbose=False):
+        """
+        Transforms the targets (outputs) via Principal Component Analysis
+
+        Parameters
+        ----------
+        target : array of shape (n_samples,)
+            Target values.
+
+        Returns
+        -------
+        pca : obj
+            Fitted sklearnPCA object.
+        OutputMatrix : array of shape (n_samples,)
+            Transformed target values.
+        n_pca_components : int
+            Number of selected principal components.
+
+        """
+        # Transform via Principal Component Analysis
+        if hasattr(self, 'var_pca_threshold'):
+            var_pca_threshold = self.var_pca_threshold
+        else:
+            var_pca_threshold = 100.0
+        n_samples, n_features = target.shape
+
+        if hasattr(self, 'n_pca_components'):
+            n_pca_components = self.n_pca_components
+        else:
+            # Instantiate and fit sklearnPCA object
+            covar_matrix = sklearnPCA(n_components=None)
+            covar_matrix.fit(target)
+            var = np.cumsum(np.round(covar_matrix.explained_variance_ratio_,
+                                     decimals=5)*100)
+            # Find the number of components to explain self.varPCAThreshold of
+            # variance
+            try:
+                n_components = np.where(var >= var_pca_threshold)[0][0] + 1
+            except IndexError:
+                n_components = min(n_samples, n_features)
+
+            n_pca_components = min(n_samples, n_features, n_components)
+
+        # Print out a report
+        if verbose:
+            print()
+            print('-' * 50)
+            print(f"PCA transformation is performed with {n_pca_components}"
+                  " components.")
+            print('-' * 50)
+            print()
+
+        # Fit and transform with the selected number of components
+        pca = sklearnPCA(n_components=n_pca_components, svd_solver='arpack')
+        scaled_target = pca.fit_transform(target)
+
+        return pca, scaled_target, n_pca_components
+
+    # -------------------------------------------------------------------------
+    def gaussian_process_emulator(self, X, y, nug_term=None, autoSelect=False,
+                                  varIdx=None):
+        """
+        Fits a Gaussian Process Emulator to the target given the training
+         points.
+
+        Parameters
+        ----------
+        X : array of shape (n_samples, n_params)
+            Training points.
+        y : array of shape (n_samples,)
+            Target values.
+        nug_term : float, optional
+            Nugget term. The default is None, i.e. variance of y.
+        autoSelect : bool, optional
+            Loop over some kernels and select the best. The default is False.
+        varIdx : int, optional
+            The index number. The default is None.
+
+        Returns
+        -------
+        gp : object
+            Fitted estimator.
+
+        """
+
+        nug_term = nug_term if nug_term else np.var(y)
+
+        Kernels = [nug_term * kernels.RBF(length_scale=1.0,
+                                          length_scale_bounds=(1e-25, 1e15)),
+                   nug_term * kernels.RationalQuadratic(length_scale=0.2,
+                                                        alpha=1.0),
+                   nug_term * kernels.Matern(length_scale=1.0,
+                                             length_scale_bounds=(1e-15, 1e5),
+                                             nu=1.5)]
+
+        # Automatic selection of the kernel
+        if autoSelect:
+            gp = {}
+            BME = []
+            for i, kernel in enumerate(Kernels):
+                gp[i] = GaussianProcessRegressor(kernel=kernel,
+                                                 n_restarts_optimizer=3,
+                                                 normalize_y=False)
+
+                # Fit to data using Maximum Likelihood Estimation
+                gp[i].fit(X, y)
+
+                # Store the MLE as BME score
+                BME.append(gp[i].log_marginal_likelihood())
+
+            gp = gp[np.argmax(BME)]
+
+        else:
+            gp = GaussianProcessRegressor(kernel=Kernels[0],
+                                          n_restarts_optimizer=3,
+                                          normalize_y=False)
+            gp.fit(X, y)
+
+        # Compute score
+        if varIdx is not None:
+            Score = gp.score(X, y)
+            print('-'*50)
+            print(f'Output variable {varIdx}:')
+            print('The estimation of GPE coefficients converged,')
+            print(f'with the R^2 score: {Score:.3f}')
+            print('-'*50)
+
+        return gp
+
+    # -------------------------------------------------------------------------
+    def eval_metamodel(self, samples):
+        """
+        Evaluates meta-model at the requested samples. One can also generate
+        nsamples.
+
+        Parameters
+        ----------
+        samples : array of shape (n_samples, n_params), optional
+            Samples to evaluate meta-model at. The default is None.
+        nsamples : int, optional
+            Number of samples to generate, if no `samples` is provided. The
+            default is None.
+        sampling_method : str, optional
+            Type of sampling, if no `samples` is provided. The default is
+            'random'.
+        return_samples : bool, optional
+            Retun samples, if no `samples` is provided. The default is False.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean of the predictions.
+        std_pred : dict
+            Standard deviatioon of the predictions.
+        """
+        # Transform into np array - can also be given as list
+        samples = np.array(samples)
+        
+        # Transform samples to the independent space
+        samples = self.InputSpace.transform(
+            samples,
+            method='user'
+            )
+        # Compute univariate bases for the given samples
+        if self.meta_model_type.lower() != 'gpe':
+            univ_p_val = self.univ_basis_vals(
+                samples,
+                n_max=np.max(self.pce_deg)
+                )
+
+        mean_pred_b = {}
+        std_pred_b = {}
+        # Loop over bootstrap iterations
+        for b_i in range(self.n_bootstrap_itrs):
+
+            # Extract model dictionary
+            if self.meta_model_type.lower() == 'gpe':
+                model_dict = self.gp_poly[f'b_{b_i+1}']
+            else:
+                model_dict = self.coeffs_dict[f'b_{b_i+1}']
+
+            # Loop over outputs
+            mean_pred = {}
+            std_pred = {}
+            for output, values in model_dict.items():
+
+                mean = np.empty((len(samples), len(values)))
+                std = np.empty((len(samples), len(values)))
+                idx = 0
+                for in_key, InIdxValues in values.items():
+
+                    # Prediction with GPE
+                    if self.meta_model_type.lower() == 'gpe':
+                        X_T = self.x_scaler[f'b_{b_i+1}'][output].transform(samples)
+                        gp = self.gp_poly[f'b_{b_i+1}'][output][in_key]
+                        y_mean, y_std = gp.predict(X_T, return_std=True)
+
+                    else:
+                        # Prediction with PCE
+                        # Assemble Psi matrix
+                        basis = self.basis_dict[f'b_{b_i+1}'][output][in_key]
+                        psi = self.create_psi(basis, univ_p_val)
+
+                        # Prediction
+                        if self.bootstrap_method != 'fast' or b_i == 0:
+                            # with error bar, i.e. use clf_poly
+                            clf_poly = self.clf_poly[f'b_{b_i+1}'][output][in_key]
+                            try:
+                                y_mean, y_std = clf_poly.predict(
+                                    psi, return_std=True
+                                    )
+                            except TypeError:
+                                y_mean = clf_poly.predict(psi)
+                                y_std = np.zeros_like(y_mean)
+                        else:
+                            # without error bar
+                            coeffs = self.coeffs_dict[f'b_{b_i+1}'][output][in_key]
+                            y_mean = np.dot(psi, coeffs)
+                            y_std = np.zeros_like(y_mean)
+
+                    mean[:, idx] = y_mean
+                    std[:, idx] = y_std
+                    idx += 1
+
+                # Save predictions for each output
+                if self.dim_red_method.lower() == 'pca':
+                    PCA = self.pca[f'b_{b_i+1}'][output]
+                    mean_pred[output] = PCA.inverse_transform(mean)
+                    std_pred[output] = np.zeros(mean.shape)
+                else:
+                    mean_pred[output] = mean
+                    std_pred[output] = std
+
+            # Save predictions for each bootstrap iteration
+            mean_pred_b[b_i] = mean_pred
+            std_pred_b[b_i] = std_pred
+
+        # Change the order of nesting
+        mean_pred_all = {}
+        for i in sorted(mean_pred_b):
+            for k, v in mean_pred_b[i].items():
+                if k not in mean_pred_all:
+                    mean_pred_all[k] = [None] * len(mean_pred_b)
+                mean_pred_all[k][i] = v
+
+        # Compute the moments of predictions over the predictions
+        for output in self.out_names:
+            # Only use bootstraps with finite values
+            finite_rows = np.isfinite(
+                mean_pred_all[output]).all(axis=2).all(axis=1)
+            outs = np.asarray(mean_pred_all[output])[finite_rows]
+            # Compute mean
+            mean_pred[output] = np.mean(outs, axis=0)
+            # Compute standard deviation
+            if self.n_bootstrap_itrs > 1:
+                std_pred[output] = np.std(outs, axis=0)
+            else:
+                std_pred[output] = std_pred_b[b_i][output]
+
+        return mean_pred, std_pred
+
+    # -------------------------------------------------------------------------
+    def create_model_error(self, X, y, Model, name='Calib'):
+        """
+        Fits a GPE-based model error.
+
+        Parameters
+        ----------
+        X : array of shape (n_outputs, n_inputs)
+            Input array. It can contain any forcing inputs or coordinates of
+             extracted data.
+        y : array of shape (n_outputs,)
+            The model response for the MAP parameter set.
+        name : str, optional
+            Calibration or validation. The default is `'Calib'`.
+
+        Returns
+        -------
+        self: object
+            Self object.
+
+        """
+        outputNames = self.out_names
+        self.errorRegMethod = 'GPE'
+        self.errorclf_poly = self.auto_vivification()
+        self.errorScale = self.auto_vivification()
+
+        # Read data
+        # TODO: do this call outside the metamodel
+        MeasuredData = Model.read_observation(case=name)
+
+        # Fitting GPR based bias model
+        for out in outputNames:
+            nan_idx = ~np.isnan(MeasuredData[out])
+            # Select data
+            try:
+                data = MeasuredData[out].values[nan_idx]
+            except AttributeError:
+                data = MeasuredData[out][nan_idx]
+
+            # Prepare the input matrix
+            scaler = MinMaxScaler()
+            delta = data  # - y[out][0]
+            BiasInputs = np.hstack((X[out], y[out].reshape(-1, 1)))
+            X_S = scaler.fit_transform(BiasInputs)
+            gp = self.gaussian_process_emulator(X_S, delta)
+
+            self.errorScale[out]["y_1"] = scaler
+            self.errorclf_poly[out]["y_1"] = gp
+
+        return self
+
+    # -------------------------------------------------------------------------
+    def eval_model_error(self, X, y_pred):
+        """
+        Evaluates the error model.
+
+        Parameters
+        ----------
+        X : array
+            Inputs.
+        y_pred : dict
+            Predictions.
+
+        Returns
+        -------
+        mean_pred : dict
+            Mean predition of the GPE-based error model.
+        std_pred : dict
+            standard deviation of the GPE-based error model.
+
+        """
+        mean_pred = {}
+        std_pred = {}
+
+        for Outkey, ValuesDict in self.errorclf_poly.items():
+
+            pred_mean = np.zeros_like(y_pred[Outkey])
+            pred_std = np.zeros_like(y_pred[Outkey])
+
+            for Inkey, InIdxValues in ValuesDict.items():
+
+                gp = self.errorclf_poly[Outkey][Inkey]
+                scaler = self.errorScale[Outkey][Inkey]
+
+                # Transform Samples using scaler
+                for j, pred in enumerate(y_pred[Outkey]):
+                    BiasInputs = np.hstack((X[Outkey], pred.reshape(-1, 1)))
+                    Samples_S = scaler.transform(BiasInputs)
+                    y_hat, y_std = gp.predict(Samples_S, return_std=True)
+                    pred_mean[j] = y_hat
+                    pred_std[j] = y_std
+                    # pred_mean[j] += pred
+
+            mean_pred[Outkey] = pred_mean
+            std_pred[Outkey] = pred_std
+
+        return mean_pred, std_pred
+
+    # -------------------------------------------------------------------------
+    class auto_vivification(dict):
+        """
+        Implementation of perl's AutoVivification feature.
+
+        Source: https://stackoverflow.com/a/651879/18082457
+        """
+
+        def __getitem__(self, item):
+            try:
+                return dict.__getitem__(self, item)
+            except KeyError:
+                value = self[item] = type(self)()
+                return value
+
+    # -------------------------------------------------------------------------
+    def copy_meta_model_opts(self):
+        """
+        This method is a convinient function to copy the metamodel options.
+
+        Returns
+        -------
+        new_MetaModelOpts : object
+            The copied object.
+
+        """
+        # TODO: what properties should be moved to the new object?
+        new_MetaModelOpts = copy.deepcopy(self)
+        new_MetaModelOpts.input_obj = self.input_obj#InputObj
+        new_MetaModelOpts.InputSpace = self.InputSpace
+        #new_MetaModelOpts.InputSpace.meta_Model = 'aPCE'
+        #new_MetaModelOpts.InputSpace.InputObj = self.input_obj
+        #new_MetaModelOpts.InputSpace.ndim = len(self.input_obj.Marginals)
+        new_MetaModelOpts.n_params = len(self.input_obj.Marginals)
+        #new_MetaModelOpts.InputSpace.hdf5_file = None
+
+        return new_MetaModelOpts
+
+    # -------------------------------------------------------------------------
+    def __select_degree(self, ndim, n_samples):
+        """
+        Selects degree based on the number of samples and parameters in the
+        sequential design.
+
+        Parameters
+        ----------
+        ndim : int
+            Dimension of the parameter space.
+        n_samples : int
+            Number of samples.
+
+        Returns
+        -------
+        deg_array: array
+            Array containing the arrays.
+
+        """
+        # Define the deg_array
+        max_deg = np.max(self.pce_deg)
+        min_Deg = np.min(self.pce_deg)
+        
+        # TODO: remove the options for sequential?
+        #nitr = n_samples - self.InputSpace.n_init_samples
+
+        # Check q-norm
+        if not np.isscalar(self.pce_q_norm):
+            self.pce_q_norm = np.array(self.pce_q_norm)
+        else:
+            self.pce_q_norm = np.array([self.pce_q_norm])
+
+        def M_uptoMax(maxDeg):
+            n_combo = np.zeros(maxDeg)
+            for i, d in enumerate(range(1, maxDeg+1)):
+                n_combo[i] = math.factorial(ndim+d)
+                n_combo[i] /= math.factorial(ndim) * math.factorial(d)
+            return n_combo
+
+        deg_new = max_deg
+        #d = nitr if nitr != 0 and self.n_params > 5 else 1
+        # d = 1
+        # min_index = np.argmin(abs(M_uptoMax(max_deg)-ndim*n_samples*d))
+        # deg_new = range(1, max_deg+1)[min_index]
+
+        if deg_new > min_Deg and self.pce_reg_method.lower() != 'fastard':
+            deg_array = np.arange(min_Deg, deg_new+1)
+        else:
+            deg_array = np.array([deg_new])
+
+        return deg_array
+
+    def generate_polynomials(self, max_deg=None):
+        # Check for InputSpace
+        if not hasattr(self, 'InputSpace'):
+            raise AttributeError('Generate or add InputSpace before generating polynomials')
+            
+        ndim = self.InputSpace.ndim
+        # Create orthogonal polynomial coefficients if necessary
+        if (self.meta_model_type.lower()!='gpe') and max_deg is not None:# and self.input_obj.poly_coeffs_flag:
+            self.polycoeffs = {}
+            for parIdx in tqdm(range(ndim), ascii=True,
+                               desc="Computing orth. polynomial coeffs"):
+                poly_coeffs = apoly_construction(
+                    self.InputSpace.raw_data[parIdx],
+                    max_deg
+                    )
+                self.polycoeffs[f'p_{parIdx+1}'] = poly_coeffs
+        else:
+            raise AttributeError('MetaModel cannot generate polynomials in the given scenario!')
+
+    # -------------------------------------------------------------------------
+    def _compute_pce_moments(self):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moment (mean) of the surrogate.
+        pce_stds: dict
+            The second moment (standard deviation) of the surrogate.
+
+        """
+        
+        # Check if its truly a pce-surrogate
+        if self.meta_model_type.lower() == 'gpe':
+            raise AttributeError('Moments can only be computed for pce-type surrogates')
+        
+        outputs = self.out_names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(self.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = self.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = self.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = self.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if self.dim_red_method.lower() == 'pca':
+                    PCA = self.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.inverse_transform(pce_mean)
+                    stds[output] = PCA.inverse_transform(np.sqrt(pce_var))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+        return pce_means, pce_stds
diff --git a/examples/model-comparison/example_model_comparison.py b/examples/model-comparison/example_model_comparison.py
index ebd80fea82a3caf3ff204b89da96c59737ba502b..e712bba8c370cfb6bdbfd5a42dda64803edab571 100644
--- a/examples/model-comparison/example_model_comparison.py
+++ b/examples/model-comparison/example_model_comparison.py
@@ -280,7 +280,7 @@ if __name__ == "__main__":
     # BME Bootstrap optuions
     opts_bootstrap = {
         "bootstrap": True,
-        "n_samples": 10000,
+        "n_samples": 1000,#0,
         "Discrepancy": DiscrepancyOpts,
         "emulator": True,
         "plot_post_pred": False
@@ -289,7 +289,7 @@ if __name__ == "__main__":
     # Run model comparison
     BayesOpts = BayesModelComparison(
         justifiability=True,
-        n_bootstarp=100,#00,
+        n_bootstarp=1000,#00,
         just_n_meas=2
         )
     output_dict = BayesOpts.create_model_comparison(
diff --git a/src/bayesvalidrox.egg-info/PKG-INFO b/src/bayesvalidrox.egg-info/PKG-INFO
index dc88c833512df7b831d0849f008eda731b639173..279feebe3b7b4b5a53bdc43208dd65c926f06f2f 100644
--- a/src/bayesvalidrox.egg-info/PKG-INFO
+++ b/src/bayesvalidrox.egg-info/PKG-INFO
@@ -1,10 +1,10 @@
 Metadata-Version: 2.1
 Name: bayesvalidrox
-Version: 0.0.5
+Version: 1.0.0
 Summary: An open-source, object-oriented Python package for surrogate-assisted Bayesain Validation of computational models.
 Home-page: https://git.iws.uni-stuttgart.de/inversemodeling/bayesian-validation
-Author: Farid Mohammadi
-Author-email: farid.mohammadi@iws.uni-stuttgart.de
+Author: Farid Mohammadi, Rebecca Kohlhaas
+Author-email: farid.mohammadi@iws.uni-stuttgart.de, rebecca.kohlhaas@iws.uni-stuttgart.de
 Classifier: Programming Language :: Python :: 3
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Operating System :: OS Independent
diff --git a/src/bayesvalidrox.egg-info/SOURCES.txt b/src/bayesvalidrox.egg-info/SOURCES.txt
index d6619704eee21931221fa73b5d2076a2dce99991..344e9840627bb3e5a89593dbd9256472a8ef41d9 100644
--- a/src/bayesvalidrox.egg-info/SOURCES.txt
+++ b/src/bayesvalidrox.egg-info/SOURCES.txt
@@ -29,10 +29,13 @@ src/bayesvalidrox/surrogate_models/exploration.py
 src/bayesvalidrox/surrogate_models/glexindex.py
 src/bayesvalidrox/surrogate_models/input_space.py
 src/bayesvalidrox/surrogate_models/inputs.py
+src/bayesvalidrox/surrogate_models/meta_model_engine.py
 src/bayesvalidrox/surrogate_models/orthogonal_matching_pursuit.py
 src/bayesvalidrox/surrogate_models/reg_fast_ard.py
 src/bayesvalidrox/surrogate_models/reg_fast_laplace.py
+src/bayesvalidrox/surrogate_models/sequential_design.py
 src/bayesvalidrox/surrogate_models/surrogate_models.py
+tests/test_BayesModelComparison.py
 tests/test_Discrepancy.py
 tests/test_ExpDesign.py
 tests/test_Input.py
diff --git a/src/bayesvalidrox/bayes_inference/bayes_inference.py b/src/bayesvalidrox/bayes_inference/bayes_inference.py
index 1898a8ae619597d92bc355ac4249f57019f0aed7..44e90b77c69b4c2c7bd27639727c5d2bd4fe8720 100644
--- a/src/bayesvalidrox/bayes_inference/bayes_inference.py
+++ b/src/bayesvalidrox/bayes_inference/bayes_inference.py
@@ -27,6 +27,91 @@ from .mcmc import MCMC
 plt.style.use(os.path.join(os.path.split(__file__)[0],
                            '../', 'bayesvalidrox.mplstyle'))
 
+# -------------------------------------------------------------------------
+def _kernel_rbf(X, hyperparameters):
+    """
+    Isotropic squared exponential kernel.
+
+    Higher l values lead to smoother functions and therefore to coarser
+    approximations of the training data. Lower l values make functions
+    more wiggly with wide uncertainty regions between training data points.
+
+    sigma_f controls the marginal variance of b(x)
+
+    Parameters
+    ----------
+    X : ndarray of shape (n_samples_X, n_features)
+
+    hyperparameters : Dict
+        Lambda characteristic length
+        sigma_f controls the marginal variance of b(x)
+        sigma_0 unresolvable error nugget term, interpreted as random
+                error that cannot be attributed to measurement error.
+    Returns
+    -------
+    var_cov_matrix : ndarray of shape (n_samples_X,n_samples_X)
+        Kernel k(X, X).
+
+    """
+    from sklearn.gaussian_process.kernels import RBF
+    min_max_scaler = preprocessing.MinMaxScaler()
+    X_minmax = min_max_scaler.fit_transform(X)
+
+    nparams = len(hyperparameters)
+    if nparams <3:
+        raise AttributeError('Provide 3 parameters for the RBF kernel!')
+        
+    # characteristic length (0,1]
+    Lambda = hyperparameters[0]
+    # sigma_f controls the marginal variance of b(x)
+    sigma2_f = hyperparameters[1]
+
+    rbf = RBF(length_scale=Lambda)
+    cov_matrix = sigma2_f * rbf(X_minmax)
+    
+    # (unresolvable error) nugget term that is interpreted as random
+    # error that cannot be attributed to measurement error.
+    sigma2_0 = hyperparameters[2:]
+    for i, j in np.ndindex(cov_matrix.shape):
+        cov_matrix[i, j] += np.sum(sigma2_0) if i == j else 0
+
+    return cov_matrix
+
+
+# -------------------------------------------------------------------------
+def _logpdf(x, mean, cov):
+    """
+    Computes the likelihood based on a multivariate normal distribution.
+
+    Parameters
+    ----------
+    x : TYPE
+        DESCRIPTION.
+    mean : array_like
+        Observation data.
+    cov : 2d array
+        Covariance matrix of the distribution.
+
+    Returns
+    -------
+    log_lik : float
+        Log likelihood.
+
+    """
+    
+    # Tranform into np arrays
+    x = np.array(x)
+    mean = np.array(mean)
+    cov = np.array(cov)
+    
+    n = len(mean)
+    L = spla.cholesky(cov, lower=True)
+    beta = np.sum(np.log(np.diag(L)))
+    dev = x - mean
+    alpha = dev.dot(spla.cho_solve((L, True), dev))
+    log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+    return log_lik
+
 
 class BayesInference:
     """
@@ -42,7 +127,7 @@ class BayesInference:
         of the variance matrix for a multivariate normal likelihood.
     name : str, optional
         The type of analysis, either calibration (`Calib`) or validation
-        (`Valid`). The default is `'Calib'`.
+        (`Valid`). The default is `'Calib'`. # TODO: what is going on here for validation?
     emulator : bool, optional
         Analysis with emulator (MetaModel). The default is `True`.
     bootstrap : bool, optional
@@ -55,11 +140,11 @@ class BayesInference:
         A dictionary with the selected indices of each model output. The
         default is `None`. If `None`, all measurement points are used in the
         analysis.
-    samples : array of shape (n_samples, n_params), optional
+    prior_samples : array of shape (n_samples, n_params), optional
         The samples to be used in the analysis. The default is `None`. If
         None the samples are drawn from the probablistic input parameter
         object of the MetaModel object.
-    n_samples : int, optional
+    n_prior_samples : int, optional
         Number of samples to be used in the analysis. The default is `500000`.
         If samples is not `None`, this argument will be assigned based on the
         number of samples given.
@@ -127,13 +212,13 @@ class BayesInference:
 
     def __init__(self, engine, MetaModel = None, discrepancy=None, emulator=True,
                  name='Calib', bootstrap=False, req_outputs=None,
-                 selected_indices=None, samples=None, n_samples=100000,
+                 selected_indices=None, prior_samples=None, n_prior_samples=100000,
                  measured_data=None, inference_method='rejection',
                  mcmc_params=None, bayes_loocv=False, n_bootstrap_itrs=1,
                  perturbed_data=[], bootstrap_noise=0.05, just_analysis=False,
                  valid_metrics=['BME'], plot_post_pred=True,
                  plot_map_pred=False, max_a_posteriori='mean',
-                 corner_title_fmt='.2e'):
+                 corner_title_fmt='.2e', out_dir = ''):
 
         self.engine = engine
         self.MetaModel = engine.MetaModel
@@ -143,8 +228,8 @@ class BayesInference:
         self.bootstrap = bootstrap
         self.req_outputs = req_outputs
         self.selected_indices = selected_indices
-        self.samples = samples
-        self.n_samples = n_samples
+        self.prior_samples = prior_samples
+        self.n_prior_samples = n_prior_samples
         self.measured_data = measured_data
         self.inference_method = inference_method
         self.mcmc_params = mcmc_params
@@ -158,6 +243,7 @@ class BayesInference:
         self.plot_map_pred = plot_map_pred
         self.max_a_posteriori = max_a_posteriori
         self.corner_title_fmt = corner_title_fmt
+        self.out_dir = out_dir
 
     # -------------------------------------------------------------------------
     def create_inference(self):
@@ -168,31 +254,37 @@ class BayesInference:
         -------
         BayesInference : obj
             The Bayes inference object.
+            
+        # TODO: should this function really return the class?
 
         """
-
         # Set some variables
         MetaModel = self.MetaModel
         Model = self.engine.Model
         n_params = MetaModel.n_params
         output_names = Model.Output.names
         par_names = self.engine.ExpDesign.par_names
-
-        # If the prior is set by the user, take it.
-        if self.samples is None:
-            self.samples = self.engine.ExpDesign.generate_samples(
-                self.n_samples, 'random')
+        
+        # Create output directory
+        if self.out_dir == '':
+            self.out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
+        os.makedirs(self.out_dir, exist_ok=True)
+        
+        # If the prior is set by the user, take it, else generate from ExpDes
+        if self.prior_samples is None:
+            self.prior_samples = self.engine.ExpDesign.generate_samples(
+                self.n_prior_samples, 'random')
         else:
             try:
-                samples = self.samples.values
+                samples = self.prior_samples.values
             except AttributeError:
-                samples = self.samples
-
+                samples = self.prior_samples
+        
             # Take care of an additional Sigma2s
-            self.samples = samples[:, :n_params]
-
+            self.prior_samples = samples[:, :self.engine.MetaModel.n_params]
+        
             # Update number of samples
-            self.n_samples = self.samples.shape[0]
+            self.n_prior_samples = self.prior_samples.shape[0]
 
         # ---------- Preparation of observation data ----------
         # Read observation data and perturb it if requested.
@@ -229,239 +321,58 @@ class BayesInference:
         opt_sigma_flag = isinstance(self.Discrepancy, dict)
         opt_sigma = None
         for key_idx, key in enumerate(output_names):
-
             # Find opt_sigma
             if opt_sigma_flag and opt_sigma is None:
                 # Option A: known error with unknown bias term
                 opt_sigma = 'A'
-                known_discrepancy = self.Discrepancy['known']
-                self.Discrepancy = self.Discrepancy['infer']
+                known_discrepancy = self.Discrepancy['known']  # TODO: the syntax here looks different from expected
+                self.Discrepancy = self.Discrepancy['infer'] # TODO: the syntax here looks different from expected
                 sigma2 = np.array(known_discrepancy.parameters[key])
 
-            elif opt_sigma == 'A' or self.Discrepancy.parameters is not None:
+            elif self.Discrepancy.parameters is not None:
                 # Option B: The sigma2 is known (no bias term)
-                if opt_sigma == 'A':
-                    sigma2 = np.array(known_discrepancy.parameters[key])
-                else:
-                    opt_sigma = 'B'
-                    sigma2 = np.array(self.Discrepancy.parameters[key])
+                opt_sigma = 'B'
+                sigma2 = np.array(self.Discrepancy.parameters[key])
 
             elif not isinstance(self.Discrepancy.InputDisc, str):
                 # Option C: The sigma2 is unknown (bias term including error)
                 opt_sigma = 'C'
-                self.Discrepancy.opt_sigma = opt_sigma
                 n_measurement = self.measured_data[key].values.shape
                 sigma2 = np.zeros((n_measurement[0]))
 
             total_sigma2[key] = sigma2
 
-            self.Discrepancy.opt_sigma = opt_sigma
-            self.Discrepancy.total_sigma2 = total_sigma2
+        self.Discrepancy.opt_sigma = opt_sigma
+        self.Discrepancy.total_sigma2 = total_sigma2
 
         # If inferred sigma2s obtained from e.g. calibration are given
         try:
-            self.sigma2s = self.Discrepancy.get_sample(self.n_samples)
+            self.sigma2s = self.Discrepancy.get_sample(self.n_prior_samples)
         except:
-            pass
+            pass #TODO: should an error be raised in this case?
 
         # ---------------- Bootstrap & TOM --------------------
+        
         if self.bootstrap or self.bayes_loocv or self.just_analysis:
-            if len(self.perturbed_data) == 0:
-                # zero mean noise Adding some noise to the observation function
-                self.perturbed_data = self._perturb_data(
-                    self.measured_data, output_names
-                    )
-            else:
-                self.n_bootstrap_itrs = len(self.perturbed_data)
-
-            # -------- Model Discrepancy -----------
-            if hasattr(self, 'error_model') and self.error_model \
-               and self.name.lower() != 'calib':
-                # Select posterior mean as MAP
-                MAP_theta = self.samples.mean(axis=0).reshape((1, n_params))
-                # MAP_theta = stats.mode(self.samples,axis=0)[0]
-
-                # Evaluate the (meta-)model at the MAP
-                y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=MAP_theta)
-
-                # Train a GPR meta-model using MAP
-                self.error_MetaModel = MetaModel.create_model_error(
-                    self.bias_inputs, y_MAP, Name=self.name
-                    )
-
-            # -----------------------------------------------------
-            # ----- Loop over the perturbed observation data ------
-            # -----------------------------------------------------
-            # Initilize arrays
-            logLikelihoods = np.zeros((self.n_samples, self.n_bootstrap_itrs),
-                                      dtype=np.float16)
-            BME_Corr = np.zeros((self.n_bootstrap_itrs))
-            log_BME = np.zeros((self.n_bootstrap_itrs))
-            KLD = np.zeros((self.n_bootstrap_itrs))
-            inf_entropy = np.zeros((self.n_bootstrap_itrs))
-
-            # Compute the prior predtions
-            # Evaluate the MetaModel
-            if self.emulator:
-                y_hat, y_std = MetaModel.eval_metamodel(samples=self.samples)
-                self.__mean_pce_prior_pred = y_hat
-                self._std_pce_prior_pred = y_std
-
-                # Correct the predictions with Model discrepancy
-                if hasattr(self, 'error_model') and self.error_model:
-                    y_hat_corr, y_std = self.error_MetaModel.eval_model_error(
-                        self.bias_inputs, self.__mean_pce_prior_pred
-                        )
-                    self.__mean_pce_prior_pred = y_hat_corr
-                    self._std_pce_prior_pred = y_std
-
-                # Surrogate model's error using RMSE of test data
-                if hasattr(MetaModel, 'rmse'):
-                    surrError = MetaModel.rmse
-                else:
-                    surrError = None
-
-            else:
-                # Evaluate the original model
-                self.__model_prior_pred = self._eval_model(
-                    samples=self.samples, key='PriorPred'
-                    )
-                surrError = None
-
-            # Start the likelihood-BME computations for the perturbed data
-            for itr_idx, data in tqdm(
-                    enumerate(self.perturbed_data),
-                    total=self.n_bootstrap_itrs,
-                    desc="Bootstrapping the BME calculations", ascii=True
-                    ):
-
-                # ---------------- Likelihood calculation ----------------
-                if self.emulator:
-                    model_evals = self.__mean_pce_prior_pred
-                else:
-                    model_evals = self.__model_prior_pred
-
-                # Leave one out
-                if self.bayes_loocv or self.just_analysis:
-                    self.selected_indices = np.nonzero(data)[0]
-
-                # Prepare data dataframe
-                nobs = list(self.measured_data.count().values[1:])
-                numbers = list(np.cumsum(nobs))
-                indices = list(zip([0] + numbers, numbers))
-                data_dict = {
-                    output_names[i]: data[j:k] for i, (j, k) in
-                    enumerate(indices)
-                    }
-                #print(output_names)
-                #print(indices)
-                #print(numbers)
-                #print(nobs)
-                #print(self.measured_data)
-                #for i, (j, k) in enumerate(indices):
-                #    print(i,j,k)
-                #print(data)
-                #print(data_dict)
-                #stop
-
-                # Unknown sigma2
-                if opt_sigma == 'C' or hasattr(self, 'sigma2s'):
-                    logLikelihoods[:, itr_idx] = self.normpdf(
-                        model_evals, data_dict, total_sigma2,
-                        sigma2=self.sigma2s, std=surrError
-                        )
-                else:
-                    # known sigma2
-                    logLikelihoods[:, itr_idx] = self.normpdf(
-                        model_evals, data_dict, total_sigma2,
-                        std=surrError
-                        )
-
-                # ---------------- BME Calculations ----------------
-                # BME (log)
-                log_BME[itr_idx] = np.log(
-                    np.nanmean(np.exp(logLikelihoods[:, itr_idx],
-                                      dtype=np.longdouble))#float128))
-                    )
-
-                # BME correction when using Emulator
-                if self.emulator:
-                    BME_Corr[itr_idx] = self.__corr_factor_BME(
-                        data_dict, total_sigma2, log_BME[itr_idx]
-                        )
-
-                # Rejection Step
-                if 'kld' in list(map(str.lower, self.valid_metrics)) and\
-                   'inf_entropy' in list(map(str.lower, self.valid_metrics)):
-                    # Random numbers between 0 and 1
-                    unif = np.random.rand(1, self.n_samples)[0]
-
-                    # Reject the poorly performed prior
-                    Likelihoods = np.exp(logLikelihoods[:, itr_idx],
-                                         dtype=np.float64)
-                    accepted = (Likelihoods/np.max(Likelihoods)) >= unif
-                    posterior = self.samples[accepted]
-
-                    # Posterior-based expectation of likelihoods
-                    postExpLikelihoods = np.mean(
-                        logLikelihoods[:, itr_idx][accepted]
-                        )
-
-                    # Calculate Kullback-Leibler Divergence
-                    KLD[itr_idx] = postExpLikelihoods - log_BME[itr_idx]
-
-                # Posterior-based expectation of prior densities
-                if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
-                    n_thread = int(0.875 * multiprocessing.cpu_count())
-                    with multiprocessing.Pool(n_thread) as p:
-                        postExpPrior = np.mean(np.concatenate(
-                            p.map(
-                                self.engine.ExpDesign.JDist.pdf,
-                                np.array_split(posterior.T, n_thread, axis=1))
-                            )
-                            )
-                    # Information Entropy based on Entropy paper Eq. 38
-                    inf_entropy[itr_idx] = log_BME[itr_idx] - postExpPrior - \
-                        postExpLikelihoods
-
-                # Clear memory
-                gc.collect(generation=2)
-
-            # ---------- Store metrics for perturbed data set ----------------
-            # Likelihoods (Size: n_samples, n_bootstrap_itr)
-            self.log_likes = logLikelihoods
-
-            # BME (log), KLD, infEntropy (Size: 1,n_bootstrap_itr)
-            self.log_BME = log_BME
-
-            # BMECorrFactor (log) (Size: 1,n_bootstrap_itr)
-            if self.emulator:
-                self.log_BME_corr_factor = BME_Corr
-
-            if 'kld' in list(map(str.lower, self.valid_metrics)):
-                self.KLD = KLD
-            if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
-                self.inf_entropy = inf_entropy
-
-            # BME = BME + BMECorrFactor
-            if self.emulator:
-                self.log_BME += self.log_BME_corr_factor
-
+            self.perform_bootstrap(opt_sigma, total_sigma2)            
+        else:
+            print('No bootstrap for TOM performed!') # TODO: stop the code? Use n_bootstrap = 1?
+        
         # ---------------- Parameter Bayesian inference ----------------
-        if self.inference_method.lower() == 'mcmc':
+        if self.name.lower() == 'valid':
+            # Convert to a dataframe if samples are provided after calibration.
+            self.posterior_df = pd.DataFrame(self.prior_samples, columns=par_names)
+        elif self.inference_method.lower() == 'mcmc':
             # Instantiate the MCMC object
             MCMC_Obj = MCMC(self)
             self.posterior_df = MCMC_Obj.run_sampler(
                 self.measured_data, total_sigma2
                 )
-
-        elif self.name.lower() == 'valid':
-            # Convert to a dataframe if samples are provided after calibration.
-            self.posterior_df = pd.DataFrame(self.samples, columns=par_names)
-
-        else:
+        elif self.inference_method.lower() == 'rejection':
             # Rejection sampling
             self.posterior_df = self._rejection_sampling()
+        else:
+            raise AttributeError('The chosen inference method is not available!')
 
         # Provide posterior's summary
         print('\n')
@@ -502,10 +413,8 @@ class BayesInference:
         # ------------------ Visualization --------------------
         # -----------------------------------------------------
         # Create Output directory, if it doesn't exist already.
-        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
-        os.makedirs(out_dir, exist_ok=True)
 
-        # -------- Posteior parameters --------
+        # -------- Posterior parameters --------
         if opt_sigma != "B":
             par_names.extend(
                 [self.Discrepancy.InputDisc.Marginals[i].name for i
@@ -549,7 +458,7 @@ class BayesInference:
             plotname = f'/Posterior_Dist_{Model.name}'
 
         figPosterior.set_size_inches((24, 16))
-        figPosterior.savefig(f'./{out_dir}{plotname}.pdf',
+        figPosterior.savefig(f'./{self.out_dir}{plotname}.pdf',
                              bbox_inches='tight')
 
         # -------- Plot MAP --------
@@ -558,47 +467,212 @@ class BayesInference:
 
         # -------- Plot log_BME dist --------
         if self.bootstrap:
+            self.plot_log_BME()
+
+        # -------- Posterior perdictives --------
+        if self.plot_post_pred:
+            # Plot the posterior predictive
+            self._plot_post_predictive()
+
+        return self
+    
+    def perform_bootstrap(self, opt_sigma, total_sigma2):
+        """
+        Perform bootstrap to get TOM (??)
+        
+        Parameters
+        ----------
+        opt_sigma : string
+            'A', 'B', or 'C'
+        total_sigma2 : dict
+            Dictionary containing the sigma2 for the training(?) data
+        Returns
+        -------
+        None.
+
+        """
+        MetaModel = self.MetaModel
+        n_params = MetaModel.n_params
+        output_names = self.engine.Model.Output.names
 
-            # Computing the TOM performance
-            self.log_BME_tom = stats.chi2.rvs(
-                self.n_tot_measurement, size=self.log_BME.shape[0]
+        # Adding some zero mean noise to the observation function
+        if len(self.perturbed_data) == 0:
+            self.perturbed_data = self._perturb_data(
+                self.measured_data, output_names
                 )
+        else:
+            self.n_bootstrap_itrs = len(self.perturbed_data)
 
-            fig, ax = plt.subplots()
-            sns.kdeplot(self.log_BME_tom, ax=ax, color="green", shade=True)
-            sns.kdeplot(
-                self.log_BME, ax=ax, color="blue", shade=True,
-                label='Model BME')
+        # -------- Model Discrepancy -----------
+        if hasattr(self, 'error_model') and self.error_model \
+           and self.name.lower() != 'calib':# TODO: what should be set so that this is tested?
+            # Select prior ? mean as MAP
+            MAP_theta = self.prior_samples.mean(axis=0).reshape((1, n_params))
 
-            ax.set_xlabel('log$_{10}$(BME)')
-            ax.set_ylabel('Probability density')
+            # Evaluate the (meta-)model at the MAP
+            y_MAP, y_std_MAP = MetaModel.eval_metamodel(samples=MAP_theta)
 
-            legend_elements = [
-                Patch(facecolor='green', edgecolor='green', label='TOM BME'),
-                Patch(facecolor='blue', edgecolor='blue', label='Model BME')
-                ]
-            ax.legend(handles=legend_elements)
+            # Train a GPR meta-model using MAP
+            self.error_MetaModel = MetaModel.create_model_error(
+                self.bias_inputs, y_MAP, Name=self.name
+                )
 
-            if self.emulator:
-                plotname = f'/BME_hist_{Model.name}_emulator'
+        # -----------------------------------------------------
+        # ----- Loop over the perturbed observation data ------
+        # -----------------------------------------------------
+        # Initilize arrays
+        logLikelihoods = np.zeros((self.n_prior_samples, self.n_bootstrap_itrs),
+                                  dtype=np.float16)
+        BME_Corr = np.zeros((self.n_bootstrap_itrs))
+        log_BME = np.zeros((self.n_bootstrap_itrs))
+        KLD = np.zeros((self.n_bootstrap_itrs))
+        inf_entropy = np.zeros((self.n_bootstrap_itrs))
+
+        # Compute the prior predictions
+        # Evaluate the MetaModel
+        if self.emulator:
+            y_hat, y_std = MetaModel.eval_metamodel(samples=self.prior_samples)
+            self.__mean_pce_prior_pred = y_hat
+            self._std_pce_prior_pred = y_std
+
+            # Correct the predictions with Model discrepancy
+            if hasattr(self, 'error_model') and self.error_model: # TODO this does not check for calib?
+                y_hat_corr, y_std = self.error_MetaModel.eval_model_error(
+                    self.bias_inputs, self.__mean_pce_prior_pred
+                    )
+                self.__mean_pce_prior_pred = y_hat_corr
+                self._std_pce_prior_pred = y_std
+
+            # Surrogate model's error using RMSE of test data
+            if hasattr(MetaModel, 'rmse'):
+                surrError = MetaModel.rmse
             else:
-                plotname = f'/BME_hist_{Model.name}'
+                surrError = None
 
-            plt.savefig(f'./{out_dir}{plotname}.pdf', bbox_inches='tight')
-            plt.show()
-            plt.close()
+        else:
+            # Evaluate the original model
+            self.__model_prior_pred = self._eval_model(
+                samples=self.prior_samples, key='PriorPred'
+                )
+            surrError = None
+        
+        # Start the likelihood-BME computations for the perturbed data
+        for itr_idx, data in tqdm(
+                enumerate(self.perturbed_data),
+                total=self.n_bootstrap_itrs,
+                desc="Bootstrapping the BME calculations", ascii=True
+                ):
+            print('')
+            #print(itr_idx, data)
+            #print(np.nonzero(data))
+
+            # ---------------- Likelihood calculation ----------------
+            if self.emulator: # TODO: do this outside of the loop?
+                model_evals = self.__mean_pce_prior_pred
+            else:
+                model_evals = self.__model_prior_pred
+
+            # Leave one out  # TODO: why is this loo? It just looks at perturbed data?
+            if self.bayes_loocv or self.just_analysis:
+                # Consider only non-zero entries
+                self.selected_indices = np.nonzero(data)[0]
+
+            # Prepare data dataframe # TODO: what's with this transformation?
+            nobs = list(self.measured_data.count().values[1:])
+            numbers = list(np.cumsum(nobs))
+            indices = list(zip([0] + numbers, numbers))
+            data_dict = {
+                output_names[i]: data[j:k] for i, (j, k) in
+                enumerate(indices)
+                }
 
-        # -------- Posteior perdictives --------
-        if self.plot_post_pred:
-            # Plot the posterior predictive
-            self._plot_post_predictive()
+            # Unknown sigma2
+            if opt_sigma == 'C' or hasattr(self, 'sigma2s'):
+                logLikelihoods[:, itr_idx] = self.normpdf(
+                    model_evals, data_dict, total_sigma2,
+                    sigma2=self.sigma2s, std=surrError
+                    )
+            else:
+                # known sigma2
+                logLikelihoods[:, itr_idx] = self.normpdf(
+                    model_evals, data_dict, total_sigma2,
+                    std=surrError
+                    )
+            # ---------------- BME Calculations ----------------
+            # BME (log)
+            log_BME[itr_idx] = np.log(
+                np.nanmean(np.exp(logLikelihoods[:, itr_idx],
+                                  dtype=np.longdouble))#float128))
+                )
+
+            # BME correction when using Emulator
+            if self.emulator:
+                BME_Corr[itr_idx] = self._corr_factor_BME(
+                    data_dict, total_sigma2, log_BME[itr_idx]
+                    )
+
+            # Rejection Step
+            if 'kld' in list(map(str.lower, self.valid_metrics)) and\
+               'inf_entropy' in list(map(str.lower, self.valid_metrics)):  # TODO: why and and not or?
+                # Random numbers between 0 and 1
+                unif = np.random.rand(1, self.n_prior_samples)[0]
+
+                # Reject the poorly performed prior
+                Likelihoods = np.exp(logLikelihoods[:, itr_idx],
+                                     dtype=np.float64)
+                accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+                posterior = self.prior_samples[accepted]
+
+                # Posterior-based expectation of likelihoods
+                postExpLikelihoods = np.mean(
+                    logLikelihoods[:, itr_idx][accepted]
+                    )
+
+                # Calculate Kullback-Leibler Divergence
+                KLD[itr_idx] = postExpLikelihoods - log_BME[itr_idx]
+
+            # Posterior-based expectation of prior densities
+            if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+                n_thread = int(0.875 * multiprocessing.cpu_count())
+                with multiprocessing.Pool(n_thread) as p:
+                    postExpPrior = np.mean(np.concatenate(
+                        p.map(
+                            self.engine.ExpDesign.JDist.pdf,
+                            np.array_split(posterior.T, n_thread, axis=1))
+                        )
+                        )
+                # Information Entropy based on Entropy paper Eq. 38
+                inf_entropy[itr_idx] = log_BME[itr_idx] - postExpPrior - \
+                    postExpLikelihoods
+
+            # Clear memory
+            gc.collect(generation=2)
+
+        # ---------- Store metrics for perturbed data set ----------------
+        # Likelihoods (Size: n_samples, n_bootstrap_itr)
+        self.log_likes = logLikelihoods
+
+        # BME (log), KLD, infEntropy (Size: 1,n_bootstrap_itr)
+        self.log_BME = log_BME
+
+        # BMECorrFactor (log) (Size: 1,n_bootstrap_itr)
+        if self.emulator:
+            self.log_BME_corr_factor = BME_Corr
+            # BME = BME + BMECorrFactor
+            self.log_BME += self.log_BME_corr_factor
+
+        if 'kld' in list(map(str.lower, self.valid_metrics)):
+            self.KLD = KLD
+        if 'inf_entropy' in list(map(str.lower, self.valid_metrics)):
+            self.inf_entropy = inf_entropy
+
+    
 
-        return self
 
     # -------------------------------------------------------------------------
     def _perturb_data(self, data, output_names):
         """
-        Returns an array with n_bootstrap_itrs rowsof perturbed data.
+        Returns an array with n_bootstrap_itrs rows of perturbed data.
         The first row includes the original observation data.
         If `self.bayes_loocv` is True, a 2d-array will be returned with
         repeated rows and zero diagonal entries.
@@ -620,12 +694,11 @@ class BayesInference:
         obs_data = data[output_names].values
         n_measurement, n_outs = obs_data.shape
         self.n_tot_measurement = obs_data[~np.isnan(obs_data)].shape[0]
-        # Number of bootstrap iterations
-        if self.bayes_loocv:
-            self.n_bootstrap_itrs = self.n_tot_measurement
-
+        
         # Pass loocv dataset
         if self.bayes_loocv:
+            # Number of bootstrap iterations
+            self.n_bootstrap_itrs = self.n_tot_measurement
             obs = obs_data.T[~np.isnan(obs_data.T)]
             final_data = np.repeat(np.atleast_2d(obs), self.n_bootstrap_itrs,
                                    axis=0)
@@ -633,6 +706,7 @@ class BayesInference:
             return final_data
 
         else:
+            # Init return data with original data
             final_data = np.zeros(
                 (self.n_bootstrap_itrs, self.n_tot_measurement)
                 )
@@ -640,9 +714,11 @@ class BayesInference:
             for itrIdx in range(1, self.n_bootstrap_itrs):
                 data = np.zeros((n_measurement, n_outs))
                 for idx in range(len(output_names)):
+                    # Perturb the data
                     std = np.nanstd(obs_data[:, idx])
                     if std == 0:
-                        std = 0.001
+                        print('Note: Use std=0.01 for perturbation')
+                        std = 0.001 
                     noise = std * noise_level
                     data[:, idx] = np.add(
                         obs_data[:, idx],
@@ -653,45 +729,18 @@ class BayesInference:
 
             return final_data
 
-    # -------------------------------------------------------------------------
-    def _logpdf(self, x, mean, cov):
-        """
-        computes the likelihood based on a multivariate normal distribution.
-
-        Parameters
-        ----------
-        x : TYPE
-            DESCRIPTION.
-        mean : array_like
-            Observation data.
-        cov : 2d array
-            Covariance matrix of the distribution.
-
-        Returns
-        -------
-        log_lik : float
-            Log likelihood.
-
-        """
-        n = len(mean)
-        L = spla.cholesky(cov, lower=True)
-        beta = np.sum(np.log(np.diag(L)))
-        dev = x - mean
-        alpha = dev.dot(spla.cho_solve((L, True), dev))
-        log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
-        return log_lik
-
+    
     # -------------------------------------------------------------------------
     def _eval_model(self, samples=None, key='MAP'):
         """
-        Evaluates Forward Model.
+        Evaluates Forward Model and zips the results
 
         Parameters
         ----------
         samples : array of shape (n_samples, n_params), optional
             Parameter sets. The default is None.
         key : str, optional
-            Key string to be passed to the run_model_parallel method.
+            Descriptive key string for the run_model_parallel method.
             The default is 'MAP'.
 
         Returns
@@ -700,18 +749,17 @@ class BayesInference:
             Model outputs.
 
         """
-        MetaModel = self.MetaModel
         Model = self.engine.Model
 
         if samples is None:
-            self.samples = self.engine.ExpDesign.generate_samples(
-                self.n_samples, 'random')
+            self.prior_samples = self.engine.ExpDesign.generate_samples(
+                self.n_prior_samples, 'random')
         else:
-            self.samples = samples
-            self.n_samples = len(samples)
+            self.prior_samples = samples
+            self.n_prior_samples = len(samples)
 
         model_outputs, _ = Model.run_model_parallel(
-            self.samples, key_str=key+self.name)
+            self.prior_samples, key_str=key+self.name)
 
         # Clean up
         # Zip the subdirectories
@@ -724,55 +772,6 @@ class BayesInference:
 
         return model_outputs
 
-    # -------------------------------------------------------------------------
-    def _kernel_rbf(self, X, hyperparameters):
-        """
-        Isotropic squared exponential kernel.
-
-        Higher l values lead to smoother functions and therefore to coarser
-        approximations of the training data. Lower l values make functions
-        more wiggly with wide uncertainty regions between training data points.
-
-        sigma_f controls the marginal variance of b(x)
-
-        Parameters
-        ----------
-        X : ndarray of shape (n_samples_X, n_features)
-
-        hyperparameters : Dict
-            Lambda characteristic length
-            sigma_f controls the marginal variance of b(x)
-            sigma_0 unresolvable error nugget term, interpreted as random
-                    error that cannot be attributed to measurement error.
-        Returns
-        -------
-        var_cov_matrix : ndarray of shape (n_samples_X,n_samples_X)
-            Kernel k(X, X).
-
-        """
-        from sklearn.gaussian_process.kernels import RBF
-        min_max_scaler = preprocessing.MinMaxScaler()
-        X_minmax = min_max_scaler.fit_transform(X)
-
-        nparams = len(hyperparameters)
-        # characteristic length (0,1]
-        Lambda = hyperparameters[0]
-        # sigma_f controls the marginal variance of b(x)
-        sigma2_f = hyperparameters[1]
-
-        # cov_matrix = sigma2_f*rbf_kernel(X_minmax, gamma = 1/Lambda**2)
-
-        rbf = RBF(length_scale=Lambda)
-        cov_matrix = sigma2_f * rbf(X_minmax)
-        if nparams > 2:
-            # (unresolvable error) nugget term that is interpreted as random
-            # error that cannot be attributed to measurement error.
-            sigma2_0 = hyperparameters[2:]
-            for i, j in np.ndindex(cov_matrix.shape):
-                cov_matrix[i, j] += np.sum(sigma2_0) if i == j else 0
-
-        return cov_matrix
-
     # -------------------------------------------------------------------------
     def normpdf(self, outputs, obs_data, total_sigma2s, sigma2=None, std=None):
         """
@@ -809,11 +808,11 @@ class BayesInference:
 
         # Extract the requested model outputs for likelihood calulation
         if self.req_outputs is None:
-            req_outputs = Model.Output.names
+            req_outputs = Model.Output.names  # TODO: should this then be saved as self.req_outputs?
         else:
             req_outputs = list(self.req_outputs)
-
-        # Loop over the outputs
+            
+        # Loop over the output keys
         for idx, out in enumerate(req_outputs):
 
             # (Meta)Model Output
@@ -825,27 +824,26 @@ class BayesInference:
             except AttributeError:
                 data = obs_data[out][~np.isnan(obs_data[out])]
 
-            # Prepare sigma2s
+            # Prepare data uncertainty / error estimation (sigma2s)
             non_nan_indices = ~np.isnan(total_sigma2s[out])
             tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
 
-            # Add the std of the PCE is chosen as emulator.
+            # Add the std of the PCE if an emulator is used
             if self.emulator:
                 if std is not None:
                     tot_sigma2s += std[out]**2
 
-            # Covariance Matrix
-            covMatrix = np.diag(tot_sigma2s)
-
             # Select the data points to compare
             try:
                 indices = self.selected_indices[out]
             except:
                 indices = list(range(nout))
-            covMatrix = np.diag(covMatrix[indices, indices])
+                
+            # Set up Covariance Matrix
+            covMatrix = np.diag(np.diag(tot_sigma2s)[indices, indices])
 
-            # If sigma2 is not given, use given total_sigma2s
-            if sigma2 is None:
+            # If sigma2 is not given, use given total_sigma2s and move to next itr
+            if sigma2 is None: 
                 logLik += stats.multivariate_normal.logpdf(
                     outputs[out][:, indices], data[indices], covMatrix)
                 continue
@@ -860,26 +858,25 @@ class BayesInference:
                 # Covariance Matrix
                 covMatrix = np.diag(tot_sigma2s)
 
-                if sigma2 is not None:
-                    # Check the type error term
-                    if hasattr(self, 'bias_inputs') and \
-                       not hasattr(self, 'error_model'):
-                        # Infer a Bias model usig Gaussian Process Regression
-                        bias_inputs = np.hstack(
-                            (self.bias_inputs[out],
-                             tot_outputs[s_idx].reshape(-1, 1)))
-
-                        params = sigma2[s_idx, idx*3:(idx+1)*3]
-                        covMatrix = self._kernel_rbf(bias_inputs, params)
-                    else:
-                        # Infer equal sigma2s
-                        try:
-                            sigma_2 = sigma2[s_idx, idx]
-                        except TypeError:
-                            sigma_2 = 0.0
+                # Check the type error term
+                if hasattr(self, 'bias_inputs') and \
+                   not hasattr(self, 'error_model'):
+                    # Infer a Bias model usig Gaussian Process Regression
+                    bias_inputs = np.hstack(
+                        (self.bias_inputs[out],
+                         tot_outputs[s_idx].reshape(-1, 1)))
+
+                    params = sigma2[s_idx, idx*3:(idx+1)*3]
+                    covMatrix = _kernel_rbf(bias_inputs, params)
+                else:
+                    # Infer equal sigma2s
+                    try:
+                        sigma_2 = sigma2[s_idx, idx]
+                    except TypeError:
+                        sigma_2 = 0.0
 
-                        covMatrix += sigma_2 * np.eye(nout)
-                        # covMatrix = np.diag(sigma2 * total_sigma2s)
+                    covMatrix += sigma_2 * np.eye(nout)
+                    # covMatrix = np.diag(sigma2 * total_sigma2s)
 
                 # Select the data points to compare
                 try:
@@ -889,80 +886,40 @@ class BayesInference:
                 covMatrix = np.diag(covMatrix[indices, indices])
 
                 # Compute loglikelihood
-                logliks[s_idx] = self._logpdf(
+                logliks[s_idx] = _logpdf(
                     tot_outputs[s_idx, indices], data[indices], covMatrix
                     )
-
+            #print(logLik)
             logLik += logliks
         return logLik
 
     # -------------------------------------------------------------------------
-    def _corr_factor_BME_old(self, Data, total_sigma2s, posterior):
+    def _corr_factor_BME(self, obs_data, total_sigma2s, logBME):
         """
         Calculates the correction factor for BMEs.
-        """
-        MetaModel = self.MetaModel
-        OrigModelOutput = self.engine.ExpDesign.Y
-        Model = self.engine.Model
-
-        # Posterior with guassian-likelihood
-        postDist = stats.gaussian_kde(posterior.T)
-
-        # Remove NaN
-        Data = Data[~np.isnan(Data)]
-        total_sigma2s = total_sigma2s[~np.isnan(total_sigma2s)]
-
-        # Covariance Matrix
-        covMatrix = np.diag(total_sigma2s[:self.n_tot_measurement])
-
-        # Extract the requested model outputs for likelihood calulation
-        if self.req_outputs is None:
-            OutputType = Model.Output.names
-        else:
-            OutputType = list(self.req_outputs)
-
-        # SampleSize = OrigModelOutput[OutputType[0]].shape[0]
-
-
-        # Flatten the OutputType for OrigModel
-        TotalOutputs = np.concatenate([OrigModelOutput[x] for x in OutputType], 1)
-
-        NrofBayesSamples = self.n_samples
-        # Evaluate MetaModel on the experimental design
-        Samples = self.engine.ExpDesign.X
-        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=Samples)
-
-        # Reset the NrofSamples to NrofBayesSamples
-        self.n_samples = NrofBayesSamples
-
-        # Flatten the OutputType for MetaModel
-        TotalPCEOutputs = np.concatenate([OutputRS[x] for x in OutputRS], 1)
-        TotalPCEstdOutputRS= np.concatenate([stdOutputRS[x] for x in stdOutputRS], 1)
+        
+        Parameters
+        ----------
+        obs_data : dict
+            A dictionary/dataframe containing the observation data.
+        total_sigma2s : dict
+            A dictionary with known values of the covariance diagonal entries,
+            a.k.a sigma^2.
+        logBME : ??
+            ??
 
-        logweight = 0
-        for i, sample in enumerate(Samples):
-            # Compute likelilhood output vs RS
-            covMatrix = np.diag(TotalPCEstdOutputRS[i]**2)
-            logLik = self._logpdf(TotalOutputs[i], TotalPCEOutputs[i], covMatrix)
-            # Compute posterior likelihood of the collocation points
-            logpostLik = np.log(postDist.pdf(sample[:, None]))[0]
-            if logpostLik != -np.inf:
-                logweight += logLik + logpostLik
-        return logweight
+        Returns
+        -------
+        np.log(weights) : ??
+            Correction factors # TODO: factors or log of factors?
 
-    # -------------------------------------------------------------------------
-    def __corr_factor_BME(self, obs_data, total_sigma2s, logBME):
-        """
-        Calculates the correction factor for BMEs.
         """
+        # Extract the requested model outputs for likelihood calulation
         MetaModel = self.MetaModel
         samples = self.engine.ExpDesign.X
         model_outputs = self.engine.ExpDesign.Y
-        Model = self.engine.Model
         n_samples = samples.shape[0]
-
-        # Extract the requested model outputs for likelihood calulation
-        output_names = Model.Output.names
+        output_names = self.engine.Model.Output.names
 
         # Evaluate MetaModel on the experimental design and ValidSet
         OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
@@ -1008,13 +965,13 @@ class BayesInference:
                 covMatrix_data = np.diag(covMatrix_data[indices, indices])
 
                 # Compute likelilhood output vs data
-                logLik_data[i] += self._logpdf(
+                logLik_data[i] += _logpdf(
                     y_m_hat[indices], data[indices],
                     covMatrix_data
                     )
 
                 # Compute likelilhood output vs surrogate
-                logLik_model[i] += self._logpdf(
+                logLik_model[i] += _logpdf(
                     y_m_hat[indices], y_m[indices],
                     covMatrix
                     )
@@ -1037,48 +994,51 @@ class BayesInference:
             Posterior samples of the input parameters.
 
         """
+        if self.prior_samples is None:
+            raise AttributeError('No prior samples available!')
 
-        MetaModel = self.MetaModel
+        if not hasattr(self, 'log_likes'):
+            raise AttributeError('No log-likelihoods available!')
+        
+        # Get sigmas # TODO: is this data uncertainty?
         try:
             sigma2_prior = self.Discrepancy.sigma2_prior
         except:
             sigma2_prior = None
 
-        # Check if the discrepancy is defined as a distribution:
-        samples = self.samples
-
+        # Combine samples and sigma2 for the return
+        samples = self.prior_samples
         if sigma2_prior is not None:
             samples = np.hstack((samples, sigma2_prior))
 
         # Take the first column of Likelihoods (Observation data without noise)
         if self.just_analysis or self.bayes_loocv:
             index = self.n_tot_measurement-1
-            likelihoods = np.exp(self.log_likes[:, index], dtype=np.longdouble)#np.float128)
         else:
-            likelihoods = np.exp(self.log_likes[:, 0], dtype=np.longdouble)#np.float128)
+            index = 0
+        
+        # Use longdouble on windows, float128 on linux
+        if os.name != 'nt':
+            likelihoods = np.exp(self.log_likes[:, index], dtype=np.float128)
+        else:
+            print('WARNING: Performing the inference on windows can lead to reduced accuracy!')
+            likelihoods = np.exp(self.log_likes[:, index], dtype=np.longdouble)
 
         n_samples = len(likelihoods)
-        norm_ikelihoods = likelihoods / np.max(likelihoods)
+        norm_likelihoods = likelihoods / np.max(likelihoods)
 
         # Normalize based on min if all Likelihoods are zero
         if all(likelihoods == 0.0):
             likelihoods = self.log_likes[:, 0]
-            norm_ikelihoods = likelihoods / np.min(likelihoods)
+            norm_likelihoods = likelihoods / np.min(likelihoods)
 
-        # Random numbers between 0 and 1
+        # Reject the poorly performed prior compared to a uniform distribution
         unif = np.random.rand(1, n_samples)[0]
-
-        # Reject the poorly performed prior
-        accepted_samples = samples[norm_ikelihoods >= unif]
-
-        # Output the Posterior
-        par_names = self.engine.ExpDesign.par_names
-        if sigma2_prior is not None:
-            for name in self.Discrepancy.name:
-                par_names.append(name)
+        accepted_samples = samples[norm_likelihoods >= unif]
 
         return pd.DataFrame(accepted_samples, columns=sigma2_prior)
 
+
     # -------------------------------------------------------------------------
     def _posterior_predictive(self):
         """
@@ -1100,11 +1060,7 @@ class BayesInference:
         MetaModel = self.MetaModel
         Model = self.engine.Model
 
-        # Make a directory to save the prior/posterior predictive
-        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
-        os.makedirs(out_dir, exist_ok=True)
-
-        # Read observation data and perturb it if requested
+        # Read observation data and perturb it if requested # TODO: where is the perturbation?
         if self.measured_data is None:
             self.measured_data = Model.read_observation(case=self.name)
 
@@ -1125,7 +1081,7 @@ class BayesInference:
         # Take care of the sigma2
         if sigma2_prior is not None:
             try:
-                sigma2s = posterior_df[self.Discrepancy.name].values
+                sigma2s = posterior_df[self.Discrepancy.name].values # TODO: what is Discrepancy.name?
                 posterior_df = posterior_df.drop(
                     labels=self.Discrepancy.name, axis=1
                     )
@@ -1134,17 +1090,17 @@ class BayesInference:
 
         # Posterior predictive
         if self.emulator:
-            if self.inference_method == 'rejection':
+            if self.inference_method == 'rejection': # TODO: combine these two?
                 prior_pred = self.__mean_pce_prior_pred
             if self.name.lower() != 'calib':
                 post_pred = self.__mean_pce_prior_pred
                 post_pred_std = self._std_pce_prior_pred
             else:
-                post_pred, post_pred_std = MetaModel.eval_metamodel(
+                post_pred, post_pred_std = MetaModel.eval_metamodel( # TODO: recheck if this is needed
                     samples=posterior_df.values
                     )
 
-        else:
+        else: # TODO: see emulator version
             if self.inference_method == 'rejection':
                 prior_pred = self.__model_prior_pred
             if self.name.lower() != 'calib':
@@ -1182,7 +1138,7 @@ class BayesInference:
                         bias_inputs = np.hstack((
                             self.bias_inputs[var], pred.reshape(-1, 1)))
                         params = sigma2s[i, varIdx*3:(varIdx+1)*3]
-                        cov = self._kernel_rbf(bias_inputs, params)
+                        cov = _kernel_rbf(bias_inputs, params)
                     else:
                         # Infer equal sigma2s
                         try:
@@ -1211,7 +1167,7 @@ class BayesInference:
         # ----- Prior Predictive -----
         if self.inference_method.lower() == 'rejection':
             # Create hdf5 metadata
-            hdf5file = f'{out_dir}/priorPredictive.hdf5'
+            hdf5file = f'{self.out_dir}/priorPredictive.hdf5'
             hdf5_exist = os.path.exists(hdf5file)
             if hdf5_exist:
                 os.remove(hdf5file)
@@ -1232,7 +1188,7 @@ class BayesInference:
 
         # ----- Posterior Predictive only model evaluations -----
         # Create hdf5 metadata
-        hdf5file = out_dir+'/postPredictive_wo_noise.hdf5'
+        hdf5file = self.out_dir+'/postPredictive_wo_noise.hdf5'
         hdf5_exist = os.path.exists(hdf5file)
         if hdf5_exist:
             os.remove(hdf5file)
@@ -1253,7 +1209,7 @@ class BayesInference:
 
         # ----- Posterior Predictive with noise -----
         # Create hdf5 metadata
-        hdf5file = out_dir+'/postPredictive.hdf5'
+        hdf5file = self.out_dir+'/postPredictive.hdf5'
         hdf5_exist = os.path.exists(hdf5file)
         if hdf5_exist:
             os.remove(hdf5file)
@@ -1288,7 +1244,6 @@ class BayesInference:
 
         MetaModel = self.MetaModel
         Model = self.engine.Model
-        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
         opt_sigma = self.Discrepancy.opt_sigma
 
         # -------- Find MAP and run MetaModel and origModel --------
@@ -1324,7 +1279,7 @@ class BayesInference:
         Marker = 'x'
 
         # Create a PdfPages object
-        pdf = PdfPages(f'./{out_dir}MAP_PCE_vs_Model_{self.name}.pdf')
+        pdf = PdfPages(f'./{self.out_dir}MAP_PCE_vs_Model_{self.name}.pdf')
         fig = plt.figure()
         for i, key in enumerate(Model.Output.names):
 
@@ -1374,6 +1329,38 @@ class BayesInference:
             plt.clf()
 
         pdf.close()
+        
+    def plot_log_BME(self):
+        
+        # Computing the TOM performance
+        self.log_BME_tom = stats.chi2.rvs(
+            self.n_tot_measurement, size=self.log_BME.shape[0]
+            )
+
+        fig, ax = plt.subplots()
+        sns.kdeplot(self.log_BME_tom, ax=ax, color="green", shade=True)
+        sns.kdeplot(
+            self.log_BME, ax=ax, color="blue", shade=True,
+            label='Model BME')
+
+        ax.set_xlabel('log$_{10}$(BME)')
+        ax.set_ylabel('Probability density')
+
+        legend_elements = [
+            Patch(facecolor='green', edgecolor='green', label='TOM BME'),
+            Patch(facecolor='blue', edgecolor='blue', label='Model BME')
+            ]
+        ax.legend(handles=legend_elements)
+
+        if self.emulator:
+            plotname = f'/BME_hist_{self.Model.name}_emulator'
+        else:
+            plotname = f'/BME_hist_{self.Model.name}'
+
+        plt.savefig(f'./{self.self.out_dir}{plotname}.pdf', bbox_inches='tight')
+        plt.show()
+        plt.close()
+
 
     # -------------------------------------------------------------------------
     def _plot_post_predictive(self):
@@ -1387,7 +1374,6 @@ class BayesInference:
         """
 
         Model = self.engine.Model
-        out_dir = f'Outputs_Bayes_{Model.name}_{self.name}'
         # Plot the posterior predictive
         for out_idx, out_name in enumerate(Model.Output.names):
             fig, ax = plt.subplots()
@@ -1400,7 +1386,7 @@ class BayesInference:
                     #  --- Prior ---
                     # Load posterior predictive
                     f = h5py.File(
-                        f'{out_dir}/priorPredictive.hdf5', 'r+')
+                        f'{self.out_dir}/priorPredictive.hdf5', 'r+')
 
                     try:
                         x_coords = np.array(f[f"x_values/{out_name}"])
@@ -1422,7 +1408,7 @@ class BayesInference:
                     f.close()
 
                     # --- Posterior ---
-                    f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+')
+                    f = h5py.File(f"{self.out_dir}/postPredictive.hdf5", 'r+')
 
                     X_values = np.repeat(
                         x_coords, np.array(f[f"EDY/{out_name}"]).shape[0])
@@ -1477,7 +1463,7 @@ class BayesInference:
 
                 else:
                     # Load posterior predictive
-                    f = h5py.File(f"{out_dir}/postPredictive.hdf5", 'r+')
+                    f = h5py.File(f"{self.out_dir}/postPredictive.hdf5", 'r+')
 
                     try:
                         x_coords = np.array(f[f"x_values/{out_name}"])
@@ -1528,5 +1514,5 @@ class BayesInference:
                 else:
                     plotname = f'/Post_Prior_Perd_{Model.name}'
 
-                fig.savefig(f'./{out_dir}{plotname}_{out_name}.pdf',
+                fig.savefig(f'./{self.out_dir}{plotname}_{out_name}.pdf',
                             bbox_inches='tight')
diff --git a/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py b/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py
index 828613556e90ec0c529b91f2592eec148c98136b..769ad2ceaaced2c1fb6f18d22a9ca27278c3e8a1 100644
--- a/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py
+++ b/src/bayesvalidrox/bayes_inference/bayes_model_comparison.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3
 # -*- coding: utf-8 -*-
 
+import emcee
 import numpy as np
 import os
 from scipy import stats
@@ -27,7 +28,7 @@ class BayesModelComparison:
         `True`.
     perturbed_data : array of shape (n_bootstrap_itrs, n_obs), optional
         User defined perturbed data. The default is `None`.
-    n_bootstarp : int
+    n_bootstrap : int
         Number of bootstrap iteration. The default is `1000`.
     data_noise_level : float
         A noise level to perturb the data set. The default is `0.01`.
@@ -38,13 +39,15 @@ class BayesModelComparison:
     """
 
     def __init__(self, justifiability=True, perturbed_data=None,
-                 n_bootstarp=1000, data_noise_level=0.01, just_n_meas=2):
+                 n_bootstrap=1000, data_noise_level=0.01, just_n_meas=2):
 
+        # TODO: check valid ranges of the parameters
+        
         self.justifiability = justifiability
         self.perturbed_data = perturbed_data
-        self.n_bootstarp = n_bootstarp
+        self.n_bootstrap = n_bootstrap
         self.data_noise_level = data_noise_level
-        self.just_n_meas = just_n_meas
+        self.just_n_meas = just_n_meas  # TODO: what is this parameter?
 
     # --------------------------------------------------------------------------
     def create_model_comparison(self, model_dict, opts_dict):
@@ -77,7 +80,9 @@ class BayesModelComparison:
             comparison using Bayes factors and justifiability analysis.
 
         """
-
+        # TODO: why are these two separate calls of the same function?
+        # They should be performable at the same time
+        
         # Bayes factor
         bayes_dict_bf, model_weights_dict_bf = self.compare_models(
             model_dict, opts_dict
@@ -93,7 +98,8 @@ class BayesModelComparison:
             bayes_dict_ja, model_weights_dict_ja = self.compare_models(
                 model_dict, opts_dict, justifiability=True
                 )
-
+            
+            # TODO: why does this version of the call not return a summarized confusion matrix?
             output['Bayes objects JA'] = bayes_dict_ja
             output['Model weights JA'] = model_weights_dict_ja
 
@@ -135,6 +141,7 @@ class BayesModelComparison:
         self.model_names = [*model_dict]
 
         # Compute total number of the measurement points
+        # TODO: there could be a different option for this here
         Engine = list(model_dict.items())[0][1]
         Engine.Model.read_observation()
         self.n_meas = Engine.Model.n_obs
@@ -142,21 +149,40 @@ class BayesModelComparison:
         # ----- Generate data -----
         # Find n_bootstrap
         if self.perturbed_data is None:
-            n_bootstarp = self.n_bootstarp
+            n_bootstrap = self.n_bootstrap
         else:
-            n_bootstarp = self.perturbed_data.shape[0]
+            n_bootstrap = self.perturbed_data.shape[0]
 
         # Create dataset
         justData = self.generate_dataset(
-            model_dict, justifiability, n_bootstarp=n_bootstarp)
+            model_dict, justifiability, n_bootstarp=n_bootstrap)
 
         # Run create Interface for each model
-        bayes_dict = {}
+        self.bayes_dict = {}
         for model in model_dict.keys():
             print("-"*20)
             print("Bayesian inference of {}.\n".format(model))
 
             BayesOpts = BayesInference(model_dict[model])
+            
+            # Explicitly set the settings of the BayesOpts
+            if self.use_Bayes_settings:
+                BayesOpts.emulator= True
+                BayesOpts.plot_post_pred = True
+                #BayesOpts.inference_method = 'rejection'
+                BayesOpts.bootstrap = True
+                BayesOpts.n_bootstrap_itrs = 10
+                BayesOpts.bootstrap_noise = 0.05
+                
+                # Set the MCMC parameters
+                BayesOpts.inference_method = "MCMC"
+                BayesOpts.mcmc_params = {
+                    'n_steps': 1e3,#5,
+                    'n_walkers': 30,
+                    'moves': emcee.moves.KDEMove(),
+                    'multiprocessing': False,
+                    'verbose': False
+                    }
 
             # Set BayesInference options
             for key, value in opts_dict.items():
@@ -170,17 +196,17 @@ class BayesModelComparison:
             BayesOpts.perturbed_data = justData
             BayesOpts.just_analysis = justifiability
 
-            bayes_dict[model] = BayesOpts.create_inference()
+            self.bayes_dict[model] = BayesOpts.create_inference()
             print("-"*20)
 
         # Compute model weights
-        BME_Dict = dict()
-        for modelName, bayesObj in bayes_dict.items():
-            BME_Dict[modelName] = np.exp(bayesObj.log_BME, dtype=np.longdouble)#float128)
+        self.BME_Dict = dict()
+        for modelName, bayesObj in self.bayes_dict.items():
+            self.BME_Dict[modelName] = np.exp(bayesObj.log_BME, dtype=np.longdouble)#float128)
 
         # BME correction in BayesInference class
-        model_weights = self.cal_model_weight(
-            BME_Dict, justifiability, n_bootstarp=n_bootstarp)
+        self.model_weights = self.cal_model_weight(
+            self.BME_Dict, justifiability, n_bootstarp=n_bootstrap)
 
         # Plot model weights
         if justifiability:
@@ -189,27 +215,27 @@ class BayesModelComparison:
 
             # Split the model weights and save in a dict
             list_ModelWeights = np.split(
-                model_weights, model_weights.shape[1]/self.n_meas, axis=1)
+                self.model_weights, self.model_weights.shape[1]/self.n_meas, axis=1)
             model_weights_dict = {key: weights for key, weights in
                                   zip(model_names, list_ModelWeights)}
 
             #self.plot_just_analysis(model_weights_dict)
         else:
             # Create box plot for model weights
-            self.plot_model_weights(model_weights, 'model_weights')
+            self.plot_model_weights(self.model_weights, 'model_weights')
 
             # Create kde plot for bayes factors
-            self.plot_bayes_factor(BME_Dict, 'kde_plot')
+            self.plot_bayes_factor(self.BME_Dict, 'kde_plot')
 
             # Store model weights in a dict
             model_weights_dict = {key: weights for key, weights in
-                                  zip(self.model_names, model_weights)}
+                                  zip(self.model_names, self.model_weights)}
 
-        return bayes_dict, model_weights_dict
+        return self.bayes_dict, model_weights_dict
 
     # -------------------------------------------------------------------------
     def generate_dataset(self, model_dict, justifiability=False,
-                         n_bootstarp=1):
+                         n_bootstrap=1):
         """
         Generates the perturbed data set for the Bayes factor calculations and
         the data set for the justifiability analysis.
@@ -221,7 +247,7 @@ class BayesModelComparison:
         bool, optional
             Whether to perform the justifiability analysis. The default is
             `False`.
-        n_bootstarp : int, optional
+        n_bootstrap : int, optional
             Number of bootstrap iterations. The default is `1`.
 
         Returns
@@ -238,7 +264,7 @@ class BayesModelComparison:
         # Perturb observations for Bayes Factor
         if self.perturbed_data is None:
             self.perturbed_data = self.__perturb_data(
-                    Engine.Model.observations, out_names, n_bootstarp,
+                    Engine.Model.observations, out_names, n_bootstrap,
                     noise_level=self.data_noise_level)
 
         # Only for Bayes Factor
@@ -248,11 +274,11 @@ class BayesModelComparison:
         # Evaluate metamodel
         runs = {}
         for key, metaModel in model_dict.items():
-            y_hat, _ = metaModel.eval_metamodel(nsamples=n_bootstarp)
+            y_hat, _ = metaModel.eval_metamodel(nsamples=n_bootstrap)
             runs[key] = y_hat
 
         # Generate data
-        for i in range(n_bootstarp):
+        for i in range(n_bootstrap):
             y_data = self.perturbed_data[i].reshape(1, -1)
             justData = np.tril(np.repeat(y_data, y_data.shape[1], axis=0))
             # Use surrogate runs for data-generating process
@@ -318,7 +344,7 @@ class BayesModelComparison:
         return final_data
 
     # -------------------------------------------------------------------------
-    def cal_model_weight(self, BME_Dict, justifiability=False, n_bootstarp=1):
+    def cal_model_weight(self, BME_Dict, justifiability=False, n_bootstrap=1):
         """
         Normalize the BME (Asumption: Model Prior weights are equal for models)
 
@@ -339,7 +365,7 @@ class BayesModelComparison:
         if justifiability:
             # Compute expected log_BME for justifiabiliy analysis
             all_BME = all_BME.reshape(
-                all_BME.shape[0], -1, n_bootstarp).mean(axis=2)
+                all_BME.shape[0], -1, n_bootstrap).mean(axis=2)
 
         # Model weights
         model_weights = np.divide(all_BME, np.nansum(all_BME, axis=0))
diff --git a/src/bayesvalidrox/bayes_inference/discrepancy.py b/src/bayesvalidrox/bayes_inference/discrepancy.py
index fff32a2500ae20b3667c7b0ec2cc85c1da614688..b3c235ebeb6d6ae9e109ca862cc522cc21efb45e 100644
--- a/src/bayesvalidrox/bayes_inference/discrepancy.py
+++ b/src/bayesvalidrox/bayes_inference/discrepancy.py
@@ -36,7 +36,7 @@ class Discrepancy:
     * Option B: With unknown redidual covariance matrix \\(\\Sigma\\),
     paramethrized as \\(\\Sigma(\\theta_{\\epsilon})=\\sigma^2 \\textbf{I}_
     {N_{out}}\\) with unknown residual variances \\(\\sigma^2\\).
-    This term will be jointly infer with the uncertain input parameters. For
+    This term will be jointly infered with the uncertain input parameters. For
     the inversion, you need to define a prior marginal via `Input` class. Note
     that \\(\\sigma^2\\) is only a single scalar multiplier for the diagonal
     entries of the covariance matrix \\(\\Sigma\\).
@@ -58,10 +58,17 @@ class Discrepancy:
     """
 
     def __init__(self, InputDisc='', disc_type='Gaussian', parameters=None):
+        # Set the values
         self.InputDisc = InputDisc
         self.disc_type = disc_type
         self.parameters = parameters
-
+        
+        # Other inits
+        self.ExpDesign = None
+        self.n_samples = None
+        self.sigma2_prior = None
+        self.name = None
+        self.opt_sigma = None # This will be set in the inference class and used in mcmc
     # -------------------------------------------------------------------------
     def get_sample(self, n_samples):
         """
@@ -87,6 +94,11 @@ class Discrepancy:
         # Create and store BoundTuples
         self.ExpDesign = ExpDesigns(self.InputDisc)
         self.ExpDesign.sampling_method = 'random'
+        
+        # TODO: why does it call 'generate_ED' instead of 'generate_samples?
+        # ExpDesign.bound_tuples, onp_sigma, prior_space needed from the outside
+        # Discrepancy opt_sigma, InputDisc needed from the outside
+        # TODO: opt_sigma not defined here, but called from the outside??
         self.ExpDesign.generate_ED(
             n_samples, max_pce_deg=1
             )
diff --git a/src/bayesvalidrox/bayes_inference/mcmc.py b/src/bayesvalidrox/bayes_inference/mcmc.py
index fe22a152f117aab7023bfe6592ce3a48bb0b3aec..d78d15b5fd90dc4477da7d0fd58da835acc75310 100755
--- a/src/bayesvalidrox/bayes_inference/mcmc.py
+++ b/src/bayesvalidrox/bayes_inference/mcmc.py
@@ -99,7 +99,7 @@ class MCMC:
                 initsamples = priorDist.sample(self.nwalkers).T
             except:
                 # when aPCE selected - gaussian kernel distribution
-                inputSamples = MetaModel.ExpDesign.raw_data.T
+                inputSamples = self.BayesOpts.engine.ExpDesign.raw_data.T
                 random_indices = np.random.choice(
                     len(inputSamples), size=self.nwalkers, replace=False
                     )
diff --git a/src/bayesvalidrox/surrogate_models/desktop.ini b/src/bayesvalidrox/surrogate_models/desktop.ini
new file mode 100644
index 0000000000000000000000000000000000000000..632de13ae6b61cecf0d9fdbf9c97cfb16bfb51a4
--- /dev/null
+++ b/src/bayesvalidrox/surrogate_models/desktop.ini
@@ -0,0 +1,2 @@
+[LocalizedFileNames]
+exploration.py=@exploration.py,0
diff --git a/src/bayesvalidrox/surrogate_models/engine.py b/src/bayesvalidrox/surrogate_models/engine.py
index 42307d4770d4ae23a40107dfea64057aac682c23..387cec5010373a087b01e838aba89404f2069c51 100644
--- a/src/bayesvalidrox/surrogate_models/engine.py
+++ b/src/bayesvalidrox/surrogate_models/engine.py
@@ -143,6 +143,7 @@ class Engine():
         self.Model = Model
         self.ExpDesign = ExpDes
         self.parallel = False
+        self.trained = False
         
     def start_engine(self) -> None:
         """
@@ -225,6 +226,9 @@ class Engine():
                self.ExpDesign.sampling_method.lower() != 'user':
                 self.Model.zip_subdirs(self.Model.name, f'{self.Model.name}_')
                 
+        # Set that training was done
+        self.trained = True
+                
             
     def train_sequential(self, parallel = False, verbose = False) -> None:
         """
@@ -353,7 +357,8 @@ class Engine():
             TotalSigma2 = {}
             
         # ---------- Initial self.MetaModel ----------
-        self.train_normal(parallel = parallel, verbose=verbose)
+        if not self.trained:
+            self.train_normal(parallel = parallel, verbose=verbose)
         
         initMetaModel = deepcopy(self.MetaModel)
 
@@ -745,11 +750,14 @@ class Engine():
         logPriorLikelihoods = np.zeros((mc_size))
        # print(y_hat)
        # print(list[y_hat])
+        #print(std)
         for key in list(y_hat):
+            #print(std[key])
             cov = np.diag(std[key]**2)
-           # print(y_hat[key], cov)
+            #print(y_hat[key], cov)
+            print(key, y_hat[key], std[key])
             # TODO: added the allow_singular = True here
-            rv = stats.multivariate_normal(mean=y_hat[key], cov=cov,)
+            rv = stats.multivariate_normal(mean=y_hat[key], cov=cov,allow_singular = True)
             Y_MC[key] = rv.rvs(size=mc_size)
             logPriorLikelihoods += rv.logpdf(Y_MC[key])
             std_MC[key] = np.zeros((mc_size, y_hat[key].shape[0]))
diff --git a/src/bayesvalidrox/surrogate_models/exp_designs.py b/src/bayesvalidrox/surrogate_models/exp_designs.py
index fa03fe17d96fb2c1f19546b7b72fb2fd6dd1c13a..96012162a614c308c18ba09c8c57344e442f9c43 100644
--- a/src/bayesvalidrox/surrogate_models/exp_designs.py
+++ b/src/bayesvalidrox/surrogate_models/exp_designs.py
@@ -371,10 +371,18 @@ class ExpDesigns(InputSpace):
                 # store the raw data with given random indices
                 samples[:, pa_idx] = self.raw_data[pa_idx, rand_idx]
         else:
+            if not hasattr(self, 'JDist'):
+                raise AttributeError('Sampling cannot proceed, build ExpDesign with max_deg != 0 to create JDist!')
             try:
+                # Use resample if JDist is of type gaussian_kde
                 samples = self.JDist.resample(int(n_samples)).T
             except AttributeError:
+                # Use sample if JDist is of type chaospy.J
                 samples = self.JDist.sample(int(n_samples)).T
+            # If there is only one input transform the samples
+            if self.ndim == 1:
+                samples = np.swapaxes(np.atleast_2d(samples),0,1)
+            
             # Check if all samples are in the bound_tuples
             for idx, param_set in enumerate(samples):
                 if not check_ranges(param_set, self.bound_tuples):
diff --git a/src/bayesvalidrox/surrogate_models/input_space.py b/src/bayesvalidrox/surrogate_models/input_space.py
index 4e010d66f2933ec243bad756d8f2c5454808d802..d722e387a8106a07472f8612aaaa7f2fe8daea84 100644
--- a/src/bayesvalidrox/surrogate_models/input_space.py
+++ b/src/bayesvalidrox/surrogate_models/input_space.py
@@ -100,8 +100,6 @@ class InputSpace:
                 up_bound = np.max(Inputs.Marginals[i].input_data)
                 Inputs.Marginals[i].parameters = [low_bound, up_bound]
 
-  
-
     # -------------------------------------------------------------------------
     def init_param_space(self, max_deg=None):
         """
@@ -386,6 +384,8 @@ class InputSpace:
                     if params == None:
                         raise AttributeError('Additional parameters have to be set for the gamma distribution!')
                     params_Y = [1, params[1]]
+                    
+                    # TOOD: update the call to the gamma function, seems like source code has been changed!
                     dist_Y = st.gamma(loc=params_Y[0], scale=params_Y[1])
                     inv_cdf = np.vectorize(lambda x: dist_Y.ppf(x))
 
diff --git a/src/bayesvalidrox/surrogate_models/meta_model_engine.py b/src/bayesvalidrox/surrogate_models/meta_model_engine.py
new file mode 100644
index 0000000000000000000000000000000000000000..71c0244216b0c87a22174a3ad2043a4c0a80efab
--- /dev/null
+++ b/src/bayesvalidrox/surrogate_models/meta_model_engine.py
@@ -0,0 +1,2195 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jan 28 09:21:18 2022
+
+@author: farid
+"""
+import numpy as np
+from scipy import stats, signal, linalg, sparse
+from scipy.spatial import distance
+from copy import deepcopy, copy
+from tqdm import tqdm
+import scipy.optimize as opt
+from sklearn.metrics import mean_squared_error
+import multiprocessing
+import matplotlib.pyplot as plt
+import sys
+import os
+import gc
+import seaborn as sns
+from joblib import Parallel, delayed
+
+import bayesvalidrox
+from .exploration import Exploration
+from bayesvalidrox.bayes_inference.bayes_inference import BayesInference
+from bayesvalidrox.bayes_inference.discrepancy import Discrepancy
+import pandas as pd
+
+
+class MetaModelEngine():
+    """ Sequential experimental design
+    This class provieds method for trainig the meta-model in an iterative
+    manners.
+    The main method to execute the task is `train_seq_design`, which
+      recieves a model object and returns the trained metamodel.
+    """
+
+    def __init__(self, meta_model_opts):
+        self.MetaModel = meta_model_opts
+
+    # -------------------------------------------------------------------------
+    def run(self):
+
+        Model = self.MetaModel.ModelObj
+        self.MetaModel.n_params = len(self.MetaModel.input_obj.Marginals)
+        self.MetaModel.ExpDesignFlag = 'normal'
+        # --- Prepare pce degree ---
+        if self.MetaModel.meta_model_type.lower() == 'pce':
+            if type(self.MetaModel.pce_deg) is not np.ndarray:
+                self.MetaModel.pce_deg = np.array(self.MetaModel.pce_deg)
+
+        if self.MetaModel.ExpDesign.method == 'normal':
+            self.MetaModel.ExpDesignFlag = 'normal'
+            self.MetaModel.train_norm_design(parallel = False)
+
+        elif self.MetaModel.ExpDesign.method == 'sequential':
+            self.train_seq_design()
+        else:
+            raise Exception("The method for experimental design you requested"
+                            " has not been implemented yet.")
+
+        # Zip the model run directories
+        if self.MetaModel.ModelObj.link_type.lower() == 'pylink' and\
+           self.MetaModel.ExpDesign.sampling_method.lower() != 'user':
+            Model.zip_subdirs(Model.name, f'{Model.name}_')
+
+    # -------------------------------------------------------------------------
+    def train_seq_design(self):
+        """
+        Starts the adaptive sequential design for refining the surrogate model
+        by selecting training points in a sequential manner.
+
+        Returns
+        -------
+        MetaModel : object
+            Meta model object.
+
+        """
+        # Set model to have shorter call 
+        Model = self.MetaModel.ModelObj
+        # MetaModel = self.MetaModel
+        self.Model = Model
+
+        # Initialization
+        self.MetaModel.SeqModifiedLOO = {}
+        self.MetaModel.seqValidError = {}
+        self.MetaModel.SeqBME = {}
+        self.MetaModel.SeqKLD = {}
+        self.MetaModel.SeqDistHellinger = {}
+        self.MetaModel.seqRMSEMean = {}
+        self.MetaModel.seqRMSEStd = {}
+        self.MetaModel.seqMinDist = []
+
+        # Determine the metamodel type
+        if self.MetaModel.meta_model_type.lower() != 'gpe':
+            pce = True
+        else:
+            pce = False
+        # If given, use mc reference data
+        mc_ref = True if bool(Model.mc_reference) else False
+        if mc_ref:
+            Model.read_mc_reference()
+
+        # if valid_samples not defined, do so now
+        if not hasattr(self.MetaModel, 'valid_samples'):
+            self.MetaModel.valid_samples = []
+            self.MetaModel.valid_model_runs = []
+            self.MetaModel.valid_likelihoods = []
+
+        # Get the parameters
+        max_n_samples = self.MetaModel.ExpDesign.n_max_samples
+        mod_LOO_threshold = self.MetaModel.ExpDesign.mod_LOO_threshold
+        n_canddidate = self.MetaModel.ExpDesign.n_canddidate
+        post_snapshot = self.MetaModel.ExpDesign.post_snapshot
+        n_replication = self.MetaModel.ExpDesign.n_replication
+        util_func = self.MetaModel.ExpDesign.util_func
+        output_name = Model.Output.names
+        validError = None
+        # Handle if only one UtilityFunctions is provided
+        if not isinstance(util_func, list):
+            util_func = [self.MetaModel.ExpDesign.util_func]
+
+        # Read observations or MCReference
+        if len(Model.observations) != 0 or Model.meas_file is not None:
+            self.observations = Model.read_observation()
+            obs_data = self.observations
+        else:
+            obs_data = []
+            TotalSigma2 = {}
+            
+        # TODO: ---------- Initial self.MetaModel ----------
+        # First run MetaModel on non-sequential design
+        self.MetaModel.train_norm_design(parallel = False)
+        initMetaModel = deepcopy(self.MetaModel)
+
+        # Validation error if validation set is provided. - use as initial errors
+        if self.MetaModel.valid_model_runs:
+            init_rmse, init_valid_error = self.__validError(initMetaModel)
+            init_valid_error = list(init_valid_error.values())
+        else:
+            init_rmse = None
+
+        # Check if discrepancy is provided
+        if len(obs_data) != 0 and hasattr(self.MetaModel, 'Discrepancy'):
+            TotalSigma2 = self.MetaModel.Discrepancy.parameters
+
+            # Calculate the initial BME
+            out = self.__BME_Calculator(
+                initMetaModel, obs_data, TotalSigma2, init_rmse)
+            init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out
+            print(f"\nInitial BME: {init_BME:.2f}")
+            print(f"Initial KLD: {init_KLD:.2f}")
+
+            # Posterior snapshot (initial)
+            if post_snapshot:
+                parNames = self.MetaModel.ExpDesign.par_names
+                print('Posterior snapshot (initial) is being plotted...')
+                self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init')
+
+        # Check the convergence of the Mean & Std
+        if mc_ref and pce:
+            init_rmse_mean, init_rmse_std = self.__error_Mean_Std()
+            print(f"Initial Mean and Std error: {init_rmse_mean:.2f},"
+                  f" {init_rmse_std:.2f}")
+
+        # Read the initial experimental design
+        # TODO: this sequential, or the non-sequential samples??
+        Xinit = initMetaModel.ExpDesign.X
+        init_n_samples = len(initMetaModel.ExpDesign.X)
+        initYprev = initMetaModel.ModelOutputDict
+        initLCerror = initMetaModel.LCerror
+        n_itrs = max_n_samples - init_n_samples
+
+        # Read the initial ModifiedLOO
+        if pce:
+            Scores_all, varExpDesignY = [], []
+            for out_name in output_name:
+                y = self.MetaModel.ExpDesign.Y[out_name]
+                Scores_all.append(list(
+                    self.MetaModel.score_dict['b_1'][out_name].values()))
+                if self.MetaModel.dim_red_method.lower() == 'pca':
+                    pca = self.MetaModel.pca['b_1'][out_name]
+                    components = pca.transform(y)
+                    varExpDesignY.append(np.var(components, axis=0))
+                else:
+                    varExpDesignY.append(np.var(y, axis=0))
+
+            Scores = [item for sublist in Scores_all for item in sublist]
+            weights = [item for sublist in varExpDesignY for item in sublist]
+            init_mod_LOO = [np.average([1-score for score in Scores],
+                                       weights=weights)]
+
+        prevMetaModel_dict = {}
+        # Replicate the sequential design
+        for repIdx in range(n_replication):                     # TODO: what does this do?
+            print(f'\n>>>> Replication: {repIdx+1}<<<<')
+
+            # To avoid changes ub original aPCE object
+            self.MetaModel.ExpDesign.X = Xinit
+            self.MetaModel.ExpDesign.Y = initYprev
+            self.MetaModel.LCerror = initLCerror
+
+            for util_f in util_func:                            # TODO: recheck choices for this
+                print(f'\n>>>> Utility Function: {util_f} <<<<')
+                # To avoid changes ub original aPCE object
+                self.MetaModel.ExpDesign.X = Xinit
+                self.MetaModel.ExpDesign.Y = initYprev
+                self.MetaModel.LCerror = initLCerror
+
+                # Set the experimental design
+                Xprev = Xinit
+                total_n_samples = init_n_samples
+                Yprev = initYprev
+
+                Xfull = []
+                Yfull = []
+
+                # Store the initial ModifiedLOO
+                if pce:
+                    print("\nInitial ModifiedLOO:", init_mod_LOO)
+                    SeqModifiedLOO = np.array(init_mod_LOO)
+
+                if len(self.MetaModel.valid_model_runs) != 0:
+                    SeqValidError = np.array(init_valid_error)
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    SeqBME = np.array([init_BME])
+                    SeqKLD = np.array([init_KLD])
+                    SeqDistHellinger = np.array([init_dist_hellinger])
+
+                if mc_ref and pce:
+                    seqRMSEMean = np.array([init_rmse_mean])
+                    seqRMSEStd = np.array([init_rmse_std])
+
+                # ------- Start Sequential Experimental Design -------
+                postcnt = 1
+                for itr_no in range(1, n_itrs+1):
+                    print(f'\n>>>> Iteration number {itr_no} <<<<')
+
+                    # Save the metamodel prediction before updating
+                    prevMetaModel_dict[itr_no] = deepcopy(self.MetaModel)           # Write last MetaModel here
+                    if itr_no > 1:
+                        pc_model = prevMetaModel_dict[itr_no-1]                     
+                        self._y_hat_prev, _ = pc_model.eval_metamodel(              # What's the use of this here??
+                            samples=Xfull[-1].reshape(1, -1))
+                        del prevMetaModel_dict[itr_no-1]                            # Delete second to last metamodel here?
+
+                    # Optimal Bayesian Design
+                    self.MetaModel.ExpDesignFlag = 'sequential'
+                    Xnew, updatedPrior = self.opt_SeqDesign(TotalSigma2,            # TODO: check in this!!
+                                                            n_canddidate,
+                                                            util_f)
+                    S = np.min(distance.cdist(Xinit, Xnew, 'euclidean'))
+                    self.MetaModel.seqMinDist.append(S)
+                    print(f"\nmin Dist from OldExpDesign: {S:2f}")
+                    print("\n")
+
+                    # Evaluate the full model response at the new sample
+                    Ynew, _ = Model.run_model_parallel(
+                        Xnew, prevRun_No=total_n_samples
+                        )
+                    total_n_samples += Xnew.shape[0]
+
+                    # ------ Plot the surrogate model vs Origninal Model ------
+                    if hasattr(self.MetaModel, 'adapt_verbose') and \
+                       self.MetaModel.adapt_verbose:
+                        from .adaptPlot import adaptPlot
+                        y_hat, std_hat = self.MetaModel.eval_metamodel(
+                            samples=Xnew
+                            )
+                        adaptPlot(
+                            self.MetaModel, Ynew, y_hat, std_hat,
+                            plotED=False
+                            )
+
+                    # -------- Retrain the surrogate model -------
+                    # Extend new experimental design
+                    Xfull = np.vstack((Xprev, Xnew))
+
+                    # Updating experimental design Y
+                    for out_name in output_name:
+                        Yfull = np.vstack((Yprev[out_name], Ynew[out_name]))
+                        self.MetaModel.ModelOutputDict[out_name] = Yfull
+
+                    # Pass new design to the metamodel object
+                    self.MetaModel.ExpDesign.sampling_method = 'user'
+                    self.MetaModel.ExpDesign.X = Xfull
+                    self.MetaModel.ExpDesign.Y = self.MetaModel.ModelOutputDict
+
+                    # Save the Experimental Design for next iteration
+                    Xprev = Xfull
+                    Yprev = self.MetaModel.ModelOutputDict
+
+                    # Pass the new prior as the input
+                    self.MetaModel.input_obj.poly_coeffs_flag = False
+                    if updatedPrior is not None:
+                        self.MetaModel.input_obj.poly_coeffs_flag = True
+                        print("updatedPrior:", updatedPrior.shape)
+                        # Arbitrary polynomial chaos
+                        for i in range(updatedPrior.shape[1]):
+                            self.MetaModel.input_obj.Marginals[i].dist_type = None
+                            x = updatedPrior[:, i]
+                            self.MetaModel.input_obj.Marginals[i].raw_data = x
+
+                    # Train the surrogate model for new ExpDesign
+                    self.MetaModel.train_norm_design(parallel=False)
+
+                    # -------- Evaluate the retrained surrogate model -------
+                    # Extract Modified LOO from Output
+                    if pce:
+                        Scores_all, varExpDesignY = [], []
+                        for out_name in output_name:
+                            y = self.MetaModel.ExpDesign.Y[out_name]
+                            Scores_all.append(list(
+                                self.MetaModel.score_dict['b_1'][out_name].values()))
+                            if self.MetaModel.dim_red_method.lower() == 'pca':
+                                pca = self.MetaModel.pca['b_1'][out_name]
+                                components = pca.transform(y)
+                                varExpDesignY.append(np.var(components,
+                                                            axis=0))
+                            else:
+                                varExpDesignY.append(np.var(y, axis=0))
+                        Scores = [item for sublist in Scores_all for item
+                                  in sublist]
+                        weights = [item for sublist in varExpDesignY for item
+                                   in sublist]
+                        ModifiedLOO = [np.average(
+                            [1-score for score in Scores], weights=weights)]
+
+                        print('\n')
+                        print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO)
+                        print('\n')
+
+                    # Compute the validation error
+                    if self.MetaModel.valid_model_runs:
+                        rmse, validError = self.__validError(self.MetaModel)
+                        ValidError = list(validError.values())
+                    else:
+                        rmse = None
+
+                    # Store updated ModifiedLOO
+                    if pce:
+                        SeqModifiedLOO = np.vstack(
+                            (SeqModifiedLOO, ModifiedLOO))
+                        if len(self.MetaModel.valid_model_runs) != 0:
+                            SeqValidError = np.vstack(
+                                (SeqValidError, ValidError))
+                    # -------- Caclulation of BME as accuracy metric -------
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        # Calculate the initial BME
+                        out = self.__BME_Calculator(self.MetaModel, obs_data,
+                                                    TotalSigma2, rmse)
+                        BME, KLD, Posterior, likes, DistHellinger = out
+                        print('\n')
+                        print(f"Updated BME: {BME:.2f}")
+                        print(f"Updated KLD: {KLD:.2f}")
+                        print('\n')
+
+                        # Plot some snapshots of the posterior
+                        step_snapshot = self.MetaModel.ExpDesign.step_snapshot
+                        if post_snapshot and postcnt % step_snapshot == 0:
+                            parNames = self.MetaModel.ExpDesign.par_names
+                            print('Posterior snapshot is being plotted...')
+                            self.__posteriorPlot(Posterior, parNames,
+                                                 f'SeqPosterior_{postcnt}')
+                        postcnt += 1
+
+                    # Check the convergence of the Mean&Std
+                    if mc_ref and pce:
+                        print('\n')
+                        RMSE_Mean, RMSE_std = self.__error_Mean_Std()
+                        print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, "
+                              f"{RMSE_std:.2f}")
+                        print('\n')
+
+                    # Store the updated BME & KLD
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        SeqBME = np.vstack((SeqBME, BME))
+                        SeqKLD = np.vstack((SeqKLD, KLD))
+                        SeqDistHellinger = np.vstack((SeqDistHellinger,
+                                                      DistHellinger))
+                    if mc_ref and pce:
+                        seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean))
+                        seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std))
+
+                    if pce and any(LOO < mod_LOO_threshold
+                                   for LOO in ModifiedLOO):
+                        break
+
+                    # Clean up
+                    if len(obs_data) != 0:
+                        del out
+                    print()
+                    print('-'*50)
+                    print()
+
+                # Store updated ModifiedLOO and BME in dictonary
+                strKey = f'{util_f}_rep_{repIdx+1}'
+                if pce:
+                    self.MetaModel.SeqModifiedLOO[strKey] = SeqModifiedLOO
+                if len(self.MetaModel.valid_model_runs) != 0:
+                    self.MetaModel.seqValidError[strKey] = SeqValidError
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    self.MetaModel.SeqBME[strKey] = SeqBME
+                    self.MetaModel.SeqKLD[strKey] = SeqKLD
+                if hasattr(self.MetaModel, 'valid_likelihoods') and \
+                   self.MetaModel.valid_likelihoods:
+                    self.MetaModel.SeqDistHellinger[strKey] = SeqDistHellinger
+                if mc_ref and pce:
+                    self.MetaModel.seqRMSEMean[strKey] = seqRMSEMean
+                    self.MetaModel.seqRMSEStd[strKey] = seqRMSEStd
+
+        # return self.MetaModel
+
+    # -------------------------------------------------------------------------
+    def util_VarBasedDesign(self, X_can, index, util_func='Entropy'):
+        """
+        Computes the exploitation scores based on:
+        active learning MacKay(ALM) and active learning Cohn (ALC)
+        Paper: Sequential Design with Mutual Information for Computer
+        Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas
+        (2016)
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        index : int
+            Model output index.
+        UtilMethod : string, optional
+            Exploitation utility function. The default is 'Entropy'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+        MetaModel = self.MetaModel
+        ED_X = MetaModel.ExpDesign.X
+        out_dict_y = MetaModel.ExpDesign.Y
+        out_names = MetaModel.ModelObj.Output.names
+
+        # Run the Metamodel for the candidate
+        X_can = X_can.reshape(1, -1)
+        Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can)
+
+        if util_func.lower() == 'alm':
+            # ----- Entropy/MMSE/active learning MacKay(ALM)  -----
+            # Compute perdiction variance of the old model
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            varPCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                varPCE[KeyIdx] = np.max(canPredVar[key], axis=1)
+            score = np.max(varPCE, axis=0)
+
+        elif util_func.lower() == 'eigf':
+            # ----- Expected Improvement for Global fit -----
+            # Find closest EDX to the candidate
+            distances = distance.cdist(ED_X, X_can, 'euclidean')
+            index = np.argmin(distances)
+
+            # Compute perdiction error and variance of the old model
+            predError = {key: Y_PC_can[key] for key in out_names}
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            # Compute perdiction error and variance of the old model
+            # Eq (5) from Liu et al.(2018)
+            EIGF_PCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                residual = predError[key] - out_dict_y[key][int(index)]
+                var = canPredVar[key]
+                EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1)
+            score = np.max(EIGF_PCE, axis=0)
+
+        return -1 * score   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianActiveDesign(self, y_hat, std, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian active design criterion (var).
+
+        It is based on the following paper:
+        Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak.
+        "Bayesian3 active learning for the gaussian process emulator using
+        information theory." Entropy 22, no. 8 (2020): 890.
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            BAL design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # Get the data
+        obs_data = self.observations
+        n_obs = self.Model.n_obs
+        mc_size = 10000
+
+        # Sample a distribution for a normal dist
+        # with Y_mean_can as the mean and Y_std_can as std.
+        Y_MC, std_MC = {}, {}
+        logPriorLikelihoods = np.zeros((mc_size))
+        for key in list(y_hat):
+            cov = np.diag(std[key]**2)
+            rv = stats.multivariate_normal(mean=y_hat[key], cov=cov)
+            Y_MC[key] = rv.rvs(size=mc_size)
+            logPriorLikelihoods += rv.logpdf(Y_MC[key])
+            std_MC[key] = np.zeros((mc_size, y_hat[key].shape[0]))
+
+        #  Likelihood computation (Comparison of data and simulation
+        #  results via PCE with candidate design)
+        likelihoods = self.__normpdf(Y_MC, std_MC, obs_data, sigma2Dict)
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, mc_size)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(likelihoods), dtype=np.longdouble)
+
+        # Posterior-based expectation of likelihoods
+        postLikelihoods = likelihoods[accepted]
+        postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+        # Utility function Eq.2 in Ref. (2)
+        # Posterior covariance matrix after observing data y
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # TODO: Calculate the correction factor for BME
+            # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can,
+            #                                      ObservationData, sigma2Dict)
+            # BME += BMECorrFactor
+            # Haun et al implementation
+            # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+            U_J_d = postExpLikelihoods - logBME
+
+        # Marginal log likelihood
+        elif var == 'BME':
+            U_J_d = np.nanmean(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            logBME = np.log(np.nanmean(likelihoods))
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # Bayesian information criterion
+        elif var == 'BIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxL = np.nanmax(likelihoods)
+            U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams
+
+        # Akaike information criterion
+        elif var == 'AIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxlogL = np.log(np.nanmax(likelihoods))
+            AIC = -2 * maxlogL + 2 * nModelParams
+            # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1)
+            penTerm = 0
+            U_J_d = 1*(AIC + penTerm)
+
+        # Deviance information criterion
+        elif var == 'DIC':
+            # D_theta_bar = np.mean(-2 * Likelihoods)
+            N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0]))
+            Likelihoods_theta_mean = self.__normpdf(
+                y_hat, std, obs_data, sigma2Dict
+                )
+            DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p
+
+            U_J_d = DIC
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Handle inf and NaN (replace by zero)
+        if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf:
+            U_J_d = 0.0
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def update_metamodel(self, MetaModel, output, y_hat_can, univ_p_val, index,
+                         new_pca=False):
+        BasisIndices = MetaModel.basis_dict[output]["y_"+str(index+1)]
+        clf_poly = MetaModel.clf_poly[output]["y_"+str(index+1)]
+        Mn = clf_poly.coef_
+        Sn = clf_poly.sigma_
+        beta = clf_poly.alpha_
+        active = clf_poly.active_
+        Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val)
+
+        Sn_new_inv = np.linalg.inv(Sn)
+        Sn_new_inv += beta * np.dot(Psi[:, active].T, Psi[:, active])
+        Sn_new = np.linalg.inv(Sn_new_inv)
+
+        Mn_new = np.dot(Sn_new_inv, Mn[active]).reshape(-1, 1)
+        Mn_new += beta * np.dot(Psi[:, active].T, y_hat_can)
+        Mn_new = np.dot(Sn_new, Mn_new).flatten()
+
+        # Compute the old and new moments of PCEs
+        mean_old = Mn[0]
+        mean_new = Mn_new[0]
+        std_old = np.sqrt(np.sum(np.square(Mn[1:])))
+        std_new = np.sqrt(np.sum(np.square(Mn_new[1:])))
+
+        # Back transformation if PCA is selected.
+        if MetaModel.dim_red_method.lower() == 'pca':
+            old_pca = MetaModel.pca[output]
+            mean_old = old_pca.mean_[index]
+            mean_old += np.sum(mean_old * old_pca.components_[:, index])
+            std_old = np.sqrt(np.sum(std_old**2 *
+                                     old_pca.components_[:, index]**2))
+            mean_new = new_pca.mean_[index]
+            mean_new += np.sum(mean_new * new_pca.components_[:, index])
+            std_new = np.sqrt(np.sum(std_new**2 *
+                                     new_pca.components_[:, index]**2))
+            # print(f"mean_old: {mean_old:.2f} mean_new: {mean_new:.2f}")
+            # print(f"std_old: {std_old:.2f} std_new: {std_new:.2f}")
+        # Store the old and new moments of PCEs
+        results = {
+            'mean_old': mean_old,
+            'mean_new': mean_new,
+            'std_old': std_old,
+            'std_new': std_new
+            }
+        return results
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign_old(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        Model = self.Model
+        MetaModel = deepcopy(self.MetaModel)
+        old_EDY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels using the candidate design
+        Y_PC_can, Y_std_can = self.MetaModel.eval_metamodel(
+            samples=np.array([X_can])
+            )
+
+        # Generate y from posterior predictive
+        m_size = 100
+        y_hat_samples = {}
+        for idx, key in enumerate(Model.Output.names):
+            means, stds = Y_PC_can[key][0], Y_std_can[key][0]
+            y_hat_samples[key] = np.random.multivariate_normal(
+                means, np.diag(stds), m_size)
+
+        # Create the SparseBayes-based PCE metamodel:
+        MetaModel.input_obj.poly_coeffs_flag = False
+        univ_p_val = self.MetaModel.univ_basis_vals(X_can)
+        G_n_m_all = np.zeros((m_size, len(Model.Output.names), Model.n_obs))
+
+        for i in range(m_size):
+            for idx, key in enumerate(Model.Output.names):
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    # Equal number of components
+                    new_outputs = np.vstack(
+                        (old_EDY[key], y_hat_samples[key][i])
+                        )
+                    new_pca, _ = MetaModel.pca_transformation(new_outputs)
+                    target = new_pca.transform(
+                        y_hat_samples[key][i].reshape(1, -1)
+                        )[0]
+                else:
+                    new_pca, target = False, y_hat_samples[key][i]
+
+                for j in range(len(target)):
+
+                    # Update surrogate
+                    result = self.update_metamodel(
+                        MetaModel, key, target[j], univ_p_val, j, new_pca)
+
+                    # Compute Expected Information Gain (Eq. 39)
+                    G_n_m = np.log(result['std_old']/result['std_new']) - 1./2
+                    G_n_m += result['std_new']**2 / (2*result['std_old']**2)
+                    G_n_m += (result['mean_new'] - result['mean_old'])**2 /\
+                        (2*result['std_old']**2)
+
+                    G_n_m_all[i, idx, j] = G_n_m
+
+        U_J_d = G_n_m_all.mean(axis=(1, 2)).mean()
+        return -1 * U_J_d
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        MetaModel = self.MetaModel
+        out_names = MetaModel.ModelObj.Output.names
+        if X_can.ndim == 1:
+            X_can = X_can.reshape(1, -1)
+
+        # Compute the mean and std based on the MetaModel
+        # pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+        if var == 'ALC':
+            Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+        oldExpDesignY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can)
+        PCE_Model_can = deepcopy(MetaModel)
+        # Add the candidate to the ExpDesign
+        NewExpDesignX = np.vstack((oldExpDesignX, X_can))
+
+        NewExpDesignY = {}
+        for key in oldExpDesignY.keys():
+            NewExpDesignY[key] = np.vstack(
+                (oldExpDesignY[key], Y_PC_can[key])
+                )
+
+        PCE_Model_can.ExpDesign.sampling_method = 'user'
+        PCE_Model_can.ExpDesign.X = NewExpDesignX
+        PCE_Model_can.ModelOutputDict = NewExpDesignY
+        PCE_Model_can.ExpDesign.Y = NewExpDesignY
+
+        # Train the model for the observed data using x_can
+        PCE_Model_can.input_obj.poly_coeffs_flag = False
+        PCE_Model_can.train_norm_design(parallel=False)
+
+        # Set the ExpDesign to its original values
+        PCE_Model_can.ExpDesign.X = oldExpDesignX
+        PCE_Model_can.ModelOutputDict = oldExpDesignY
+        PCE_Model_can.ExpDesign.Y = oldExpDesignY
+
+        if var.lower() == 'mi':
+            # Mutual information based on Krause et al
+            # Adapted from Beck & Guillas (MICE) paper
+            _, std_PC_can = PCE_Model_can.eval_metamodel(samples=X_can)
+            std_can = {key: std_PC_can[key] for key in out_names}
+
+            std_old = {key: Y_std_can[key] for key in out_names}
+
+            varPCE = np.zeros((len(out_names)))
+            for i, key in enumerate(out_names):
+                varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2)
+            score = np.mean(varPCE)
+
+            return -1 * score
+
+        elif var.lower() == 'alc':
+            # Active learning based on Gramyc and Lee
+            # Adaptive design and analysis of supercomputer experiments Techno-
+            # metrics, 51 (2009), pp. 130–145.
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC_can, Y_MC_std_can = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Compute the score
+            score = []
+            for i, key in enumerate(out_names):
+                pce_var = Y_MC_std_can[key]**2
+                pce_var_can = Y_MC_std[key]**2
+                score.append(np.mean(pce_var-pce_var_can, axis=0))
+            score = np.mean(score)
+
+            return -1 * score
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = X_MC.shape[0]
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Enriching Monte Carlo samples if need be
+            if ESS != 0:
+                X_MC = self.MetaModel.ExpDesign.generate_samples(
+                    MCsize, 'random'
+                    )
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and simulation
+            # results via PCE with candidate design)
+            likelihoods = self.__normpdf(
+                Y_MC, std_MC, self.observations, sigma2Dict
+                )
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                print("--- increasing MC size---")
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # -------------------- Utility functions --------------------
+        # Utility function Eq.2 in Ref. (2)
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods, dtype=np.longdouble))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Haun et al implementation
+            U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME)
+
+            # U_J_d = np.sum(G_n_m_all)
+            # Ryan et al (2014) implementation
+            # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0])
+            # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME
+
+            # U_J_d = postExpLikelihoods - logBME
+
+        # Marginal likelihood
+        elif var == 'BME':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+            U_J_d = logBME
+
+        # Bayes risk likelihood
+        elif var == 'BayesRisk':
+
+            U_J_d = -1 * np.var(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postLikelihoods /= np.nansum(likelihoods[accepted])
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Posterior-based expectation of prior densities
+            postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # D-Posterior-precision
+        elif var == 'DPP':
+            X_Posterior = X_MC[accepted]
+            # covariance of the posterior parameters
+            U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior)))
+
+        # A-Posterior-precision
+        elif var == 'APP':
+            X_Posterior = X_MC[accepted]
+            # trace of the posterior parameters
+            U_J_d = -np.log(np.trace(np.cov(X_Posterior)))
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def subdomain(self, Bounds, n_new_samples):
+        """
+        Divides a domain defined by Bounds into sub domains.
+
+        Parameters
+        ----------
+        Bounds : list of tuples
+            List of lower and upper bounds.
+        n_new_samples : TYPE
+            DESCRIPTION.
+
+        Returns
+        -------
+        Subdomains : TYPE
+            DESCRIPTION.
+
+        """
+        n_params = self.MetaModel.n_params
+        n_subdomains = n_new_samples + 1
+        LinSpace = np.zeros((n_params, n_subdomains))
+
+        for i in range(n_params):
+            LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1],
+                                      num=n_subdomains)
+        Subdomains = []
+        for k in range(n_subdomains-1):
+            mylist = []
+            for i in range(n_params):
+                mylist.append((LinSpace[i, k+0], LinSpace[i, k+1]))
+            Subdomains.append(tuple(mylist))
+
+        return Subdomains
+
+    # -------------------------------------------------------------------------
+    def run_util_func(self, method, candidates, index, sigma2Dict=None,
+                      var=None, X_MC=None):
+        """
+        Runs the utility function based on the given method.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        candidates : array of shape (n_samples, n_params)
+            All candidate parameter sets.
+        index : int
+            ExpDesign index.
+        sigma2Dict : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        var : string, optional
+            Utility function. The default is None.
+        X_MC : TYPE, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        index : TYPE
+            DESCRIPTION.
+        List
+            Scores.
+
+        """
+
+        if method.lower() == 'varoptdesign':
+            # U_J_d = self.util_VarBasedDesign(candidates, index, var)
+            U_J_d = np.zeros((candidates.shape[0]))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="varoptdesign"):
+                U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var)
+
+        elif method.lower() == 'bayesactdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            # Evaluate all candidates
+            y_can, std_can = self.MetaModel.eval_metamodel(samples=candidates)
+            # loop through candidates
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="BAL Design"):
+                y_hat = {key: items[idx] for key, items in y_can.items()}
+                std = {key: items[idx] for key, items in std_can.items()}
+                U_J_d[idx] = self.util_BayesianActiveDesign(
+                    y_hat, std, sigma2Dict, var)
+
+        elif method.lower() == 'bayesoptdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict,
+                                                      var)
+        return (index, -1 * U_J_d)
+
+    # -------------------------------------------------------------------------
+    def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No,
+                       verbose=False):
+        """
+        Exploration algorithim to find the optimum parameter space.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        Bounds : list of tuples
+            List of lower and upper boundaries of parameters.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        Run_No : int
+            Run number.
+        verbose : bool, optional
+            Print out a summary. The default is False.
+
+        Returns
+        -------
+        Run_No : int
+            Run number.
+        array
+            Optimial candidate.
+
+        """
+
+        Model = self.Model
+        max_func_itr = self.MetaModel.ExpDesign.max_func_itr
+
+        if method == 'VarOptDesign':
+            Res_Global = opt.dual_annealing(self.util_VarBasedDesign,
+                                            bounds=Bounds,
+                                            args=(Model, var),
+                                            maxfun=max_func_itr)
+
+        elif method == 'BayesOptDesign':
+            Res_Global = opt.dual_annealing(self.util_BayesianDesign,
+                                            bounds=Bounds,
+                                            args=(Model, sigma2Dict, var),
+                                            maxfun=max_func_itr)
+
+        if verbose:
+            print(f"global minimum: xmin = {Res_Global.x}, "
+                  f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}")
+
+        return (Run_No, Res_Global.x)
+
+    # -------------------------------------------------------------------------
+    def tradoff_weights(self, tradeoff_scheme, old_EDX, old_EDY):
+        """
+        Calculates weights for exploration scores based on the requested
+        scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`.
+
+        `None`: No exploration.
+        `equal`: Same weights for exploration and exploitation scores.
+        `epsilon-decreasing`: Start with more exploration and increase the
+            influence of exploitation along the way with a exponential decay
+            function
+        `adaptive`: An adaptive method based on:
+            Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling
+            approach for Kriging metamodeling by maximizing expected prediction
+            error." Computers & Chemical Engineering 106 (2017): 171-182.
+
+        Parameters
+        ----------
+        tradeoff_scheme : string
+            Trade-off scheme for exloration and exploitation scores.
+        old_EDX : array (n_samples, n_params)
+            Old experimental design (training points).
+        old_EDY : dict
+            Old model responses (targets).
+
+        Returns
+        -------
+        exploration_weight : float
+            Exploration weight.
+        exploitation_weight: float
+            Exploitation weight.
+
+        """
+        if tradeoff_scheme is None:
+            exploration_weight = 0
+
+        elif tradeoff_scheme == 'equal':
+            exploration_weight = 0.5
+
+        elif tradeoff_scheme == 'epsilon-decreasing':
+            # epsilon-decreasing scheme
+            # Start with more exploration and increase the influence of
+            # exploitation along the way with a exponential decay function
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8)
+            exploration_weight = signal.exponential(n_max_samples-initNSamples,
+                                                    0, tau2, False)[itrNumber]
+
+        elif tradeoff_scheme == 'adaptive':
+
+            # Extract itrNumber
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            if itrNumber == 0:
+                exploration_weight = 0.5
+            else:
+                # New adaptive trade-off according to Liu et al. (2017)
+                # Mean squared error for last design point
+                last_EDX = old_EDX[-1].reshape(1, -1)
+                lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX)
+                pce_y = np.array(list(lastPCEY.values()))[:, 0]
+                y = np.array(list(old_EDY.values()))[:, -1, :]
+                mseError = mean_squared_error(pce_y, y)
+
+                # Mean squared CV - error for last design point
+                pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0]
+                mseCVError = mean_squared_error(pce_y_prev, y)
+
+                exploration_weight = min([0.5*mseError/mseCVError, 1])
+
+        # Exploitation weight
+        exploitation_weight = 1 - exploration_weight
+
+        return exploration_weight, exploitation_weight
+
+    # -------------------------------------------------------------------------
+    def opt_SeqDesign(self, sigma2, n_candidates=5, var='DKL'):
+        """
+        Runs optimal sequential design.
+
+        Parameters
+        ----------
+        sigma2 : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        n_candidates : int, optional
+            Number of candidate samples. The default is 5.
+        var : string, optional
+            Utility function. The default is None.
+
+        Raises
+        ------
+        NameError
+            Wrong utility function.
+
+        Returns
+        -------
+        Xnew : array (n_samples, n_params)
+            Selected new training point(s).
+        """
+
+        # Initialization
+        MetaModel = self.MetaModel
+        Bounds = MetaModel.bound_tuples
+        n_new_samples = MetaModel.ExpDesign.n_new_samples
+        explore_method = MetaModel.ExpDesign.explore_method
+        exploit_method = MetaModel.ExpDesign.exploit_method
+        n_cand_groups = MetaModel.ExpDesign.n_cand_groups
+        tradeoff_scheme = MetaModel.ExpDesign.tradeoff_scheme
+
+        old_EDX = MetaModel.ExpDesign.X
+        old_EDY = MetaModel.ExpDesign.Y.copy()
+        ndim = MetaModel.ExpDesign.X.shape[1]
+        OutputNames = MetaModel.ModelObj.Output.names
+
+        # -----------------------------------------
+        # ----------- CUSTOMIZED METHODS ----------
+        # -----------------------------------------
+        # Utility function exploit_method provided by user
+        if exploit_method.lower() == 'user':
+
+            Xnew, filteredSamples = MetaModel.ExpDesign.ExploitFunction(self)
+
+            print("\n")
+            print("\nXnew:\n", Xnew)
+
+            return Xnew, filteredSamples
+
+        # -----------------------------------------
+        # ---------- EXPLORATION METHODS ----------
+        # -----------------------------------------
+        if explore_method == 'dual annealing':
+            # ------- EXPLORATION: OPTIMIZATION -------
+            import time
+            start_time = time.time()
+
+            # Divide the domain to subdomains
+            args = []
+            subdomains = self.subdomain(Bounds, n_new_samples)
+            for i in range(n_new_samples):
+                args.append((exploit_method, subdomains[i], sigma2, var, i))
+
+            # Multiprocessing
+            pool = multiprocessing.Pool(multiprocessing.cpu_count())
+
+            # With Pool.starmap_async()
+            results = pool.starmap_async(self.dual_annealing, args).get()
+
+            # Close the pool
+            pool.close()
+
+            Xnew = np.array([results[i][1] for i in range(n_new_samples)])
+
+            print("\nXnew:\n", Xnew)
+
+            elapsed_time = time.time() - start_time
+            print("\n")
+            print(f"elapsed_time: {round(elapsed_time,2)} sec.")
+            print('-'*20)
+
+        elif explore_method == 'LOOCV':
+            # -----------------------------------------------------------------
+            # TODO: LOOCV model construnction based on Feng et al. (2020)
+            # 'LOOCV':
+            # Initilize the ExploitScore array
+
+            # Generate random samples
+            allCandidates = MetaModel.ExpDesign.generate_samples(n_candidates,
+                                                                'random')
+
+            # Construct error model based on LCerror
+            errorModel = MetaModel.create_ModelError(old_EDX, self.LCerror)
+            self.errorModel.append(copy(errorModel))
+
+            # Evaluate the error models for allCandidates
+            eLCAllCands, _ = errorModel.eval_errormodel(allCandidates)
+            # Select the maximum as the representative error
+            eLCAllCands = np.dstack(eLCAllCands.values())
+            eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0]
+
+            # Normalize the error w.r.t the maximum error
+            scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates)
+
+        else:
+            # ------- EXPLORATION: SPACE-FILLING DESIGN -------
+            # Generate candidate samples from Exploration class
+            explore = Exploration(MetaModel, n_candidates)
+            explore.w = 100  # * ndim #500
+            # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+            explore.mc_criterion = 'mc-intersite-proj'
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+
+            # Temp: ---- Plot all candidates -----
+            if ndim == 2:
+                def plotter(points, allCandidates, Method,
+                            scoreExploration=None):
+                    if Method == 'Voronoi':
+                        from scipy.spatial import Voronoi, voronoi_plot_2d
+                        vor = Voronoi(points)
+                        fig = voronoi_plot_2d(vor)
+                        ax1 = fig.axes[0]
+                    else:
+                        fig = plt.figure()
+                        ax1 = fig.add_subplot(111)
+                    ax1.scatter(points[:, 0], points[:, 1], s=10, c='r',
+                                marker="s", label='Old Design Points')
+                    ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10,
+                                c='b', marker="o", label='Design candidates')
+                    for i in range(points.shape[0]):
+                        txt = 'p'+str(i+1)
+                        ax1.annotate(txt, (points[i, 0], points[i, 1]))
+                    if scoreExploration is not None:
+                        for i in range(allCandidates.shape[0]):
+                            txt = str(round(scoreExploration[i], 5))
+                            ax1.annotate(txt, (allCandidates[i, 0],
+                                               allCandidates[i, 1]))
+
+                    plt.xlim(self.bound_tuples[0])
+                    plt.ylim(self.bound_tuples[1])
+                    # plt.show()
+                    plt.legend(loc='upper left')
+
+        # -----------------------------------------
+        # --------- EXPLOITATION METHODS ----------
+        # -----------------------------------------
+        if exploit_method == 'BayesOptDesign' or\
+           exploit_method == 'BayesActDesign':
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\n Exploration weight={explore_w:0.3f} "
+                  f"Exploitation weight={exploit_w:0.3f}\n")
+
+            # ------- EXPLOITATION: BayesOptDesign & ActiveLearning -------
+            if explore_w != 1.0:
+
+                # Create a sample pool for rejection sampling
+                MCsize = 15000
+                X_MC = MetaModel.ExpDesign.generate_samples(MCsize, 'random')
+                candidates = MetaModel.ExpDesign.generate_samples(
+                    MetaModel.ExpDesign.max_func_itr, 'latin_hypercube')
+
+                # Split the candidates in groups for multiprocessing
+                split_cand = np.array_split(
+                    candidates, n_cand_groups, axis=0
+                    )
+
+                results = Parallel(n_jobs=-1, backend='multiprocessing')(
+                        delayed(self.run_util_func)(
+                            exploit_method, split_cand[i], i, sigma2, var, X_MC)
+                        for i in range(n_cand_groups))
+                # out = map(self.run_util_func,
+                #           [exploit_method]*n_cand_groups,
+                #           split_cand,
+                #           range(n_cand_groups),
+                #           [sigma2] * n_cand_groups,
+                #           [var] * n_cand_groups,
+                #           [X_MC] * n_cand_groups
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                U_J_d = np.concatenate([results[NofE][1] for NofE in
+                                        range(n_cand_groups)])
+
+                # Check if all scores are inf
+                if np.isinf(U_J_d).all() or np.isnan(U_J_d).all():
+                    U_J_d = np.ones(len(U_J_d))
+
+                # Get the expected value (mean) of the Utility score
+                # for each cell
+                if explore_method == 'Voronoi':
+                    U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1)
+
+                # create surrogate model for U_J_d
+                # from sklearn.preprocessing import MinMaxScaler
+                # # Take care of inf entries
+                # good_indices = [i for i, arr in enumerate(U_J_d)
+                #                 if np.isfinite(arr).all()]
+                # scaler = MinMaxScaler()
+                # X_S = scaler.fit_transform(candidates[good_indices])
+                # gp = MetaModel.gaussian_process_emulator(
+                #     X_S, U_J_d[good_indices], autoSelect=False
+                #     )
+                # U_J_d = gp.predict(scaler.transform(allCandidates))
+
+                # Normalize U_J_d
+                norm_U_J_d = U_J_d / np.sum(U_J_d)
+            else:
+                norm_U_J_d = np.zeros((len(scoreExploration)))
+
+            # ------- Calculate Total score -------
+            # ------- Trade off between EXPLORATION & EXPLOITATION -------
+            # Accumulate the samples
+            # TODO: added this, recheck!!
+            finalCandidates = np.concatenate((allCandidates, candidates), axis = 0)   
+            finalCandidates = np.unique(finalCandidates, axis = 0)
+            
+            #self.allCandidates = allCandidates
+            #self.candidates = candidates
+            #self.norm_U_J_d = norm_U_J_d
+            #self.exploit_w = exploit_w
+            #self.scoreExploration = scoreExploration
+            
+            # Total score
+            #totalScore = exploit_w * norm_U_J_d
+            #totalScore += explore_w * scoreExploration
+            
+            # TODO: changed this from the above to take into account both exploration and exploitation samples without duplicates
+            totalScore = np.zeros(finalCandidates.shape[0])
+            #self.totalScore = totalScore
+            
+            for cand_idx in range(finalCandidates.shape[0]):
+                # find candidate indices
+                idx1 = np.where(allCandidates == finalCandidates[cand_idx])[0]
+                idx2 = np.where(candidates == finalCandidates[cand_idx])[0]
+                #print(f'Candidate number {cand_idx}')
+                #print(finalCandidates[cand_idx])
+                #print(f'Idx1: {idx1}, Idx2: {idx2}')
+                
+                # exploration 
+                if idx1 != []:
+                    idx1 = idx1[0]
+                    #print(f'Values1: {allCandidates[idx1]}')
+                    totalScore[cand_idx] += explore_w * scoreExploration[idx1]
+                    
+                # exploitation
+                if idx2 != []:
+                    idx2 = idx2[0]
+                    #print(f'Values1: {candidates[idx2]}')
+                    totalScore[cand_idx] += exploit_w * norm_U_J_d[idx2]
+                
+
+            # temp: Plot
+            # dim = self.ExpDesign.X.shape[1]
+            # if dim == 2:
+            #     plotter(self.ExpDesign.X, allCandidates, explore_method)
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            # select the requested number of samples
+            if explore_method == 'Voronoi':
+                Xnew = np.zeros((n_new_samples, ndim))
+                for i, idx in enumerate(bestIdx):
+                    X_can = explore.closestPoints[idx]
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+            else:
+                # TODO: changed this from allCandiates to full set of candidates - still not changed for e.g. 'Voronoi'
+                Xnew = finalCandidates[sorted_idxtotalScore[:n_new_samples]]          # here candidates(exploitation) vs allCandidates (exploration)!!
+
+        elif exploit_method == 'VarOptDesign':
+            # ------- EXPLOITATION: VarOptDesign -------
+            UtilMethod = var
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\nweightExploration={explore_w:0.3f} "
+                  f"weightExploitation={exploit_w:0.3f}")
+
+            # Generate candidate samples from Exploration class
+            nMeasurement = old_EDY[OutputNames[0]].shape[1]
+
+            # Find sensitive region
+            if UtilMethod == 'LOOCV':
+                LCerror = MetaModel.LCerror
+                allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames),
+                                           nMeasurement))
+                for y_idx, y_key in enumerate(OutputNames):
+                    for idx, key in enumerate(LCerror[y_key].keys()):
+                        allModifiedLOO[:, y_idx, idx] = abs(
+                            LCerror[y_key][key])
+
+                ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1)
+
+            elif UtilMethod in ['EIGF', 'ALM']:
+                # ----- All other in  ['EIGF', 'ALM'] -----
+                # Initilize the ExploitScore array
+                ExploitScore = np.zeros((len(old_EDX), len(OutputNames)))
+
+                # Split the candidates in groups for multiprocessing
+                if explore_method != 'Voronoi':
+                    split_cand = np.array_split(allCandidates,
+                                                n_cand_groups,
+                                                axis=0)
+                    goodSampleIdx = range(n_cand_groups)
+                else:
+                    # Find indices of the Vornoi cells with samples
+                    goodSampleIdx = []
+                    for idx in range(len(explore.closest_points)):
+                        if len(explore.closest_points[idx]) != 0:
+                            goodSampleIdx.append(idx)
+                    split_cand = explore.closest_points
+
+                # Split the candidates in groups for multiprocessing
+                args = []
+                for index in goodSampleIdx:
+                    args.append((exploit_method, split_cand[index], index,
+                                 sigma2, var))
+
+                # Multiprocessing
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.run_util_func, args).get()
+
+                # Close the pool
+                pool.close()
+                # out = map(self.run_util_func,
+                #           [exploit_method]*len(goodSampleIdx),
+                #           split_cand,
+                #           range(len(goodSampleIdx)),
+                #           [sigma2] * len(goodSampleIdx),
+                #           [var] * len(goodSampleIdx)
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                if explore_method == 'Voronoi':
+                    ExploitScore = [np.mean(results[k][1]) for k in
+                                    range(len(goodSampleIdx))]
+                else:
+                    ExploitScore = np.concatenate(
+                        [results[k][1] for k in range(len(goodSampleIdx))])
+
+            else:
+                raise NameError('The requested utility function is not '
+                                'available.')
+
+            # print("ExploitScore:\n", ExploitScore)
+
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            # Total score
+            # Normalize U_J_d
+            ExploitScore = ExploitScore / np.sum(ExploitScore)
+            totalScore = exploit_w * ExploitScore
+            totalScore += explore_w * scoreExploration
+
+            temp = totalScore.copy()
+            sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            Xnew = np.zeros((n_new_samples, ndim))
+            if explore_method != 'Voronoi':
+                Xnew = allCandidates[bestIdx]
+            else:
+                for i, idx in enumerate(bestIdx.flatten()):
+                    X_can = explore.closest_points[idx]
+                    # plotter(self.ExpDesign.X, X_can, explore_method,
+                    # scoreExploration=None)
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+
+        elif exploit_method == 'alphabetic':
+            # ------- EXPLOITATION: ALPHABETIC -------
+            Xnew = self.util_AlphOptDesign(allCandidates, var)
+
+        elif exploit_method == 'Space-filling':
+            # ------- EXPLOITATION: SPACE-FILLING -------
+            totalScore = scoreExploration
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+
+            # select the requested number of samples
+            Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        else:
+            raise NameError('The requested design method is not available.')
+
+        print("\n")
+        print("\nRun No. {}:".format(old_EDX.shape[0]+1))
+        print("Xnew:\n", Xnew)
+
+        return Xnew, None
+
+    # -------------------------------------------------------------------------
+    def util_AlphOptDesign(self, candidates, var='D-Opt'):
+        """
+        Enriches the Experimental design with the requested alphabetic
+        criterion based on exploring the space with number of sampling points.
+
+        Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos
+        expansion: A review of sampling strategies., Computer Methods in
+        Applied Mechanics and Engineering, 332, 382-407.
+
+        Arguments
+        ---------
+        NCandidate : int
+            Number of candidate points to be searched
+
+        var : string
+            Alphabetic optimality criterion
+
+        Returns
+        -------
+        X_new : array of shape (1, n_params)
+            The new sampling location in the input space.
+        """
+        MetaModelOrig = self
+        Model = self.Model
+        n_new_samples = MetaModelOrig.ExpDesign.n_new_samples
+        NCandidate = candidates.shape[0]
+
+        # TODO: Loop over outputs
+        OutputName = Model.Output.names[0]
+
+        # To avoid changes ub original aPCE object
+        MetaModel = deepcopy(MetaModelOrig)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+
+        # TODO: Only one psi can be selected.
+        # Suggestion: Go for the one with the highest LOO error
+        Scores = list(MetaModel.score_dict[OutputName].values())
+        ModifiedLOO = [1-score for score in Scores]
+        outIdx = np.argmax(ModifiedLOO)
+
+        # Initialize Phi to save the criterion's values
+        Phi = np.zeros((NCandidate))
+
+        BasisIndices = MetaModelOrig.basis_dict[OutputName]["y_"+str(outIdx+1)]
+        P = len(BasisIndices)
+
+        # ------ Old Psi ------------
+        univ_p_val = MetaModelOrig.univ_basis_vals(oldExpDesignX)
+        Psi = MetaModelOrig.create_psi(BasisIndices, univ_p_val)
+
+        # ------ New candidates (Psi_c) ------------
+        # Assemble Psi_c
+        univ_p_val_c = self.univ_basis_vals(candidates)
+        Psi_c = self.create_psi(BasisIndices, univ_p_val_c)
+
+        for idx in range(NCandidate):
+
+            # Include the new row to the original Psi
+            Psi_cand = np.vstack((Psi, Psi_c[idx]))
+
+            # Information matrix
+            PsiTPsi = np.dot(Psi_cand.T, Psi_cand)
+            M = PsiTPsi / (len(oldExpDesignX)+1)
+
+            if np.linalg.cond(PsiTPsi) > 1e-12 \
+               and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon:
+                # faster
+                invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray())
+            else:
+                # stabler
+                invM = np.linalg.pinv(M)
+
+            # ---------- Calculate optimality criterion ----------
+            # Optimality criteria according to Section 4.5.1 in Ref.
+
+            # D-Opt
+            if var == 'D-Opt':
+                Phi[idx] = (np.linalg.det(invM)) ** (1/P)
+
+            # A-Opt
+            elif var == 'A-Opt':
+                Phi[idx] = np.trace(invM)
+
+            # K-Opt
+            elif var == 'K-Opt':
+                Phi[idx] = np.linalg.cond(M)
+
+            else:
+                raise Exception('The optimality criterion you requested has '
+                      'not been implemented yet!')
+
+        # find an optimal point subset to add to the initial design
+        # by minimization of the Phi
+        sorted_idxtotalScore = np.argsort(Phi)
+
+        # select the requested number of samples
+        Xnew = candidates[sorted_idxtotalScore[:n_new_samples]]
+
+        return Xnew
+
+    # -------------------------------------------------------------------------
+    def __normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s,
+                  rmse=None):
+
+        Model = self.Model
+        likelihoods = 1.0
+
+        # Loop over the outputs
+        for idx, out in enumerate(Model.Output.names):
+
+            # (Meta)Model Output
+            nsamples, nout = y_hat_pce[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values
+
+            # Surrogate error if valid dataset is given.
+            if rmse is not None:
+                tot_sigma2s += rmse[out]**2
+            else:
+                tot_sigma2s += np.mean(std_pce[out])**2
+
+            likelihoods *= stats.multivariate_normal.pdf(
+                y_hat_pce[out], data, np.diag(tot_sigma2s),
+                allow_singular=True)
+
+        self.Likelihoods = likelihoods
+
+        return likelihoods
+
+    # -------------------------------------------------------------------------
+    def __corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = MetaModel.ExpDesign.X  # valid_samples
+        model_outputs = MetaModel.ExpDesign.Y  # valid_model_runs
+        Model = MetaModel.ModelObj
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = Model.Output.names
+
+        # TODO: Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                # covMatrix = np.diag(stdOutputRS[out][i]**2)
+                covMatrix = np.diag((y_m-y_m_hat)**2)
+                covMatrix = np.diag(
+                    np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2
+                    )
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += self.__logpdf(
+                    y_m_hat, data, covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += self.__logpdf(y_m_hat, y_m, covMatrix)
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.exp(logLik_model+logLik_data)
+
+        return np.log(np.mean(weights))
+
+    # -------------------------------------------------------------------------
+    def __logpdf(self, x, mean, cov):
+        """
+        computes the likelihood based on a multivariate normal distribution.
+
+        Parameters
+        ----------
+        x : TYPE
+            DESCRIPTION.
+        mean : array_like
+            Observation data.
+        cov : 2d array
+            Covariance matrix of the distribution.
+
+        Returns
+        -------
+        log_lik : float
+            Log likelihood.
+
+        """
+        n = len(mean)
+        L = linalg.cholesky(cov, lower=True)
+        beta = np.sum(np.log(np.diag(L)))
+        dev = x - mean
+        alpha = dev.dot(linalg.cho_solve((L, True), dev))
+        log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+
+        return log_lik
+
+    # -------------------------------------------------------------------------
+    def __posteriorPlot(self, posterior, par_names, key):
+
+        # Initialization
+        newpath = (r'Outputs_SeqPosteriorComparison/posterior')
+        os.makedirs(newpath, exist_ok=True)
+
+        bound_tuples = self.MetaModel.bound_tuples
+        n_params = len(par_names)
+        font_size = 40
+        if n_params == 2:
+
+            figPosterior, ax = plt.subplots(figsize=(15, 15))
+
+            sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1],
+                        fill=True, ax=ax, cmap=plt.cm.jet,
+                        clip=bound_tuples)
+            # Axis labels
+            plt.xlabel(par_names[0], fontsize=font_size)
+            plt.ylabel(par_names[1], fontsize=font_size)
+
+            # Set axis limit
+            plt.xlim(bound_tuples[0])
+            plt.ylim(bound_tuples[1])
+
+            # Increase font size
+            plt.xticks(fontsize=font_size)
+            plt.yticks(fontsize=font_size)
+
+            # Switch off the grids
+            plt.grid(False)
+
+        else:
+            import corner
+            figPosterior = corner.corner(posterior, labels=par_names,
+                                         title_fmt='.2e', show_titles=True,
+                                         title_kwargs={"fontsize": 12})
+
+        figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight')
+        plt.close()
+
+        # Save the posterior as .npy
+        np.save(f'./{newpath}/{key}.npy', posterior)
+
+        return figPosterior
+
+    # -------------------------------------------------------------------------
+    def __hellinger_distance(self, P, Q):
+        """
+        Hellinger distance between two continuous distributions.
+
+        The maximum distance 1 is achieved when P assigns probability zero to
+        every set to which Q assigns a positive probability, and vice versa.
+        0 (identical) and 1 (maximally different)
+
+        Parameters
+        ----------
+        P : array
+            Reference likelihood.
+        Q : array
+            Estimated likelihood.
+
+        Returns
+        -------
+        float
+            Hellinger distance of two distributions.
+
+        """
+        mu1 = P.mean()
+        Sigma1 = np.std(P)
+
+        mu2 = Q.mean()
+        Sigma2 = np.std(Q)
+
+        term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2))
+
+        term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2))
+
+        H_squared = 1 - term1 * term2
+
+        return np.sqrt(H_squared)
+
+    # -------------------------------------------------------------------------
+    def __BME_Calculator(self, MetaModel, obs_data, sigma2Dict, rmse=None):
+        """
+        This function computes the Bayesian model evidence (BME) via Monte
+        Carlo integration.
+
+        """
+        # Initializations
+        if hasattr(MetaModel, 'valid_likelihoods'):
+            valid_likelihoods = MetaModel.valid_likelihoods
+        else:
+            valid_likelihoods = []
+
+        post_snapshot = MetaModel.ExpDesign.post_snapshot
+        #print(f'post_snapshot: {post_snapshot}')
+        if post_snapshot or len(valid_likelihoods) != 0:
+            newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref')
+            os.makedirs(newpath, exist_ok=True)
+
+        SamplingMethod = 'random'
+        MCsize = 10000
+        ESS = 0
+
+        # Estimation of the integral via Monte Varlo integration
+        while (ESS > MCsize) or (ESS < 1):
+
+            # Generate samples for Monte Carlo simulation
+            X_MC = MetaModel.ExpDesign.generate_samples(
+                MCsize, SamplingMethod
+                )
+
+            # Monte Carlo simulation for the candidate design
+            Y_MC, std_MC = MetaModel.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and
+            # simulation results via PCE with candidate design)
+            Likelihoods = self.__normpdf(
+                Y_MC, std_MC, obs_data, sigma2Dict, rmse
+                )
+
+            # Check the Effective Sample Size (1000<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if (ESS > MCsize) or (ESS < 1):
+                print(f'ESS={ESS} MC size should be larger.')
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+        X_Posterior = X_MC[accepted]
+
+        # ------------------------------------------------------------
+        # --- Kullback-Leibler Divergence & Information Entropy ------
+        # ------------------------------------------------------------
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(Likelihoods))
+
+        # TODO: Correction factor
+        # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME)
+
+        # Posterior-based expectation of likelihoods
+        postExpLikelihoods = np.mean(np.log(Likelihoods[accepted]))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(
+            np.log(MetaModel.ExpDesign.JDist.pdf(X_Posterior.T))
+            )
+
+        # Calculate Kullback-Leibler Divergence
+        # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+        KLD = postExpLikelihoods - logBME
+
+        # Information Entropy based on Entropy paper Eq. 38
+        infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+        # If post_snapshot is True, plot likelihood vs refrence
+        if post_snapshot or valid_likelihoods:
+            # Hellinger distance
+            #print('arrived here')
+            #print(np.array(valid_likelihoods))
+            valid_likelihoods = np.array(valid_likelihoods)
+            #valid_likelihoods = np.array(valid_likelihoods)
+            ref_like = np.log(valid_likelihoods[(valid_likelihoods > 0)])
+            est_like = np.log(Likelihoods[Likelihoods > 0])
+            distHellinger = self.__hellinger_distance(ref_like, est_like)
+
+            idx = len([name for name in os.listdir(newpath) if 'Likelihoods_'
+                       in name and os.path.isfile(os.path.join(newpath, name))])
+            fig, ax = plt.subplots()
+            try:
+                sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]),
+                            shade=True, color="g", label='Ref. Likelihood')
+                sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True,
+                            color="b", label='Likelihood with PCE')
+            except:
+                pass
+
+            text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}"
+            "\n DKL={KLD:.3f}"
+
+            plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat',
+                                                 edgecolor='black',
+                                                 boxstyle='round,pad=1'),
+                     transform=ax.transAxes)
+
+            fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf',
+                        bbox_inches='tight')
+            plt.close()
+
+        else:
+            distHellinger = 0.0
+
+        # Bayesian inference with Emulator only for 2D problem
+        if post_snapshot and MetaModel.n_params == 2 and not idx % 5:
+            BayesOpts = BayesInference(MetaModel)
+            BayesOpts.emulator = True
+            BayesOpts.plot_post_pred = False
+
+            # Select the inference method
+            import emcee
+            BayesOpts.inference_method = "MCMC"
+            # Set the MCMC parameters passed to self.mcmc_params
+            BayesOpts.mcmc_params = {
+                'n_steps': 1e5,
+                'n_walkers': 30,
+                'moves': emcee.moves.KDEMove(),
+                'verbose': False
+                }
+
+            # ----- Define the discrepancy model -------
+            obs_data = pd.DataFrame(obs_data, columns=self.Model.Output.names)
+            BayesOpts.measurement_error = obs_data
+
+            # # -- (Option B) --
+            DiscrepancyOpts = Discrepancy('')
+            DiscrepancyOpts.type = 'Gaussian'
+            DiscrepancyOpts.parameters = obs_data**2
+            BayesOpts.Discrepancy = DiscrepancyOpts
+            # Start the calibration/inference
+            Bayes_PCE = BayesOpts.create_inference()
+            X_Posterior = Bayes_PCE.posterior_df.values
+
+        return (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+
+    # -------------------------------------------------------------------------
+    def __validError(self, MetaModel):
+
+        # MetaModel = self.MetaModel
+        Model = MetaModel.ModelObj
+        OutputName = Model.Output.names
+
+        # Extract the original model with the generated samples
+        valid_samples = MetaModel.valid_samples
+        valid_model_runs = MetaModel.valid_model_runs
+
+        # Run the PCE model with the generated samples
+        valid_PCE_runs, _ = MetaModel.eval_metamodel(samples=valid_samples)
+
+        rms_error = {}
+        valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in OutputName:
+            rms_error[key] = mean_squared_error(
+                valid_model_runs[key], valid_PCE_runs[key],
+                multioutput='raw_values',
+                sample_weight=None,
+                squared=False)
+            # Validation error
+            valid_error[key] = (rms_error[key]**2)
+            valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Updated Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(rms_error[key],
+                                             valid_error[key]))))
+
+        return rms_error, valid_error
+
+    # -------------------------------------------------------------------------
+    def __error_Mean_Std(self):
+
+        MetaModel = self.MetaModel
+        # Extract the mean and std provided by user
+        df_MCReference = MetaModel.ModelObj.mc_reference
+
+        # Compute the mean and std based on the MetaModel
+        pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+
+        # Compute the root mean squared error
+        for output in MetaModel.ModelObj.Output.names:
+
+            # Compute the error between mean and std of MetaModel and OrigModel
+            RMSE_Mean = mean_squared_error(
+                df_MCReference['mean'], pce_means[output], squared=False
+                )
+            RMSE_std = mean_squared_error(
+                df_MCReference['std'], pce_means[output], squared=False
+                )
+
+        return RMSE_Mean, RMSE_std
+
+    # -------------------------------------------------------------------------
+    def _compute_pce_moments(self, MetaModel):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moment (mean) of the surrogate.
+        pce_stds: dict
+            The second moment (standard deviation) of the surrogate.
+
+        """
+        outputs = MetaModel.ModelObj.Output.names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(MetaModel.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    PCA = MetaModel.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.inverse_transform(pce_mean)
+                    stds[output] = PCA.inverse_transform(np.sqrt(pce_var))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+        return pce_means, pce_stds
diff --git a/src/bayesvalidrox/surrogate_models/sequential_design.py b/src/bayesvalidrox/surrogate_models/sequential_design.py
new file mode 100644
index 0000000000000000000000000000000000000000..fc81dcd4529ca0708dfba47385aef4415992eb3e
--- /dev/null
+++ b/src/bayesvalidrox/surrogate_models/sequential_design.py
@@ -0,0 +1,2187 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+Created on Fri Jan 28 09:21:18 2022
+
+@author: farid
+"""
+import numpy as np
+from scipy import stats, signal, linalg, sparse
+from scipy.spatial import distance
+from copy import deepcopy, copy
+from tqdm import tqdm
+import scipy.optimize as opt
+from sklearn.metrics import mean_squared_error
+import multiprocessing
+import matplotlib.pyplot as plt
+import sys
+import os
+import gc
+import seaborn as sns
+from joblib import Parallel, delayed
+import resource
+from .exploration import Exploration
+
+
+class SeqDesign():
+    """ Sequential experimental design
+    This class provieds method for trainig the meta-model in an iterative
+    manners.
+    The main method to execute the task is `train_seq_design`, which
+      recieves a model object and returns the trained metamodel.
+    """
+
+    # -------------------------------------------------------------------------
+    def train_seq_design(self, MetaModel):
+        """
+        Starts the adaptive sequential design for refining the surrogate model
+        by selecting training points in a sequential manner.
+
+        Parameters
+        ----------
+        Model : object
+            An object containing all model specifications.
+
+        Returns
+        -------
+        MetaModel : object
+            Meta model object.
+
+        """
+        # MetaModel = self
+        Model = MetaModel.ModelObj
+        self.MetaModel = MetaModel
+        self.Model = Model
+
+        # Initialization
+        MetaModel.SeqModifiedLOO = {}
+        MetaModel.seqValidError = {}
+        MetaModel.SeqBME = {}
+        MetaModel.SeqKLD = {}
+        MetaModel.SeqDistHellinger = {}
+        MetaModel.seqRMSEMean = {}
+        MetaModel.seqRMSEStd = {}
+        MetaModel.seqMinDist = []
+        pce = True if MetaModel.meta_model_type.lower() != 'gpe' else False
+        mc_ref = True if bool(Model.mc_reference) else False
+        if mc_ref:
+            Model.read_mc_reference()
+
+        if not hasattr(MetaModel, 'valid_likelihoods'):
+            MetaModel.valid_samples = []
+            MetaModel.valid_model_runs = []
+            MetaModel.valid_likelihoods = []
+
+        # Get the parameters
+        max_n_samples = MetaModel.ExpDesign.n_max_samples
+        mod_LOO_threshold = MetaModel.ExpDesign.mod_LOO_threshold
+        n_canddidate = MetaModel.ExpDesign.n_canddidate
+        post_snapshot = MetaModel.ExpDesign.post_snapshot
+        n_replication = MetaModel.ExpDesign.n_replication
+        util_func = MetaModel.ExpDesign.util_func
+        output_name = Model.Output.names
+        validError = None
+        # Handle if only one UtilityFunctions is provided
+        if not isinstance(util_func, list):
+            util_func = [MetaModel.ExpDesign.util_func]
+
+        # Read observations or MCReference
+        if len(Model.observations) != 0 or Model.meas_file is not None:
+            self.observations = Model.read_observation()
+            obs_data = self.observations
+        else:
+            obs_data = []
+            TotalSigma2 = {}
+        # ---------- Initial MetaModel ----------
+        initMetaModel = deepcopy(MetaModel)
+
+        # Validation error if validation set is provided.
+        if len(MetaModel.valid_model_runs) != 0:
+            init_rmse, init_valid_error = self.__validError(initMetaModel)
+            init_valid_error = list(init_valid_error.values())
+        else:
+            init_rmse = None
+
+        # Check if discrepancy is provided
+        if len(obs_data) != 0 and hasattr(MetaModel, 'Discrepancy'):
+            TotalSigma2 = MetaModel.Discrepancy.parameters
+
+            # Calculate the initial BME
+            out = self.__BME_Calculator(
+                initMetaModel, obs_data, TotalSigma2, init_rmse)
+            init_BME, init_KLD, init_post, init_likes, init_dist_hellinger = out
+            print(f"\nInitial BME: {init_BME:.2f}")
+            print(f"Initial KLD: {init_KLD:.2f}")
+
+            # Posterior snapshot (initial)
+            if post_snapshot:
+                parNames = MetaModel.ExpDesign.par_names
+                print('Posterior snapshot (initial) is being plotted...')
+                self.__posteriorPlot(init_post, parNames, 'SeqPosterior_init')
+
+        # Check the convergence of the Mean & Std
+        if mc_ref and pce:
+            init_rmse_mean, init_rmse_std = self.__error_Mean_Std()
+            print(f"Initial Mean and Std error: {init_rmse_mean},"
+                  f" {init_rmse_std}")
+
+        # Read the initial experimental design
+        Xinit = initMetaModel.ExpDesign.X
+        init_n_samples = len(MetaModel.ExpDesign.X)
+        initYprev = initMetaModel.ModelOutputDict
+        initLCerror = initMetaModel.LCerror
+        n_itrs = max_n_samples - init_n_samples
+
+        # Read the initial ModifiedLOO
+        if pce:
+            Scores_all, varExpDesignY = [], []
+            for out_name in output_name:
+                y = initMetaModel.ExpDesign.Y[out_name]
+                Scores_all.append(list(
+                    initMetaModel.score_dict['b_1'][out_name].values()))
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    pca = MetaModel.pca['b_1'][out_name]
+                    components = pca.transform(y)
+                    varExpDesignY.append(np.var(components, axis=0))
+                else:
+                    varExpDesignY.append(np.var(y, axis=0))
+
+            Scores = [item for sublist in Scores_all for item in sublist]
+            weights = [item for sublist in varExpDesignY for item in sublist]
+            init_mod_LOO = [np.average([1-score for score in Scores],
+                                       weights=weights)]
+
+        prevMetaModel_dict = {}
+        # Replicate the sequential design
+        for repIdx in range(n_replication):
+            print(f'\n>>>> Replication: {repIdx+1}<<<<')
+
+            # To avoid changes ub original aPCE object
+            MetaModel.ExpDesign.X = Xinit
+            MetaModel.ExpDesign.Y = initYprev
+            MetaModel.LCerror = initLCerror
+
+            for util_f in util_func:
+                print(f'\n>>>> Utility Function: {util_f} <<<<')
+                # To avoid changes ub original aPCE object
+                MetaModel.ExpDesign.X = Xinit
+                MetaModel.ExpDesign.Y = initYprev
+                MetaModel.LCerror = initLCerror
+
+                # Set the experimental design
+                Xprev = Xinit
+                total_n_samples = init_n_samples
+                Yprev = initYprev
+
+                Xfull = []
+                Yfull = []
+
+                # Store the initial ModifiedLOO
+                if pce:
+                    print("\nInitial ModifiedLOO:", init_mod_LOO)
+                    SeqModifiedLOO = np.array(init_mod_LOO)
+
+                if len(MetaModel.valid_model_runs) != 0:
+                    SeqValidError = np.array(init_valid_error)
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    SeqBME = np.array([init_BME])
+                    SeqKLD = np.array([init_KLD])
+                    SeqDistHellinger = np.array([init_dist_hellinger])
+
+                if mc_ref and pce:
+                    seqRMSEMean = np.array([init_rmse_mean])
+                    seqRMSEStd = np.array([init_rmse_std])
+
+                # ------- Start Sequential Experimental Design -------
+                postcnt = 1
+                for itr_no in range(1, n_itrs+1):
+                    print(f'\n>>>> Iteration number {itr_no} <<<<')
+
+                    # Save the metamodel prediction before updating
+                    prevMetaModel_dict[itr_no] = deepcopy(MetaModel)
+                    if itr_no > 1:
+                        pc_model = prevMetaModel_dict[itr_no-1]
+                        self._y_hat_prev, _ = pc_model.eval_metamodel(
+                            samples=Xfull[-1].reshape(1, -1))
+
+                    # Optimal Bayesian Design
+                    m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    MetaModel.ExpDesignFlag = 'sequential'
+                    Xnew, updatedPrior = self.opt_SeqDesign(TotalSigma2,
+                                                            n_canddidate,
+                                                            util_f)
+                    m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    S = np.min(distance.cdist(Xinit, Xnew, 'euclidean'))
+                    MetaModel.seqMinDist.append(S)
+                    print(f"\nmin Dist from OldExpDesign: {S:2f}")
+                    print("\n")
+
+                    # Evaluate the full model response at the new sample
+                    Ynew, _ = Model.run_model_parallel(
+                        Xnew, prevRun_No=total_n_samples
+                        )
+                    total_n_samples += Xnew.shape[0]
+                    # ------ Plot the surrogate model vs Origninal Model ------
+                    if hasattr(MetaModel, 'adapt_verbose') and \
+                       MetaModel.adapt_verbose:
+                        from .adaptPlot import adaptPlot
+                        y_hat, std_hat = MetaModel.eval_metamodel(samples=Xnew)
+                        adaptPlot(MetaModel, Ynew, y_hat, std_hat, plotED=False)
+
+                    # -------- Retrain the surrogate model -------
+                    # Extend new experimental design
+                    Xfull = np.vstack((Xprev, Xnew))
+
+                    # Updating experimental design Y
+                    for out_name in output_name:
+                        Yfull = np.vstack((Yprev[out_name], Ynew[out_name]))
+                        MetaModel.ModelOutputDict[out_name] = Yfull
+
+                    # Pass new design to the metamodel object
+                    MetaModel.ExpDesign.sampling_method = 'user'
+                    MetaModel.ExpDesign.X = Xfull
+                    MetaModel.ExpDesign.Y = MetaModel.ModelOutputDict
+
+                    # Save the Experimental Design for next iteration
+                    Xprev = Xfull
+                    Yprev = MetaModel.ModelOutputDict
+
+                    # Pass the new prior as the input
+                    MetaModel.input_obj.poly_coeffs_flag = False
+                    if updatedPrior is not None:
+                        MetaModel.input_obj.poly_coeffs_flag = True
+                        print("updatedPrior:", updatedPrior.shape)
+                        # Arbitrary polynomial chaos
+                        for i in range(updatedPrior.shape[1]):
+                            MetaModel.input_obj.Marginals[i].dist_type = None
+                            x = updatedPrior[:, i]
+                            MetaModel.input_obj.Marginals[i].raw_data = x
+
+                    # Train the surrogate model for new ExpDesign
+                    MetaModel.train_norm_design(parallel=False)
+                    m_3 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+
+                    # -------- Evaluate the retrained surrogate model -------
+                    # Extract Modified LOO from Output
+                    if pce:
+                        Scores_all, varExpDesignY = [], []
+                        for out_name in output_name:
+                            y = MetaModel.ExpDesign.Y[out_name]
+                            Scores_all.append(list(
+                                MetaModel.score_dict['b_1'][out_name].values()))
+                            if MetaModel.dim_red_method.lower() == 'pca':
+                                pca = MetaModel.pca['b_1'][out_name]
+                                components = pca.transform(y)
+                                varExpDesignY.append(np.var(components,
+                                                            axis=0))
+                            else:
+                                varExpDesignY.append(np.var(y, axis=0))
+                        Scores = [item for sublist in Scores_all for item
+                                  in sublist]
+                        weights = [item for sublist in varExpDesignY for item
+                                   in sublist]
+                        ModifiedLOO = [np.average(
+                            [1-score for score in Scores], weights=weights)]
+
+                        print('\n')
+                        print(f"Updated ModifiedLOO {util_f}:\n", ModifiedLOO)
+                        print('\n')
+
+                    # Compute the validation error
+                    if len(MetaModel.valid_model_runs) != 0:
+                        rmse, validError = self.__validError(MetaModel)
+                        ValidError = list(validError.values())
+                    else:
+                        rmse = None
+
+                    # Store updated ModifiedLOO
+                    if pce:
+                        SeqModifiedLOO = np.vstack(
+                            (SeqModifiedLOO, ModifiedLOO))
+                        if len(MetaModel.valid_model_runs) != 0:
+                            SeqValidError = np.vstack(
+                                (SeqValidError, ValidError))
+                    m_4 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    # -------- Caclulation of BME as accuracy metric -------
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        # Calculate the initial BME
+                        out = self.__BME_Calculator(MetaModel, obs_data,
+                                                    TotalSigma2, rmse)
+                        BME, KLD, Posterior, likes, DistHellinger = out
+                        print('\n')
+                        print(f"Updated BME: {BME:.2f}")
+                        print(f"Updated KLD: {KLD:.2f}")
+                        print('\n')
+
+                        # Plot some snapshots of the posterior
+                        step_snapshot = MetaModel.ExpDesign.step_snapshot
+                        if post_snapshot and postcnt % step_snapshot == 0:
+                            parNames = MetaModel.ExpDesign.par_names
+                            print('Posterior snapshot is being plotted...')
+                            self.__posteriorPlot(Posterior, parNames,
+                                                 f'SeqPosterior_{postcnt}')
+                        postcnt += 1
+                    m_5 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+
+                    # Check the convergence of the Mean&Std
+                    if mc_ref and pce:
+                        print('\n')
+                        RMSE_Mean, RMSE_std = self.__error_Mean_Std()
+                        print(f"Updated Mean and Std error: {RMSE_Mean:.2f}, "
+                              f"{RMSE_std:.2f}")
+                        print('\n')
+
+                    # Store the updated BME & KLD
+                    # Check if data is provided
+                    if len(obs_data) != 0:
+                        SeqBME = np.vstack((SeqBME, BME))
+                        SeqKLD = np.vstack((SeqKLD, KLD))
+                        SeqDistHellinger = np.vstack((SeqDistHellinger,
+                                                      DistHellinger))
+                    if mc_ref and pce:
+                        seqRMSEMean = np.vstack((seqRMSEMean, RMSE_Mean))
+                        seqRMSEStd = np.vstack((seqRMSEStd, RMSE_std))
+
+                    if pce and any(LOO < mod_LOO_threshold
+                                   for LOO in ModifiedLOO):
+                        break
+
+                    print(f"Memory itr {itr_no}: I: {m_2-m_1:.2f} MB")
+                    print(f"Memory itr {itr_no}: II: {m_3-m_2:.2f} MB")
+                    print(f"Memory itr {itr_no}: III: {m_4-m_3:.2f} MB")
+                    print(f"Memory itr {itr_no}: IV: {m_5-m_4:.2f} MB")
+                    m_6 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+                    print(f"Memory itr {itr_no}: total: {m_6:.2f} MB")
+
+                    # Clean up
+                    if len(obs_data) != 0:
+                        del out
+                    gc.collect()
+                    print()
+                    print('-'*50)
+                    print()
+
+                # Store updated ModifiedLOO and BME in dictonary
+                strKey = f'{util_f}_rep_{repIdx+1}'
+                if pce:
+                    MetaModel.SeqModifiedLOO[strKey] = SeqModifiedLOO
+                if len(MetaModel.valid_model_runs) != 0:
+                    MetaModel.seqValidError[strKey] = SeqValidError
+
+                # Check if data is provided
+                if len(obs_data) != 0:
+                    MetaModel.SeqBME[strKey] = SeqBME
+                    MetaModel.SeqKLD[strKey] = SeqKLD
+                if len(MetaModel.valid_likelihoods) != 0:
+                    MetaModel.SeqDistHellinger[strKey] = SeqDistHellinger
+                if mc_ref and pce:
+                    MetaModel.seqRMSEMean[strKey] = seqRMSEMean
+                    MetaModel.seqRMSEStd[strKey] = seqRMSEStd
+
+        return MetaModel
+
+    # -------------------------------------------------------------------------
+    def util_VarBasedDesign(self, X_can, index, util_func='Entropy'):
+        """
+        Computes the exploitation scores based on:
+        active learning MacKay(ALM) and active learning Cohn (ALC)
+        Paper: Sequential Design with Mutual Information for Computer
+        Experiments (MICE): Emulation of a Tsunami Model by Beck and Guillas
+        (2016)
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        index : int
+            Model output index.
+        UtilMethod : string, optional
+            Exploitation utility function. The default is 'Entropy'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+        MetaModel = self.MetaModel
+        ED_X = MetaModel.ExpDesign.X
+        out_dict_y = MetaModel.ExpDesign.Y
+        out_names = MetaModel.ModelObj.Output.names
+
+        # Run the Metamodel for the candidate
+        X_can = X_can.reshape(1, -1)
+        Y_PC_can, std_PC_can = MetaModel.eval_metamodel(samples=X_can)
+
+        if util_func.lower() == 'alm':
+            # ----- Entropy/MMSE/active learning MacKay(ALM)  -----
+            # Compute perdiction variance of the old model
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            varPCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                varPCE[KeyIdx] = np.max(canPredVar[key], axis=1)
+            score = np.max(varPCE, axis=0)
+
+        elif util_func.lower() == 'eigf':
+            # ----- Expected Improvement for Global fit -----
+            # Find closest EDX to the candidate
+            distances = distance.cdist(ED_X, X_can, 'euclidean')
+            index = np.argmin(distances)
+
+            # Compute perdiction error and variance of the old model
+            predError = {key: Y_PC_can[key] for key in out_names}
+            canPredVar = {key: std_PC_can[key]**2 for key in out_names}
+
+            # Compute perdiction error and variance of the old model
+            # Eq (5) from Liu et al.(2018)
+            EIGF_PCE = np.zeros((len(out_names), X_can.shape[0]))
+            for KeyIdx, key in enumerate(out_names):
+                residual = predError[key] - out_dict_y[key][int(index)]
+                var = canPredVar[key]
+                EIGF_PCE[KeyIdx] = np.max(residual**2 + var, axis=1)
+            score = np.max(EIGF_PCE, axis=0)
+
+        return -1 * score   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def util_BayesianActiveDesign(self, X_can, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian active design criterion (var).
+
+        It is based on the following paper:
+        Oladyshkin, Sergey, Farid Mohammadi, Ilja Kroeker, and Wolfgang Nowak.
+        "Bayesian3 active learning for the gaussian process emulator using
+        information theory." Entropy 22, no. 8 (2020): 890.
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            BAL design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_mean_can, Y_std_can = self.MetaModel.eval_metamodel(
+            samples=np.array([X_can])
+            )
+
+        # Get the data
+        obs_data = self.observations
+        n_obs = self.Model.n_obs
+        # TODO: Analytical DKL
+        # Sample a distribution for a normal dist
+        # with Y_mean_can as the mean and Y_std_can as std.
+
+        # priorMean, priorSigma2, Obs = np.empty((0)),np.empty((0)),np.empty((0))
+
+        # for key in list(Y_mean_can):
+        #     # concatenate the measurement error
+        #     Obs = np.hstack((Obs,ObservationData[key]))
+
+        #     # concatenate the mean and variance of prior predictive
+        #     means, stds = Y_mean_can[key][0], Y_std_can[key][0]
+        #     priorMean = np.hstack((priorSigma2,means))
+        #     priorSigma2 = np.hstack((priorSigma2,stds**2))
+
+        # # Covariance Matrix of prior
+        # covPrior = np.zeros((priorSigma2.shape[0], priorSigma2.shape[0]), float)
+        # np.fill_diagonal(covPrior, priorSigma2)
+
+        # # Covariance Matrix of Likelihood
+        # covLikelihood = np.zeros((sigma2Dict.shape[0], sigma2Dict.shape[0]), float)
+        # np.fill_diagonal(covLikelihood, sigma2Dict)
+
+        # # Calculate moments of the posterior (Analytical derivation)
+        # n = priorSigma2.shape[0]
+        # covPost = np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))),covLikelihood/n)
+
+        # meanPost = np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))) , Obs) + \
+        #             np.dot(np.dot(covPrior,np.linalg.inv(covPrior+(covLikelihood/n))),
+        #                     priorMean/n)
+        # # Compute DKL from prior to posterior
+        # term1 = np.trace(np.dot(np.linalg.inv(covPrior),covPost))
+        # deltaMean = priorMean-meanPost
+        # term2 = np.dot(np.dot(deltaMean,np.linalg.inv(covPrior)),deltaMean[:,None])
+        # term3 = np.log(np.linalg.det(covPrior)/np.linalg.det(covPost))
+        # DKL = 0.5 * (term1 + term2 - n + term3)[0]
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = 20000
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Sample a distribution for a normal dist
+            # with Y_mean_can as the mean and Y_std_can as std.
+            Y_MC, std_MC = {}, {}
+            logPriorLikelihoods = np.zeros((MCsize))
+            for key in list(Y_mean_can):
+                means, stds = Y_mean_can[key][0], Y_std_can[key][0]
+                # cov = np.zeros((means.shape[0], means.shape[0]), float)
+                # np.fill_diagonal(cov, stds**2)
+
+                Y_MC[key] = np.zeros((MCsize, n_obs))
+                logsamples = np.zeros((MCsize, n_obs))
+                for i in range(n_obs):
+                    NormalDensity = stats.norm(means[i], stds[i])
+                    Y_MC[key][:, i] = NormalDensity.rvs(MCsize)
+                    logsamples[:, i] = NormalDensity.logpdf(Y_MC[key][:, i])
+
+                logPriorLikelihoods = np.sum(logsamples, axis=1)
+                std_MC[key] = np.zeros((MCsize, means.shape[0]))
+
+            #  Likelihood computation (Comparison of data and simulation
+            #  results via PCE with candidate design)
+            likelihoods = self.__normpdf(Y_MC, std_MC, obs_data, sigma2Dict)
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.nansum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(likelihoods))
+
+        # Posterior-based expectation of likelihoods
+        postLikelihoods = likelihoods[accepted]
+        postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+        # Utility function Eq.2 in Ref. (2)
+        # Posterior covariance matrix after observing data y
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # TODO: Calculate the correction factor for BME
+            # BMECorrFactor = self.BME_Corr_Weight(PCE_SparseBayes_can,
+            #                                      ObservationData, sigma2Dict)
+            # BME += BMECorrFactor
+            # Haun et al implementation
+            # U_J_d = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+            U_J_d = postExpLikelihoods - logBME
+
+        # Marginal log likelihood
+        elif var == 'BME':
+            U_J_d = logBME
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            logBME = np.log(np.nanmean(likelihoods))
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # Bayesian information criterion
+        elif var == 'BIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxL = np.nanmax(likelihoods)
+            U_J_d = -2 * np.log(maxL) + np.log(n_obs) * nModelParams
+
+        # Akaike information criterion
+        elif var == 'AIC':
+            coeffs = self.MetaModel.coeffs_dict.values()
+            nModelParams = max(len(v) for val in coeffs for v in val.values())
+            maxlogL = np.log(np.nanmax(likelihoods))
+            AIC = -2 * maxlogL + 2 * nModelParams
+            # 2 * nModelParams * (nModelParams+1) / (n_obs-nModelParams-1)
+            penTerm = 0
+            U_J_d = 1*(AIC + penTerm)
+
+        # Deviance information criterion
+        elif var == 'DIC':
+            # D_theta_bar = np.mean(-2 * Likelihoods)
+            N_star_p = 0.5 * np.var(np.log(likelihoods[likelihoods != 0]))
+            Likelihoods_theta_mean = self.__normpdf(
+                Y_mean_can, Y_std_can, obs_data, sigma2Dict
+                )
+            DIC = -2 * np.log(Likelihoods_theta_mean) + 2 * N_star_p
+
+            U_J_d = DIC
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Handle inf and NaN (replace by zero)
+        if np.isnan(U_J_d) or U_J_d == -np.inf or U_J_d == np.inf:
+            U_J_d = 0.0
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+        gc.collect(generation=2)
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def update_metamodel(self, MetaModel, output, y_hat_can, univ_p_val, index,
+                         new_pca=False):
+        BasisIndices = MetaModel.basis_dict[output]["y_"+str(index+1)]
+        clf_poly = MetaModel.clf_poly[output]["y_"+str(index+1)]
+        Mn = clf_poly.coef_
+        Sn = clf_poly.sigma_
+        beta = clf_poly.alpha_
+        active = clf_poly.active_
+        Psi = self.MetaModel.create_psi(BasisIndices, univ_p_val)
+
+        Sn_new_inv = np.linalg.inv(Sn)
+        Sn_new_inv += beta * np.dot(Psi[:, active].T, Psi[:, active])
+        Sn_new = np.linalg.inv(Sn_new_inv)
+
+        Mn_new = np.dot(Sn_new_inv, Mn[active]).reshape(-1, 1)
+        Mn_new += beta * np.dot(Psi[:, active].T, y_hat_can)
+        Mn_new = np.dot(Sn_new, Mn_new).flatten()
+
+        # Compute the old and new moments of PCEs
+        mean_old = Mn[0]
+        mean_new = Mn_new[0]
+        std_old = np.sqrt(np.sum(np.square(Mn[1:])))
+        std_new = np.sqrt(np.sum(np.square(Mn_new[1:])))
+
+        # Back transformation if PCA is selected.
+        if MetaModel.dim_red_method.lower() == 'pca':
+            old_pca = MetaModel.pca[output]
+            mean_old = old_pca.mean_[index]
+            mean_old += np.sum(mean_old * old_pca.components_[:, index])
+            std_old = np.sqrt(np.sum(std_old**2 *
+                                     old_pca.components_[:, index]**2))
+            mean_new = new_pca.mean_[index]
+            mean_new += np.sum(mean_new * new_pca.components_[:, index])
+            std_new = np.sqrt(np.sum(std_new**2 *
+                                     new_pca.components_[:, index]**2))
+            # print(f"mean_old: {mean_old:.2f} mean_new: {mean_new:.2f}")
+            # print(f"std_old: {std_old:.2f} std_new: {std_new:.2f}")
+        # Store the old and new moments of PCEs
+        results = {
+            'mean_old': mean_old,
+            'mean_new': mean_new,
+            'std_old': std_old,
+            'std_new': std_new
+            }
+        return results
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign_old(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        Model = self.Model
+        MetaModel = deepcopy(self.MetaModel)
+        old_EDY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels using the candidate design
+        Y_PC_can, Y_std_can = self.MetaModel.eval_metamodel(
+            samples=np.array([X_can])
+            )
+
+        # Generate y from posterior predictive
+        m_size = 100
+        y_hat_samples = {}
+        for idx, key in enumerate(Model.Output.names):
+            means, stds = Y_PC_can[key][0], Y_std_can[key][0]
+            y_hat_samples[key] = np.random.multivariate_normal(
+                means, np.diag(stds), m_size)
+
+        # Create the SparseBayes-based PCE metamodel:
+        MetaModel.input_obj.poly_coeffs_flag = False
+        univ_p_val = self.MetaModel.univ_basis_vals(X_can)
+        G_n_m_all = np.zeros((m_size, len(Model.Output.names), Model.n_obs))
+
+        for i in range(m_size):
+            for idx, key in enumerate(Model.Output.names):
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    # Equal number of components
+                    new_outputs = np.vstack(
+                        (old_EDY[key], y_hat_samples[key][i])
+                        )
+                    new_pca, _ = MetaModel.pca_transformation(new_outputs)
+                    target = new_pca.transform(
+                        y_hat_samples[key][i].reshape(1, -1)
+                        )[0]
+                else:
+                    new_pca, target = False, y_hat_samples[key][i]
+
+                for j in range(len(target)):
+
+                    # Update surrogate
+                    result = self.update_metamodel(
+                        MetaModel, key, target[j], univ_p_val, j, new_pca)
+
+                    # Compute Expected Information Gain (Eq. 39)
+                    G_n_m = np.log(result['std_old']/result['std_new']) - 1./2
+                    G_n_m += result['std_new']**2 / (2*result['std_old']**2)
+                    G_n_m += (result['mean_new'] - result['mean_old'])**2 /\
+                        (2*result['std_old']**2)
+
+                    G_n_m_all[i, idx, j] = G_n_m
+
+        U_J_d = G_n_m_all.mean(axis=(1, 2)).mean()
+        return -1 * U_J_d
+
+    # -------------------------------------------------------------------------
+    def util_BayesianDesign(self, X_can, X_MC, sigma2Dict, var='DKL'):
+        """
+        Computes scores based on Bayesian sequential design criterion (var).
+
+        Parameters
+        ----------
+        X_can : array of shape (n_samples, n_params)
+            Candidate samples.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        var : string, optional
+            Bayesian design criterion. The default is 'DKL'.
+
+        Returns
+        -------
+        float
+            Score.
+
+        """
+
+        # To avoid changes ub original aPCE object
+        Model = self.Model
+        MetaModel = deepcopy(self.MetaModel)
+        out_names = MetaModel.ModelObj.Output.names
+        if X_can.ndim == 1:
+            X_can = X_can.reshape(1, -1)
+
+        # Compute the mean and std based on the MetaModel
+        # pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+        if var == 'ALC':
+            Y_MC, Y_MC_std = MetaModel.eval_metamodel(samples=X_MC)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+        oldExpDesignY = MetaModel.ExpDesign.Y
+
+        # Evaluate the PCE metamodels at that location ???
+        Y_PC_can, Y_std_can = MetaModel.eval_metamodel(samples=X_can)
+
+        # Add all suggestion as new ExpDesign
+        NewExpDesignX = np.vstack((oldExpDesignX, X_can))
+
+        NewExpDesignY = {}
+        for key in oldExpDesignY.keys():
+            try:
+                NewExpDesignY[key] = np.vstack((oldExpDesignY[key],
+                                                Y_PC_can[key]))
+            except:
+                NewExpDesignY[key] = oldExpDesignY[key]
+
+        MetaModel.ExpDesign.sampling_method = 'user'
+        MetaModel.ExpDesign.X = NewExpDesignX
+        MetaModel.ExpDesign.Y = NewExpDesignY
+
+        # Train the model for the observed data using x_can
+        MetaModel.input_obj.poly_coeffs_flag = False
+        MetaModel.train_norm_design(parallel=False)
+        PCE_Model_can = MetaModel
+
+        if var.lower() == 'mi':
+            # Mutual information based on Krause et al
+            # Adapted from Beck & Guillas (MICE) paper
+            _, std_PC_can = PCE_Model_can.eval_metamodel(samples=X_can)
+            std_can = {key: std_PC_can[key] for key in out_names}
+
+            std_old = {key: Y_std_can[key] for key in out_names}
+
+            varPCE = np.zeros((len(out_names)))
+            for i, key in enumerate(out_names):
+                varPCE[i] = np.mean(std_old[key]**2/std_can[key]**2)
+            score = np.mean(varPCE)
+
+            return -1 * score
+
+        elif var.lower() == 'alc':
+            # Active learning based on Gramyc and Lee
+            # Adaptive design and analysis of supercomputer experiments Techno-
+            # metrics, 51 (2009), pp. 130–145.
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC_can, Y_MC_std_can = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Compute the score
+            score = []
+            for i, key in enumerate(out_names):
+                pce_var = Y_MC_std_can[key]**2
+                pce_var_can = Y_MC_std[key]**2
+                score.append(np.mean(pce_var-pce_var_can, axis=0))
+            score = np.mean(score)
+
+            return -1 * score
+
+        # ---------- Inner MC simulation for computing Utility Value ----------
+        # Estimation of the integral via Monte Varlo integration
+        MCsize = X_MC.shape[0]
+        ESS = 0
+
+        while ((ESS > MCsize) or (ESS < 1)):
+
+            # Enriching Monte Carlo samples if need be
+            if ESS != 0:
+                X_MC = self.MetaModel.ExpDesign.generate_samples(
+                    MCsize, 'random'
+                    )
+
+            # Evaluate the MetaModel at the given samples
+            Y_MC, std_MC = PCE_Model_can.eval_metamodel(samples=X_MC)
+
+            # Likelihood computation (Comparison of data and simulation
+            # results via PCE with candidate design)
+            likelihoods = self.__normpdf(
+                Y_MC, std_MC, self.observations, sigma2Dict
+                )
+
+            # Check the Effective Sample Size (1<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(likelihoods/np.sum(likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if ((ESS > MCsize) or (ESS < 1)):
+                print("--- increasing MC size---")
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (likelihoods/np.max(likelihoods)) >= unif
+
+        # -------------------- Utility functions --------------------
+        # Utility function Eq.2 in Ref. (2)
+        # Kullback-Leibler Divergence (Sergey's paper)
+        if var == 'DKL':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods, dtype=np.float128))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted]
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Haun et al implementation
+            U_J_d = np.mean(np.log(likelihoods[likelihoods != 0]) - logBME)
+
+            # U_J_d = np.sum(G_n_m_all)
+            # Ryan et al (2014) implementation
+            # importanceWeights = Likelihoods[Likelihoods!=0]/np.sum(Likelihoods[Likelihoods!=0])
+            # U_J_d = np.mean(importanceWeights*np.log(Likelihoods[Likelihoods!=0])) - logBME
+
+            # U_J_d = postExpLikelihoods - logBME
+
+        # Marginal likelihood
+        elif var == 'BME':
+
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+            U_J_d = logBME
+
+        # Bayes risk likelihood
+        elif var == 'BayesRisk':
+
+            U_J_d = -1 * np.var(likelihoods)
+
+        # Entropy-based information gain
+        elif var == 'infEntropy':
+            # Prior-based estimation of BME
+            logBME = np.log(np.nanmean(likelihoods))
+
+            # Posterior-based expectation of likelihoods
+            postLikelihoods = likelihoods[accepted] / np.nansum(likelihoods[accepted])
+            postExpLikelihoods = np.mean(np.log(postLikelihoods))
+
+            # Posterior-based expectation of prior densities
+            postExpPrior = np.mean(logPriorLikelihoods[accepted])
+
+            infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+            U_J_d = infEntropy * -1  # -1 for minimization
+
+        # D-Posterior-precision
+        elif var == 'DPP':
+            X_Posterior = X_MC[accepted]
+            # covariance of the posterior parameters
+            U_J_d = -np.log(np.linalg.det(np.cov(X_Posterior)))
+
+        # A-Posterior-precision
+        elif var == 'APP':
+            X_Posterior = X_MC[accepted]
+            # trace of the posterior parameters
+            U_J_d = -np.log(np.trace(np.cov(X_Posterior)))
+
+        else:
+            print('The algorithm you requested has not been implemented yet!')
+
+        # Clear memory
+        del likelihoods
+        del Y_MC
+        del std_MC
+        gc.collect(generation=2)
+
+        return -1 * U_J_d   # -1 is for minimization instead of maximization
+
+    # -------------------------------------------------------------------------
+    def subdomain(self, Bounds, n_new_samples):
+        """
+        Divides a domain defined by Bounds into sub domains.
+
+        Parameters
+        ----------
+        Bounds : list of tuples
+            List of lower and upper bounds.
+        n_new_samples : TYPE
+            DESCRIPTION.
+
+        Returns
+        -------
+        Subdomains : TYPE
+            DESCRIPTION.
+
+        """
+        n_params = self.MetaModel.n_params
+        n_subdomains = n_new_samples + 1
+        LinSpace = np.zeros((n_params, n_subdomains))
+
+        for i in range(n_params):
+            LinSpace[i] = np.linspace(start=Bounds[i][0], stop=Bounds[i][1],
+                                      num=n_subdomains)
+        Subdomains = []
+        for k in range(n_subdomains-1):
+            mylist = []
+            for i in range(n_params):
+                mylist.append((LinSpace[i, k+0], LinSpace[i, k+1]))
+            Subdomains.append(tuple(mylist))
+
+        return Subdomains
+
+    # -------------------------------------------------------------------------
+    def run_util_func(self, method, candidates, index, sigma2Dict=None,
+                      var=None, X_MC=None):
+        """
+        Runs the utility function based on the given method.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        candidates : array of shape (n_samples, n_params)
+            All candidate parameter sets.
+        index : int
+            ExpDesign index.
+        sigma2Dict : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        var : string, optional
+            Utility function. The default is None.
+        X_MC : TYPE, optional
+            DESCRIPTION. The default is None.
+
+        Returns
+        -------
+        index : TYPE
+            DESCRIPTION.
+        List
+            Scores.
+
+        """
+
+        if method.lower() == 'varoptdesign':
+            # U_J_d = self.util_VarBasedDesign(candidates, index, var)
+            U_J_d = np.zeros((candidates.shape[0]))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="varoptdesign"):
+                U_J_d[idx] = self.util_VarBasedDesign(X_can, index, var)
+
+        elif method.lower() == 'bayesactdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianActiveDesign(X_can, sigma2Dict,
+                                                            var)
+        elif method.lower() == 'bayesoptdesign':
+            NCandidate = candidates.shape[0]
+            U_J_d = np.zeros((NCandidate))
+            for idx, X_can in tqdm(enumerate(candidates), ascii=True,
+                                   desc="OptBayesianDesign"):
+                U_J_d[idx] = self.util_BayesianDesign(X_can, X_MC, sigma2Dict,
+                                                      var)
+        return (index, -1 * U_J_d)
+
+    # -------------------------------------------------------------------------
+    def dual_annealing(self, method, Bounds, sigma2Dict, var, Run_No,
+                       verbose=False):
+        """
+        Exploration algorithim to find the optimum parameter space.
+
+        Parameters
+        ----------
+        method : string
+            Exploitation method: `VarOptDesign`, `BayesActDesign` and
+            `BayesOptDesign`.
+        Bounds : list of tuples
+            List of lower and upper boundaries of parameters.
+        sigma2Dict : dict
+            A dictionary containing the measurement errors (sigma^2).
+        Run_No : int
+            Run number.
+        verbose : bool, optional
+            Print out a summary. The default is False.
+
+        Returns
+        -------
+        Run_No : int
+            Run number.
+        array
+            Optimial candidate.
+
+        """
+
+        Model = self.Model
+        max_func_itr = self.MetaModel.ExpDesign.max_func_itr
+
+        if method == 'VarOptDesign':
+            Res_Global = opt.dual_annealing(self.util_VarBasedDesign,
+                                            bounds=Bounds,
+                                            args=(Model, var),
+                                            maxfun=max_func_itr)
+
+        elif method == 'BayesOptDesign':
+            Res_Global = opt.dual_annealing(self.util_BayesianDesign,
+                                            bounds=Bounds,
+                                            args=(Model, sigma2Dict, var),
+                                            maxfun=max_func_itr)
+
+        if verbose:
+            print(f"global minimum: xmin = {Res_Global.x}, "
+                  f"f(xmin) = {Res_Global.fun:.6f}, nfev = {Res_Global.nfev}")
+
+        return (Run_No, Res_Global.x)
+
+    # -------------------------------------------------------------------------
+    def tradoff_weights(self, tradeoff_scheme, old_EDX, old_EDY):
+        """
+        Calculates weights for exploration scores based on the requested
+        scheme: `None`, `equal`, `epsilon-decreasing` and `adaptive`.
+
+        `None`: No exploration.
+        `equal`: Same weights for exploration and exploitation scores.
+        `epsilon-decreasing`: Start with more exploration and increase the
+            influence of exploitation along the way with a exponential decay
+            function
+        `adaptive`: An adaptive method based on:
+            Liu, Haitao, Jianfei Cai, and Yew-Soon Ong. "An adaptive sampling
+            approach for Kriging metamodeling by maximizing expected prediction
+            error." Computers & Chemical Engineering 106 (2017): 171-182.
+
+        Parameters
+        ----------
+        tradeoff_scheme : string
+            Trade-off scheme for exloration and exploitation scores.
+        old_EDX : array (n_samples, n_params)
+            Old experimental design (training points).
+        old_EDY : dict
+            Old model responses (targets).
+
+        Returns
+        -------
+        exploration_weight : float
+            Exploration weight.
+        exploitation_weight: float
+            Exploitation weight.
+
+        """
+        if tradeoff_scheme is None:
+            exploration_weight = 0
+
+        elif tradeoff_scheme == 'equal':
+            exploration_weight = 0.5
+
+        elif tradeoff_scheme == 'epsilon-decreasing':
+            # epsilon-decreasing scheme
+            # Start with more exploration and increase the influence of
+            # exploitation along the way with a exponential decay function
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            tau2 = -(n_max_samples-initNSamples-1) / np.log(1e-8)
+            exploration_weight = signal.exponential(n_max_samples-initNSamples,
+                                                    0, tau2, False)[itrNumber]
+
+        elif tradeoff_scheme == 'adaptive':
+
+            # Extract itrNumber
+            initNSamples = self.MetaModel.ExpDesign.n_init_samples
+            n_max_samples = self.MetaModel.ExpDesign.n_max_samples
+            itrNumber = (self.MetaModel.ExpDesign.X.shape[0] - initNSamples)
+            itrNumber //= self.MetaModel.ExpDesign.n_new_samples
+
+            if itrNumber == 0:
+                exploration_weight = 0.5
+            else:
+                # New adaptive trade-off according to Liu et al. (2017)
+                # Mean squared error for last design point
+                last_EDX = old_EDX[-1].reshape(1, -1)
+                lastPCEY, _ = self.MetaModel.eval_metamodel(samples=last_EDX)
+                pce_y = np.array(list(lastPCEY.values()))[:, 0]
+                y = np.array(list(old_EDY.values()))[:, -1, :]
+                mseError = mean_squared_error(pce_y, y)
+
+                # Mean squared CV - error for last design point
+                pce_y_prev = np.array(list(self._y_hat_prev.values()))[:, 0]
+                mseCVError = mean_squared_error(pce_y_prev, y)
+
+                exploration_weight = min([0.5*mseError/mseCVError, 1])
+
+        # Exploitation weight
+        exploitation_weight = 1 - exploration_weight
+
+        return exploration_weight, exploitation_weight
+
+    # -------------------------------------------------------------------------
+    def opt_SeqDesign(self, sigma2, n_candidates=5, var='DKL'):
+        """
+        Runs optimal sequential design.
+
+        Parameters
+        ----------
+        sigma2 : dict, optional
+            A dictionary containing the measurement errors (sigma^2). The
+            default is None.
+        n_candidates : int, optional
+            Number of candidate samples. The default is 5.
+        var : string, optional
+            Utility function. The default is None.
+
+        Raises
+        ------
+        NameError
+            Wrong utility function.
+
+        Returns
+        -------
+        Xnew : array (n_samples, n_params)
+            Selected new training point(s).
+        """
+
+        # Initialization
+        MetaModel = self.MetaModel
+        Bounds = MetaModel.bound_tuples
+        n_new_samples = MetaModel.ExpDesign.n_new_samples
+        explore_method = MetaModel.ExpDesign.explore_method
+        exploit_method = MetaModel.ExpDesign.exploit_method
+        n_cand_groups = MetaModel.ExpDesign.n_cand_groups
+        tradeoff_scheme = MetaModel.ExpDesign.tradeoff_scheme
+
+        old_EDX = MetaModel.ExpDesign.X
+        old_EDY = MetaModel.ExpDesign.Y.copy()
+        ndim = MetaModel.ExpDesign.X.shape[1]
+        OutputNames = MetaModel.ModelObj.Output.names
+
+        # -----------------------------------------
+        # ----------- CUSTOMIZED METHODS ----------
+        # -----------------------------------------
+        # Utility function exploit_method provided by user
+        if exploit_method.lower() == 'user':
+
+            Xnew, filteredSamples = MetaModel.ExpDesign.ExploitFunction(self)
+
+            print("\n")
+            print("\nXnew:\n", Xnew)
+
+            return Xnew, filteredSamples
+
+        # -----------------------------------------
+        # ---------- EXPLORATION METHODS ----------
+        # -----------------------------------------
+        if explore_method == 'dual annealing':
+            # ------- EXPLORATION: OPTIMIZATION -------
+            import time
+            start_time = time.time()
+
+            # Divide the domain to subdomains
+            args = []
+            subdomains = self.subdomain(Bounds, n_new_samples)
+            for i in range(n_new_samples):
+                args.append((exploit_method, subdomains[i], sigma2, var, i))
+
+            # Multiprocessing
+            pool = multiprocessing.Pool(multiprocessing.cpu_count())
+
+            # With Pool.starmap_async()
+            results = pool.starmap_async(self.dual_annealing, args).get()
+
+            # Close the pool
+            pool.close()
+
+            Xnew = np.array([results[i][1] for i in range(n_new_samples)])
+
+            print("\nXnew:\n", Xnew)
+
+            elapsed_time = time.time() - start_time
+            print("\n")
+            print(f"elapsed_time: {round(elapsed_time,2)} sec.")
+            print('-'*20)
+
+        elif explore_method == 'LOOCV':
+            # -----------------------------------------------------------------
+            # TODO: LOOCV model construnction based on Feng et al. (2020)
+            # 'LOOCV':
+            # Initilize the ExploitScore array
+
+            # Generate random samples
+            allCandidates = MetaModel.ExpDesign.generate_samples(n_candidates,
+                                                                'random')
+
+            # Construct error model based on LCerror
+            errorModel = MetaModel.create_ModelError(old_EDX, self.LCerror)
+            self.errorModel.append(copy(errorModel))
+
+            # Evaluate the error models for allCandidates
+            eLCAllCands, _ = errorModel.eval_errormodel(allCandidates)
+            # Select the maximum as the representative error
+            eLCAllCands = np.dstack(eLCAllCands.values())
+            eLCAllCandidates = np.max(eLCAllCands, axis=1)[:, 0]
+
+            # Normalize the error w.r.t the maximum error
+            scoreExploration = eLCAllCandidates / np.sum(eLCAllCandidates)
+
+        else:
+            # ------- EXPLORATION: SPACE-FILLING DESIGN -------
+            # Generate candidate samples from Exploration class
+            explore = Exploration(MetaModel, n_candidates)
+            explore.w = 100  # * ndim #500
+            # Select criterion (mc-intersite-proj-th, mc-intersite-proj)
+            explore.mc_criterion = 'mc-intersite-proj'
+            allCandidates, scoreExploration = explore.get_exploration_samples()
+
+            # Temp: ---- Plot all candidates -----
+            if ndim == 2:
+                def plotter(points, allCandidates, Method,
+                            scoreExploration=None):
+                    if Method == 'Voronoi':
+                        from scipy.spatial import Voronoi, voronoi_plot_2d
+                        vor = Voronoi(points)
+                        fig = voronoi_plot_2d(vor)
+                        ax1 = fig.axes[0]
+                    else:
+                        fig = plt.figure()
+                        ax1 = fig.add_subplot(111)
+                    ax1.scatter(points[:, 0], points[:, 1], s=10, c='r',
+                                marker="s", label='Old Design Points')
+                    ax1.scatter(allCandidates[:, 0], allCandidates[:, 1], s=10,
+                                c='b', marker="o", label='Design candidates')
+                    for i in range(points.shape[0]):
+                        txt = 'p'+str(i+1)
+                        ax1.annotate(txt, (points[i, 0], points[i, 1]))
+                    if scoreExploration is not None:
+                        for i in range(allCandidates.shape[0]):
+                            txt = str(round(scoreExploration[i], 5))
+                            ax1.annotate(txt, (allCandidates[i, 0],
+                                               allCandidates[i, 1]))
+
+                    plt.xlim(self.bound_tuples[0])
+                    plt.ylim(self.bound_tuples[1])
+                    # plt.show()
+                    plt.legend(loc='upper left')
+
+        # -----------------------------------------
+        # --------- EXPLOITATION METHODS ----------
+        # -----------------------------------------
+        if exploit_method == 'BayesOptDesign' or\
+           exploit_method == 'BayesActDesign':
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\n Exploration weight={explore_w:0.3f} "
+                  f"Exploitation weight={exploit_w:0.3f}\n")
+
+            # ------- EXPLOITATION: BayesOptDesign & ActiveLearning -------
+            if explore_w != 1.0:
+
+                # Create a sample pool for rejection sampling
+                MCsize = 15000
+                X_MC = MetaModel.ExpDesign.generate_samples(MCsize, 'random')
+                candidates = MetaModel.ExpDesign.generate_samples(
+                    MetaModel.ExpDesign.max_func_itr, 'latin_hypercube')
+
+                # Split the candidates in groups for multiprocessing
+                split_cand = np.array_split(
+                    candidates, n_cand_groups, axis=0
+                    )
+
+                results = Parallel(n_jobs=-1, backend='threading')(
+                        delayed(self.run_util_func)(
+                            exploit_method, split_cand[i], i, sigma2, var, X_MC)
+                        for i in range(n_cand_groups))
+                # out = map(self.run_util_func,
+                #           [exploit_method]*n_cand_groups,
+                #           split_cand,
+                #           range(n_cand_groups),
+                #           [sigma2] * n_cand_groups,
+                #           [var] * n_cand_groups,
+                #           [X_MC] * n_cand_groups
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                U_J_d = np.concatenate([results[NofE][1] for NofE in
+                                        range(n_cand_groups)])
+
+                # Check if all scores are inf
+                if np.isinf(U_J_d).all() or np.isnan(U_J_d).all():
+                    U_J_d = np.ones(len(U_J_d))
+
+                # Get the expected value (mean) of the Utility score
+                # for each cell
+                if explore_method == 'Voronoi':
+                    U_J_d = np.mean(U_J_d.reshape(-1, n_candidates), axis=1)
+
+                # create surrogate model for U_J_d
+                from sklearn.preprocessing import MinMaxScaler
+                # Take care of inf entries
+                good_indices = [i for i, arr in enumerate(U_J_d)
+                                if np.isfinite(arr).all()]
+                scaler = MinMaxScaler()
+                X_S = scaler.fit_transform(candidates[good_indices])
+                gp = MetaModel.gaussian_process_emulator(
+                    X_S, U_J_d[good_indices], autoSelect=True
+                    )
+                U_J_d = gp.predict(scaler.transform(allCandidates))
+
+                # Normalize U_J_d
+                norm_U_J_d = U_J_d / np.sum(U_J_d)
+                print("norm_U_J_d:\n", norm_U_J_d)
+            else:
+                norm_U_J_d = np.zeros((len(scoreExploration)))
+
+            # ------- Calculate Total score -------
+            # ------- Trade off between EXPLORATION & EXPLOITATION -------
+            # Total score
+            totalScore = exploit_w * norm_U_J_d
+            totalScore += explore_w * scoreExploration
+
+            # temp: Plot
+            # dim = self.ExpDesign.X.shape[1]
+            # if dim == 2:
+            #     plotter(self.ExpDesign.X, allCandidates, explore_method)
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            # select the requested number of samples
+            if explore_method == 'Voronoi':
+                Xnew = np.zeros((n_new_samples, ndim))
+                for i, idx in enumerate(bestIdx):
+                    X_can = explore.closestPoints[idx]
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+            else:
+                Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        elif exploit_method == 'VarOptDesign':
+            # ------- EXPLOITATION: VarOptDesign -------
+            UtilMethod = var
+
+            # ------- Calculate Exoploration weight -------
+            # Compute exploration weight based on trade off scheme
+            explore_w, exploit_w = self.tradoff_weights(tradeoff_scheme,
+                                                        old_EDX,
+                                                        old_EDY)
+            print(f"\nweightExploration={explore_w:0.3f} "
+                  f"weightExploitation={exploit_w:0.3f}")
+
+            # Generate candidate samples from Exploration class
+            nMeasurement = old_EDY[OutputNames[0]].shape[1]
+
+            # Find sensitive region
+            if UtilMethod == 'LOOCV':
+                LCerror = MetaModel.LCerror
+                allModifiedLOO = np.zeros((len(old_EDX), len(OutputNames),
+                                           nMeasurement))
+                for y_idx, y_key in enumerate(OutputNames):
+                    for idx, key in enumerate(LCerror[y_key].keys()):
+                        allModifiedLOO[:, y_idx, idx] = abs(
+                            LCerror[y_key][key])
+
+                ExploitScore = np.max(np.max(allModifiedLOO, axis=1), axis=1)
+
+            elif UtilMethod in ['EIGF', 'ALM']:
+                # ----- All other in  ['EIGF', 'ALM'] -----
+                # Initilize the ExploitScore array
+                ExploitScore = np.zeros((len(old_EDX), len(OutputNames)))
+
+                # Split the candidates in groups for multiprocessing
+                if explore_method != 'Voronoi':
+                    split_cand = np.array_split(allCandidates,
+                                                n_cand_groups,
+                                                axis=0)
+                    goodSampleIdx = range(n_cand_groups)
+                else:
+                    # Find indices of the Vornoi cells with samples
+                    goodSampleIdx = []
+                    for idx in range(len(explore.closest_points)):
+                        if len(explore.closest_points[idx]) != 0:
+                            goodSampleIdx.append(idx)
+                    split_cand = explore.closest_points
+
+                # Split the candidates in groups for multiprocessing
+                args = []
+                for index in goodSampleIdx:
+                    args.append((exploit_method, split_cand[index], index,
+                                 sigma2, var))
+
+                # Multiprocessing
+                pool = multiprocessing.Pool(multiprocessing.cpu_count())
+                # With Pool.starmap_async()
+                results = pool.starmap_async(self.run_util_func, args).get()
+
+                # Close the pool
+                pool.close()
+                # out = map(self.run_util_func,
+                #           [exploit_method]*len(goodSampleIdx),
+                #           split_cand,
+                #           range(len(goodSampleIdx)),
+                #           [sigma2] * len(goodSampleIdx),
+                #           [var] * len(goodSampleIdx)
+                #           )
+                # results = list(out)
+
+                # Retrieve the results and append them
+                if explore_method == 'Voronoi':
+                    ExploitScore = [np.mean(results[k][1]) for k in
+                                    range(len(goodSampleIdx))]
+                else:
+                    ExploitScore = np.concatenate(
+                        [results[k][1] for k in range(len(goodSampleIdx))])
+
+            else:
+                raise NameError('The requested utility function is not '
+                                'available.')
+
+            # print("ExploitScore:\n", ExploitScore)
+
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            # Total score
+            # Normalize U_J_d
+            ExploitScore = ExploitScore / np.sum(ExploitScore)
+            totalScore = exploit_w * ExploitScore
+            totalScore += explore_w * scoreExploration
+
+            temp = totalScore.copy()
+            sorted_idxtotalScore = np.argsort(temp, axis=0)[::-1]
+            bestIdx = sorted_idxtotalScore[:n_new_samples]
+
+            Xnew = np.zeros((n_new_samples, ndim))
+            if explore_method != 'Voronoi':
+                Xnew = allCandidates[bestIdx]
+            else:
+                for i, idx in enumerate(bestIdx.flatten()):
+                    X_can = explore.closest_points[idx]
+                    # plotter(self.ExpDesign.X, X_can, explore_method,
+                    # scoreExploration=None)
+
+                    # Calculate the maxmin score for the region of interest
+                    newSamples, maxminScore = explore.get_mc_samples(X_can)
+
+                    # select the requested number of samples
+                    Xnew[i] = newSamples[np.argmax(maxminScore)]
+
+        elif exploit_method == 'alphabetic':
+            # ------- EXPLOITATION: ALPHABETIC -------
+            Xnew = self.util_AlphOptDesign(allCandidates, var)
+
+        elif exploit_method == 'Space-filling':
+            # ------- EXPLOITATION: SPACE-FILLING -------
+            totalScore = scoreExploration
+
+            # ------- Select the best candidate -------
+            # find an optimal point subset to add to the initial design by
+            # maximization of the utility score and taking care of NaN values
+            temp = totalScore.copy()
+            temp[np.isnan(totalScore)] = -np.inf
+            sorted_idxtotalScore = np.argsort(temp)[::-1]
+
+            # select the requested number of samples
+            Xnew = allCandidates[sorted_idxtotalScore[:n_new_samples]]
+
+        else:
+            raise NameError('The requested design method is not available.')
+
+        print("\n")
+        print("\nRun No. {}:".format(old_EDX.shape[0]+1))
+        print("Xnew:\n", Xnew)
+        gc.collect()
+
+        return Xnew, None
+
+    # -------------------------------------------------------------------------
+    def util_AlphOptDesign(self, candidates, var='D-Opt'):
+        """
+        Enriches the Experimental design with the requested alphabetic
+        criterion based on exploring the space with number of sampling points.
+
+        Ref: Hadigol, M., & Doostan, A. (2018). Least squares polynomial chaos
+        expansion: A review of sampling strategies., Computer Methods in
+        Applied Mechanics and Engineering, 332, 382-407.
+
+        Arguments
+        ---------
+        NCandidate : int
+            Number of candidate points to be searched
+
+        var : string
+            Alphabetic optimality criterion
+
+        Returns
+        -------
+        X_new : array of shape (1, n_params)
+            The new sampling location in the input space.
+        """
+        MetaModelOrig = self
+        Model = self.Model
+        n_new_samples = MetaModelOrig.ExpDesign.n_new_samples
+        NCandidate = candidates.shape[0]
+
+        # TODO: Loop over outputs
+        OutputName = Model.Output.names[0]
+
+        # To avoid changes ub original aPCE object
+        MetaModel = deepcopy(MetaModelOrig)
+
+        # Old Experimental design
+        oldExpDesignX = MetaModel.ExpDesign.X
+
+        # TODO: Only one psi can be selected.
+        # Suggestion: Go for the one with the highest LOO error
+        Scores = list(MetaModel.score_dict[OutputName].values())
+        ModifiedLOO = [1-score for score in Scores]
+        outIdx = np.argmax(ModifiedLOO)
+
+        # Initialize Phi to save the criterion's values
+        Phi = np.zeros((NCandidate))
+
+        BasisIndices = MetaModelOrig.basis_dict[OutputName]["y_"+str(outIdx+1)]
+        P = len(BasisIndices)
+
+        # ------ Old Psi ------------
+        univ_p_val = MetaModelOrig.univ_basis_vals(oldExpDesignX)
+        Psi = MetaModelOrig.create_psi(BasisIndices, univ_p_val)
+
+        # ------ New candidates (Psi_c) ------------
+        # Assemble Psi_c
+        univ_p_val_c = self.univ_basis_vals(candidates)
+        Psi_c = self.create_psi(BasisIndices, univ_p_val_c)
+
+        for idx in range(NCandidate):
+
+            # Include the new row to the original Psi
+            Psi_cand = np.vstack((Psi, Psi_c[idx]))
+
+            # Information matrix
+            PsiTPsi = np.dot(Psi_cand.T, Psi_cand)
+            M = PsiTPsi / (len(oldExpDesignX)+1)
+
+            if np.linalg.cond(PsiTPsi) > 1e-12 \
+               and np.linalg.cond(PsiTPsi) < 1 / sys.float_info.epsilon:
+                # faster
+                invM = linalg.solve(M, sparse.eye(PsiTPsi.shape[0]).toarray())
+            else:
+                # stabler
+                invM = np.linalg.pinv(M)
+
+            # ---------- Calculate optimality criterion ----------
+            # Optimality criteria according to Section 4.5.1 in Ref.
+
+            # D-Opt
+            if var == 'D-Opt':
+                Phi[idx] = (np.linalg.det(invM)) ** (1/P)
+
+            # A-Opt
+            elif var == 'A-Opt':
+                Phi[idx] = np.trace(invM)
+
+            # K-Opt
+            elif var == 'K-Opt':
+                Phi[idx] = np.linalg.cond(M)
+
+            else:
+                raise Exception('The optimality criterion you requested has '
+                      'not been implemented yet!')
+
+        # find an optimal point subset to add to the initial design
+        # by minimization of the Phi
+        sorted_idxtotalScore = np.argsort(Phi)
+
+        # select the requested number of samples
+        Xnew = candidates[sorted_idxtotalScore[:n_new_samples]]
+
+        return Xnew
+
+    # -------------------------------------------------------------------------
+    def __normpdf(self, y_hat_pce, std_pce, obs_data, total_sigma2s,
+                  rmse=None):
+
+        Model = self.Model
+        likelihoods = 1.0
+
+        # Loop over the outputs
+        for idx, out in enumerate(Model.Output.names):
+
+            # (Meta)Model Output
+            nsamples, nout = y_hat_pce[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout].values
+
+            # Surrogate error if valid dataset is given.
+            if rmse is not None:
+                tot_sigma2s += rmse[out]**2
+
+            likelihoods *= stats.multivariate_normal.pdf(
+                y_hat_pce[out], data, np.diag(tot_sigma2s),
+                allow_singular=True)
+        self.Likelihoods = likelihoods
+
+        return likelihoods
+
+    # -------------------------------------------------------------------------
+    def __corr_factor_BME(self, obs_data, total_sigma2s, logBME):
+        """
+        Calculates the correction factor for BMEs.
+        """
+        MetaModel = self.MetaModel
+        samples = MetaModel.ExpDesign.X  # valid_samples
+        model_outputs = MetaModel.ExpDesign.Y  # valid_model_runs
+        Model = MetaModel.ModelObj
+        n_samples = samples.shape[0]
+
+        # Extract the requested model outputs for likelihood calulation
+        output_names = Model.Output.names
+
+        # TODO: Evaluate MetaModel on the experimental design and ValidSet
+        OutputRS, stdOutputRS = MetaModel.eval_metamodel(samples=samples)
+
+        logLik_data = np.zeros((n_samples))
+        logLik_model = np.zeros((n_samples))
+        # Loop over the outputs
+        for idx, out in enumerate(output_names):
+
+            # (Meta)Model Output
+            nsamples, nout = model_outputs[out].shape
+
+            # Prepare data and remove NaN
+            try:
+                data = obs_data[out].values[~np.isnan(obs_data[out])]
+            except AttributeError:
+                data = obs_data[out][~np.isnan(obs_data[out])]
+
+            # Prepare sigma2s
+            non_nan_indices = ~np.isnan(total_sigma2s[out])
+            tot_sigma2s = total_sigma2s[out][non_nan_indices][:nout]
+
+            # Covariance Matrix
+            covMatrix_data = np.diag(tot_sigma2s)
+
+            for i, sample in enumerate(samples):
+
+                # Simulation run
+                y_m = model_outputs[out][i]
+
+                # Surrogate prediction
+                y_m_hat = OutputRS[out][i]
+
+                # CovMatrix with the surrogate error
+                # covMatrix = np.diag(stdOutputRS[out][i]**2)
+                covMatrix = np.diag((y_m-y_m_hat)**2)
+                covMatrix = np.diag(
+                    np.mean((model_outputs[out]-OutputRS[out]), axis=0)**2
+                    )
+
+                # Compute likelilhood output vs data
+                logLik_data[i] += self.__logpdf(
+                    y_m_hat, data, covMatrix_data
+                    )
+
+                # Compute likelilhood output vs surrogate
+                logLik_model[i] += self.__logpdf(y_m_hat, y_m, covMatrix)
+
+        # Weight
+        logLik_data -= logBME
+        weights = np.exp(logLik_model+logLik_data)
+
+        return np.log(np.mean(weights))
+
+    # -------------------------------------------------------------------------
+    def __logpdf(self, x, mean, cov):
+        """
+        computes the likelihood based on a multivariate normal distribution.
+
+        Parameters
+        ----------
+        x : TYPE
+            DESCRIPTION.
+        mean : array_like
+            Observation data.
+        cov : 2d array
+            Covariance matrix of the distribution.
+
+        Returns
+        -------
+        log_lik : float
+            Log likelihood.
+
+        """
+        n = len(mean)
+        L = linalg.cholesky(cov, lower=True)
+        beta = np.sum(np.log(np.diag(L)))
+        dev = x - mean
+        alpha = dev.dot(linalg.cho_solve((L, True), dev))
+        log_lik = -0.5 * alpha - beta - n / 2. * np.log(2 * np.pi)
+
+        return log_lik
+
+    # -------------------------------------------------------------------------
+    def __posteriorPlot(self, posterior, par_names, key):
+
+        # Initialization
+        newpath = (r'Outputs_SeqPosteriorComparison/posterior')
+        os.makedirs(newpath, exist_ok=True)
+
+        bound_tuples = self.MetaModel.bound_tuples
+        n_params = len(par_names)
+        font_size = 40
+        if n_params == 2:
+
+            figPosterior, ax = plt.subplots(figsize=(15, 15))
+
+            sns.kdeplot(x=posterior[:, 0], y=posterior[:, 1],
+                        fill=True, ax=ax, cmap=plt.cm.jet,
+                        clip=bound_tuples)
+            # Axis labels
+            plt.xlabel(par_names[0], fontsize=font_size)
+            plt.ylabel(par_names[1], fontsize=font_size)
+
+            # Set axis limit
+            plt.xlim(bound_tuples[0])
+            plt.ylim(bound_tuples[1])
+
+            # Increase font size
+            plt.xticks(fontsize=font_size)
+            plt.yticks(fontsize=font_size)
+
+            # Switch off the grids
+            plt.grid(False)
+
+        else:
+            import corner
+            figPosterior = corner.corner(posterior, labels=par_names,
+                                         title_fmt='.2e', show_titles=True,
+                                         title_kwargs={"fontsize": 12})
+
+        figPosterior.savefig(f'./{newpath}/{key}.pdf', bbox_inches='tight')
+        plt.close()
+
+        # Save the posterior as .npy
+        np.save(f'./{newpath}/{key}.npy', posterior)
+
+        return figPosterior
+
+    # -------------------------------------------------------------------------
+    def __hellinger_distance(self, P, Q):
+        """
+        Hellinger distance between two continuous distributions.
+
+        The maximum distance 1 is achieved when P assigns probability zero to
+        every set to which Q assigns a positive probability, and vice versa.
+        0 (identical) and 1 (maximally different)
+
+        Parameters
+        ----------
+        P : array
+            Reference likelihood.
+        Q : array
+            Estimated likelihood.
+
+        Returns
+        -------
+        float
+            Hellinger distance of two distributions.
+
+        """
+        mu1 = P.mean()
+        Sigma1 = np.std(P)
+
+        mu2 = Q.mean()
+        Sigma2 = np.std(Q)
+
+        term1 = np.sqrt(2*Sigma1*Sigma2 / (Sigma1**2 + Sigma2**2))
+
+        term2 = np.exp(-.25 * (mu1 - mu2)**2 / (Sigma1**2 + Sigma2**2))
+
+        H_squared = 1 - term1 * term2
+
+        return np.sqrt(H_squared)
+
+    # -------------------------------------------------------------------------
+    def __BME_Calculator(self, MetaModel, obs_data, sigma2Dict, rmse=None):
+        """
+        This function computes the Bayesian model evidence (BME) via Monte
+        Carlo integration.
+
+        """
+        # Initializations
+        valid_likelihoods = MetaModel.valid_likelihoods
+
+        post_snapshot = MetaModel.ExpDesign.post_snapshot
+        if post_snapshot or len(valid_likelihoods) != 0:
+            newpath = (r'Outputs_SeqPosteriorComparison/likelihood_vs_ref')
+            os.makedirs(newpath, exist_ok=True)
+
+        SamplingMethod = 'random'
+        MCsize = 10000
+        ESS = 0
+
+        # Estimation of the integral via Monte Varlo integration
+        while (ESS > MCsize) or (ESS < 1):
+
+            # Generate samples for Monte Carlo simulation
+            X_MC = MetaModel.ExpDesign.generate_samples(
+                MCsize, SamplingMethod
+                )
+
+            # Monte Carlo simulation for the candidate design
+            m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+            Y_MC, std_MC = MetaModel.eval_metamodel(samples=X_MC)
+            m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+            print(f"\nMemory eval_metamodel in BME: {m_2-m_1:.2f} MB")
+
+            # Likelihood computation (Comparison of data and
+            # simulation results via PCE with candidate design)
+            Likelihoods = self.__normpdf(
+                Y_MC, std_MC, obs_data, sigma2Dict, rmse
+                )
+
+            # Check the Effective Sample Size (1000<ESS<MCsize)
+            ESS = 1 / np.sum(np.square(Likelihoods/np.sum(Likelihoods)))
+
+            # Enlarge sample size if it doesn't fulfill the criteria
+            if (ESS > MCsize) or (ESS < 1):
+                print(f'ESS={ESS} MC size should be larger.')
+                MCsize *= 10
+                ESS = 0
+
+        # Rejection Step
+        # Random numbers between 0 and 1
+        unif = np.random.rand(1, MCsize)[0]
+
+        # Reject the poorly performed prior
+        accepted = (Likelihoods/np.max(Likelihoods)) >= unif
+        X_Posterior = X_MC[accepted]
+
+        # ------------------------------------------------------------
+        # --- Kullback-Leibler Divergence & Information Entropy ------
+        # ------------------------------------------------------------
+        # Prior-based estimation of BME
+        logBME = np.log(np.nanmean(Likelihoods))
+
+        # TODO: Correction factor
+        # log_weight = self.__corr_factor_BME(obs_data, sigma2Dict, logBME)
+
+        # Posterior-based expectation of likelihoods
+        postExpLikelihoods = np.mean(np.log(Likelihoods[accepted]))
+
+        # Posterior-based expectation of prior densities
+        postExpPrior = np.mean(
+            np.log(MetaModel.ExpDesign.JDist.pdf(X_Posterior.T))
+            )
+
+        # Calculate Kullback-Leibler Divergence
+        # KLD = np.mean(np.log(Likelihoods[Likelihoods!=0])- logBME)
+        KLD = postExpLikelihoods - logBME
+
+        # Information Entropy based on Entropy paper Eq. 38
+        infEntropy = logBME - postExpPrior - postExpLikelihoods
+
+        # If post_snapshot is True, plot likelihood vs refrence
+        if post_snapshot or len(valid_likelihoods) != 0:
+            # Hellinger distance
+            ref_like = np.log(valid_likelihoods[valid_likelihoods > 0])
+            est_like = np.log(Likelihoods[Likelihoods > 0])
+            distHellinger = self.__hellinger_distance(ref_like, est_like)
+
+            idx = len([name for name in os.listdir(newpath) if 'Likelihoods_'
+                       in name and os.path.isfile(os.path.join(newpath, name))])
+            fig, ax = plt.subplots()
+            try:
+                sns.kdeplot(np.log(valid_likelihoods[valid_likelihoods > 0]),
+                            shade=True, color="g", label='Ref. Likelihood')
+                sns.kdeplot(np.log(Likelihoods[Likelihoods > 0]), shade=True,
+                            color="b", label='Likelihood with PCE')
+            except:
+                pass
+
+            text = f"Hellinger Dist.={distHellinger:.3f}\n logBME={logBME:.3f}"
+            "\n DKL={KLD:.3f}"
+
+            plt.text(0.05, 0.75, text, bbox=dict(facecolor='wheat',
+                                                 edgecolor='black',
+                                                 boxstyle='round,pad=1'),
+                     transform=ax.transAxes)
+
+            fig.savefig(f'./{newpath}/Likelihoods_{idx}.pdf',
+                        bbox_inches='tight')
+            plt.close()
+
+        else:
+            distHellinger = 0.0
+
+        # Bayesian inference with Emulator only for 2D problem
+        if post_snapshot and MetaModel.n_params == 2 and not idx % 5:
+            from bayes_inference.bayes_inference import BayesInference
+            from bayes_inference.discrepancy import Discrepancy
+            import pandas as pd
+            BayesOpts = BayesInference(MetaModel)
+            BayesOpts.emulator = True
+            BayesOpts.plot_post_pred = False
+
+            # Select the inference method
+            import emcee
+            BayesOpts.inference_method = "MCMC"
+            # Set the MCMC parameters passed to self.mcmc_params
+            BayesOpts.mcmc_params = {
+                'n_steps': 1e5,
+                'n_walkers': 30,
+                'moves': emcee.moves.KDEMove(),
+                'verbose': False
+                }
+
+            # ----- Define the discrepancy model -------
+            obs_data = pd.DataFrame(obs_data, columns=self.Model.Output.names)
+            BayesOpts.measurement_error = obs_data
+
+            # # -- (Option B) --
+            DiscrepancyOpts = Discrepancy('')
+            DiscrepancyOpts.type = 'Gaussian'
+            DiscrepancyOpts.parameters = obs_data**2
+            BayesOpts.Discrepancy = DiscrepancyOpts
+            # Start the calibration/inference
+            Bayes_PCE = BayesOpts.create_inference()
+            X_Posterior = Bayes_PCE.posterior_df.values
+
+        # Clean up
+        del Y_MC, std_MC
+        gc.collect()
+
+        return (logBME, KLD, X_Posterior, Likelihoods, distHellinger)
+
+    # -------------------------------------------------------------------------
+    def __validError(self, MetaModel):
+
+        # MetaModel = self.MetaModel
+        Model = MetaModel.ModelObj
+        OutputName = Model.Output.names
+
+        # Extract the original model with the generated samples
+        valid_samples = MetaModel.valid_samples
+        valid_model_runs = MetaModel.valid_model_runs
+
+        # Run the PCE model with the generated samples
+        m_1 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+        valid_PCE_runs, valid_PCE_std = MetaModel.eval_metamodel(samples=valid_samples)
+        m_2 = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss/1024
+        print(f"\nMemory eval_metamodel: {m_2-m_1:.2f} MB")
+
+        rms_error = {}
+        valid_error = {}
+        # Loop over the keys and compute RMSE error.
+        for key in OutputName:
+            rms_error[key] = mean_squared_error(
+                valid_model_runs[key], valid_PCE_runs[key],
+                multioutput='raw_values',
+                sample_weight=None,
+                squared=False)
+
+            # Validation error
+            valid_error[key] = (rms_error[key]**2)
+            valid_error[key] /= np.var(valid_model_runs[key], ddof=1, axis=0)
+
+            # Print a report table
+            print("\n>>>>> Updated Errors of {} <<<<<".format(key))
+            print("\nIndex  |  RMSE   |  Validation Error")
+            print('-'*35)
+            print('\n'.join(f'{i+1}  |  {k:.3e}  |  {j:.3e}' for i, (k, j)
+                            in enumerate(zip(rms_error[key],
+                                             valid_error[key]))))
+
+        return rms_error, valid_error
+
+    # -------------------------------------------------------------------------
+    def __error_Mean_Std(self):
+
+        MetaModel = self.MetaModel
+        # Extract the mean and std provided by user
+        df_MCReference = MetaModel.ModelObj.mc_reference
+
+        # Compute the mean and std based on the MetaModel
+        pce_means, pce_stds = self._compute_pce_moments(MetaModel)
+
+        # Compute the root mean squared error
+        for output in MetaModel.ModelObj.Output.names:
+
+            # Compute the error between mean and std of MetaModel and OrigModel
+            RMSE_Mean = mean_squared_error(
+                df_MCReference['mean'], pce_means[output], squared=False
+                )
+            RMSE_std = mean_squared_error(
+                df_MCReference['std'], pce_means[output], squared=False
+                )
+
+        return RMSE_Mean, RMSE_std
+
+    # -------------------------------------------------------------------------
+    def _compute_pce_moments(self, MetaModel):
+        """
+        Computes the first two moments using the PCE-based meta-model.
+
+        Returns
+        -------
+        pce_means: dict
+            The first moment (mean) of the surrogate.
+        pce_stds: dict
+            The second moment (standard deviation) of the surrogate.
+
+        """
+        outputs = MetaModel.ModelObj.Output.names
+        pce_means_b = {}
+        pce_stds_b = {}
+
+        # Loop over bootstrap iterations
+        for b_i in range(MetaModel.n_bootstrap_itrs):
+            # Loop over the metamodels
+            coeffs_dicts = MetaModel.coeffs_dict[f'b_{b_i+1}'].items()
+            means = {}
+            stds = {}
+            for output, coef_dict in coeffs_dicts:
+
+                pce_mean = np.zeros((len(coef_dict)))
+                pce_var = np.zeros((len(coef_dict)))
+
+                for index, values in coef_dict.items():
+                    idx = int(index.split('_')[1]) - 1
+                    coeffs = MetaModel.coeffs_dict[f'b_{b_i+1}'][output][index]
+
+                    # Mean = c_0
+                    if coeffs[0] != 0:
+                        pce_mean[idx] = coeffs[0]
+                    else:
+                        clf_poly = MetaModel.clf_poly[f'b_{b_i+1}'][output]
+                        pce_mean[idx] = clf_poly[index].intercept_
+                    # Var = sum(coeffs[1:]**2)
+                    pce_var[idx] = np.sum(np.square(coeffs[1:]))
+
+                # Save predictions for each output
+                if MetaModel.dim_red_method.lower() == 'pca':
+                    PCA = MetaModel.pca[f'b_{b_i+1}'][output]
+                    means[output] = PCA.mean_ + np.dot(
+                        pce_mean, PCA.components_)
+                    stds[output] = np.sqrt(np.dot(pce_var,
+                                                  PCA.components_**2))
+                else:
+                    means[output] = pce_mean
+                    stds[output] = np.sqrt(pce_var)
+
+            # Save predictions for each bootstrap iteration
+            pce_means_b[b_i] = means
+            pce_stds_b[b_i] = stds
+
+        # Change the order of nesting
+        mean_all = {}
+        for i in sorted(pce_means_b):
+            for k, v in pce_means_b[i].items():
+                if k not in mean_all:
+                    mean_all[k] = [None] * len(pce_means_b)
+                mean_all[k][i] = v
+        std_all = {}
+        for i in sorted(pce_stds_b):
+            for k, v in pce_stds_b[i].items():
+                if k not in std_all:
+                    std_all[k] = [None] * len(pce_stds_b)
+                std_all[k][i] = v
+
+        # Back transformation if PCA is selected.
+        pce_means, pce_stds = {}, {}
+        for output in outputs:
+            pce_means[output] = np.mean(mean_all[output], axis=0)
+            pce_stds[output] = np.mean(std_all[output], axis=0)
+
+        return pce_means, pce_stds
diff --git a/src/bayesvalidrox/surrogate_models/surrogate_models.py b/src/bayesvalidrox/surrogate_models/surrogate_models.py
index ca902f26bef0c45e8befb72ff67313ef09a77603..a13a96cc53da289b08f05683234c7d7203b6ed8e 100644
--- a/src/bayesvalidrox/surrogate_models/surrogate_models.py
+++ b/src/bayesvalidrox/surrogate_models/surrogate_models.py
@@ -151,6 +151,7 @@ class MetaModel():
         
         if not hasattr(self, 'CollocationPoints'):
             raise AttributeError('Please provide samples to the metamodel before building it.')
+        self.CollocationPoints = np.array(self.CollocationPoints)
             
         # Transform input samples
         # TODO: this is probably not yet correct! Make 'method' variable
@@ -220,6 +221,10 @@ class MetaModel():
         None.
 
         """
+#        print(X)
+#        print(X.shape)
+#        print(y)
+#        print(y['Z'].shape)
         X = np.array(X)
         for key in y.keys():
             y_val = np.array(y[key])
@@ -301,6 +306,7 @@ class MetaModel():
                     if fast_bootstrap and b_i == 0:
                         n_comp_dict[key] = n_comp
                 else:
+                    #print(b_indices)
                     target = Output[b_indices]
 
                 # Parallel fit regression
@@ -1288,7 +1294,7 @@ class MetaModel():
         return mean_pred, std_pred
 
     # -------------------------------------------------------------------------
-    def create_model_error(self, X, y, Model, name='Calib'):
+    def create_model_error(self, X, y, MeasuredData, name='Calib'):
         """
         Fits a GPE-based model error.
 
@@ -1315,7 +1321,7 @@ class MetaModel():
 
         # Read data
         # TODO: do this call outside the metamodel
-        MeasuredData = Model.read_observation(case=name)
+        #MeasuredData = Model.read_observation(case=name)
 
         # Fitting GPR based bias model
         for out in outputNames:
diff --git a/tests/test_engine.py b/tests/itest_Engine.py
similarity index 100%
rename from tests/test_engine.py
rename to tests/itest_Engine.py
diff --git a/tests/test_MetaModel.py b/tests/itest_MetaModel.py
similarity index 100%
rename from tests/test_MetaModel.py
rename to tests/itest_MetaModel.py
diff --git a/tests/test_BayesInference.py b/tests/test_BayesInference.py
new file mode 100644
index 0000000000000000000000000000000000000000..7fbb79195799724a8ae820a2c777c99c0b86d9b0
--- /dev/null
+++ b/tests/test_BayesInference.py
@@ -0,0 +1,422 @@
+# -*- coding: utf-8 -*-
+"""
+Test the BayesInference class for bayesvalidrox
+
+Tests are available for the following functions
+    _logpdf                 - x
+    _kernel_rbf             - x
+class BayesInference:
+    create_inference
+    perform_bootstrap
+    _perturb_data           - x
+    _eval_model             Need working model to test this
+    normpdf
+    _corr_factor_BME_old    - removed
+    _corr_factor_BME        - x
+    _rejection_sampling     - x
+    _posterior_predictive
+    _plot_max_a_posteriori
+    _plot_post_predictive
+"""
+import sys
+sys.path.append("src/")
+sys.path.append("../src/")
+import pytest
+import numpy as np
+import pandas as pd
+
+from bayesvalidrox.surrogate_models.inputs import Input
+from bayesvalidrox.surrogate_models.exp_designs import ExpDesigns
+from bayesvalidrox.surrogate_models.surrogate_models import MetaModel
+from bayesvalidrox.pylink.pylink import PyLinkForwardModel as PL
+from bayesvalidrox.surrogate_models.engine import Engine
+from bayesvalidrox.bayes_inference.discrepancy import Discrepancy
+from bayesvalidrox.bayes_inference.mcmc import MCMC
+from bayesvalidrox.bayes_inference.bayes_inference import BayesInference
+from bayesvalidrox.bayes_inference.bayes_inference import _logpdf, _kernel_rbf
+    
+#%% Test _logpdf
+
+def test_logpdf() -> None:
+    """
+    Calculate loglikelihood
+
+    """
+    _logpdf([0],[0],[1])
+    
+#%% Test _kernel_rbf
+
+def test_kernel_rbf() -> None:
+    """
+    Create RBF kernel
+    """
+    X = [[0,0],[1,1.5]]
+    pars = [1,0.5,1]
+    _kernel_rbf(X, pars)
+    
+def test_kernel_rbf_lesspar() -> None:
+    """
+    Create RBF kernel with too few parameters
+    """
+    X = [[0,0],[1,1.5]]
+    pars = [1,2]
+    with pytest.raises(AttributeError) as excinfo:
+        _kernel_rbf(X, pars)
+    assert str(excinfo.value) == 'Provide 3 parameters for the RBF kernel!'
+    
+#%% Test MCMC init
+
+def test_BayesInference() -> None:
+    """
+    Construct a BayesInference object
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    mod = PL()
+    mm = MetaModel(inp)
+    expdes = ExpDesigns(inp)
+    engine = Engine(mm, mod, expdes)
+    BayesInference(engine)
+  
+#%% Test create_inference
+# TODO: disabled this test!
+def itest_create_inference() -> None:
+    """
+    Run inference
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    
+    expdes = ExpDesigns(inp)
+    expdes.n_init_samples = 2
+    expdes.n_max_samples = 4
+    expdes.X = np.array([[0],[1],[0.5]])
+    expdes.Y = {'Z':[[0.4],[0.5],[0.45]]}
+    expdes.x_values = np.array([0]) #  Error in plots if this is not available
+    
+    mm = MetaModel(inp)
+    mm.fit(expdes.X, expdes.Y)
+    expdes.generate_ED(expdes.n_init_samples, transform=True, max_pce_deg=np.max(mm.pce_deg))
+    
+    mod = PL()
+    mod.observations = {'Z':np.array([0.45])}
+    mod.observations = {'Z':np.array([0.45]), 'x_values':np.array([0])} # Error if x_values not given
+    mod.Output.names = ['Z']
+    
+    engine = Engine(mm, mod, expdes)
+    
+    sigma2Dict = {'Z':np.array([0.05])}
+    sigma2Dict = pd.DataFrame(sigma2Dict, columns = ['Z'])
+    obsData = pd.DataFrame(mod.observations, columns=mod.Output.names)
+    DiscrepancyOpts = Discrepancy('')
+    DiscrepancyOpts.type = 'Gaussian'
+    DiscrepancyOpts.parameters = (obsData*0.15)**2
+    
+    bi = BayesInference(engine)
+    bi.Discrepancy = DiscrepancyOpts # Error if this not class 'DiscrepancyOpts' or dict(?)
+    bi.bootstrap = True # Error if this and bayes_loocv and just_analysis are all False?
+    bi.plot_post_pred = False # Remaining issue in the violinplot
+    bi.create_inference()
+    # Remaining issue in the violinplot in plot_post_predictive
+    
+    
+#%% Test rejection_sampling
+def test_rejection_sampling_nologlik() -> None:
+    """
+    Perform rejection sampling without given log likelihood
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    mod = PL()
+    mm = MetaModel(inp)
+    expdes = ExpDesigns(inp)
+    expdes.init_param_space(max_deg=1)
+    engine = Engine(mm, mod, expdes)
+    bi = BayesInference(engine)
+    bi.prior_samples = expdes.generate_samples(100, 'random')
+    with pytest.raises(AttributeError) as excinfo:
+        bi._rejection_sampling()
+    assert str(excinfo.value) == 'No log-likelihoods available!'
+    
+def test_rejection_sampling_noprior() -> None:
+    """
+    Perform rejection sampling without prior samples
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    mod = PL()
+    mm = MetaModel(inp)
+    expdes = ExpDesigns(inp)
+    engine = Engine(mm, mod, expdes)
+    bi = BayesInference(engine)
+    with pytest.raises(AttributeError) as excinfo:
+        bi._rejection_sampling()
+    assert str(excinfo.value) == 'No prior samples available!'
+    
+def test_rejection_sampling() -> None:
+    """
+    Perform rejection sampling
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    mod = PL()
+    mm = MetaModel(inp)
+    expdes = ExpDesigns(inp)
+    expdes.init_param_space(max_deg=1)
+    engine = Engine(mm, mod, expdes)
+    bi = BayesInference(engine)
+    bi.prior_samples = expdes.generate_samples(100, 'random')
+    bi.log_likes = np.swapaxes(np.atleast_2d(np.log(np.random.random(100)*3)),0,1)
+    bi._rejection_sampling()
+
+    
+#%% Test _perturb_data
+
+def test_perturb_data() -> None:
+    """
+    Perturb data
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    mod = PL()
+    mm = MetaModel(inp)
+    expdes = ExpDesigns(inp)
+    engine = Engine(mm, mod, expdes)
+    
+    bi = BayesInference(engine)
+    data = pd.DataFrame()
+    data['Z'] = [0.45]
+    bi._perturb_data(data, ['Z'])
+    
+
+def test_perturb_data_loocv() -> None:
+    """
+    Perturb data with bayes_loocv
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    mod = PL()
+    mm = MetaModel(inp)
+    expdes = ExpDesigns(inp)
+    engine = Engine(mm, mod, expdes)
+    
+    bi = BayesInference(engine)
+    data = pd.DataFrame()
+    data['Z'] = [0.45]
+    bi.bayes_loocv = True
+    bi._perturb_data(data, ['Z'])
+    
+#%% Test _eval_model
+
+def test_eval_model() -> None:
+    """
+    Run model with descriptive key
+    """
+    # TODO: need functioning example model to test this
+    None
+    
+#%% Test corr_factor_BME
+
+def test_corr_factor_BME() -> None:
+    """
+    Calculate correction factor
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    expdes = ExpDesigns(inp)
+    expdes.init_param_space(max_deg=1)
+    expdes.X = np.array([[0],[1],[0.5]])
+    expdes.Y = {'Z':[[0.4],[0.5],[0.45]]}
+    
+    mm = MetaModel(inp)
+    mm.fit(expdes.X, expdes.Y)
+    mod = PL()
+    engine = Engine(mm, mod, expdes)
+    
+    obs_data = {'Z':np.array([0.45])}
+    total_sigma2s = {'Z':np.array([0.15])}
+    logBME = [0,0,0]
+    
+    bi = BayesInference(engine)
+    bi.selected_indices = {'Z':0}
+    bi. _corr_factor_BME(obs_data, total_sigma2s, logBME)
+   
+def test_corr_factor_BME_selectedindices() -> None:
+    """
+    Calculate correction factor
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    expdes = ExpDesigns(inp)
+    expdes.init_param_space(max_deg=1)
+    expdes.X = np.array([[0],[1],[0.5]])
+    expdes.Y = {'Z':[[0.4],[0.5],[0.45]]}
+    
+    mm = MetaModel(inp)
+    mm.fit(expdes.X, expdes.Y)
+    mod = PL()
+    engine = Engine(mm, mod, expdes)
+    
+    obs_data = {'Z':np.array([0.45])}
+    total_sigma2s = {'Z':np.array([0.15])}
+    logBME = [0,0,0]
+    
+    bi = BayesInference(engine)
+    bi.selected_indices = {'Z':0}
+    bi. _corr_factor_BME(obs_data, total_sigma2s, logBME)
+        
+    
+#%% Test normpdf
+
+def test_normpdf_nosigmas() -> None:
+    """
+    Run normpdf without any additional sigmas
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    expdes = ExpDesigns(inp)
+    expdes.init_param_space(max_deg=1)
+    expdes.X = np.array([[0],[1],[0.5]])
+    expdes.Y = {'Z':np.array([[0.4],[0.5],[0.45]])}
+    
+    mm = MetaModel(inp)
+    mod = PL()
+    mod.Output.names = ['Z']
+    engine = Engine(mm, mod, expdes)
+    
+    obs_data = {'Z':np.array([0.45])}
+    total_sigma2s = {'Z':np.array([0.15])}
+    
+    bi = BayesInference(engine)
+    bi.normpdf(expdes.Y, obs_data, total_sigma2s, sigma2=None, std=None)
+    
+def test_normpdf_sigma2() -> None:
+    """
+    Run normpdf with sigma2
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    expdes = ExpDesigns(inp)
+    expdes.init_param_space(max_deg=1)
+    expdes.X = np.array([[0],[1],[0.5]])
+    expdes.Y = {'Z':np.array([[0.4],[0.5],[0.45]])}
+    
+    mm = MetaModel(inp)
+    mod = PL()
+    mod.Output.names = ['Z']
+    engine = Engine(mm, mod, expdes)
+    
+    obs_data = {'Z':np.array([0.45])}
+    total_sigma2s = {'Z':np.array([0.15])}
+    sigma2 = [[0]]
+    
+    bi = BayesInference(engine)
+    bi.normpdf(expdes.Y, obs_data, total_sigma2s, sigma2=sigma2, std=None)
+    
+def test_normpdf_allsigmas() -> None:
+    """
+    Run normpdf with all additional sigmas
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    expdes = ExpDesigns(inp)
+    expdes.init_param_space(max_deg=1)
+    expdes.X = np.array([[0],[1],[0.5]])
+    expdes.Y = {'Z':np.array([[0.4],[0.5],[0.45]])}
+    
+    mm = MetaModel(inp)
+    mod = PL()
+    mod.Output.names = ['Z']
+    engine = Engine(mm, mod, expdes)
+    
+    obs_data = {'Z':np.array([0.45])}
+    total_sigma2s = {'Z':np.array([0.15])}
+    sigma2 = [[0]]
+    
+    bi = BayesInference(engine)
+    bi.normpdf(expdes.Y, obs_data, total_sigma2s, sigma2=sigma2, std=total_sigma2s)
+
+
+#%% Test perform_bootstrap
+   
+def test_perform_bootstrap() -> None:
+    """
+    Do bootstrap
+    """
+    
+    opt_sigma = 'B'
+    total_sigma2s = {'Z':np.array([0.15])}
+    bi.perform_bootstrap(opt_sigma, total_sigma2s)
+    
+if __name__ == '__main__':
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    
+    expdes = ExpDesigns(inp)
+    expdes.n_init_samples = 2
+    expdes.n_max_samples = 4
+    expdes.X = np.array([[0],[1],[0.5]])
+    expdes.Y = {'Z':[[0.4],[0.5],[0.45]]}
+    expdes.x_values = np.array([0]) #  Error in plots if this is not available
+    
+    mm = MetaModel(inp)
+    mm.n_params = 1
+    mm.fit(expdes.X, expdes.Y)
+    expdes.generate_ED(expdes.n_init_samples, transform=True, max_pce_deg=np.max(mm.pce_deg))
+    
+    mod = PL()
+    mod.observations = {'Z':np.array([0.45])}
+    mod.observations = {'Z':np.array([0.45]), 'x_values':np.array([0])} # Error if x_values not given
+    mod.Output.names = ['Z']
+    
+    engine = Engine(mm, mod, expdes)
+    
+    sigma2Dict = {'Z':np.array([0.05])}
+    sigma2Dict = pd.DataFrame(sigma2Dict, columns = ['Z'])
+    obsData = pd.DataFrame(mod.observations, columns=mod.Output.names)
+    DiscrepancyOpts = Discrepancy('')
+    DiscrepancyOpts.type = 'Gaussian'
+    DiscrepancyOpts.parameters = (obsData*0.15)**2
+    
+    bi = BayesInference(engine)
+    bi.Discrepancy = DiscrepancyOpts # Error if this not class 'DiscrepancyOpts' or dict(?)
+    bi.bootstrap = True # Error if this and bayes_loocv and just_analysis are all False?
+    bi.plot_post_pred = False # Remaining issue in the violinplot
+    #bi.error_model = True
+    #bi.bayes_loocv = True
+    bi.create_inference()
+    stop
+    opt_sigma = 'B'
+    total_sigma2s = {'Z':np.array([0.15])}
+    data = pd.DataFrame()
+    data['Z'] = [0.45]
+    data['x_values'] = [0.3]
+    bi.measured_data = data
+    bi.perform_bootstrap(opt_sigma, total_sigma2s)
+    
\ No newline at end of file
diff --git a/tests/test_BayesModelComparison.py b/tests/test_BayesModelComparison.py
new file mode 100644
index 0000000000000000000000000000000000000000..91f328ec7ae39cf7cded6b228edc1442053a2dfb
--- /dev/null
+++ b/tests/test_BayesModelComparison.py
@@ -0,0 +1,28 @@
+# -*- coding: utf-8 -*-
+"""
+Test the BayesModelComparison class in bayesvalidrox.
+Tests are available for the following functions
+Class BayesModelComparison: 
+    create_model_comparison
+    compare_models
+    generate_dataset
+    __perturb_data
+    cal_model_weight
+    plot_just_analysis
+    plot_model_weights
+    plot_bayes_factor
+    
+"""
+import sys
+sys.path.append("src/")
+import pytest
+import numpy as np
+
+from bayesvalidrox.bayes_inference.bayes_model_comparison import BayesModelComparison
+#from bayesvalidrox.surrogate_models.input_space import InputSpace
+
+def test_BMC() -> None:
+    """
+    Build BMC without inputs
+    """
+    BayesModelComparison()
\ No newline at end of file
diff --git a/tests/test_Discrepancy.py b/tests/test_Discrepancy.py
index c46e0a13751756e0583f3176489e7215da77f4ba..7fb948d905031e7d7e6235857c27792fb29ece57 100644
--- a/tests/test_Discrepancy.py
+++ b/tests/test_Discrepancy.py
@@ -36,22 +36,8 @@ def test_get_sample() -> None:
     """
     Get discrepancy sample
     """
-    inp = Input()
-    inp.add_marginals()
-    inp.Marginals[0].dist_type = 'normal'
-    inp.Marginals[0].parameters = [0,1]
-    disc = Discrepancy(InputDisc = inp)
+    disc = Discrepancy()
     with pytest.raises(AttributeError) as excinfo:
         disc.get_sample(2)
     assert str(excinfo.value) == 'Cannot create new samples, please provide input distributions'
-    
-    
-    
-    
-if __name__ == '__main__':
-    inp = Input()
-    inp.add_marginals()
-    inp.Marginals[0].dist_type = 'normal'
-    inp.Marginals[0].parameters = [0,1]
-    disc = Discrepancy(InputDisc = inp)
-    disc.get_sample(2)
\ No newline at end of file
+    
\ No newline at end of file
diff --git a/tests/test_ExpDesign.py b/tests/test_ExpDesign.py
index 42f87663c2d843c4fa3a23e047270673501dbd4c..8b8618e5447870bea2afbd5c9148fa87ddab7b52 100644
--- a/tests/test_ExpDesign.py
+++ b/tests/test_ExpDesign.py
@@ -131,6 +131,47 @@ def test_random_sampler() -> None:
     exp = ExpDesigns(inp)
     exp.random_sampler(4)
     
+def test_random_sampler_largedatanoJDist() -> None:
+    """
+    Sample randomly, init_param_space implicitly, more samples wanted than given, no JDist available
+    """
+    x = np.random.uniform(0,1,1000)
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].input_data = x
+    exp = ExpDesigns(inp)
+    with pytest.raises(AttributeError) as excinfo:
+        exp.random_sampler(100000) 
+    assert str(excinfo.value) == 'Sampling cannot proceed, build ExpDesign with max_deg != 0 to create JDist!'
+    
+def test_random_sampler_largedataJDist0() -> None:
+    """
+    Sample randomly, init_param_space implicitly, more samples wanted than given, 
+    JDist available, priors given via samples
+    """
+    x = np.random.uniform(0,1,1000)
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].input_data = x
+    exp = ExpDesigns(inp)
+    exp.init_param_space(max_deg = 1)
+    exp.random_sampler(100000) 
+    
+def test_random_sampler_largedataJDist1() -> None:
+    """
+    Sample randomly, init_param_space implicitly, more samples wanted than given, 
+    JDist available, prior distributions given
+    """
+    inp = Input()
+    inp.add_marginals()
+    inp.Marginals[0].dist_type = 'normal'
+    inp.Marginals[0].parameters = [0,1]
+    exp = ExpDesigns(inp)
+    exp.init_param_space(max_deg = 1)
+    exp.random_sampler(100000) 
+     
+        
+        
 def test_random_sampler_rawdata() -> None:
     """
     Sample randomly, init_param_space implicitly, has 2d raw data
diff --git a/tests/test_InputSpace.py b/tests/test_InputSpace.py
index 1b5a28fa3eb4b1ad11c8a666a9e98e2b0dbaa8b9..719336e88e285b3c4bbc61cc1704c0d0c1fa64bf 100644
--- a/tests/test_InputSpace.py
+++ b/tests/test_InputSpace.py
@@ -11,6 +11,7 @@ Class InputSpace:
 """
 import sys
 sys.path.append("src/")
+sys.path.append("../src/")
 import pytest
 import numpy as np
 
@@ -577,4 +578,3 @@ def test_transform_gammaparam() -> None:
     exp = InputSpace(inp)
     exp.init_param_space(max_deg=2)
     exp.transform(y, params = [1,1])
-  
\ No newline at end of file
diff --git a/tests/test_MCMC.py b/tests/test_MCMC.py
new file mode 100644
index 0000000000000000000000000000000000000000..50f6d69b67449ef0647e777f876dd10210566d8f
--- /dev/null
+++ b/tests/test_MCMC.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+"""
+Test the MCM class of bayesvalidrox
+Tests are available for the following functions
+Class MCMC: 
+    run_sampler
+    log_prior
+    log_likelihood
+    log_posterior
+    eval_model
+    train_error_model
+    gelmain_rubin
+    marginal_llk_emcee
+    _iterative_scheme
+    _my_ESS
+    _check_ranges
+"""
+import sys
+sys.path.append("src/")
+sys.path.append("../src/")
+import pytest
+import numpy as np
+
+from bayesvalidrox.bayes_inference.mcmc import MCMC
+from bayesvalidrox.bayes_inference.bayes_inference import BayesInference
+
+#%% Test MCMC init
+
+def test_MCMC() -> None:
+    """
+    Construct an MCMC object
+    """
+    MCMC('')
+    
+def test_MCMC() -> None:
+    """
+    Construct an MCMC object
+    """
+    MCMC('')
+    
+
+
+    
+