Tutorial 2: Phase analysis with tree search#
Dara is equipped with a parallelized tree search algorithm to identify possible phases present in a given XRD pattern.
In this tutorial, we will try to identify the phases in one experimental solid-state
reaction sample between GeO2 and ZnO.
You can download this tutorial project from here.
%pip install ipywidgets nbformat
from pathlib import Path
from dara import search_phases
pattern_path = "tutorial_data/GeO2-ZnO_700C_60min.xrdml"
# three elements are present in the sample
chemical_system = "Ge-O-Zn"
Step 1: Prepare reference phases#
Dara pre-builds an index of all the unique and low-energy phases in ICSD and COD databases. It also implements a method to download CIF structures from COD data server so that there is no need to obtain the offline database.
Before every search, we will need to gather all the reference phases in the chemical
system for the search algorithm. Dara provides ICSDDatabase and CODDatabase to do
the filtering.
In this example, we will use CODDatabase to download all the phases in the chemical system of Ge-O-Zn.
from dara.structure_db import CODDatabase
# The COD database contains methods to filter phases in the chemical system
cod_database = CODDatabase()
# gather reference phases and save them to a directory called "cifs"
all_icsd_ids = cod_database.get_cifs_by_chemsys(chemical_system, dest_dir="cifs")
2026-07-13 22:50:05,586 WARNING dara.structure_db Local copy of database not found. Attempting to download structures...
2026-07-13 22:50:08,631 INFO dara.structure_db Saving downloaded CIFs to dara_downloaded_cifs
Skipping high-energy phase: 1528389 (Ge, 96): e_hull = 0.1494
Skipping high-energy phase: 9013109 (Ge, 64): e_hull = 0.3137
2026-07-13 22:50:08,641 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,642 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,642 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,642 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,643 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,643 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,644 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,644 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,644 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,646 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,646 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,646 INFO dara.structure_db Skipping common gas: O2
2026-07-13 22:50:08,647 INFO dara.structure_db Skipping common gas: O2
Skipping high-energy phase: 1525835 (GeO2, 205): e_hull = 0.2246
Skipping high-energy phase: 1533322 (Ge7O23, 215): e_hull = 0.6571
Skipping high-energy phase: 1011223 (ZnO2, 19): e_hull = 0.1674
Skipping high-energy phase: 1529590 (ZnO2, 164): e_hull = 0.4588
Skipping high-energy phase: 1534836 (ZnO, 225): e_hull = 0.1473
Successfully copied 9011050.cif to Ge_227_(cod_9011050)-0.cif in cifs
Successfully copied 7101738.cif to Ge_227_(cod_7101738)-0.cif in cifs
Successfully copied 9012435.cif to Zn_194_(cod_9012435)-0.cif in cifs
Successfully copied 4030923.cif to Zn_12_(cod_4030923)-None.cif in cifs
Successfully copied 1538108.cif to O17.28_12_(cod_1538108)-None.cif in cifs
Successfully copied 9007435.cif to GeO2_136_(cod_9007435)-0.cif in cifs
Successfully copied 1525833.cif to GeO2_60_(cod_1525833)-36.cif in cifs
Successfully copied 2104024.cif to GeO2_60_(cod_2104024)-36.cif in cifs
Successfully copied 1526227.cif to GeO2_14_(cod_1526227)-None.cif in cifs
Successfully copied 2300365.cif to GeO2_152_(cod_2300365)-0.cif in cifs
Successfully copied 8000212.cif to Ge5O11_12_(cod_8000212)-None.cif in cifs
Successfully copied 9006858.cif to GeO2_58_(cod_9006858)-6.cif in cifs
Successfully copied 9007477.cif to GeO2_154_(cod_9007477)-0.cif in cifs
Successfully copied 9015579.cif to GeO2_92_(cod_9015579)-1.cif in cifs
Successfully copied 9004178.cif to ZnO_186_(cod_9004178)-0.cif in cifs
Successfully copied 1527883.cif to ZnO2_44_(cod_1527883)-None.cif in cifs
Successfully copied 1536063.cif to Zn10.26O48_160_(cod_1536063)-None.cif in cifs
Successfully copied 1537875.cif to ZnO_216_(cod_1537875)-7.cif in cifs
Successfully copied 4517837.cif to Zn5O12_15_(cod_4517837)-None.cif in cifs
Successfully copied 1007256.cif to Zn2Ge3O8_212_(cod_1007256)-2.cif in cifs
Successfully copied 1549040.cif to Zn2GeO4_227_(cod_1549040)-None.cif in cifs
Successfully copied 1549041.cif to Zn2GeO4_95_(cod_1549041)-None.cif in cifs
Successfully copied 9014631.cif to Zn2GeO4_148_(cod_9014631)-0.cif in cifs
Since we are using a pre-filterd database (i.e., the COD), the downloaded CIF files will automatically be named according to the following convention:
{composition}_{spacegroup}_(cod|icsd_{id})-{e_hull}.cif
Where the e_hull is the energy above the convex hull in meV/atom, as determined from
the Materials Project database for the ground-state entry with matching composition and spacegroup.
Step 2: Search for phases#
After preparing the reference CIFs, we can start the phase search on a provided XRD pattern.
In this case, we are using the XRD pattern from the solid-state reaction sample
on our laboratory’s Aeris diffractometer (tutorial_data/GeO2-ZnO_700C_60min.xrdml).
# gather all the phases in the "cifs" directory
all_cifs = list(Path("cifs").glob("*.cif"))
search_results = search_phases(
pattern_path=pattern_path,
phases=all_cifs,
wavelength="Cu",
instrument_profile="Aeris-fds-Pixcel1d-Medipix3",
)
2026-07-13 22:50:10,541 INFO worker.py:1852 -- Started a local Ray instance.
2026-07-13 22:50:11,460 INFO dara.search.tree rpb_threshold automatically set to 1.00 based on pattern SNR.
2026-07-13 22:50:11,526 INFO dara.search.tree Detecting peaks in the pattern.
2026-07-13 22:50:39,254 INFO dara.search.tree The wmax is automatically adjusted to 60.04.
2026-07-13 22:50:39,255 INFO dara.search.tree The intensity threshold is automatically set to 9.06 % of maximum peak intensity.
2026-07-13 22:50:39,256 INFO dara.search.tree Creating the root node.
2026-07-13 22:50:39,257 INFO dara.search.tree Refining all the phases in the dataset.
(remote_do_refinement_no_saving pid=3155) /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/pymatgen/core/composition.py:1372: FutureWarning: gcd is deprecated, and will be removed on 2028-01-01
(remote_do_refinement_no_saving pid=3155) Use math.gcd instead.
(remote_do_refinement_no_saving pid=3155) factor = abs(gcd(*(int(i) for i in sym_amt.values())))
(remote_do_refinement_no_saving pid=3156) /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/pymatgen/core/composition.py:1372: FutureWarning: gcd is deprecated, and will be removed on 2028-01-01 [repeated 3x across cluster] (Ray deduplicates logs by default. Set RAY_DEDUP_LOGS=0 to disable log deduplication, or see https://docs.ray.io/en/master/ray-observability/user-guides/configure-logging.html#log-deduplication for more options.)
(remote_do_refinement_no_saving pid=3156) Use math.gcd instead. [repeated 3x across cluster]
(remote_do_refinement_no_saving pid=3156) factor = abs(gcd(*(int(i) for i in sym_amt.values()))) [repeated 3x across cluster]
(remote_do_refinement_no_saving pid=3156) /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/pymatgen/core/composition.py:1372: FutureWarning: gcd is deprecated, and will be removed on 2028-01-01 [repeated 3x across cluster]
(remote_do_refinement_no_saving pid=3156) Use math.gcd instead. [repeated 3x across cluster]
(remote_do_refinement_no_saving pid=3156) factor = abs(gcd(*(int(i) for i in sym_amt.values()))) [repeated 3x across cluster]
(remote_do_refinement_no_saving pid=3154) /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/pymatgen/core/composition.py:1372: FutureWarning: gcd is deprecated, and will be removed on 2028-01-01 [repeated 3x across cluster]
(remote_do_refinement_no_saving pid=3154) Use math.gcd instead. [repeated 3x across cluster]
(remote_do_refinement_no_saving pid=3154) factor = abs(gcd(*(int(i) for i in sym_amt.values()))) [repeated 3x across cluster]
---------------------------------------------------------------------------
KeyboardInterrupt Traceback (most recent call last)
Cell In[5], line 4
1 # gather all the phases in the "cifs" directory
2 all_cifs = list(Path("cifs").glob("*.cif"))
3
----> 4 search_results = search_phases(
5 pattern_path=pattern_path,
6 phases=all_cifs,
7 wavelength="Cu",
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/dara/search/core.py:109, in search_phases(pattern_path, phases, pinned_phases, max_phases, wavelength, instrument_profile, express_mode, enable_angular_cut, phase_params, refinement_params, return_search_tree, record_peak_matcher_scores, rpb_threshold, peak_matching_strategy)
106 refinement_params = {**DEFAULT_REFINEMENT_PARAMS, **refinement_params}
108 # build the search tree
--> 109 search_tree = SearchTree(
110 pattern_path=pattern_path,
111 cif_paths=phases,
112 pinned_phases=pinned_phases,
113 refine_params=refinement_params,
114 phase_params=phase_params,
115 wavelength=wavelength,
116 instrument_profile=instrument_profile,
117 express_mode=express_mode,
118 enable_angular_cut=enable_angular_cut,
119 max_phases=max_phases,
120 rpb_threshold=rpb_threshold,
121 record_peak_matcher_scores=record_peak_matcher_scores,
122 peak_matching_strategy=peak_matching_strategy,
123 )
125 max_worker = ray.cluster_resources()["CPU"]
126 pending = [remote_expand_node(search_tree, search_tree.root)]
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/dara/search/tree.py:1034, in SearchTree.__init__(self, pattern_path, cif_paths, pinned_phases, refine_params, phase_params, wavelength, instrument_profile, express_mode, enable_angular_cut, maximum_grouping_distance, max_phases, rpb_threshold, record_peak_matcher_scores, peak_matching_strategy, *args, **kwargs)
1031 root_node = self._create_root_node()
1032 self.add_node(root_node)
-> 1034 all_phases_result = self._get_all_cleaned_phases_result()
1036 if self.express_mode:
1037 logger.info("Express mode is enabled. Grouping phases before starting.")
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/dara/search/tree.py:1145, in SearchTree._get_all_cleaned_phases_result(self)
1141 pinned_phases_set = set(self.pinned_phases)
1142 cif_paths = [
1143 cif_path for cif_path in self.cif_paths if cif_path not in pinned_phases_set
1144 ]
-> 1145 all_phases_result = self.refine_phases(
1146 cif_paths,
1147 pinned_phases=self.pinned_phases,
1148 )
1150 # adjust the initial value of eps1 based on the weighted average of all the phases
1151 if not isinstance(self.refinement_params.get("eps1", 0), Number):
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/dara/search/tree.py:826, in BaseSearchTree.refine_phases(self, phases, pinned_phases)
820 if pinned_phases is None:
821 pinned_phases = []
823 return dict(
824 zip_longest(
825 phases,
--> 826 self._batch_refine(
827 all_references=[[*pinned_phases, phase] for phase in phases],
828 ),
829 fillvalue=None,
830 )
831 )
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/dara/search/tree.py:837, in BaseSearchTree._batch_refine(self, all_references)
833 def _batch_refine(
834 self,
835 all_references: list[list[RefinementPhase]],
836 ) -> list[RefinementResult]:
--> 837 return batch_refinement(
838 self.pattern_path,
839 all_references,
840 wavelength=self.wavelength,
841 instrument_profile=self.instrument_profile,
842 phase_params=self.phase_params,
843 refinement_params=self.refinement_params,
844 )
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/dara/search/tree.py:143, in batch_refinement(pattern_path, cif_paths, wavelength, instrument_profile, phase_params, refinement_params)
124 def batch_refinement(
125 pattern_path: Path,
126 cif_paths: list[list[RefinementPhase]],
(...) 130 refinement_params: dict[str, float] | None = None,
131 ) -> list[RefinementResult]:
132 handles = [
133 remote_do_refinement_no_saving.remote(
134 pattern_path,
(...) 141 for cif_paths in cif_paths
142 ]
--> 143 return ray.get(handles)
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/ray/_private/auto_init_hook.py:21, in wrap_auto_init.<locals>.auto_init_wrapper(*args, **kwargs)
18 @wraps(fn)
19 def auto_init_wrapper(*args, **kwargs):
20 auto_init_ray()
---> 21 return fn(*args, **kwargs)
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/ray/_private/client_mode_hook.py:103, in client_mode_hook.<locals>.wrapper(*args, **kwargs)
101 if func.__name__ != "init" or is_client_mode_enabled_by_default:
102 return getattr(ray, func.__name__)(*args, **kwargs)
--> 103 return func(*args, **kwargs)
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/ray/_private/worker.py:2782, in get(object_refs, timeout)
2776 raise ValueError(
2777 f"Invalid type of object refs, {type(object_refs)}, is given. "
2778 "'object_refs' must either be an ObjectRef or a list of ObjectRefs. "
2779 )
2781 # TODO(ujvl): Consider how to allow user to retrieve the ready objects.
-> 2782 values, debugger_breakpoint = worker.get_objects(object_refs, timeout=timeout)
2783 for i, value in enumerate(values):
2784 if isinstance(value, RayError):
File /opt/hostedtoolcache/Python/3.12.13/x64/lib/python3.12/site-packages/ray/_private/worker.py:903, in Worker.get_objects(self, object_refs, timeout, return_exceptions, skip_deserialization)
893 raise TypeError(
894 f"Attempting to call `get` on the value {object_ref}, "
895 "which is not an ray.ObjectRef."
896 )
898 timeout_ms = (
899 int(timeout * 1000) if timeout is not None and timeout != -1 else -1
900 )
901 data_metadata_pairs: List[
902 Tuple[ray._raylet.Buffer, bytes]
--> 903 ] = self.core_worker.get_objects(
904 object_refs,
905 timeout_ms,
906 )
908 debugger_breakpoint = b""
909 for data, metadata in data_metadata_pairs:
File python/ray/_raylet.pyx:3211, in ray._raylet.CoreWorker.get_objects()
-> 3211 'Could not get source, probably due dynamically evaluated source code.'
File python/ray/includes/common.pxi:83, in ray._raylet.check_status()
---> 83 'Could not get source, probably due dynamically evaluated source code.'
KeyboardInterrupt:
Step 3: Result analysis#
The returned search result will be a list of SearchResult object.
search_results
[SearchResult(refinement_result=RefinementResult(lst_data=LstResult(raw_lst='Rietveld refinement to file(s) GeO2-ZnO_700C_60min.xy\nBGMN version 4.2.23, 4416 measured points, 121 peaks, 24 parameters\nStart: Wed Jun 26 14:30:50 2024; End: Wed Jun 26 14:30:51 2024\n20 iteration steps\n\nRp=9.96% Rpb=19.06% R=10.99% Rwp=12.04% Rexp=2.69%\nDurbin-Watson d=0.10\n1-rho=1.99%\n\nGlobal parameters and GOALs\n****************************\nQGeO2152cod23003650=0.4771+-0.0021\nQZnO186cod90041780=0.3870+-0.0024\nQZn2GeO4148cod90146310=0.1359+-0.0013\nEPS2=-0.002856+-0.000013\n\nLocal parameters and GOALs for phase GeO2152cod23003650\n******************************************************\nSpacegroupNo=152\nHermannMauguin=P3_121\nXrayDensity=4.276\nRphase=10.88%\nUNIT=NM\nA=0.499111+-0.000024\nC=0.564768+-0.000033\nk1=0.0100000\nB1=0.00500000\nGEWICHT=0.2793+-0.0012\nGrainSize(1,1,1)=84.1811\nAtomic positions for phase GeO2152cod23003650\n---------------------------------------------\n 3 0.4512 0.0000 0.3333 E=(GE(1.0000))\n 6 0.3974 0.3022 0.2429 E=(O(1.0000))\n\nLocal parameters and GOALs for phase ZnO186cod90041780\n******************************************************\nSpacegroupNo=186\nHermannMauguin=P6_3mc\nXrayDensity=5.669\nRphase=9.52%\nUNIT=NM\nA=0.325072+-0.000011\nC=0.520812+-0.000030\nk1=0\nB1=0.003509+-0.000094\nGEWICHT=0.2266+-0.0021\nGrainSize(1,1,1)=120.9+-3.2\nAtomic positions for phase ZnO186cod90041780\n---------------------------------------------\n 2 0.3333 0.6667 0.0000 E=(ZN(1.0000))\n 2 0.3333 0.6667 0.3821 E=(O(1.0000))\n\nLocal parameters and GOALs for phase Zn2GeO4148cod90146310\n******************************************************\nSpacegroupNo=148\nHermannMauguin=R-3\nXrayDensity=4.777\nRphase=20.28%\nUNIT=NM\nA=1.423755+-0.000083\nC=0.952849+-0.000079\nk1=0.0100000\nB1=0.00500000\nGEWICHT=0.07959+-0.00075\nGrainSize(1,1,1)=84.1811\nAtomic positions for phase Zn2GeO4148cod90146310\n---------------------------------------------\n 18 0.2150 0.1940 0.5830 E=(ZN(1.0000))\n 18 0.5483 0.8607 0.5837 E=(ZN(1.0000))\n 18 0.2150 0.1940 0.2500 E=(GE(1.0000))\n 18 0.8877 0.4633 0.4293 E=(O(1.0000))\n 18 0.2220 0.1310 0.4030 E=(O(1.0000))\n 18 0.2230 0.1140 0.7500 E=(O(1.0000))\n 18 0.9957 0.6613 0.5833 E=(O(1.0000))\n', pattern_name='GeO2-ZnO_700C_60min.xy', num_steps=20, rp=9.96, rpb=19.06, r=10.99, rwp=12.04, rexp=2.69, d=0.1, rho=1.99, phases_results={'GeO2_152_(cod_2300365)-0': PhaseResult(spacegroup_no=152, hermann_mauguin='P3_121', xray_density=4.276, rphase=10.88, unit='NM', gewicht=(0.2793, 0.0012), gewicht_name=None, a=(0.499111, 2.4e-05), b=None, c=(0.564768, 3.3e-05), alpha=None, beta=None, gamma=None, k1=0.01, B1=0.005), 'ZnO_186_(cod_9004178)-0': PhaseResult(spacegroup_no=186, hermann_mauguin='P6_3mc', xray_density=5.669, rphase=9.52, unit='NM', gewicht=(0.2266, 0.0021), gewicht_name=None, a=(0.325072, 1.1e-05), b=None, c=(0.520812, 3e-05), alpha=None, beta=None, gamma=None, k1=0, B1=(0.003509, 9.4e-05)), 'Zn2GeO4_148_(cod_9014631)-0': PhaseResult(spacegroup_no=148, hermann_mauguin='R-3', xray_density=4.777, rphase=20.28, unit='NM', gewicht=(0.07959, 0.00075), gewicht_name=None, a=(1.423755, 8.3e-05), b=None, c=(0.952849, 7.9e-05), alpha=None, beta=None, gamma=None, k1=0.01, B1=0.005)})), phases=((RefinementPhase(path=PosixPath('cifs/GeO2_152_(cod_2300365)-0.cif'), params={}), RefinementPhase(path=PosixPath('cifs/GeO2_154_(cod_9007477)-0.cif'), params={})), (RefinementPhase(path=PosixPath('cifs/ZnO_186_(cod_9004178)-0.cif'), params={}),), (RefinementPhase(path=PosixPath('cifs/Zn2GeO4_148_(cod_9014631)-0.cif'), params={}),)), foms=((0.0,), (0.03536489998694256, 0.035273051226012), (0.13639804324187943,), (0.33427282595205166,)), lattice_strains=((0.0,), (0.000172525492243293, 0.0005005169609892967), (0.0005712782487738224,), (-0.0033382277696805732,)), missing_peaks=[], extra_peaks=[])]
In this pattern, we only have one solution found with Rwp = 12.04 %.
for i in range(len(search_results)):
print(f"Rwp of solution {i} = {search_results[i].refinement_result.lst_data.rwp} %")
Rwp of solution 0 = 12.04 %
Each SearchResult has a .visualize() method to visualize the refined pattern and
missing/extra peaks in the solution. If there are no missing or extra peaks, this option
will not appear.
search_results[0].visualize()
You can also view all the alternative phases in one solution from SearchResult.phases attribute.
print("Phases found in solution 0:")
for i, phases_ in enumerate(search_results[0].phases):
print(f" - Phase {i}: {[phase.path.name for phase in phases_]}")
Phases found in solution 0:
- Phase 0: ['GeO2_152_(cod_2300365)-0.cif', 'GeO2_154_(cod_9007477)-0.cif']
- Phase 1: ['ZnO_186_(cod_9004178)-0.cif']
- Phase 2: ['Zn2GeO4_148_(cod_9014631)-0.cif']
From the result, you can see that for the phase GeO2, the algorithm identifies two
similar phases with slightly different spacegroups (152 and 154).