@@ -505,6 +505,8 @@ def asu_structure_from_cif(
505505 species_by_name : dict [str , Species ] = {}
506506 wyckoff_sites : list [WyckoffSite ] = []
507507 warning_uncertainties : list [Any ] = []
508+ ambiguous_warning_count = 0
509+ debug_uncertainties : list [Any ] = []
508510 warned_type_symbols : set [str ] = set ()
509511 for index , coordinate in enumerate (coordinates ):
510512 if occupancies_exact is not None and occupancies_exact [index ] is not None :
@@ -560,6 +562,11 @@ def asu_structure_from_cif(
560562 declared_position , declaration , declaration_error , declared_positions = _declared_wyckoff_position (
561563 declared_wyckoff , declared_multiplicities , declared_site_symmetry_orders , index , setting , standard
562564 )
565+ coarse_uncertainty = uncertainty is not None and uncertainty [0 ] >= CIF_POSITIONAL_UNCERTAINTY_WARNING ** 2
566+ classifiable_uncertainty = (
567+ uncertainty is not None and uncertainty [0 ] < CIF_POSITIONAL_UNCERTAINTY_ERROR ** 2 and coarse_uncertainty
568+ )
569+ matched_letters : set [str ] | None = set () if classifiable_uncertainty and declared_position is None else None
563570 orbit_screen : list [tuple [tuple [float , float , float ], Any , FracVector ]] | None = (
564571 [] if declaration is not None or repair else None
565572 )
@@ -625,6 +632,7 @@ def asu_structure_from_cif(
625632 allow_large_cif_uncertainty = allow_large_cif_uncertainty ,
626633 positions = declared_positions ,
627634 orbit_screen = orbit_screen ,
635+ matched_letters = matched_letters ,
628636 )
629637 if match is None :
630638 declaration_error = f"does not lie on any position allowed by declared { declaration } "
@@ -639,6 +647,7 @@ def asu_structure_from_cif(
639647 assert declaration is not None # an error always describes a present declaration
640648 ignored_declaration = declaration , declaration_error
641649 declaration = None
650+ matched_letters = set () if classifiable_uncertainty else None
642651 match = _snap (
643652 standard ,
644653 standard_point ,
@@ -651,6 +660,7 @@ def asu_structure_from_cif(
651660 allow_large_cif_uncertainty = allow_large_cif_uncertainty ,
652661 orbit_screen = orbit_screen ,
653662 general_screen = general_screen ,
663+ matched_letters = matched_letters ,
654664 )
655665 elif declaration is None :
656666 match = _snap (
@@ -665,6 +675,7 @@ def asu_structure_from_cif(
665675 allow_large_cif_uncertainty = allow_large_cif_uncertainty ,
666676 orbit_screen = orbit_screen ,
667677 general_screen = general_screen ,
678+ matched_letters = matched_letters ,
668679 )
669680 if repair and declaration is None and match is not None :
670681 assert orbit_screen is not None
@@ -679,6 +690,7 @@ def asu_structure_from_cif(
679690 coordinate_bounds = coordinate_bounds ,
680691 allow_large_cif_uncertainty = allow_large_cif_uncertainty ,
681692 most_specific = True ,
693+ matched_letters = matched_letters ,
682694 )
683695 if corrected_match != match :
684696 assert corrected_match is not None
@@ -718,13 +730,26 @@ def asu_structure_from_cif(
718730 and matched_position .free_count != 3
719731 and matched_position .parameters_of (standard_point ) is not None
720732 )
721- if not exact_special and uncertainty is not None and uncertainty [0 ] >= CIF_POSITIONAL_UNCERTAINTY_WARNING ** 2 :
722- warning_uncertainties .append (uncertainty [0 ])
733+ if not exact_special and uncertainty is not None :
734+ if uncertainty [0 ] >= CIF_POSITIONAL_UNCERTAINTY_ERROR ** 2 :
735+ warning_uncertainties .append (uncertainty [0 ])
736+ elif classifiable_uncertainty and matched_letters is not None and len (matched_letters ) > 1 :
737+ warning_uncertainties .append (uncertainty [0 ])
738+ ambiguous_warning_count += 1
739+ elif classifiable_uncertainty :
740+ debug_uncertainties .append (uncertainty [0 ])
723741
724742 if warning_uncertainties :
725743 maximum = max (warning_uncertainties )
726744 _cif_warning (
727- f"CIF block has { len (warning_uncertainties )} site(s) with projected positional uncertainty; "
745+ f"CIF block has { len (warning_uncertainties )} site(s) with projected positional uncertainty"
746+ f"{ f'; { ambiguous_warning_count } had ambiguous Wyckoff matches' if ambiguous_warning_count else '' } ; "
747+ f"maximum is { math .sqrt (maximum .to_float ()):.6g} Å"
748+ )
749+ if debug_uncertainties :
750+ maximum = max (debug_uncertainties )
751+ _cif_debug (
752+ f"CIF block has { len (debug_uncertainties )} site(s) with projected positional uncertainty; "
728753 f"maximum is { math .sqrt (maximum .to_float ()):.6g} Å"
729754 )
730755
@@ -1337,6 +1362,11 @@ def _cif_warning(message: str) -> None:
13371362 logging .getLogger (__name__ ).warning (message , extra = {"context" : "cif" })
13381363
13391364
1365+ def _cif_debug (message : str ) -> None :
1366+ """Send one low-confidence but unambiguous CIF diagnostic through the report channel."""
1367+ logging .getLogger (__name__ ).debug (message , extra = {"context" : "cif" })
1368+
1369+
13401370def _block_name (data : Mapping [str , Any ]) -> str :
13411371 """The CIF data-block name retained by the repair reader bridge."""
13421372 return str (data .get ("_httk_atomistic_block_name" , "<unnamed>" ))
@@ -1780,12 +1810,14 @@ def _snap(
17801810 positions : Sequence [Any ] | None = None ,
17811811 orbit_screen : list [tuple [tuple [float , float , float ], Any , FracVector ]] | None = None ,
17821812 general_screen : _GeneralPositionScreen | None = None ,
1813+ matched_letters : set [str ] | None = None ,
17831814) -> tuple [str , FracVector ] | None :
17841815 """The most specific Wyckoff position within ``tolerance``, and its free parameters.
17851816
17861817 Floating point screens branch candidates at twice the tolerance, but exact distance and
17871818 per-component coordinate bounds still decide every result. ``positions`` limits the search
17881819 to authoritative CIF declarations; otherwise every standard position is tried in its established order.
1820+ ``matched_letters`` collects every compatible Wyckoff letter and forces the complete candidate traversal.
17891821 """
17901822 from httk .atomistic .symmetry .recognition import _cartesian_distance_squared
17911823
@@ -1799,7 +1831,7 @@ def inside_coordinate_bounds(candidate: FracVector) -> bool:
17991831 for own , snapped , bound in zip (own_point .to_fractions (), candidate .to_fractions (), coordinate_bounds )
18001832 )
18011833
1802- def accepted_parameters (branch : Any , parameters : FracVector , * , general : bool ) -> FracVector | None :
1834+ def accepted_parameters (branch : Any , parameters : FracVector , * , general : bool , letter : str ) -> FracVector | None :
18031835 candidate = branch .coordinate (parameters )
18041836 if not identity :
18051837 candidate = transform .to_setting (candidate )
@@ -1815,6 +1847,8 @@ def accepted_parameters(branch: Any, parameters: FracVector, *, general: bool) -
18151847 return None
18161848 if general or candidate .normalize () != own_point .normalize ():
18171849 reject_large_uncertainty ()
1850+ if matched_letters is not None :
1851+ matched_letters .add (letter )
18181852 return parameters
18191853
18201854 def finish (letter : str , parameters : FracVector ) -> tuple [str , FracVector ]:
@@ -1872,6 +1906,8 @@ def reject_large_uncertainty() -> None:
18721906 general = standard .wyckoff [- 1 ]
18731907 parameters = general .representative .parameters_of (standard_point )
18741908 assert general .free_count == 3 and parameters is not None
1909+ if matched_letters is not None :
1910+ matched_letters .add (general .letter )
18751911 return finish (general .letter , parameters )
18761912
18771913 limit = tolerance * tolerance
@@ -1901,14 +1937,22 @@ def reject_large_uncertainty() -> None:
19011937 )
19021938 deferred : list [tuple [Any , Any ]] = []
19031939 matches : list [tuple [int , str , FracVector ]] = []
1940+ collecting = matched_letters is not None
1941+ preferred_match : tuple [str , FracVector ] | None = None
19041942 for position in candidates :
19051943 if exact_first and position .free_count == 3 :
19061944 reject_large_uncertainty ()
19071945 for deferred_position , deferred_branch in deferred :
19081946 parameters = deferred_branch .nearest_parameters (standard_point )
1909- parameters = accepted_parameters (deferred_branch , parameters , general = False )
1947+ parameters = accepted_parameters (
1948+ deferred_branch , parameters , general = False , letter = deferred_position .letter
1949+ )
19101950 if parameters is not None :
1911- return finish (deferred_position .letter , parameters )
1951+ if not collecting :
1952+ return finish (deferred_position .letter , parameters )
1953+ if not most_specific and preferred_match is None :
1954+ preferred_match = deferred_position .letter , parameters
1955+ matches .append ((deferred_position .multiplicity , deferred_position .letter , parameters ))
19121956 exact_first = False
19131957 for branch in position .branches :
19141958 projection = (
@@ -1925,7 +1969,14 @@ def reject_large_uncertainty() -> None:
19251969 if exact_first and position .free_count != 3 and exact_candidate :
19261970 parameters = branch .parameters_of (standard_point )
19271971 if parameters is not None :
1928- return finish (position .letter , parameters )
1972+ if not collecting :
1973+ return finish (position .letter , parameters )
1974+ assert matched_letters is not None
1975+ matched_letters .add (position .letter )
1976+ matches .append ((position .multiplicity , position .letter , parameters ))
1977+ if not most_specific and preferred_match is None :
1978+ preferred_match = position .letter , parameters
1979+ continue
19291980 if screen is not None and projected is not None :
19301981 assert float_geometry is not None
19311982 basis , point , matrix , vector = float_geometry
@@ -1948,22 +1999,32 @@ def reject_large_uncertainty() -> None:
19481999 deferred .append ((position , branch ))
19492000 continue
19502001 parameters = branch .nearest_parameters (standard_point )
1951- parameters = accepted_parameters (branch , parameters , general = position .free_count == 3 )
2002+ parameters = accepted_parameters (
2003+ branch , parameters , general = position .free_count == 3 , letter = position .letter
2004+ )
19522005 if parameters is not None :
1953- if not most_specific :
2006+ if not most_specific and not collecting :
19542007 return finish (position .letter , parameters )
2008+ if not most_specific and preferred_match is None :
2009+ preferred_match = position .letter , parameters
19552010 matches .append ((position .multiplicity , position .letter , parameters ))
19562011 break
19572012 if exact_first :
19582013 reject_large_uncertainty ()
19592014 for position , branch in deferred :
19602015 parameters = branch .nearest_parameters (standard_point )
1961- parameters = accepted_parameters (branch , parameters , general = False )
2016+ parameters = accepted_parameters (branch , parameters , general = False , letter = position . letter )
19622017 if parameters is not None :
1963- return finish (position .letter , parameters )
2018+ if not collecting :
2019+ return finish (position .letter , parameters )
2020+ if not most_specific and preferred_match is None :
2021+ preferred_match = position .letter , parameters
2022+ matches .append ((position .multiplicity , position .letter , parameters ))
19642023 if not matches :
19652024 return None
1966- _ , letter , parameters = min (matches )
2025+ if preferred_match is not None :
2026+ return finish (* preferred_match )
2027+ _ , letter , parameters = min (matches , key = lambda match : (match [0 ], match [1 ]))
19672028 return finish (letter , parameters )
19682029
19692030
0 commit comments