diff --git a/examples/bcdi_pipeline_example.ipynb b/examples/bcdi_pipeline_example.ipynb index 74b1d233..26b8fd28 100644 --- a/examples/bcdi_pipeline_example.ipynb +++ b/examples/bcdi_pipeline_example.ipynb @@ -487,7 +487,7 @@ ")\n", "\n", "# perform mode decomposition on the selected reconstructions\n", - "bcdi_pipeline.mode_decomposition()\n" + "bcdi_pipeline.mode_decomposition()" ] }, { @@ -1386,9 +1386,9 @@ "scene": { "annotations": [], "aspectratio": { - "x": 1.2464591606043993, + "x": 1.246459160604399, "y": 0.6196507368679935, - "z": 1.2947173737350615 + "z": 1.2947173737350617 }, "bgcolor": "rgba(0,0,0,0)", "domain": { @@ -1417,7 +1417,7 @@ "minexponent": 3, "nticks": 0, "range": [ - 99.37376499176025, + 99.37376499176024, 713.6424398422241 ], "rangemode": "normal", @@ -1696,7 +1696,7 @@ "rgb(18,47,108)" ], [ - 0.011764705882352941, + 0.01176470588235294, "rgb(19,48,110)" ], [ @@ -1708,7 +1708,7 @@ "rgb(22,51,114)" ], [ - 0.023529411764705882, + 0.02352941176470588, "rgb(23,52,116)" ], [ @@ -1732,7 +1732,7 @@ "rgb(28,58,127)" ], [ - 0.047058823529411764, + 0.04705882352941176, "rgb(29,60,129)" ], [ @@ -1776,39 +1776,39 @@ "rgb(38,73,150)" ], [ - 0.09019607843137255, + 0.09019607843137256, "rgb(39,74,152)" ], [ - 0.09411764705882353, + 0.09411764705882351, "rgb(39,76,155)" ], [ - 0.09803921568627451, + 0.09803921568627452, "rgb(40,77,157)" ], [ - 0.10196078431372549, + 0.10196078431372547, "rgb(41,79,159)" ], [ - 0.10588235294117647, + 0.10588235294117648, "rgb(42,80,161)" ], [ - 0.10980392156862745, + 0.10980392156862744, "rgb(43,81,163)" ], [ - 0.11372549019607843, + 0.11372549019607844, "rgb(43,83,165)" ], [ - 0.11764705882352941, + 0.1176470588235294, "rgb(44,84,167)" ], [ - 0.12156862745098039, + 0.1215686274509804, "rgb(45,86,169)" ], [ @@ -1852,7 +1852,7 @@ "rgb(51,101,189)" ], [ - 0.16470588235294117, + 0.16470588235294115, "rgb(51,103,191)" ], [ @@ -1860,7 +1860,7 @@ "rgb(52,104,193)" ], [ - 0.17254901960784313, + 0.1725490196078431, "rgb(52,106,195)" ], [ @@ -1876,7 +1876,7 @@ "rgb(53,111,201)" ], [ - 0.18823529411764706, + 0.18823529411764703, "rgb(54,112,202)" ], [ @@ -1884,7 +1884,7 @@ "rgb(54,114,204)" ], [ - 0.19607843137254902, + 0.19607843137254904, "rgb(54,116,206)" ], [ @@ -1892,7 +1892,7 @@ "rgb(54,117,208)" ], [ - 0.20392156862745098, + 0.20392156862745095, "rgb(54,119,210)" ], [ @@ -1900,7 +1900,7 @@ "rgb(54,121,211)" ], [ - 0.21176470588235294, + 0.21176470588235297, "rgb(54,122,213)" ], [ @@ -1916,7 +1916,7 @@ "rgb(53,128,218)" ], [ - 0.22745098039215686, + 0.2274509803921569, "rgb(53,129,220)" ], [ @@ -1924,19 +1924,19 @@ "rgb(53,131,221)" ], [ - 0.23529411764705882, + 0.2352941176470588, "rgb(52,133,223)" ], [ - 0.23921568627450981, + 0.2392156862745098, "rgb(52,135,224)" ], [ - 0.24313725490196078, + 0.2431372549019608, "rgb(51,137,226)" ], [ - 0.24705882352941178, + 0.24705882352941175, "rgb(51,139,227)" ], [ @@ -2056,7 +2056,7 @@ "rgb(105,188,250)" ], [ - 0.36470588235294116, + 0.3647058823529411, "rgb(108,190,251)" ], [ @@ -2084,7 +2084,7 @@ "rgb(131,199,251)" ], [ - 0.39215686274509803, + 0.392156862745098, "rgb(135,201,251)" ], [ @@ -2100,7 +2100,7 @@ "rgb(147,205,250)" ], [ - 0.40784313725490196, + 0.4078431372549019, "rgb(151,206,250)" ], [ @@ -2120,7 +2120,7 @@ "rgb(167,212,249)" ], [ - 0.42745098039215684, + 0.4274509803921568, "rgb(171,213,249)" ], [ @@ -2128,7 +2128,7 @@ "rgb(175,215,248)" ], [ - 0.43529411764705883, + 0.4352941176470588, "rgb(179,216,248)" ], [ @@ -2136,7 +2136,7 @@ "rgb(182,217,247)" ], [ - 0.44313725490196076, + 0.4431372549019608, "rgb(186,219,247)" ], [ @@ -2144,7 +2144,7 @@ "rgb(190,220,247)" ], [ - 0.45098039215686275, + 0.4509803921568627, "rgb(194,222,246)" ], [ @@ -2164,7 +2164,7 @@ "rgb(208,227,244)" ], [ - 0.47058823529411764, + 0.4705882352941176, "rgb(211,228,244)" ], [ @@ -2172,7 +2172,7 @@ "rgb(214,229,243)" ], [ - 0.47843137254901963, + 0.4784313725490196, "rgb(216,231,242)" ], [ @@ -2180,15 +2180,15 @@ "rgb(219,232,241)" ], [ - 0.48627450980392156, + 0.4862745098039216, "rgb(220,233,241)" ], [ - 0.49019607843137253, + 0.4901960784313726, "rgb(222,233,240)" ], [ - 0.49411764705882355, + 0.4941176470588235, "rgb(223,234,238)" ], [ @@ -2604,11 +2604,11 @@ "rgb(16,96,26)" ], [ - 0.9019607843137255, + 0.9019607843137256, "rgb(16,94,25)" ], [ - 0.9058823529411765, + 0.9058823529411764, "rgb(15,93,23)" ], [ @@ -2624,7 +2624,7 @@ "rgb(12,88,20)" ], [ - 0.9215686274509803, + 0.9215686274509804, "rgb(11,87,19)" ], [ @@ -2636,15 +2636,15 @@ "rgb(10,84,17)" ], [ - 0.9333333333333333, + 0.9333333333333332, "rgb(9,82,16)" ], [ - 0.9372549019607843, + 0.9372549019607844, "rgb(8,81,15)" ], [ - 0.9411764705882353, + 0.9411764705882352, "rgb(7,79,14)" ], [ @@ -2660,7 +2660,7 @@ "rgb(5,75,11)" ], [ - 0.9568627450980393, + 0.9568627450980391, "rgb(4,73,10)" ], [ @@ -2676,15 +2676,15 @@ "rgb(2,69,8)" ], [ - 0.9725490196078431, + 0.9725490196078432, "rgb(2,67,7)" ], [ - 0.9764705882352941, + 0.976470588235294, "rgb(1,66,6)" ], [ - 0.9803921568627451, + 0.9803921568627452, "rgb(1,64,5)" ], [ @@ -2692,11 +2692,11 @@ "rgb(0,63,4)" ], [ - 0.9882352941176471, + 0.9882352941176472, "rgb(0,61,4)" ], [ - 0.9921568627450981, + 0.992156862745098, "rgb(0,60,3)" ], [ @@ -2785,7 +2785,7 @@ }, "up": { "x": -0.09960017411944162, - "y": 0.9674560666171167, + "y": 0.9674560666171168, "z": 0.23261247705382 } }, diff --git a/examples/bcdi_reconstruction_analysis.ipynb b/examples/bcdi_reconstruction_analysis.ipynb index a0a6662a..ab849590 100644 --- a/examples/bcdi_reconstruction_analysis.ipynb +++ b/examples/bcdi_reconstruction_analysis.ipynb @@ -55,9 +55,7 @@ "metadata": {}, "outputs": [], "source": [ - "results_dir = (\n", - " \"path/to/the/results/directory\" # Replace with the actual path to your results directory\n", - ")" + "results_dir = \"path/to/the/results/directory\" # Replace with the actual path to your results directory" ] }, { @@ -103,7 +101,7 @@ "explorer = cdiutils.io.CXIExplorer(cxi_path)\n", "\n", "# Launch the interactive browser\n", - "explorer.explore()\n" + "explorer.explore()" ] }, { @@ -167,8 +165,14 @@ "source": [ "# List of quantities to extract from CXI files\n", "quantities = (\n", - " \"support\", \"het_strain\", \"het_strain_from_dspacing\", \"dspacing\",\n", - " \"amplitude\", \"displacement\", \"phase\", \"lattice_parameter\"\n", + " \"support\",\n", + " \"het_strain\",\n", + " \"het_strain_from_dspacing\",\n", + " \"dspacing\",\n", + " \"amplitude\",\n", + " \"displacement\",\n", + " \"phase\",\n", + " \"lattice_parameter\",\n", " # Add or remove quantities based on your needs\n", ")\n", "\n", @@ -177,9 +181,7 @@ "# Path to the results directory\n", "\n", "# Initialize a dictionary to store the structural properties\n", - "structural_properties = {\n", - " condition: {} for condition, _, _ in table\n", - "}\n", + "structural_properties = {condition: {} for condition, _, _ in table}\n", "\n", "# Path template for post-processed data\n", "path_template = results_dir + \"{}/S{}/S{}_postprocessed_data.cxi\"\n", @@ -187,17 +189,21 @@ "# Load data for each condition\n", "for condition, sample_name, scan in table:\n", " path = path_template.format(sample_name, scan, scan)\n", - " \n", + "\n", " # Load all specified quantities from the CXI file\n", " structural_properties[condition] = cdiutils.io.load_cxi(path, *quantities)\n", - " voxel_sizes[condition] = cdiutils.io.load_cxi(path, \"voxel_size\")\n", + " voxel_sizes[condition] = cdiutils.io.load_cxi(path, \"voxel_size\")\n", "\n", "# Apply support mask: set values outside the support to NaN\n", "for key in quantities:\n", - " if key != \"support\" and key != \"amplitude\": # Keep amplitude outside support\n", + " if (\n", + " key != \"support\" and key != \"amplitude\"\n", + " ): # Keep amplitude outside support\n", " for condition, _, _ in table:\n", - " structural_properties[condition][key] *= cdiutils.utils.zero_to_nan(\n", - " structural_properties[condition][\"support\"]\n", + " structural_properties[condition][key] *= (\n", + " cdiutils.utils.zero_to_nan(\n", + " structural_properties[condition][\"support\"]\n", + " )\n", " )" ] }, @@ -312,28 +318,26 @@ "quantity = \"het_strain\" # Change this to any quantity from your list\n", "\n", "# Plot the selected quantity for each condition\n", - "for (condition, _, _) in table:\n", + "for condition, _, _ in table:\n", " fig, axes = cdiutils.plot.plot_volume_slices(\n", " structural_properties[condition][quantity],\n", " title=condition,\n", " cmap=plot_configs[quantity][\"cmap\"],\n", - " \n", " # comment this block if you don't need real size extents\n", " voxel_size=voxel_sizes[condition],\n", " data_centre=(0, 0, 0),\n", " show=False,\n", " convention=\"cxi\",\n", - " \n", " # Adjust these colouring limits based on your data\n", " vmin=-0.05,\n", " vmax=0.05,\n", " )\n", - " \n", + "\n", " # comment this block if you don't need real size extents\n", " for ax in axes.flat:\n", " ax.set_xlim(-300, 300) # nm\n", - " ax.set_ylim(-300, 300) #nm\n", - " \n", + " ax.set_ylim(-300, 300) # nm\n", + "\n", " # comment this block if you don't need real size extents\n", " cdiutils.plot.add_labels(axes)\n", " display(fig)" @@ -391,20 +395,22 @@ "fig = cdiutils.plot.plot_multiple_volume_slices(\n", " *[structural_properties[c][quantity] for c, _, _ in table],\n", " voxel_sizes=[voxel_sizes[c] for c, _, _ in table], # For physical units\n", - " data_labels=[c for c, _, _ in table], # Label each dataset\n", - " data_centres=[(0, 0, 0) for _ in table], # Center of each dataset\n", - " convention=\"cxi\", # Use CXI convention for views \n", + " data_labels=[c for c, _, _ in table], # Label each dataset\n", + " data_centres=[(0, 0, 0) for _ in table], # Center of each dataset\n", + " convention=\"cxi\", # Use CXI convention for views\n", " # data_stacking=\"v\", # Stack datasets vertically\n", " # pvs_args={\"views\": [\"z+\", \"y+\", \"x+\"]}, # Specific view directions\n", - " cbar_args={\"location\": \"right\", # Colorbar on the right\n", - " \"title\": plot_configs[quantity][\"title\"]}, # Title from configs\n", - " xlim=(-300, 300), # Consistent x limits in the same units as voxel size\n", - " ylim=(-300, 300), # Consistent y limits in the same units as voxel size\n", - " cmap=plot_configs[quantity][\"cmap\"], # Apply a custom colormap\n", - " vmin=-0.05, # Set min value for colormap\n", - " vmax=0.05, # Set max value for colormap\n", - " remove_ticks=True, # Clean appearance without ticks\n", - " title=f\"{quantity} Comparison\" # Title above the figure\n", + " cbar_args={\n", + " \"location\": \"right\", # Colorbar on the right\n", + " \"title\": plot_configs[quantity][\"title\"],\n", + " }, # Title from configs\n", + " xlim=(-300, 300), # Consistent x limits in the same units as voxel size\n", + " ylim=(-300, 300), # Consistent y limits in the same units as voxel size\n", + " cmap=plot_configs[quantity][\"cmap\"], # Apply a custom colormap\n", + " vmin=-0.05, # Set min value for colormap\n", + " vmax=0.05, # Set max value for colormap\n", + " remove_ticks=True, # Clean appearance without ticks\n", + " title=f\"{quantity} Comparison\", # Title above the figure\n", ")" ] }, @@ -467,7 +473,7 @@ " \"dspacing\": None, # Set to None for automatic range\n", " \"amplitude\": None,\n", " \"displacement\": -0.2,\n", - " \"phase\": -np.pi/2,\n", + " \"phase\": -np.pi / 2,\n", "}\n", "vmaxs = {\n", " \"support\": 1,\n", @@ -476,7 +482,7 @@ " \"dspacing\": None,\n", " \"amplitude\": None,\n", " \"displacement\": 0.2,\n", - " \"phase\": np.pi/2,\n", + " \"phase\": np.pi / 2,\n", "}\n", "for key in plot_configs.keys():\n", " if key not in vmins:\n", @@ -489,21 +495,23 @@ "# Then use custom_quantities instead of quantities in the loop below\n", "\n", "# For each quantity, plot all conditions\n", - "for quantity in custom_quantities: # Change to custom_quantities if defined above\n", + "for (\n", + " quantity\n", + ") in custom_quantities: # Change to custom_quantities if defined above\n", " fig = cdiutils.plot.plot_multiple_volume_slices(\n", " *[structural_properties[c][quantity] for c, _, _ in table],\n", " voxel_sizes=[voxel_sizes[c] for c, _, _ in table],\n", " data_labels=[c for c, _, _ in table],\n", " data_centres=[(0, 0, 0) for _ in table],\n", - " convention=\"cxi\", \n", + " convention=\"cxi\",\n", " cbar_args={\"title\": plot_configs[quantity][\"title\"]},\n", - " xlim=(-300, 300), \n", + " xlim=(-300, 300),\n", " ylim=(-300, 300),\n", " cmap=plot_configs[quantity][\"cmap\"],\n", " vmin=vmins[quantity],\n", " vmax=vmaxs[quantity],\n", " remove_ticks=True,\n", - " title=f\"{quantity.capitalize()} Comparison\"\n", + " title=f\"{quantity.capitalize()} Comparison\",\n", " )" ] }, @@ -536,21 +544,21 @@ "# Load reciprocal space data for each condition\n", "for condition, sample_name, scan in table:\n", " path = path_template.format(sample_name, scan, scan)\n", - " \n", + "\n", " # Load orthogonalized detector data\n", " reciprocal_space_data[condition][\"ortho_data\"] = cdiutils.io.load_cxi(\n", " path, \"orthogonalised_detector_data\"\n", " )\n", - " \n", + "\n", " # Get q-space information\n", " reciprocal_space_data[condition][\"q_spacing\"] = []\n", " for ax in (\"qx_xu\", \"qy_xu\", \"qz_xu\"):\n", " reciprocal_space_data[condition][\"q_spacing\"].append(\n", " np.mean(\n", " np.diff(cdiutils.io.load_cxi(path, f\"entry_1/result_2/{ax}\"))\n", - " ) \n", + " )\n", " )\n", - " \n", + "\n", " # Get q-space center\n", " reciprocal_space_data[condition][\"q_centre\"] = cdiutils.io.load_cxi(\n", " path, \"entry_1/result_2/q_lab_shift\"\n", @@ -615,7 +623,7 @@ ], "source": [ "# Plot reciprocal space data for each condition\n", - "for (condition, _, _) in table:\n", + "for condition, _, _ in table:\n", " fig, axes = cdiutils.plot.plot_volume_slices(\n", " reciprocal_space_data[condition][\"ortho_data\"],\n", " voxel_size=reciprocal_space_data[condition][\"q_spacing\"],\n", @@ -624,7 +632,7 @@ " cmap=\"turbo\",\n", " norm=LogNorm(1e-1), # Log scale for diffraction patterns\n", " convention=\"xu\",\n", - " show=False\n", + " show=False,\n", " )\n", " # Add appropriate labels for reciprocal space\n", " cdiutils.plot.add_labels(axes, space=\"rcp\", convention=\"xu\")\n", @@ -689,9 +697,9 @@ "# Example: Calculate average lattice parameter for each condition\n", "avg_lat_par = {}\n", "for condition, _, _ in table:\n", - " lat_par_data = structural_properties[condition][\"lattice_parameter\"] \n", + " lat_par_data = structural_properties[condition][\"lattice_parameter\"]\n", " support = structural_properties[condition][\"support\"]\n", - " \n", + "\n", " # Calculate average within support\n", " avg_lat_par[condition] = np.nanmean(lat_par_data[support > 0])\n", " print(\n", @@ -745,18 +753,17 @@ } ], "source": [ - "quantity = \"het_strain_from_dspacing\" \n", + "quantity = \"het_strain_from_dspacing\"\n", "\n", "colors = {\n", " \"overall\": \"lightcoral\",\n", " \"bulk\": \"limegreen\",\n", - " \"surface\": \"dodgerblue\"\n", + " \"surface\": \"dodgerblue\",\n", "}\n", "\n", "\n", "fig, axes = plt.subplots(\n", - " len(table), 3, layout=\"tight\", sharex=True,\n", - " figsize=(6, 1.5*len(table))\n", + " len(table), 3, layout=\"tight\", sharex=True, figsize=(6, 1.5 * len(table))\n", ")\n", "\n", "for i, (condition, _, _) in enumerate(table):\n", @@ -765,7 +772,7 @@ " structural_properties[condition][\"support\"],\n", " bins=50,\n", " density=False, # If False you get counts, if True you get density\n", - " region=\"all\"\n", + " region=\"all\",\n", " )\n", " # Plot histograms and KDEs\n", " for j, region in enumerate(histograms.keys()):\n", @@ -775,17 +782,26 @@ " *kdes[region],\n", " color=colors[region],\n", " fwhm=True, # Set to True for FWHM plot,\n", - " \n", " # comment/uncomment lines below to play with the plot options\n", " bar_args={\"edgecolor\": \"w\", \"label\": \"strain histogram\"},\n", - " kde_args={\"fill\": True, \"fill_alpha\": 0.45, \"color\": \"k\", \"lw\": 0.2},\n", + " kde_args={\n", + " \"fill\": True,\n", + " \"fill_alpha\": 0.45,\n", + " \"color\": \"k\",\n", + " \"lw\": 0.2,\n", + " },\n", " )\n", - " \n", + "\n", " # Plot the mean\n", " axes[i, j].plot(\n", - " means[region], 0, color=colors[region], ms=4,\n", - " markeredgecolor=\"k\", marker=\"o\", mew=0.5,\n", - " label=f\"Mean = {means[region]:.4f} %\"\n", + " means[region],\n", + " 0,\n", + " color=colors[region],\n", + " ms=4,\n", + " markeredgecolor=\"k\",\n", + " marker=\"o\",\n", + " mew=0.5,\n", + " label=f\"Mean = {means[region]:.4f} %\",\n", " )\n", "\n", " axes[i, j].legend(\n", @@ -793,9 +809,9 @@ " )\n", " axes[i, j].set_xlim(-0.06, 0.06) # change this according to your data\n", " axes[0, j].set_title(f\"{region.capitalize()}\")\n", - " axes[len(table)-1, j].set_xlabel(\"Strain (%)\")\n", + " axes[len(table) - 1, j].set_xlabel(\"Strain (%)\")\n", " axes[i, 0].set_ylabel(condition)\n", - " \n", + "\n", " print(\n", " f\"Average {quantity} in {condition}: \"\n", " f\"{means['overall']:.5f} +/- {stds['overall']:.5f}\"\n", @@ -805,8 +821,7 @@ "# cdiutils.plot.save_fig(\n", "# \"output.svg\" # 'svg' if you want to edit with inkscape, 'pdf', 'png'...\n", "# dpi=300,\n", - "# )\n", - " " + "# )" ] }, { @@ -872,12 +887,11 @@ ], "source": [ "for condition, _, _ in table:\n", - " strain_data = structural_properties[condition][\"het_strain\"] \n", + " strain_data = structural_properties[condition][\"het_strain\"]\n", " support = structural_properties[condition][\"support\"]\n", " cdiutils.pipeline.PipelinePlotter.strain_statistics(\n", " strain_data, support, title=condition\n", - " )\n", - " " + " )" ] }, { diff --git a/examples/pole_figure.ipynb b/examples/pole_figure.ipynb index e528eab3..58c25b62 100644 --- a/examples/pole_figure.ipynb +++ b/examples/pole_figure.ipynb @@ -13,8 +13,8 @@ "metadata": {}, "outputs": [], "source": [ - "from matplotlib.colors import LogNorm\n", "import numpy as np\n", + "from matplotlib.colors import LogNorm\n", "\n", "import cdiutils\n", "\n", @@ -36,7 +36,7 @@ "metadata": {}, "outputs": [], "source": [ - "path = (\"path/to/data.cxi\")" + "path = \"path/to/data.cxi\"" ] }, { @@ -79,15 +79,15 @@ " shift = cxi[\"entry_1/result_2/q_space_shift\"]\n", "\n", "print(qx.shape, qy.shape, qz.shape, data.shape)\n", - "voxel_size = (\n", - " np.diff(qx).mean(),\n", - " np.diff(qy).mean(),\n", - " np.diff(qz).mean()\n", - ")\n", + "voxel_size = (np.diff(qx).mean(), np.diff(qy).mean(), np.diff(qz).mean())\n", "\n", "fig, axes = cdiutils.plot.plot_volume_slices(\n", - " data, voxel_size=voxel_size, data_centre=shift,\n", - " norm=LogNorm(), convention=\"xu\", show=False\n", + " data,\n", + " voxel_size=voxel_size,\n", + " data_centre=shift,\n", + " norm=LogNorm(),\n", + " convention=\"xu\",\n", + " show=False,\n", ")\n", "\n", "cdiutils.plot.add_labels(axes, convention=\"xu\")\n", @@ -168,11 +168,13 @@ " data,\n", " [qx, qy, qz],\n", " radius=0.020,\n", - " dr=0.0002, \n", + " dr=0.0002,\n", " axis=\"2\",\n", - " norm=LogNorm(1, ),\n", + " norm=LogNorm(\n", + " 1,\n", + " ),\n", " verbose=True,\n", - ")\n" + ")" ] } ], diff --git a/pyproject.toml b/pyproject.toml index 9f0407d3..6bf386c6 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,6 +106,7 @@ where = ["src"] "cdiutils" = [ "pipeline/pynx-id01-cdi_template.slurm", "templates/bcdi_pipeline.ipynb", + "templates/i16_bcdi_pipeline.ipynb", "templates/step_by_step_bcdi_analysis.ipynb", "templates/detector_calibration.ipynb", "plot/lut_*.npz" diff --git a/src/cdiutils/geometry.py b/src/cdiutils/geometry.py index 4d7e629c..2808b44f 100644 --- a/src/cdiutils/geometry.py +++ b/src/cdiutils/geometry.py @@ -169,6 +169,7 @@ def from_setup( - ``"NanoMAX"``: MAX IV NanoMAX - ``"CRISTAL"``: SOLEIL CRISTAL - ``"ID27"``: ESRF ID27 + - ``"I16"``: DLS I16 beamline_setup: Deprecated. Use ``beamline`` instead. sample_orientation: Sample mounting style: @@ -287,11 +288,34 @@ def from_setup( sample_surface_normal=[0, 1, 0], # default sample facing up name="ID27", ) + + if beamline.lower() == "i16": + geometry = cls( + sample_circles=[ + "x-", + "y+", + ], # eta, mu or eta, phi when chi==90 + # sample_circles=["x-", "z+", "x-", "y+"], # TODO: phi, chi, eta, mu (how do I add these angles in the loader?) + detector_circles=[ + "y+", + "x-", + ], # gam, delta (this doesn't match spec but same as id01) + detector_axis0_orientation="x+", # at Stokes=0, merlin detector horiz. pixels low=high delta + detector_axis1_orientation="y-", # at Stokes=0, merlin detector vert. pixels low=low gamma + beam_direction=[1, 0, 0], + sample_surface_normal=[ + 0, + 1, + 0, + ], # CXI z,y,x default sample facing up + name="I16", + ) + if geometry is None: raise NotImplementedError( f"The beamline name {beamline} is not valid. Available:\n" "'ID01', 'ID01SPEC', 'ID27', 'P10', 'P10EH2', 'SIXS2022' " - "and NanoMAX." + "'I16' and 'NanoMAX'." ) # if the sample orientation is provided, override any default diff --git a/src/cdiutils/io/__init__.py b/src/cdiutils/io/__init__.py index 569c9ae3..dbbdc57c 100755 --- a/src/cdiutils/io/__init__.py +++ b/src/cdiutils/io/__init__.py @@ -7,6 +7,7 @@ from .cristal import CristalLoader from .cxi import CXIFile, load_cxi, save_as_cxi +from .i16 import I16Loader from .id01 import ID01Loader, SpecLoader from .id27 import ID27Loader from .loader import Loader, h5_safe_load @@ -21,6 +22,7 @@ "ID01Loader", "ID27Loader", "P10Loader", + "I16Loader", "SpecLoader", "SIXSLoader", "CristalLoader", diff --git a/src/cdiutils/io/i16.py b/src/cdiutils/io/i16.py new file mode 100644 index 00000000..c1fc5ab1 --- /dev/null +++ b/src/cdiutils/io/i16.py @@ -0,0 +1,542 @@ +import dateutil.parser +import numpy as np +import silx.io + +from cdiutils.io.loader import H5TypeLoader, h5_safe_load + + +class I16Loader(H5TypeLoader): + """ + Data loader for Diamond Light Source I16 beamline. + + Loads data from NeXus files. + + Attributes: + angle_names: Mapping from canonical names to I16 Eulerian pseudo-motor names: + + - ``sample_outofplane_angle`` -> ``"eta"`` + - ``sample_inplane_angle`` -> ``"mu"`` + - ``detector_outofplane_angle`` -> ``"delta"`` + - ``detector_inplane_angle`` -> ``"gamma"`` + + authorised_detector_names: Tuple of supported detectors: + ``("merlin")``. + + Examples: + Basic usage with factory pattern: + + >>> from cdiutils.io import Loader + >>> loader = Loader.from_setup( + ... beamline_setup="i16", + ... experiment_file_path="/dls/i16/data/2026/mm12345-1/12345.nxs" + ... ) + + Direct instantiation: + + >>> from cdiutils.io.i16 import I16Loader + >>> loader = I16Loader( + ... experiment_file_path="/dls/i16/data/2026/mm12345-1/12345.nxs", + ... sample_name="PtNP", + ... detector_name="merlin" + ... ) + + Load data with preprocessing: + + >>> data, angles = loader.load_data( + ... roi=(100, 400, 150, 450), + ... rocking_angle_binning=2 + ... ) + + See Also: + :class:`Loader` for factory method and base class documentation. + """ + + # I16 is a 6-circle kappa diffractometer, motors are in the Kappa convention + # but the Eulerian convention is also stored (CXI basis here): + # sample rotations: phi (x-), chi (z+), eta (x-), mu (y+), + # detector rotations: delta (x-), gamma (y+) + # TODO: How to add additional angles below? + angle_names = { + "sample_outofplane_angle": "eta", + "sample_inplane_angle": "mu", + "detector_outofplane_angle": "delta", + "detector_inplane_angle": "gam", + } + authorised_detector_names = ("merlin",) + + def __init__( + self, + experiment_file_path: str, + detector_name: str = None, + flat_field: np.ndarray | str = None, + alien_mask: np.ndarray | str = None, + **kwargs, + ): + """ + Initialise I16 data loader with experiment file and metadata. + + Args: + experiment_file_path: Path to Nexus scan file + detector_name: Detector identifier (``"mpxgaas"``, + ``"mpx1x4"``, or ``"eiger2M"``). If None, automatically + detected from first available scan. + flat_field: Flat-field correction array or path to .npy/.npz + file. Shape must match detector's 2D frame. Applied + multiplicatively to raw data. + alien_mask: Bad pixel mask array or path. Binary mask with + 1 = bad pixel, 0 = good pixel. Combined with detector's + chip gap mask. + **kwargs: Additional parameters (currently unused, reserved + for future extensions). + + Raises: + FileNotFoundError: If ``experiment_file_path`` does not + exist. + ValueError: If ``detector_name`` is not in + :attr:`authorised_detector_names`. + KeyError: If ``scan`` or ``sample_name`` do not match HDF5 + structure. + + Examples: + Minimal setup (auto-detect detector): + + >>> loader = I16Loader( + ... experiment_file_path="/dls/i16/data/2026/mm12345-1/123456.nxs", + ... ) + + With flat-field and detector specification: + + >>> loader = I16Loader( + ... experiment_file_path="/dls/i16/data/2026/mm12345-1/123456.nxs", + ... detector_name="merlin", + ... flat_field="/path/to/flatfield.npy" + ... ) + """ + super().__init__( + experiment_file_path, + scan=None, + sample_name=None, + detector_name=detector_name or "merlin", + flat_field=flat_field, + alien_mask=alien_mask, + ) + + @h5_safe_load + def get_detector_name( + self, start_scan: int = 1, max_attempts: int = 5 + ) -> str: + """ + Auto-detect detector from HDF5 file scan metadata. + + Returns the name of the first NXdetector in the instrument group. + """ + + # Get detctor name from first NXdetector in instrument + instrument = self.h5file["entry/instrument"] + for name, object in instrument.items(): + nx_class = object.attrs.get("NX_class") + if nx_class and nx_class.astype(str) == "NXdetector": + return name + raise KeyError("No NXdetector found in HDF5 file") + + @h5_safe_load + def load_det_calib_params( + self, scan: int = None, sample_name: str = None + ) -> dict: + """ + Load detector calibration from scan metadata. + + Retrieves calibration parameters stored in NeXus file + during detector alignment. Returns parameters compatible with + xrayutilities conventions. + + Args: + scan: Unused. + sample_name: Unused. + + Returns: + dict: Calibration parameters with keys: + + - ``"cch1"``: Direct beam row (y) position in pixels + - ``"cch2"``: Direct beam column (x) position in pixels + - ``"pwidth1"``: Pixel height in metres + - ``"pwidth2"``: Pixel width in metres + - ``"distance"``: Sample-to-detector distance in metres + - ``"tiltazimuth"``: Detector azimuthal tilt (0.0, not + calibrated by BLISS) + - ``"tilt"``: Detector polar tilt (0.0, not calibrated) + - ``"detrot"``: Detector rotation (0.0, not calibrated) + + Raises: + KeyError: If fields are not available in NeXus file. + + Examples: + Load calibration for current scan: + + >>> loader = I16Loader( + ... experiment_file_path="/dls/i16/data/20XX/mmXXXX-1/12345.nxs", + ... ) + >>> calib = loader.load_det_calib_params() + >>> print(f"Direct beam at ({calib['cch1']}, {calib['cch2']})") + + Load from different scan: + + >>> calib = loader.load_det_calib_params(scan=15) + + Notes: + Tilt angles (``tiltazimuth``, ``tilt``, ``detrot``) are set + to 0.0. For accurate tilt values, run detector calibration + notebook or use PyNX's ``cdi_findcenter`` utility. + + See Also: + :doc:`/user_guide/detector_calibration` for calibration + procedures and angle definitions. + """ + instrument = self.h5file["entry/instrument"] + detector = instrument[self.detector_name] + module = detector["module"] + try: + return { + "cch1": float(instrument["merlin_centre_i"][()]) + if "merlin_centre_i" in instrument + else 159, + "cch2": float(instrument["merlin_centre_j"][()]) + if "merlin_centre_j" in instrument + else 348, + "pwidth1": float(module["fast_pixel_direction"][()].squeeze()), + "pwidth2": float(module["slow_pixel_direction"][()].squeeze()), + "distance": float( + detector["transformations/origin_offset"][()] + ), + "tiltazimuth": 0.0, + "tilt": 0.0, + "detrot": 0.0, + } + except KeyError as exc: + raise KeyError( + f"key_path is wrong (key_path='{module.name}'). " + "Are sample_name, scan number or detector name correct?" + ) from exc + + @h5_safe_load + def load_detector_shape( + self, + ) -> tuple: + """ + Load detector's native pixel array dimensions from scan. + + Returns: + Two-element tuple ``(n_rows, n_columns)`` with detector's + full frame shape (e.g., ``(2164, 1030)`` for Eiger2M). + + Raises: + KeyError: If detector not found in HDF5 file. + """ + # /entry/instrument/merlin/module/data_size + instrument = self.h5file["entry/instrument"] + detector = instrument[self.detector_name] + module = detector["module"] + return module["data_size"][()] + + @h5_safe_load + def load_detector_data( + self, + roi: tuple[slice] = None, + rocking_angle_binning: int = None, + binning_method: str = "sum", + ) -> np.ndarray: + """ + Load raw detector frames from I16 NeXus file. + + Retrieves 3D detector data array with optional ROI selection, + binning, flat-field correction, and masking applied via + :meth:`Loader.bin_flat_mask`. + + Args: + roi: Region of interest as tuple of slices or integers. See + :meth:`Loader._check_roi` for format. Applied before + binning to reduce memory usage. + rocking_angle_binning: Binning factor along rocking curve + (frame) axis. If None or 1, no binning performed. + binning_method: Binning operation (``"sum"``, ``"mean"``, or + ``"max"``). Default ``"sum"`` preserves total counts. + + Returns: + Preprocessed detector data with shape + ``(n_frames//binning, n_y, n_x)``. Data type is uint16 + (Maxipix) or uint32 (Eiger). + + Raises: + KeyError: If detector does not exist in HDF5 file. + + Examples: + Full detector, no preprocessing: + + >>> data = loader.load_detector_data() + >>> data.shape + (51, 2164, 1030) + + With ROI and binning: + + >>> data = loader.load_detector_data( + ... roi=(100, 400, 150, 450), + ... rocking_angle_binning=2, + ... binning_method="sum" + ... ) + >>> # Returns (25, 300, 300) array + + See Also: + :meth:`load_data` for combined data + motor positions. + """ + key_path = f"/entry/instrument/{self.detector_name}/data" + roi = self._check_roi(roi) + try: + if rocking_angle_binning: + # we first apply the roi for axis1 and axis2 + data = self.h5file[key_path][(slice(None), roi[1], roi[2])] + else: + data = self.h5file[key_path][roi] + except KeyError as exc: + raise KeyError( + f"key_path is wrong (key_path='{key_path}'). " + "Are sample_name, scan number or detector name correct?" + ) from exc + + return self.bin_flat_mask( + data, + roi, + self.flat_field, + self.alien_mask, + rocking_angle_binning, + binning_method, + ) + + @h5_safe_load + def load_angles(self) -> dict: + diffractometer = self.h5file["entry/instrument/diffractometer_sample"] + measurement = self.h5file["entry/measurement"] + angles = {} + for name in self.angle_names.values(): + if name is not None: + if name in measurement: + # measurement contains scanned array + angles[name] = measurement[name][()] + else: + # diffractometer_sample contains metadata (value at start) + angles[name] = diffractometer[name][()] + + return angles + + @h5_safe_load + def load_motor_positions( + self, + roi: tuple[slice] = None, + rocking_angle_binning: int = None, + ) -> dict: + """ + Load diffractometer motor angles for scan. + + Retrieves sample and detector motor positions, applying same ROI + and binning as detector data to maintain synchronisation. + + Args: + roi: ROI tuple matching detector data ROI. Only first + element (rocking curve axis) is used. If None, full scan + loaded. + rocking_angle_binning: Binning factor matching detector + binning. Angles are averaged (mean) when binned. + + Returns: + dict: Motor angles with canonical keys (see + :attr:`angle_names` for ID01-specific mapping): + + - ``"sample_outofplane_angle"``: eta values (degrees) + - ``"sample_inplane_angle"``: mu values (degrees) + - ``"detector_outofplane_angle"``: delta values (degrees) + - ``"detector_inplane_angle"``: nu/ gamma values (degrees) + + Values are scalars (if motor fixed) or 1D arrays (if + scanned). Array lengths match binned detector's first + dimension. + + Raises: + KeyError: If scan/sample combination not found in HDF5 file. + + Examples: + Load angles matching data: + + >>> data = loader.load_detector_data( + ... roi=(10, 40, 100, 400), + ... rocking_angle_binning=2 + ... ) + >>> angles = loader.load_motor_positions( + ... roi=(slice(10, 40),), + ... rocking_angle_binning=2 + ... ) + >>> angles["sample_outofplane_angle"].shape + (15,) # (40-10)//2 = 15 + + See Also: + :meth:`load_data` for combined data + angles loading. + """ + angles = self.load_angles() + + # ensure angles dictionary has correct keys and defaults to 0.0 + # if missing + # TODO: only canonical angles are stored here, unclear how to add additional angles + formatted_angles = { + key: angles.get(name, 0.0) + for key, name in I16Loader.angle_names.items() + } + self.rocking_angle = self.get_rocking_angle(formatted_angles) + + scan_axis_roi = self._check_roi(roi)[0] + + # format the angles and map them back to their corresponding keys + formatted_values = self.format_scanned_counters( + *formatted_angles.values(), + scan_axis_roi=scan_axis_roi, + rocking_angle_binning=rocking_angle_binning, + ) + + # return a dictionary mapping original angle keys to their + # formatted values. This is possible because Python maintains + # order ! + return dict(zip(formatted_angles.keys(), formatted_values)) + + @h5_safe_load + def load_energy(self) -> float: + """ + Load X-ray beam energy for scan. + + Returns: + Beam energy in eV (converted from monochromator energy in + keV). Returns scalar or array depending on whether energy + was scanned. + + Warns: + UserWarning: If energy key not found in HDF5 + file, returns None. + + Examples: + >>> energy = loader.load_energy() + >>> print(f"Energy: {energy/1e3:.2f} keV") + """ + energy = ( + self.h5file["entry/sample/beam/incident_energy"][()] * 1e3 + ) # keV -> eV + return float(energy) + + @h5_safe_load + def show_scan_attributes( + self, + ) -> None: + """ + Print HDF5 keys available for scan (debugging utility). + + Displays top-level group structure for specified scan, useful + for inspecting file organisation and finding custom metadata. + """ + print(self.h5file["entry"].keys()) + + @h5_safe_load + def load_measurement_parameters(self, parameter_name: str) -> tuple: + """ + Load custom measurement data from scan. + + Retrieves arbitrary datasets stored under + ``{scan}/measurement/`` HDF5 group. Useful for accessing + non-standard counters or experimental metadata. + + Args: + parameter_name: Dataset name under measurement group (e.g., + ``"mu"``, ``"chi"``, custom IOC counters). + scan: Scan number. If None, uses ``self.scan``. + sample_name: Sample name. If None, uses ``self.sample_name``. + + Returns: + Dataset contents (type depends on stored data: array, + scalar, or string). + """ + key_path = "entry/measurement" + return self.h5file[f"{key_path}/{parameter_name}"][()] + + @h5_safe_load + def load_instrument_parameters( + self, + instrument_parameter: str, + ) -> tuple: + """ + Load instrument metadata from scan. + + Retrieves datasets under ``{scan}/instrument/`` group, including + positioners, detectors, and beamline equipment metadata. + + Args: + instrument_parameter: Dataset path under instrument group + (e.g., ``"positioners/delta"``, ``"eiger2M/roi_mode"``). + + Returns: + Dataset contents (type depends on stored data). + """ + key_path = "entry/instrument" + return self.h5file[f"{key_path}/{instrument_parameter}"][()] + + @h5_safe_load + def load_sample_parameters( + self, + sam_parameter: str, + ) -> tuple: + """ + Load sample metadata from scan. + + Retrieves sample-specific information stored under + ``{scan}/sample/`` group (e.g., temperature, pressure, notes). + + Args: + sam_parameter: Dataset name under sample group. + + Returns: + Dataset contents (type depends on stored data). + """ + key_path = "entry/sample" + return self.h5file[f"{key_path}/{sam_parameter}"][()] + + @h5_safe_load + def get_start_time(self, scan: int = None, sample_name: str = None) -> str: + """ + Get scan acquisition start timestamp. + + Parses ISO 8601 timestamp stored by BLISS into datetime object + for temporal analysis or logging. + + Args: + scan: Scan number. If None, uses ``self.scan``. + sample_name: Sample name. If None, uses ``self.sample_name``. + + Returns: + ISO-formatted timestamp string parsable by + :func:`dateutil.parser.isoparse`. + """ + key_path = "entry/start_time" + return dateutil.parser.isoparse(self.h5file[key_path][()]) + + @h5_safe_load + def get_hkl(self) -> tuple[int, int, int]: + """ + Return the HKL value from the NeXus file + """ + key_paths = [ + "/entry/instrument/diffractometer_sample/h", + "/entry/instrument/diffractometer_sample/k", + "/entry/instrument/diffractometer_sample/l", + ] + return tuple([round(self.h5file[k][()]) for k in key_paths]) + + +def safe(func): + def wrap(self, *args, **kwargs): + with silx.io.open(self.experiment_file_path) as self.specfile: + return func(self, *args, **kwargs) + + return wrap diff --git a/src/cdiutils/io/loader.py b/src/cdiutils/io/loader.py index 0d79b3ca..2e4badde 100644 --- a/src/cdiutils/io/loader.py +++ b/src/cdiutils/io/loader.py @@ -220,6 +220,11 @@ def from_setup(cls, beamline_setup: str, **metadata) -> "Loader": return P10Loader(hutch="EH2", **metadata) return P10Loader(**metadata) + if "i16" in beamline_setup.lower(): + from . import I16Loader + + return I16Loader(**metadata) + if beamline_setup.lower() == "cristal": from . import CristalLoader diff --git a/src/cdiutils/pipeline/bcdi.py b/src/cdiutils/pipeline/bcdi.py index 870f77d2..988bece2 100755 --- a/src/cdiutils/pipeline/bcdi.py +++ b/src/cdiutils/pipeline/bcdi.py @@ -357,9 +357,9 @@ def preprocess(self, **params) -> None: self._load() self._from_2d_to_3d_shape() self.logger.info( - "The preprocessing output shape is: and " - f"{self.params['preprocess_shape']} will be used for the " - "determination of the ROI dimensions." + "The preprocessing output shape is: " + f"{self.params['preprocess_shape']} and will be used " + "for the determination of the ROI dimensions." ) # Filter, crop and centre the detector data. self.cropped_detector_data, roi = self._crop_centre( diff --git a/src/cdiutils/scripts/prepare_bcdi_notebooks.py b/src/cdiutils/scripts/prepare_bcdi_notebooks.py index 6e52a4a9..f6d2d646 100644 --- a/src/cdiutils/scripts/prepare_bcdi_notebooks.py +++ b/src/cdiutils/scripts/prepare_bcdi_notebooks.py @@ -37,6 +37,12 @@ def main() -> None: "already exist." ), ) + parser.add_argument( + "--i16", + default=False, + action="store_true", + help="Create I16 version of the notebooks.", + ) args = parser.parse_args() @@ -44,7 +50,12 @@ def main() -> None: templates_dir = get_templates_path() # Update paths to notebooks in the examples directory - bcdi_notebook = os.path.join(templates_dir, "bcdi_pipeline.ipynb") + if ( + args.i16 or os.environ.get("BEAMLINE") == "i16" + ): # on DLS I16, create a specific notebook + bcdi_notebook = os.path.join(templates_dir, "i16_bcdi_pipeline.ipynb") + else: + bcdi_notebook = os.path.join(templates_dir, "bcdi_pipeline.ipynb") step_by_step_notebook = os.path.join( templates_dir, "step_by_step_bcdi_analysis.ipynb" ) diff --git a/src/cdiutils/templates/i16_bcdi_pipeline.ipynb b/src/cdiutils/templates/i16_bcdi_pipeline.ipynb new file mode 100644 index 00000000..85ccc852 --- /dev/null +++ b/src/cdiutils/templates/i16_bcdi_pipeline.ipynb @@ -0,0 +1,430 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# **BCDI Pipeline for I16**\n", + "### A Notebook to Run the `BcdiPipeline` Instance \n", + "\n", + "This notebook provides a structured workflow for running a **Bragg Coherent Diffraction Imaging (BCDI) pipeline**. \n", + "\n", + "The `BcdiPipeline` class handles the entire process, including: \n", + "- **Pre-processing** → Data preparation and corrections. \n", + "- **Phase retrieval** → Running PyNX algorithms to reconstruct the phase. \n", + "- **Post-processing** → Refining, analysing (get the strain!), and visualising results. \n", + "\n", + "You can provide **either**: \n", + "- A **YAML parameter file** for full automation. \n", + "- A **Python dictionary** for interactive control in this notebook. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# import required packages\n", + "import os\n", + "\n", + "# I16 local workstations - set GPU usage to opencl for PyNX\n", + "os.environ[\"PYNX_PU\"] = \"opencl\"\n", + "\n", + "import cdiutils # core library for BCDI processing" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": "## **Specify I16 Scan File**" + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "visit_path: str = \"\" # location of scan files\n", + "scan_number: int = 12345 # scan number\n", + "sample_name: str = \"\" # Optional: provide a sample name for separating folders" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **General Parameters**\n", + "Here, define the key parameters for **accessing and saving data** before running the pipeline. \n", + "- **These parameters must be set manually by the user** before execution. \n", + "- The output data will be saved in a structured directory format based on `sample_name` and `scan`. However, you can change the directory path if you like.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define the key parameters (must be filled in by the user)\n", + "beamline_setup: str = \"i16\" # example: \"ID01\" (provide the beamline setup)\n", + "experiment_file_path: str = os.path.join(visit_path, f\"{scan_number}.nxs\")\n", + "scan: int = scan_number\n", + "\n", + "_loader = cdiutils.Loader.from_setup(\n", + " beamline_setup=beamline_setup,\n", + " experiment_file_path=experiment_file_path,\n", + ")\n", + "hkl = _loader.get_hkl()\n", + "detector_name = _loader.get_detector_name()\n", + "\n", + "# choose where to save the results (default: current working directory)\n", + "dump_dir = os.getcwd() + f\"/results/{sample_name}/S{scan}/\"\n", + "\n", + "# load the parameters and parse them into the BcdiPipeline class instance\n", + "params = cdiutils.pipeline.get_params_from_variables(dir(), globals())\n", + "bcdi_pipeline = cdiutils.BcdiPipeline(params=params)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Pre-Processing** \n", + "\n", + "If you need to update specific parameters, you can **pass them directly** into the `preprocess` method. \n", + "\n", + "### **Main Parameters**\n", + "- `preprocess_shape` → The shape of the cropped window used throughout the processes. \n", + " - Can be a **tuple of 2 or 3 values**. \n", + " - If only **2 values**, the entire rocking curve is used. \n", + "\n", + "- `voxel_reference_methods` → A `list` (or a single value) defining how to centre the data. \n", + " - Can include `\"com\"`, `\"max\"`, or a `tuple` of `int` (specific voxel position). \n", + " - Example:\n", + " ```python\n", + " voxel_reference_methods = [(70, 200, 200), \"com\", \"com\"]\n", + " ```\n", + " - This centres a box of size `preprocess_shape` around `(70, 200, 200)`, then iteratively refines it using `\"com\"` (only computed within this box).\n", + " - Useful when `\"com\"` fails due to artifacts or `\"max\"` fails due to hot pixels. \n", + " - Default: `[\"max\", \"com\", \"com\"]`. \n", + "\n", + "- `rocking_angle_binning` → If you want to bin in the **rocking curve direction**, provide a binning factor (ex.: `2`). \n", + "\n", + "- `light_loading` → If `True`, loads only the **ROI of the data** based on `voxel_reference_methods` and `preprocess_output_shape`. \n", + "\n", + "- `hot_pixel_filter` → Removes isolated hot pixels. \n", + " - Default: `False`. \n", + "\n", + "- `background_level` → Sets the background intensity to be removed. \n", + " - Example: `3`. \n", + " - Default: `None`. \n", + "\n", + "- `hkl` → Defines the **Bragg reflection** measured to extend *d*-spacing values to the lattice parameter. \n", + " - Default: `[1, 1, 1]`. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bcdi_pipeline.preprocess(\n", + " preprocess_shape=(150, 150), # define cropped window size\n", + " voxel_reference_methods=[\"max\", \"com\", \"com\"], # centring method sequence\n", + " hot_pixel_filter=False, # remove isolated hot pixels\n", + " background_level=None, # background intensity level to remove\n", + " hkl=hkl,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **[PyNX](https://pynx.esrf.fr/en/latest/index.html) Phase Retrieval**\n", + "See the [pynx.cdi](https://pynx.esrf.fr/en/latest/scripts/pynx-cdi-id01.html) documentation for details on the phasing algorithms used here. \n", + "\n", + "**Algorithm recipe**\n", + "\n", + "You can either: \n", + "- provide the exact chain of algorithms. \n", + "- or specify the number of iterations for **RAAR**, **HIO**, and **ER**. \n", + "\n", + "```python\n", + "algorithm = None # ex: \"(Sup * (ER**20)) ** 10, (Sup*(HIO**20)) ** 15, (Sup*(RAAR**20)) ** 25\"\n", + "nb_raar = 500\n", + "nb_hio = 300\n", + "nb_er = 200\n", + "psf = \"pseudo-voigt,1,0.05,20\"\n", + "```\n", + "**Support-related parameters**\n", + "```python\n", + "support = \"auto\" # ex: bcdi_pipeline.pynx_phasing_dir + \"support.cxi\" (path to an existing support)\n", + "```\n", + ">_Note: If strain seems to large, don't use \"auto\" (autocorrelation) but use \"circle\" or \"square\", in combination with \"support_size\"_ \n", + "```python\n", + "support_threshold = \"0.15, 0.40\" # must be a string\n", + "support_update_period = 20\n", + "support_only_shrink = False\n", + "support_post_expand = None # ex: \"-1,1\" or \"-1,2,-1\"\n", + "support_update_border_n = None\n", + "support_smooth_width_begin = 2\n", + "support_smooth_width_end = 0.5\n", + "```\n", + "**Other parameters**\n", + "```python\n", + "positivity = False\n", + "beta = 0.9 # β parameter in HIO and RAAR\n", + "detwin = True\n", + "rebin = \"1, 1, 1\" # must be a string\n", + "```\n", + "**Number of Runs & Reconstructions to Keep**\n", + "```python\n", + "nb_run = 20 # total number of runs\n", + "nb_run_keep = 10 # number of reconstructions to keep\n", + "```\n", + "\n", + "**Override defaults in `phase_retrieval`**\n", + "\n", + "You can override any default parameter directly in the phase_retrieval method:\n", + "```python\n", + "bcdi_pipeline.phase_retrieval(nb_run=50, nb_run_keep=25)\n", + "```\n", + "If a parameter is not provided, the default value is used.\n", + "\n", + "### **Phase Retrieval GUI**\n", + "You can also launch a **Graphical User Interface (GUI)** to interactively set parameters and run phase retrieval. \n", + "```python\n", + "bcdi_pipeline.phase_retrieval_gui()\n", + "```\n", + "In that case, you can take care of the the result analysis, the selection of the best reconstructions, and the mode decomposition. Then, simply jump to the post-processing step cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bcdi_pipeline.phase_retrieval(\n", + " clear_former_results=True,\n", + " nb_run=20,\n", + " nb_run_keep=10,\n", + " # support=bcdi_pipeline.pynx_phasing_dir + \"support.cxi\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Analyse the phasing results**\n", + "\n", + "This step evaluates the quality of the phase retrieval results by sorting reconstructions based on a `sorting_criterion`. \n", + "\n", + "##### **Available Sorting Criteria**\n", + "- `\"mean_to_max\"` → Difference between the mean of the **Gaussian fit of the amplitude histogram** and its maximum value. A **smaller difference** indicates a more homogeneous reconstruction. \n", + "- `\"sharpness\"` → Sum of the amplitude within the support raised to the power of 4. **Lower values** indicate greater homogeneity. \n", + "- `\"std\"` → **Standard deviation** of the amplitude. \n", + "- `\"llk\"` → **Log-likelihood** of the reconstruction. \n", + "- `\"llkf\"` → **Free log-likelihood** of the reconstruction. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bcdi_pipeline.analyse_phasing_results(\n", + " sorting_criterion=\"mean_to_max\", # selects the sorting method\n", + " # Optional parameters\n", + " # plot_phasing_results=False, # uncomment to disable plotting\n", + " # plot_phase=True, # uncomment to enable phase plotting\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Optionally, generate a support for further phasing attempts** \n", + "\n", + "##### **Parameters**\n", + "- `run` → set to either: \n", + " - `\"best\"` to use the best reconstruction. \n", + " - an **integer** corresponding to the specific run you want. \n", + "- `output_path` → the location to save the generated support. By default, it will be saved in the `pynx_phasing` folder. \n", + "- `fill` → whether to fill the support if it contains holes. \n", + " - Default: `False`.\n", + "- `verbose` → whether to print logs and display a plot of the support. \n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# bcdi_pipeline.generate_support_from(\"best\", fill=False) # uncomment to generate a support" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### **Selection of the best reconstructions & mode decomposition**\n", + "\n", + "You can select the best reconstructions based on a **sorting criterion** and keep a specified number of top candidates. \n", + "\n", + "##### **Parameters**\n", + "- `nb_of_best_sorted_runs` → the number of best reconstructions to keep, selected based on the `sorting_criterion` used in the `analyse_phasing_results` method above. \n", + "- `best_runs` → instead of selecting based on sorting, you can manually specify a list of reconstruction numbers.\n", + "\n", + "By default, the **best reconstructions** are automatically selected. \n", + "\n", + "Once the best candidates are chosen, `mode_decomposition` analyses them to extract dominant features. \n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# define how many of the best candidates to keep\n", + "number_of_best_candidates: int = 5\n", + "\n", + "# select the best reconstructions based on the sorting criterion\n", + "bcdi_pipeline.select_best_candidates(\n", + " nb_of_best_sorted_runs=number_of_best_candidates\n", + " # best_runs=[10] # uncomment to manually select a specific run\n", + ")\n", + "\n", + "# perform mode decomposition on the selected reconstructions\n", + "bcdi_pipeline.mode_decomposition()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Post-processing**\n", + "\n", + "This stage includes several key operations: \n", + "- **orthogonalisation** of the reconstructed data. \n", + "- **phase manipulation**: \n", + " - phase unwrapping \n", + " - phase ramp removal \n", + "- **computation of physical properties**: \n", + " - displacement field \n", + " - strain \n", + " - d-spacing \n", + "- **visualisation**: Generate multiple plots for analysis. \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bcdi_pipeline.postprocess(\n", + " isosurface=0.3, # threshold for isosurface\n", + " voxel_size=None, # use default voxel size if not provided\n", + " flip=False, # whether to flip the reconstruction if you got the twin image (enantiomorph)\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **3D interactive plot**\n", + "\n", + "Display an interactive 3D isosurface of the final reconstruction and explore different quantities for colouring.\n", + "\n", + "What you can do\n", + "- Visualise an isosurface of the reconstructed object (amplitude / support).\n", + "- Colour the surface by different quantities: amplitude, phase, displacement, strain, d-spacing, etc.\n", + "- Interactively adjust:\n", + " - isosurface threshold (isosurface level)\n", + " - colormap and value range\n", + "- Rotate, zoom and pan the scene with the mouse; use the toolbar to reset view or save a screenshot.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bcdi_pipeline.show_3d_final_result()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## **Feedback & Issue Reporting** \n", + "\n", + "If you have **comments, suggestions, or encounter any issues**, please reach out: \n", + "\n", + "📧 **Email:** [clement.atlan@esrf.fr](mailto:clement.atlan@esrf.fr?subject=cdiutils) \n", + "🐙 **GitHub Issues:** [Report an issue](https://github.com/clatlan/cdiutils/issues) \n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Credits\n", + "This notebook was created by Clément Atlan, ESRF, 2025. It is part of the `cdiutils` package, which provides tools for BCDI data analysis and visualisation.\n", + "If you have used this notebook or the `cdiutils` package in your research, please consider citing the package https://github.com/clatlan/cdiutils/\n", + "You'll find the citation information in the `cdiutils` package documentation.\n", + "\n", + "```bibtex\n", + "@software{Atlan_Cdiutils_A_python,\n", + "author = {Atlan, Clement},\n", + "doi = {10.5281/zenodo.7656853},\n", + "license = {MIT},\n", + "title = {{Cdiutils: A python package for Bragg Coherent Diffraction Imaging processing, analysis and visualisation workflows}},\n", + "url = {https://github.com/clatlan/cdiutils},\n", + "version = {0.2.0}\n", + "}\n", + "```\n", + "\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.14" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +}