Skip to content

[Architecture] Decouple protocol compute logic from file I/O for XSAnalysis/Protocols.py #1

Description

@swu4bnl

Background

Most data processing protocols in SciAnalysis/XSAnalysis/Protocols.py currently intermix scientific computation and file I/O within their run() methods. While this works for legacy file-based batch processing, it limits future extensibility, such as:

  • API use (e.g., GUI, cloud batch jobs)
  • AI-driven automation
  • Integration with tiled data services, in-memory channels, etc.

Re-engineering is needed to improve reusability and maintainability for modern applications.

Identified Problems

  • Protocols (e.g., circular_average, SectorAverage, GraininessAnalysis, etc.) combine core calculations and all saving/plotting/export in the same function. This prevents using results in non-file-based workflows.
  • Many protocols require output_dir even if no files will be written.
  • Protocols read dependencies (e.g., fit results, 1D data) from disk, hard-wiring workflows to file-based outputs.

Proposed Solution

  • Create a parallel set of new protocol classes (e.g., in ProtocolsV2.py) that split the computation (pure in-memory results) from file I/O.
  • New base class:
    class ProtocolBase:
        def compute(self, data, **kwargs) -> dict:
            """Pure computation; returns results dict (no I/O)."""
            raise NotImplementedError
    
        def run(self, data, output_dir, **run_args):
            results = self.compute(data, **run_args)
            self._save(results, data, output_dir, **run_args)
            return results
    
        def _save(self, results, data, output_dir, **run_args):
            pass
  • Rewrite key protocols with this pattern.
  • Pass dependencies explicitly as arguments (not via disk reads).

Example: CircularAverage

class CircularAverage(ProtocolBase):
    def compute(self, data, bins_relative=1.0, trim_range=None, **kwargs) -> dict:
        line = data.circular_average_q_bin(error=True, bins_relative=bins_relative)
        if trim_range is not None:
            line.trim(trim_range[0], trim_range[1])
        return {'line': line}
    def _save(self, results, data, output_dir, save_results=(), **run_args):
        line = results['line']
        if 'txt' in save_results:
            line.save_data(self.get_outfile(data.name, output_dir, ext='.dat'))
        if 'plots' in save_results:
            line.plot(save=self.get_outfile(data.name, output_dir), **run_args)

Tasks

  • Create ProtocolsV2.py (or equivalent module)
  • Implement ProtocolBase class
  • Convert priority protocols to new pattern
  • Add tests for compute() (no file I/O)
  • Add example: API-style in-memory protocol chaining

Not In Scope

  • Modifying/renaming/deleting any existing protocol or code in Protocols.py
  • Breaking legacy batch processing interfaces
  • Refactoring data source/result abstractions (separate issue)

Related

  • Future: data source/result sink abstraction
  • Future: composable protocol pipelines

This refactor will enable new use cases (APIs, GUIs, AI/auto-processing) without breaking existing scripts.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions