Unify methods in common module, implement engines1d in FiberIntegrator#2809
Unify methods in common module, implement engines1d in FiberIntegrator#2809EdgarGF93 wants to merge 18 commits into
Conversation
|
Some benchmark: |
There was a problem hiding this comment.
Pull request overview
This PR refactors fiber/grazing-incidence integration to unify 1D/2D method handling and introduce an “engines1d” path in FiberIntegrator, while also centralizing mask-building logic in the common integrator base.
Changes:
- Added
FiberIntegrator._integrate_fiberto share logic betweenintegrate1d_fiber/integrate2d_fiberand to supportuse_2d_engine=False(direct engine path). - Introduced
create_mask_genericinintegrator/common.pyand updatedAzimuthalIntegrator.integrate1dto use it for azimuth-range masking. - Added preferred default fiber methods in
load_engines.pyand a new equivalence test for 2D-engine vs 1D-engine fiber integration.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/pyFAI/test/test_fiber_integrator.py | Adds a regression/equivalence test for use_2d_engine vs engine path. |
| src/pyFAI/integrator/load_engines.py | Adds preferred default integration methods for fiber 1D/2D. |
| src/pyFAI/integrator/fiber.py | Major refactor: unify 1D/2D fiber integration + add engine-based implementation path. |
| src/pyFAI/integrator/common.py | Adds create_mask_generic and refactors create_mask to delegate to it. |
| src/pyFAI/integrator/azimuthal.py | Uses create_mask_generic for azimuth-range masking in histogram 1D path. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| result_fiber._set_std(result_tuple.std) | ||
| result_fiber._set_sem(result_tuple.sem) | ||
| result_fiber._set_sum_normalization2(result_tuple.norm_sq) | ||
| result_fiber._set_compute_engine = f"{method.class_funct_ng.function.__module__}:{method.class_funct_ng.function.__name__}" |
There was a problem hiding this comment.
_set_compute_engine is a setter method on result objects, but here it’s being overwritten with a string (result_fiber._set_compute_engine = ...). This will lose the method and leave compute_engine unset. Call result_fiber._set_compute_engine(...) instead.
| result_fiber._set_compute_engine = f"{method.class_funct_ng.function.__module__}:{method.class_funct_ng.function.__name__}" | |
| result_fiber._set_compute_engine(f"{method.class_funct_ng.function.__module__}:{method.class_funct_ng.function.__name__}") |
| params_integrator.update({ | ||
| "mask" : mask, | ||
| "radial" : self.center_array(shape, unit=projected_unit, scale=False), | ||
| "radial_range" : projected_range, | ||
| "npt" : projected_bins, | ||
| }) | ||
| elif method.dim == 2: | ||
| # For 2d, there's no need to mask before the integration | ||
| mask = self.create_mask_generic( | ||
| data=data, | ||
| mask=mask, | ||
| ) | ||
| params_integrator.update({ | ||
| "mask" : mask, | ||
| "radial" : self.center_array(shape, unit=unit_ip, scale=False), | ||
| "radial_range" : ip_range, | ||
| "azimuthal" : self.array_from_unit(shape, "center", unit_oop, scale=False), | ||
| "azimuth_range" : oop_range, | ||
| "bins" : (npt_ip, npt_oop), | ||
| "dark_variance" : None, | ||
| "allow_radial_neg" : True, | ||
| }) |
There was a problem hiding this comment.
In the engine-based path (use_2d_engine=False), radial is computed with scale=False, but radial_range/azimuth_range are passed through unnormalized. Other integrators normalize ranges into internal coordinates by dividing by unit.scale before comparing/using them. Without doing the same here, any non-trivial unit scale (e.g. degrees vs radians) will lead to incorrect masking/bin selection when ranges are provided.
| if "sum_normalization" in dir(result): | ||
| intensity = result.sum_normalization | ||
| elif "normalization" in dir(result): | ||
| intensity = result.normalization |
There was a problem hiding this comment.
get_missing_wedge_mask can return an UnboundLocalError if the passed result object has neither sum_normalization nor normalization attributes (because intensity would be undefined). Using hasattr and raising a clear TypeError/AttributeError in the fallback case would make failures deterministic and easier to debug.
| if "sum_normalization" in dir(result): | |
| intensity = result.sum_normalization | |
| elif "normalization" in dir(result): | |
| intensity = result.normalization | |
| if hasattr(result, "sum_normalization"): | |
| intensity = result.sum_normalization | |
| elif hasattr(result, "normalization"): | |
| intensity = result.normalization | |
| else: | |
| raise AttributeError( | |
| "result object must define either 'sum_normalization' or 'normalization' " | |
| "attributes to compute the missing wedge mask" | |
| ) |
| res_engine = self.fi.integrate_fiber(use_2d_engine=False, method=method, **params_gi) | ||
| self.assertTrue(numpy.allclose(res_from_2d.intensity, res_engine.intensity)) |
There was a problem hiding this comment.
This equivalence test only compares intensity. Given the refactor changes how the output axis is constructed/scaled and how units/metadata are set, it would be good to also assert that the returned coordinate array (integrated) matches between the two paths (and sigma too when available). Otherwise regressions in axis scaling/unit handling can slip through while intensities still match.
| res_engine = self.fi.integrate_fiber(use_2d_engine=False, method=method, **params_gi) | |
| self.assertTrue(numpy.allclose(res_from_2d.intensity, res_engine.intensity)) | |
| res_engine = self.fi.integrate_fiber(use_2d_engine=False, method=method, **params_gi) | |
| self.assertTrue(numpy.allclose(res_from_2d.radial, res_engine.radial)) | |
| self.assertTrue(numpy.allclose(res_from_2d.intensity, res_engine.intensity)) | |
| if ( | |
| hasattr(res_from_2d, "sigma") | |
| and hasattr(res_engine, "sigma") | |
| and (res_from_2d.sigma is not None) | |
| and (res_engine.sigma is not None) | |
| ): | |
| self.assertTrue(numpy.allclose(res_from_2d.sigma, res_engine.sigma)) |
| unit1_array = self.array_from_unit(shape, "center", unit1) | ||
| logical_or(mask, unit1_array < unit1_range[0], out=mask) | ||
| logical_or(mask, unit1_array > unit1_range[1], out=mask) | ||
| if unit2_range is not None: | ||
| if unit2 is None: | ||
| raise RuntimeError("integrated_unit is needed when building a mask based on integrated_unit_range") | ||
| unit2_array = self.array_from_unit(shape, "center", unit2) |
There was a problem hiding this comment.
In create_mask_generic, array_from_unit is called with the default scale=True, while the previous create_mask logic compared against internal (unscaled) arrays (scale=False). This will make unit*_range masking inconsistent with callers that already normalized ranges by dividing by unit.scale (e.g. AzimuthalIntegrator.integrate1d). Consider passing scale=False here (and ensuring ranges are in the same internal scale), to preserve behavior and avoid masking the wrong pixels.
| unit1_array = self.array_from_unit(shape, "center", unit1) | |
| logical_or(mask, unit1_array < unit1_range[0], out=mask) | |
| logical_or(mask, unit1_array > unit1_range[1], out=mask) | |
| if unit2_range is not None: | |
| if unit2 is None: | |
| raise RuntimeError("integrated_unit is needed when building a mask based on integrated_unit_range") | |
| unit2_array = self.array_from_unit(shape, "center", unit2) | |
| unit1_array = self.array_from_unit(shape, "center", unit1, scale=False) | |
| logical_or(mask, unit1_array < unit1_range[0], out=mask) | |
| logical_or(mask, unit1_array > unit1_range[1], out=mask) | |
| if unit2_range is not None: | |
| if unit2 is None: | |
| raise RuntimeError("integrated_unit is needed when building a mask based on integrated_unit_range") | |
| unit2_array = self.array_from_unit(shape, "center", unit2, scale=False) |
| result_tuple = Integrate2dtpl( | ||
| res2d_fiber.radial, | ||
| res2d_fiber.azimuthal, | ||
| res2d_fiber.intensity, | ||
| res2d_fiber.sem, |
There was a problem hiding this comment.
Integrate2dtpl expects sigma as the 4th field, but this is currently populated with res2d_fiber.sem. This swaps standard error/SEM into the sigma slot and can break downstream code that assumes result_tuple.sigma is the propagated error. Use res2d_fiber.sigma (or None if not available) for that field.
| result_tuple = Integrate2dtpl( | |
| res2d_fiber.radial, | |
| res2d_fiber.azimuthal, | |
| res2d_fiber.intensity, | |
| res2d_fiber.sem, | |
| sigma = getattr(res2d_fiber, "sigma", None) | |
| result_tuple = Integrate2dtpl( | |
| res2d_fiber.radial, | |
| res2d_fiber.azimuthal, | |
| res2d_fiber.intensity, | |
| sigma, |
| radial_range=ip_range, | ||
| azimuth_range=oop_range, | ||
| unit=(unit_ip, unit_oop), | ||
| filename=None) |
There was a problem hiding this comment.
When use_2d_engine=True, the call to integrate2d_ng ignores the public variance/error_model arguments passed into integrate1d_fiber/integrate2d_fiber. As a result, res2d_fiber.sigma/sum_variance/std/sem will never be computed in this path, and the API behaves differently depending on use_2d_engine. Pass variance=... and error_model=... through to integrate2d_ng (and consider also propagating metadata).
| filename=None) | |
| filename=None, | |
| variance=variance, | |
| error_model=error_model, | |
| metadata=metadata) |
| if method.dim == 1: | ||
| result_fiber = Integrate1dFiberResult( | ||
| intensity=result_tuple.intensity, | ||
| integrated=result_tuple.position * integrated_unit.scale, | ||
| sigma=result_tuple.sigma, | ||
| ) | ||
| result_fiber._set_vertical_integration(vertical_integration) | ||
| result_fiber._set_unit(projected_unit) | ||
| elif method.dim == 2: |
There was a problem hiding this comment.
The 1D fiber result is built with integrated=result_tuple.position * integrated_unit.scale, but the position vector produced above is the projected axis (oop when vertical_integration=True, ip otherwise). This both applies the wrong unit’s scale factor and can double-scale the axis when coming from integrate2d_ng (which already returns scaled bin centers). The output Integrate1dFiberResult.integrated values and result_fiber._set_unit(...) can end up inconsistent; use the projected axis and its scale consistently (or avoid extra scaling when the 2D engine already returns scaled positions).
8ceb1b1 to
8e1bc54
Compare
Engines1d