diff --git a/pyproject.toml b/pyproject.toml index bdfecd61..c612dfc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -186,10 +186,8 @@ benchmark = [ # Bridge Module - External system integration # Use: pip install scitex[bridge] -bridge = [ - "matplotlib", - "scipy", -] +# Real implementation lives in the standalone scitex-bridge package. +bridge = ["scitex-bridge>=0.1.0"] # Container Module - Unified container management (Apptainer + Docker) # Use: pip install scitex[container] diff --git a/src/scitex/bridge/__init__.py b/src/scitex/bridge/__init__.py index 2f1629d7..6b23852a 100755 --- a/src/scitex/bridge/__init__.py +++ b/src/scitex/bridge/__init__.py @@ -1,119 +1,20 @@ -#!/usr/bin/env python3 -# File: ./src/scitex/bridge/__init__.py -# Time-stamp: "2024-12-09 09:30:00 (ywatanabe)" -""" -SciTeX Bridge Module - Cross-module adapters and transformations. - -This module provides the official API for connecting SciTeX modules: -- stats ↔ plt: Statistical results to plot annotations -- stats ↔ vis: Statistical results to vis annotations -- plt ↔ vis: Matplotlib figures to vis FigureModel - -Design Principles: -- Bridge functions use only public APIs of each module -- All transformations go through schema validation -- Single source of truth for cross-module conventions -- Protocol versioning for forward/backward compatibility - -Protocol Version: 1.0.0 - -Coordinate Conventions: -- plt bridge: Uses axes coordinates (0-1 normalized) -- vis bridge: Uses data coordinates (actual x/y values) -- See COORDINATE_SYSTEMS for full definitions - -Usage: - from scitex.bridge import ( - # Protocol version - BRIDGE_PROTOCOL_VERSION, - check_protocol_compatibility, +"""SciTeX bridge — thin compatibility shim for scitex-bridge. - # Stats to Plt - add_stat_to_axes, - extract_stats_from_axes, +Aliases ``scitex.bridge`` to the standalone ``scitex_bridge`` package via +``sys.modules``. ``scitex.bridge is scitex_bridge``. - # Stats to Vis - stat_result_to_annotation, - add_stats_to_figure_model, - - # Plt to Vis - figure_to_vis_model, - axes_to_vis_axes, - tracking_to_plot_configs, - ) +Install: ``pip install scitex[bridge]`` (or ``pip install scitex-bridge``). +See: https://github.com/ywatanabe1989/scitex-bridge """ -# Stats ↔ Plt bridges -# FigRecipe integration (optional) -from scitex.bridge._figrecipe import ( - FIGRECIPE_AVAILABLE, - has_figrecipe, - load_recipe, - save_with_recipe, -) - -# High-level helpers -from scitex.bridge._helpers import add_stats_from_results - -# Plt ↔ Vis bridges -from scitex.bridge._plt_vis import ( - axes_to_vis_axes, - collect_figure_data, - figure_to_vis_model, - tracking_to_plot_configs, -) - -# Protocol versioning -from scitex.bridge._protocol import ( - BRIDGE_PROTOCOL_VERSION, - COORDINATE_SYSTEMS, - ProtocolInfo, - add_protocol_metadata, - check_protocol_compatibility, - extract_protocol_metadata, -) -from scitex.bridge._stats_plt import ( - add_stat_to_axes, - extract_stats_from_axes, - format_stat_for_plot, -) - -# Stats ↔ Vis bridges -from scitex.bridge._stats_vis import ( - add_stats_to_figure_model, - position_stat_annotation, - stat_result_to_annotation, -) - -__all__ = [ - # Protocol - "BRIDGE_PROTOCOL_VERSION", - "ProtocolInfo", - "check_protocol_compatibility", - "add_protocol_metadata", - "extract_protocol_metadata", - "COORDINATE_SYSTEMS", - # Stats ↔ Plt - "add_stat_to_axes", - "extract_stats_from_axes", - "format_stat_for_plot", - # Stats ↔ Vis - "stat_result_to_annotation", - "add_stats_to_figure_model", - "position_stat_annotation", - # Plt ↔ Vis - "figure_to_vis_model", - "axes_to_vis_axes", - "tracking_to_plot_configs", - "collect_figure_data", - # High-level helpers - "add_stats_from_results", - # FigRecipe integration - "save_with_recipe", - "load_recipe", - "has_figrecipe", - "FIGRECIPE_AVAILABLE", -] +import sys as _sys +try: + import scitex_bridge as _real +except ImportError as _e: # pragma: no cover + raise ImportError( + "scitex.bridge requires the 'scitex-bridge' package. " + "Install with: pip install scitex[bridge] (or: pip install scitex-bridge)" + ) from _e -# EOF +_sys.modules[__name__] = _real diff --git a/src/scitex/bridge/_figrecipe.py b/src/scitex/bridge/_figrecipe.py deleted file mode 100755 index d33ad5c1..00000000 --- a/src/scitex/bridge/_figrecipe.py +++ /dev/null @@ -1,284 +0,0 @@ -#!/usr/bin/env python3 -"""Bridge adapter for figrecipe integration. - -This module provides functions to save figures with both: -- SigmaPlot-compatible CSV (scitex format) -- figrecipe YAML recipe (reproducible figures) - -The FTS bundle structure: - figure/ - ├── recipe.yaml # Source of truth (figrecipe format) - ├── recipe_data/ # Large arrays (if needed) - ├── plot.csv # SigmaPlot combined CSV (derived) - ├── plot.png # Primary image (derived) - └── meta.yaml # FTS metadata (optional) -""" - -from pathlib import Path -from typing import Any, Dict, Optional, Union - -# Check figrecipe availability -try: - import figrecipe as fr - from figrecipe._serializer import save_recipe as _fr_save_recipe - - FIGRECIPE_AVAILABLE = True -except ImportError: - FIGRECIPE_AVAILABLE = False - - -def save_with_recipe( - fig, - path: Union[str, Path], - include_csv: bool = True, - include_recipe: bool = True, - data_format: str = "csv", - dpi: int = 300, - **kwargs, -) -> Dict[str, Path]: - """Save figure with both CSV and figrecipe recipe. - - Parameters - ---------- - fig : FigWrapper or matplotlib Figure - The figure to save. - path : str or Path - Output path. Can be: - - Directory path (creates bundle) - - File path with .zip extension (creates zip bundle) - - File path with image extension (saves image + sidecar files) - include_csv : bool - If True, save SigmaPlot-compatible CSV. - include_recipe : bool - If True, save figrecipe YAML recipe (requires figrecipe). - data_format : str - Format for recipe data: 'csv', 'npz', or 'inline'. - dpi : int - Resolution for image output. - **kwargs - Additional arguments passed to savefig (including facecolor). - - Returns - ------- - dict - Paths to saved files: {'image': Path, 'csv': Path, 'recipe': Path} - """ - from scitex.io.bundle._bundle._storage import get_storage - - path = Path(path) - result = {} - - # Determine if this is a bundle (directory or zip) - is_bundle = path.suffix == ".zip" or path.suffix == "" or path.is_dir() - - if is_bundle: - # Create bundle storage - storage = get_storage(path) - storage.ensure_exists() - - # 1. Save image - use fig.savefig() to get facecolor fix from FigWrapper - image_path = storage.path / "plot.png" - _save_figure_image(fig, image_path, dpi=dpi, **kwargs) - result["image"] = image_path - - # 2. Save SigmaPlot CSV - if include_csv and hasattr(fig, "export_as_csv"): - try: - csv_df = fig.export_as_csv() - if not csv_df.empty: - csv_path = storage.path / "plot.csv" - csv_df.to_csv(csv_path, index=False) - result["csv"] = csv_path - except Exception: - pass # CSV export is optional - - # 3. Save figrecipe recipe - if include_recipe: - recipe_path = _save_recipe_to_path( - fig, storage.path / "recipe.yaml", data_format - ) - if recipe_path: - result["recipe"] = recipe_path - - else: - # Single file save (image + sidecars) - _save_figure_image(fig, path, dpi=dpi, **kwargs) - result["image"] = path - - # Save CSV sidecar - if include_csv and hasattr(fig, "export_as_csv"): - try: - csv_df = fig.export_as_csv() - if not csv_df.empty: - csv_path = path.with_suffix(".csv") - csv_df.to_csv(csv_path, index=False) - result["csv"] = csv_path - except Exception: - pass - - # Save recipe sidecar - if include_recipe: - recipe_path = _save_recipe_to_path( - fig, path.with_suffix(".yaml"), data_format - ) - if recipe_path: - result["recipe"] = recipe_path - - return result - - -def _save_figure_image(fig, path: Path, dpi: int = 300, **kwargs): - """Save figure image using the best available method with facecolor support. - - Uses fig.savefig() when available (FigWrapper or RecordingFigure) to get - the facecolor override fix for transparent figures. - """ - # Check if this is a figrecipe RecordingFigure - use fr.save() for full support - if FIGRECIPE_AVAILABLE: - try: - from figrecipe.utils import RecordingFigure - - if isinstance(fig, RecordingFigure): - # Use figrecipe's save with facecolor support - facecolor = kwargs.pop("facecolor", None) - fr.save( - fig, - path, - save_recipe=False, # Recipe saved separately - dpi=dpi, - facecolor=facecolor, - verbose=False, - **kwargs, - ) - return - except (ImportError, AttributeError): - pass - - # Use fig.savefig() if available (FigWrapper has facecolor fix) - if hasattr(fig, "savefig"): - fig.savefig(path, dpi=dpi, **kwargs) - else: - # Fallback to matplotlib figure's savefig - mpl_fig = fig._fig_mpl if hasattr(fig, "_fig_mpl") else fig - mpl_fig.savefig(path, dpi=dpi, **kwargs) - - -def _save_recipe_to_path( - fig, - path: Path, - data_format: str = "csv", -) -> Optional[Path]: - """Save figrecipe recipe if available. - - Parameters - ---------- - fig : FigWrapper - Figure with optional _figrecipe_recorder attribute. - path : Path - Output path for recipe.yaml. - data_format : str - Format for data: 'csv', 'npz', or 'inline'. - - Returns - ------- - Path or None - Path to saved recipe, or None if not available. - """ - if not FIGRECIPE_AVAILABLE: - return None - - try: - # Check if figure has figrecipe recorder - if hasattr(fig, "_figrecipe_recorder") and fig._figrecipe_enabled: - recorder = fig._figrecipe_recorder - figure_record = recorder.figure_record - - # Capture current figure state into record - _capture_figure_state(fig, figure_record) - - # Save using figrecipe's serializer - _fr_save_recipe( - figure_record, path, include_data=True, data_format=data_format - ) - return path - - # Alternative: if figure was created with fr.subplots() directly - if hasattr(fig, "save_recipe"): - fig.save_recipe(path, include_data=True, data_format=data_format) - return path - - # Diagram figures: d.render() attaches _figrecipe_diagram - if hasattr(fig, "_figrecipe_diagram"): - from figrecipe._diagram._diagram._io import save_diagram_recipe - - save_diagram_recipe(fig._figrecipe_diagram, path) - return path - - except Exception: - pass # Recipe saving is optional - - return None - - -def _capture_figure_state(fig, figure_record): - """Capture current figure state into the record. - - This syncs the matplotlib figure state with the figrecipe record, - ensuring the recipe reflects the final figure appearance. - """ - try: - mpl_fig = fig._fig_mpl if hasattr(fig, "_fig_mpl") else fig - - # Update figure dimensions - figsize = mpl_fig.get_size_inches() - figure_record.figsize = list(figsize) - figure_record.dpi = int(mpl_fig.dpi) - - # Capture style from scitex metadata if available - if hasattr(mpl_fig, "_scitex_theme"): - if not hasattr(figure_record, "style") or figure_record.style is None: - figure_record.style = {} - figure_record.style["theme"] = mpl_fig._scitex_theme - - except Exception: - pass # Non-critical - - -def load_recipe( - path: Union[str, Path], -) -> Any: - """Load figrecipe recipe from FTS bundle. - - Parameters - ---------- - path : str or Path - Path to bundle directory, zip file, or recipe.yaml. - - Returns - ------- - tuple - (fig, axes) reproduced from recipe. - """ - if not FIGRECIPE_AVAILABLE: - raise ImportError("figrecipe is required for loading recipes") - - path = Path(path) - - # Handle bundle paths - if path.is_dir(): - recipe_path = path / "recipe.yaml" - elif path.suffix == ".zip": - # figrecipe can handle zip files directly - recipe_path = path - else: - recipe_path = path - - return fr.reproduce(recipe_path) - - -def has_figrecipe() -> bool: - """Check if figrecipe is available.""" - return FIGRECIPE_AVAILABLE - - -# EOF diff --git a/src/scitex/bridge/_helpers.py b/src/scitex/bridge/_helpers.py deleted file mode 100755 index e3f3e420..00000000 --- a/src/scitex/bridge/_helpers.py +++ /dev/null @@ -1,151 +0,0 @@ -#!/usr/bin/env python3 -# File: ./src/scitex/bridge/_helpers.py -# Time-stamp: "2024-12-09 11:00:00 (ywatanabe)" -""" -High-level helper functions for cross-module operations. - -These helpers provide a unified API for common workflows that span -multiple modules, abstracting away backend-specific details. -""" - -from typing import List, Literal, Union - -# StatResult is now a dict - the GUI-specific StatResult is deprecated -StatResult = dict - - -def add_stats_from_results( - target, - stat_results: Union[StatResult, List[StatResult]], - backend: Literal["auto", "plt", "vis"] = "auto", - format_style: str = "asterisk", - **kwargs, -): - """ - Add statistical results to a figure or axes, auto-detecting backend. - - This is a high-level helper that works with both matplotlib axes - and vis FigureModel, choosing the appropriate bridge function. - - Parameters - ---------- - target : matplotlib.axes.Axes, scitex.plt.AxisWrapper, or FigureModel - The target to add statistics to - stat_results : StatResult or List[StatResult] - Statistical result(s) to add - backend : {"auto", "plt", "vis"} - Backend to use. "auto" detects from target type: - - matplotlib axes or scitex AxisWrapper → "plt" - - FigureModel → "vis" - format_style : str - Format style for stat text ("asterisk", "compact", "detailed", "publication") - **kwargs - Additional arguments passed to the backend-specific function: - - plt: passed to add_stat_to_axes (x, y, transform, etc.) - - vis: passed to add_stats_to_figure_model (axes_index, auto_position, etc.) - - Returns - ------- - target - The modified target (for chaining) - - Examples - -------- - >>> # With matplotlib axes - >>> fig, ax = plt.subplots() - >>> stat = create_stat_result("pearson", "r", 0.85, 0.001) - >>> add_stats_from_results(ax, stat) - - >>> # With vis FigureModel - >>> model = FigureModel(width_mm=170, height_mm=120, axes=[{}]) - >>> add_stats_from_results(model, [stat1, stat2], backend="vis") - - Notes - ----- - Coordinate conventions differ between backends: - - plt: uses axes coordinates (0-1 normalized) by default - - vis: uses data coordinates - - For precise control, use the backend-specific functions directly: - - scitex.bridge.add_stat_to_axes (plt backend) - - scitex.bridge.add_stats_to_figure_model (vis backend) - """ - # Normalize to list - if isinstance(stat_results, StatResult): - stat_results = [stat_results] - - # Auto-detect backend - if backend == "auto": - backend = _detect_backend(target) - - # Dispatch to appropriate function - if backend == "plt": - from scitex.bridge._stats_plt import add_stat_to_axes - - for stat in stat_results: - add_stat_to_axes(target, stat, format_style=format_style, **kwargs) - - elif backend == "vis": - from scitex.bridge._stats_vis import add_stats_to_figure_model - - add_stats_to_figure_model( - target, - stat_results, - format_style=format_style, - **kwargs, - ) - - else: - raise ValueError(f"Unknown backend: {backend}. Use 'auto', 'plt', or 'vis'.") - - return target - - -def _detect_backend(target) -> Literal["plt", "vis"]: - """ - Detect the appropriate backend from target type. - - Parameters - ---------- - target : any - The target object - - Returns - ------- - str - "plt" or "vis" - """ - # Check for vis FigureModel - try: - from scitex.io.bundle.kinds._plot._models import FigureModel - - if isinstance(target, FigureModel): - return "vis" - except ImportError: - pass - - # Check for matplotlib axes - try: - import matplotlib.axes - - if isinstance(target, matplotlib.axes.Axes): - return "plt" - except ImportError: - pass - - # Check for scitex plt wrappers - if hasattr(target, "_axes_mpl"): - return "plt" - if hasattr(target, "_axes_scitex"): - return "plt" - - # Default to plt (most common case) - return "plt" - - -__all__ = [ - "add_stats_from_results", -] - - -# EOF diff --git a/src/scitex/bridge/_plt_vis.py b/src/scitex/bridge/_plt_vis.py deleted file mode 100755 index 8a715ce1..00000000 --- a/src/scitex/bridge/_plt_vis.py +++ /dev/null @@ -1,547 +0,0 @@ -#!/usr/bin/env python3 -# File: ./src/scitex/bridge/_plt_vis.py -# Time-stamp: "2024-12-09 10:00:00 (ywatanabe)" -""" -Bridge module for plt ↔ vis integration. - -Provides adapters to: -- Convert scitex.plt figures to vis FigureModel -- Extract tracking data as PlotModel configurations -- Synchronize matplotlib state with vis JSON -""" - -import warnings -from typing import Any, Dict, List, Optional, Tuple - -# Legacy model imports - deprecated module, suppress warnings -try: - with warnings.catch_warnings(): - warnings.simplefilter("ignore", DeprecationWarning) - from scitex.io.bundle.kinds._plot._models import ( - AnnotationModel, - AxesModel, - AxesStyle, - FigureModel, - GuideModel, - PlotModel, - PlotStyle, - TextStyle, - ) - VIS_MODEL_AVAILABLE = True -except ImportError: - FigureModel = AxesModel = PlotModel = AnnotationModel = None - GuideModel = PlotStyle = AxesStyle = TextStyle = None - VIS_MODEL_AVAILABLE = False - - -def figure_to_vis_model( - fig, - include_data: bool = True, - include_style: bool = True, -) -> FigureModel: - """ - Convert a scitex.plt figure to a vis FigureModel. - - Parameters - ---------- - fig : scitex.plt.FigWrapper or matplotlib.figure.Figure - The figure to convert - include_data : bool - Whether to include plot data in the model - include_style : bool - Whether to include style information - - Returns - ------- - FigureModel - The vis figure model - """ - # Get matplotlib figure - mpl_fig = _get_mpl_figure(fig) - - # Get figure dimensions - width_inch = mpl_fig.get_figwidth() - height_inch = mpl_fig.get_figheight() - dpi = mpl_fig.get_dpi() - - # Convert to mm - width_mm = width_inch * 25.4 - height_mm = height_inch * 25.4 - - # Determine layout from axes - axes_list = mpl_fig.axes - nrows, ncols = _infer_layout(axes_list, mpl_fig) - - # Create figure model - figure_model = FigureModel( - width_mm=width_mm, - height_mm=height_mm, - nrows=nrows, - ncols=ncols, - dpi=int(dpi), - facecolor=_color_to_hex(mpl_fig.get_facecolor()), - edgecolor=_color_to_hex(mpl_fig.get_edgecolor()), - ) - - # Handle suptitle - if hasattr(mpl_fig, "_suptitle") and mpl_fig._suptitle: - figure_model.suptitle = mpl_fig._suptitle.get_text() - figure_model.suptitle_fontsize = mpl_fig._suptitle.get_fontsize() - - # Convert each axes - scitex_axes = _get_scitex_axes(fig) - - for idx, ax in enumerate(axes_list): - row = idx // ncols - col = idx % ncols - - # Find corresponding scitex axis wrapper for history - scitex_ax = _find_scitex_axis(scitex_axes, ax) - - axes_model = axes_to_vis_axes( - ax, - row=row, - col=col, - scitex_ax=scitex_ax, - include_data=include_data, - include_style=include_style, - ) - figure_model.axes.append(axes_model.to_dict()) - - return figure_model - - -def axes_to_vis_axes( - ax, - row: int = 0, - col: int = 0, - scitex_ax=None, - include_data: bool = True, - include_style: bool = True, -) -> AxesModel: - """ - Convert a matplotlib axes to a vis AxesModel. - - Parameters - ---------- - ax : matplotlib.axes.Axes - The axes to convert - row : int - Row position in layout - col : int - Column position in layout - scitex_ax : AxisWrapper, optional - Scitex axis wrapper with tracking history - include_data : bool - Whether to include plot data - include_style : bool - Whether to include style information - - Returns - ------- - AxesModel - The vis axes model - """ - # Get underlying matplotlib axes - mpl_ax = ax._axes_mpl if hasattr(ax, "_axes_mpl") else ax - - # Extract axis properties - axes_model = AxesModel( - row=row, - col=col, - xlabel=mpl_ax.get_xlabel() or None, - ylabel=mpl_ax.get_ylabel() or None, - title=mpl_ax.get_title() or None, - xlim=list(mpl_ax.get_xlim()), - ylim=list(mpl_ax.get_ylim()), - xscale=mpl_ax.get_xscale(), - yscale=mpl_ax.get_yscale(), - ) - - # Extract tick info - xticks = mpl_ax.get_xticks() - yticks = mpl_ax.get_yticks() - if len(xticks) > 0: - axes_model.xticks = [float(t) for t in xticks] - if len(yticks) > 0: - axes_model.yticks = [float(t) for t in yticks] - - # Extract style if requested - if include_style: - axes_model.style = _extract_axes_style(mpl_ax) - - # Extract plots from tracking history - if include_data and scitex_ax and hasattr(scitex_ax, "history"): - plots = tracking_to_plot_configs(scitex_ax.history) - for plot in plots: - axes_model.plots.append( - plot.to_dict() if hasattr(plot, "to_dict") else plot - ) - - # Extract annotations - for text_obj in mpl_ax.texts: - annotation = _text_to_annotation(text_obj) - if annotation: - axes_model.annotations.append(annotation.to_dict()) - - # Extract guides (axhline, axvline, etc.) - guides = _extract_guides(mpl_ax) - for guide in guides: - axes_model.guides.append(guide.to_dict()) - - return axes_model - - -def tracking_to_plot_configs( - history: Dict[str, Tuple], -) -> List[PlotModel]: - """ - Convert scitex.plt tracking history to PlotModel configurations. - - Parameters - ---------- - history : Dict[str, Tuple] - Tracking history from AxisWrapper - Format: {id: (id, method_name, tracked_dict, kwargs)} - - Returns - ------- - List[PlotModel] - List of PlotModel configurations - """ - plots = [] - - for plot_id, (_, method_name, tracked_dict, kwargs) in history.items(): - plot_model = _history_entry_to_plot_model( - plot_id, method_name, tracked_dict, kwargs - ) - if plot_model: - plots.append(plot_model) - - return plots - - -def collect_figure_data( - fig, -) -> Dict[str, Any]: - """ - Collect all data from a figure for export. - - This is a simpler version that just extracts data without - full vis model conversion. - - Parameters - ---------- - fig : scitex.plt.FigWrapper or matplotlib.figure.Figure - The figure to collect data from - - Returns - ------- - Dict[str, Any] - Dictionary with figure data organized by axes/plot - """ - data = { - "figure": {}, - "axes": [], - } - - mpl_fig = _get_mpl_figure(fig) - - # Figure info - data["figure"]["width_mm"] = mpl_fig.get_figwidth() * 25.4 - data["figure"]["height_mm"] = mpl_fig.get_figheight() * 25.4 - data["figure"]["dpi"] = mpl_fig.get_dpi() - - # Get scitex axes for history - scitex_axes = _get_scitex_axes(fig) - - # Collect axes data - for idx, ax in enumerate(mpl_fig.axes): - mpl_ax = ax._axes_mpl if hasattr(ax, "_axes_mpl") else ax - scitex_ax = _find_scitex_axis(scitex_axes, mpl_ax) - - axes_data = { - "index": idx, - "xlabel": mpl_ax.get_xlabel(), - "ylabel": mpl_ax.get_ylabel(), - "title": mpl_ax.get_title(), - "xlim": list(mpl_ax.get_xlim()), - "ylim": list(mpl_ax.get_ylim()), - "plots": [], - } - - # Get plot data from history - if scitex_ax and hasattr(scitex_ax, "history"): - for plot_id, (_, method, tracked, kwargs) in scitex_ax.history.items(): - plot_data = { - "id": plot_id, - "method": method, - "kwargs": {k: v for k, v in kwargs.items() if _is_serializable(v)}, - } - # Extract data arrays from tracked_dict - if "args" in tracked: - plot_data["args"] = [ - _array_to_list(a) for a in tracked["args"] if _is_array_like(a) - ] - axes_data["plots"].append(plot_data) - - data["axes"].append(axes_data) - - return data - - -# ============================================================================= -# Helper Functions -# ============================================================================= - - -def _get_mpl_figure(fig): - """Get the underlying matplotlib figure.""" - if hasattr(fig, "_fig_mpl"): - return fig._fig_mpl - return fig - - -def _get_scitex_axes(fig): - """Get scitex axes wrappers from figure.""" - if hasattr(fig, "_axes_scitex"): - axes = fig._axes_scitex - if hasattr(axes, "flat"): - return list(axes.flat) - return [axes] - return [] - - -def _find_scitex_axis(scitex_axes, mpl_ax): - """Find the scitex axis wrapper that wraps the given mpl axis.""" - for ax in scitex_axes: - if hasattr(ax, "_axes_mpl") and ax._axes_mpl is mpl_ax: - return ax - return None - - -def _infer_layout(axes_list, fig) -> Tuple[int, int]: - """Infer nrows, ncols from axes positions.""" - if not axes_list: - return 1, 1 - - # Check if using gridspec - if hasattr(fig, "_gridspecs") and fig._gridspecs: - gs = fig._gridspecs[0] - return gs.nrows, gs.ncols - - # Fallback: guess from axes count - n = len(axes_list) - if n == 1: - return 1, 1 - elif n == 2: - return 1, 2 - elif n <= 4: - return 2, 2 - else: - # Try to make it roughly square - import math - - ncols = int(math.ceil(math.sqrt(n))) - nrows = int(math.ceil(n / ncols)) - return nrows, ncols - - -def _color_to_hex(color) -> str: - """Convert matplotlib color to hex string.""" - try: - import matplotlib.colors as mcolors - - rgb = mcolors.to_rgb(color) - return f"#{int(rgb[0] * 255):02x}{int(rgb[1] * 255):02x}{int(rgb[2] * 255):02x}" - except (ValueError, TypeError): - return "#ffffff" - - -def _extract_axes_style(mpl_ax) -> AxesStyle: - """Extract style information from matplotlib axes.""" - # Check grid visibility - grid_visible = False - try: - gridlines = mpl_ax.xaxis.get_gridlines() - if gridlines: - grid_visible = gridlines[0].get_visible() - except (AttributeError, IndexError): - pass - - return AxesStyle( - facecolor=_color_to_hex(mpl_ax.get_facecolor()), - grid=grid_visible, - spines_visible={ - "top": mpl_ax.spines["top"].get_visible(), - "right": mpl_ax.spines["right"].get_visible(), - "bottom": mpl_ax.spines["bottom"].get_visible(), - "left": mpl_ax.spines["left"].get_visible(), - }, - ) - - -def _text_to_annotation(text_obj) -> Optional[AnnotationModel]: - """Convert matplotlib text object to AnnotationModel.""" - text = text_obj.get_text() - if not text or not text.strip(): - return None - - pos = text_obj.get_position() - - style = TextStyle( - fontsize=text_obj.get_fontsize(), - color=_color_to_hex(text_obj.get_color()), - ha=text_obj.get_ha(), - va=text_obj.get_va(), - rotation=text_obj.get_rotation(), - ) - - return AnnotationModel( - annotation_type="text", - text=text, - x=pos[0], - y=pos[1], - style=style, - ) - - -def _extract_guides(mpl_ax) -> List[GuideModel]: - """Extract guide lines (axhline, axvline) from axes.""" - guides = [] - - # Check for horizontal lines - for line in mpl_ax.lines: - data = line.get_xydata() - if len(data) >= 2: - # Check if horizontal (y values same) - if data[0][1] == data[-1][1] and data[0][0] != data[-1][0]: - xlim = mpl_ax.get_xlim() - if ( - abs(data[0][0] - xlim[0]) < 0.01 - and abs(data[-1][0] - xlim[1]) < 0.01 - ): - guides.append( - GuideModel( - guide_type="axhline", - y=data[0][1], - color=_color_to_hex(line.get_color()), - linestyle=line.get_linestyle(), - linewidth=line.get_linewidth(), - ) - ) - # Check if vertical - elif data[0][0] == data[-1][0] and data[0][1] != data[-1][1]: - ylim = mpl_ax.get_ylim() - if ( - abs(data[0][1] - ylim[0]) < 0.01 - and abs(data[-1][1] - ylim[1]) < 0.01 - ): - guides.append( - GuideModel( - guide_type="axvline", - x=data[0][0], - color=_color_to_hex(line.get_color()), - linestyle=line.get_linestyle(), - linewidth=line.get_linewidth(), - ) - ) - - return guides - - -def _history_entry_to_plot_model( - plot_id: str, - method_name: str, - tracked_dict: Dict, - kwargs: Dict, -) -> Optional[PlotModel]: - """Convert a tracking history entry to PlotModel.""" - # Map matplotlib methods to vis plot types - method_to_type = { - "plot": "line", - "scatter": "scatter", - "bar": "bar", - "barh": "barh", - "hist": "histogram", - "boxplot": "boxplot", - "violinplot": "violin", - "fill_between": "fill_between", - "errorbar": "errorbar", - "imshow": "imshow", - "contour": "contour", - "contourf": "contourf", - } - - plot_type = method_to_type.get(method_name, method_name) - - # Extract data from tracked_dict - data = {} - if "args" in tracked_dict: - args = tracked_dict["args"] - if method_name in ("plot", "scatter") and len(args) >= 2: - data["x"] = _array_to_list(args[0]) - data["y"] = _array_to_list(args[1]) - elif method_name == "bar" and len(args) >= 2: - data["x"] = _array_to_list(args[0]) - data["height"] = _array_to_list(args[1]) - elif method_name == "hist" and len(args) >= 1: - data["x"] = _array_to_list(args[0]) - - # Extract style from kwargs - style = PlotStyle() - if "color" in kwargs: - style.color = _color_to_hex(kwargs["color"]) if kwargs["color"] else None - if "linewidth" in kwargs or "lw" in kwargs: - style.linewidth = kwargs.get("linewidth") or kwargs.get("lw") - if "linestyle" in kwargs or "ls" in kwargs: - style.linestyle = kwargs.get("linestyle") or kwargs.get("ls") - if "marker" in kwargs: - style.marker = kwargs.get("marker") - if "alpha" in kwargs: - style.alpha = kwargs.get("alpha") - if "label" in kwargs: - style.label = kwargs.get("label") - - return PlotModel( - plot_type=plot_type, - plot_id=plot_id, - data=data, - style=style, - ) - - -def _array_to_list(arr) -> List: - """Convert array-like to list for serialization.""" - if hasattr(arr, "tolist"): - return arr.tolist() - elif isinstance(arr, (list, tuple)): - return list(arr) - return [arr] - - -def _is_array_like(obj) -> bool: - """Check if object is array-like.""" - return hasattr(obj, "__len__") and not isinstance(obj, (str, dict)) - - -def _is_serializable(obj) -> bool: - """Check if object is JSON serializable.""" - import json - - try: - json.dumps(obj) - return True - except (TypeError, ValueError): - return False - - -__all__ = [ - "figure_to_vis_model", - "axes_to_vis_axes", - "tracking_to_plot_configs", - "collect_figure_data", -] - - -# EOF diff --git a/src/scitex/bridge/_protocol.py b/src/scitex/bridge/_protocol.py deleted file mode 100755 index f1cdf72e..00000000 --- a/src/scitex/bridge/_protocol.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File: ./src/scitex/bridge/_protocol.py -# Time-stamp: "2024-12-09 09:30:00 (ywatanabe)" -""" -Bridge Protocol - Versioning and compatibility for cross-module communication. - -This module defines the bridge protocol version and provides utilities -for ensuring compatibility between different versions of scitex modules. - -Protocol Versioning -------------------- -The bridge protocol version follows semantic versioning: -- MAJOR: Breaking changes in bridge interfaces -- MINOR: New bridge functions added (backward compatible) -- PATCH: Bug fixes (backward compatible) - -Usage: - from scitex.bridge import BRIDGE_PROTOCOL_VERSION, check_protocol_compatibility -""" - -from dataclasses import dataclass -from typing import Any, Dict, Optional, Tuple - -# ============================================================================= -# Protocol Version -# ============================================================================= - -BRIDGE_PROTOCOL_VERSION = "1.0.0" -""" -Current bridge protocol version. - -Changes: -- 1.0.0: Initial protocol - - Stats → Plt: add_stat_to_axes, extract_stats_from_axes - - Stats → Vis: stat_result_to_annotation, add_stats_to_figure_model - - Plt → Vis: figure_to_vis_model, axes_to_vis_axes - - Coordinate conventions: axes coords (0-1) for plt, data coords for vis -""" - - -# ============================================================================= -# Protocol Metadata -# ============================================================================= - - -@dataclass -class ProtocolInfo: - """ - Bridge protocol information for serialization and compatibility. - - Parameters - ---------- - version : str - Protocol version string (semver) - source_module : str - Module that created the data - target_module : str - Target module for the data - coordinate_system : str - Coordinate system used ("axes", "data", "figure", "mm", "px") - """ - - version: str = BRIDGE_PROTOCOL_VERSION - source_module: str = "" - target_module: str = "" - coordinate_system: str = "data" - - def to_dict(self) -> Dict[str, Any]: - """Convert to dictionary.""" - return { - "bridge_protocol_version": self.version, - "source_module": self.source_module, - "target_module": self.target_module, - "coordinate_system": self.coordinate_system, - } - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> "ProtocolInfo": - """Create from dictionary.""" - return cls( - version=data.get("bridge_protocol_version", BRIDGE_PROTOCOL_VERSION), - source_module=data.get("source_module", ""), - target_module=data.get("target_module", ""), - coordinate_system=data.get("coordinate_system", "data"), - ) - - -# ============================================================================= -# Compatibility Utilities -# ============================================================================= - - -def parse_version(version: str) -> Tuple[int, int, int]: - """ - Parse a version string into (major, minor, patch) tuple. - - Parameters - ---------- - version : str - Version string like "1.2.3" - - Returns - ------- - tuple - (major, minor, patch) integers - """ - parts = version.split(".") - major = int(parts[0]) if len(parts) > 0 else 0 - minor = int(parts[1]) if len(parts) > 1 else 0 - patch = int(parts[2]) if len(parts) > 2 else 0 - return (major, minor, patch) - - -def check_protocol_compatibility( - data_version: str, - current_version: str = BRIDGE_PROTOCOL_VERSION, -) -> Tuple[bool, Optional[str]]: - """ - Check if a data version is compatible with the current protocol. - - Parameters - ---------- - data_version : str - Version of the data being loaded - current_version : str - Current protocol version (default: BRIDGE_PROTOCOL_VERSION) - - Returns - ------- - tuple - (is_compatible, warning_message) - - is_compatible: True if data can be safely used - - warning_message: None if compatible, else a warning string - - Examples - -------- - >>> is_compat, msg = check_protocol_compatibility("1.0.0") - >>> is_compat - True - - >>> is_compat, msg = check_protocol_compatibility("2.0.0") - >>> is_compat - False - >>> msg - 'Major version mismatch: data v2.0.0, current v1.0.0' - """ - data_major, data_minor, _ = parse_version(data_version) - curr_major, curr_minor, _ = parse_version(current_version) - - # Major version mismatch = incompatible - if data_major != curr_major: - return ( - False, - f"Major version mismatch: data v{data_version}, current v{current_version}", - ) - - # Minor version newer than current = warning (may have unknown fields) - if data_minor > curr_minor: - return ( - True, - f"Data version newer than current: data v{data_version}, " - f"current v{current_version}. Some features may be ignored.", - ) - - return (True, None) - - -def add_protocol_metadata( - data: Dict[str, Any], - source_module: str, - target_module: str, - coordinate_system: str = "data", -) -> Dict[str, Any]: - """ - Add bridge protocol metadata to a dictionary. - - Parameters - ---------- - data : dict - Data dictionary to annotate - source_module : str - Source module name (e.g., "stats", "plt") - target_module : str - Target module name (e.g., "vis", "plt") - coordinate_system : str - Coordinate system used (default: "data") - - Returns - ------- - dict - Data with protocol metadata added - - Examples - -------- - >>> data = {"x": 10, "y": 20} - >>> annotated = add_protocol_metadata(data, "stats", "vis") - >>> annotated["_bridge_protocol"]["bridge_protocol_version"] - '1.0.0' - """ - protocol = ProtocolInfo( - source_module=source_module, - target_module=target_module, - coordinate_system=coordinate_system, - ) - data["_bridge_protocol"] = protocol.to_dict() - return data - - -def extract_protocol_metadata(data: Dict[str, Any]) -> Optional[ProtocolInfo]: - """ - Extract bridge protocol metadata from a dictionary. - - Parameters - ---------- - data : dict - Data dictionary that may contain protocol metadata - - Returns - ------- - ProtocolInfo or None - Protocol info if present, None otherwise - """ - if "_bridge_protocol" in data: - return ProtocolInfo.from_dict(data["_bridge_protocol"]) - return None - - -# ============================================================================= -# Coordinate System Definitions -# ============================================================================= - -COORDINATE_SYSTEMS = { - "axes": { - "description": "Normalized axes coordinates (0-1)", - "x_range": (0.0, 1.0), - "y_range": (0.0, 1.0), - "used_by": ["plt", "matplotlib"], - }, - "data": { - "description": "Data coordinates (actual x/y values)", - "x_range": None, # Depends on data - "y_range": None, - "used_by": ["vis", "FigureModel"], - }, - "figure": { - "description": "Figure coordinates (0-1 over entire figure)", - "x_range": (0.0, 1.0), - "y_range": (0.0, 1.0), - "used_by": ["matplotlib", "suptitle"], - }, - "mm": { - "description": "Physical millimeters", - "x_range": None, # Depends on figure size - "y_range": None, - "used_by": ["vis", "publication"], - }, - "px": { - "description": "Pixels", - "x_range": None, # Depends on DPI and size - "y_range": None, - "used_by": ["canvas", "gui"], - }, -} - - -# ============================================================================= -# Public API -# ============================================================================= - -__all__ = [ - "BRIDGE_PROTOCOL_VERSION", - "ProtocolInfo", - "parse_version", - "check_protocol_compatibility", - "add_protocol_metadata", - "extract_protocol_metadata", - "COORDINATE_SYSTEMS", -] - - -# EOF diff --git a/src/scitex/bridge/_skills/SKILL.md b/src/scitex/bridge/_skills/SKILL.md deleted file mode 100644 index d220425d..00000000 --- a/src/scitex/bridge/_skills/SKILL.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -name: stx.bridge -description: Cross-module adapters connecting stx.stats, stx.plt, and visualization models with protocol versioning. ---- - -# stx.bridge - -The `stx.bridge` module provides official adapters for converting data between SciTeX modules. It connects statistical results to plot annotations and visualization models using only public APIs with schema validation and protocol versioning. - -## Sub-skills - -- [stats-plt-bridge.md](stats-plt-bridge.md) — `add_stat_to_axes`, `extract_stats_from_axes`, `format_stat_for_plot` -- [figrecipe-bridge.md](figrecipe-bridge.md) — `save_with_recipe`, `load_recipe`, `has_figrecipe`, bundle structure -- [protocol-versioning.md](protocol-versioning.md) — `BRIDGE_PROTOCOL_VERSION`, `check_protocol_compatibility`, `ProtocolInfo`, `COORDINATE_SYSTEMS` - -## Quick Reference - -```python -import scitex as stx -from scitex.bridge import ( - add_stat_to_axes, - save_with_recipe, - load_recipe, - FIGRECIPE_AVAILABLE, - BRIDGE_PROTOCOL_VERSION, - check_protocol_compatibility, - COORDINATE_SYSTEMS, -) - -# Stats -> Plt -result = stx.stats.test_ttest_ind(group1, group2) -add_stat_to_axes(ax, result) - -# Save with figrecipe recipe -saved = save_with_recipe(fig, "./my_figure/") - -# Protocol version check -is_compat, msg = check_protocol_compatibility("1.0.0") -``` - -## Module Connections - -| Bridge | Source | Target | Key functions | -|--------|--------|--------|---------------| -| Stats-Plt | `stx.stats` | `stx.plt` | `add_stat_to_axes`, `extract_stats_from_axes` | -| Stats-Vis | `stx.stats` | vis models | `stat_result_to_annotation`, `add_stats_to_figure_model` | -| Plt-Vis | `stx.plt` | vis models | `figure_to_vis_model`, `axes_to_vis_axes` | -| FigRecipe | any | figrecipe | `save_with_recipe`, `load_recipe` | diff --git a/src/scitex/bridge/_skills/figrecipe-bridge.md b/src/scitex/bridge/_skills/figrecipe-bridge.md deleted file mode 100644 index eb0edce7..00000000 --- a/src/scitex/bridge/_skills/figrecipe-bridge.md +++ /dev/null @@ -1,74 +0,0 @@ -# FigRecipe Bridge (stx.bridge) - -The `_figrecipe` bridge enables saving figures with both SigmaPlot-compatible CSV sidecars and figrecipe YAML recipes for reproducibility. - -## Availability Check - -```python -from scitex.bridge import FIGRECIPE_AVAILABLE, has_figrecipe - -if has_figrecipe(): - print("figrecipe is installed") -# FIGRECIPE_AVAILABLE is a bool constant set at import time -``` - -## save_with_recipe - -Save a figure to a bundle directory (or single file) with optional CSV and recipe sidecar: - -```python -from scitex.bridge import save_with_recipe - -fig, ax = stx.plt.subplots() -ax.plot([1, 2, 3], [4, 5, 6]) - -# Save to a directory bundle (creates plot.png, plot.csv, recipe.yaml) -saved = save_with_recipe(fig, "./my_figure/", include_csv=True, include_recipe=True) -print(saved) -# {"image": Path("my_figure/plot.png"), -# "csv": Path("my_figure/plot.csv"), -# "recipe": Path("my_figure/recipe.yaml")} - -# Save as a single image file with sidecars -saved = save_with_recipe(fig, "plot.png", dpi=300) -# Creates: plot.png, plot.csv, plot.yaml -``` - -### Parameters - -| Parameter | Default | Description | -|-----------|---------|-------------| -| `path` | required | Directory, `.zip`, or image file path | -| `include_csv` | `True` | Export SigmaPlot-compatible CSV | -| `include_recipe` | `True` | Save figrecipe YAML recipe | -| `data_format` | `"csv"` | Recipe data format: `"csv"`, `"npz"`, or `"inline"` | -| `dpi` | `300` | Image resolution | - -## load_recipe - -Reproduce a figure from a saved recipe: - -```python -from scitex.bridge import load_recipe - -# From bundle directory -fig, axes = load_recipe("./my_figure/") - -# From recipe.yaml directly -fig, axes = load_recipe("my_figure/recipe.yaml") - -# From zip bundle -fig, axes = load_recipe("my_figure.zip") -``` - -## Bundle Structure - -When saving to a directory or zip, the FTS bundle layout is: - -``` -figure/ -├── recipe.yaml # Source of truth (figrecipe format) -├── plot.csv # SigmaPlot combined CSV (derived from recorded data) -├── plot.png # Primary image (derived) -└── meta.yaml # FTS metadata (optional) -``` diff --git a/src/scitex/bridge/_skills/figrecipe.md b/src/scitex/bridge/_skills/figrecipe.md deleted file mode 100644 index 1e6d7b06..00000000 --- a/src/scitex/bridge/_skills/figrecipe.md +++ /dev/null @@ -1,58 +0,0 @@ ---- -description: Save figures with figrecipe recipe metadata using save_with_recipe(), reload figures from recipe files with load_recipe(), and check figrecipe availability with has_figrecipe(). ---- - -# FigRecipe Integration - -## save_with_recipe - -Save a figure alongside a figrecipe `.yaml` recipe file. - -```python -save_with_recipe(fig, path: str, **kwargs) -> dict -``` - -```python -import scitex as stx - -fig, ax = stx.plt.subplots() -ax.plot([1, 2, 3], [4, 5, 6]) - -# Saves both plot.png and plot.yaml (figrecipe recipe) -stx.bridge.save_with_recipe(fig, "plot.png") -``` - ---- - -## load_recipe - -Load a figrecipe `.yaml` recipe file and return the metadata. - -```python -load_recipe(path: str) -> dict -``` - -```python -import scitex as stx - -recipe = stx.bridge.load_recipe("plot.yaml") -print(recipe["plots"][0]["type"]) # 'line' -``` - ---- - -## has_figrecipe / FIGRECIPE_AVAILABLE - -Check whether figrecipe is installed. - -```python -import scitex as stx - -if stx.bridge.has_figrecipe(): - stx.bridge.save_with_recipe(fig, "plot.png") -else: - print("Install figrecipe: pip install figrecipe") - -# Or use the module-level flag -print(stx.bridge.FIGRECIPE_AVAILABLE) # True / False -``` diff --git a/src/scitex/bridge/_skills/plt-vis.md b/src/scitex/bridge/_skills/plt-vis.md deleted file mode 100644 index e38062ec..00000000 --- a/src/scitex/bridge/_skills/plt-vis.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -description: Convert matplotlib Figure objects to vis FigureModel with figure_to_vis_model(), convert axes with axes_to_vis_axes(), collect tracked data with collect_figure_data(), and convert tracking records with tracking_to_plot_configs(). ---- - -# Plt to Vis Bridge - -## figure_to_vis_model - -Convert a matplotlib `Figure` to a vis `FigureModel`. - -```python -figure_to_vis_model(fig) -> dict -``` - -```python -import scitex as stx -import matplotlib.pyplot as plt - -fig, ax = stx.plt.subplots() -ax.plot([1, 2, 3], [4, 5, 6]) - -model = stx.bridge.figure_to_vis_model(fig) -``` - ---- - -## axes_to_vis_axes - -Convert a single matplotlib `Axes` to a vis axes dict. - -```python -axes_to_vis_axes(ax) -> dict -``` - ---- - -## collect_figure_data - -Collect all tracked plot data from a scitex-managed figure. - -```python -collect_figure_data(fig) -> dict -``` - -Returns data keyed by axes index, containing the tracked arrays for each plot call. - ---- - -## tracking_to_plot_configs - -Convert scitex's internal tracking records to a list of vis plot config dicts. - -```python -tracking_to_plot_configs(tracking_data: dict) -> list[dict] -``` - -Useful for reconstructing plot specifications from a saved CSV. diff --git a/src/scitex/bridge/_skills/protocol-versioning.md b/src/scitex/bridge/_skills/protocol-versioning.md deleted file mode 100644 index 41a940fc..00000000 --- a/src/scitex/bridge/_skills/protocol-versioning.md +++ /dev/null @@ -1,94 +0,0 @@ -# Bridge Protocol Versioning (stx.bridge) - -The bridge protocol system ensures forward and backward compatibility when cross-module data is serialized and later loaded by a different version of SciTeX. - -## Current Version - -```python -from scitex.bridge import BRIDGE_PROTOCOL_VERSION -print(BRIDGE_PROTOCOL_VERSION) # "1.0.0" -``` - -Protocol versioning follows semantic versioning: -- **MAJOR** — breaking interface change -- **MINOR** — new bridge functions added (backward compatible) -- **PATCH** — bug fixes (backward compatible) - -## check_protocol_compatibility - -```python -from scitex.bridge import check_protocol_compatibility - -# Data saved by v1.0.0, loaded by current version (also 1.0.0) -is_compat, warning = check_protocol_compatibility("1.0.0") -# (True, None) - -# Data from older minor (1.1.0 loading 1.0.0 data) — OK -is_compat, warning = check_protocol_compatibility("1.0.0", "1.1.0") -# (True, None) - -# Data newer than current — warn but still load -is_compat, warning = check_protocol_compatibility("1.2.0", "1.0.0") -# (True, "Data version newer than current: data v1.2.0, current v1.0.0. Some features may be ignored.") - -# Major mismatch — incompatible -is_compat, warning = check_protocol_compatibility("2.0.0") -# (False, "Major version mismatch: data v2.0.0, current v1.0.0") -``` - -## ProtocolInfo - -Dataclass that carries protocol metadata in serialized objects: - -```python -from scitex.bridge import ProtocolInfo - -info = ProtocolInfo( - version="1.0.0", - source_module="stats", - target_module="plt", - coordinate_system="axes", -) - -d = info.to_dict() -# {"bridge_protocol_version": "1.0.0", "source_module": "stats", -# "target_module": "plt", "coordinate_system": "axes"} - -restored = ProtocolInfo.from_dict(d) -``` - -## add_protocol_metadata / extract_protocol_metadata - -Tag any dict with protocol metadata before serializing, then verify when loading: - -```python -from scitex.bridge import add_protocol_metadata, extract_protocol_metadata - -data = {"p_value": 0.01, "effect_size": 0.85} - -# Annotate before saving -tagged = add_protocol_metadata(data, source_module="stats", target_module="vis") -# tagged["_bridge_protocol"]["bridge_protocol_version"] == "1.0.0" - -# Extract and check when loading -info = extract_protocol_metadata(tagged) -if info: - is_compat, msg = check_protocol_compatibility(info.version) - if not is_compat: - raise RuntimeError(f"Incompatible bridge data: {msg}") -``` - -## Coordinate Systems Reference - -```python -from scitex.bridge import COORDINATE_SYSTEMS - -# "axes" — normalized 0-1 axes coords (plt / matplotlib annotations) -# "data" — actual x/y values (vis / FigureModel) -# "figure" — normalized 0-1 figure coords (suptitle, figure-level) -# "mm" — physical millimeters (publication layouts) -# "px" — pixels (canvas, GUI) - -print(COORDINATE_SYSTEMS["axes"]["description"]) -# "Normalized axes coordinates (0-1)" -``` diff --git a/src/scitex/bridge/_skills/protocol.md b/src/scitex/bridge/_skills/protocol.md deleted file mode 100644 index 8b065b26..00000000 --- a/src/scitex/bridge/_skills/protocol.md +++ /dev/null @@ -1,71 +0,0 @@ ---- -description: Bridge protocol version management — check compatibility with check_protocol_compatibility(), attach version metadata with add_protocol_metadata(), and extract it with extract_protocol_metadata(). ---- - -# Bridge Protocol - -## BRIDGE_PROTOCOL_VERSION - -Current protocol version string: `"1.0.0"`. - -```python -import scitex as stx - -print(stx.bridge.BRIDGE_PROTOCOL_VERSION) # '1.0.0' -``` - ---- - -## check_protocol_compatibility - -Verify that a saved object's protocol version is compatible with the current bridge. - -```python -check_protocol_compatibility(metadata: dict) -> bool -``` - -```python -import scitex as stx - -loaded = stx.io.load("saved_annotations.pkl") -if stx.bridge.check_protocol_compatibility(loaded.get("_bridge_meta", {})): - # safe to use - pass -``` - ---- - -## add_protocol_metadata / extract_protocol_metadata - -Embed and retrieve protocol metadata in a dict object. - -```python -add_protocol_metadata(data: dict) -> dict -extract_protocol_metadata(data: dict) -> ProtocolInfo -``` - -```python -import scitex as stx - -annotations = {"x1": 0, "x2": 1, "symbol": "**"} -versioned = stx.bridge.add_protocol_metadata(annotations) -stx.io.save(versioned, "annotations.pkl") - -# Later -loaded = stx.io.load("annotations.pkl") -info = stx.bridge.extract_protocol_metadata(loaded) -print(info.version) # '1.0.0' -``` - ---- - -## COORDINATE_SYSTEMS - -Dict describing the coordinate convention for each bridge type. - -```python -import scitex as stx - -print(stx.bridge.COORDINATE_SYSTEMS) -# {'plt': 'axes (0-1 normalized)', 'vis': 'data coordinates'} -``` diff --git a/src/scitex/bridge/_skills/stats-plt-bridge.md b/src/scitex/bridge/_skills/stats-plt-bridge.md deleted file mode 100644 index afa0f0fc..00000000 --- a/src/scitex/bridge/_skills/stats-plt-bridge.md +++ /dev/null @@ -1,49 +0,0 @@ -# Stats-to-Plot Bridge (stx.bridge) - -The `_stats_plt` bridge converts statistical test results into matplotlib axes annotations. - -## add_stat_to_axes - -```python -from scitex.bridge import add_stat_to_axes, extract_stats_from_axes -import scitex as stx - -# Run a statistical test -result = stx.stats.test_ttest_ind(group1, group2) - -fig, ax = stx.plt.subplots() -ax.boxplot([group1, group2]) - -# Annotate the axes with the statistical result -add_stat_to_axes(ax, result) -``` - -## extract_stats_from_axes - -```python -# Retrieve previously stored statistical annotations from axes -stats = extract_stats_from_axes(ax) -# Returns list of stat annotation dicts -``` - -## format_stat_for_plot - -```python -from scitex.bridge import format_stat_for_plot - -# Format a stat result as a display-ready string -label = format_stat_for_plot(result) -ax.text(0.5, 0.95, label, transform=ax.transAxes) -``` - -## Coordinate convention - -The stats-to-plt bridge uses **axes coordinates** (0–1 normalized), not data coordinates. Annotations positioned at `(x=0.5, y=0.95)` are centered at the top of any axes regardless of data range. - -This is defined in `COORDINATE_SYSTEMS["axes"]`: - -```python -from scitex.bridge import COORDINATE_SYSTEMS -print(COORDINATE_SYSTEMS["axes"]) -# {"description": "Normalized axes coordinates (0-1)", "x_range": (0.0, 1.0), ...} -``` diff --git a/src/scitex/bridge/_skills/stats-plt.md b/src/scitex/bridge/_skills/stats-plt.md deleted file mode 100644 index 0e4c182b..00000000 --- a/src/scitex/bridge/_skills/stats-plt.md +++ /dev/null @@ -1,75 +0,0 @@ ---- -description: Annotate matplotlib axes with statistical test results using add_stat_to_axes(), extract existing annotations with extract_stats_from_axes(), and format stat results for display with format_stat_for_plot(). ---- - -# Stats to Plt Bridge - -Coordinate system: **axes coordinates** (0–1 normalized). - -## add_stat_to_axes - -Add a statistical annotation (bracket + significance symbol) to a matplotlib `Axes`. - -```python -add_stat_to_axes( - ax, - stat_result: dict, - x1: float, - x2: float, - y: float | None = None, -) -> None -``` - -```python -import scitex as stx -import matplotlib.pyplot as plt - -fig, ax = plt.subplots() -ax.bar([0, 1], [3.2, 5.1]) - -result = stx.stats.test_ttest_ind(group1, group2, return_as="dict") -stx.bridge.add_stat_to_axes(ax, result, x1=0, x2=1) -stx.io.save(fig, "comparison.png") -``` - ---- - -## extract_stats_from_axes - -Read back statistical annotations that were previously added to an axes. - -```python -extract_stats_from_axes(ax) -> list[dict] -``` - -```python -import scitex as stx - -annotations = stx.bridge.extract_stats_from_axes(ax) -for a in annotations: - print(a["symbol"], a["p_value"]) -``` - ---- - -## format_stat_for_plot - -Format a stats result dict into a display string (e.g., `"***"`, `"n.s."`, `"p=0.023"`). - -```python -format_stat_for_plot(stat_result: dict, style: str = "stars") -> str -``` - -| `style` | Example output | -|---------|---------------| -| `"stars"` | `"***"` | -| `"p_value"` | `"p=0.001"` | -| `"both"` | `"*** (p=0.001)"` | - -```python -import scitex as stx - -result = stx.stats.test_mannwhitneyu(a, b, return_as="dict") -label = stx.bridge.format_stat_for_plot(result, style="both") -print(label) # "** (p=0.012)" -``` diff --git a/src/scitex/bridge/_skills/stats-vis.md b/src/scitex/bridge/_skills/stats-vis.md deleted file mode 100644 index 135b2db3..00000000 --- a/src/scitex/bridge/_skills/stats-vis.md +++ /dev/null @@ -1,65 +0,0 @@ ---- -description: Convert statistical results to vis annotation objects with stat_result_to_annotation(), add them to a FigureModel with add_stats_to_figure_model(), and position annotations with position_stat_annotation(). ---- - -# Stats to Vis Bridge - -Coordinate system: **data coordinates** (actual x/y values). - -## stat_result_to_annotation - -Convert a stats result dict to a vis-compatible annotation object. - -```python -stat_result_to_annotation( - stat_result: dict, - x1: float, - x2: float, - y: float, -) -> dict -``` - -```python -import scitex as stx - -result = stx.stats.test_ttest_ind(group1, group2, return_as="dict") -annotation = stx.bridge.stat_result_to_annotation(result, x1=1.0, x2=2.0, y=5.5) -``` - ---- - -## add_stats_to_figure_model - -Add multiple statistical annotations to a vis FigureModel object. - -```python -add_stats_to_figure_model(figure_model, annotations: list[dict]) -> None -``` - -```python -import scitex as stx - -fm = stx.bridge.figure_to_vis_model(fig) -annotations = [ - stx.bridge.stat_result_to_annotation(r, x1=0, x2=1, y=6) - for r in results -] -stx.bridge.add_stats_to_figure_model(fm, annotations) -``` - ---- - -## position_stat_annotation - -Calculate the optimal y-position for a statistical bracket given existing plot elements. - -```python -position_stat_annotation(ax, x1: float, x2: float, padding: float = 0.05) -> float -``` - -```python -import scitex as stx - -y = stx.bridge.position_stat_annotation(ax, x1=0, x2=1) -annotation = stx.bridge.stat_result_to_annotation(result, x1=0, x2=1, y=y) -``` diff --git a/src/scitex/bridge/_stats_plt.py b/src/scitex/bridge/_stats_plt.py deleted file mode 100755 index 78981d12..00000000 --- a/src/scitex/bridge/_stats_plt.py +++ /dev/null @@ -1,272 +0,0 @@ -#!/usr/bin/env python3 -# File: ./src/scitex/bridge/_stats_plt.py -# Time-stamp: "2024-12-09 10:00:00 (ywatanabe)" -""" -Bridge module for stats ↔ plt integration. - -Provides adapters to: -- Add statistical results as annotations on matplotlib axes -- Extract stats from axes with tracked annotations -- Format statistical text for plot display - -Coordinate Convention ---------------------- -This module uses **axes coordinates** (0-1 normalized) for positioning -when auto-positioning is used (no explicit x, y given). This matches -matplotlib's typical annotation workflow where positions are relative -to the axes bounding box. - -- (0, 0) = bottom-left of axes -- (1, 1) = top-right of axes -- Default auto-position: (0.5, 0.95) = top-center - -When explicit x, y are provided, they use whatever coordinate system -the caller intends (typically data coordinates unless transform is set). -""" - -from typing import Any, List, Optional - -# Import GUI classes from FTS (single source of truth) - -# StatResult is now a dict - the GUI-specific StatResult is deprecated -StatResult = dict - - -def format_stat_for_plot( - stat_result: StatResult, - format_style: str = "asterisk", -) -> str: - """ - Format a StatResult for display on a plot. - - Parameters - ---------- - stat_result : StatResult - The statistical result to format - format_style : str - One of "asterisk", "compact", "detailed", "publication" - - Returns - ------- - str - Formatted string for plot annotation - """ - return stat_result.format_text(format_style) - - -def add_stat_to_axes( - ax, - stat_result: StatResult, - x: Optional[float] = None, - y: Optional[float] = None, - format_style: str = "asterisk", - **kwargs, -) -> Any: - """ - Add a statistical result annotation to a matplotlib axes. - - Parameters - ---------- - ax : matplotlib.axes.Axes or scitex.plt AxisWrapper - The axes to annotate - stat_result : StatResult - The statistical result to display - x : float, optional - X position for the annotation. If None, uses stat_result.positioning - y : float, optional - Y position for the annotation. If None, uses stat_result.positioning - format_style : str - Format style for the text ("asterisk", "compact", "detailed", "publication") - **kwargs - Additional kwargs passed to ax.annotate() or ax.text() - - Returns - ------- - matplotlib.text.Text or matplotlib.text.Annotation - The created annotation object - """ - # Get formatted text - text = format_stat_for_plot(stat_result, format_style) - - # Determine position - # Check if using StatResult positioning - use_stat_positioning = False - if x is None or y is None: - positioning = stat_result.positioning - if positioning and positioning.position: - pos = positioning.position - if x is None: - x = pos.x - if y is None: - y = pos.y - use_stat_positioning = True - - # Auto-position: top center of axes in axes coordinates - if x is None: - x = 0.5 - if y is None: - y = 0.95 - - # Default to axes coordinates (0-1) unless user explicitly sets transform - # This makes positioning intuitive: (0.5, 0.9) = top center of plot - if "transform" not in kwargs: - kwargs["transform"] = _get_axes_transform(ax) - kwargs.setdefault("ha", "center") - kwargs.setdefault("va", "top") - - # Apply styling from StatResult if available - styling = stat_result.styling - if styling: - kwargs.setdefault("fontsize", styling.font_size_pt) - kwargs.setdefault("fontfamily", styling.font_family) - kwargs.setdefault("color", styling.color) - - # Get the actual matplotlib axes - mpl_ax = _get_mpl_axes(ax) - - # Create the annotation - annotation = mpl_ax.text(x, y, text, **kwargs) - - # Store reference to stat_result on the annotation for later extraction - annotation._scitex_stat_result = stat_result - - return annotation - - -def extract_stats_from_axes( - ax, - include_non_stat: bool = False, -) -> List[StatResult]: - """ - Extract StatResult objects from axes annotations. - - Parameters - ---------- - ax : matplotlib.axes.Axes or scitex.plt AxisWrapper - The axes to extract stats from - include_non_stat : bool - If True, create basic StatResult for non-stat annotations - - Returns - ------- - List[StatResult] - List of StatResult objects found in annotations - """ - results = [] - mpl_ax = _get_mpl_axes(ax) - - # Check all text objects (annotations and texts) - for text_obj in mpl_ax.texts: - if hasattr(text_obj, "_scitex_stat_result"): - results.append(text_obj._scitex_stat_result) - elif include_non_stat: - # Create a basic StatResult for non-stat text - content = text_obj.get_text() - if content.strip(): - # Try to detect if it's a stat-like annotation - result = _parse_stat_annotation(content) - if result: - results.append(result) - - return results - - -def _get_mpl_axes(ax): - """Get the underlying matplotlib axes from wrapper or native.""" - # Handle scitex AxisWrapper - if hasattr(ax, "_axes_mpl"): - return ax._axes_mpl - # Handle scitex AxesWrapper (multiple axes) - if hasattr(ax, "_axes_scitex"): - axes = ax._axes_scitex - if hasattr(axes, "flat"): - return axes.flat[0] - return axes - # Already matplotlib axes - return ax - - -def _get_axes_transform(ax): - """Get the axes transform for positioning.""" - mpl_ax = _get_mpl_axes(ax) - return mpl_ax.transAxes - - -def _parse_stat_annotation(text: str) -> Optional[StatResult]: - """ - Try to parse a text annotation as a statistical result. - - Parameters - ---------- - text : str - Text content to parse - - Returns - ------- - Optional[StatResult] - Parsed StatResult or None if not parseable - """ - text = text.strip() - - def _create_stat_dict(test_type, statistic_name, statistic_value, p_value): - """Create a simple stat result dict.""" - from scitex.stats._utils import p2stars - - return { - "test_type": test_type, - "test_category": "other", - "statistic": {"name": statistic_name, "value": statistic_value}, - "p_value": p_value, - "stars": p2stars(p_value, ns_symbol=False), - } - - # Try to detect asterisks pattern - if text in ["*", "**", "***", "ns", "n.s."]: - stars = text.replace("n.s.", "ns") - # Can't determine actual stats, create placeholder - p_value = { - "***": 0.0001, - "**": 0.005, - "*": 0.03, - "ns": 0.5, - }.get(stars, 0.5) - return _create_stat_dict( - test_type="unknown", - statistic_name="stat", - statistic_value=0.0, - p_value=p_value, - ) - - # Try to parse patterns like "r = 0.85***" or "(t = 2.5, p < 0.01)" - import re - - # Pattern: statistic = value[stars] - match = re.match(r"([a-zA-Z]+)\s*=\s*([\d.-]+)(\*+|ns)?", text) - if match: - stat_name = match.group(1) - stat_value = float(match.group(2)) - stars = match.group(3) or "ns" - p_value = { - "***": 0.0001, - "**": 0.005, - "*": 0.03, - "ns": 0.5, - }.get(stars, 0.5) - return _create_stat_dict( - test_type="unknown", - statistic_name=stat_name, - statistic_value=stat_value, - p_value=p_value, - ) - - return None - - -__all__ = [ - "add_stat_to_axes", - "extract_stats_from_axes", - "format_stat_for_plot", -] - - -# EOF diff --git a/src/scitex/bridge/_stats_vis.py b/src/scitex/bridge/_stats_vis.py deleted file mode 100755 index c9fa8d99..00000000 --- a/src/scitex/bridge/_stats_vis.py +++ /dev/null @@ -1,286 +0,0 @@ -#!/usr/bin/env python3 -# File: ./src/scitex/bridge/_stats_vis.py -# Time-stamp: "2024-12-09 10:00:00 (ywatanabe)" -""" -Bridge module for stats ↔ vis integration. - -Provides adapters to: -- Convert StatResult to vis AnnotationModel -- Add statistical annotations to FigureModel -- Position stat annotations using vis coordinate system - -Coordinate Convention ---------------------- -This module uses **data coordinates** for positioning (via Position with -unit="data"). This matches the vis model's approach where positions -correspond to actual data values on the plot. - -- Positions are in the same units as the plot data -- position_stat_annotation() returns Position(unit="data") -- For normalized positioning, use axes_bounds to define the data range - -This differs from _stats_plt which uses axes coordinates (0-1 normalized). -When bridging between plt and vis, coordinate transformation may be needed. -""" - -from typing import Dict, List, Optional, Tuple - -# Import GUI classes from FTS (single source of truth) -from scitex.io.bundle.kinds._stats import Position - -# Legacy model imports - may not be available -try: - from scitex.io.bundle.kinds._plot._models import ( - AnnotationModel, - AxesModel, - FigureModel, - TextStyle, - ) - - VIS_MODEL_AVAILABLE = True -except ImportError: - AnnotationModel = None - FigureModel = None - AxesModel = None - TextStyle = None - VIS_MODEL_AVAILABLE = False - -# StatResult placeholder for type hints (actual usage is through dict) -StatResult = dict # Use dict as StatResult is deprecated - - -def stat_result_to_annotation( - stat_result: StatResult, - format_style: str = "asterisk", - x: Optional[float] = None, - y: Optional[float] = None, -) -> AnnotationModel: - """ - Convert a StatResult to a vis AnnotationModel. - - Parameters - ---------- - stat_result : StatResult - The statistical result to convert - format_style : str - Format style for the text ("asterisk", "compact", "detailed", "publication") - x : float, optional - X position (data coordinates). Overrides stat_result positioning - y : float, optional - Y position (data coordinates). Overrides stat_result positioning - - Returns - ------- - AnnotationModel - Annotation model for vis rendering - """ - # Get formatted text - text = stat_result.format_text(format_style) - - # Determine position - if x is None or y is None: - positioning = stat_result.positioning - if positioning and positioning.position: - pos = positioning.position - x = x if x is not None else pos.x - y = y if y is not None else pos.y - else: - # Default center-top position (will be overridden by positioning logic) - x = x if x is not None else 0.5 - y = y if y is not None else 0.95 - - # Build text style from stat styling - styling = stat_result.styling - text_style = TextStyle( - fontsize=styling.font_size_pt if styling else 7.0, - color=styling.color if styling else "#000000", - ha="center", - va="top", - ) - - # Create annotation model - return AnnotationModel( - annotation_type="text", - text=text, - x=x, - y=y, - annotation_id=stat_result.plot_id or f"stat_{id(stat_result)}", - style=text_style, - ) - - -def add_stats_to_figure_model( - figure_model: FigureModel, - stat_results: List[StatResult], - axes_index: int = 0, - format_style: str = "asterisk", - auto_position: bool = True, -) -> FigureModel: - """ - Add statistical results as annotations to a FigureModel. - - Parameters - ---------- - figure_model : FigureModel - The figure model to annotate - stat_results : List[StatResult] - List of statistical results to add - axes_index : int - Index of axes to add annotations to - format_style : str - Format style for the text - auto_position : bool - Whether to automatically position stats to avoid overlap - - Returns - ------- - FigureModel - The modified figure model (same instance) - """ - if not stat_results: - return figure_model - - # Ensure axes exist - if axes_index >= len(figure_model.axes): - raise IndexError(f"Axes index {axes_index} out of range") - - axes_dict = figure_model.axes[axes_index] - - # Get or initialize annotations list - if "annotations" not in axes_dict: - axes_dict["annotations"] = [] - - # Calculate positions if auto_position - positions = [] - if auto_position: - positions = _calculate_stat_positions( - stat_results, - len(axes_dict["annotations"]), - ) - - # Add each stat as annotation - for i, stat_result in enumerate(stat_results): - x, y = positions[i] if positions else (None, None) - annotation = stat_result_to_annotation( - stat_result, - format_style=format_style, - x=x, - y=y, - ) - axes_dict["annotations"].append(annotation.to_dict()) - - return figure_model - - -def position_stat_annotation( - stat_result: StatResult, - axes_bounds: Dict[str, float], - existing_positions: Optional[List[Tuple[float, float]]] = None, - preferred_corner: str = "top-right", -) -> Position: - """ - Calculate optimal position for a stat annotation. - - Parameters - ---------- - stat_result : StatResult - The statistical result to position - axes_bounds : Dict[str, float] - Axes bounds with keys: x_min, x_max, y_min, y_max - existing_positions : List[Tuple[float, float]], optional - List of existing annotation positions to avoid - preferred_corner : str - Preferred corner: "top-left", "top-right", "bottom-left", "bottom-right" - - Returns - ------- - Position - Calculated position in data coordinates - """ - existing = existing_positions or [] - - # Get axes range - x_min = axes_bounds.get("x_min", 0) - x_max = axes_bounds.get("x_max", 1) - y_min = axes_bounds.get("y_min", 0) - y_max = axes_bounds.get("y_max", 1) - - x_range = x_max - x_min - y_range = y_max - y_min - - # Calculate corner positions (as fraction, then convert to data) - corner_fractions = { - "top-right": (0.95, 0.95), - "top-left": (0.05, 0.95), - "bottom-right": (0.95, 0.05), - "bottom-left": (0.05, 0.05), - "top-center": (0.5, 0.95), - "bottom-center": (0.5, 0.05), - } - - # Start with preferred corner - base_x, base_y = corner_fractions.get(preferred_corner, (0.95, 0.95)) - x = x_min + base_x * x_range - y = y_min + base_y * y_range - - # Check overlap and adjust if needed - min_dist = ( - stat_result.positioning.min_distance_mm if stat_result.positioning else 2.0 - ) - - for ex_x, ex_y in existing: - dist = ((x - ex_x) ** 2 + (y - ex_y) ** 2) ** 0.5 - if dist < min_dist: - # Shift down - y -= min_dist * 1.5 - - return Position(x=x, y=y, unit="data") - - -def _calculate_stat_positions( - stat_results: List[StatResult], - existing_count: int = 0, -) -> List[Tuple[float, float]]: - """ - Calculate non-overlapping positions for multiple stats. - - Parameters - ---------- - stat_results : List[StatResult] - List of stats to position - existing_count : int - Number of existing annotations - - Returns - ------- - List[Tuple[float, float]] - List of (x, y) positions in axes coordinates (0-1) - """ - positions = [] - y_start = 0.95 - y_step = 0.05 - - for i, stat in enumerate(stat_results): - # Stack vertically from top - y = y_start - (i + existing_count) * y_step - x = 0.5 # Center - - # Check stat's own positioning preference - if stat.positioning and stat.positioning.position: - pos = stat.positioning.position - x = pos.x - y = pos.y - - positions.append((x, y)) - - return positions - - -__all__ = [ - "stat_result_to_annotation", - "add_stats_to_figure_model", - "position_stat_annotation", -] - - -# EOF diff --git a/tests/scitex/bridge/__init__.py b/tests/scitex/bridge/__init__.py deleted file mode 100644 index 72a976be..00000000 --- a/tests/scitex/bridge/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File: ./tests/scitex/bridge/__init__.py -"""Tests for scitex.bridge module.""" - -# EOF diff --git a/tests/scitex/bridge/test__figrecipe.py b/tests/scitex/bridge/test__figrecipe.py deleted file mode 100644 index e3b8fee8..00000000 --- a/tests/scitex/bridge/test__figrecipe.py +++ /dev/null @@ -1,293 +0,0 @@ -# Add your tests here - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_figrecipe.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# """Bridge adapter for figrecipe integration. -# -# This module provides functions to save figures with both: -# - SigmaPlot-compatible CSV (scitex format) -# - figrecipe YAML recipe (reproducible figures) -# -# The FTS bundle structure: -# figure/ -# ├── recipe.yaml # Source of truth (figrecipe format) -# ├── recipe_data/ # Large arrays (if needed) -# ├── plot.csv # SigmaPlot combined CSV (derived) -# ├── plot.png # Primary image (derived) -# └── meta.yaml # FTS metadata (optional) -# """ -# -# from pathlib import Path -# from typing import Any, Dict, Optional, Union -# -# # Check figrecipe availability -# try: -# import figrecipe as fr -# from figrecipe._serializer import save_recipe as _fr_save_recipe -# -# FIGRECIPE_AVAILABLE = True -# except ImportError: -# FIGRECIPE_AVAILABLE = False -# -# -# def save_with_recipe( -# fig, -# path: Union[str, Path], -# include_csv: bool = True, -# include_recipe: bool = True, -# data_format: str = "csv", -# dpi: int = 300, -# **kwargs, -# ) -> Dict[str, Path]: -# """Save figure with both CSV and figrecipe recipe. -# -# Parameters -# ---------- -# fig : FigWrapper or matplotlib Figure -# The figure to save. -# path : str or Path -# Output path. Can be: -# - Directory path (creates bundle) -# - File path with .zip extension (creates zip bundle) -# - File path with image extension (saves image + sidecar files) -# include_csv : bool -# If True, save SigmaPlot-compatible CSV. -# include_recipe : bool -# If True, save figrecipe YAML recipe (requires figrecipe). -# data_format : str -# Format for recipe data: 'csv', 'npz', or 'inline'. -# dpi : int -# Resolution for image output. -# **kwargs -# Additional arguments passed to savefig (including facecolor). -# -# Returns -# ------- -# dict -# Paths to saved files: {'image': Path, 'csv': Path, 'recipe': Path} -# """ -# from scitex.io.bundle._bundle._storage import get_storage -# -# path = Path(path) -# result = {} -# -# # Determine if this is a bundle (directory or zip) -# is_bundle = path.suffix == ".zip" or path.suffix == "" or path.is_dir() -# -# if is_bundle: -# # Create bundle storage -# storage = get_storage(path) -# storage.ensure_exists() -# -# # 1. Save image - use fig.savefig() to get facecolor fix from FigWrapper -# image_path = storage.path / "plot.png" -# _save_figure_image(fig, image_path, dpi=dpi, **kwargs) -# result["image"] = image_path -# -# # 2. Save SigmaPlot CSV -# if include_csv and hasattr(fig, "export_as_csv"): -# try: -# csv_df = fig.export_as_csv() -# if not csv_df.empty: -# csv_path = storage.path / "plot.csv" -# csv_df.to_csv(csv_path, index=False) -# result["csv"] = csv_path -# except Exception: -# pass # CSV export is optional -# -# # 3. Save figrecipe recipe -# if include_recipe: -# recipe_path = _save_recipe_to_path( -# fig, storage.path / "recipe.yaml", data_format -# ) -# if recipe_path: -# result["recipe"] = recipe_path -# -# else: -# # Single file save (image + sidecars) -# _save_figure_image(fig, path, dpi=dpi, **kwargs) -# result["image"] = path -# -# # Save CSV sidecar -# if include_csv and hasattr(fig, "export_as_csv"): -# try: -# csv_df = fig.export_as_csv() -# if not csv_df.empty: -# csv_path = path.with_suffix(".csv") -# csv_df.to_csv(csv_path, index=False) -# result["csv"] = csv_path -# except Exception: -# pass -# -# # Save recipe sidecar -# if include_recipe: -# recipe_path = _save_recipe_to_path( -# fig, path.with_suffix(".yaml"), data_format -# ) -# if recipe_path: -# result["recipe"] = recipe_path -# -# return result -# -# -# def _save_figure_image(fig, path: Path, dpi: int = 300, **kwargs): -# """Save figure image using the best available method with facecolor support. -# -# Uses fig.savefig() when available (FigWrapper or RecordingFigure) to get -# the facecolor override fix for transparent figures. -# """ -# # Check if this is a figrecipe RecordingFigure - use fr.save() for full support -# if FIGRECIPE_AVAILABLE: -# try: -# from figrecipe._wrappers import RecordingFigure -# -# if isinstance(fig, RecordingFigure): -# # Use figrecipe's save with facecolor support -# facecolor = kwargs.pop("facecolor", None) -# fr.save( -# fig, -# path, -# save_recipe=False, # Recipe saved separately -# dpi=dpi, -# facecolor=facecolor, -# verbose=False, -# **kwargs, -# ) -# return -# except (ImportError, AttributeError): -# pass -# -# # Use fig.savefig() if available (FigWrapper has facecolor fix) -# if hasattr(fig, "savefig"): -# fig.savefig(path, dpi=dpi, **kwargs) -# else: -# # Fallback to matplotlib figure's savefig -# mpl_fig = fig._fig_mpl if hasattr(fig, "_fig_mpl") else fig -# mpl_fig.savefig(path, dpi=dpi, **kwargs) -# -# -# def _save_recipe_to_path( -# fig, -# path: Path, -# data_format: str = "csv", -# ) -> Optional[Path]: -# """Save figrecipe recipe if available. -# -# Parameters -# ---------- -# fig : FigWrapper -# Figure with optional _figrecipe_recorder attribute. -# path : Path -# Output path for recipe.yaml. -# data_format : str -# Format for data: 'csv', 'npz', or 'inline'. -# -# Returns -# ------- -# Path or None -# Path to saved recipe, or None if not available. -# """ -# if not FIGRECIPE_AVAILABLE: -# return None -# -# try: -# # Check if figure has figrecipe recorder -# if hasattr(fig, "_figrecipe_recorder") and fig._figrecipe_enabled: -# recorder = fig._figrecipe_recorder -# figure_record = recorder.figure_record -# -# # Capture current figure state into record -# _capture_figure_state(fig, figure_record) -# -# # Save using figrecipe's serializer -# _fr_save_recipe( -# figure_record, path, include_data=True, data_format=data_format -# ) -# return path -# -# # Alternative: if figure was created with fr.subplots() directly -# if hasattr(fig, "save_recipe"): -# fig.save_recipe(path, include_data=True, data_format=data_format) -# return path -# -# except Exception: -# pass # Recipe saving is optional -# -# return None -# -# -# def _capture_figure_state(fig, figure_record): -# """Capture current figure state into the record. -# -# This syncs the matplotlib figure state with the figrecipe record, -# ensuring the recipe reflects the final figure appearance. -# """ -# try: -# mpl_fig = fig._fig_mpl if hasattr(fig, "_fig_mpl") else fig -# -# # Update figure dimensions -# figsize = mpl_fig.get_size_inches() -# figure_record.figsize = list(figsize) -# figure_record.dpi = int(mpl_fig.dpi) -# -# # Capture style from scitex metadata if available -# if hasattr(mpl_fig, "_scitex_theme"): -# if not hasattr(figure_record, "style") or figure_record.style is None: -# figure_record.style = {} -# figure_record.style["theme"] = mpl_fig._scitex_theme -# -# except Exception: -# pass # Non-critical -# -# -# def load_recipe( -# path: Union[str, Path], -# ) -> Any: -# """Load figrecipe recipe from FTS bundle. -# -# Parameters -# ---------- -# path : str or Path -# Path to bundle directory, zip file, or recipe.yaml. -# -# Returns -# ------- -# tuple -# (fig, axes) reproduced from recipe. -# """ -# if not FIGRECIPE_AVAILABLE: -# raise ImportError("figrecipe is required for loading recipes") -# -# path = Path(path) -# -# # Handle bundle paths -# if path.is_dir(): -# recipe_path = path / "recipe.yaml" -# elif path.suffix == ".zip": -# # figrecipe can handle zip files directly -# recipe_path = path -# else: -# recipe_path = path -# -# return fr.reproduce(recipe_path) -# -# -# def has_figrecipe() -> bool: -# """Check if figrecipe is available.""" -# return FIGRECIPE_AVAILABLE -# -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_figrecipe.py -# -------------------------------------------------------------------------------- diff --git a/tests/scitex/bridge/test__helpers.py b/tests/scitex/bridge/test__helpers.py deleted file mode 100644 index 2b31e849..00000000 --- a/tests/scitex/bridge/test__helpers.py +++ /dev/null @@ -1,316 +0,0 @@ -#!/usr/bin/env python3 -# File: ./tests/scitex/bridge/test__helpers.py -# Time-stamp: "2024-12-09 11:00:00 (ywatanabe)" -"""Tests for scitex.bridge._helpers module.""" - -import pytest - - -class TestAddStatsFromResults: - """Tests for add_stats_from_results helper.""" - - @pytest.fixture - def stat_results(self): - """Create test StatResults.""" - from scitex.schema import create_stat_result - - return [ - create_stat_result("pearson", "r", 0.85, 0.001), - create_stat_result("t-test", "t", 2.5, 0.02), - ] - - def test_auto_detect_matplotlib_axes(self, stat_results): - """Test auto-detection works with matplotlib axes.""" - import matplotlib.pyplot as plt - - from scitex.bridge import add_stats_from_results - - fig, ax = plt.subplots() - - # Should auto-detect plt backend - result = add_stats_from_results(ax, stat_results) - - # Should return the axes (for chaining) - assert result is ax - - # Should have added text annotations - assert len(ax.texts) == 2 - - plt.close(fig) - - def test_auto_detect_figure_model(self, stat_results): - """Test auto-detection works with FigureModel.""" - from scitex.bridge import add_stats_from_results - from scitex.io.bundle.kinds._plot._models import FigureModel - - model = FigureModel( - width_mm=170, - height_mm=120, - axes=[{"row": 0, "col": 0, "plots": []}], - ) - - # Should auto-detect vis backend - result = add_stats_from_results(model, stat_results) - - # Should return the model (for chaining) - assert result is model - - # Should have added annotations - assert len(model.axes[0].get("annotations", [])) == 2 - - def test_explicit_plt_backend(self, stat_results): - """Test explicit plt backend.""" - import matplotlib.pyplot as plt - - from scitex.bridge import add_stats_from_results - - fig, ax = plt.subplots() - - add_stats_from_results(ax, stat_results, backend="plt") - assert len(ax.texts) == 2 - - plt.close(fig) - - def test_explicit_vis_backend(self, stat_results): - """Test explicit vis backend.""" - from scitex.bridge import add_stats_from_results - from scitex.io.bundle.kinds._plot._models import FigureModel - - model = FigureModel( - width_mm=170, - height_mm=120, - axes=[{"row": 0, "col": 0, "plots": []}], - ) - - add_stats_from_results(model, stat_results, backend="vis") - assert len(model.axes[0].get("annotations", [])) == 2 - - def test_single_stat_result(self): - """Test with single StatResult (not list).""" - import matplotlib.pyplot as plt - - from scitex.bridge import add_stats_from_results - from scitex.schema import create_stat_result - - fig, ax = plt.subplots() - stat = create_stat_result("pearson", "r", 0.85, 0.001) - - # Should accept single StatResult - add_stats_from_results(ax, stat) - assert len(ax.texts) == 1 - - plt.close(fig) - - def test_format_style_applied(self): - """Test format_style is passed through.""" - import matplotlib.pyplot as plt - - from scitex.bridge import add_stats_from_results - from scitex.schema import create_stat_result - - fig, ax = plt.subplots() - stat = create_stat_result("pearson", "r", 0.85, 0.001) - - add_stats_from_results(ax, stat, format_style="compact") - - # Text should be in compact format - text_content = ax.texts[0].get_text() - assert "r = 0.850" in text_content - - plt.close(fig) - - def test_chaining_support(self, stat_results): - """Test method chaining works.""" - import matplotlib.pyplot as plt - - from scitex.bridge import add_stats_from_results, extract_stats_from_axes - - fig, ax = plt.subplots() - - # Should support chaining - extracted = extract_stats_from_axes(add_stats_from_results(ax, stat_results)) - - assert len(extracted) == 2 - - plt.close(fig) - - def test_invalid_backend_raises(self, stat_results): - """Test invalid backend raises ValueError.""" - import matplotlib.pyplot as plt - - from scitex.bridge import add_stats_from_results - - fig, ax = plt.subplots() - - with pytest.raises(ValueError, match="Unknown backend"): - add_stats_from_results(ax, stat_results, backend="invalid") - - plt.close(fig) - - -# EOF - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_helpers.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # -*- coding: utf-8 -*- -# # File: ./src/scitex/bridge/_helpers.py -# # Time-stamp: "2024-12-09 11:00:00 (ywatanabe)" -# """ -# High-level helper functions for cross-module operations. -# -# These helpers provide a unified API for common workflows that span -# multiple modules, abstracting away backend-specific details. -# """ -# -# from typing import Union, List, Optional, Literal -# -# # StatResult is now a dict - the GUI-specific StatResult is deprecated -# StatResult = dict -# -# -# def add_stats_from_results( -# target, -# stat_results: Union[StatResult, List[StatResult]], -# backend: Literal["auto", "plt", "vis"] = "auto", -# format_style: str = "asterisk", -# **kwargs, -# ): -# """ -# Add statistical results to a figure or axes, auto-detecting backend. -# -# This is a high-level helper that works with both matplotlib axes -# and vis FigureModel, choosing the appropriate bridge function. -# -# Parameters -# ---------- -# target : matplotlib.axes.Axes, scitex.plt.AxisWrapper, or FigureModel -# The target to add statistics to -# stat_results : StatResult or List[StatResult] -# Statistical result(s) to add -# backend : {"auto", "plt", "vis"} -# Backend to use. "auto" detects from target type: -# - matplotlib axes or scitex AxisWrapper → "plt" -# - FigureModel → "vis" -# format_style : str -# Format style for stat text ("asterisk", "compact", "detailed", "publication") -# **kwargs -# Additional arguments passed to the backend-specific function: -# - plt: passed to add_stat_to_axes (x, y, transform, etc.) -# - vis: passed to add_stats_to_figure_model (axes_index, auto_position, etc.) -# -# Returns -# ------- -# target -# The modified target (for chaining) -# -# Examples -# -------- -# >>> # With matplotlib axes -# >>> fig, ax = plt.subplots() -# >>> stat = create_stat_result("pearson", "r", 0.85, 0.001) -# >>> add_stats_from_results(ax, stat) -# -# >>> # With vis FigureModel -# >>> model = FigureModel(width_mm=170, height_mm=120, axes=[{}]) -# >>> add_stats_from_results(model, [stat1, stat2], backend="vis") -# -# Notes -# ----- -# Coordinate conventions differ between backends: -# - plt: uses axes coordinates (0-1 normalized) by default -# - vis: uses data coordinates -# -# For precise control, use the backend-specific functions directly: -# - scitex.bridge.add_stat_to_axes (plt backend) -# - scitex.bridge.add_stats_to_figure_model (vis backend) -# """ -# # Normalize to list -# if isinstance(stat_results, StatResult): -# stat_results = [stat_results] -# -# # Auto-detect backend -# if backend == "auto": -# backend = _detect_backend(target) -# -# # Dispatch to appropriate function -# if backend == "plt": -# from scitex.bridge._stats_plt import add_stat_to_axes -# -# for stat in stat_results: -# add_stat_to_axes(target, stat, format_style=format_style, **kwargs) -# -# elif backend == "vis": -# from scitex.bridge._stats_vis import add_stats_to_figure_model -# -# add_stats_to_figure_model( -# target, -# stat_results, -# format_style=format_style, -# **kwargs, -# ) -# -# else: -# raise ValueError(f"Unknown backend: {backend}. Use 'auto', 'plt', or 'vis'.") -# -# return target -# -# -# def _detect_backend(target) -> Literal["plt", "vis"]: -# """ -# Detect the appropriate backend from target type. -# -# Parameters -# ---------- -# target : any -# The target object -# -# Returns -# ------- -# str -# "plt" or "vis" -# """ -# # Check for vis FigureModel -# try: -# from scitex.io.bundle.kinds._plot._models import FigureModel -# if isinstance(target, FigureModel): -# return "vis" -# except ImportError: -# pass -# -# # Check for matplotlib axes -# try: -# import matplotlib.axes -# if isinstance(target, matplotlib.axes.Axes): -# return "plt" -# except ImportError: -# pass -# -# # Check for scitex plt wrappers -# if hasattr(target, "_axes_mpl"): -# return "plt" -# if hasattr(target, "_axes_scitex"): -# return "plt" -# -# # Default to plt (most common case) -# return "plt" -# -# -# __all__ = [ -# "add_stats_from_results", -# ] -# -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_helpers.py -# -------------------------------------------------------------------------------- diff --git a/tests/scitex/bridge/test__plt_vis.py b/tests/scitex/bridge/test__plt_vis.py deleted file mode 100644 index 924e5bc1..00000000 --- a/tests/scitex/bridge/test__plt_vis.py +++ /dev/null @@ -1,735 +0,0 @@ -#!/usr/bin/env python3 -# File: ./tests/scitex/bridge/test__plt_vis.py -# Time-stamp: "2024-12-09 10:30:00 (ywatanabe)" -"""Tests for scitex.bridge._plt_vis module.""" - -import pytest - - -class TestFigureToVisModel: - """Tests for figure_to_vis_model function.""" - - @pytest.fixture - def mpl_figure(self): - """Create a matplotlib figure.""" - import matplotlib.pyplot as plt - - fig, ax = plt.subplots() - ax.plot([1, 2, 3], [4, 5, 6], label="test") - ax.set_xlabel("X Label") - ax.set_ylabel("Y Label") - ax.set_title("Test Title") - yield fig - plt.close(fig) - - def test_converts_to_figure_model(self, mpl_figure): - """Test conversion to FigureModel.""" - from scitex.bridge import figure_to_vis_model - from scitex.io.bundle.kinds._plot._models import FigureModel - - model = figure_to_vis_model(mpl_figure) - - assert isinstance(model, FigureModel) - assert model.width_mm > 0 - assert model.height_mm > 0 - - def test_captures_dimensions(self, mpl_figure): - """Test that dimensions are captured correctly.""" - from scitex.bridge import figure_to_vis_model - - model = figure_to_vis_model(mpl_figure) - - # Default matplotlib figure is 6.4 x 4.8 inches - expected_width_mm = 6.4 * 25.4 # ~162.56 mm - expected_height_mm = 4.8 * 25.4 # ~121.92 mm - - assert abs(model.width_mm - expected_width_mm) < 1 - assert abs(model.height_mm - expected_height_mm) < 1 - - def test_captures_axes_properties(self, mpl_figure): - """Test that axes properties are captured.""" - from scitex.bridge import figure_to_vis_model - - model = figure_to_vis_model(mpl_figure) - - assert len(model.axes) == 1 - axes_dict = model.axes[0] - assert axes_dict.get("xlabel") == "X Label" - assert axes_dict.get("ylabel") == "Y Label" - assert axes_dict.get("title") == "Test Title" - - -class TestAxesToVisAxes: - """Tests for axes_to_vis_axes function.""" - - @pytest.fixture - def mpl_axes(self): - """Create matplotlib axes.""" - import matplotlib.pyplot as plt - - fig, ax = plt.subplots() - ax.set_xlim(0, 10) - ax.set_ylim(0, 100) - ax.set_xlabel("X") - ax.set_ylabel("Y") - yield ax - plt.close(fig) - - def test_creates_axes_model(self, mpl_axes): - """Test creation of AxesModel.""" - from scitex.bridge import axes_to_vis_axes - from scitex.io.bundle.kinds._plot._models import AxesModel - - model = axes_to_vis_axes(mpl_axes) - - assert isinstance(model, AxesModel) - assert model.xlabel == "X" - assert model.ylabel == "Y" - - def test_captures_limits(self, mpl_axes): - """Test that axis limits are captured.""" - from scitex.bridge import axes_to_vis_axes - - model = axes_to_vis_axes(mpl_axes) - - assert model.xlim == [0, 10] - assert model.ylim == [0, 100] - - -class TestTrackingToPlotConfigs: - """Tests for tracking_to_plot_configs function.""" - - def test_converts_plot_history(self): - """Test conversion of plot tracking history.""" - import numpy as np - - from scitex.bridge import tracking_to_plot_configs - - history = { - "plot_0": ( - "plot_0", - "plot", - {"args": (np.array([1, 2, 3]), np.array([4, 5, 6]))}, - {"color": "blue", "label": "test"}, - ), - } - - plots = tracking_to_plot_configs(history) - - assert len(plots) == 1 - assert plots[0].plot_type == "line" - assert plots[0].plot_id == "plot_0" - - def test_handles_scatter(self): - """Test conversion of scatter plot history.""" - import numpy as np - - from scitex.bridge import tracking_to_plot_configs - - history = { - "scatter_0": ( - "scatter_0", - "scatter", - {"args": (np.array([1, 2, 3]), np.array([4, 5, 6]))}, - {"marker": "o"}, - ), - } - - plots = tracking_to_plot_configs(history) - - assert len(plots) == 1 - assert plots[0].plot_type == "scatter" - - -class TestCollectFigureData: - """Tests for collect_figure_data function.""" - - @pytest.fixture - def mpl_figure(self): - """Create a matplotlib figure.""" - import matplotlib.pyplot as plt - - fig, ax = plt.subplots() - ax.plot([1, 2, 3], [4, 5, 6]) - yield fig - plt.close(fig) - - def test_returns_dict(self, mpl_figure): - """Test that function returns dict.""" - from scitex.bridge import collect_figure_data - - data = collect_figure_data(mpl_figure) - - assert isinstance(data, dict) - assert "figure" in data - assert "axes" in data - - def test_captures_figure_info(self, mpl_figure): - """Test that figure info is captured.""" - from scitex.bridge import collect_figure_data - - data = collect_figure_data(mpl_figure) - - assert "width_mm" in data["figure"] - assert "height_mm" in data["figure"] - assert data["figure"]["width_mm"] > 0 - - -# EOF - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_plt_vis.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # -*- coding: utf-8 -*- -# # File: ./src/scitex/bridge/_plt_vis.py -# # Time-stamp: "2024-12-09 10:00:00 (ywatanabe)" -# """ -# Bridge module for plt ↔ vis integration. -# -# Provides adapters to: -# - Convert scitex.plt figures to vis FigureModel -# - Extract tracking data as PlotModel configurations -# - Synchronize matplotlib state with vis JSON -# """ -# -# from typing import Optional, Dict, Any, List, Tuple, Union -# import warnings -# -# # Legacy model imports - may not be available (deleted module) -# try: -# from scitex.io.bundle.kinds._plot._models import ( -# FigureModel, -# AxesModel, -# PlotModel, -# AnnotationModel, -# GuideModel, -# PlotStyle, -# AxesStyle, -# TextStyle, -# ) -# VIS_MODEL_AVAILABLE = True -# except ImportError: -# FigureModel = None -# AxesModel = None -# PlotModel = None -# AnnotationModel = None -# GuideModel = None -# PlotStyle = None -# AxesStyle = None -# TextStyle = None -# VIS_MODEL_AVAILABLE = False -# -# -# def figure_to_vis_model( -# fig, -# include_data: bool = True, -# include_style: bool = True, -# ) -> FigureModel: -# """ -# Convert a scitex.plt figure to a vis FigureModel. -# -# Parameters -# ---------- -# fig : scitex.plt.FigWrapper or matplotlib.figure.Figure -# The figure to convert -# include_data : bool -# Whether to include plot data in the model -# include_style : bool -# Whether to include style information -# -# Returns -# ------- -# FigureModel -# The vis figure model -# """ -# # Get matplotlib figure -# mpl_fig = _get_mpl_figure(fig) -# -# # Get figure dimensions -# width_inch = mpl_fig.get_figwidth() -# height_inch = mpl_fig.get_figheight() -# dpi = mpl_fig.get_dpi() -# -# # Convert to mm -# width_mm = width_inch * 25.4 -# height_mm = height_inch * 25.4 -# -# # Determine layout from axes -# axes_list = mpl_fig.axes -# nrows, ncols = _infer_layout(axes_list, mpl_fig) -# -# # Create figure model -# figure_model = FigureModel( -# width_mm=width_mm, -# height_mm=height_mm, -# nrows=nrows, -# ncols=ncols, -# dpi=int(dpi), -# facecolor=_color_to_hex(mpl_fig.get_facecolor()), -# edgecolor=_color_to_hex(mpl_fig.get_edgecolor()), -# ) -# -# # Handle suptitle -# if hasattr(mpl_fig, "_suptitle") and mpl_fig._suptitle: -# figure_model.suptitle = mpl_fig._suptitle.get_text() -# figure_model.suptitle_fontsize = mpl_fig._suptitle.get_fontsize() -# -# # Convert each axes -# scitex_axes = _get_scitex_axes(fig) -# -# for idx, ax in enumerate(axes_list): -# row = idx // ncols -# col = idx % ncols -# -# # Find corresponding scitex axis wrapper for history -# scitex_ax = _find_scitex_axis(scitex_axes, ax) -# -# axes_model = axes_to_vis_axes( -# ax, -# row=row, -# col=col, -# scitex_ax=scitex_ax, -# include_data=include_data, -# include_style=include_style, -# ) -# figure_model.axes.append(axes_model.to_dict()) -# -# return figure_model -# -# -# def axes_to_vis_axes( -# ax, -# row: int = 0, -# col: int = 0, -# scitex_ax=None, -# include_data: bool = True, -# include_style: bool = True, -# ) -> AxesModel: -# """ -# Convert a matplotlib axes to a vis AxesModel. -# -# Parameters -# ---------- -# ax : matplotlib.axes.Axes -# The axes to convert -# row : int -# Row position in layout -# col : int -# Column position in layout -# scitex_ax : AxisWrapper, optional -# Scitex axis wrapper with tracking history -# include_data : bool -# Whether to include plot data -# include_style : bool -# Whether to include style information -# -# Returns -# ------- -# AxesModel -# The vis axes model -# """ -# # Get underlying matplotlib axes -# mpl_ax = ax._axes_mpl if hasattr(ax, "_axes_mpl") else ax -# -# # Extract axis properties -# axes_model = AxesModel( -# row=row, -# col=col, -# xlabel=mpl_ax.get_xlabel() or None, -# ylabel=mpl_ax.get_ylabel() or None, -# title=mpl_ax.get_title() or None, -# xlim=list(mpl_ax.get_xlim()), -# ylim=list(mpl_ax.get_ylim()), -# xscale=mpl_ax.get_xscale(), -# yscale=mpl_ax.get_yscale(), -# ) -# -# # Extract tick info -# xticks = mpl_ax.get_xticks() -# yticks = mpl_ax.get_yticks() -# if len(xticks) > 0: -# axes_model.xticks = [float(t) for t in xticks] -# if len(yticks) > 0: -# axes_model.yticks = [float(t) for t in yticks] -# -# # Extract style if requested -# if include_style: -# axes_model.style = _extract_axes_style(mpl_ax) -# -# # Extract plots from tracking history -# if include_data and scitex_ax and hasattr(scitex_ax, "history"): -# plots = tracking_to_plot_configs(scitex_ax.history) -# for plot in plots: -# axes_model.plots.append(plot.to_dict() if hasattr(plot, "to_dict") else plot) -# -# # Extract annotations -# for text_obj in mpl_ax.texts: -# annotation = _text_to_annotation(text_obj) -# if annotation: -# axes_model.annotations.append(annotation.to_dict()) -# -# # Extract guides (axhline, axvline, etc.) -# guides = _extract_guides(mpl_ax) -# for guide in guides: -# axes_model.guides.append(guide.to_dict()) -# -# return axes_model -# -# -# def tracking_to_plot_configs( -# history: Dict[str, Tuple], -# ) -> List[PlotModel]: -# """ -# Convert scitex.plt tracking history to PlotModel configurations. -# -# Parameters -# ---------- -# history : Dict[str, Tuple] -# Tracking history from AxisWrapper -# Format: {id: (id, method_name, tracked_dict, kwargs)} -# -# Returns -# ------- -# List[PlotModel] -# List of PlotModel configurations -# """ -# plots = [] -# -# for plot_id, (_, method_name, tracked_dict, kwargs) in history.items(): -# plot_model = _history_entry_to_plot_model( -# plot_id, method_name, tracked_dict, kwargs -# ) -# if plot_model: -# plots.append(plot_model) -# -# return plots -# -# -# def collect_figure_data( -# fig, -# ) -> Dict[str, Any]: -# """ -# Collect all data from a figure for export. -# -# This is a simpler version that just extracts data without -# full vis model conversion. -# -# Parameters -# ---------- -# fig : scitex.plt.FigWrapper or matplotlib.figure.Figure -# The figure to collect data from -# -# Returns -# ------- -# Dict[str, Any] -# Dictionary with figure data organized by axes/plot -# """ -# data = { -# "figure": {}, -# "axes": [], -# } -# -# mpl_fig = _get_mpl_figure(fig) -# -# # Figure info -# data["figure"]["width_mm"] = mpl_fig.get_figwidth() * 25.4 -# data["figure"]["height_mm"] = mpl_fig.get_figheight() * 25.4 -# data["figure"]["dpi"] = mpl_fig.get_dpi() -# -# # Get scitex axes for history -# scitex_axes = _get_scitex_axes(fig) -# -# # Collect axes data -# for idx, ax in enumerate(mpl_fig.axes): -# mpl_ax = ax._axes_mpl if hasattr(ax, "_axes_mpl") else ax -# scitex_ax = _find_scitex_axis(scitex_axes, mpl_ax) -# -# axes_data = { -# "index": idx, -# "xlabel": mpl_ax.get_xlabel(), -# "ylabel": mpl_ax.get_ylabel(), -# "title": mpl_ax.get_title(), -# "xlim": list(mpl_ax.get_xlim()), -# "ylim": list(mpl_ax.get_ylim()), -# "plots": [], -# } -# -# # Get plot data from history -# if scitex_ax and hasattr(scitex_ax, "history"): -# for plot_id, (_, method, tracked, kwargs) in scitex_ax.history.items(): -# plot_data = { -# "id": plot_id, -# "method": method, -# "kwargs": {k: v for k, v in kwargs.items() if _is_serializable(v)}, -# } -# # Extract data arrays from tracked_dict -# if "args" in tracked: -# plot_data["args"] = [ -# _array_to_list(a) for a in tracked["args"] -# if _is_array_like(a) -# ] -# axes_data["plots"].append(plot_data) -# -# data["axes"].append(axes_data) -# -# return data -# -# -# # ============================================================================= -# # Helper Functions -# # ============================================================================= -# -# -# def _get_mpl_figure(fig): -# """Get the underlying matplotlib figure.""" -# if hasattr(fig, "_fig_mpl"): -# return fig._fig_mpl -# return fig -# -# -# def _get_scitex_axes(fig): -# """Get scitex axes wrappers from figure.""" -# if hasattr(fig, "_axes_scitex"): -# axes = fig._axes_scitex -# if hasattr(axes, "flat"): -# return list(axes.flat) -# return [axes] -# return [] -# -# -# def _find_scitex_axis(scitex_axes, mpl_ax): -# """Find the scitex axis wrapper that wraps the given mpl axis.""" -# for ax in scitex_axes: -# if hasattr(ax, "_axes_mpl") and ax._axes_mpl is mpl_ax: -# return ax -# return None -# -# -# def _infer_layout(axes_list, fig) -> Tuple[int, int]: -# """Infer nrows, ncols from axes positions.""" -# if not axes_list: -# return 1, 1 -# -# # Check if using gridspec -# if hasattr(fig, "_gridspecs") and fig._gridspecs: -# gs = fig._gridspecs[0] -# return gs.nrows, gs.ncols -# -# # Fallback: guess from axes count -# n = len(axes_list) -# if n == 1: -# return 1, 1 -# elif n == 2: -# return 1, 2 -# elif n <= 4: -# return 2, 2 -# else: -# # Try to make it roughly square -# import math -# ncols = int(math.ceil(math.sqrt(n))) -# nrows = int(math.ceil(n / ncols)) -# return nrows, ncols -# -# -# def _color_to_hex(color) -> str: -# """Convert matplotlib color to hex string.""" -# try: -# import matplotlib.colors as mcolors -# rgb = mcolors.to_rgb(color) -# return "#{:02x}{:02x}{:02x}".format( -# int(rgb[0] * 255), -# int(rgb[1] * 255), -# int(rgb[2] * 255), -# ) -# except (ValueError, TypeError): -# return "#ffffff" -# -# -# def _extract_axes_style(mpl_ax) -> AxesStyle: -# """Extract style information from matplotlib axes.""" -# # Check grid visibility -# grid_visible = False -# try: -# gridlines = mpl_ax.xaxis.get_gridlines() -# if gridlines: -# grid_visible = gridlines[0].get_visible() -# except (AttributeError, IndexError): -# pass -# -# return AxesStyle( -# facecolor=_color_to_hex(mpl_ax.get_facecolor()), -# grid=grid_visible, -# spines_visible={ -# "top": mpl_ax.spines["top"].get_visible(), -# "right": mpl_ax.spines["right"].get_visible(), -# "bottom": mpl_ax.spines["bottom"].get_visible(), -# "left": mpl_ax.spines["left"].get_visible(), -# }, -# ) -# -# -# def _text_to_annotation(text_obj) -> Optional[AnnotationModel]: -# """Convert matplotlib text object to AnnotationModel.""" -# text = text_obj.get_text() -# if not text or not text.strip(): -# return None -# -# pos = text_obj.get_position() -# -# style = TextStyle( -# fontsize=text_obj.get_fontsize(), -# color=_color_to_hex(text_obj.get_color()), -# ha=text_obj.get_ha(), -# va=text_obj.get_va(), -# rotation=text_obj.get_rotation(), -# ) -# -# return AnnotationModel( -# annotation_type="text", -# text=text, -# x=pos[0], -# y=pos[1], -# style=style, -# ) -# -# -# def _extract_guides(mpl_ax) -> List[GuideModel]: -# """Extract guide lines (axhline, axvline) from axes.""" -# guides = [] -# -# # Check for horizontal lines -# for line in mpl_ax.lines: -# data = line.get_xydata() -# if len(data) >= 2: -# # Check if horizontal (y values same) -# if data[0][1] == data[-1][1] and data[0][0] != data[-1][0]: -# xlim = mpl_ax.get_xlim() -# if abs(data[0][0] - xlim[0]) < 0.01 and abs(data[-1][0] - xlim[1]) < 0.01: -# guides.append(GuideModel( -# guide_type="axhline", -# y=data[0][1], -# color=_color_to_hex(line.get_color()), -# linestyle=line.get_linestyle(), -# linewidth=line.get_linewidth(), -# )) -# # Check if vertical -# elif data[0][0] == data[-1][0] and data[0][1] != data[-1][1]: -# ylim = mpl_ax.get_ylim() -# if abs(data[0][1] - ylim[0]) < 0.01 and abs(data[-1][1] - ylim[1]) < 0.01: -# guides.append(GuideModel( -# guide_type="axvline", -# x=data[0][0], -# color=_color_to_hex(line.get_color()), -# linestyle=line.get_linestyle(), -# linewidth=line.get_linewidth(), -# )) -# -# return guides -# -# -# def _history_entry_to_plot_model( -# plot_id: str, -# method_name: str, -# tracked_dict: Dict, -# kwargs: Dict, -# ) -> Optional[PlotModel]: -# """Convert a tracking history entry to PlotModel.""" -# # Map matplotlib methods to vis plot types -# method_to_type = { -# "plot": "line", -# "scatter": "scatter", -# "bar": "bar", -# "barh": "barh", -# "hist": "histogram", -# "boxplot": "boxplot", -# "violinplot": "violin", -# "fill_between": "fill_between", -# "errorbar": "errorbar", -# "imshow": "imshow", -# "contour": "contour", -# "contourf": "contourf", -# } -# -# plot_type = method_to_type.get(method_name, method_name) -# -# # Extract data from tracked_dict -# data = {} -# if "args" in tracked_dict: -# args = tracked_dict["args"] -# if method_name in ("plot", "scatter") and len(args) >= 2: -# data["x"] = _array_to_list(args[0]) -# data["y"] = _array_to_list(args[1]) -# elif method_name == "bar" and len(args) >= 2: -# data["x"] = _array_to_list(args[0]) -# data["height"] = _array_to_list(args[1]) -# elif method_name == "hist" and len(args) >= 1: -# data["x"] = _array_to_list(args[0]) -# -# # Extract style from kwargs -# style = PlotStyle() -# if "color" in kwargs: -# style.color = _color_to_hex(kwargs["color"]) if kwargs["color"] else None -# if "linewidth" in kwargs or "lw" in kwargs: -# style.linewidth = kwargs.get("linewidth") or kwargs.get("lw") -# if "linestyle" in kwargs or "ls" in kwargs: -# style.linestyle = kwargs.get("linestyle") or kwargs.get("ls") -# if "marker" in kwargs: -# style.marker = kwargs.get("marker") -# if "alpha" in kwargs: -# style.alpha = kwargs.get("alpha") -# if "label" in kwargs: -# style.label = kwargs.get("label") -# -# return PlotModel( -# plot_type=plot_type, -# plot_id=plot_id, -# data=data, -# style=style, -# ) -# -# -# def _array_to_list(arr) -> List: -# """Convert array-like to list for serialization.""" -# if hasattr(arr, "tolist"): -# return arr.tolist() -# elif isinstance(arr, (list, tuple)): -# return list(arr) -# return [arr] -# -# -# def _is_array_like(obj) -> bool: -# """Check if object is array-like.""" -# return hasattr(obj, "__len__") and not isinstance(obj, (str, dict)) -# -# -# def _is_serializable(obj) -> bool: -# """Check if object is JSON serializable.""" -# import json -# try: -# json.dumps(obj) -# return True -# except (TypeError, ValueError): -# return False -# -# -# __all__ = [ -# "figure_to_vis_model", -# "axes_to_vis_axes", -# "tracking_to_plot_configs", -# "collect_figure_data", -# ] -# -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_plt_vis.py -# -------------------------------------------------------------------------------- diff --git a/tests/scitex/bridge/test__protocol.py b/tests/scitex/bridge/test__protocol.py deleted file mode 100644 index 8c0b7859..00000000 --- a/tests/scitex/bridge/test__protocol.py +++ /dev/null @@ -1,498 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File: ./tests/scitex/bridge/test__protocol.py -"""Tests for bridge protocol versioning.""" - -import pytest - -from scitex.bridge._protocol import ( - BRIDGE_PROTOCOL_VERSION, - COORDINATE_SYSTEMS, - ProtocolInfo, - add_protocol_metadata, - check_protocol_compatibility, - extract_protocol_metadata, - parse_version, -) - - -class TestProtocolVersion: - """Tests for protocol version constant.""" - - def test_version_format(self): - """Protocol version should follow semver format.""" - parts = BRIDGE_PROTOCOL_VERSION.split(".") - assert len(parts) == 3 - assert all(part.isdigit() for part in parts) - - def test_current_version(self): - """Current version should be 1.0.0.""" - assert BRIDGE_PROTOCOL_VERSION == "1.0.0" - - -class TestParseVersion: - """Tests for version parsing.""" - - def test_full_version(self): - """Parse full version string.""" - major, minor, patch = parse_version("1.2.3") - assert major == 1 - assert minor == 2 - assert patch == 3 - - def test_partial_version(self): - """Parse partial version string.""" - major, minor, patch = parse_version("2.1") - assert major == 2 - assert minor == 1 - assert patch == 0 - - def test_major_only(self): - """Parse major version only.""" - major, minor, patch = parse_version("3") - assert major == 3 - assert minor == 0 - assert patch == 0 - - -class TestProtocolCompatibility: - """Tests for protocol compatibility checking.""" - - def test_same_version_compatible(self): - """Same version should be compatible.""" - is_compat, msg = check_protocol_compatibility("1.0.0", "1.0.0") - assert is_compat is True - assert msg is None - - def test_older_minor_compatible(self): - """Older minor version should be compatible.""" - is_compat, msg = check_protocol_compatibility("1.0.0", "1.1.0") - assert is_compat is True - assert msg is None - - def test_newer_minor_warning(self): - """Newer minor version should warn but be compatible.""" - is_compat, msg = check_protocol_compatibility("1.2.0", "1.0.0") - assert is_compat is True - assert msg is not None - assert "newer" in msg.lower() - - def test_major_mismatch_incompatible(self): - """Different major version should be incompatible.""" - is_compat, msg = check_protocol_compatibility("2.0.0", "1.0.0") - assert is_compat is False - assert msg is not None - assert "major" in msg.lower() - - def test_current_version_default(self): - """Should use BRIDGE_PROTOCOL_VERSION as default.""" - is_compat, msg = check_protocol_compatibility(BRIDGE_PROTOCOL_VERSION) - assert is_compat is True - - -class TestProtocolInfo: - """Tests for ProtocolInfo dataclass.""" - - def test_default_values(self): - """Default values should be set.""" - info = ProtocolInfo() - assert info.version == BRIDGE_PROTOCOL_VERSION - assert info.coordinate_system == "data" - - def test_custom_values(self): - """Custom values should be set.""" - info = ProtocolInfo( - source_module="stats", - target_module="vis", - coordinate_system="axes", - ) - assert info.source_module == "stats" - assert info.target_module == "vis" - assert info.coordinate_system == "axes" - - def test_to_dict(self): - """Should convert to dictionary.""" - info = ProtocolInfo(source_module="plt", target_module="vis") - d = info.to_dict() - - assert d["bridge_protocol_version"] == BRIDGE_PROTOCOL_VERSION - assert d["source_module"] == "plt" - assert d["target_module"] == "vis" - - def test_from_dict(self): - """Should create from dictionary.""" - d = { - "bridge_protocol_version": "1.0.0", - "source_module": "stats", - "target_module": "plt", - "coordinate_system": "mm", - } - info = ProtocolInfo.from_dict(d) - - assert info.version == "1.0.0" - assert info.source_module == "stats" - assert info.target_module == "plt" - assert info.coordinate_system == "mm" - - -class TestProtocolMetadata: - """Tests for protocol metadata utilities.""" - - def test_add_metadata(self): - """Should add protocol metadata to dict.""" - data = {"x": 10, "y": 20} - result = add_protocol_metadata(data, "stats", "vis", "data") - - assert "_bridge_protocol" in result - assert ( - result["_bridge_protocol"]["bridge_protocol_version"] - == BRIDGE_PROTOCOL_VERSION - ) - assert result["_bridge_protocol"]["source_module"] == "stats" - assert result["_bridge_protocol"]["target_module"] == "vis" - - def test_extract_metadata(self): - """Should extract protocol metadata from dict.""" - data = { - "x": 10, - "_bridge_protocol": { - "bridge_protocol_version": "1.0.0", - "source_module": "plt", - "target_module": "vis", - "coordinate_system": "axes", - }, - } - info = extract_protocol_metadata(data) - - assert info is not None - assert info.source_module == "plt" - assert info.coordinate_system == "axes" - - def test_extract_missing_metadata(self): - """Should return None if no metadata.""" - data = {"x": 10, "y": 20} - info = extract_protocol_metadata(data) - assert info is None - - -class TestCoordinateSystems: - """Tests for coordinate system definitions.""" - - def test_axes_defined(self): - """Axes coordinate system should be defined.""" - assert "axes" in COORDINATE_SYSTEMS - assert COORDINATE_SYSTEMS["axes"]["x_range"] == (0.0, 1.0) - - def test_data_defined(self): - """Data coordinate system should be defined.""" - assert "data" in COORDINATE_SYSTEMS - assert COORDINATE_SYSTEMS["data"]["x_range"] is None # Depends on data - - def test_mm_defined(self): - """Millimeter coordinate system should be defined.""" - assert "mm" in COORDINATE_SYSTEMS - - def test_px_defined(self): - """Pixel coordinate system should be defined.""" - assert "px" in COORDINATE_SYSTEMS - - -# EOF - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_protocol.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # -*- coding: utf-8 -*- -# # File: ./src/scitex/bridge/_protocol.py -# # Time-stamp: "2024-12-09 09:30:00 (ywatanabe)" -# """ -# Bridge Protocol - Versioning and compatibility for cross-module communication. -# -# This module defines the bridge protocol version and provides utilities -# for ensuring compatibility between different versions of scitex modules. -# -# Protocol Versioning -# ------------------- -# The bridge protocol version follows semantic versioning: -# - MAJOR: Breaking changes in bridge interfaces -# - MINOR: New bridge functions added (backward compatible) -# - PATCH: Bug fixes (backward compatible) -# -# Usage: -# from scitex.bridge import BRIDGE_PROTOCOL_VERSION, check_protocol_compatibility -# """ -# -# from typing import Dict, Any, Tuple, Optional -# from dataclasses import dataclass -# -# -# # ============================================================================= -# # Protocol Version -# # ============================================================================= -# -# BRIDGE_PROTOCOL_VERSION = "1.0.0" -# """ -# Current bridge protocol version. -# -# Changes: -# - 1.0.0: Initial protocol -# - Stats → Plt: add_stat_to_axes, extract_stats_from_axes -# - Stats → Vis: stat_result_to_annotation, add_stats_to_figure_model -# - Plt → Vis: figure_to_vis_model, axes_to_vis_axes -# - Coordinate conventions: axes coords (0-1) for plt, data coords for vis -# """ -# -# -# # ============================================================================= -# # Protocol Metadata -# # ============================================================================= -# -# -# @dataclass -# class ProtocolInfo: -# """ -# Bridge protocol information for serialization and compatibility. -# -# Parameters -# ---------- -# version : str -# Protocol version string (semver) -# source_module : str -# Module that created the data -# target_module : str -# Target module for the data -# coordinate_system : str -# Coordinate system used ("axes", "data", "figure", "mm", "px") -# """ -# -# version: str = BRIDGE_PROTOCOL_VERSION -# source_module: str = "" -# target_module: str = "" -# coordinate_system: str = "data" -# -# def to_dict(self) -> Dict[str, Any]: -# """Convert to dictionary.""" -# return { -# "bridge_protocol_version": self.version, -# "source_module": self.source_module, -# "target_module": self.target_module, -# "coordinate_system": self.coordinate_system, -# } -# -# @classmethod -# def from_dict(cls, data: Dict[str, Any]) -> "ProtocolInfo": -# """Create from dictionary.""" -# return cls( -# version=data.get("bridge_protocol_version", BRIDGE_PROTOCOL_VERSION), -# source_module=data.get("source_module", ""), -# target_module=data.get("target_module", ""), -# coordinate_system=data.get("coordinate_system", "data"), -# ) -# -# -# # ============================================================================= -# # Compatibility Utilities -# # ============================================================================= -# -# -# def parse_version(version: str) -> Tuple[int, int, int]: -# """ -# Parse a version string into (major, minor, patch) tuple. -# -# Parameters -# ---------- -# version : str -# Version string like "1.2.3" -# -# Returns -# ------- -# tuple -# (major, minor, patch) integers -# """ -# parts = version.split(".") -# major = int(parts[0]) if len(parts) > 0 else 0 -# minor = int(parts[1]) if len(parts) > 1 else 0 -# patch = int(parts[2]) if len(parts) > 2 else 0 -# return (major, minor, patch) -# -# -# def check_protocol_compatibility( -# data_version: str, -# current_version: str = BRIDGE_PROTOCOL_VERSION, -# ) -> Tuple[bool, Optional[str]]: -# """ -# Check if a data version is compatible with the current protocol. -# -# Parameters -# ---------- -# data_version : str -# Version of the data being loaded -# current_version : str -# Current protocol version (default: BRIDGE_PROTOCOL_VERSION) -# -# Returns -# ------- -# tuple -# (is_compatible, warning_message) -# - is_compatible: True if data can be safely used -# - warning_message: None if compatible, else a warning string -# -# Examples -# -------- -# >>> is_compat, msg = check_protocol_compatibility("1.0.0") -# >>> is_compat -# True -# -# >>> is_compat, msg = check_protocol_compatibility("2.0.0") -# >>> is_compat -# False -# >>> msg -# 'Major version mismatch: data v2.0.0, current v1.0.0' -# """ -# data_major, data_minor, _ = parse_version(data_version) -# curr_major, curr_minor, _ = parse_version(current_version) -# -# # Major version mismatch = incompatible -# if data_major != curr_major: -# return ( -# False, -# f"Major version mismatch: data v{data_version}, current v{current_version}", -# ) -# -# # Minor version newer than current = warning (may have unknown fields) -# if data_minor > curr_minor: -# return ( -# True, -# f"Data version newer than current: data v{data_version}, " -# f"current v{current_version}. Some features may be ignored.", -# ) -# -# return (True, None) -# -# -# def add_protocol_metadata( -# data: Dict[str, Any], -# source_module: str, -# target_module: str, -# coordinate_system: str = "data", -# ) -> Dict[str, Any]: -# """ -# Add bridge protocol metadata to a dictionary. -# -# Parameters -# ---------- -# data : dict -# Data dictionary to annotate -# source_module : str -# Source module name (e.g., "stats", "plt") -# target_module : str -# Target module name (e.g., "vis", "plt") -# coordinate_system : str -# Coordinate system used (default: "data") -# -# Returns -# ------- -# dict -# Data with protocol metadata added -# -# Examples -# -------- -# >>> data = {"x": 10, "y": 20} -# >>> annotated = add_protocol_metadata(data, "stats", "vis") -# >>> annotated["_bridge_protocol"]["bridge_protocol_version"] -# '1.0.0' -# """ -# protocol = ProtocolInfo( -# source_module=source_module, -# target_module=target_module, -# coordinate_system=coordinate_system, -# ) -# data["_bridge_protocol"] = protocol.to_dict() -# return data -# -# -# def extract_protocol_metadata(data: Dict[str, Any]) -> Optional[ProtocolInfo]: -# """ -# Extract bridge protocol metadata from a dictionary. -# -# Parameters -# ---------- -# data : dict -# Data dictionary that may contain protocol metadata -# -# Returns -# ------- -# ProtocolInfo or None -# Protocol info if present, None otherwise -# """ -# if "_bridge_protocol" in data: -# return ProtocolInfo.from_dict(data["_bridge_protocol"]) -# return None -# -# -# # ============================================================================= -# # Coordinate System Definitions -# # ============================================================================= -# -# COORDINATE_SYSTEMS = { -# "axes": { -# "description": "Normalized axes coordinates (0-1)", -# "x_range": (0.0, 1.0), -# "y_range": (0.0, 1.0), -# "used_by": ["plt", "matplotlib"], -# }, -# "data": { -# "description": "Data coordinates (actual x/y values)", -# "x_range": None, # Depends on data -# "y_range": None, -# "used_by": ["vis", "FigureModel"], -# }, -# "figure": { -# "description": "Figure coordinates (0-1 over entire figure)", -# "x_range": (0.0, 1.0), -# "y_range": (0.0, 1.0), -# "used_by": ["matplotlib", "suptitle"], -# }, -# "mm": { -# "description": "Physical millimeters", -# "x_range": None, # Depends on figure size -# "y_range": None, -# "used_by": ["vis", "publication"], -# }, -# "px": { -# "description": "Pixels", -# "x_range": None, # Depends on DPI and size -# "y_range": None, -# "used_by": ["canvas", "gui"], -# }, -# } -# -# -# # ============================================================================= -# # Public API -# # ============================================================================= -# -# __all__ = [ -# "BRIDGE_PROTOCOL_VERSION", -# "ProtocolInfo", -# "parse_version", -# "check_protocol_compatibility", -# "add_protocol_metadata", -# "extract_protocol_metadata", -# "COORDINATE_SYSTEMS", -# ] -# -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_protocol.py -# -------------------------------------------------------------------------------- diff --git a/tests/scitex/bridge/test__stats_plt.py b/tests/scitex/bridge/test__stats_plt.py deleted file mode 100644 index 291ddb6e..00000000 --- a/tests/scitex/bridge/test__stats_plt.py +++ /dev/null @@ -1,400 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# File: ./tests/scitex/bridge/test__stats_plt.py -# Time-stamp: "2024-12-09 10:30:00 (ywatanabe)" -"""Tests for scitex.bridge._stats_plt module.""" - -import pytest - - -class TestFormatStatForPlot: - """Tests for format_stat_for_plot function.""" - - def test_asterisk_format(self): - """Test asterisk formatting.""" - from scitex.bridge import format_stat_for_plot - from scitex.schema import create_stat_result - - result = create_stat_result("t-test", "t", 2.5, 0.001) - text = format_stat_for_plot(result, "asterisk") - assert text == "***" - - def test_compact_format(self): - """Test compact formatting.""" - from scitex.bridge import format_stat_for_plot - from scitex.schema import create_stat_result - - result = create_stat_result("pearson", "r", 0.85, 0.001) - text = format_stat_for_plot(result, "compact") - assert "r = 0.850" in text - assert "***" in text - - def test_ns_format(self): - """Test non-significant formatting.""" - from scitex.bridge import format_stat_for_plot - from scitex.schema import StatResult - - # Use StatResult directly with stars="ns" to test ns formatting - result = StatResult( - test_type="t-test", - test_category="parametric", - statistic={"name": "t", "value": 0.5}, - p_value=0.6, - stars="ns", - ) - text = format_stat_for_plot(result, "asterisk") - assert text == "ns" - - -class TestAddStatToAxes: - """Tests for add_stat_to_axes function.""" - - @pytest.fixture - def mock_ax(self): - """Create a mock matplotlib axes.""" - import matplotlib.pyplot as plt - - fig, ax = plt.subplots() - yield ax - plt.close(fig) - - def test_add_stat_creates_annotation(self, mock_ax): - """Test that add_stat_to_axes creates an annotation.""" - from scitex.bridge import add_stat_to_axes - from scitex.schema import create_stat_result - - result = create_stat_result("t-test", "t", 2.5, 0.01) - annotation = add_stat_to_axes(mock_ax, result, x=0.5, y=0.9) - - assert annotation is not None - assert annotation.get_text() == "**" - - def test_stat_result_stored_on_annotation(self, mock_ax): - """Test that StatResult is stored on annotation.""" - from scitex.bridge import add_stat_to_axes - from scitex.schema import create_stat_result - - result = create_stat_result("t-test", "t", 2.5, 0.01) - annotation = add_stat_to_axes(mock_ax, result, x=0.5, y=0.9) - - assert hasattr(annotation, "_scitex_stat_result") - assert annotation._scitex_stat_result is result - - -class TestExtractStatsFromAxes: - """Tests for extract_stats_from_axes function.""" - - @pytest.fixture - def ax_with_stats(self): - """Create axes with stat annotations.""" - import matplotlib.pyplot as plt - - from scitex.bridge import add_stat_to_axes - from scitex.schema import create_stat_result - - fig, ax = plt.subplots() - result1 = create_stat_result("t-test", "t", 2.5, 0.01) - result2 = create_stat_result("pearson", "r", 0.85, 0.001) - add_stat_to_axes(ax, result1, x=0.3, y=0.9) - add_stat_to_axes(ax, result2, x=0.7, y=0.9) - yield ax - plt.close(fig) - - def test_extract_returns_stat_results(self, ax_with_stats): - """Test extraction of StatResults from axes.""" - from scitex.bridge import extract_stats_from_axes - - stats = extract_stats_from_axes(ax_with_stats) - assert len(stats) == 2 - assert stats[0].test_type == "t-test" - assert stats[1].test_type == "pearson" - - -# EOF - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_stats_plt.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # File: ./src/scitex/bridge/_stats_plt.py -# # Time-stamp: "2024-12-09 10:00:00 (ywatanabe)" -# """ -# Bridge module for stats ↔ plt integration. -# -# Provides adapters to: -# - Add statistical results as annotations on matplotlib axes -# - Extract stats from axes with tracked annotations -# - Format statistical text for plot display -# -# Coordinate Convention -# --------------------- -# This module uses **axes coordinates** (0-1 normalized) for positioning -# when auto-positioning is used (no explicit x, y given). This matches -# matplotlib's typical annotation workflow where positions are relative -# to the axes bounding box. -# -# - (0, 0) = bottom-left of axes -# - (1, 1) = top-right of axes -# - Default auto-position: (0.5, 0.95) = top-center -# -# When explicit x, y are provided, they use whatever coordinate system -# the caller intends (typically data coordinates unless transform is set). -# """ -# -# from typing import Any, List, Optional -# -# # Import GUI classes from FTS (single source of truth) -# -# # StatResult is now a dict - the GUI-specific StatResult is deprecated -# StatResult = dict -# -# -# def format_stat_for_plot( -# stat_result: StatResult, -# format_style: str = "asterisk", -# ) -> str: -# """ -# Format a StatResult for display on a plot. -# -# Parameters -# ---------- -# stat_result : StatResult -# The statistical result to format -# format_style : str -# One of "asterisk", "compact", "detailed", "publication" -# -# Returns -# ------- -# str -# Formatted string for plot annotation -# """ -# return stat_result.format_text(format_style) -# -# -# def add_stat_to_axes( -# ax, -# stat_result: StatResult, -# x: Optional[float] = None, -# y: Optional[float] = None, -# format_style: str = "asterisk", -# **kwargs, -# ) -> Any: -# """ -# Add a statistical result annotation to a matplotlib axes. -# -# Parameters -# ---------- -# ax : matplotlib.axes.Axes or scitex.plt AxisWrapper -# The axes to annotate -# stat_result : StatResult -# The statistical result to display -# x : float, optional -# X position for the annotation. If None, uses stat_result.positioning -# y : float, optional -# Y position for the annotation. If None, uses stat_result.positioning -# format_style : str -# Format style for the text ("asterisk", "compact", "detailed", "publication") -# **kwargs -# Additional kwargs passed to ax.annotate() or ax.text() -# -# Returns -# ------- -# matplotlib.text.Text or matplotlib.text.Annotation -# The created annotation object -# """ -# # Get formatted text -# text = format_stat_for_plot(stat_result, format_style) -# -# # Determine position -# # Check if using StatResult positioning -# use_stat_positioning = False -# if x is None or y is None: -# positioning = stat_result.positioning -# if positioning and positioning.position: -# pos = positioning.position -# if x is None: -# x = pos.x -# if y is None: -# y = pos.y -# use_stat_positioning = True -# -# # Auto-position: top center of axes in axes coordinates -# if x is None: -# x = 0.5 -# if y is None: -# y = 0.95 -# -# # Default to axes coordinates (0-1) unless user explicitly sets transform -# # This makes positioning intuitive: (0.5, 0.9) = top center of plot -# if "transform" not in kwargs: -# kwargs["transform"] = _get_axes_transform(ax) -# kwargs.setdefault("ha", "center") -# kwargs.setdefault("va", "top") -# -# # Apply styling from StatResult if available -# styling = stat_result.styling -# if styling: -# kwargs.setdefault("fontsize", styling.font_size_pt) -# kwargs.setdefault("fontfamily", styling.font_family) -# kwargs.setdefault("color", styling.color) -# -# # Get the actual matplotlib axes -# mpl_ax = _get_mpl_axes(ax) -# -# # Create the annotation -# annotation = mpl_ax.text(x, y, text, **kwargs) -# -# # Store reference to stat_result on the annotation for later extraction -# annotation._scitex_stat_result = stat_result -# -# return annotation -# -# -# def extract_stats_from_axes( -# ax, -# include_non_stat: bool = False, -# ) -> List[StatResult]: -# """ -# Extract StatResult objects from axes annotations. -# -# Parameters -# ---------- -# ax : matplotlib.axes.Axes or scitex.plt AxisWrapper -# The axes to extract stats from -# include_non_stat : bool -# If True, create basic StatResult for non-stat annotations -# -# Returns -# ------- -# List[StatResult] -# List of StatResult objects found in annotations -# """ -# results = [] -# mpl_ax = _get_mpl_axes(ax) -# -# # Check all text objects (annotations and texts) -# for text_obj in mpl_ax.texts: -# if hasattr(text_obj, "_scitex_stat_result"): -# results.append(text_obj._scitex_stat_result) -# elif include_non_stat: -# # Create a basic StatResult for non-stat text -# content = text_obj.get_text() -# if content.strip(): -# # Try to detect if it's a stat-like annotation -# result = _parse_stat_annotation(content) -# if result: -# results.append(result) -# -# return results -# -# -# def _get_mpl_axes(ax): -# """Get the underlying matplotlib axes from wrapper or native.""" -# # Handle scitex AxisWrapper -# if hasattr(ax, "_axes_mpl"): -# return ax._axes_mpl -# # Handle scitex AxesWrapper (multiple axes) -# if hasattr(ax, "_axes_scitex"): -# axes = ax._axes_scitex -# if hasattr(axes, "flat"): -# return axes.flat[0] -# return axes -# # Already matplotlib axes -# return ax -# -# -# def _get_axes_transform(ax): -# """Get the axes transform for positioning.""" -# mpl_ax = _get_mpl_axes(ax) -# return mpl_ax.transAxes -# -# -# def _parse_stat_annotation(text: str) -> Optional[StatResult]: -# """ -# Try to parse a text annotation as a statistical result. -# -# Parameters -# ---------- -# text : str -# Text content to parse -# -# Returns -# ------- -# Optional[StatResult] -# Parsed StatResult or None if not parseable -# """ -# text = text.strip() -# -# def _create_stat_dict(test_type, statistic_name, statistic_value, p_value): -# """Create a simple stat result dict.""" -# from scitex.stats._utils import p2stars -# -# return { -# "test_type": test_type, -# "test_category": "other", -# "statistic": {"name": statistic_name, "value": statistic_value}, -# "p_value": p_value, -# "stars": p2stars(p_value, ns_symbol=False), -# } -# -# # Try to detect asterisks pattern -# if text in ["*", "**", "***", "ns", "n.s."]: -# stars = text.replace("n.s.", "ns") -# # Can't determine actual stats, create placeholder -# p_value = { -# "***": 0.0001, -# "**": 0.005, -# "*": 0.03, -# "ns": 0.5, -# }.get(stars, 0.5) -# return _create_stat_dict( -# test_type="unknown", -# statistic_name="stat", -# statistic_value=0.0, -# p_value=p_value, -# ) -# -# # Try to parse patterns like "r = 0.85***" or "(t = 2.5, p < 0.01)" -# import re -# -# # Pattern: statistic = value[stars] -# match = re.match(r"([a-zA-Z]+)\s*=\s*([\d.-]+)(\*+|ns)?", text) -# if match: -# stat_name = match.group(1) -# stat_value = float(match.group(2)) -# stars = match.group(3) or "ns" -# p_value = { -# "***": 0.0001, -# "**": 0.005, -# "*": 0.03, -# "ns": 0.5, -# }.get(stars, 0.5) -# return _create_stat_dict( -# test_type="unknown", -# statistic_name=stat_name, -# statistic_value=stat_value, -# p_value=p_value, -# ) -# -# return None -# -# -# __all__ = [ -# "add_stat_to_axes", -# "extract_stats_from_axes", -# "format_stat_for_plot", -# ] -# -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_stats_plt.py -# -------------------------------------------------------------------------------- diff --git a/tests/scitex/bridge/test__stats_vis.py b/tests/scitex/bridge/test__stats_vis.py deleted file mode 100644 index 183ac3fa..00000000 --- a/tests/scitex/bridge/test__stats_vis.py +++ /dev/null @@ -1,415 +0,0 @@ -#!/usr/bin/env python3 -# File: ./tests/scitex/bridge/test__stats_vis.py -# Time-stamp: "2024-12-09 10:30:00 (ywatanabe)" -"""Tests for scitex.bridge._stats_vis module.""" - -import pytest - - -class TestStatResultToAnnotation: - """Tests for stat_result_to_annotation function.""" - - def test_creates_annotation_model(self): - """Test that function creates AnnotationModel.""" - from scitex.bridge import stat_result_to_annotation - from scitex.io.bundle.kinds._plot._models import AnnotationModel - from scitex.schema import create_stat_result - - result = create_stat_result("t-test", "t", 2.5, 0.01) - annotation = stat_result_to_annotation(result) - - assert isinstance(annotation, AnnotationModel) - assert annotation.text == "**" - assert annotation.annotation_type == "text" - - def test_annotation_has_position(self): - """Test annotation has position set.""" - from scitex.bridge import stat_result_to_annotation - from scitex.schema import create_stat_result - - result = create_stat_result("t-test", "t", 2.5, 0.01) - annotation = stat_result_to_annotation(result, x=0.5, y=0.9) - - assert annotation.x == 0.5 - assert annotation.y == 0.9 - - def test_format_style_applied(self): - """Test different format styles.""" - from scitex.bridge import stat_result_to_annotation - from scitex.schema import create_stat_result - - result = create_stat_result("pearson", "r", 0.85, 0.001) - - ann_asterisk = stat_result_to_annotation(result, format_style="asterisk") - ann_compact = stat_result_to_annotation(result, format_style="compact") - - assert ann_asterisk.text == "***" - assert "r = 0.850" in ann_compact.text - - -class TestAddStatsToFigureModel: - """Tests for add_stats_to_figure_model function.""" - - @pytest.fixture - def figure_model(self): - """Create a basic FigureModel.""" - from scitex.io.bundle.kinds._plot._models import FigureModel - - return FigureModel( - width_mm=170, - height_mm=120, - axes=[{"row": 0, "col": 0, "plots": []}], - ) - - def test_adds_annotations_to_axes(self, figure_model): - """Test adding stats adds annotations to axes.""" - from scitex.bridge import add_stats_to_figure_model - from scitex.schema import create_stat_result - - stats = [ - create_stat_result("t-test", "t", 2.5, 0.01), - create_stat_result("pearson", "r", 0.85, 0.001), - ] - result = add_stats_to_figure_model(figure_model, stats) - - assert "annotations" in result.axes[0] - assert len(result.axes[0]["annotations"]) == 2 - - def test_handles_empty_stats(self, figure_model): - """Test handling empty stats list.""" - from scitex.bridge import add_stats_to_figure_model - - result = add_stats_to_figure_model(figure_model, []) - assert result is figure_model - - -class TestPositionStatAnnotation: - """Tests for position_stat_annotation function.""" - - def test_returns_position(self): - """Test that function returns Position object.""" - from scitex.bridge import position_stat_annotation - from scitex.schema import Position, create_stat_result - - result = create_stat_result("t-test", "t", 2.5, 0.01) - bounds = {"x_min": 0, "x_max": 10, "y_min": 0, "y_max": 100} - - position = position_stat_annotation(result, bounds) - - assert isinstance(position, Position) - assert position.unit == "data" - - def test_preferred_corner(self): - """Test preferred corner positioning.""" - from scitex.bridge import position_stat_annotation - from scitex.schema import create_stat_result - - result = create_stat_result("t-test", "t", 2.5, 0.01) - bounds = {"x_min": 0, "x_max": 10, "y_min": 0, "y_max": 100} - - pos_tr = position_stat_annotation(result, bounds, preferred_corner="top-right") - pos_tl = position_stat_annotation(result, bounds, preferred_corner="top-left") - - # Top-right should have higher x than top-left - assert pos_tr.x > pos_tl.x - # Both should have same y (top) - assert abs(pos_tr.y - pos_tl.y) < 1 - - -# EOF - -if __name__ == "__main__": - import os - - import pytest - - pytest.main([os.path.abspath(__file__)]) - -# -------------------------------------------------------------------------------- -# Start of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_stats_vis.py -# -------------------------------------------------------------------------------- -# #!/usr/bin/env python3 -# # File: ./src/scitex/bridge/_stats_vis.py -# # Time-stamp: "2024-12-09 10:00:00 (ywatanabe)" -# """ -# Bridge module for stats ↔ vis integration. -# -# Provides adapters to: -# - Convert StatResult to vis AnnotationModel -# - Add statistical annotations to FigureModel -# - Position stat annotations using vis coordinate system -# -# Coordinate Convention -# --------------------- -# This module uses **data coordinates** for positioning (via Position with -# unit="data"). This matches the vis model's approach where positions -# correspond to actual data values on the plot. -# -# - Positions are in the same units as the plot data -# - position_stat_annotation() returns Position(unit="data") -# - For normalized positioning, use axes_bounds to define the data range -# -# This differs from _stats_plt which uses axes coordinates (0-1 normalized). -# When bridging between plt and vis, coordinate transformation may be needed. -# """ -# -# from typing import Dict, List, Optional, Tuple -# -# # Import GUI classes from FTS (single source of truth) -# from scitex.io.bundle.kinds._stats import Position -# -# # Legacy model imports - may not be available -# try: -# from scitex.io.bundle.kinds._plot._models import AnnotationModel, AxesModel, FigureModel, TextStyle -# -# VIS_MODEL_AVAILABLE = True -# except ImportError: -# AnnotationModel = None -# FigureModel = None -# AxesModel = None -# TextStyle = None -# VIS_MODEL_AVAILABLE = False -# -# # StatResult placeholder for type hints (actual usage is through dict) -# StatResult = dict # Use dict as StatResult is deprecated -# -# -# def stat_result_to_annotation( -# stat_result: StatResult, -# format_style: str = "asterisk", -# x: Optional[float] = None, -# y: Optional[float] = None, -# ) -> AnnotationModel: -# """ -# Convert a StatResult to a vis AnnotationModel. -# -# Parameters -# ---------- -# stat_result : StatResult -# The statistical result to convert -# format_style : str -# Format style for the text ("asterisk", "compact", "detailed", "publication") -# x : float, optional -# X position (data coordinates). Overrides stat_result positioning -# y : float, optional -# Y position (data coordinates). Overrides stat_result positioning -# -# Returns -# ------- -# AnnotationModel -# Annotation model for vis rendering -# """ -# # Get formatted text -# text = stat_result.format_text(format_style) -# -# # Determine position -# if x is None or y is None: -# positioning = stat_result.positioning -# if positioning and positioning.position: -# pos = positioning.position -# x = x if x is not None else pos.x -# y = y if y is not None else pos.y -# else: -# # Default center-top position (will be overridden by positioning logic) -# x = x if x is not None else 0.5 -# y = y if y is not None else 0.95 -# -# # Build text style from stat styling -# styling = stat_result.styling -# text_style = TextStyle( -# fontsize=styling.font_size_pt if styling else 7.0, -# color=styling.color if styling else "#000000", -# ha="center", -# va="top", -# ) -# -# # Create annotation model -# return AnnotationModel( -# annotation_type="text", -# text=text, -# x=x, -# y=y, -# annotation_id=stat_result.plot_id or f"stat_{id(stat_result)}", -# style=text_style, -# ) -# -# -# def add_stats_to_figure_model( -# figure_model: FigureModel, -# stat_results: List[StatResult], -# axes_index: int = 0, -# format_style: str = "asterisk", -# auto_position: bool = True, -# ) -> FigureModel: -# """ -# Add statistical results as annotations to a FigureModel. -# -# Parameters -# ---------- -# figure_model : FigureModel -# The figure model to annotate -# stat_results : List[StatResult] -# List of statistical results to add -# axes_index : int -# Index of axes to add annotations to -# format_style : str -# Format style for the text -# auto_position : bool -# Whether to automatically position stats to avoid overlap -# -# Returns -# ------- -# FigureModel -# The modified figure model (same instance) -# """ -# if not stat_results: -# return figure_model -# -# # Ensure axes exist -# if axes_index >= len(figure_model.axes): -# raise IndexError(f"Axes index {axes_index} out of range") -# -# axes_dict = figure_model.axes[axes_index] -# -# # Get or initialize annotations list -# if "annotations" not in axes_dict: -# axes_dict["annotations"] = [] -# -# # Calculate positions if auto_position -# positions = [] -# if auto_position: -# positions = _calculate_stat_positions( -# stat_results, -# len(axes_dict["annotations"]), -# ) -# -# # Add each stat as annotation -# for i, stat_result in enumerate(stat_results): -# x, y = positions[i] if positions else (None, None) -# annotation = stat_result_to_annotation( -# stat_result, -# format_style=format_style, -# x=x, -# y=y, -# ) -# axes_dict["annotations"].append(annotation.to_dict()) -# -# return figure_model -# -# -# def position_stat_annotation( -# stat_result: StatResult, -# axes_bounds: Dict[str, float], -# existing_positions: Optional[List[Tuple[float, float]]] = None, -# preferred_corner: str = "top-right", -# ) -> Position: -# """ -# Calculate optimal position for a stat annotation. -# -# Parameters -# ---------- -# stat_result : StatResult -# The statistical result to position -# axes_bounds : Dict[str, float] -# Axes bounds with keys: x_min, x_max, y_min, y_max -# existing_positions : List[Tuple[float, float]], optional -# List of existing annotation positions to avoid -# preferred_corner : str -# Preferred corner: "top-left", "top-right", "bottom-left", "bottom-right" -# -# Returns -# ------- -# Position -# Calculated position in data coordinates -# """ -# existing = existing_positions or [] -# -# # Get axes range -# x_min = axes_bounds.get("x_min", 0) -# x_max = axes_bounds.get("x_max", 1) -# y_min = axes_bounds.get("y_min", 0) -# y_max = axes_bounds.get("y_max", 1) -# -# x_range = x_max - x_min -# y_range = y_max - y_min -# -# # Calculate corner positions (as fraction, then convert to data) -# corner_fractions = { -# "top-right": (0.95, 0.95), -# "top-left": (0.05, 0.95), -# "bottom-right": (0.95, 0.05), -# "bottom-left": (0.05, 0.05), -# "top-center": (0.5, 0.95), -# "bottom-center": (0.5, 0.05), -# } -# -# # Start with preferred corner -# base_x, base_y = corner_fractions.get(preferred_corner, (0.95, 0.95)) -# x = x_min + base_x * x_range -# y = y_min + base_y * y_range -# -# # Check overlap and adjust if needed -# min_dist = ( -# stat_result.positioning.min_distance_mm if stat_result.positioning else 2.0 -# ) -# -# for ex_x, ex_y in existing: -# dist = ((x - ex_x) ** 2 + (y - ex_y) ** 2) ** 0.5 -# if dist < min_dist: -# # Shift down -# y -= min_dist * 1.5 -# -# return Position(x=x, y=y, unit="data") -# -# -# def _calculate_stat_positions( -# stat_results: List[StatResult], -# existing_count: int = 0, -# ) -> List[Tuple[float, float]]: -# """ -# Calculate non-overlapping positions for multiple stats. -# -# Parameters -# ---------- -# stat_results : List[StatResult] -# List of stats to position -# existing_count : int -# Number of existing annotations -# -# Returns -# ------- -# List[Tuple[float, float]] -# List of (x, y) positions in axes coordinates (0-1) -# """ -# positions = [] -# y_start = 0.95 -# y_step = 0.05 -# -# for i, stat in enumerate(stat_results): -# # Stack vertically from top -# y = y_start - (i + existing_count) * y_step -# x = 0.5 # Center -# -# # Check stat's own positioning preference -# if stat.positioning and stat.positioning.position: -# pos = stat.positioning.position -# x = pos.x -# y = pos.y -# -# positions.append((x, y)) -# -# return positions -# -# -# __all__ = [ -# "stat_result_to_annotation", -# "add_stats_to_figure_model", -# "position_stat_annotation", -# ] -# -# -# # EOF - -# -------------------------------------------------------------------------------- -# End of Source Code from: /home/ywatanabe/proj/scitex-code/src/scitex/bridge/_stats_vis.py -# --------------------------------------------------------------------------------