pax_global_header 0000666 0000000 0000000 00000000064 15140141176 0014512 g ustar 00root root 0000000 0000000 52 comment=64a20cdbff580b3d2802daf7cb3e703d9219800e
DataLab-1.1.0/ 0000775 0000000 0000000 00000000000 15140141176 0013001 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/.coveragerc 0000664 0000000 0000000 00000001253 15140141176 0015123 0 ustar 00root root 0000000 0000000 [run]
#--- Ignore this warning because the process isolation feature of DataLab
#--- causes coverage to report 0% coverage when no computation is performed
#--- in the isolated process during the session.
disable_warnings = no-data-collected
parallel = True
concurrency = multiprocessing,thread
omit =
*/datalab/utils/tests.py
*/datalab/tests/*
*/guidata/*
*/plotpy/*
*/qwt/*
*/sigima/*
#--- Workaround for certain builds of python-opencv package:
./config-3.9.py
./config-3.py
./config.py
#---
[report]
exclude_also =
def __repr__
if __name__ == .__main__.:
if TYPE_CHECKING:
if DEBUG[\s]*:
if self.__debug[\s]*:
DataLab-1.1.0/.env.template 0000664 0000000 0000000 00000000014 15140141176 0015377 0 ustar 00root root 0000000 0000000 PYTHONPATH=. DataLab-1.1.0/.github/ 0000775 0000000 0000000 00000000000 15140141176 0014341 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 15140141176 0016524 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/.github/ISSUE_TEMPLATE/bug_report.md 0000664 0000000 0000000 00000001334 15140141176 0021217 0 ustar 00root root 0000000 0000000 ---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Installation information**
- DataLab installation type ["Python package" or "stand-alone Windows version"]
- Copy/paste here the contents of "About DataLab installation..." window (Menu "?")
**Additional context**
Add any other context about the problem here.
DataLab-1.1.0/.github/ISSUE_TEMPLATE/doc_request.md 0000664 0000000 0000000 00000001141 15140141176 0021360 0 ustar 00root root 0000000 0000000 ---
name: Documentation request
about: Ask for documentation about a specific topic
title: ''
labels: documentation
assignees: ''
---
**Is your documentation request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the topic you would like to be covered**
A clear and concise description of what you want to be documented.
**Describe the nature of the documentation you would like to see**
Tutorial, reference, etc.
**Additional context**
Add any other context or screenshots about the feature request here.
DataLab-1.1.0/.github/ISSUE_TEMPLATE/feature_request.md 0000664 0000000 0000000 00000001134 15140141176 0022250 0 ustar 00root root 0000000 0000000 ---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
DataLab-1.1.0/.github/ISSUE_TEMPLATE/tutorial_request.md 0000664 0000000 0000000 00000001457 15140141176 0022470 0 ustar 00root root 0000000 0000000 ---
name: Tutorial request
about: Ask for a tutorial on a specific topic
title: ''
labels: documentation
assignees: ''
---
**Describe the context and technical field of the tutorial you would like to see**
A clear and concise description of your technical field, and of the specific application.
**Describe the topic you would like to be covered**
A clear and concise description of what you want to be documented.
**Describe the features you would like to see in the tutorial**
A clear and concise description of the features you would like to see in the tutorial.
**Additional context**
Add any other context or screenshots about the feature request here.
_Please attach an example data set, if possible.
And please confirm that you are willing to share this data set under the terms of DataLab's license._
DataLab-1.1.0/.github/copilot-instructions.md 0000664 0000000 0000000 00000050164 15140141176 0021104 0 ustar 00root root 0000000 0000000 # DataLab AI Coding Agent Instructions
This document provides comprehensive guidance for AI coding agents working on the DataLab codebase. It covers architecture patterns, development workflows, and project-specific conventions.
## Project Overview
**DataLab** is an open-source platform for scientific and technical data processing with a Qt-based GUI. It processes **signals** (1D curves) and **images** (2D arrays).
### Key Architecture Components
1. **Sigima**: Separate computation library providing all processing algorithms (`sigima.proc`)
2. **DataLab GUI**: Qt application layer built on PlotPyStack (PlotPy + guidata)
3. **Processor Pattern**: Bridge between GUI and computation functions
4. **Action Handler**: Manages menus, toolbars, and GUI actions
5. **Plugin System**: Extensible architecture for third-party features
6. **Macro System**: User-scriptable automation via Python
7. **Remote Control**: XML-RPC API for external applications
### Technology Stack
- **Python**: 3.9+ (using `from __future__ import annotations`)
- **Core Libraries**: NumPy (=1.26.4), SciPy (=1.10.1), scikit-image, OpenCV
- **GUI**: Qt via PlotPy (=2.8.2) and guidata (=3.13.3)
- **Computation**: Sigima (=1.0.2) - separate package
- **Testing**: pytest with coverage
- **Linting/Formatting**: Ruff (preferred), Pylint (with specific disables)
- **Internationalization**: gettext (.po files), sphinx-intl for docs
- **Documentation**: Sphinx with French translations
- **Packaging**: PyInstaller (standalone), WiX (MSI installer)
### Workspace Structure
```
DataLab/
+-- datalab/ # Main application code
+-- gui/ # GUI layer
+-- processor/ # Processor pattern (signal.py, image.py, base.py)
+-- actionhandler.py # Menu/action management
+-- main.py # Main window
+-- panel/ # Signal/Image panels
+-- control/ # Remote control API (proxy.py, remote.py)
+-- plugins/ # Plugin system
+-- tests/ # pytest test suite
+-- locale/ # Translations (.po files)
+-- config.py # Configuration management
+-- doc/ # Sphinx documentation
+-- locale/fr/ # French documentation translations
+-- features/ # Feature documentation (signal/, image/)
+-- macros/examples/ # Demo macros
+-- scripts/ # Build/development scripts
+-- run_with_env.py # Environment loader (.env support)
+-- .env # Local Python path (PYTHONPATH=.;../guidata;../plotpy;../sigima)
+-- pyproject.toml # Project configuration
```
**Related Projects** (sibling directories in multi-root workspace):
- `../Sigima/` - Computation library
- `../PlotPy/` - Plotting library
- `../guidata/` - GUI toolkit
- `../PythonQwt/` - Qwt bindings
## Development Workflows
### Running Commands
**ALWAYS use `scripts/run_with_env.py` for Python commands** to load environment from `.env`:
```powershell
# ? CORRECT - Loads PYTHONPATH from .env
python scripts/run_with_env.py python -m pytest
# ? WRONG - Misses local development packages
python -m pytest
```
### Testing
```powershell
# Run all tests
python scripts/run_with_env.py python -m pytest --ff
# Run specific test
python scripts/run_with_env.py python -m pytest datalab/tests/features/signal/
# Coverage
python scripts/run_with_env.py python -m coverage run -m pytest datalab
python -m coverage html
```
### Linting and Formatting
**Prefer Ruff** (fast, modern):
```powershell
# Format code
python scripts/run_with_env.py python -m ruff format
# Lint with auto-fix
python scripts/run_with_env.py python -m ruff check --fix
```
**Pylint** (with extensive disables for code structure):
```powershell
python scripts/run_with_env.py python -m pylint datalab \
--disable=duplicate-code,fixme,too-many-arguments,too-many-branches, \
too-many-instance-attributes,too-many-lines,too-many-locals, \
too-many-public-methods,too-many-statements
```
### Translations
**UI Translations** (gettext):
```powershell
# Scan and update .po files
python scripts/run_with_env.py python -m guidata.utils.translations scan \
--name datalab --directory . --copyright-holder "DataLab Platform Developers" \
--languages fr
# Compile .mo files
python scripts/run_with_env.py python -m guidata.utils.translations compile \
--name datalab --directory .
```
**Documentation Translations** (sphinx-intl):
```powershell
# Extract translatable strings
python scripts/run_with_env.py python -m sphinx build doc build/gettext -b gettext -W
# Update French .po files
python scripts/run_with_env.py python -m sphinx_intl update -d doc/locale -p build/gettext -l fr
# Build localized docs
python scripts/run_with_env.py python -m sphinx build doc build/doc -b html -D language=fr
```
## Core Patterns
### 1. Processor Pattern (GUI ? Computation Bridge)
**Location**: `datalab/gui/processor/`
**Key Concept**: Processors bridge GUI panels and Sigima computation functions. They define **generic processing types** based on input/output patterns.
#### Generic Processing Types
| Method | Pattern | Multi-selection | Use Cases |
|--------|---------|----------------|-----------|
| `compute_1_to_1` | 1 obj ? 1 obj | k ? k | Independent transformations (FFT, normalization) |
| `compute_1_to_0` | 1 obj ? metadata | k ? 0 | Analysis producing scalar results (FWHM, centroid) |
| `compute_1_to_n` | 1 obj ? n objs | k ? kn | ROI extraction, splitting |
| `compute_n_to_1` | n objs ? 1 obj | n ? 1 (or n ? n pairwise) | Averaging, summing, concatenation |
| `compute_2_to_1` | 1 obj + 1 operand ? 1 obj | k + 1 ? k (or n + n pairwise) | Binary operations (add, multiply) |
**Example: Implementing a New Processing Feature**
```python
# 1. Implement computation in Sigima (sigima/proc/signal/processing.py)
def my_processing_func(src: SignalObj, param: MyParam) -> SignalObj:
"""My processing function."""
dst = src.copy()
# ... computation logic ...
return dst
# 2. Register in DataLab processor (datalab/gui/processor/signal.py)
def register_processing(self) -> None:
self.register_1_to_1(
sips.my_processing_func,
_("My Processing"),
paramclass=MyParam,
icon_name="my_icon.svg",
)
```
#### Registration Methods
```python
# In SignalProcessor or ImageProcessor class
def register_operations(self) -> None:
"""Register operations (basic math, transformations)."""
# 1-to-1: Apply to each selected object independently
self.register_1_to_1(
sips.normalize,
_("Normalize"),
paramclass=sigima.params.NormalizeParam,
icon_name="normalize.svg",
)
# 2-to-1: Binary operation with a second operand
self.register_2_to_1(
sips.difference,
_("Difference"),
icon_name="difference.svg",
obj2_name=_("signal to subtract"),
skip_xarray_compat=False, # Enable X-array compatibility check
)
# n-to-1: Aggregate multiple objects
self.register_n_to_1(
sips.average,
_("Average"),
icon_name="average.svg",
)
```
### 2. X-array Compatibility System
**Critical Pattern**: For **2-to-1** and **n-to-1** operations on signals, DataLab checks if X arrays match. If not, it **interpolates** the second signal to match the first.
**When to Skip**: Use `skip_xarray_compat=True` when operations **intentionally use mismatched X arrays** (e.g., replacing X with Y values from another signal).
```python
# ? BAD: Will trigger unwanted interpolation
self.register_2_to_1(
sips.replace_x_by_other_y,
_("Replace X by other signal's Y"),
)
# ? GOOD: Skips compatibility check
self.register_2_to_1(
sips.replace_x_by_other_y,
_("Replace X by other signal's Y"),
skip_xarray_compat=True, # Operation uses Y values as X coordinates
)
```
**Code Location**: `datalab/gui/processor/base.py` (lines ~1764, 1886)
### 3. Action Handler Pattern (Menu Management)
**Location**: `datalab/gui/actionhandler.py`
**Purpose**: Manages all GUI actions (menus, toolbars, context menus). Actions point to processors, panels, or direct operations.
**Key Classes**:
- `SignalActionHandler`: Signal-specific actions
- `ImageActionHandler`: Image-specific actions
- `SelectCond`: Conditions for enabling/disabling actions
**Example: Adding a Menu Action**
```python
# In SignalActionHandler or ImageActionHandler
def setup_processing_actions(self) -> None:
"""Setup processing menu actions."""
# Reference registered processor operation
act = self.action_for("my_processing_func")
# Add to menu
self.processing_menu.addAction(act)
```
**Menu Organization**:
Menus are organized by function:
- `File` ? Import/export, project management
- `Edit` ? Copy/paste, delete, metadata editing
- `Operation` ? Basic math (add, multiply, etc.)
- `Processing` ? Advanced transformations, filters
- `Axis transformation` ? Calibration, X-Y mode, replace X
- `Analysis` ? Measurements, ROI extraction
- `Computing` ? FFT, convolution, fit
The complete menu structure is defined in `datalab/gui/actionhandler.py`.
A text extract of the menu hiearchy is available in `scripts/datalab_menus.txt` (it is
generated with `scripts/print_datalab_menus.py`).
### 4. Plugin System
**Location**: `datalab/plugins.py`, `datalab/plugins/`
**Key Classes**:
- `PluginBase`: Base class for all plugins (uses metaclass `PluginRegistry`)
- `PluginInfo`: Plugin metadata (name, version, description, icon)
**Example: Creating a Plugin**
```python
from datalab.plugins import PluginBase, PluginInfo
class MyPlugin(PluginBase):
"""My custom plugin."""
def __init__(self):
super().__init__()
self.info = PluginInfo(
name="My Plugin",
version="1.0.0",
description="Does something useful",
icon="my_icon.svg",
)
def register(self, mainwindow: DLMainWindow) -> None:
"""Register plugin with main window."""
# Add menu items, actions, etc.
pass
def unregister(self) -> None:
"""Unregister plugin."""
pass
```
**Plugin Discovery**: Plugins are loaded from:
1. `datalab/plugins/` (built-in)
2. User-defined paths in `Conf.get_path("plugins")`
3. For frozen apps, from `plugins/` directory next to executable
### 5. Macro System
**Location**: `macros/examples/`
**Purpose**: User-scriptable automation using Python. Macros use the **Remote Proxy** API to control DataLab.
**Example Macro**:
```python
from datalab.control.proxy import RemoteProxy
import numpy as np
proxy = RemoteProxy()
# Create signal
x = np.linspace(0, 10, 100)
y = np.sin(x)
proxy.add_signal("My Signal", x, y)
# Apply processing
proxy.calc("normalize")
proxy.calc("moving_average", sigima.params.MovingAverageParam.create(n=5))
```
**Key API Methods**:
- `proxy.add_signal()`, `proxy.add_image()`: Create objects
- `proxy.calc()`: Run processor methods
- `proxy.get_object()`: Retrieve data
- `proxy.call_method()`: Call any public panel or window method
**Generic Method Calling**:
```python
# Remove objects from current panel
proxy.call_method("remove_object", force=True)
# Call method on specific panel
proxy.call_method("delete_all_objects", panel="signal")
# Call main window method
panel_name = proxy.call_method("get_current_panel")
```
### 6. Remote Control API
**Location**: `datalab/control/`
**Classes**:
- `RemoteProxy`: XML-RPC client for remote DataLab control
- `LocalProxy`: Direct access for same-process scripting
**Calling Processor Methods**:
```python
# Without parameters
proxy.calc("average")
# With parameters
p = sigima.params.MovingAverageParam.create(n=30)
proxy.calc("moving_average", p)
```
## Coding Conventions
### Naming
- **Functions**: `snake_case` (e.g., `replace_x_by_other_y`)
- **Classes**: `PascalCase` (e.g., `SignalProcessor`)
- **Constants**: `UPPER_SNAKE_CASE` (e.g., `PARAM_DEFAULTS`)
- **Private methods**: `_snake_case` (single underscore prefix)
### UI Text vs Function Names
- **Function name**: Technical, precise (e.g., `replace_x_by_other_y`)
- **UI text**: User-friendly, explicit (e.g., _("Replace X by other signal's Y"))
- **French translation**: Natural phrasing (e.g., "Remplacer X par le Y d'un autre signal")
### Type Annotations
**Always use** `from __future__ import annotations` for forward references:
```python
from __future__ import annotations
def process_signal(src: SignalObj) -> SignalObj:
"""Process signal."""
pass
```
### Docstrings
Use **Google-style docstrings** with type hints:
```python
def compute_feature(obj: SignalObj, param: MyParam) -> SignalObj:
"""Compute feature on signal.
Args:
obj: Input signal object
param: Processing parameters
Returns:
Processed signal object
"""
```
For continued lines in enumerations (args, returns), indent subsequent lines by 1 space:
```python
def compute_feature(obj: SignalObj, param: MyParam) -> SignalObj:
"""Compute feature on signal.
Args:
obj: Input signal object
param: Processing parameters, with a very long description that
continues on the next line.
Returns:
Processed signal object
"""
```
### Imports
**Order**: Standard library ? Third-party ? Local
```python
from __future__ import annotations
import os
from typing import TYPE_CHECKING
import numpy as np
from guidata.qthelpers import create_action
from datalab.config import _
from datalab.gui.processor.base import BaseProcessor
if TYPE_CHECKING:
from sigima.objects import SignalObj
```
### Internationalization
**Always wrap UI strings** with `_()`:
```python
from datalab.config import _
# ? CORRECT
menu_title = _("Processing")
action_text = _("Replace X by other signal's Y")
# ? WRONG
menu_title = "Processing" # Not translatable!
```
## Common Tasks
### Adding a New Signal Processing Feature
**Complete workflow**:
1. **Implement computation in Sigima** (`sigima/proc/signal/processing.py`):
```python
def my_feature(src: SignalObj, param: MyParam | None = None) -> SignalObj:
"""My feature."""
dst = src.copy()
# ... computation ...
return dst
```
2. **Export from Sigima** (`sigima/proc/signal/__init__.py`):
```python
from sigima.proc.signal.processing import my_feature # Import
__all__ = [..., "my_feature"] # Export
```
3. **Register in DataLab processor** (`datalab/gui/processor/signal.py`):
```python
def register_processing(self) -> None:
self.register_1_to_1(
sips.my_feature,
_("My Feature"),
paramclass=MyParam,
icon_name="my_icon.svg",
)
```
4. **Add menu action** (`datalab/gui/actionhandler.py`):
```python
def setup_processing_actions(self) -> None:
act = self.action_for("my_feature")
self.processing_menu.addAction(act)
```
5. **Add tests** (`datalab/tests/features/signal/`):
```python
def test_my_feature():
obj = SignalObj.create(...)
result = sips.my_feature(obj)
assert result is not None
```
6. **Document** (`doc/features/signal/menu_processing.rst`):
````rst
My Feature
^^^^^^^^^^
Create a new signal by applying my feature:
.. image:: /images/my_feature.png
Parameters:
- **Parameter 1**: Description
````
7. **Translate**:
```powershell
# UI translation
python scripts/run_with_env.py python -m guidata.utils.translations scan ...
# Doc translation
python scripts/run_with_env.py python -m sphinx_intl update -d doc/locale -p build/gettext -l fr
```
### Working with X-array Compatibility
**Rule of thumb**:
- **Most 2-to-1 operations**: Default behavior (auto-interpolation) is correct
- **X coordinate manipulation**: Set `skip_xarray_compat=True`
**Examples**:
- ? `difference` (subtract two signals): Compatible X arrays expected ? `skip_xarray_compat=False`
- ? `xy_mode` (swap X and Y): Uses Y as new X ? `skip_xarray_compat=True`
- ? `replace_x_by_other_y`: Takes Y from second signal as X ? `skip_xarray_compat=True`
### Debugging Tips
1. **Check processor registration**: Look in `register_operations()` methods
2. **Verify action handler**: Search `actionhandler.py` for `action_for("function_name")`
3. **Test in isolation**: Use pytest with `--ff` flag (fail-fast)
4. **Check translations**: Missing `_()` wrapper causes English-only UI
5. **Verify imports**: Ensure function is in `__all__` export list
## Release Classification
**Bug Fix** (patch release: 1.0.x):
- Fixes incorrect behavior
- Restores expected functionality
- Addresses user-reported issues
- **Example**: Adding `replace_x_by_other_y` to handle wavelength calibration (was previously impossible)
**Feature** (minor release: 1.x.0):
- Adds entirely new capability
- Extends functionality beyond original scope
- Introduces new UI elements or workflows
## VS Code Tasks
The workspace includes predefined tasks (`.vscode/tasks.json`). Access via:
- `Ctrl+Shift+B` ? "???? Ruff" (format + lint)
- Terminal ? "Run Task..." ? "?? Pytest", "?? Compile translations", etc.
**Key Tasks**:
- `???? Ruff`: Format and lint code
- `?? Pytest`: Run tests with `--ff`
- `?? Compile translations`: Build .mo files
- `?? Scan translations`: Update .po files
- `?? Build/open HTML doc`: Generate and open Sphinx docs
## Multi-Root Workspace
DataLab development uses a **multi-root workspace** (`.code-workspace` file) with sibling projects:
- `DataLab/` - Main GUI application
- `Sigima/` - Computation library
- `PlotPy/` - Plotting library
- `guidata/` - GUI toolkit
- `PythonQwt/` - Qwt bindings
**When working across projects**:
1. Changes in `Sigima` require importing in `DataLab`
2. Use `.env` file to point to local development versions
3. Test changes in both Sigima unit tests AND DataLab integration tests
## Release Notes Guidelines
**Location**: `doc/release_notes/release_MAJOR.MINOR.md` where MINOR is zero-padded to 2 digits (e.g., `release_1.00.md` for v1.0.x, `release_1.01.md` for v1.1.x)
**Writing Style**: Focus on **user impact**, not implementation details.
**Good release note** (user-focused):
- ? "Fixed syntax errors when using f-strings with nested quotes in macros"
- ? "Fixed corrupted Unicode characters in macro console output on Windows"
- ? "Fixed 'Lock LUT range' setting not persisting after closing Settings dialog"
**Bad release note** (implementation-focused):
- ? "Removed `code.replace('"', "'")` that broke f-strings"
- ? "Changed QTextCodec.codecForLocale() to codecForName(b'UTF-8')"
- ? "Added missing `ima_def_keep_lut_range` option in configuration"
**Structure**:
- **What went wrong**: Describe the symptom users experienced
- **When it occurred**: Specify the context/scenario
- **What's fixed**: Explain the benefit, not the implementation
**Example**:
```markdown
**Macro execution:**
* Fixed syntax errors when using f-strings with nested quotes in macros (e.g., `f'text {func("arg")}'` now works correctly)
* Fixed corrupted Unicode characters in macro console output on Windows - special characters like ?, ??, and ? now display correctly instead of showing garbled text
```
## Key Files Reference
| File | Purpose |
|------|---------|
| `datalab/gui/processor/signal.py` | Signal processor registration |
| `datalab/gui/processor/image.py` | Image processor registration |
| `datalab/gui/processor/base.py` | Base processor class (generic methods) |
| `datalab/gui/actionhandler.py` | Menu and action management |
| `datalab/config.py` | Configuration, `_()` translation function |
| `datalab/plugins.py` | Plugin system implementation |
| `datalab/control/proxy.py` | Remote control API (RemoteProxy, LocalProxy) |
| `sigima/proc/signal/processing.py` | Signal computation functions |
| `sigima/proc/image/processing.py` | Image computation functions |
| `scripts/run_with_env.py` | Environment loader (loads `.env`) |
| `.env` | Local PYTHONPATH for development |
| `doc/release_notes/release_MAJOR.MINOR.md` | Release notes (MINOR is zero-padded: release_1.00.md for v1.0.x, release_1.01.md for v1.1.x) |
## Getting Help
- **Documentation**: https://datalab-platform.com/
- **Issues**: https://github.com/DataLab-Platform/DataLab/issues
- **Contributing**: https://datalab-platform.com/en/contributing/index.html
- **Support**: p.raybaut@codra.fr
---
**Remember**: Always use `scripts/run_with_env.py` for Python commands, wrap UI strings with `_()`, and consider X-array compatibility when adding 2-to-1 signal operations.
DataLab-1.1.0/.github/workflows/ 0000775 0000000 0000000 00000000000 15140141176 0016376 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/.github/workflows/test_pyqt5.yml 0000664 0000000 0000000 00000016423 15140141176 0021250 0 ustar 00root root 0000000 0000000 # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
# Inspired from https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions
name: Install and Test on Ubuntu (latest) with PyQt5
on:
push:
branches: [ "main", "develop", "release" ]
pull_request:
branches: [ "main", "develop", "release" ]
workflow_dispatch:
inputs:
job_to_run:
description: 'Which job to run'
required: true
type: choice
options:
- 'all'
- 'build'
- 'build_latest'
default: 'all'
schedule:
# Only the "build_latest" job runs on schedule (see execution conditions below)
- cron: "0 5 * * 1"
jobs:
build:
if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'build')) }}
env:
DISPLAY: ':99.0'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX
python -m pip install --upgrade pip
python -m pip install ruff pytest httpx
pip install PyQt5
if [ "${{ github.ref_name }}" = "develop" ]; then
# Clone and install development versions of key dependencies with editable install
cd ..
git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/guidata.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/plotpy.git
git clone --depth 1 --branch develop https://github.com/DataLab-Platform/sigima.git
cd DataLab
pip install -e ../guidata
pip install -e ../PythonQwt
pip install -e ../plotpy
pip install -e ../sigima
# Install tomli for TOML parsing (safe if already present)
pip install tomli
# Extract dependencies and save to file, then install
python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata','PlotPy','Sigima'])]; open('deps.txt','w').write('\n'.join(deps))"
pip install -r deps.txt
# Install DataLab without dependencies
pip install --no-deps .
elif [ "${{ github.ref_name }}" = "release" ]; then
# Clone dependencies from release branches (with fallback to main/master)
cd ..
# Try cloning PythonQwt from main or master
git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/PythonQwt.git
# Try cloning guidata from release, fallback to main or master
git clone --depth 1 --branch release https://github.com/PlotPyStack/guidata.git || git clone --depth 1 https://github.com/PlotPyStack/guidata.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/guidata.git
# Try cloning plotpy from release, fallback to main or master
git clone --depth 1 --branch release https://github.com/PlotPyStack/plotpy.git || git clone --depth 1 https://github.com/PlotPyStack/plotpy.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/plotpy.git
# Try cloning sigima from release, fallback to main
git clone --depth 1 --branch release https://github.com/DataLab-Platform/sigima.git || git clone --depth 1 https://github.com/DataLab-Platform/sigima.git
cd DataLab
pip install -e ../guidata
pip install -e ../PythonQwt
pip install -e ../plotpy
pip install -e ../sigima
# Install tomli for TOML parsing (safe if already present)
pip install tomli
# Extract dependencies and save to file, then install
python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata','PlotPy','Sigima'])]; open('deps.txt','w').write('\n'.join(deps))"
pip install -r deps.txt
# Install DataLab without dependencies
pip install --no-deps .
else
# Install from PyPI normally for main branch
pip install .
fi
- name: Lint with Ruff
run: ruff check --output-format=github datalab
- name: Test with pytest
run: pytest -v --tb=long
build_latest:
if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'build_latest')) }}
env:
DISPLAY: ':99.0'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install dependencies (latest)
run: |
set -euxo pipefail
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils
/sbin/start-stop-daemon --start --quiet \
--pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background \
--exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX
python -m pip install --upgrade pip
python -m pip install ruff pytest httpx
python -m pip install PyQt5
# Clone and install Sigima from the same branch as DataLab
cd ..
git clone --depth 1 --branch ${{ github.ref_name }} https://github.com/DataLab-Platform/sigima.git || git clone --depth 1 https://github.com/DataLab-Platform/sigima.git
cd DataLab
# Install DataLab itself, but do NOT install its pinned deps
python -m pip install -e . --no-deps
# Install Sigima from local clone
python -m pip install -e ../sigima
# Extract dependency names from pyproject.toml (excluding Sigima) and install latest versions
python -m pip install -U --upgrade-strategy eager $(python -c "import tomllib, re; print(' '.join(re.sub(r'[\[\]<>=!~,.\s].*$', '', d).strip() for d in tomllib.loads(open('pyproject.toml', 'rb').read().decode())['project']['dependencies'] if 'Sigima' not in d))")
- name: Lint with Ruff (latest)
run: ruff check --output-format=github datalab
- name: Test with pytest (latest)
run: pytest -v --tb=long DataLab-1.1.0/.github/workflows/test_pyqt6.yml 0000664 0000000 0000000 00000016443 15140141176 0021253 0 ustar 00root root 0000000 0000000 # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
# Inspired from https://pytest-qt.readthedocs.io/en/latest/troubleshooting.html#github-actions
name: Install and Test on Ubuntu (latest) with PyQt6
on:
push:
branches: [ "main", "develop", "release" ]
pull_request:
branches: [ "main", "develop", "release" ]
workflow_dispatch:
inputs:
job_to_run:
description: 'Which job to run'
required: true
type: choice
options:
- 'all'
- 'build'
- 'build_latest'
default: 'all'
schedule:
# Only the "build_latest" job runs on schedule (see execution conditions below)
- cron: "0 5 * * 1"
jobs:
build:
if: ${{ (github.event_name == 'push' || github.event_name == 'pull_request') || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'build')) }}
env:
DISPLAY: ':99.0'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1
/sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX
python -m pip install --upgrade pip
python -m pip install ruff pytest httpx
pip install PyQt6
if [ "${{ github.ref_name }}" = "develop" ]; then
# Clone and install development versions of key dependencies with editable install
cd ..
git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/guidata.git
git clone --depth 1 --branch develop https://github.com/PlotPyStack/plotpy.git
git clone --depth 1 --branch develop https://github.com/DataLab-Platform/sigima.git
cd DataLab
pip install -e ../guidata
pip install -e ../PythonQwt
pip install -e ../plotpy
pip install -e ../sigima
# Install tomli for TOML parsing (safe if already present)
pip install tomli
# Extract dependencies and save to file, then install
python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata','PlotPy','Sigima'])]; open('deps.txt','w').write('\n'.join(deps))"
pip install -r deps.txt
# Install DataLab without dependencies
pip install --no-deps .
elif [ "${{ github.ref_name }}" = "release" ]; then
# Clone dependencies from release branches (with fallback to main/master)
cd ..
# Try cloning PythonQwt from main or master
git clone --depth 1 https://github.com/PlotPyStack/PythonQwt.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/PythonQwt.git
# Try cloning guidata from release, fallback to main or master
git clone --depth 1 --branch release https://github.com/PlotPyStack/guidata.git || git clone --depth 1 https://github.com/PlotPyStack/guidata.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/guidata.git
# Try cloning plotpy from release, fallback to main or master
git clone --depth 1 --branch release https://github.com/PlotPyStack/plotpy.git || git clone --depth 1 https://github.com/PlotPyStack/plotpy.git || git clone --depth 1 --branch master https://github.com/PlotPyStack/plotpy.git
# Try cloning sigima from release, fallback to main
git clone --depth 1 --branch release https://github.com/DataLab-Platform/sigima.git || git clone --depth 1 https://github.com/DataLab-Platform/sigima.git
cd DataLab
pip install -e ../guidata
pip install -e ../PythonQwt
pip install -e ../plotpy
pip install -e ../sigima
# Install tomli for TOML parsing (safe if already present)
pip install tomli
# Extract dependencies and save to file, then install
python -c "import tomli; f=open('pyproject.toml','rb'); data=tomli.load(f); deps=[d for d in data['project']['dependencies'] if not any(p in d for p in ['guidata','PlotPy','Sigima'])]; open('deps.txt','w').write('\n'.join(deps))"
pip install -r deps.txt
# Install DataLab without dependencies
pip install --no-deps .
else
# Install from PyPI normally for main branch
pip install .
fi
- name: Lint with Ruff
run: ruff check --output-format=github datalab
- name: Test with pytest
run: pytest -v --tb=long
build_latest:
if: ${{ github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && (github.event.inputs.job_to_run == 'all' || github.event.inputs.job_to_run == 'build_latest')) }}
env:
DISPLAY: ':99.0'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python 3.13
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install dependencies (latest)
run: |
set -euxo pipefail
sudo apt-get update
sudo apt-get install -y \
libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 \
libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1
/sbin/start-stop-daemon --start --quiet \
--pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background \
--exec /usr/bin/Xvfb -- :99 -screen 0 1920x1200x24 -ac +extension GLX
python -m pip install --upgrade pip
python -m pip install ruff pytest httpx
python -m pip install PyQt6
# Clone and install Sigima from the same branch as DataLab
cd ..
git clone --depth 1 --branch ${{ github.ref_name }} https://github.com/DataLab-Platform/sigima.git || git clone --depth 1 https://github.com/DataLab-Platform/sigima.git
cd DataLab
# Install DataLab itself, but do NOT install its pinned deps
python -m pip install -e . --no-deps
# Install Sigima from local clone
python -m pip install -e ../sigima
# Extract dependency names from pyproject.toml (excluding Sigima) and install latest versions
python -m pip install -U --upgrade-strategy eager $(python -c "import tomllib, re; print(' '.join(re.sub(r'[\[\]<>=!~,.\s].*$', '', d).strip() for d in tomllib.loads(open('pyproject.toml', 'rb').read().decode())['project']['dependencies'] if 'Sigima' not in d))")
- name: Lint with Ruff (latest)
run: ruff check --output-format=github datalab
- name: Test with pytest (latest)
run: pytest -v --tb=long DataLab-1.1.0/.gitignore 0000664 0000000 0000000 00000013136 15140141176 0014775 0 ustar 00root root 0000000 0000000 # ---------------------------- Specific to this project --------------------------------
# DataLab project specific
resources/*.png
doc.zip
releases/
/tutorialnotes*.md
doc/changelog.md
scenario_*.h5
# Windows specific
Thumbs.db
# Microsoft Visual Studio
*.pyproj
*.sln
# Sphinx documentation
doctmp/
.doctrees/
doc/install_requires.txt
doc/extras_require-dev.txt
doc/extras_require-doc.txt
doc/locale/pot/_sphinx_design_static/
doc/_download/
DataLab_*.pdf
# Backup files (e.g. created during merge conflicts)
*.bak
# WiX files
.wix/
wix/DataLab-*.wxs
wix/bin/
wix/obj/
wix/*.bmp
*.wixpdb
*.msi
# ------------------ Template `Python.gitignore` from gitignore.io ---------------------
# Created by https://www.gitignore.io/api/python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
# *.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
#poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
#pdm.lock
#pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
#pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
# Workspace files are ignored because they may contain user-specific settings and
# committing them could interfere with branch switching for example. A workspace
# template file is provided in the repository.
*.code-workspace
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Cursor
# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
# refer to https://docs.cursor.com/context/ignore-files
.cursorignore
.cursorindexingignore
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
DataLab-1.1.0/.pre-commit-config.yaml 0000664 0000000 0000000 00000000325 15140141176 0017262 0 ustar 00root root 0000000 0000000 repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.12.2
hooks:
# Run the linter.
- id: ruff-check
args: [ --fix ]
# Run the formatter.
- id: ruff-format DataLab-1.1.0/.pylintrc 0000664 0000000 0000000 00000001060 15140141176 0014643 0 ustar 00root root 0000000 0000000 [FORMAT]
# Essential to be able to compare code side-by-side (`black` default setting)
# and best compromise to minimize file size
max-line-length=88
[TYPECHECK]
ignored-modules=qtpy.QtWidgets,qtpy.QtCore,qtpy.QtGui,cv2,plotpy._scaler,skimage.restoration,skimage.feature
[MESSAGES CONTROL]
disable=wrong-import-order
[DESIGN]
max-args=10 # default: 5
max-positional-arguments=11 # default: 5
max-attributes=12 # default: 7
max-branches=20 # default: 12
max-locals=20 # default: 15
min-public-methods=0 # default: 2
max-public-methods=25 # default: 20 DataLab-1.1.0/.readthedocs.yaml 0000664 0000000 0000000 00000000530 15140141176 0016226 0 ustar 00root root 0000000 0000000 # Read the Docs configuration file for Sphinx projects
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
version: 2
build:
os: ubuntu-22.04
tools:
python: "3.11"
sphinx:
configuration: doc/conf.py
formats:
- pdf
python:
install:
- method: pip
path: .
extra_requirements:
- doc
DataLab-1.1.0/.vscode/ 0000775 0000000 0000000 00000000000 15140141176 0014342 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/.vscode/launch.json 0000664 0000000 0000000 00000007574 15140141176 0016524 0 ustar 00root root 0000000 0000000 {
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Run DataLab",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/datalab/app.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"justMyCode": false,
"args": [
// "--reset"
// "--h5browser",
// "${workspaceFolder}/datalab/data/tests/empty.h5"
// "${workspaceFolder}/datalab/data/tests/reordering_test.h5",
// "${workspaceFolder}/datalab/data/tests/reordering_test.h5,/DataLab_Sig/g001: /s003: wiener(s002)/xydata",
],
"env": {
// "DEBUG": "1",
// "LANG": "en",
// "QT_COLOR_MODE": "light",
}
},
{
"name": "Run current file",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"justMyCode": false,
"args": [
// "--h5browser",
// "${workspaceFolder}/datalab/data/tests/format_v1.7.h5",
// "${workspaceFolder}/datalab/data/tests/format_v1.7.h5,/DataLab_Ima/i002: i002+i004",
// "--unattended",
// "--screenshot",
// "--verbose",
// "quiet",
// "--delay",
// "1000"
],
"env": {
// "DEBUG": "1", // ☣️ Debug mode will reset .ini settings
// "TEST_SEGFAULT_ERROR": "1",
"LANG": "en",
"QT_COLOR_MODE": "light",
}
},
{
"name": "Run current file (unattended)",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"pythonArgs": [
"-W error::DeprecationWarning",
"-W error::RuntimeWarning",
],
"justMyCode": false,
"args": [
"--unattended",
],
"env": {
// "DEBUG": "1", // ☣️ Debug mode will reset .ini settings
// "QT_QPA_PLATFORM": "offscreen",
// The `DATALAB_DATA` environment variable is set here just for checking
// that the data path is not added twice to the test data path list:
"DATALAB_DATA": "${workspaceFolder}/datalab/data/tests",
"LANG": "en",
"QT_COLOR_MODE": "light",
}
},
{
"name": "Profile current file",
"type": "debugpy",
"request": "launch",
"module": "cProfile",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"args": [
"-o",
"${file}.prof",
"${file}"
],
},
{
"name": "Run H5browser",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/datalab/tests/features/hdf5/h5browser1_unit.py",
"console": "integratedTerminal",
"envFile": "${workspaceFolder}/.env",
"justMyCode": true,
"env": {
"DEBUG": "1",
// "QT_COLOR_MODE": "light",
},
"args": [
// "--unattended",
// "--screenshot",
],
},
]
} DataLab-1.1.0/.vscode/settings.json 0000664 0000000 0000000 00000001514 15140141176 0017076 0 ustar 00root root 0000000 0000000 {
"[bat]": {
"files.encoding": "cp850"
},
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff"
},
"[restructuredtext]": {
"editor.wordWrap": "on"
},
"editor.codeActionsOnSave": {
"source.organizeImports.ruff": "explicit"
},
"editor.formatOnSave": true,
"editor.rulers": [
88
],
"files.exclude": {
"**/__pycache__": true,
"**/*.pyc": true,
"**/*.pyo": true
},
"files.trimFinalNewlines": true,
"files.trimTrailingWhitespace": true,
"python.analysis.autoFormatStrings": true,
"python.testing.pytestArgs": [],
"python.testing.pytestEnabled": true,
"python.testing.pytestPath": "pytest",
"python.testing.unittestEnabled": false,
"terminal.integrated.tabs.description": "${workspaceFolder}",
} DataLab-1.1.0/.vscode/tasks.json 0000664 0000000 0000000 00000073717 15140141176 0016401 0 ustar 00root root 0000000 0000000 {
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "🧽 Ruff Formatter",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"ruff",
"format",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
{
"label": "🔦 Ruff Linter",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"ruff",
"check",
"--fix",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
{
"label": "🧽🔦 Ruff",
"dependsOrder": "sequence",
"dependsOn": [
"🧽 Ruff Formatter",
"🔦 Ruff Linter",
],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
{
"label": "🔦 Pylint",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"pylint",
"datalab",
"--disable=duplicate-code",
"--disable=fixme",
"--disable=too-many-arguments",
"--disable=too-many-branches",
"--disable=too-many-instance-attributes",
"--disable=too-many-lines",
"--disable=too-many-locals",
"--disable=too-many-public-methods",
"--disable=too-many-statements",
],
"options": {
"cwd": "${workspaceFolder}",
},
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
{
"label": "🚀 Pytest",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"pytest",
"--ff",
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
// "DEBUG": "1", // ☣️ Debug mode will reset .ini settings
"UNATTENDED": "1",
},
},
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true,
},
"type": "shell",
},
{
"label": "sphinx-build",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"sphinx",
"build",
"doc",
"build/gettext",
"-b",
"gettext",
"-W",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
{
"label": "sphinx-intl update",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"sphinx_intl",
"update",
"-d",
"doc/locale",
"-p",
"build/gettext",
"-l",
"fr",
"--no-obsolete",
"-w",
"0",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"sphinx-build",
],
},
{
"label": "cleanup-doc-translations",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"guidata.utils.translations",
"cleanup-doc",
"--directory",
".",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"sphinx-intl update",
],
},
{
"label": "sphinx-intl build",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"sphinx_intl",
"build",
"-d",
"doc/locale",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
{
"label": "🔎 Scan translations",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"guidata.utils.translations",
"scan",
"--name",
"datalab",
"--directory",
".",
"--copyright-holder",
"DataLab Platform Developers",
"--languages",
"fr",
],
"group": {
"kind": "build",
"isDefault": false,
},
"options": {
"cwd": "${workspaceFolder}",
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"cleanup-doc-translations",
],
},
{
"label": "📚 Compile translations",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"guidata.utils.translations",
"compile",
"--name",
"datalab",
"--directory",
".",
],
"group": {
"kind": "build",
"isDefault": false,
},
"options": {
"cwd": "${workspaceFolder}",
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
"dependsOrder": "sequence",
"dependsOn": [
"sphinx-intl build",
],
},
{
"label": "🛠️ Generate doc assets",
"command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m guidata.utils.genreqs all && ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} doc/update_validation_status.py",
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
{
"label": "🧪 Coverage tests",
"type": "shell",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"coverage",
"run",
"-m",
"pytest",
"datalab",
],
"options": {
"cwd": "${workspaceFolder}",
"env": {
"COVERAGE_PROCESS_START": "${workspaceFolder}/.coveragerc",
},
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "test",
"isDefault": true,
},
"presentation": {
"panel": "dedicated",
},
"problemMatcher": [],
},
{
"label": "📊 Coverage full",
"type": "shell",
"windows": {
"command": "${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} -m coverage html && start htmlcov\\index.html",
},
"linux": {
"command": "${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} -m coverage html && xdg-open htmlcov/index.html",
},
"osx": {
"command": "${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} -m coverage html && open htmlcov/index.html",
},
"options": {
"cwd": "${workspaceFolder}",
"env": {
"COVERAGE_PROCESS_START": "${workspaceFolder}/.coveragerc",
},
},
"presentation": {
"panel": "dedicated",
},
"problemMatcher": [],
"dependsOrder": "sequence",
"dependsOn": [
"🧪 Coverage tests",
],
},
{
"label": "Upgrade Sigima/PlotPyStack",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"pip",
"install",
"--upgrade",
"pip",
"PythonQwt",
"guidata",
"PlotPy",
"Sigima",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false,
},
"type": "shell",
},
{
"label": "🔁 Reinstall guidata/plotpy/sigima dev",
"type": "shell",
"windows": {
"command": ".venv/scripts/pip uninstall -y guidata plotpy sigima; Remove-Item -Recurse -Force .venv/Lib/site-packages/guidata -ErrorAction SilentlyContinue; Remove-Item -Recurse -Force .venv/Lib/site-packages/plotpy -ErrorAction SilentlyContinue; Remove-Item -Recurse -Force .venv/Lib/site-packages/sigima -ErrorAction SilentlyContinue; .venv/scripts/pip install -e ../guidata; .venv/scripts/pip install -e ../plotpy; .venv/scripts/pip install -e ../sigima",
},
"linux": {
"command": ".venv/bin/pip uninstall -y guidata plotpy sigima && rm -rf .venv/lib/python*/site-packages/guidata && rm -rf .venv/lib/python*/site-packages/plotpy && rm -rf .venv/lib/python*/site-packages/sigima && .venv/bin/pip install -e ../guidata && .venv/bin/pip install -e ../plotpy && .venv/bin/pip install -e ../sigima",
},
"osx": {
"command": ".venv/bin/pip uninstall -y guidata plotpy sigima && rm -rf .venv/lib/python*/site-packages/guidata && rm -rf .venv/lib/python*/site-packages/plotpy && rm -rf .venv/lib/python*/site-packages/sigima && .venv/bin/pip install -e ../guidata && .venv/bin/pip install -e ../plotpy && .venv/bin/pip install -e ../sigima",
},
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"presentation": {
"panel": "dedicated",
"reveal": "always",
},
"problemMatcher": [],
},
{
"label": "🧹 Clean Up",
"type": "shell",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"guidata.utils.cleanup",
],
"options": {
"cwd": "${workspaceFolder}"
},
"group": {
"kind": "build",
"isDefault": true
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false,
},
},
{
"label": "Create executable",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
// "RELEASE": "1",
"UNATTENDED": "1",
},
"statusbar": {
"hide": true,
},
},
"args": [
"/c",
"build_exe.bat",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true,
},
},
{
"label": "Create installer",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
"UNATTENDED": "1",
},
"statusbar": {
"hide": true,
},
},
"args": [
"/c",
"build_installer.bat",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true,
},
},
{
"label": "Build PDF doc",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
"QT_COLOR_MODE": "light",
// "RELEASE": "1",
"UNATTENDED": "1",
},
"statusbar": {
"hide": true,
},
},
"args": [
"/c",
"build_doc.bat",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true,
},
"dependsOrder": "sequence",
"dependsOn": [
"🛠️ Generate doc assets",
],
},
{
"label": "HTML doc: quick preview",
"type": "shell",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"sphinx",
"build",
"doc",
"${workspaceFolder}/build/doc",
"-b",
"html",
"-W",
],
"options": {
"cwd": "${workspaceFolder}",
"statusbar": {
"hide": true,
},
},
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true,
},
},
{
"label": "🌐 Build/open HTML doc",
"type": "shell",
"windows": {
"command": "start build/doc/index.html",
},
"linux": {
"command": "xdg-open build/doc/index.html",
},
"osx": {
"command": "open build/doc/index.html",
},
"options": {
"cwd": "${workspaceFolder}",
},
"problemMatcher": [],
"dependsOrder": "sequence",
"dependsOn": [
"HTML doc: quick preview",
],
},
{
"label": "GitHub Pages: build",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
"QT_COLOR_MODE": "light",
"RELEASE": "1",
"UNATTENDED": "1",
},
"statusbar": {
"hide": true,
},
},
"args": [
"/c",
"build_ghpages.bat",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true,
},
"dependsOrder": "sequence",
"dependsOn": [
"🛠️ Generate doc assets",
],
},
{
"label": "GitHub Pages: preview",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
"QT_COLOR_MODE": "light",
"RELEASE": "1",
"UNATTENDED": "1",
},
"statusbar": {
"hide": true,
},
},
"args": [
"/c",
"preview_ghpages.bat",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true,
},
"dependsOrder": "sequence",
"dependsOn": [
"GitHub Pages: build",
],
},
{
"label": "GitHub Pages: upload",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
"QT_COLOR_MODE": "light",
"UNATTENDED": "1",
},
"statusbar": {
"hide": true,
},
},
"args": [
"/c",
"upload_ghpages.bat",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": true,
},
"dependsOrder": "sequence",
"dependsOn": [
"GitHub Pages: build",
],
},
{
"label": "Build Python packages",
"type": "shell",
"command": "cmd",
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
// "RELEASE": "1",
"UNATTENDED": "1",
},
"statusbar": {
"hide": true,
},
},
"args": [
"/c",
"build_dist.bat",
],
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true,
},
"dependsOrder": "sequence",
"dependsOn": [
"🛠️ Generate doc assets",
"Build PDF doc",
],
},
{
"label": "✨ Release",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"release.bat",
],
"options": {
"cwd": "scripts",
"env": {
"PYTHON": "${command:python.interpreterPath}",
// "RELEASE": "1",
"UNATTENDED": "1",
},
},
"problemMatcher": [],
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true,
},
"dependsOrder": "sequence",
"dependsOn": [
"🧹 Clean Up",
"Upgrade Sigima/PlotPyStack",
"📚 Compile translations",
"Build Python packages",
"Create executable",
"Create installer",
],
},
{
"label": "Run _tests_.bat",
"type": "shell",
"command": "cmd",
"args": [
"/c",
"_tests_.bat",
],
"options": {
"cwd": "scripts",
"env": {
"UNATTENDED": "1",
"RELEASE": "0",
"PYTHON": "${command:python.interpreterPath}",
},
"statusbar": {
"hide": true,
},
},
"group": {
"kind": "build",
"isDefault": false,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true,
},
},
{
"label": "❔ Untracked files",
"type": "shell",
"command": "git ls-files --others | Where-Object { $_ -notmatch '^\\.' -and $_ -notmatch '^(build|dist|releases)/' -and $_ -notmatch '.(pyc|mo)$'}",
"options": {
"cwd": "${workspaceFolder}",
},
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "dedicated",
"showReuseMessage": true,
"clear": true,
},
},
{
"label": "🖼️ Icon Browser",
"command": "${command:python.interpreterPath}",
"args": [
"scripts/run_with_env.py",
"${command:python.interpreterPath}",
"-m",
"guidata.widgets.iconbrowser",
"${workspaceFolder}/datalab/data/icons",
],
"options": {
"cwd": "${workspaceFolder}",
},
"group": {
"kind": "build",
"isDefault": true,
},
"presentation": {
"clear": true,
"echo": true,
"focus": false,
"panel": "dedicated",
"reveal": "always",
"showReuseMessage": true,
},
"type": "shell",
},
],
} DataLab-1.1.0/CONTRIBUTING.md 0000664 0000000 0000000 00000001012 15140141176 0015224 0 ustar 00root root 0000000 0000000 # Contributing Guidelines
DataLab is **your** platform. If you want it to be improved, you **can** contribute to
the project, whether you are a developer or not.
There are many ways to contribute to DataLab project, depending on how much time you
have, your experience with open source projects, and your skills.
To get started, please refer to our [contributing documentation](https://datalab-platform.com/en/contributing/index.html) for detailed instructions on how to contribute to the project.
Happy contributing!
DataLab-1.1.0/DataLab.bat 0000664 0000000 0000000 00000002213 15140141176 0014757 0 ustar 00root root 0000000 0000000
@echo off
setlocal EnableDelayedExpansion
REM Store script directory in a variable (outside IF block for batch compatibility)
set SCRIPT_DIR=%~dp0
REM Check if a Python executable path is provided as an argument
if "%1" == "" (
REM If not, try to use .venv\Scripts\pythonw.exe in the current directory
set PYTHON_EXE=!SCRIPT_DIR!.venv\Scripts\pythonw.exe
if not exist "!PYTHON_EXE!" (
echo Error: Python executable path must be provided as an argument.
echo Usage: DataLab.bat path\to\python.exe
exit /b 1
)
) else (
REM Use the provided Python executable path
set PYTHON_EXE=%1
)
REM Validate that the provided Python executable exists
if not exist "%PYTHON_EXE%" (
echo Error: The specified Python executable does not exist: %PYTHON_EXE%
exit /b 2
)
cd/D %~dp0
set ORIGINAL_PYTHONPATH=%PYTHONPATH%
for /F "tokens=*" %%A in (.env) do (set %%A)
set PYTHONPATH=%PYTHONPATH%;%ORIGINAL_PYTHONPATH%
REM Extract pythonw.exe from the same directory as the provided python.exe
for %%a in ("%PYTHON_EXE%") do set "PYTHON_DIR=%%~dpa"
start "" "%PYTHON_DIR%pythonw.exe" datalab\start.pyw %2 %3 %4 %5 %6 %7 %8 %9 DataLab-1.1.0/DataLab.code-workspace.template 0000664 0000000 0000000 00000000461 15140141176 0020734 0 ustar 00root root 0000000 0000000 {
"folders": [
{
"path": "."
},
{
"path": "../Sigima"
},
{
"path": "../PlotPy"
},
{
"path": "../guidata"
}
],
"settings": {
"python.analysis.autoFormatStrings": true
}
} DataLab-1.1.0/DataLab.desktop 0000664 0000000 0000000 00000000347 15140141176 0015670 0 ustar 00root root 0000000 0000000 [Desktop Entry]
Version=1.0
Type=Application
Name=datalab
GenericName=DataLab
Comment=Run DataLab, the Python Signal and image processing software
TryExec=datalab
Exec=datalab
Icon=DataLab.svg
Categories=Education;Science;Physics;
DataLab-1.1.0/DataLab.spec 0000664 0000000 0000000 00000002350 15140141176 0015145 0 ustar 00root root 0000000 0000000 # -*- mode: python ; coding: utf-8 -*-
# Initial command:
# pyinstaller -y --clean -n DataLab -i resources\DataLab.ico datalab\start.pyw
from PyInstaller.utils.hooks import collect_submodules, collect_data_files, copy_metadata
all_hidden_imports = collect_submodules('datalab')
datas = collect_data_files('datalab') + [('datalab\\plugins', 'datalab\\plugins')]
datas += collect_data_files('guidata') + collect_data_files('plotpy')
datas += collect_data_files('sigima')
datas += copy_metadata('imageio')
a = Analysis(
['datalab\\start.pyw'],
pathex=[],
binaries=[],
datas=datas,
hiddenimports=all_hidden_imports,
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=[],
noarchive=False,
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
[],
exclude_binaries=True,
name='DataLab',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=True,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon=['resources\\DataLab.ico'],
)
coll = COLLECT(
exe,
a.binaries,
a.datas,
strip=False,
upx=True,
upx_exclude=[],
name='DataLab',
)
DataLab-1.1.0/LICENSE 0000664 0000000 0000000 00000003000 15140141176 0013777 0 ustar 00root root 0000000 0000000 BSD 3-Clause License
Copyright (c) 2023, DataLab Platform Developers.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
DataLab-1.1.0/MANIFEST.in 0000664 0000000 0000000 00000000240 15140141176 0014533 0 ustar 00root root 0000000 0000000 graft doc
graft resources
graft macros
graft plugins
graft datalab/locale
include conftest.py
include CONTRIBUTING.md
include requirements.txt
include *.desktop DataLab-1.1.0/README.md 0000664 0000000 0000000 00000007233 15140141176 0014265 0 ustar 00root root 0000000 0000000 
[](./LICENSE)
[](https://pypi.org/project/datalab-platform/)
[](https://github.com/DataLab-Platform/DataLab)
[](https://pypi.python.org/pypi/datalab-platform/)
DataLab is an **open-source platform for scientific and technical data processing
and visualization** with unique features designed to meet industrial requirements.
See [DataLab website](https://datalab-platform.com/) for more details.
> **Note:** This project (DataLab Platform) should not be confused with the [datalab-org](https://datalab-org.io/) project, which is a separate and unrelated initiative focused on materials science databases and computational tools.
ℹ️ Created by [CODRA](https://codra.net/)/[Pierre Raybaut](https://github.com/PierreRaybaut) in 2023, developed and maintained by DataLab Platform Developers.

🧮 DataLab's processing power comes from the advanced algorithms of the object-oriented signal and image processing library [Sigima](https://github.com/DataLab-Platform/Sigima) 🚀 which is part of the DataLab Platform.

ℹ️ DataLab is powered by [PlotPyStack](https://github.com/PlotPyStack) 🚀 for curve plotting and fast image visualization.

ℹ️ DataLab is built on Python and scientific libraries.
      
----
Try DataLab online, without installing anything, using Binder:
[](https://mybinder.org/v2/gh/DataLab-Platform/DataLab/binder-environments?urlpath=git-pull%3Frepo%3Dhttps%253A%252F%252Fgithub.com%252FDataLab-Platform%252FDataLab%26urlpath%3Ddesktop%252F%26branch%3Dbinder-environments)
----
✨ Add features to DataLab by writing your own [plugin](https://datalab-platform.com/en/features/advanced/plugins.html)
(see [plugin examples](https://github.com/DataLab-Platform/DataLab/tree/main/plugins/examples))
or macro (see [macro examples](https://github.com/DataLab-Platform/DataLab/tree/main/macros/examples))
✨ DataLab may be remotely controlled from a third-party application (such as Jupyter,
Spyder or any IDE):
* Using the integrated [remote control](https://datalab-platform.com/en/features/advanced/remote.html)
feature (this requires to install DataLab as a Python package)
* Using the lightweight client integrated in [Sigima](https://github.com/DataLab-Platform/Sigima) (`pip install sigima`)
DataLab-1.1.0/babel.cfg 0000664 0000000 0000000 00000000132 15140141176 0014523 0 ustar 00root root 0000000 0000000 # This file is used to configure Babel for the project.
[python: **.py]
encoding = utf-8
DataLab-1.1.0/datalab/ 0000775 0000000 0000000 00000000000 15140141176 0014371 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/__init__.py 0000664 0000000 0000000 00000002272 15140141176 0016505 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
DataLab
=======
DataLab is a generic signal and image processing software based on Python
scientific libraries (such as NumPy, SciPy or scikit-image) and Qt graphical
user interfaces (thanks to `PlotPyStack`_ libraries).
.. _PlotPyStack: https://github.com/PlotPyStack
"""
import multiprocessing
import os
# Set multiprocessing start method to 'spawn' early to avoid fork-related warnings
# on Linux systems when using Qt and multithreading. This must be done before
# any multiprocessing.Pool is created.
try:
multiprocessing.set_start_method("spawn")
except RuntimeError:
# This exception is raised if the method is already set (this may happen because
# this module is imported more than once, e.g. when running tests)
pass
__version__ = "1.1.0"
__docurl__ = __homeurl__ = "https://datalab-platform.com/"
__supporturl__ = "https://github.com/DataLab-Platform/DataLab/issues/new/choose"
os.environ["DATALAB_VERSION"] = __version__
# Dear (Debian, RPM, ...) package makers, please feel free to customize the
# following path to module's data (images) and translations:
DATAPATH = LOCALEPATH = ""
DataLab-1.1.0/datalab/adapters_metadata/ 0000775 0000000 0000000 00000000000 15140141176 0020034 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/adapters_metadata/__init__.py 0000664 0000000 0000000 00000001420 15140141176 0022142 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Adapters for Sigima's TableResult and GeometryResult, providing features
for storing and retrieving those objects as metadata for DataLab's signal
and image objects.
"""
from .base_adapter import BaseResultAdapter
from .common import (
ResultData,
create_resultdata_dict,
have_geometry_results,
have_results,
resultadapter_to_html,
show_resultdata,
)
from .geometry_adapter import GeometryAdapter
from .table_adapter import TableAdapter
__all__ = [
"BaseResultAdapter",
"GeometryAdapter",
"ResultData",
"TableAdapter",
"create_resultdata_dict",
"have_geometry_results",
"have_results",
"resultadapter_to_html",
"show_resultdata",
]
DataLab-1.1.0/datalab/adapters_metadata/base_adapter.py 0000664 0000000 0000000 00000022723 15140141176 0023026 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Base adapter class for result objects, providing common functionality
for storing and retrieving result objects as metadata for DataLab's signal
and image objects.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, ClassVar, Generator
import guidata.dataset as gds
from sigima.objects import ImageObj, SignalObj
if TYPE_CHECKING:
import pandas as pd
from sigima.objects.scalar import GeometryResult, TableResult
class BaseResultAdapter(ABC):
"""Base adapter for result objects.
This base class provides common functionality for working with result objects,
including metadata storage/retrieval and various data representations.
Args:
result: Result object to adapt
"""
# Class constants for metadata storage - to be overridden by subclasses
META_PREFIX: ClassVar[str] = ""
META_SUFFIX: ClassVar[str] = "_dict"
def __init__(self, result: TableResult | GeometryResult) -> None:
self.result = result
def set_applicative_attr(self, key: str, value: Any) -> None:
"""Set an applicative attribute for the result.
Args:
key: Attribute key
value: Attribute value
"""
self.result.attrs[key] = value
def get_applicative_attr(self, key: str, default: Any = None) -> Any:
"""Get an applicative attribute for the result.
Args:
key: Attribute key
default: Default value to return if key not found
Returns:
Attribute value, or default if not set. If default is not None, assign it
to the attribute if it was not already set.
"""
if key not in self.result.attrs and default is not None:
self.result.attrs[key] = default
return self.result.attrs.get(key, default)
@property
def title(self) -> str:
"""Get the title.
Returns:
Title
"""
return self.result.title
@property
def name(self) -> str:
"""Get the result kind name.
Returns:
The string value of the kind attribute (e.g., "segment", "circle",
"statistics"). This is NOT unique - multiple results can share the
same kind.
"""
return self.result.name
@property
def func_name(self) -> str:
"""Get the computation function name.
Returns:
The name of the computation function that produced this result.
"""
return self.result.func_name
@property
@abstractmethod
def headers(self) -> list[str]:
"""Get column headers.
Returns:
List of column headers
"""
@property
@abstractmethod
def category(self) -> str:
"""Get the category.
Returns:
Category
"""
def get_roi_data(self, roi_index: int) -> "pd.DataFrame":
"""Get data for a specific ROI index.
Args:
roi_index: ROI index to filter by
Returns:
DataFrame containing only data for the specified ROI
"""
df = self.to_dataframe()
if "roi_index" in df.columns:
return df[df["roi_index"] == roi_index].drop(columns=["roi_index"])
return df
def get_column_values(self, column_name: str, roi_index: int = None) -> list:
"""Get values for a specific column, optionally filtered by ROI.
Args:
column_name: Name of the column to retrieve
roi_index: Optional ROI index to filter by
Returns:
List of values for the specified column
"""
df = self.to_dataframe()
if roi_index is not None and "roi_index" in df.columns:
df = df[df["roi_index"] == roi_index]
return df[column_name].tolist()
def get_unique_roi_indices(self) -> list[int]:
"""Get unique ROI indices present in the data.
Returns:
List of unique ROI indices
"""
df = self.to_dataframe()
if "roi_index" in df.columns:
return sorted(df["roi_index"].unique().tolist())
return [] if len(df) == 0 else [0] # Default ROI index for result data
@property
def metadata_key(self) -> str:
"""Get the metadata key used to store this result.
Returns:
Metadata key based on the result's title
"""
return f"{self.META_PREFIX}{self.func_name}{self.META_SUFFIX}"
def add_to(
self, obj: SignalObj | ImageObj, param: gds.DataSet | None = None
) -> None:
"""Add result to object metadata.
Args:
obj: Signal or image object
param: Optional parameter dataset associated with this result
"""
assert self.func_name, "func_name must be set before adding to object metadata"
# Store parameter in result attrs (will be serialized with result)
if param is not None:
self.result.attrs["param_json"] = gds.dataset_to_json(param)
obj.metadata[self.metadata_key] = self.result.to_dict()
def get_param(self) -> gds.DataSet | None:
"""Get parameter dataset associated with this result.
Returns:
Parameter dataset if present, None otherwise
"""
param_json = self.result.attrs.get("param_json")
if param_json is not None:
return gds.json_to_dataset(param_json)
return None
def remove_from(self, obj: SignalObj | ImageObj) -> None:
"""Remove result from object metadata.
Args:
obj: Signal or image object
"""
obj.metadata.pop(self.metadata_key, None)
@classmethod
def remove_all_from(cls, obj: SignalObj | ImageObj) -> None:
"""Remove all results of this type from object metadata.
Args:
obj: Signal or image object
"""
# Find all results in the object and remove them
for adapter in list(cls.iterate_from_obj(obj)):
adapter.remove_from(obj)
@classmethod
def match(cls, key: str, _value: Any) -> bool:
"""Check if the key matches the pattern for this result type.
Args:
key: Metadata key
_value: Metadata value (unused)
Returns:
True if the key matches the pattern
"""
return key.startswith(cls.META_PREFIX) and key.endswith(cls.META_SUFFIX)
@classmethod
@abstractmethod
def from_metadata_entry(cls, obj: SignalObj | ImageObj, key: str):
"""Create a result adapter from a metadata entry.
Args:
obj: Object containing the metadata
key: Metadata key for the result data
Returns:
Adapter object
Raises:
ValueError: Invalid metadata entry
"""
@classmethod
def from_obj(
cls, obj: SignalObj | ImageObj, func_name: str
) -> BaseResultAdapter | None:
"""Create a result adapter from an object's metadata entry.
Args:
obj: Signal or image object
func_name: Function name to look for
Returns:
Adapter object if found, None otherwise
"""
for adapter in cls.iterate_from_obj(obj):
if adapter.func_name == func_name:
return adapter
return None
@classmethod
def iterate_from_obj(
cls, obj: SignalObj | ImageObj
) -> Generator["BaseResultAdapter", None, None]:
"""Iterate over results stored in an object's metadata.
Args:
obj: Signal or image object
Yields:
Adapter objects
"""
for key, value in obj.metadata.items():
if cls.match(key, value):
try:
yield cls.from_metadata_entry(obj, key)
except (ValueError, TypeError):
# Skip invalid entries
pass
def to_dataframe(self, visible_only: bool = False):
"""Convert the result to a pandas DataFrame.
Args:
visible_only: If True, include only visible headers based on display
preferences. Default is False.
Returns:
DataFrame with an optional 'roi_index' column.
If visible_only is True, only columns with visible headers are included.
"""
return self.result.to_dataframe(visible_only=visible_only)
def to_html(
self,
obj=None,
visible_only: bool = True,
transpose_single_row: bool = True,
**kwargs,
) -> str:
"""Convert the result to HTML format.
Args:
obj: Optional SignalObj or ImageObj for ROI title extraction
visible_only: If True, include only visible headers based on display
preferences. Default is False.
transpose_single_row: If True, transpose the table when there's only one row
**kwargs: Additional arguments passed to DataFrame.to_html()
Returns:
HTML representation of the result
"""
return self.result.to_html(
obj=obj,
visible_only=visible_only,
transpose_single_row=transpose_single_row,
**kwargs,
)
def get_visible_headers(self) -> list[str]:
"""Get list of currently visible headers based on display preferences.
Returns:
List of header names that should be displayed
"""
return self.result.get_visible_headers()
DataLab-1.1.0/datalab/adapters_metadata/common.py 0000664 0000000 0000000 00000035151 15140141176 0021703 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Common functions for metadata adapters.
"""
from __future__ import annotations
import dataclasses
import warnings
from typing import TYPE_CHECKING
import numpy as np
import pandas as pd
from guidata.qthelpers import exec_dialog
from guidata.widgets.dataframeeditor import DataFrameEditor
from sigima.objects import ImageObj, SignalObj
from datalab.adapters_metadata.base_adapter import BaseResultAdapter
from datalab.adapters_metadata.geometry_adapter import GeometryAdapter
from datalab.adapters_metadata.table_adapter import TableAdapter
from datalab.config import Conf, _
from datalab.objectmodel import get_short_id
if TYPE_CHECKING:
from qtpy.QtWidgets import QWidget
@dataclasses.dataclass
class ResultData:
"""Result data associated to a shapetype"""
# We now store adapted objects from the new architecture
results: list[BaseResultAdapter] | None = None
ylabels: list[str] | None = None
short_ids: list[str] | None = None
def __bool__(self) -> bool:
"""Return True if there are results stored"""
return bool(self.results)
@property
def category(self) -> str:
"""Return category of results"""
if not self.results:
raise ValueError("No result available")
return self.results[0].category
@property
def headers(self) -> list[str]:
"""Return headers of results"""
if not self.results:
raise ValueError("No result available")
# Return the intersection of all headers
headers = set(self.results[0].headers)
if len(self.results) > 1:
for adapter in self.results[1:]:
headers.intersection_update(adapter.headers)
return list(headers)
def __post_init__(self):
"""Check and initialize fields"""
if self.results is None:
self.results = []
if self.ylabels is None:
self.ylabels = []
if self.short_ids is None:
self.short_ids = []
def append(self, adapter: BaseResultAdapter, obj: SignalObj | ImageObj) -> None:
"""Append a result adapter
Args:
adapter: Adapter to append
obj: Object associated to the adapter
"""
# Check that the adapter is compatible with existing ones
if self.results:
if adapter.category != self.results[0].category:
raise ValueError("Incompatible adapter category")
if len(set(self.headers).intersection(set(adapter.headers))) == 0:
raise ValueError("Incompatible adapter headers")
self.results.append(adapter)
df = adapter.to_dataframe()
for i_row_res in range(len(df)):
sid = get_short_id(obj)
ylabel = f"{adapter.func_name}({sid})"
if "roi_index" in df.columns:
i_roi = int(df.iloc[i_row_res]["roi_index"])
roititle = ""
if i_roi >= 0 and obj.roi is not None:
roititle = obj.roi.get_single_roi_title(i_roi)
ylabel += f"|{roititle}"
self.ylabels.append(ylabel)
self.short_ids.append(sid)
def have_results(objs: list[SignalObj | ImageObj]) -> bool:
"""Return True if any object has results
Args:
objs: List of objects
Returns:
True if any object has results, False otherwise
"""
return any(
item for obj in objs for item in GeometryAdapter.iterate_from_obj(obj)
) or any(item for obj in objs for item in TableAdapter.iterate_from_obj(obj))
def have_geometry_results(objs: list[SignalObj | ImageObj]) -> bool:
"""Return True if any object has geometry results
Args:
objs: List of objects
Returns:
True if any object has geometry results, False otherwise
"""
return any(item for obj in objs for item in GeometryAdapter.iterate_from_obj(obj))
def create_resultdata_dict(
objs: list[SignalObj | ImageObj],
) -> dict[str, ResultData]:
"""Return result data dictionary
Args:
objs: List of objects
Returns:
Result data dictionary: keys are result categories, values are ResultData
"""
rdatadict: dict[str, ResultData] = {}
for obj in objs:
for adapter in list(GeometryAdapter.iterate_from_obj(obj)) + list(
TableAdapter.iterate_from_obj(obj)
):
rdata = rdatadict.setdefault(adapter.category, ResultData())
rdata.append(adapter, obj)
return rdatadict
def show_resultdata(parent: QWidget, rdata: ResultData, object_name: str = "") -> None:
"""Show result data in a DataFrame editor window
Args:
parent: Parent widget
rdata: Result data to show
object_name: Optional object name for the dialog
"""
with warnings.catch_warnings():
warnings.simplefilter("ignore", RuntimeWarning)
# Generate dataframes with visible columns only
# Use the object-level visible_only parameter for cleaner implementation
dfs = [result.to_dataframe(visible_only=True) for result in rdata.results]
# Combine all dataframes
df = pd.concat(dfs, ignore_index=True)
# Add comparison columns if we have multiple results of the same kind
if len(dfs) > 1:
df = _add_comparison_columns_to_dataframe(df, rdata)
# Remove roi_index column for display (not needed in the GUI)
if "roi_index" in df.columns:
df = df.drop(columns=["roi_index"])
df.set_index(pd.Index(rdata.ylabels), inplace=True)
dlg = DataFrameEditor(parent)
dlg.setup_and_check(
df,
_("Results") + f" ({rdata.category})",
readonly=True,
add_title_suffix=False,
)
if object_name:
dlg.setObjectName(object_name)
dlg.resize(750, 300)
exec_dialog(dlg)
def _add_comparison_columns_to_dataframe(
df: pd.DataFrame, rdata: ResultData
) -> pd.DataFrame:
"""Add comparison columns to dataframe with ROI-aware grouping.
For each original column, adds one comparison column showing the difference
between the current row and the corresponding reference row.
Args:
df: Combined DataFrame with all results
rdata: ResultData containing ylabels and short_ids
Returns:
DataFrame with comparison columns added (one Δ column per original column)
"""
# Build signal/image groups:
# list of (object_id, start_row, end_row) for each signal/image
obj_groups = []
current_obj_id = None
group_start = 0
for i, obj_id in enumerate(rdata.short_ids):
if obj_id != current_obj_id:
# New signal group
if current_obj_id is not None:
# Close previous group
obj_groups.append((current_obj_id, group_start, i - 1))
current_obj_id = obj_id
group_start = i
# Close the last group
if current_obj_id is not None:
obj_groups.append((current_obj_id, group_start, len(rdata.ylabels) - 1))
# If we only have one signal group, no need for comparisons
if len(obj_groups) <= 1:
return df
# Use the first signal group as reference
reference_group = obj_groups[0]
_ref_obj_id, reference_start, reference_end = reference_group
# Get columns to compare (exclude roi_index)
cols_to_compare = [col for col in df.columns if col != "roi_index"]
# Create new dataframe with original columns plus one comparison column per
# original column
result_df = df.copy()
# Add comparison columns - one per original column
for col in cols_to_compare:
comparison_col_name = f"Δ({col})"
comparison_values = []
# For each row in the entire dataframe
for row_idx in range(len(df)):
# Find which group this row belongs to
row_group_idx = None
for group_idx, (obj_id, start, end) in enumerate(obj_groups):
if start <= row_idx <= end:
row_group_idx = group_idx
break
if row_group_idx == 0:
# Reference group - no comparison needed
comparison_values.append("")
elif row_group_idx is not None:
# Non-reference group - calculate comparison with corresponding
# reference row
group_start = obj_groups[row_group_idx][1]
ref_row_idx = reference_start + (row_idx - group_start)
if ref_row_idx <= reference_end:
ref_val = df.iloc[ref_row_idx][col]
curr_val = df.iloc[row_idx][col]
comparison_values.append(
_compute_comparison_value(ref_val, curr_val)
)
else:
comparison_values.append("")
else:
# Should not happen, but handle gracefully
comparison_values.append("")
# Insert comparison column right after the original column
col_idx = result_df.columns.get_loc(col)
result_df.insert(col_idx + 1, comparison_col_name, comparison_values)
return result_df
def _compute_comparison_value(ref_val, curr_val) -> str:
"""Compute a comparison value between reference and current values.
Args:
ref_val: Reference value
curr_val: Current value to compare
Returns:
String representation of the comparison
"""
# Handle different data types
if pd.isna(ref_val) or pd.isna(curr_val):
return "N/A"
if isinstance(ref_val, str) or isinstance(curr_val, str):
# String comparison
return "=" if str(ref_val) == str(curr_val) else "≠"
if isinstance(ref_val, (int, float, np.integer, np.floating)) and isinstance(
curr_val, (int, float, np.integer, np.floating)
):
# Numeric comparison - show difference
diff = curr_val - ref_val
# For integers, check exact equality; for floats, use small tolerance
if isinstance(ref_val, (int, np.integer)) and isinstance(
curr_val, (int, np.integer)
):
tolerance = 0
else:
tolerance = 1e-10
if abs(diff) <= tolerance:
return "="
# Format the difference with appropriate sign
sign = "+" if diff > 0 else ""
return f"{sign}{diff:.4g}"
# Default comparison
return "=" if ref_val == curr_val else "≠"
def resultadapter_to_html(
adapter: BaseResultAdapter | list[BaseResultAdapter],
obj: SignalObj | ImageObj,
visible_only: bool = True,
transpose_single_row: bool = True,
max_cells: int | None = None,
**kwargs,
) -> str:
"""Convert a result adapter to HTML format
Args:
adapter: Adapter to convert, or list of adapters to concatenate
obj: Object associated to the adapter
visible_only: If True, include only visible headers based on display
preferences. Default is False.
transpose_single_row: If True, transpose the table when there's only one row
max_cells: Maximum number of table cells (rows × columns) to display per
result. If None, use the configuration value. If a result has more cells,
it will be truncated with a notice.
**kwargs: Additional arguments passed to DataFrame.to_html()
Returns:
HTML representation of the adapter
"""
if not isinstance(adapter, BaseResultAdapter) and not all(
isinstance(adp, BaseResultAdapter) for adp in adapter
):
raise ValueError(
"Adapter must be a BaseResultAdapter "
"or a list of BaseResultAdapter instances"
)
# Get max_cells from config if not provided
if max_cells is None:
max_cells = Conf.view.max_cells_in_label.get(100)
if isinstance(adapter, BaseResultAdapter):
# Get the dataframe FIRST to check truncation needs
df = adapter.to_dataframe(visible_only=visible_only)
# Remove roi_index column for display calculations
display_df = df.drop(columns=["roi_index"]) if "roi_index" in df.columns else df
# For merged labels, limit display columns for readability
max_display_cols = Conf.view.max_cols_in_label.get(20)
num_cols = len(display_df.columns)
cols_truncated = num_cols > max_display_cols
if cols_truncated:
display_df = display_df.iloc[:, :max_display_cols]
num_cols = max_display_cols
# Calculate number of cells (rows × columns)
num_rows = len(df)
num_cells = num_rows * num_cols
# Check if truncation is needed BEFORE calling to_html()
if num_cells > max_cells or cols_truncated:
# Calculate how many rows we can display given max_cells
max_rows = max(1, max_cells // num_cols) if num_cols > 0 else num_rows
# Truncate to max_rows and make a copy to avoid SettingWithCopyWarning
df_truncated = display_df.head(max_rows).copy()
# Generate HTML directly from truncated DataFrame for performance
# This is MUCH faster than calling adapter.to_html() on full data
html_kwargs = {"border": 0}
html_kwargs.update(kwargs)
# Format numeric columns efficiently
for col in df_truncated.select_dtypes(include=["number"]).columns:
df_truncated[col] = df_truncated[col].map(
lambda x: f"{x:.3g}" if pd.notna(x) else x
)
text = f'{adapter.result.title}:'
text += df_truncated.to_html(**html_kwargs)
# Add truncation notice
omitted_parts = []
if num_rows > max_rows:
omitted_parts.append(_("%d more rows") % (num_rows - max_rows))
if cols_truncated:
num_omitted_cols = len(df.columns) - max_display_cols
omitted_parts.append(_("%d more columns") % num_omitted_cols)
if omitted_parts:
omitted_str = _("%s omitted") % ", ".join(omitted_parts)
text += f"
... ({omitted_str})
"
return text
# No truncation needed, use the standard adapter.to_html() method
return adapter.to_html(
obj=obj,
visible_only=visible_only,
transpose_single_row=transpose_single_row,
**kwargs,
)
# For lists of adapters, recursively process each one
return "".join(
[
resultadapter_to_html(
res, obj, visible_only, transpose_single_row, max_cells
)
for res in adapter
]
)
DataLab-1.1.0/datalab/adapters_metadata/geometry_adapter.py 0000664 0000000 0000000 00000005420 15140141176 0023742 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Adapter for Sigima's GeometryResult, providing features
for storing and retrieving those objects as metadata for DataLab's signal
and image objects.
"""
from __future__ import annotations
from typing import ClassVar
from sigima.objects import GeometryResult, ImageObj, SignalObj
from datalab.adapters_metadata.base_adapter import BaseResultAdapter
class GeometryAdapter(BaseResultAdapter):
"""Adapter for GeometryResult objects.
This adapter provides a unified interface for working with GeometryResult objects,
including metadata storage/retrieval and various data representations.
Args:
geometry: GeometryResult object to adapt
"""
# Class constants for metadata storage
META_PREFIX: ClassVar[str] = "Geometry_"
def __init__(self, geometry: GeometryResult) -> None:
super().__init__(geometry)
@classmethod
def from_geometry_result(cls, geometry: GeometryResult) -> GeometryAdapter:
"""Create GeometryAdapter from GeometryResult.
Args:
geometry: GeometryResult object
Returns:
GeometryAdapter instance
"""
return cls(geometry)
@property
def headers(self) -> list[str]:
"""Get column headers for the coordinates.
Returns:
List of column headers
"""
# Get headers directly from the DataFrame
df = self.result.to_dataframe()
# Return coordinate columns (exclude 'roi_index' if present)
return [col for col in df.columns if col != "roi_index"]
@property
def category(self) -> str:
"""Get the category.
Returns:
Category
"""
return f"shape_{self.result.kind.value}"
@staticmethod
def add_geometry_result_to_obj(
geometry: GeometryResult, obj: SignalObj | ImageObj
) -> None:
"""Static method to add GeometryResult to object.
Args:
geometry: GeometryResult object
obj: Signal or image object
"""
# Create adapter and add to object
adapter = GeometryAdapter.from_geometry_result(geometry)
adapter.add_to(obj)
@classmethod
def from_metadata_entry(cls, obj: SignalObj | ImageObj, key: str):
"""Create a geometry result adapter from a metadata entry.
Args:
obj: Object containing the metadata
key: Metadata key for the array data
Returns:
GeometryAdapter object
Raises:
ValueError: Invalid metadata entry
"""
# Load the geometry data from the dictionary
geometry_dict = obj.metadata[key]
geometry = GeometryResult.from_dict(geometry_dict)
return cls(geometry)
DataLab-1.1.0/datalab/adapters_metadata/table_adapter.py 0000664 0000000 0000000 00000004544 15140141176 0023204 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Adapter for Sigima's TableResult, providing features
for storing and retrieving those objects as metadata for DataLab's signal
and image objects.
"""
from __future__ import annotations
from typing import ClassVar, Union
from sigima.objects import ImageObj, SignalObj
from sigima.objects.scalar import NO_ROI, TableResult
from datalab.adapters_metadata.base_adapter import BaseResultAdapter
class TableAdapter(BaseResultAdapter):
"""Adapter for TableResult objects.
This adapter provides a unified interface for working with TableResult objects,
including metadata storage/retrieval and various data representations.
Args:
table: TableResult object to adapt
"""
# Class constants for metadata storage
META_PREFIX: ClassVar[str] = "Table_"
def __init__(self, table: TableResult) -> None:
super().__init__(table)
@property
def headers(self) -> list[str]:
"""Get the column headers.
Returns:
Column headers
"""
return list(self.result.headers)
@property
def category(self) -> str:
"""Get the category.
Returns:
Category (uses the title for backward compatibility)
"""
return self.result.title
def get_unique_roi_indices(self) -> list[int]:
"""Get unique ROI indices present in the data.
Returns:
List of unique ROI indices
"""
df = self.to_dataframe()
if "roi_index" in df.columns:
return sorted(df["roi_index"].unique().tolist())
return [NO_ROI] if len(df) > 0 else []
@classmethod
def from_metadata_entry(cls, obj: Union[SignalObj, ImageObj], key: str):
"""Create a table result adapter from a metadata entry.
Args:
obj: Object containing the metadata
key: Metadata key for the table data
Returns:
TableAdapter object
Raises:
ValueError: Invalid metadata entry
"""
if not cls.match(key, obj.metadata[key]):
raise ValueError(f"Invalid metadata key for table result: {key}")
# Parse the metadata entry as a TableResult dictionary
table_dict = obj.metadata[key]
table = TableResult.from_dict(table_dict)
return cls(table)
DataLab-1.1.0/datalab/adapters_plotpy/ 0000775 0000000 0000000 00000000000 15140141176 0017603 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/adapters_plotpy/__init__.py 0000664 0000000 0000000 00000002611 15140141176 0021714 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Adapters for PlotPy
===================
The :mod:`datalab.adapters_plotpy` package provides adapters for
PlotPy to integrate with DataLab's data model and GUI.
"""
__all__ = [
"CURVESTYLES",
"CircularROIPlotPyAdapter",
"GeometryPlotPyAdapter",
"ImageObjPlotPyAdapter",
"PolygonalROIPlotPyAdapter",
"RectangularROIPlotPyAdapter",
"SegmentROIPlotPyAdapter",
"SignalObjPlotPyAdapter",
"SignalROIPlotPyAdapter",
"TablePlotPyAdapter",
"TypePlotItem",
"TypeROIItem",
"configure_roi_item",
"create_adapter_from_object",
"items_to_json",
"json_to_items",
"plotitem_to_singleroi",
"singleroi_to_plotitem",
]
from .base import items_to_json, json_to_items
from .converters import (
create_adapter_from_object,
plotitem_to_singleroi,
singleroi_to_plotitem,
)
from .objects.base import TypePlotItem
from .objects.image import (
ImageObjPlotPyAdapter,
)
from .objects.scalar import (
GeometryPlotPyAdapter,
TablePlotPyAdapter,
)
from .objects.signal import CURVESTYLES, SignalObjPlotPyAdapter
from .roi.base import TypeROIItem, configure_roi_item
from .roi.image import (
CircularROIPlotPyAdapter,
PolygonalROIPlotPyAdapter,
RectangularROIPlotPyAdapter,
)
from .roi.signal import SegmentROIPlotPyAdapter, SignalROIPlotPyAdapter
DataLab-1.1.0/datalab/adapters_plotpy/annotations.py 0000664 0000000 0000000 00000007425 15140141176 0022522 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Annotation Adapter for PlotPy Integration
-----------------------------------------
This module bridges Sigima's format-agnostic annotation storage with PlotPy's
plot item system. It handles bidirectional conversion between:
- Sigima: list[dict] (JSON-serializable)
- PlotPy: list[AnnotatedShape] (plot items)
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from guidata.io import JSONReader, JSONWriter
from plotpy.io import load_items, save_items
if TYPE_CHECKING:
from plotpy.items import AnnotatedShape
from sigima.objects.base import BaseObj
class PlotPyAnnotationAdapter:
"""Adapter for converting between Sigima annotations and PlotPy items.
This class provides the bridge between Sigima's generic annotation storage
(list of dicts) and PlotPy's specific plot item format.
Example:
>>> from sigima.objects.signal.creation import create_signal
>>> obj = create_signal("Test")
>>> adapter = PlotPyAnnotationAdapter(obj)
>>>
>>> # Add PlotPy items
>>> from plotpy.items import AnnotatedRectangle
>>> rect = AnnotatedRectangle(0, 0, 10, 10)
>>> adapter.add_items([rect])
>>>
>>> # Retrieve as PlotPy items
>>> items = adapter.get_items()
>>> len(items)
1
"""
def __init__(self, obj: BaseObj):
"""Initialize adapter with an object.
Args:
obj: Signal or image object with annotation support
"""
self.obj = obj
def get_items(self) -> list[AnnotatedShape]:
"""Get annotations as PlotPy items.
Returns:
List of PlotPy annotation items
Notes:
This method deserializes the JSON data stored in the object using
PlotPy's load_items() function.
"""
annotations = self.obj.get_annotations()
if not annotations:
return []
items = []
for ann_dict in annotations:
# Each annotation dict should contain PlotPy's JSON serialization
if "plotpy_json" in ann_dict:
try:
json_str = ann_dict["plotpy_json"]
for item in load_items(JSONReader(json_str)):
items.append(item)
except (json.JSONDecodeError, ValueError, KeyError):
# Skip invalid items
continue
return items
def set_items(self, items: list[AnnotatedShape]) -> None:
"""Set annotations from PlotPy items.
Args:
items: List of PlotPy annotation items
Notes:
This method serializes PlotPy items to JSON using PlotPy's
save_items() function and stores them in the Sigima format.
"""
if not items:
self.obj.clear_annotations()
return
# Convert PlotPy items to our annotation format
annotations = []
for item in items:
writer = JSONWriter(None)
save_items(writer, [item])
ann_dict = {
"type": "plotpy_item",
"item_class": item.__class__.__name__,
"plotpy_json": writer.get_json(),
}
annotations.append(ann_dict)
self.obj.set_annotations(annotations)
def add_items(self, items: list[AnnotatedShape]) -> None:
"""Add PlotPy items to existing annotations.
Args:
items: List of PlotPy annotation items to add
"""
current_items = self.get_items()
current_items.extend(items)
self.set_items(current_items)
def clear(self) -> None:
"""Clear all annotations."""
self.obj.clear_annotations()
DataLab-1.1.0/datalab/adapters_plotpy/base.py 0000664 0000000 0000000 00000005354 15140141176 0021076 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Base Module
--------------------------
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from guidata.io import JSONReader, JSONWriter
from plotpy.io import load_items, save_items
from plotpy.items import (
AbstractLabelItem,
AnnotatedSegment,
AnnotatedShape,
)
if TYPE_CHECKING:
from plotpy.items import AbstractShape
from plotpy.styles import AnnotationParam
def config_annotated_shape(
item: AnnotatedShape,
fmt: str,
lbl: bool,
section: str | None = None,
option: str | None = None,
show_computations: bool | None = None,
):
"""Configurate annotated shape
Args:
item: Annotated shape item
fmt: Format string
lbl: Show label
section: Shape style section (e.g. "plot")
option: Shape style option (e.g. "shape/drag")
show_computations: Show computations
"""
param: AnnotationParam = item.annotationparam
param.format = fmt
param.show_label = lbl
if show_computations is not None:
param.show_computations = show_computations
if isinstance(item, AnnotatedSegment):
item.label.labelparam.anchor = "T"
item.label.labelparam.update_item(item.label)
param.update_item(item)
if section is not None and option is not None:
item.set_style(section, option)
# TODO: [P3] Move this function as a method of plot items in PlotPy
def set_plot_item_editable(
item: AbstractShape | AbstractLabelItem | AnnotatedShape, state
):
"""Set plot item editable state
Args:
item: Plot item
state: State
"""
item.set_movable(state)
item.set_resizable(state and not isinstance(item, AbstractLabelItem))
item.set_rotatable(state and not isinstance(item, AbstractLabelItem))
item.set_readonly(not state)
item.set_selectable(state)
def items_to_json(items: list) -> str | None:
"""Convert plot items to JSON string
Args:
items: list of plot items
Returns:
JSON string or None if items is empty
"""
if items:
writer = JSONWriter(None)
save_items(writer, items)
return writer.get_json(indent=4)
return None
def json_to_items(json_str: str | None) -> list:
"""Convert JSON string to plot items
Args:
json_str: JSON string or None
Returns:
List of plot items
"""
items = []
if json_str:
try:
for item in load_items(JSONReader(json_str)):
items.append(item)
except json.decoder.JSONDecodeError:
pass
return items
DataLab-1.1.0/datalab/adapters_plotpy/converters.py 0000664 0000000 0000000 00000005042 15140141176 0022350 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Converters
-------------------------
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from __future__ import annotations
from typing import TYPE_CHECKING
from plotpy.items import (
AnnotatedCircle,
AnnotatedPolygon,
AnnotatedRectangle,
AnnotatedXRange,
)
from sigima.objects import (
CircularROI,
PolygonalROI,
RectangularROI,
SegmentROI,
)
from datalab.adapters_plotpy.factories import create_adapter_from_object
from datalab.adapters_plotpy.roi.image import (
PolygonalROIPlotPyAdapter,
RectangularROIPlotPyAdapter,
)
if TYPE_CHECKING:
from sigima.objects import ImageObj, SignalObj
def plotitem_to_singleroi(
plot_item: AnnotatedXRange
| AnnotatedRectangle
| AnnotatedCircle
| AnnotatedPolygon,
obj: SignalObj | ImageObj | None = None,
) -> SegmentROI | RectangularROI | CircularROI | PolygonalROI:
"""Create a single ROI from the given PlotPy item to integrate with DataLab
Args:
plot_item: The PlotPy item for which to create a single ROI
obj: Optional signal or image object for coordinate rounding
Returns:
A single ROI instance
"""
# pylint: disable=import-outside-toplevel
from datalab.adapters_plotpy.roi.image import (
CircularROIPlotPyAdapter,
)
from datalab.adapters_plotpy.roi.signal import (
SegmentROIPlotPyAdapter,
)
if isinstance(plot_item, AnnotatedXRange):
adapter = SegmentROIPlotPyAdapter
elif isinstance(plot_item, AnnotatedRectangle):
adapter = RectangularROIPlotPyAdapter
elif isinstance(plot_item, AnnotatedCircle):
adapter = CircularROIPlotPyAdapter
elif isinstance(plot_item, AnnotatedPolygon):
adapter = PolygonalROIPlotPyAdapter
else:
raise TypeError(f"Unsupported PlotPy item type: {type(plot_item)}")
return adapter.from_plot_item(plot_item, obj)
def singleroi_to_plotitem(
roi: SegmentROI | RectangularROI | CircularROI | PolygonalROI,
obj: SignalObj | ImageObj,
) -> AnnotatedXRange | AnnotatedRectangle | AnnotatedCircle | AnnotatedPolygon:
"""Create a PlotPy item from the given single ROI to integrate with DataLab
Args:
roi: The single ROI for which to create a PlotPy item
obj: The object (signal or image) associated with the ROI
Returns:
A PlotPy item instance
"""
adapter = create_adapter_from_object(roi)
return adapter.to_plot_item(obj)
DataLab-1.1.0/datalab/adapters_plotpy/coordutils.py 0000664 0000000 0000000 00000013142 15140141176 0022345 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
ROI Coordinate Utilities
=========================
This module provides utility functions for rounding ROI coordinates to appropriate
precision based on the sampling characteristics of signals and images.
These functions are used when converting interactive PlotPy shapes to ROI objects
to ensure coordinates are displayed with reasonable precision.
"""
from __future__ import annotations
import numpy as np
from sigima.objects import ImageObj, ROI1DParam, ROI2DParam, SignalObj
def round_signal_coords(
obj: SignalObj, coords: list[float], precision_factor: float = 0.1
) -> list[float]:
"""Round signal coordinates to appropriate precision based on sampling period.
Rounds to a fraction of the median sampling period to avoid excessive decimal
places while maintaining reasonable precision.
Args:
obj: signal object
coords: coordinates to round
precision_factor: fraction of sampling period to use as rounding precision.
Default is 0.1 (1/10th of sampling period).
Returns:
Rounded coordinates
"""
if len(obj.x) < 2:
# Cannot compute sampling period, return coords as-is
return coords
# Compute median sampling period
sampling_period = float(np.median(np.diff(obj.x)))
if sampling_period == 0:
# Avoid division by zero for constant x arrays
return coords
# Round to specified fraction of sampling period
precision = sampling_period * precision_factor
# Determine number of decimal places
if precision > 0:
decimals = max(0, int(-np.floor(np.log10(precision))))
return [round(c, decimals) for c in coords]
return coords
def round_image_coords(
obj: ImageObj, coords: list[float], precision_factor: float = 0.1
) -> list[float]:
"""Round image coordinates to appropriate precision based on pixel spacing.
Rounds to a fraction of the pixel spacing to avoid excessive decimal places
while maintaining reasonable precision. Uses separate precision for X and Y.
Args:
obj: image object
coords: flat list of coordinates [x0, y0, x1, y1, ...] to round
precision_factor: fraction of pixel spacing to use as rounding precision.
Default is 0.1 (1/10th of pixel spacing).
Returns:
Rounded coordinates
Raises:
ValueError: if coords does not contain an even number of elements
"""
if len(coords) % 2 != 0:
raise ValueError("coords must contain an even number of elements (x, y pairs).")
if len(coords) == 0:
return coords
rounded = list(coords)
if obj.is_uniform_coords:
# Use dx, dy for uniform coordinates
precision_x = abs(obj.dx) * precision_factor
precision_y = abs(obj.dy) * precision_factor
else:
# Compute average spacing for non-uniform coordinates
if len(obj.xcoords) > 1:
avg_dx = float(np.mean(np.abs(np.diff(obj.xcoords))))
precision_x = avg_dx * precision_factor
else:
precision_x = 0
if len(obj.ycoords) > 1:
avg_dy = float(np.mean(np.abs(np.diff(obj.ycoords))))
precision_y = avg_dy * precision_factor
else:
precision_y = 0
# Round X coordinates (even indices)
if precision_x > 0:
decimals_x = max(0, int(-np.floor(np.log10(precision_x))))
for i in range(0, len(rounded), 2):
rounded[i] = round(rounded[i], decimals_x)
# Round Y coordinates (odd indices)
if precision_y > 0:
decimals_y = max(0, int(-np.floor(np.log10(precision_y))))
for i in range(1, len(rounded), 2):
rounded[i] = round(rounded[i], decimals_y)
return rounded
def round_signal_roi_param(
obj: SignalObj, param: ROI1DParam, precision_factor: float = 0.1
) -> None:
"""Round signal ROI parameter coordinates in-place.
Args:
obj: signal object
param: ROI parameter to round (modified in-place)
precision_factor: fraction of sampling period to use as rounding precision
"""
coords = round_signal_coords(obj, [param.xmin, param.xmax], precision_factor)
param.xmin, param.xmax = coords
def round_image_roi_param(
obj: ImageObj, param: ROI2DParam, precision_factor: float = 0.1
) -> None:
"""Round image ROI parameter coordinates in-place.
Args:
obj: image object
param: ROI parameter to round (modified in-place)
precision_factor: fraction of pixel spacing to use as rounding precision
"""
if param.geometry == "rectangle":
# Round x0, y0, dx, dy
x0, y0, x1, y1 = param.x0, param.y0, param.x0 + param.dx, param.y0 + param.dy
coords = round_image_coords(obj, [x0, y0, x1, y1], precision_factor)
param.x0, param.y0 = coords[0], coords[1]
# Round dx and dy to avoid floating-point errors in subtraction
dx_dy_rounded = round_image_coords(
obj, [coords[2] - coords[0], coords[3] - coords[1]], precision_factor
)
param.dx = dx_dy_rounded[0]
param.dy = dx_dy_rounded[1]
elif param.geometry == "circle":
# Round xc, yc, r
coords = round_image_coords(obj, [param.xc, param.yc], precision_factor)
param.xc, param.yc = coords
# Round radius using X precision
r_rounded = round_image_coords(obj, [param.r, 0], precision_factor)[0]
param.r = r_rounded
elif param.geometry == "polygon":
# Round polygon points
rounded = round_image_coords(obj, param.points.tolist(), precision_factor)
param.points = np.array(rounded)
DataLab-1.1.0/datalab/adapters_plotpy/factories.py 0000664 0000000 0000000 00000005532 15140141176 0022141 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Factories
------------------------
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from __future__ import annotations
def create_adapter_from_object(object_to_adapt):
"""Create an adapter for the given object to integrate with PlotPy
Args:
object_to_adapt: The object to adapt (signal, image, ROI, or scalar result)
Returns:
An adapter instance
"""
# pylint: disable=import-outside-toplevel
# Import adapters as needed to avoid circular imports
from sigima.objects import (
CircularROI,
ImageObj,
ImageROI,
PolygonalROI,
RectangularROI,
SegmentROI,
SignalObj,
SignalROI,
)
from datalab.adapters_metadata import GeometryAdapter, TableAdapter
if isinstance(object_to_adapt, GeometryAdapter):
from datalab.adapters_plotpy.objects.scalar import GeometryPlotPyAdapter
adapter = GeometryPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, TableAdapter):
from datalab.adapters_plotpy.objects.scalar import TablePlotPyAdapter
adapter = TablePlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, SignalObj):
from datalab.adapters_plotpy.objects.signal import SignalObjPlotPyAdapter
adapter = SignalObjPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, SignalROI):
from datalab.adapters_plotpy.roi.signal import SignalROIPlotPyAdapter
adapter = SignalROIPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, SegmentROI):
from datalab.adapters_plotpy.roi.signal import SegmentROIPlotPyAdapter
adapter = SegmentROIPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, ImageObj):
from datalab.adapters_plotpy.objects.image import ImageObjPlotPyAdapter
adapter = ImageObjPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, RectangularROI):
from datalab.adapters_plotpy.roi.image import RectangularROIPlotPyAdapter
adapter = RectangularROIPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, CircularROI):
from datalab.adapters_plotpy.roi.image import CircularROIPlotPyAdapter
adapter = CircularROIPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, PolygonalROI):
from datalab.adapters_plotpy.roi.image import PolygonalROIPlotPyAdapter
adapter = PolygonalROIPlotPyAdapter(object_to_adapt)
elif isinstance(object_to_adapt, ImageROI):
from datalab.adapters_plotpy.roi.image import ImageROIPlotPyAdapter
adapter = ImageROIPlotPyAdapter(object_to_adapt)
else:
raise TypeError(f"Unsupported object type: {type(object_to_adapt)}")
return adapter
DataLab-1.1.0/datalab/adapters_plotpy/objects/ 0000775 0000000 0000000 00000000000 15140141176 0021234 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/adapters_plotpy/objects/__init__.py 0000664 0000000 0000000 00000000000 15140141176 0023333 0 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/adapters_plotpy/objects/base.py 0000664 0000000 0000000 00000015075 15140141176 0022530 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Base Object Module
---------------------------------
"""
from __future__ import annotations
import abc
from typing import (
TYPE_CHECKING,
Any,
Generic,
TypeVar,
)
from guidata.dataset import update_dataset
from plotpy.items import (
AnnotatedShape,
)
from sigima.objects.base import (
ROI_KEY,
TypeObj,
)
from datalab.adapters_metadata import GeometryAdapter
from datalab.adapters_plotpy.annotations import PlotPyAnnotationAdapter
from datalab.adapters_plotpy.base import (
config_annotated_shape,
set_plot_item_editable,
)
from datalab.adapters_plotpy.objects.scalar import GeometryPlotPyAdapter
from datalab.config import Conf
if TYPE_CHECKING:
from plotpy.items import CurveItem, MaskedXYImageItem
TypePlotItem = TypeVar("TypePlotItem", bound="CurveItem | MaskedXYImageItem")
class BaseObjPlotPyAdapter(Generic[TypeObj, TypePlotItem]):
"""Object (signal/image) plot item adapter class"""
DEFAULT_FMT = "s" # This is overriden in children classes
CONF_FMT = Conf.view.sig_format # This is overriden in children classes
def __init__(self, obj: TypeObj) -> None:
"""Initialize the adapter with the object.
Args:
obj: object (signal/image)
"""
self.obj = obj
self.__default_options = {
"format": "%" + self.CONF_FMT.get(self.DEFAULT_FMT),
"showlabel": Conf.view.show_label.get(False),
}
self.annotation_adapter = PlotPyAnnotationAdapter(obj)
def get_obj_option(self, name: str) -> Any:
"""Get object option value.
Args:
name: option name
Returns:
Option value
"""
default = self.__default_options[name]
return self.obj.get_metadata_option(name, default)
@abc.abstractmethod
def make_item(self, update_from: TypePlotItem | None = None) -> TypePlotItem:
"""Make plot item from data.
Args:
update_from: update
Returns:
Plot item
"""
@abc.abstractmethod
def update_item(self, item: TypePlotItem, data_changed: bool = True) -> None:
"""Update plot item from data.
Args:
item: plot item
data_changed: if True, data has changed
"""
def add_annotations_from_items(self, items: list) -> None:
"""Add object annotations (annotation plot items).
Args:
items: annotation plot items
"""
# Use the new annotation adapter
self.annotation_adapter.add_items(items)
def set_annotations_from_items(self, items: list) -> None:
"""Set object annotations (annotation plot items), replacing any existing ones.
Args:
items: annotation plot items
"""
# Use the new annotation adapter
self.annotation_adapter.set_items(items)
@abc.abstractmethod
def add_label_with_title(self, title: str | None = None) -> None:
"""Add label with title annotation
Args:
title: title (if None, use object title)
"""
def iterate_shape_items(self, editable: bool = False):
"""Iterate over shape items encoded in metadata (if any).
Args:
editable: if True, annotations are editable
Yields:
Plot item
"""
fmt = self.get_obj_option("format")
lbl = self.get_obj_option("showlabel")
for key, _value in self.obj.metadata.items():
if key == ROI_KEY:
roi = self.obj.roi
if roi is not None:
# Delayed import to avoid circular dependency
# pylint: disable=import-outside-toplevel
from datalab.adapters_plotpy.roi.factory import create_roi_adapter
adapter = create_roi_adapter(roi)
yield from adapter.iterate_roi_items(
self.obj, fmt=fmt, lbl=lbl, editable=False
)
# Process geometry results from metadata (using GeometryAdapter)
elif GeometryAdapter.match(key, _value):
try:
geomadapter = GeometryAdapter.from_metadata_entry(self.obj, key)
plot_adapter = GeometryPlotPyAdapter(geomadapter)
yield from plot_adapter.iterate_shape_items(
fmt, lbl, self.obj.PREFIX
)
except (ValueError, TypeError):
# Skip invalid entries
pass
# Use the new annotation adapter to get items
if self.obj.has_annotations():
for item in self.annotation_adapter.get_items():
if isinstance(item, AnnotatedShape):
config_annotated_shape(item, fmt, lbl)
set_plot_item_editable(item, editable)
yield item
def update_plot_item_parameters(self, item: TypePlotItem) -> None:
"""Update plot item parameters from object data/metadata
Takes into account a subset of plot item parameters. Those parameters may
have been overriden by object metadata entries or other object data. The goal
is to update the plot item accordingly.
This is *almost* the inverse operation of `update_metadata_from_plot_item`.
Args:
item: plot item
"""
def_dict = Conf.view.get_def_dict(self.__class__.__name__[:3].lower())
self.obj.set_metadata_options_defaults(def_dict, overwrite=False)
# Subclasses have to override this method to update plot item parameters,
# then call this implementation of the method to update plot item.
update_dataset(item.param, self.obj.get_metadata_options())
item.param.update_item(item)
if item.selected:
item.select()
def update_metadata_from_plot_item(self, item: TypePlotItem) -> None:
"""Update metadata from plot item.
Takes into account a subset of plot item parameters. Those parameters may
have been modified by the user through the plot item GUI. The goal is to
update the metadata accordingly.
This is *almost* the inverse operation of `update_plot_item_parameters`.
Args:
item: plot item
"""
def_dict = Conf.view.get_def_dict(self.__class__.__name__[:3].lower())
for key in def_dict:
if hasattr(item.param, key): # In case the PlotPy version is not up-to-date
self.obj.set_metadata_option(key, getattr(item.param, key))
DataLab-1.1.0/datalab/adapters_plotpy/objects/image.py 0000664 0000000 0000000 00000012467 15140141176 0022702 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Image Module
---------------------------
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from __future__ import annotations
import numpy as np
from guidata.dataset import update_dataset
from plotpy.builder import make
from plotpy.items import MaskedXYImageItem
from sigima.objects import ImageObj
from datalab.adapters_plotpy.objects.base import (
BaseObjPlotPyAdapter,
)
from datalab.config import Conf
def get_obj_coords(obj: ImageObj) -> tuple[np.ndarray, np.ndarray]:
"""Get object coordinates
Args:
obj: image object
Returns:
x and y coordinates (pixel centers, not edges)
"""
if obj.is_uniform_coords:
shape = obj.data.shape
# Generate coordinates for pixel centers, not edges
# For N pixels: centers are at x0, x0+dx, x0+2*dx, ..., x0+(N-1)*dx
xcoords = np.linspace(obj.x0, obj.x0 + obj.dx * (shape[1] - 1), shape[1])
ycoords = np.linspace(obj.y0, obj.y0 + obj.dy * (shape[0] - 1), shape[0])
else:
xcoords, ycoords = obj.xcoords, obj.ycoords
return xcoords, ycoords
class ImageObjPlotPyAdapter(BaseObjPlotPyAdapter[ImageObj, MaskedXYImageItem]):
"""Image object plot item adapter class"""
CONF_FMT = Conf.view.ima_format
DEFAULT_FMT = ".1f"
def update_plot_item_parameters(self, item: MaskedXYImageItem) -> None:
"""Update plot item parameters from object data/metadata
Takes into account a subset of plot item parameters. Those parameters may
have been overriden by object metadata entries or other object data. The goal
is to update the plot item accordingly.
This is *almost* the inverse operation of `update_metadata_from_plot_item`.
Args:
item: plot item
"""
o = self.obj
for axis in ("x", "y", "z"):
unit = getattr(o, axis + "unit")
fmt = r"%.1f"
if unit:
fmt = r"%.1f (" + unit + ")"
setattr(item.param, axis + "format", fmt)
item.set_xy(*get_obj_coords(o))
zmin, zmax = item.get_lut_range()
if o.zscalemin is not None or o.zscalemax is not None:
zmin = zmin if o.zscalemin is None else o.zscalemin
zmax = zmax if o.zscalemax is None else o.zscalemax
item.set_lut_range([zmin, zmax])
super().update_plot_item_parameters(item)
def update_metadata_from_plot_item(self, item: MaskedXYImageItem) -> None:
"""Update metadata from plot item.
Takes into account a subset of plot item parameters. Those parameters may
have been modified by the user through the plot item GUI. The goal is to
update the metadata accordingly.
This is *almost* the inverse operation of `update_plot_item_parameters`.
Args:
item: plot item
"""
super().update_metadata_from_plot_item(item)
o = self.obj
# Updating the LUT range:
o.zscalemin, o.zscalemax = item.get_lut_range()
def __viewable_data(self) -> np.ndarray:
"""Return viewable data"""
data = self.obj.data.real
if np.any(np.isnan(data)):
data = np.nan_to_num(data, posinf=0, neginf=0)
return data
def make_item(
self, update_from: MaskedXYImageItem | None = None
) -> MaskedXYImageItem:
"""Make plot item from data.
Args:
update_from: update from plot item
Returns:
Plot item
"""
data = self.__viewable_data()
item = make.maskedxyimage(
*get_obj_coords(self.obj),
data,
self.obj.maskdata,
title=self.obj.title,
colormap="viridis",
eliminate_outliers=Conf.view.ima_eliminate_outliers.get(),
interpolation="nearest",
show_mask=True,
)
if update_from is None:
self.update_plot_item_parameters(item)
else:
update_dataset(item.param, update_from.param)
item.param.update_item(item)
return item
def update_item(self, item: MaskedXYImageItem, data_changed: bool = True) -> None:
"""Update plot item from data.
Args:
item: plot item
data_changed: if True, data has changed
"""
if data_changed:
# When data changes, let set_data() auto-calculate the LUT range from the
# new data (by not passing lut_range parameter). The subsequent call to
# update_plot_item_parameters() will override it if zscalemin/zscalemax
# are explicitly set in the object's metadata.
item.set_data(self.__viewable_data())
item.set_mask(self.obj.maskdata)
item.param.label = self.obj.title
self.update_plot_item_parameters(item)
item.plot().update_colormap_axis(item)
def add_label_with_title(self, title: str | None = None) -> None:
"""Add label with title annotation
Args:
title: title (if None, use image title)
"""
title = self.obj.title if title is None else title
if title:
label = make.label(title, (self.obj.x0, self.obj.y0), (10, 10), "TL")
self.add_annotations_from_items([label])
DataLab-1.1.0/datalab/adapters_plotpy/objects/scalar.py 0000664 0000000 0000000 00000047302 15140141176 0023061 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Scalar Module
----------------------------
This module contains adapters for scalar results (GeometryResult, TableResult)
to avoid circular imports with the base and factories modules.
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from __future__ import annotations
from collections.abc import Iterable
from typing import TYPE_CHECKING, Literal
import guidata.dataset as gds
import numpy as np
from guidata.configtools import get_font
from plotpy.builder import make
from plotpy.items import (
AnnotatedCircle,
AnnotatedEllipse,
AnnotatedPoint,
AnnotatedRectangle,
AnnotatedSegment,
AnnotatedShape,
LabelItem,
Marker,
PolygonShape,
)
from sigima.objects.base import BaseObj
from sigima.objects.scalar import KindShape
from sigima.objects.signal import SignalObj
from sigima.tools import coordinates
from sigima.tools.signal import pulse
from datalab.adapters_metadata import (
GeometryAdapter,
TableAdapter,
resultadapter_to_html,
)
from datalab.adapters_plotpy.base import (
config_annotated_shape,
items_to_json,
json_to_items,
set_plot_item_editable,
)
from datalab.config import PLOTPY_CONF, Conf, _
if TYPE_CHECKING:
from plotpy.styles import ShapeParam
class ResultPlotPyAdapter:
"""Adapter for converting `sigima` table or geometry adapters to PlotPy
Args:
result: Table or geometry adapter
"""
def __init__(self, result_adapter: TableAdapter | GeometryAdapter) -> None:
self.result_adapter = result_adapter
def get_other_items(self, obj: BaseObj) -> list: # pylint: disable=unused-argument
"""Return other items associated to this result (excluding label item)
Those items are not serialized to JSON.
Args:
obj: object (signal/image)
Returns:
List of other items
"""
return []
class GeometryPlotPyAdapter(ResultPlotPyAdapter):
"""Adapter for converting `sigima` geometry adapters to PlotPy
Args:
result: Geometry adapter
Raises:
AssertionError: invalid argument
"""
def __init__(self, result_adapter: GeometryAdapter) -> None:
assert isinstance(result_adapter, GeometryAdapter)
super().__init__(result_adapter)
def iterate_shape_items(
self, fmt: str, lbl: bool, prefix: Literal["s", "i"]
) -> Iterable:
"""Iterate over metadata shape plot items.
Args:
fmt: numeric format (e.g. "%.3f")
lbl: if True, show shape labels
prefix: "s" for signal, "i" for image
Yields:
Plot item
"""
max_shapes = Conf.view.max_shapes_to_draw.get(200)
total_coords = len(self.result_adapter.result.coords)
# Yield shapes up to the maximum limit
for idx, coords in enumerate(self.result_adapter.result.coords):
if idx >= max_shapes:
break
yield self.create_shape_item(coords, fmt, lbl, prefix)
# If shapes were truncated, create a warning label
if total_coords > max_shapes:
warning_text = "⚠ " + _("Only %d out of %d shapes are displayed") % (
max_shapes,
total_coords,
)
warning_label = make.label(warning_text, "BR", (0, 0), "BR")
warning_label.labelparam.font.bold = True
warning_label.labelparam.font.size = 10
warning_label.labelparam.bgalpha = 0.8
warning_label.labelparam.bgcolor = "#ff9800" # Orange background
warning_label.labelparam.textcolor = "#000000" # Black text
warning_label.labelparam.update_item(warning_label)
warning_label.set_readonly(True)
yield warning_label
def create_shape_item(
self, coords: np.ndarray, fmt: str, lbl: bool, prefix: Literal["s", "i"]
) -> (
AnnotatedPoint
| Marker
| AnnotatedRectangle
| AnnotatedCircle
| AnnotatedSegment
| AnnotatedEllipse
| PolygonShape
| None
):
"""Create individual shape item from coordinates
Args:
coords: coordinate array
fmt: numeric format (e.g. "%.3f")
lbl: if True, show shape labels
prefix: "s" for signal, "i" for image
Returns:
Plot item
"""
if self.result_adapter.result.kind == KindShape.POINT:
assert len(coords) == 2, "Coordinates must be a 2-element array"
x0, y0 = coords
item = AnnotatedPoint(x0, y0)
sparam: ShapeParam = item.shape.shapeparam
sparam.symbol.marker = "Ellipse"
sparam.symbol.size = 6
sparam.sel_symbol.marker = "Ellipse"
sparam.sel_symbol.size = 6
aparam = item.annotationparam
aparam.title = self.result_adapter.title
sparam.update_item(item.shape)
aparam.update_item(item)
elif self.result_adapter.result.kind == KindShape.MARKER:
assert len(coords) == 2, "Coordinates must be a 2-element array"
x0, y0 = coords
item = self.__make_marker_item(x0, y0, fmt)
elif self.result_adapter.result.kind == KindShape.RECTANGLE:
assert len(coords) == 4, "Coordinates must be a 4-element array"
x0, y0, dx, dy = coords
item = make.annotated_rectangle(
x0, y0, x0 + dx, y0 + dy, title=self.result_adapter.title
)
elif self.result_adapter.result.kind == KindShape.CIRCLE:
assert len(coords) == 3, "Coordinates must be a 3-element array"
xc, yc, r = coords
x0, y0, x1, y1 = coordinates.circle_to_diameter(xc, yc, r)
item = make.annotated_circle(
x0, y0, x1, y1, title=self.result_adapter.title
)
elif self.result_adapter.result.kind == KindShape.SEGMENT:
assert len(coords) == 4, "Coordinates must be a 4-element array"
x0, y0, x1, y1 = coords
item = make.annotated_segment(
x0, y0, x1, y1, title=self.result_adapter.title
)
elif self.result_adapter.result.kind == KindShape.ELLIPSE:
assert len(coords) == 5, "Coordinates must be a 5-element array"
xc, yc, a, b, t = coords
coords = coordinates.ellipse_to_diameters(xc, yc, a, b, t)
x0, y0, x1, y1, x2, y2, x3, y3 = coords
item = make.annotated_ellipse(
x0, y0, x1, y1, x2, y2, x3, y3, title=self.result_adapter.title
)
elif self.result_adapter.result.kind == KindShape.POLYGON:
assert len(coords) >= 6, "Coordinates must be at least 6-element array"
assert len(coords) % 2 == 0, "Coordinates must be even-length array"
x, y = coords[::2], coords[1::2]
# Filter out NaN coordinates to avoid performance issues
# Polygons are padded with NaNs to create regular arrays, but we only
# need to draw the valid coordinates
valid_mask = ~(np.isnan(x) | np.isnan(y))
x, y = x[valid_mask], y[valid_mask]
item = make.polygon(x, y, title=self.result_adapter.title, closed=False)
else:
raise NotImplementedError(
f"Unsupported shape kind: {self.result_adapter.result.kind}"
)
if isinstance(item, AnnotatedShape):
config_annotated_shape(item, fmt, lbl, "results", f"{prefix}/annotation")
# Apply settings for annotated shapes (except AnnotatedPoint)
if not isinstance(item, AnnotatedPoint):
if prefix == "s":
config_param = Conf.view.sig_shape_param.get()
else:
config_param = Conf.view.ima_shape_param.get()
shape_param: ShapeParam = item.shape.shapeparam
gds.update_dataset(shape_param, config_param)
shape_param.update_item(item.shape)
if isinstance(item, Marker):
item.set_style("results", f"{prefix}/marker")
# Apply cursor/marker settings from config
if prefix == "s":
config_param = Conf.view.sig_marker_param.get()
else:
config_param = Conf.view.ima_marker_param.get()
param = item.markerparam
gds.update_dataset(param, config_param)
param.update_item(item)
set_plot_item_editable(item, False)
return item
def __make_marker_item(self, x0: float, y0: float, fmt: str) -> Marker:
"""Make marker item
Args:
x0: x coordinate
y0: y coordinate
fmt: numeric format (e.g. '%.3f')
"""
if np.isnan(x0):
mstyle = "-"
def label(x, y): # pylint: disable=unused-argument
return (self.result_adapter.title + ": " + fmt) % y
elif np.isnan(y0):
mstyle = "|"
def label(x, y): # pylint: disable=unused-argument
return (self.result_adapter.title + ": " + fmt) % x
else:
mstyle = "+"
txt = self.result_adapter.title + ": (" + fmt + ", " + fmt + ")"
def label(x, y):
return txt % (x, y)
marker = make.marker(position=(x0, y0), markerstyle=mstyle, label_cb=label)
return marker
def create_pulse_segment(
x0: float, y0: float, x1: float, y1: float, label: str
) -> AnnotatedSegment:
"""Create a signal segment item for pulse visualization.
Args:
x0: X-coordinate of the start point
y0: Y-coordinate of the start point
x1: X-coordinate of the end point
y1: Y-coordinate of the end point
label: Label for the segment
Returns:
Annotated segment item styled for pulse visualization
"""
item = make.annotated_segment(x0, y0, x1, y1, label, show_computations=False)
# Configure label appearance similar to Sigima's vistools
item.label.labelparam.bgalpha = 0.5
item.label.labelparam.anchor = "T"
item.label.labelparam.yc = 10
item.label.labelparam.update_item(item.label)
# Configure segment appearance
param = item.shape.shapeparam
param.line.color = "#33ff00" # Green color for baselines/plateaus
param.line.width = 5
param.symbol.facecolor = "#26be00"
param.symbol.edgecolor = "#33ff00"
param.symbol.marker = "Ellipse"
param.symbol.size = 11
param.update_item(item.shape)
# Make non-interactive
item.set_movable(False)
item.set_resizable(False)
item.set_selectable(False)
return item
def create_pulse_crossing_marker(
orientation: Literal["h", "v"], position: float, label: str
) -> Marker:
"""Create a crossing marker for pulse visualization.
Args:
orientation: 'h' for horizontal, 'v' for vertical cursor
position: Position of the cursor along the relevant axis
label: Label for the cursor
Returns:
Marker item styled for crossing visualization
"""
if orientation == "h":
cursor = make.hcursor(position, label=label)
elif orientation == "v":
cursor = make.vcursor(position, label=label)
else:
raise ValueError("Orientation must be 'h' or 'v'")
# Configure appearance similar to Sigima's vistools
cursor.set_movable(False)
cursor.set_selectable(False)
cursor.markerparam.line.color = "#a7ff33" # Light green
cursor.markerparam.line.width = 3
cursor.markerparam.symbol.marker = "NoSymbol"
cursor.markerparam.text.textcolor = "#ffffff"
cursor.markerparam.text.background_color = "#000000"
cursor.markerparam.text.background_alpha = 0.5
cursor.markerparam.text.font.bold = True
cursor.markerparam.update_item(cursor)
return cursor
def are_values_valid(values: list[float | None]) -> bool:
"""Check if all values are valid (not None or nan)
Args:
values: list of values
Returns:
True if all values are valid, False otherwise
"""
for v in values:
if v is None or (isinstance(v, float) and np.isnan(v)):
return False
return True
class TablePlotPyAdapter(ResultPlotPyAdapter):
"""Adapter for converting `sigima` table adapters to PlotPy
Args:
result: Table adapter
Raises:
AssertionError: invalid argument
"""
def __init__(self, result_adapter: TableAdapter) -> None:
assert isinstance(result_adapter, TableAdapter)
super().__init__(result_adapter)
def get_other_items(self, obj: BaseObj) -> list:
"""Return other items associated to this result (excluding label item)
Those items are not serialized to JSON.
Args:
obj: object (signal/image)
Returns:
List of other items
"""
items = []
if self.result_adapter.result.is_pulse_features():
pulse_items = self.create_pulse_visualization_items(obj)
items.extend(pulse_items)
return items
def create_pulse_visualization_items(
self, obj: SignalObj
) -> list[AnnotatedSegment | Marker]:
"""Create pulse visualization items from table data.
Args:
obj: Signal object containing the pulse data
Returns:
List of PlotPy items for pulse visualization
"""
items = []
df = self.result_adapter.to_dataframe()
for _index, row in df.iterrows():
# Start baseline
xs0, xs1 = row["xstartmin"], row["xstartmax"]
ys = pulse.get_range_mean_y(obj.x, obj.y, (xs0, xs1))
if are_values_valid([xs0, xs1, ys]):
items.append(create_pulse_segment(xs0, ys, xs1, ys, "Start baseline"))
# End baseline
xe0, xe1 = row["xendmin"], row["xendmax"]
ye = pulse.get_range_mean_y(obj.x, obj.y, (xe0, xe1))
if are_values_valid([xe0, xe1, ye]):
items.append(create_pulse_segment(xe0, ye, xe1, ye, "End baseline"))
if "xplateaumin" in row and "xplateaumax" in row:
xp0, xp1 = row["xplateaumin"], row["xplateaumax"]
yp = pulse.get_range_mean_y(obj.x, obj.y, (xp0, xp1))
if are_values_valid([xp0, xp1, yp]):
items.append(create_pulse_segment(xp0, yp, xp1, yp, "Plateau"))
for metric in ("x0", "x50", "x100"):
if metric in row:
x = row[metric]
metric_str = metric.replace("x", "x|") + "%"
if are_values_valid([x]):
items.append(create_pulse_crossing_marker("v", x, metric_str))
return items
class MergedResultPlotPyAdapter:
"""Adapter for merging multiple result adapters into a single label.
This adapter manages a merged label that displays all results for a given object.
Instead of creating individual labels for each result (which causes overlapping),
it creates a single label with all results concatenated as HTML.
Args:
result_adapters: List of result adapters (GeometryAdapter or TableAdapter)
obj: Signal or image object associated with the results
"""
def __init__(
self,
result_adapters: list[GeometryAdapter | TableAdapter],
obj: BaseObj,
) -> None:
self.result_adapters = result_adapters
self.obj = obj
self._cached_label: LabelItem | None = None
def get_cached_label(self) -> LabelItem | None:
"""Get the cached merged label item, if it exists.
Returns:
Merged label item, or None if not cached
"""
return self._cached_label
def invalidate_cached_label(self) -> None:
"""Invalidate the cached merged label item."""
self._cached_label = None
@property
def item_json(self) -> str | None:
"""JSON representation of the merged label item.
The position is stored in all result adapters so they stay in sync.
"""
if self.result_adapters:
return self.result_adapters[0].get_applicative_attr("item_json")
return None
@item_json.setter
def item_json(self, value: str | None) -> None:
"""Set JSON representation of the merged label item.
The position is stored in all result adapters to keep them synchronized.
"""
for result_adapter in self.result_adapters:
result_adapter.set_applicative_attr("item_json", value)
def create_merged_label(self) -> LabelItem | None:
"""Create a single merged label from all result adapters.
Returns:
Merged label item, or None if no results
"""
if not self.result_adapters:
return None
# Create the label with merged content
merged_html = resultadapter_to_html(self.result_adapters, self.obj)
item = make.label(merged_html, "TL", (0, 0), "TL", title="Results")
font = get_font(PLOTPY_CONF, "results", "label/font")
item.set_style("results", "label")
item.labelparam.font.update_param(font)
item.labelparam.update_item(item)
# Make label read-only (user cannot delete it to remove individual results)
item.set_readonly(True)
self._cached_label = item
return item
def get_merged_label(self) -> LabelItem | None:
"""Get the merged label, creating it if necessary or updating if it exists.
Returns:
Merged label item, or None if no results
"""
if not self.result_adapters:
self._cached_label = None
return None
# Try to restore existing label from stored JSON position
if self.item_json and self._cached_label is None:
stored_item = json_to_items(self.item_json)[0]
if isinstance(stored_item, LabelItem):
# Update the stored item with current merged content
merged_html = resultadapter_to_html(self.result_adapters, self.obj)
stored_item.set_text(merged_html)
stored_item.set_readonly(True)
self._cached_label = stored_item
return stored_item
# Update existing cached label if present
if self._cached_label is not None:
merged_html = resultadapter_to_html(self.result_adapters, self.obj)
self._cached_label.set_text(merged_html)
return self._cached_label
# Create new label
return self.create_merged_label()
def update_obj_metadata_from_item(self, item: LabelItem) -> None:
"""Update all result adapters' metadata with the label item position.
Args:
item: Merged label item (after user moved it)
"""
if item is not None:
self.item_json = items_to_json([item])
# Update all result adapters in the object's metadata
for result_adapter in self.result_adapters:
result_adapter.add_to(self.obj)
def get_other_items(self) -> list:
"""Return other items from all result adapters (e.g., geometric shapes).
Returns:
List of all other items from all result adapters
"""
items = []
for result_adapter in self.result_adapters:
if isinstance(result_adapter, GeometryAdapter):
plotpy_adapter = GeometryPlotPyAdapter(result_adapter)
elif isinstance(result_adapter, TableAdapter):
plotpy_adapter = TablePlotPyAdapter(result_adapter)
else:
raise NotImplementedError(
f"Unsupported result adapter type: {type(result_adapter)}"
)
items.extend(plotpy_adapter.get_other_items(self.obj))
return items
DataLab-1.1.0/datalab/adapters_plotpy/objects/signal.py 0000664 0000000 0000000 00000022340 15140141176 0023064 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Signal Module
----------------------------
"""
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
from __future__ import annotations
from contextlib import contextmanager
from typing import Generator
import numpy as np
from guidata.dataset import restore_dataset, update_dataset
from plotpy.builder import make
from plotpy.items import CurveItem
from sigima.objects import SignalObj
from datalab.adapters_plotpy.objects.base import (
BaseObjPlotPyAdapter,
)
from datalab.config import Conf
class CurveStyles:
"""Object to manage curve styles"""
#: Curve colors
COLORS = (
"#1f77b4", # muted blue
"#ff7f0e", # safety orange
"#2ca02c", # cooked asparagus green
"#d62728", # brick red
"#9467bd", # muted purple
"#8c564b", # chestnut brown
"#e377c2", # raspberry yogurt pink
"#7f7f7f", # gray
"#bcbd22", # curry yellow-green
"#17becf", # blue-teal
)
#: Curve line styles
LINESTYLES = ("SolidLine", "DashLine", "DashDotLine", "DashDotDotLine")
def __init__(self) -> None:
self.__suspend = False
self.curve_style = self.style_generator()
@staticmethod
def style_generator() -> Generator[tuple[str, str], None, None]:
"""Cycling through curve styles"""
while True:
for linestyle in CurveStyles.LINESTYLES:
for color in CurveStyles.COLORS:
yield (color, linestyle)
def apply_style(self, item: CurveItem) -> None:
"""Apply style to curve
Args:
item: curve item
"""
if self.__suspend:
# Suspend mode: always apply the first style
color, linestyle = CurveStyles.COLORS[0], CurveStyles.LINESTYLES[0]
else:
color, linestyle = next(self.curve_style)
item.param.line.color = color
item.param.line.style = linestyle
item.param.symbol.marker = "NoSymbol"
# Note: line width is set separately via apply_line_width()
# to ensure it's always recalculated based on current data size and settings
def reset_styles(self) -> None:
"""Reset styles"""
self.curve_style = self.style_generator()
@contextmanager
def alternative(
self, other_style_generator: Generator[tuple[str, str], None, None]
) -> Generator[None, None, None]:
"""Use an alternative style generator"""
old_style_generator = self.curve_style
self.curve_style = other_style_generator
yield
self.curve_style = old_style_generator
@contextmanager
def suspend(self) -> Generator[None, None, None]:
"""Suspend style generator"""
self.__suspend = True
yield
self.__suspend = False
CURVESTYLES = CurveStyles() # This is the unique instance of the CurveStyles class
def apply_line_width(item: CurveItem) -> None:
"""Apply line width to curve item with smart clamping for large datasets
Args:
item: curve item
"""
# Get data size
data_size = item.get_data()[0].size
# Get configured line width
line_width = Conf.view.sig_linewidth.get()
# For large datasets, clamp linewidth to 1.0 for performance
# (thick lines cause ~10x rendering slowdown due to Qt raster engine)
threshold = Conf.view.sig_linewidth_perfs_threshold.get()
if data_size > threshold and line_width > 1.0:
line_width = 1.0
# Apply the line width
item.param.line.width = line_width
def apply_downsampling(item: CurveItem, do_not_update: bool = False) -> None:
"""Apply downsampling to curve item
Args:
item: curve item
do_not_update: if True, do not update the item even if the downsampling
parameters have changed
"""
old_use_dsamp = item.param.use_dsamp
item.param.use_dsamp = False
if Conf.view.sig_autodownsampling.get():
nbpoints = item.get_data()[0].size
maxpoints = Conf.view.sig_autodownsampling_maxpoints.get()
if nbpoints > 5 * maxpoints:
item.param.use_dsamp = True
item.param.dsamp_factor = nbpoints // maxpoints
if not do_not_update and old_use_dsamp != item.param.use_dsamp:
item.update_data()
class SignalObjPlotPyAdapter(BaseObjPlotPyAdapter[SignalObj, CurveItem]):
"""Signal object plot item adapter class"""
CONF_FMT = Conf.view.sig_format
DEFAULT_FMT = "g"
def update_plot_item_parameters(self, item: CurveItem) -> None:
"""Update plot item parameters from object data/metadata
Takes into account a subset of plot item parameters. Those parameters may
have been overriden by object metadata entries or other object data. The goal
is to update the plot item accordingly.
This is *almost* the inverse operation of `update_metadata_from_plot_item`.
Args:
item: plot item
"""
update_dataset(item.param.line, self.obj.metadata)
update_dataset(item.param.symbol, self.obj.metadata)
super().update_plot_item_parameters(item)
def update_metadata_from_plot_item(self, item: CurveItem) -> None:
"""Update metadata from plot item.
Takes into account a subset of plot item parameters. Those parameters may
have been modified by the user through the plot item GUI. The goal is to
update the metadata accordingly.
This is *almost* the inverse operation of `update_plot_item_parameters`.
Args:
item: plot item
"""
super().update_metadata_from_plot_item(item)
restore_dataset(item.param.line, self.obj.metadata)
restore_dataset(item.param.symbol, self.obj.metadata)
def make_item(self, update_from: CurveItem | None = None) -> CurveItem:
"""Make plot item from data.
Args:
update_from: plot item to update from
Returns:
Plot item
"""
o = self.obj
if len(o.xydata) in (2, 4):
assert isinstance(o.xydata, np.ndarray)
if len(o.xydata) == 2: # x, y signal
x, y = o.xydata
item = make.mcurve(x.real, y.real, label=o.title)
else: # x, y, dx, dy error bar signal
x, y, dx, dy = o.xydata
if o.dx is None and o.dy is None: # x, y signal with no error
item = make.mcurve(x.real, y.real, label=o.title)
elif o.dx is None: # x, y, dy error bar signal with y error
item = make.merror(x.real, y.real, dy.real, label=o.title)
else: # x, y, dx, dy error bar signal with x error
dy = np.zeros_like(y) if dy is None else dy
item = make.merror(x.real, y.real, dx.real, dy.real, label=o.title)
# Apply style (without linewidth, will be set separately)
CURVESTYLES.apply_style(item)
apply_downsampling(item, do_not_update=True)
# Apply linewidth with smart clamping based on actual data size
apply_line_width(item)
else:
raise RuntimeError("data not supported")
if update_from is None:
self.update_plot_item_parameters(item)
else:
update_dataset(item.param, update_from.param)
item.update_params()
return item
def update_item(self, item: CurveItem, data_changed: bool = True) -> None:
"""Update plot item from data.
Args:
item: plot item
data_changed: if True, data has changed
"""
o = self.obj
if data_changed:
assert isinstance(o.xydata, np.ndarray)
if len(o.xydata) == 2: # x, y signal
x, y = o.xydata
assert isinstance(x, np.ndarray) and isinstance(y, np.ndarray)
item.set_data(x.real, y.real)
elif len(o.xydata) == 3: # x, y, dy error bar signal
x, y, dy = o.xydata
assert (
isinstance(x, np.ndarray)
and isinstance(y, np.ndarray)
and isinstance(dy, np.ndarray)
)
item.set_data(x.real, y.real, dy=dy.real)
elif len(o.xydata) == 4: # x, y, dx, dy error bar signal
x, y, dx, dy = o.xydata
assert (
isinstance(x, np.ndarray)
and isinstance(y, np.ndarray)
and isinstance(dx, np.ndarray)
and isinstance(dy, np.ndarray)
)
item.set_data(x.real, y.real, dx.real, dy.real)
item.param.label = o.title
apply_downsampling(item)
# Reapply linewidth with smart clamping (data size may have changed)
apply_line_width(item)
self.update_plot_item_parameters(item)
def add_label_with_title(self, title: str | None = None) -> None:
"""Add label with title annotation
Args:
title: title (if None, use signal title)
"""
title = self.obj.title if title is None else title
if title:
label = make.label(title, "TL", (0, 0), "TL")
self.add_annotations_from_items([label])
DataLab-1.1.0/datalab/adapters_plotpy/roi/ 0000775 0000000 0000000 00000000000 15140141176 0020374 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/adapters_plotpy/roi/__init__.py 0000664 0000000 0000000 00000000000 15140141176 0022473 0 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/adapters_plotpy/roi/base.py 0000664 0000000 0000000 00000007406 15140141176 0021667 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Base ROI Module
------------------------------
"""
import abc
from typing import TYPE_CHECKING, Generic, Iterator, Literal, TypeVar
from plotpy.items import AnnotatedShape
from sigima.objects.base import (
BaseROI,
TypeObj,
TypeROI,
TypeROIParam,
TypeSingleROI,
get_generic_roi_title,
)
from datalab.adapters_plotpy.base import config_annotated_shape
if TYPE_CHECKING:
from plotpy.items import (
AnnotatedCircle,
AnnotatedPolygon,
AnnotatedRectangle,
AnnotatedXRange,
)
TypeROIItem = TypeVar(
"TypeROIItem",
bound="AnnotatedXRange | AnnotatedPolygon | AnnotatedRectangle | AnnotatedCircle",
)
class BaseSingleROIPlotPyAdapter(Generic[TypeSingleROI, TypeROIItem], abc.ABC):
"""Base class for single ROI plot item adapter
Args:
single_roi: single ROI object
"""
def __init__(self, single_roi: TypeSingleROI) -> None:
self.single_roi = single_roi
@abc.abstractmethod
def to_plot_item(self, obj: TypeObj) -> TypeROIItem:
"""Make ROI plot item from ROI.
Args:
obj: object (signal/image), for physical-indices coordinates conversion
Returns:
Plot item
"""
@classmethod
@abc.abstractmethod
def from_plot_item(cls, item: TypeROIItem) -> TypeSingleROI:
"""Create single ROI from plot item
Args:
item: plot item
Returns:
Single ROI
"""
def configure_roi_item(
item: TypeROIItem,
fmt: str,
lbl: bool,
editable: bool,
option: Literal["s", "i"],
):
"""Configure ROI plot item.
Args:
item: plot item
fmt: numeric format (e.g. "%.3f")
lbl: if True, show shape labels
editable: if True, make shape editable
option: shape style option ("s" for signal, "i" for image)
Returns:
Plot item
"""
option += "/" + ("editable" if editable else "readonly")
if not editable:
if isinstance(item, AnnotatedShape):
config_annotated_shape(
item, fmt, lbl, "roi", option, show_computations=editable
)
item.set_movable(False)
item.set_resizable(False)
item.set_readonly(True)
item.set_style("roi", option)
return item
class BaseROIPlotPyAdapter(Generic[TypeROI], abc.ABC):
"""ROI plot item adapter class
Args:
roi: ROI object
"""
def __init__(self, roi: BaseROI[TypeObj, TypeSingleROI, TypeROIParam]) -> None:
self.roi = roi
@abc.abstractmethod
def to_plot_item(self, single_roi: TypeSingleROI, obj: TypeObj) -> TypeROIItem:
"""Make ROI plot item from single ROI
Args:
single_roi: single ROI object
obj: object (signal/image), for physical-indices coordinates conversion
Returns:
Plot item
"""
def iterate_roi_items(
self, obj: TypeObj, fmt: str, lbl: bool, editable: bool = True
) -> Iterator[TypeROIItem]:
"""Iterate over ROI plot items associated to each single ROI composing
the object.
Args:
obj: object (signal/image), for physical-indices coordinates conversion
fmt: format string
lbl: if True, add label
editable: if True, ROI is editable
Yields:
Plot item
"""
for index, single_roi in enumerate(self.roi.single_rois):
roi_item = self.to_plot_item(single_roi, obj)
item = configure_roi_item(
roi_item, fmt, lbl, editable, option=self.roi.PREFIX
)
item.setTitle(single_roi.title or get_generic_roi_title(index))
yield item
DataLab-1.1.0/datalab/adapters_plotpy/roi/factory.py 0000664 0000000 0000000 00000005377 15140141176 0022431 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
ROI Adapter Factory
-------------------
Factory functions for creating ROI adapters without circular imports.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from sigima.objects.base import TypeObj
def create_roi_adapter(roi):
"""Create ROI adapter from ROI object
Args:
roi: ROI object
Returns:
ROI adapter instance
"""
# pylint: disable=import-outside-toplevel
from sigima.objects import (
CircularROI,
ImageROI,
PolygonalROI,
RectangularROI,
SegmentROI,
SignalROI,
)
if isinstance(roi, SignalROI):
from datalab.adapters_plotpy.roi.signal import SignalROIPlotPyAdapter
return SignalROIPlotPyAdapter(roi)
if isinstance(roi, RectangularROI):
from datalab.adapters_plotpy.roi.image import RectangularROIPlotPyAdapter
return RectangularROIPlotPyAdapter(roi)
if isinstance(roi, CircularROI):
from datalab.adapters_plotpy.roi.image import CircularROIPlotPyAdapter
return CircularROIPlotPyAdapter(roi)
if isinstance(roi, PolygonalROI):
from datalab.adapters_plotpy.roi.image import PolygonalROIPlotPyAdapter
return PolygonalROIPlotPyAdapter(roi)
if isinstance(roi, ImageROI):
from datalab.adapters_plotpy.roi.image import ImageROIPlotPyAdapter
return ImageROIPlotPyAdapter(roi)
if isinstance(roi, SegmentROI):
from datalab.adapters_plotpy.roi.signal import SegmentROIPlotPyAdapter
return SegmentROIPlotPyAdapter(roi)
raise TypeError(f"Unsupported ROI type: {type(roi)}")
def create_single_roi_plot_item(single_roi, obj: TypeObj):
"""Create plot item from single ROI
Args:
single_roi: single ROI object
obj: object (signal/image), for physical-indices coordinates conversion
Returns:
Plot item
"""
# pylint: disable=import-outside-toplevel
from sigima.objects import (
CircularROI,
PolygonalROI,
RectangularROI,
)
if isinstance(single_roi, RectangularROI):
from datalab.adapters_plotpy.roi.image import RectangularROIPlotPyAdapter
return RectangularROIPlotPyAdapter(single_roi).to_plot_item(obj)
if isinstance(single_roi, CircularROI):
from datalab.adapters_plotpy.roi.image import CircularROIPlotPyAdapter
return CircularROIPlotPyAdapter(single_roi).to_plot_item(obj)
if isinstance(single_roi, PolygonalROI):
from datalab.adapters_plotpy.roi.image import PolygonalROIPlotPyAdapter
return PolygonalROIPlotPyAdapter(single_roi).to_plot_item(obj)
raise TypeError(f"Unsupported ROI type: {type(single_roi)}")
DataLab-1.1.0/datalab/adapters_plotpy/roi/image.py 0000664 0000000 0000000 00000017561 15140141176 0022042 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Image ROI Module
-------------------------------
"""
from __future__ import annotations
import numpy as np
from plotpy.builder import make
from plotpy.items import AnnotatedCircle, AnnotatedPolygon, AnnotatedRectangle
from sigima.objects import CircularROI, ImageObj, ImageROI, PolygonalROI, RectangularROI
from sigima.tools import coordinates
from datalab.adapters_plotpy.coordutils import round_image_coords
from datalab.adapters_plotpy.roi.base import (
BaseROIPlotPyAdapter,
BaseSingleROIPlotPyAdapter,
)
def _vs(var: str, sub: str = "") -> str:
"""Return variable name with subscript"""
txt = f"{var}"
if sub:
txt += f"{sub}"
return txt
class PolygonalROIPlotPyAdapter(
BaseSingleROIPlotPyAdapter[PolygonalROI, AnnotatedPolygon]
):
"""Polygonal ROI plot item adapter
Args:
single_roi: single ROI object
"""
def to_plot_item(self, obj: ImageObj) -> AnnotatedPolygon:
"""Make and return the annnotated polygon associated to ROI
Args:
obj: object (image), for physical-indices coordinates conversion
"""
def info_callback(item: AnnotatedPolygon) -> str:
"""Return info string for circular ROI"""
xc, yc = item.get_center()
if self.single_roi.indices:
xc, yc = obj.physical_to_indices([xc, yc])
return " ".join(
[
f"({_vs('x', 'c')}, {_vs('y', 'c')}) = ({xc:g}, {yc:g})",
]
)
coords = np.array(self.single_roi.get_physical_coords(obj))
points = coords.reshape(-1, 2)
item = AnnotatedPolygon(points)
item.set_info_callback(info_callback)
item.annotationparam.title = self.single_roi.title
item.annotationparam.update_item(item)
item.set_style("plot", "shape/drag")
return item
@classmethod
def from_plot_item(
cls, item: AnnotatedPolygon, obj: ImageObj | None = None
) -> PolygonalROI:
"""Create ROI from plot item
Args:
item: plot item
obj: image object for coordinate rounding (optional)
"""
coords = item.get_points().flatten().tolist()
# Round coordinates to appropriate precision
if obj is not None:
coords = round_image_coords(obj, coords)
title = str(item.title().text())
return PolygonalROI(coords, False, title)
class RectangularROIPlotPyAdapter(
BaseSingleROIPlotPyAdapter[RectangularROI, AnnotatedRectangle]
):
"""Rectangular ROI plot item adapter
Args:
single_roi: single ROI object
"""
def to_plot_item(self, obj: ImageObj) -> AnnotatedRectangle:
"""Make and return the annnotated rectangle associated to ROI
Args:
obj: object (image), for physical-indices coordinates conversion
"""
def info_callback(item: AnnotatedRectangle) -> str:
"""Return info string for rectangular ROI"""
x0, y0, x1, y1 = item.get_rect()
if self.single_roi.indices:
x0, y0, x1, y1 = obj.physical_to_indices([x0, y0, x1, y1])
x0, y0, dx, dy = self.single_roi.rect_to_coords(x0, y0, x1, y1)
return " ".join(
[
f"({_vs('x', '0')}, {_vs('y', '0')}) = ({x0:g}, {y0:g})",
f"{_vs('Δx')} × {_vs('Δy')} = {dx:g} × {dy:g}",
]
)
x0, y0, dx, dy = self.single_roi.get_physical_coords(obj)
x1, y1 = x0 + dx, y0 + dy
item: AnnotatedRectangle = make.annotated_rectangle(
x0, y0, x1, y1, title=self.single_roi.title
)
item.set_info_callback(info_callback)
param = item.label.labelparam
param.anchor = "BL"
param.xc, param.yc = 5, -5
param.update_item(item.label)
return item
@classmethod
def from_plot_item(
cls, item: AnnotatedRectangle, obj: ImageObj | None = None
) -> RectangularROI:
"""Create ROI from plot item
Args:
item: plot item
obj: image object for coordinate rounding (optional)
"""
rect = item.get_rect()
coords = RectangularROI.rect_to_coords(*rect)
# Round coordinates to appropriate precision
if obj is not None:
coords = round_image_coords(obj, coords)
title = str(item.title().text())
return RectangularROI(coords, False, title)
class CircularROIPlotPyAdapter(
BaseSingleROIPlotPyAdapter[CircularROI, AnnotatedCircle]
):
"""Circular ROI plot item adapter
Args:
single_roi: single ROI object
"""
def to_plot_item(self, obj: ImageObj) -> AnnotatedCircle:
"""Make and return the annnotated circle associated to ROI
Args:
obj: object (image), for physical-indices coordinates conversion
"""
def info_callback(item: AnnotatedCircle) -> str:
"""Return info string for circular ROI"""
x0, y0, x1, y1 = item.get_rect()
if self.single_roi.indices:
x0, y0, x1, y1 = obj.physical_to_indices([x0, y0, x1, y1])
xc, yc, r = self.single_roi.rect_to_coords(x0, y0, x1, y1)
return " ".join(
[
f"({_vs('x', 'c')}, {_vs('y', 'c')}) = ({xc:g}, {yc:g})",
f"{_vs('r')} = {r:g}",
]
)
xc, yc, r = self.single_roi.get_physical_coords(obj)
x0, y0, x1, y1 = coordinates.circle_to_diameter(xc, yc, r)
item = AnnotatedCircle(x0, y0, x1, y1)
item.set_info_callback(info_callback)
item.annotationparam.title = self.single_roi.title
item.annotationparam.update_item(item)
item.set_style("plot", "shape/drag")
return item
@classmethod
def from_plot_item(
cls, item: AnnotatedCircle, obj: ImageObj | None = None
) -> CircularROI:
"""Create ROI from plot item
Args:
item: plot item
obj: image object for coordinate rounding (optional)
"""
rect = item.get_rect()
coords = CircularROI.rect_to_coords(*rect)
# Round coordinates to appropriate precision
# For circular ROI: [xc, yc, r] - round center (xc, yc) as pair, then radius
if obj is not None:
xc, yc, r = coords
# Round center coordinates
xc_rounded, yc_rounded = round_image_coords(obj, [xc, yc])
# Round radius using average of X and Y precision
# For radius, we use the X precision (could also average X and Y)
r_rounded = round_image_coords(obj, [r, 0])[0]
coords = [xc_rounded, yc_rounded, r_rounded]
title = str(item.title().text())
return CircularROI(coords, False, title)
class ImageROIPlotPyAdapter(BaseROIPlotPyAdapter[ImageROI]):
"""Image ROI plot item adapter class
Args:
roi: ROI object
"""
def to_plot_item(
self,
single_roi: PolygonalROI | RectangularROI | CircularROI,
obj: ImageObj,
) -> AnnotatedCircle | AnnotatedRectangle | AnnotatedPolygon:
"""Make ROI plot item from single ROI
Args:
single_roi: single ROI object
obj: object (signal/image), for physical-indices coordinates conversion
Returns:
Plot item
"""
if isinstance(single_roi, PolygonalROI):
return PolygonalROIPlotPyAdapter(single_roi).to_plot_item(obj)
if isinstance(single_roi, RectangularROI):
return RectangularROIPlotPyAdapter(single_roi).to_plot_item(obj)
if isinstance(single_roi, CircularROI):
return CircularROIPlotPyAdapter(single_roi).to_plot_item(obj)
raise TypeError(f"Invalid ROI type {type(single_roi)}")
DataLab-1.1.0/datalab/adapters_plotpy/roi/signal.py 0000664 0000000 0000000 00000004470 15140141176 0022230 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
PlotPy Adapter Signal ROI Module
--------------------------------
"""
from __future__ import annotations
from plotpy.builder import make
from plotpy.items import AnnotatedXRange
from sigima.objects import SegmentROI, SignalObj, SignalROI
from datalab.adapters_plotpy.coordutils import round_signal_coords
from datalab.adapters_plotpy.roi.base import (
BaseROIPlotPyAdapter,
BaseSingleROIPlotPyAdapter,
)
class SegmentROIPlotPyAdapter(BaseSingleROIPlotPyAdapter[SegmentROI, AnnotatedXRange]):
"""Segment ROI plot item adapter
Args:
coords: ROI coordinates (xmin, xmax)
title: ROI title
"""
def to_plot_item(self, obj: SignalObj) -> AnnotatedXRange:
"""Make and return the annnotated segment associated with the ROI
Args:
obj: object (signal), for physical-indices coordinates conversion
"""
xmin, xmax = self.single_roi.get_physical_coords(obj)
item = make.annotated_xrange(xmin, xmax, title=self.single_roi.title)
return item
@classmethod
def from_plot_item(
cls, item: AnnotatedXRange, obj: SignalObj | None = None
) -> SegmentROI:
"""Create ROI from plot item
Args:
item: plot item
obj: signal object for coordinate rounding (optional)
Returns:
ROI
"""
if not isinstance(item, AnnotatedXRange):
raise TypeError("Invalid plot item type")
coords = sorted(item.get_range())
# Round coordinates to appropriate precision
if obj is not None:
coords = round_signal_coords(obj, coords)
title = str(item.title().text())
return SegmentROI(coords, False, title)
class SignalROIPlotPyAdapter(BaseROIPlotPyAdapter[SignalROI]):
"""Signal ROI plot item adapter class
Args:
roi: ROI object
"""
def to_plot_item(self, single_roi: SegmentROI, obj: SignalObj) -> AnnotatedXRange:
"""Make ROI plot item from single ROI
Args:
single_roi: single ROI object
obj: object (signal/image), for physical-indices coordinates conversion
Returns:
Plot item
"""
return SegmentROIPlotPyAdapter(single_roi).to_plot_item(obj)
DataLab-1.1.0/datalab/app.py 0000664 0000000 0000000 00000005532 15140141176 0015530 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
DataLab launcher module
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from guidata.configtools import get_image_file_path
from qtpy import QtCore as QC
from qtpy import QtGui as QG
from qtpy import QtWidgets as QW
from datalab.config import Conf
from datalab.env import execenv
from datalab.gui.main import DLMainWindow
from datalab.utils.qthelpers import datalab_app_context
if TYPE_CHECKING:
from sigima.objects import ImageObj, SignalObj
def create(
splash: bool = True,
console: bool | None = None,
objects: list[ImageObj | SignalObj] | None = None,
h5files: list[str] | None = None,
size: tuple[int, int] | None = None,
) -> DLMainWindow:
"""Create DataLab application and return mainwindow instance
Args:
splash: if True, show splash screen
console: if True, show console
objects: list of objects to add to the mainwindow
h5files: list of h5files to open
size: mainwindow size (width, height)
Returns:
Main window instance
"""
if splash:
# Showing splash screen
pixmap = QG.QPixmap(get_image_file_path("DataLab-Splash.png"))
splashscreen = QW.QSplashScreen(pixmap, QC.Qt.WindowStaysOnTopHint)
splashscreen.show()
window = DLMainWindow(console=console)
if size is not None:
width, height = size
window.resize(width, height)
if splash:
splashscreen.finish(window)
if Conf.main.window_maximized.get(None):
window.showMaximized()
else:
window.showNormal()
if h5files is not None:
window.open_h5_files(h5files, import_all=True)
if objects is not None:
for obj in objects:
window.add_object(obj)
if execenv.h5browser_file is not None:
window.import_h5_file(execenv.h5browser_file)
return window
def run(
console: bool | None = None,
objects: list[ImageObj | SignalObj] | None = None,
h5files: list[str] | None = None,
size: tuple[int, int] | None = None,
) -> None:
"""Run the DataLab application
Note: this function is an entry point in `setup.py` and therefore
may not be moved without modifying the package setup script.
Args:
console: if True, show console
objects: list of objects to add to the mainwindow
h5files: list of h5files to open
size: mainwindow size (width, height)
"""
if execenv.h5files:
h5files = ([] if h5files is None else h5files) + execenv.h5files
with datalab_app_context(exec_loop=True):
window = create(
splash=True, console=console, objects=objects, h5files=h5files, size=size
)
QW.QApplication.processEvents()
window.execute_post_show_actions()
if __name__ == "__main__":
run()
DataLab-1.1.0/datalab/config.py 0000664 0000000 0000000 00000107615 15140141176 0016222 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
DataLab configuration module
----------------------------
This module handles `DataLab` configuration (options, images and icons).
"""
from __future__ import annotations
import os
import os.path as osp
import sys
from typing import Literal
from guidata import configtools
from plotpy.config import CONF as PLOTPY_CONF
from plotpy.config import MAIN_BG_COLOR, MAIN_FG_COLOR
from plotpy.constants import LUTAlpha
from plotpy.styles import MarkerParam, ShapeParam
from sigima.config import options as sigima_options
from sigima.proc.title_formatting import (
PlaceholderTitleFormatter,
set_default_title_formatter,
)
from datalab import __version__
from datalab.utils import conf
# Configure Sigima to use DataLab-compatible placeholder title formatting
set_default_title_formatter(PlaceholderTitleFormatter())
CONF_VERSION = "1.0.0"
APP_NAME = "DataLab"
MOD_NAME = "datalab"
def get_config_app_name() -> str:
"""Get configuration application name with major version suffix.
This function returns the application name used for configuration storage.
Starting from v1.0, the major version is appended to allow different major
versions to coexist on the same machine without interfering with each other.
Returns:
str: Configuration application name (e.g., "DataLab" for v0.x,
"DataLab_v1" for v1.x)
Examples:
- v0.20.x → "DataLab" (configuration stored in ~/.DataLab)
- v1.0.x → "DataLab_v1" (configuration stored in ~/.DataLab_v1)
- v2.0.x → "DataLab_v2" (configuration stored in ~/.DataLab_v2)
"""
major_version = __version__.split(".", maxsplit=1)[0]
# Keep v0.x configuration folder unchanged for backward compatibility
if major_version == "0":
return APP_NAME
return f"{APP_NAME}_v{major_version}"
_ = configtools.get_translation(MOD_NAME)
APP_DESC = _("""DataLab is a generic signal and image processing platform""")
APP_PATH = osp.dirname(__file__)
DEBUG = os.environ.get("DEBUG", "").lower() in ("1", "true")
if DEBUG:
print("*** DEBUG mode *** [Reset configuration file, do not redirect std I/O]")
TEST_SEGFAULT_ERROR = len(os.environ.get("TEST_SEGFAULT_ERROR", "")) > 0
if TEST_SEGFAULT_ERROR:
print('*** TEST_SEGFAULT_ERROR mode *** [Enabling test action in "?" menu]')
DATETIME_FORMAT = "%d/%m/%Y - %H:%M:%S"
configtools.add_image_module_path(MOD_NAME, osp.join("data", "logo"))
configtools.add_image_module_path(MOD_NAME, osp.join("data", "icons"))
DATAPATH = configtools.get_module_data_path(MOD_NAME, "data")
SHOTPATH = osp.join(
configtools.get_module_data_path(MOD_NAME), os.pardir, "doc", "images", "shots"
)
OTHER_PLUGINS_PATHLIST = [configtools.get_module_data_path(MOD_NAME, "plugins")]
def is_frozen(module_name: str) -> bool:
"""Test if module has been frozen (py2exe/cx_Freeze/pyinstaller)
Args:
module_name (str): module name
Returns:
bool: True if module has been frozen (py2exe/cx_Freeze/pyinstaller)
"""
datapath = configtools.get_module_path(module_name)
parentdir = osp.normpath(osp.join(datapath, osp.pardir))
return not osp.isfile(__file__) or osp.isfile(parentdir) # library.zip
IS_FROZEN = is_frozen(MOD_NAME)
if IS_FROZEN:
OTHER_PLUGINS_PATHLIST.append(osp.join(osp.dirname(sys.executable), "plugins"))
try:
os.mkdir(OTHER_PLUGINS_PATHLIST[-1])
except OSError:
pass
def get_mod_source_dir() -> str | None:
"""Return module source directory
Returns:
str | None: module source directory, or None if not found
"""
if IS_FROZEN:
devdir = osp.abspath(osp.join(sys.prefix, os.pardir, os.pardir))
else:
devdir = osp.abspath(osp.join(osp.dirname(__file__), os.pardir))
if osp.isfile(osp.join(devdir, MOD_NAME, "__init__.py")):
return devdir
# Unhandled case (this should not happen, but just in case):
return None
class MainSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the main configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
color_mode = conf.EnumOption(["auto", "dark", "light"], default="auto")
process_isolation_enabled = conf.Option()
rpc_server_enabled = conf.Option()
rpc_server_port = conf.Option()
webapi_localhost_no_token = conf.Option() # Allow localhost without token
traceback_log_path = conf.ConfigPathOption()
traceback_log_available = conf.Option()
faulthandler_enabled = conf.Option()
faulthandler_log_path = conf.ConfigPathOption()
faulthandler_log_available = conf.Option()
window_maximized = conf.Option()
window_position = conf.Option()
window_size = conf.Option()
window_state = conf.Option()
base_dir = conf.WorkingDirOption()
available_memory_threshold = conf.Option()
current_tab = conf.Option()
plugins_enabled = conf.Option()
plugins_path = conf.Option()
tour_enabled = conf.Option()
v020_plugins_warning_ignore = conf.Option() # True: do not warn, False: warn
class ConsoleSection(conf.Section, metaclass=conf.SectionMeta):
"""Classs defining the console configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
console_enabled = conf.Option()
show_console_on_error = conf.Option()
max_line_count = conf.Option()
external_editor_path = conf.Option()
external_editor_args = conf.Option()
class IOSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the I/O configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
# HDF5 file format options
# ------------------------
# When opening an HDF5 file, ask user for confirmation if the current workspace
# has to be cleared before loading the file:
h5_clear_workspace = conf.Option() # True: clear workspace, False: do not clear
h5_clear_workspace_ask = conf.Option() # True: ask user, False: do not ask
# Signal or image title when importing from HDF5 file:
# - True: use HDF5 full dataset path in signal or image title
# - False: use HDF5 dataset name in signal or image title
h5_fullpath_in_title = conf.Option()
# Signal or image title when importing from HDF5 file:
# - True: add HDF5 file name in signal or image title
# - False: do not add HDF5 file name in signal or image title
h5_fname_in_title = conf.Option()
# ImageIO supported file formats:
imageio_formats = conf.Option()
# Dialog settings persistence (JSON-serialized datasets):
save_to_directory_settings = conf.DataSetOption()
add_metadata_settings = conf.DataSetOption()
class ProcSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the Processing configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
# Operation mode:
# - "single": single operand mode
# - "pairwise": pairwise operation mode
operation_mode = conf.EnumOption(["single", "pairwise"], default="single")
# ROI extraction strategy:
# - True: extract all ROIs in a single signal or image
# - False: extract each ROI in a separate signal or image
extract_roi_singleobj = conf.Option()
# Keep analysis results after processing:
# - True: keep analysis results (dangerous because results may not be valid anymore)
# - False: do not keep analysis results (default)
keep_results = conf.Option()
# Show systematically result dialog after processing:
show_result_dialog = conf.Option()
# Use xmin and xmax bounds from current signal when creating a new signal:
use_signal_bounds = conf.Option()
# Use dimensions from current image when creating a new image:
use_image_dims = conf.Option()
# FFT shift enabled state for signal/image processing:
# - True: FFT shift is enabled (default)
# - False: FFT shift is disabled
fft_shift_enabled = conf.Option()
# Auto-normalize convolution kernel for signal/image processing:
# - True: automatically normalize kernel (default)
# - False: do not normalize kernel
auto_normalize_kernel = conf.Option()
# Ignore warnings during computation:
# - True: ignore warnings
# - False: do not ignore warnings
ignore_warnings = conf.Option()
# X-array compatibility behavior for multi-signal computations:
# - "ask": ask user for confirmation when x-arrays are incompatible (default)
# - "interpolate": automatically interpolate when x-arrays are incompatible
xarray_compat_behavior = conf.EnumOption(["ask", "interpolate"], default="ask")
# History and analysis tabs font
small_mono_font = conf.FontOption()
class ViewSection(conf.Section, metaclass=conf.SectionMeta):
"""Class defining the view configuration section structure.
Each class attribute is an option (metaclass is automatically affecting
option names in .INI file based on class attribute names)."""
# Toolbar position:
# - "top": top
# - "bottom": bottom
# - "left": left
# - "right": right
plot_toolbar_position = conf.Option()
# Ignore information message when inserting object title as annotation label:
ignore_title_insertion_msg = conf.Option()
# String formatting for shape legends
sig_format = conf.Option()
ima_format = conf.Option()
show_label = conf.Option()
auto_refresh = conf.Option()
show_first_only = conf.Option() # Show only first selected item
show_contrast = conf.Option()
sig_linewidth = conf.Option()
sig_linewidth_perfs_threshold = conf.Option()
sig_antialiasing = conf.Option()
sig_autodownsampling = conf.Option()
sig_autodownsampling_maxpoints = conf.Option()
# Autoscale margin settings for plots (percentage values)
sig_autoscale_margin_percent = conf.Option()
ima_autoscale_margin_percent = conf.Option()
# If True, lock aspect ratio of images to 1:1 (ignore physical pixel size)
ima_aspect_ratio_1_1 = conf.Option()
# Default visualization settings at item creation
# (e.g. see adapter's `make_item` methods in datalab/adapters_plotpy/*.py)
ima_eliminate_outliers = conf.Option()
# Default visualization settings, persisted in object metadata
# (e.g. see `BaseDataPanel.update_metadata_view_settings`)
sig_def_shade = conf.Option()
sig_def_curvestyle = conf.Option()
sig_def_baseline = conf.Option()
# ⚠️ Do not add "sig_def_use_dsamp" and "sig_def_dsamp_factor" options here
# because it would not be compatible with the auto-downsampling feature.
# Default visualization settings, persisted in object metadata
# (e.g. see `BaseDataPanel.update_metadata_view_settings`)
ima_def_colormap = conf.Option()
ima_def_invert_colormap = conf.Option()
ima_def_interpolation = conf.Option()
ima_def_alpha = conf.Option()
ima_def_alpha_function = conf.Option()
ima_def_keep_lut_range = conf.Option()
# Annotated shape and marker visualization settings for signals
sig_shape_param = conf.DataSetOption()
sig_marker_param = conf.DataSetOption()
# Annotated shape and marker visualization settings for images
ima_shape_param = conf.DataSetOption()
ima_marker_param = conf.DataSetOption()
# Datetime axis format strings for different time units
# Format strings use Python's strftime format codes
sig_datetime_format_s = conf.Option() # Format for s, min, h
sig_datetime_format_ms = conf.Option() # Format for ms, us, ns
# Maximum number of geometry shapes to draw on plot
# Even if more results are stored, only the first N shapes are drawn
max_shapes_to_draw = conf.Option()
# Maximum number of table cells (rows × columns) to display in merged result
# label on plot. If exceeded, rows are truncated to stay within this limit.
# This prevents slowdown with results that have many columns (e.g., polygons)
max_cells_in_label = conf.Option()
# Maximum number of columns to display in merged result label
# If exceeded, only the first N columns are shown. This ensures readability
# for results with many columns (e.g., polygon coordinates: x0, y0, x1, y1...)
max_cols_in_label = conf.Option()
# Show merged result label on plot by default
show_result_label = conf.Option()
@classmethod
def get_def_dict(cls, category: Literal["ima", "sig"]) -> dict:
"""Get default visualization settings as a dictionary
Args:
category: category ("ima" or "sig", respectively for image and signal)
Returns:
Default visualization settings as a dictionary
"""
assert category in ("ima", "sig")
prefix = f"{category}_def_"
def_dict = {}
for attrname in dir(cls):
if attrname.startswith(prefix):
name = attrname[len(prefix) :]
opt = getattr(cls, attrname)
defval = opt.get(None)
if defval is not None:
def_dict[name] = defval
return def_dict
@classmethod
def set_def_dict(cls, category: Literal["ima", "sig"], def_dict: dict) -> None:
"""Set default visualization settings from a dictionary
Args:
category: category ("ima" or "sig", respectively for image and signal)
def_dict: default visualization settings as a dictionary
"""
assert category in ("ima", "sig")
prefix = f"{category}_def_"
for attrname in dir(cls):
if attrname.startswith(prefix):
name = attrname[len(prefix) :]
opt = getattr(cls, attrname)
if name in def_dict:
opt.set(def_dict[name])
# Usage (example): Conf.console.console_enabled.get(True)
class Conf(conf.Configuration, metaclass=conf.ConfMeta):
"""Class defining DataLab configuration structure.
Each class attribute is a section (metaclass is automatically affecting
section names in .INI file based on class attribute names)."""
main = MainSection()
console = ConsoleSection()
view = ViewSection()
proc = ProcSection()
io = IOSection()
def get_old_log_fname(fname):
"""Return old log fname from current log fname"""
return osp.splitext(fname)[0] + ".1.log"
def initialize():
"""Initialize application configuration"""
config_app_name = get_config_app_name()
Conf.initialize(config_app_name, CONF_VERSION, load=not DEBUG)
# Set default values:
# -------------------
# (do not use "set" method here to avoid overwriting user settings in .INI file)
# Setting here the default values only for the most critical options. The other
# options default values are set when used in the application code.
#
# Main section
Conf.main.color_mode.get("auto")
Conf.main.process_isolation_enabled.get(True)
Conf.main.rpc_server_enabled.get(True)
Conf.main.webapi_localhost_no_token.get(
True
) # Enabled by default (Web API is off by default)
Conf.main.traceback_log_path.get(f".{APP_NAME}_traceback.log")
Conf.main.faulthandler_log_path.get(f".{APP_NAME}_faulthandler.log")
Conf.main.available_memory_threshold.get(500)
Conf.main.plugins_enabled.get(True)
Conf.main.plugins_path.get(Conf.get_path("plugins"))
Conf.main.tour_enabled.get(True)
Conf.main.v020_plugins_warning_ignore.get(False)
# Console section
Conf.console.console_enabled.get(True)
Conf.console.show_console_on_error.get(False)
Conf.console.external_editor_path.get("code")
Conf.console.external_editor_args.get("-g {path}:{line_number}")
# IO section
Conf.io.h5_clear_workspace.get(True) # Default to avoid objects UUID reset
Conf.io.h5_clear_workspace_ask.get(True)
Conf.io.h5_fullpath_in_title.get(False)
Conf.io.h5_fname_in_title.get(True)
iofmts = Conf.io.imageio_formats.get(())
if len(iofmts) > 0:
sigima_options.imageio_formats.set(iofmts) # Sync with sigima config
# Proc section
Conf.proc.operation_mode.get("single")
Conf.proc.use_signal_bounds.get(False)
Conf.proc.use_image_dims.get(True)
Conf.proc.fft_shift_enabled.get(True)
sigima_options.fft_shift_enabled.set(True) # Sync with sigima config
Conf.proc.auto_normalize_kernel.get(False)
sigima_options.auto_normalize_kernel.set(False) # Sync with sigima config
Conf.proc.extract_roi_singleobj.get(False)
Conf.proc.keep_results.get(False)
Conf.proc.show_result_dialog.get(True)
Conf.proc.ignore_warnings.get(False)
Conf.proc.xarray_compat_behavior.get("ask")
Conf.proc.small_mono_font.get((configtools.MONOSPACE, 8, False))
# View section
tb_pos = Conf.view.plot_toolbar_position.get("left")
assert tb_pos in ("top", "bottom", "left", "right")
Conf.view.ignore_title_insertion_msg.get(False)
Conf.view.sig_linewidth.get(1.0)
Conf.view.sig_linewidth_perfs_threshold.get(1000)
Conf.view.sig_autodownsampling.get(True)
Conf.view.sig_autodownsampling_maxpoints.get(100000)
Conf.view.sig_autoscale_margin_percent.get(2.0)
Conf.view.ima_autoscale_margin_percent.get(1.0)
Conf.view.ima_aspect_ratio_1_1.get(False)
Conf.view.ima_eliminate_outliers.get(0.1)
Conf.view.sig_def_shade.get(0.0)
Conf.view.sig_def_curvestyle.get("Lines")
Conf.view.sig_def_baseline.get(0.0)
Conf.view.ima_def_colormap.get("viridis")
Conf.view.ima_def_invert_colormap.get(False)
Conf.view.ima_def_interpolation.get(5)
Conf.view.ima_def_alpha.get(1.0)
Conf.view.ima_def_alpha_function.get(LUTAlpha.NONE.value)
Conf.view.ima_def_keep_lut_range.get(False)
# Datetime format strings: % must be escaped as %% for ConfigParser
Conf.view.sig_datetime_format_s.get("%%H:%%M:%%S")
Conf.view.sig_datetime_format_ms.get("%%H:%%M:%%S.%%f")
Conf.view.max_shapes_to_draw.get(1000)
Conf.view.max_cells_in_label.get(100)
Conf.view.max_cols_in_label.get(15)
Conf.view.show_result_label.get(True)
# Initialize PlotPy configuration with versioned app name
PLOTPY_CONF.set_application(
osp.join(config_app_name, "plotpy"), CONF_VERSION, load=False
)
def reset():
"""Reset application configuration"""
Conf.reset()
initialize()
initialize()
ROI_LINE_COLOR = "#5555ff"
ROI_SEL_LINE_COLOR = "#9393ff"
MARKER_LINE_COLOR = "#A11818"
MARKER_TEXT_COLOR = "#440909"
PLOTPY_DEFAULTS = {
"plot": {
#
# XXX: If needed in the future, add here the default settings for PlotPy:
# that will override the PlotPy settings.
# That is the right way to customize the PlotPy settings for shapes and
# annotations when they are added using tools from the DataLab application
# (see `BaseDataPanel.ANNOTATION_TOOLS`).
# For example, for shapes:
# "shape/drag/line/color": "#00ffff",
#
# Overriding default plot settings from PlotPy
"title/font/size": 11,
"title/font/bold": False,
"selected_curve_symbol/marker": "Ellipse",
"selected_curve_symbol/edgecolor": "#a0a0a4",
"selected_curve_symbol/facecolor": MAIN_FG_COLOR,
"selected_curve_symbol/alpha": 0.3,
"selected_curve_symbol/size": 5,
"marker/curve/text/textcolor": "black",
# Cross marker style (shown when pressing Alt key on plot)
"marker/cross/symbol/marker": "Cross",
"marker/cross/symbol/edgecolor": MAIN_FG_COLOR,
"marker/cross/symbol/facecolor": "#ff0000",
"marker/cross/symbol/alpha": 1.0,
"marker/cross/symbol/size": 8,
"marker/cross/text/font/family": "default",
"marker/cross/text/font/size": 8,
"marker/cross/text/font/bold": False,
"marker/cross/text/font/italic": False,
"marker/cross/text/textcolor": "#000000",
"marker/cross/text/background_color": "#ffffff",
"marker/cross/text/background_alpha": 0.7,
"marker/cross/line/style": "DashLine",
"marker/cross/line/color": MARKER_LINE_COLOR,
"marker/cross/line/width": 1.0,
"marker/cross/markerstyle": "Cross",
"marker/cross/spacing": 7,
# Cursor line and symbol style
"marker/cursor/line/style": "SolidLine",
"marker/cursor/line/color": MARKER_LINE_COLOR,
"marker/cursor/line/width": 1.0,
"marker/cursor/symbol/marker": "NoSymbol",
"marker/cursor/symbol/size": 11,
"marker/cursor/symbol/edgecolor": MAIN_BG_COLOR,
"marker/cursor/symbol/facecolor": "#ff9393",
"marker/cursor/symbol/alpha": 1.0,
"marker/cursor/sel_line/style": "SolidLine",
"marker/cursor/sel_line/color": MARKER_LINE_COLOR,
"marker/cursor/sel_line/width": 2.0,
"marker/cursor/sel_symbol/marker": "NoSymbol",
"marker/cursor/sel_symbol/size": 11,
"marker/cursor/sel_symbol/edgecolor": MAIN_BG_COLOR,
"marker/cursor/sel_symbol/facecolor": MARKER_LINE_COLOR,
"marker/cursor/sel_symbol/alpha": 0.8,
"marker/cursor/text/font/size": 9,
"marker/cursor/text/font/family": "default",
"marker/cursor/text/font/bold": False,
"marker/cursor/text/font/italic": False,
"marker/cursor/text/textcolor": MARKER_TEXT_COLOR,
"marker/cursor/text/background_color": "#ffffff",
"marker/cursor/text/background_alpha": 0.7,
"marker/cursor/sel_text/font/size": 9,
"marker/cursor/sel_text/font/family": "default",
"marker/cursor/sel_text/font/bold": False,
"marker/cursor/sel_text/font/italic": False,
"marker/cursor/sel_text/textcolor": MARKER_TEXT_COLOR,
"marker/cursor/sel_text/background_color": "#ffffff",
"marker/cursor/sel_text/background_alpha": 0.7,
# Default annotation text style for segments:
"shape/segment/line/style": "SolidLine",
"shape/segment/line/color": "#00ff55",
"shape/segment/line/width": 1.0,
"shape/segment/sel_line/style": "SolidLine",
"shape/segment/sel_line/color": "#00ff55",
"shape/segment/sel_line/width": 2.0,
"shape/segment/fill/style": "NoBrush",
"shape/segment/sel_fill/style": "NoBrush",
"shape/segment/symbol/marker": "XCross",
"shape/segment/symbol/size": 9,
"shape/segment/symbol/edgecolor": "#00ff55",
"shape/segment/symbol/facecolor": "#00ff55",
"shape/segment/symbol/alpha": 1.0,
"shape/segment/sel_symbol/marker": "XCross",
"shape/segment/sel_symbol/size": 12,
"shape/segment/sel_symbol/edgecolor": "#00ff55",
"shape/segment/sel_symbol/facecolor": "#00ff55",
"shape/segment/sel_symbol/alpha": 0.7,
# Default style for drag shapes: (global annotations style)
"shape/drag/line/style": "SolidLine",
"shape/drag/line/color": "#00ff55",
"shape/drag/line/width": 1.0,
"shape/drag/fill/style": "SolidPattern",
"shape/drag/fill/color": MAIN_BG_COLOR,
"shape/drag/fill/alpha": 0.1,
"shape/drag/symbol/marker": "Rect",
"shape/drag/symbol/size": 3,
"shape/drag/symbol/edgecolor": "#00ff55",
"shape/drag/symbol/facecolor": "#00ff55",
"shape/drag/symbol/alpha": 1.0,
"shape/drag/sel_line/style": "SolidLine",
"shape/drag/sel_line/color": "#00ff55",
"shape/drag/sel_line/width": 2.0,
"shape/drag/sel_fill/style": "SolidPattern",
"shape/drag/sel_fill/color": MAIN_BG_COLOR,
"shape/drag/sel_fill/alpha": 0.1,
"shape/drag/sel_symbol/marker": "Rect",
"shape/drag/sel_symbol/size": 7,
"shape/drag/sel_symbol/edgecolor": "#00ff55",
"shape/drag/sel_symbol/facecolor": "#00ff00",
"shape/drag/sel_symbol/alpha": 0.7,
},
"results": {
# Annotated shape style for result shapes:
# Signals:
"s/annotation/line/style": "SolidLine",
"s/annotation/line/color": "#00aa00",
"s/annotation/line/width": 2,
"s/annotation/fill/style": "NoBrush",
"s/annotation/fill/color": MAIN_BG_COLOR,
"s/annotation/fill/alpha": 0.1,
"s/annotation/symbol/marker": "XCross",
"s/annotation/symbol/size": 7,
"s/annotation/symbol/edgecolor": "#00aa00",
"s/annotation/symbol/facecolor": "#00aa00",
"s/annotation/symbol/alpha": 1.0,
"s/annotation/sel_line/style": "DashLine",
"s/annotation/sel_line/color": "#00ff00",
"s/annotation/sel_line/width": 1,
"s/annotation/sel_fill/style": "SolidPattern",
"s/annotation/sel_fill/color": MAIN_BG_COLOR,
"s/annotation/sel_fill/alpha": 0.1,
"s/annotation/sel_symbol/marker": "Rect",
"s/annotation/sel_symbol/size": 9,
"s/annotation/sel_symbol/edgecolor": "#00aa00",
"s/annotation/sel_symbol/facecolor": "#00ff00",
"s/annotation/sel_symbol/alpha": 0.7,
# Images:
"i/annotation/line/style": "SolidLine",
"i/annotation/line/color": "#ffff00",
"i/annotation/line/width": 2,
"i/annotation/fill/style": "SolidPattern",
"i/annotation/fill/color": MAIN_BG_COLOR,
"i/annotation/fill/alpha": 0.1,
"i/annotation/symbol/marker": "Rect",
"i/annotation/symbol/size": 3,
"i/annotation/symbol/edgecolor": "#ffff00",
"i/annotation/symbol/facecolor": "#ffff00",
"i/annotation/symbol/alpha": 1.0,
"i/annotation/sel_line/style": "SolidLine",
"i/annotation/sel_line/color": "#00ff00",
"i/annotation/sel_line/width": 2,
"i/annotation/sel_fill/style": "SolidPattern",
"i/annotation/sel_fill/color": MAIN_BG_COLOR,
"i/annotation/sel_fill/alpha": 0.1,
"i/annotation/sel_symbol/marker": "Rect",
"i/annotation/sel_symbol/size": 9,
"i/annotation/sel_symbol/edgecolor": "#00aa00",
"i/annotation/sel_symbol/facecolor": "#00ff00",
"i/annotation/sel_symbol/alpha": 0.7,
# Marker styles for results:
# Signals:
"s/marker/cursor/line/style": "DashLine",
"s/marker/cursor/line/color": MARKER_LINE_COLOR,
"s/marker/cursor/line/width": 1.0,
"s/marker/cursor/symbol/marker": "Ellipse",
"s/marker/cursor/symbol/size": 11,
"s/marker/cursor/symbol/edgecolor": MAIN_BG_COLOR,
"s/marker/cursor/symbol/facecolor": MARKER_LINE_COLOR,
"s/marker/cursor/symbol/alpha": 0.7,
"s/marker/cursor/sel_line/style": "DashLine",
"s/marker/cursor/sel_line/color": MARKER_LINE_COLOR,
"s/marker/cursor/sel_line/width": 2.0,
"s/marker/cursor/sel_symbol/marker": "Ellipse",
"s/marker/cursor/sel_symbol/size": 11,
"s/marker/cursor/sel_symbol/edgecolor": MARKER_LINE_COLOR,
"s/marker/cursor/sel_symbol/facecolor": MARKER_LINE_COLOR,
"s/marker/cursor/sel_symbol/alpha": 0.7,
"s/marker/cursor/text/font/size": 9,
"s/marker/cursor/text/font/family": "default",
"s/marker/cursor/text/font/bold": False,
"s/marker/cursor/text/font/italic": False,
"s/marker/cursor/text/textcolor": MARKER_TEXT_COLOR,
"s/marker/cursor/text/background_color": "#ffffff",
"s/marker/cursor/text/background_alpha": 0.7,
"s/marker/cursor/sel_text/font/size": 9,
"s/marker/cursor/sel_text/font/family": "default",
"s/marker/cursor/sel_text/font/bold": False,
"s/marker/cursor/sel_text/font/italic": False,
"s/marker/cursor/sel_text/textcolor": MARKER_TEXT_COLOR,
"s/marker/cursor/sel_text/background_color": "#ffffff",
"s/marker/cursor/sel_text/background_alpha": 0.7,
"s/marker/cursor/markerstyle": "Cross",
# Images:
"i/marker/cursor/line/style": "DashLine",
"i/marker/cursor/line/color": MARKER_LINE_COLOR,
"i/marker/cursor/line/width": 1.0,
"i/marker/cursor/symbol/marker": "Diamond",
"i/marker/cursor/symbol/size": 11,
"i/marker/cursor/symbol/edgecolor": MARKER_LINE_COLOR,
"i/marker/cursor/symbol/facecolor": MARKER_LINE_COLOR,
"i/marker/cursor/symbol/alpha": 0.7,
"i/marker/cursor/sel_line/style": "DashLine",
"i/marker/cursor/sel_line/color": MARKER_LINE_COLOR,
"i/marker/cursor/sel_line/width": 2.0,
"i/marker/cursor/sel_symbol/marker": "Diamond",
"i/marker/cursor/sel_symbol/size": 11,
"i/marker/cursor/sel_symbol/edgecolor": MARKER_LINE_COLOR,
"i/marker/cursor/sel_symbol/facecolor": MARKER_LINE_COLOR,
"i/marker/cursor/sel_symbol/alpha": 0.7,
"i/marker/cursor/text/font/size": 9,
"i/marker/cursor/text/font/family": "default",
"i/marker/cursor/text/font/bold": False,
"i/marker/cursor/text/font/italic": False,
"i/marker/cursor/text/textcolor": MARKER_TEXT_COLOR,
"i/marker/cursor/text/background_color": "#ffffff",
"i/marker/cursor/text/background_alpha": 0.7,
"i/marker/cursor/sel_text/font/size": 9,
"i/marker/cursor/sel_text/font/family": "default",
"i/marker/cursor/sel_text/font/bold": False,
"i/marker/cursor/sel_text/font/italic": False,
"i/marker/cursor/sel_text/textcolor": MARKER_TEXT_COLOR,
"i/marker/cursor/sel_text/background_color": "#ffffff",
"i/marker/cursor/sel_text/background_alpha": 0.7,
"i/marker/cursor/markerstyle": "Cross",
# Style for labels:
"label/symbol/marker": "NoSymbol",
"label/symbol/size": 0,
"label/symbol/edgecolor": MAIN_BG_COLOR,
"label/symbol/facecolor": MAIN_BG_COLOR,
"label/border/style": "SolidLine",
"label/border/color": "#cbcbcb",
"label/border/width": 1,
"label/font/size": 8,
"label/font/family/nt": ["Cascadia Code", "Consolas", "Courier New"],
"label/font/family/posix": "Bitstream Vera Sans Mono",
"label/font/family/mac": "Monaco",
"label/font/bold": False,
"label/font/italic": False,
"label/color": MAIN_FG_COLOR,
"label/bgcolor": MAIN_BG_COLOR,
"label/bgalpha": 0.8,
"label/anchor": "TL",
"label/xc": 10,
"label/yc": 10,
"label/abspos": True,
"label/absg": "TL",
"label/xg": 0.0,
"label/yg": 0.0,
},
"roi": { # Shape style for ROI
# Signals:
# - Editable ROI (ROI editor):
"s/editable/fill": "#ffff00",
"s/editable/shade": 0.10,
"s/editable/line/style": "SolidLine",
"s/editable/line/color": "#ffff00",
"s/editable/line/width": 1,
"s/editable/fill/style": "SolidPattern",
"s/editable/fill/color": MAIN_BG_COLOR,
"s/editable/fill/alpha": 0.1,
"s/editable/symbol/marker": "Rect",
"s/editable/symbol/size": 3,
"s/editable/symbol/edgecolor": "#ffff00",
"s/editable/symbol/facecolor": "#ffff00",
"s/editable/symbol/alpha": 1.0,
"s/editable/sel_line/style": "SolidLine",
"s/editable/sel_line/color": "#00ff00",
"s/editable/sel_line/width": 1,
"s/editable/sel_fill/style": "SolidPattern",
"s/editable/sel_fill/color": MAIN_BG_COLOR,
"s/editable/sel_fill/alpha": 0.1,
"s/editable/sel_symbol/marker": "Rect",
"s/editable/sel_symbol/size": 9,
"s/editable/sel_symbol/edgecolor": "#00aa00",
"s/editable/sel_symbol/facecolor": "#00ff00",
"s/editable/sel_symbol/alpha": 0.7,
# - Readonly ROI (plot):
"s/readonly/line/style": "SolidLine",
"s/readonly/line/color": ROI_LINE_COLOR,
"s/readonly/line/width": 1,
"s/readonly/sel_line/style": "SolidLine",
"s/readonly/sel_line/color": ROI_SEL_LINE_COLOR,
"s/readonly/sel_line/width": 2,
"s/readonly/fill": ROI_LINE_COLOR,
"s/readonly/shade": 0.10,
"s/readonly/symbol/marker": "Ellipse",
"s/readonly/symbol/size": 7,
"s/readonly/symbol/edgecolor": MAIN_BG_COLOR,
"s/readonly/symbol/facecolor": ROI_LINE_COLOR,
"s/readonly/symbol/alpha": 1.0,
"s/readonly/sel_symbol/marker": "Ellipse",
"s/readonly/sel_symbol/size": 9,
"s/readonly/sel_symbol/edgecolor": MAIN_BG_COLOR,
"s/readonly/sel_symbol/facecolor": ROI_SEL_LINE_COLOR,
"s/readonly/sel_symbol/alpha": 0.9,
"s/readonly/multi/color": "#806060",
# Images:
# - Editable ROI (ROI editor):
"i/editable/line/style": "SolidLine",
"i/editable/line/color": "#ffff00",
"i/editable/line/width": 1,
"i/editable/fill/style": "SolidPattern",
"i/editable/fill/color": MAIN_BG_COLOR,
"i/editable/fill/alpha": 0.1,
"i/editable/symbol/marker": "Rect",
"i/editable/symbol/size": 3,
"i/editable/symbol/edgecolor": "#ffff00",
"i/editable/symbol/facecolor": "#ffff00",
"i/editable/symbol/alpha": 1.0,
"i/editable/sel_line/style": "SolidLine",
"i/editable/sel_line/color": "#00ff00",
"i/editable/sel_line/width": 1,
"i/editable/sel_fill/style": "SolidPattern",
"i/editable/sel_fill/color": MAIN_BG_COLOR,
"i/editable/sel_fill/alpha": 0.1,
"i/editable/sel_symbol/marker": "Rect",
"i/editable/sel_symbol/size": 9,
"i/editable/sel_symbol/edgecolor": "#00aa00",
"i/editable/sel_symbol/facecolor": "#00ff00",
"i/editable/sel_symbol/alpha": 0.7,
# - Readonly ROI (plot):
"i/readonly/line/style": "DotLine",
"i/readonly/line/color": ROI_LINE_COLOR,
"i/readonly/line/width": 1,
"i/readonly/fill/style": "SolidPattern",
"i/readonly/fill/color": MAIN_BG_COLOR,
"i/readonly/fill/alpha": 0.1,
"i/readonly/symbol/marker": "NoSymbol",
"i/readonly/symbol/size": 5,
"i/readonly/symbol/edgecolor": ROI_LINE_COLOR,
"i/readonly/symbol/facecolor": ROI_LINE_COLOR,
"i/readonly/symbol/alpha": 0.6,
"i/readonly/sel_line/style": "DotLine",
"i/readonly/sel_line/color": "#0000ff",
"i/readonly/sel_line/width": 1,
"i/readonly/sel_fill/style": "SolidPattern",
"i/readonly/sel_fill/color": MAIN_BG_COLOR,
"i/readonly/sel_fill/alpha": 0.1,
"i/readonly/sel_symbol/marker": "Rect",
"i/readonly/sel_symbol/size": 8,
"i/readonly/sel_symbol/edgecolor": "#0000aa",
"i/readonly/sel_symbol/facecolor": "#0000ff",
"i/readonly/sel_symbol/alpha": 0.7,
},
}
# PlotPy configuration will be initialized in initialize() function
PLOTPY_CONF.update_defaults(PLOTPY_DEFAULTS)
class DataLabShapeParam(ShapeParam):
"""ShapeParam subclass with internal items hidden from settings dialog"""
def __init__(self):
super().__init__()
# Hide internal items that should not appear in settings dialog
for item in self._items:
if item._name in ("label", "readonly", "private"):
item.set_prop("display", hide=True)
def initialize_default_plotpy_instances():
"""Initialize default PlotPy instances for DataLab configuration options"""
# Initialize default instances for DataSetOptions now that PLOTPY_DEFAULTS exists
_sig_shapeparam = DataLabShapeParam()
_sig_shapeparam.read_config(PLOTPY_CONF, "results", "s/annotation")
Conf.view.sig_shape_param.set_default_instance(_sig_shapeparam)
Conf.view.sig_shape_param.get()
_sig_markerparam = MarkerParam()
_sig_markerparam.read_config(PLOTPY_CONF, "results", "s/marker/cursor")
Conf.view.sig_marker_param.set_default_instance(_sig_markerparam)
Conf.view.sig_marker_param.get()
_ima_shapeparam = DataLabShapeParam()
_ima_shapeparam.read_config(PLOTPY_CONF, "results", "i/annotation")
Conf.view.ima_shape_param.set_default_instance(_ima_shapeparam)
Conf.view.ima_shape_param.get()
_ima_markerparam = MarkerParam()
_ima_markerparam.read_config(PLOTPY_CONF, "results", "i/marker/cursor")
Conf.view.ima_marker_param.set_default_instance(_ima_markerparam)
Conf.view.ima_marker_param.get()
initialize_default_plotpy_instances()
DataLab-1.1.0/datalab/control/ 0000775 0000000 0000000 00000000000 15140141176 0016051 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/control/__init__.py 0000664 0000000 0000000 00000000000 15140141176 0020150 0 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/control/baseproxy.py 0000664 0000000 0000000 00000076342 15140141176 0020453 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
DataLab base proxy module
-------------------------
"""
# How to add a new method to the proxy:
# -------------------------------------
#
# 1. Add the method to the AbstractDLControl class, as an abstract method
#
# 2a. If the method requires any data conversion to get through the XML-RPC layer,
# implement the method in both LocalProxy and RemoteClient classes
#
# 2b. If the method does not require any data conversion, implement the method
# directly in the BaseProxy class, so that it is available to both LocalProxy
# and RemoteClient classes without any code duplication
#
# 3. Implement the method in the DLMainWindow class
#
# 4. Implement the method in the RemoteServer class (it will be automatically
# registered as an XML-RPC method, like all methods of AbstractDLControl)
from __future__ import annotations
import abc
from contextlib import contextmanager
from typing import TYPE_CHECKING, Generator
import guidata.dataset as gds
import numpy as np
from sigima import ImageObj, SignalObj
if TYPE_CHECKING:
from collections.abc import Iterator
from datalab.control.remote import ServerProxy
from datalab.gui.main import DLMainWindow
class AbstractDLControl(abc.ABC):
"""Abstract base class for controlling DataLab (main window or remote server)"""
def __len__(self) -> int:
"""Return number of objects"""
return len(self.get_object_uuids())
def __getitem__(self, nb_id_title: int | str | None = None) -> SignalObj | ImageObj:
"""Return object"""
return self.get_object(nb_id_title)
def __iter__(self) -> Iterator[SignalObj | ImageObj]:
"""Iterate over objects"""
uuids = self.get_object_uuids()
for uuid in uuids:
yield self.get_object(uuid)
def __str__(self) -> str:
"""Return object string representation"""
return super().__repr__()
def __repr__(self) -> str:
"""Return object representation"""
titles = self.get_object_titles()
uuids = self.get_object_uuids()
text = f"{str(self)} (DataLab, {len(titles)} items):\n"
for uuid, title in zip(uuids, titles):
text += f" {uuid}: {title}\n"
return text
def __bool__(self) -> bool:
"""Return True if model is not empty"""
return bool(self.get_object_uuids())
def __contains__(self, id_title: str) -> bool:
"""Return True if object (UUID or title) is in model"""
return id_title in (self.get_object_titles() + self.get_object_uuids())
@classmethod
def get_public_methods(cls) -> list[str]:
"""Return all public methods of the class, except itself.
Returns:
List of public methods
"""
return [
method
for method in dir(cls)
if not method.startswith(("_", "context_"))
and method != "get_public_methods"
]
@abc.abstractmethod
def get_version(self) -> str:
"""Return DataLab public version.
Returns:
DataLab version
"""
@abc.abstractmethod
def close_application(self) -> None:
"""Close DataLab application"""
@abc.abstractmethod
def raise_window(self) -> None:
"""Raise DataLab window"""
@abc.abstractmethod
def get_current_panel(self) -> str:
"""Return current panel name.
Returns:
Panel name (valid values: "signal", "image", "macro"))
"""
@abc.abstractmethod
def set_current_panel(self, panel: str) -> None:
"""Switch to panel.
Args:
panel: Panel name (valid values: "signal", "image", "macro"))
"""
@abc.abstractmethod
def reset_all(self) -> None:
"""Reset all application data"""
@abc.abstractmethod
def remove_object(self, force: bool = False) -> None:
"""Remove current object from current panel.
Args:
force: if True, remove object without confirmation. Defaults to False.
"""
@abc.abstractmethod
def toggle_auto_refresh(self, state: bool) -> None:
"""Toggle auto refresh state.
Args:
state: Auto refresh state
"""
# Returns a context manager to temporarily disable autorefresh
@contextmanager
def context_no_refresh(self) -> Generator[None, None, None]:
"""Return a context manager to temporarily disable auto refresh.
Returns:
Context manager
Example:
>>> with proxy.context_no_refresh():
... proxy.add_image("image1", data1)
... proxy.calc("fft")
... proxy.calc("wiener")
... proxy.calc("ifft")
... # Auto refresh is disabled during the above operations
"""
self.toggle_auto_refresh(False)
try:
yield
finally:
self.toggle_auto_refresh(True)
@abc.abstractmethod
def toggle_show_titles(self, state: bool) -> None:
"""Toggle show titles state.
Args:
state: Show titles state
"""
@abc.abstractmethod
def save_to_h5_file(self, filename: str) -> None:
"""Save to a DataLab HDF5 file.
Args:
filename: HDF5 file name
"""
@abc.abstractmethod
def open_h5_files(
self,
h5files: list[str] | None = None,
import_all: bool | None = None,
reset_all: bool | None = None,
) -> None:
"""Open a DataLab HDF5 file or import from any other HDF5 file.
Args:
h5files: List of HDF5 files to open. Defaults to None.
import_all: Import all objects from HDF5 files. Defaults to None.
reset_all: Reset all application data. Defaults to None.
"""
@abc.abstractmethod
def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
"""Open DataLab HDF5 browser to Import HDF5 file.
Args:
filename: HDF5 file name
reset_all: Reset all application data. Defaults to None.
"""
@abc.abstractmethod
def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
"""Load native DataLab HDF5 workspace files without any GUI elements.
This method can be safely called from scripts (e.g., internal console,
macros) as it does not create any Qt widgets, dialogs, or progress bars.
.. warning::
This method only supports native DataLab HDF5 files. For importing
arbitrary HDF5 files (non-native), use :meth:`open_h5_files` or
:meth:`import_h5_file` instead.
Args:
h5files: List of native DataLab HDF5 filenames
reset_all: Reset all application data before importing. Defaults to False.
Raises:
ValueError: If a file is not a valid native DataLab HDF5 file
"""
@abc.abstractmethod
def save_h5_workspace(self, filename: str) -> None:
"""Save current workspace to a native DataLab HDF5 file without GUI elements.
This method can be safely called from scripts (e.g., internal console,
macros) as it does not create any Qt widgets, dialogs, or progress bars.
Args:
filename: HDF5 filename to save to
Raises:
IOError: If file cannot be saved
"""
@abc.abstractmethod
def load_from_files(self, filenames: list[str]) -> None:
"""Open objects from files in current panel (signals/images).
Args:
filenames: list of file names
"""
@abc.abstractmethod
def load_from_directory(self, path: str) -> None:
"""Open objects from directory in current panel (signals/images).
Args:
path: directory path
"""
@abc.abstractmethod
def add_signal(
self,
title: str,
xdata: np.ndarray,
ydata: np.ndarray,
xunit: str = "",
yunit: str = "",
xlabel: str = "",
ylabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add signal data to DataLab.
Args:
title: Signal title
xdata: X data
ydata: Y data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
group_id: group id in which to add the signal. Defaults to ""
set_current: if True, set the added signal as current
Returns:
True if signal was added successfully, False otherwise
Raises:
ValueError: Invalid xdata dtype
ValueError: Invalid ydata dtype
"""
@abc.abstractmethod
def add_image(
self,
title: str,
data: np.ndarray,
xunit: str = "",
yunit: str = "",
zunit: str = "",
xlabel: str = "",
ylabel: str = "",
zlabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add image data to DataLab.
Args:
title: Image title
data: Image data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
zunit: Z unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
zlabel: Z label. Defaults to ""
group_id: group id in which to add the image. Defaults to ""
set_current: if True, set the added image as current
Returns:
True if image was added successfully, False otherwise
Raises:
ValueError: Invalid data dtype
"""
@abc.abstractmethod
def add_object(
self, obj: SignalObj | ImageObj, group_id: str = "", set_current: bool = True
) -> None:
"""Add object to DataLab.
Args:
obj: Signal or image object
group_id: group id in which to add the object. Defaults to ""
set_current: if True, set the added object as current
Returns:
True if object was added successfully, False otherwise
"""
@abc.abstractmethod
def add_group(
self, title: str, panel: str | None = None, select: bool = False
) -> None:
"""Add group to DataLab.
Args:
title: Group title
panel: Panel name (valid values: "signal", "image"). Defaults to None.
select: Select the group after creation. Defaults to False.
"""
@abc.abstractmethod
def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]:
"""Return selected objects uuids.
Args:
include_groups: If True, also return objects from selected groups.
Returns:
List of selected objects uuids.
"""
@abc.abstractmethod
def select_objects(
self,
selection: list[int | str],
panel: str | None = None,
) -> None:
"""Select objects in current panel.
Args:
selection: List of object numbers (1 to N) or uuids to select
panel: panel name (valid values: "signal", "image").
If None, current panel is used. Defaults to None.
"""
@abc.abstractmethod
def select_groups(
self, selection: list[int | str] | None = None, panel: str | None = None
) -> None:
"""Select groups in current panel.
Args:
selection: List of group numbers (1 to N), or list of group uuids,
or None to select all groups. Defaults to None.
panel: panel name (valid values: "signal", "image").
If None, current panel is used. Defaults to None.
"""
@abc.abstractmethod
def delete_metadata(
self, refresh_plot: bool = True, keep_roi: bool = False
) -> None:
"""Delete metadata of selected objects
Args:
refresh_plot: Refresh plot. Defaults to True.
keep_roi: Keep ROI. Defaults to False.
"""
@abc.abstractmethod
def get_group_titles_with_object_info(
self,
) -> tuple[list[str], list[list[str]], list[list[str]]]:
"""Return groups titles and lists of inner objects uuids and titles.
Returns:
Groups titles, lists of inner objects uuids and titles
"""
@abc.abstractmethod
def get_object_titles(self, panel: str | None = None) -> list[str]:
"""Get object (signal/image) list for current panel.
Objects are sorted by group number and object index in group.
Args:
panel: panel name (valid values: "signal", "image", "macro").
If None, current data panel is used (i.e. signal or image panel).
Returns:
List of object titles
Raises:
ValueError: if panel not found
"""
@abc.abstractmethod
def get_object(
self, nb_id_title: int | str | None = None, panel: str | None = None
) -> SignalObj | ImageObj:
"""Get object (signal/image) from index.
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object).
panel: Panel name. Defaults to None (current panel).
Returns:
Object
Raises:
KeyError: if object not found
"""
@abc.abstractmethod
def get_object_uuids(
self, panel: str | None = None, group: int | str | None = None
) -> list[str]:
"""Get object (signal/image) uuid list for current panel.
Objects are sorted by group number and object index in group.
Args:
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
group: Group number, or group id, or group title.
Defaults to None (all groups).
Returns:
List of object uuids
Raises:
ValueError: if panel not found
"""
@abc.abstractmethod
def get_object_shapes(
self,
nb_id_title: int | str | None = None,
panel: str | None = None,
) -> list:
"""Get plot item shapes associated to object (signal/image).
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object).
panel: Panel name. Defaults to None (current panel).
Returns:
List of plot item shapes
"""
@abc.abstractmethod
def add_annotations_from_items(
self, items: list, refresh_plot: bool = True, panel: str | None = None
) -> None:
"""Add object annotations (annotation plot items).
Args:
items: annotation plot items
refresh_plot: refresh plot. Defaults to True.
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
"""
@abc.abstractmethod
def add_label_with_title(
self, title: str | None = None, panel: str | None = None
) -> None:
"""Add a label with object title on the associated plot
Args:
title: Label title. Defaults to None.
If None, the title is the object title.
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
"""
@abc.abstractmethod
def run_macro(self, number_or_title: int | str | None = None) -> None:
"""Run macro.
Args:
number: Number of the macro (starting at 1). Defaults to None (run
current macro, or does nothing if there is no macro).
"""
@abc.abstractmethod
def stop_macro(self, number_or_title: int | str | None = None) -> None:
"""Stop macro.
Args:
number: Number of the macro (starting at 1). Defaults to None (stop
current macro, or does nothing if there is no macro).
"""
@abc.abstractmethod
def import_macro_from_file(self, filename: str) -> None:
"""Import macro from file
Args:
filename: Filename.
"""
@abc.abstractmethod
def calc(self, name: str, param: gds.DataSet | None = None) -> gds.DataSet:
"""Call computation feature ``name``
.. note::
This calls either the processor's ``compute_`` method (if it exists),
or the processor's ```` computation feature (if it is registered,
using the ``run_feature`` method).
It looks for the function in all panels, starting with the current one.
Args:
name: Compute function name
param: Compute function parameter. Defaults to None.
Raises:
ValueError: unknown function
"""
# =========================================================================
# Web API control methods
# =========================================================================
@abc.abstractmethod
def start_webapi_server(
self,
host: str | None = None,
port: int | None = None,
) -> dict:
"""Start the Web API server.
Args:
host: Host address to bind to. Defaults to "127.0.0.1".
port: Port number. Defaults to auto-detect available port.
Returns:
Dictionary with "url" and "token" keys.
Raises:
RuntimeError: If Web API deps not installed or server already running.
"""
@abc.abstractmethod
def stop_webapi_server(self) -> None:
"""Stop the Web API server."""
@abc.abstractmethod
def get_webapi_status(self) -> dict:
"""Get Web API server status.
Returns:
Dictionary with "running", "url", and "token" keys.
"""
@abc.abstractmethod
def call_method(
self,
method_name: str,
*args,
panel: str | None = None,
**kwargs,
):
"""Call a public method on a panel or main window.
This generic method allows calling any public method that is not explicitly
exposed in the proxy API. The method resolution follows this order:
1. If panel is specified: call method on that specific panel
2. If panel is None:
a. Try to call method on main window (DLMainWindow)
b. If not found, try to call method on current panel (BaseDataPanel)
This makes it convenient to call panel methods without specifying the panel
parameter when working on the current panel.
Args:
method_name: Name of the method to call
*args: Positional arguments to pass to the method
panel: Panel name ("signal", "image", or None for auto-detection).
Defaults to None.
**kwargs: Keyword arguments to pass to the method
Returns:
The return value of the called method
Raises:
AttributeError: If the method does not exist or is not public
ValueError: If the panel name is invalid
Examples:
>>> # Call remove_object on current panel (auto-detected)
>>> proxy.call_method("remove_object", force=True)
>>> # Call a signal panel method specifically
>>> proxy.call_method("delete_all_objects", panel="signal")
>>> # Call main window method
>>> proxy.call_method("raise_window")
"""
class BaseProxy(AbstractDLControl, metaclass=abc.ABCMeta):
"""Common base class for DataLab proxies
Args:
datalab: DLMainWindow instance or ServerProxy instance. If None, then the proxy
implementation will have to set it later (e.g. see RemoteClient).
"""
def __init__(self, datalab: DLMainWindow | ServerProxy | None = None) -> None:
self._datalab = datalab
def get_version(self) -> str:
"""Return DataLab public version.
Returns:
DataLab version
"""
return self._datalab.get_version()
def close_application(self) -> None:
"""Close DataLab application"""
self._datalab.close_application()
def raise_window(self) -> None:
"""Raise DataLab window"""
self._datalab.raise_window()
def get_current_panel(self) -> str:
"""Return current panel name.
Returns:
Panel name (valid values: "signal", "image", "macro"))
"""
return self._datalab.get_current_panel()
def set_current_panel(self, panel: str) -> None:
"""Switch to panel.
Args:
panel: Panel name (valid values: "signal", "image", "macro"))
"""
self._datalab.set_current_panel(panel)
def reset_all(self) -> None:
"""Reset all application data"""
self._datalab.reset_all()
def remove_object(self, force: bool = False) -> None:
"""Remove current object from current panel.
Args:
force: if True, remove object without confirmation. Defaults to False.
"""
self._datalab.remove_object(force)
def toggle_auto_refresh(self, state: bool) -> None:
"""Toggle auto refresh state.
Args:
state: Auto refresh state
"""
self._datalab.toggle_auto_refresh(state)
def toggle_show_titles(self, state: bool) -> None:
"""Toggle show titles state.
Args:
state: Show titles state
"""
self._datalab.toggle_show_titles(state)
def save_to_h5_file(self, filename: str) -> None:
"""Save to a DataLab HDF5 file.
Args:
filename: HDF5 file name
"""
self._datalab.save_to_h5_file(filename)
def open_h5_files(
self,
h5files: list[str] | None = None,
import_all: bool | None = None,
reset_all: bool | None = None,
) -> None:
"""Open a DataLab HDF5 file or import from any other HDF5 file.
Args:
h5files: List of HDF5 files to open. Defaults to None.
import_all: Import all objects from HDF5 files. Defaults to None.
reset_all: Reset all application data. Defaults to None.
"""
self._datalab.open_h5_files(h5files, import_all, reset_all)
def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
"""Open DataLab HDF5 browser to Import HDF5 file.
Args:
filename: HDF5 file name
reset_all: Reset all application data. Defaults to None.
"""
self._datalab.import_h5_file(filename, reset_all)
def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
"""Load native DataLab HDF5 workspace files without any GUI elements.
This method can be safely called from scripts (e.g., internal console,
macros) as it does not create any Qt widgets, dialogs, or progress bars.
.. warning::
This method only supports native DataLab HDF5 files. For importing
arbitrary HDF5 files (non-native), use :meth:`open_h5_files` or
:meth:`import_h5_file` instead.
Args:
h5files: List of native DataLab HDF5 filenames
reset_all: Reset all application data before importing. Defaults to False.
Raises:
ValueError: If a file is not a valid native DataLab HDF5 file
"""
self._datalab.load_h5_workspace(h5files, reset_all)
def save_h5_workspace(self, filename: str) -> None:
"""Save current workspace to a native DataLab HDF5 file without GUI elements.
This method can be safely called from scripts (e.g., internal console,
macros) as it does not create any Qt widgets, dialogs, or progress bars.
Args:
filename: HDF5 filename to save to
Raises:
IOError: If file cannot be saved
"""
self._datalab.save_h5_workspace(filename)
def load_from_files(self, filenames: list[str]) -> None:
"""Open objects from files in current panel (signals/images).
Args:
filenames: list of file names
"""
self._datalab.load_from_files(filenames)
def load_from_directory(self, path: str) -> None:
"""Open objects from directory in current panel (signals/images).
Args:
path: directory path
"""
self._datalab.load_from_directory(path)
def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]:
"""Return selected objects uuids.
Args:
include_groups: If True, also return objects from selected groups.
Returns:
List of selected objects uuids.
"""
return self._datalab.get_sel_object_uuids(include_groups)
def add_group(
self, title: str, panel: str | None = None, select: bool = False
) -> None:
"""Add group to DataLab.
Args:
title: Group title
panel: Panel name (valid values: "signal", "image"). Defaults to None.
select: Select the group after creation. Defaults to False.
"""
self._datalab.add_group(title, panel, select)
def select_objects(
self,
selection: list[int | str],
panel: str | None = None,
) -> None:
"""Select objects in current panel.
Args:
selection: List of object numbers (1 to N) or uuids to select
panel: panel name (valid values: "signal", "image").
If None, current panel is used. Defaults to None.
"""
self._datalab.select_objects(selection, panel)
def select_groups(
self, selection: list[int | str] | None = None, panel: str | None = None
) -> None:
"""Select groups in current panel.
Args:
selection: List of group numbers (1 to N), or list of group uuids,
or None to select all groups. Defaults to None.
panel: panel name (valid values: "signal", "image").
If None, current panel is used. Defaults to None.
"""
self._datalab.select_groups(selection, panel)
def delete_metadata(
self, refresh_plot: bool = True, keep_roi: bool = False
) -> None:
"""Delete metadata of selected objects
Args:
refresh_plot: Refresh plot. Defaults to True.
keep_roi: Keep ROI. Defaults to False.
"""
self._datalab.delete_metadata(refresh_plot, keep_roi)
def get_group_titles_with_object_info(
self,
) -> tuple[list[str], list[list[str]], list[list[str]]]:
"""Return groups titles and lists of inner objects uuids and titles.
Returns:
Tuple: groups titles, lists of inner objects uuids and titles
"""
return self._datalab.get_group_titles_with_object_info()
def get_object_titles(self, panel: str | None = None) -> list[str]:
"""Get object (signal/image) list for current panel.
Objects are sorted by group number and object index in group.
Args:
panel: panel name (valid values: "signal", "image", "macro").
If None, current data panel is used (i.e. signal or image panel).
Returns:
List of object titles
Raises:
ValueError: if panel not found
"""
return self._datalab.get_object_titles(panel)
def get_object_uuids(
self, panel: str | None = None, group: int | str | None = None
) -> list[str]:
"""Get object (signal/image) uuid list for current panel.
Objects are sorted by group number and object index in group.
Args:
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
group: Group number, or group id, or group title.
Defaults to None (all groups).
Returns:
List of object uuids
Raises:
ValueError: if panel not found
"""
return self._datalab.get_object_uuids(panel, group)
def add_label_with_title(
self, title: str | None = None, panel: str | None = None
) -> None:
"""Add a label with object title on the associated plot
Args:
title: Label title. Defaults to None.
If None, the title is the object title.
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
"""
self._datalab.add_label_with_title(title, panel)
def run_macro(self, number_or_title: int | str | None = None) -> None:
"""Run macro.
Args:
number_or_title: Macro number, or macro title.
Defaults to None (current macro).
Raises:
ValueError: if macro not found
"""
self._datalab.run_macro(number_or_title)
def stop_macro(self, number_or_title: int | str | None = None) -> None:
"""Stop macro.
Args:
number_or_title: Macro number, or macro title.
Defaults to None (current macro).
Raises:
ValueError: if macro not found
"""
self._datalab.stop_macro(number_or_title)
def import_macro_from_file(self, filename: str) -> None:
"""Import macro from file
Args:
filename: Filename.
"""
return self._datalab.import_macro_from_file(filename)
def call_method(
self,
method_name: str,
*args,
panel: str | None = None,
**kwargs,
):
"""Call a public method on a panel or main window.
Method resolution order when panel is None:
1. Try main window (DLMainWindow)
2. If not found, try current panel (BaseDataPanel)
Args:
method_name: Name of the method to call
*args: Positional arguments to pass to the method
panel: Panel name ("signal", "image", or None for auto-detection).
Defaults to None.
**kwargs: Keyword arguments to pass to the method
Returns:
The return value of the called method
Raises:
AttributeError: If the method does not exist or is not public
ValueError: If the panel name is invalid
"""
return self._datalab.call_method(method_name, *args, panel=panel, **kwargs)
# =========================================================================
# Web API control methods
# =========================================================================
def start_webapi_server(
self,
host: str | None = None,
port: int | None = None,
) -> dict:
"""Start the Web API server.
Args:
host: Host address to bind to. Defaults to "127.0.0.1".
port: Port number. Defaults to auto-detect available port.
Returns:
Dictionary with "url" and "token" keys.
Raises:
RuntimeError: If Web API dependencies not installed or server
already running.
"""
return self._datalab.start_webapi_server(host, port)
def stop_webapi_server(self) -> None:
"""Stop the Web API server."""
return self._datalab.stop_webapi_server()
def get_webapi_status(self) -> dict:
"""Get Web API server status.
Returns:
Dictionary with "running", "url", and "token" keys.
"""
return self._datalab.get_webapi_status()
DataLab-1.1.0/datalab/control/proxy.py 0000664 0000000 0000000 00000027151 15140141176 0017612 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
Proxy objects (:mod:`datalab.control.proxy`)
--------------------------------------------
The :mod:`datalab.control.proxy` module provides a way to access DataLab features from
a proxy class.
Remote proxy
^^^^^^^^^^^^
The remote proxy is used when DataLab is started from a different process than the
proxy. In this case, the proxy connects to DataLab XML-RPC server.
.. autoclass:: RemoteProxy
:members:
:inherited-members:
Local proxy
^^^^^^^^^^^
The local proxy is used when DataLab is started from the same process as the proxy.
In this case, the proxy is directly connected to DataLab main window instance. The
typical use case is high-level scripting.
.. autoclass:: LocalProxy
:members:
:inherited-members:
Proxy context manager
^^^^^^^^^^^^^^^^^^^^^
The proxy context manager is a convenient way to handle proxy creation and
destruction. It is used as follows:
.. code-block:: python
with proxy_context("local") as proxy:
proxy.add_signal(...)
The proxy type can be "local" or "remote". For remote proxy, the port can be
specified as "remote:port".
.. note:: The proxy context manager allows to use the proxy in various contexts
(Python script, Jupyter notebook, etc.). It also allows to switch seamlessly
between local and remote proxy, keeping the same code inside the context.
.. autofunction:: proxy_context
Calling processor methods using proxy objects
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
All the proxy objects provide access to the DataLab computing methods exposed by
the processor classes:
- :class:`datalab.gui.processor.signal.SignalProcessor`
- :class:`datalab.gui.processor.image.ImageProcessor`
To run a computation feature associated to a processor, you can use the
:meth:`calc` method of the proxy object:
.. code-block:: python
# Call a method without parameter
proxy.calc("average")
# Call a method with parameters
p = sigima.params.MovingAverageParam.create(n=30)
proxy.calc("moving_average", p)
"""
from __future__ import annotations
from collections.abc import Generator
from contextlib import contextmanager
import guidata.dataset as gds
import numpy as np
from sigima import ImageObj, SignalObj
from datalab.control.baseproxy import BaseProxy
from datalab.control.remote import RemoteClient
from datalab.utils import qthelpers as qth
class RemoteProxy(RemoteClient):
"""DataLab remote proxy class.
This class provides access to DataLab features from a proxy class. This is the
remote version of proxy, which is used when DataLab is started from a different
process than the proxy.
Args:
autoconnect: Automatically connect to DataLab XML-RPC server.
Raises:
ConnectionRefusedError: Unable to connect to DataLab
ValueError: Invalid timeout (must be >= 0.0)
ValueError: Invalid number of retries (must be >= 1)
Examples:
Here is a simple example of how to use RemoteProxy in a Python script
or in a Jupyter notebook:
>>> from datalab.control.proxy import RemoteProxy
>>> proxy = RemoteProxy()
Connecting to DataLab XML-RPC server...OK (port: 28867)
>>> proxy.get_version()
'1.0.0'
>>> proxy.add_signal("toto", np.array([1., 2., 3.]), np.array([4., 5., -1.]))
True
>>> proxy.get_object_titles()
['toto']
>>> proxy["toto"] # from title
>>> proxy[1] # from number
>>> proxy[1].data
array([1., 2., 3.])
>>> proxy.set_current_panel("image")
"""
def __init__(self, autoconnect: bool = True) -> None:
super().__init__()
if autoconnect:
self.connect()
class LocalProxy(BaseProxy):
"""DataLab local proxy class.
This class provides access to DataLab features from a proxy class. This is the
local version of proxy, which is used when DataLab is started from the same
process as the proxy.
Args:
datalab (DLMainWindow): DLMainWindow instance.
"""
def add_signal(
self,
title: str,
xdata: np.ndarray,
ydata: np.ndarray,
xunit: str = "",
yunit: str = "",
xlabel: str = "",
ylabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add signal data to DataLab.
Args:
title: Signal title
xdata: X data
ydata: Y data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
group_id: group id in which to add the signal. Defaults to ""
set_current: if True, set the added signal as current
Returns:
True if signal was added successfully, False otherwise
Raises:
ValueError: Invalid xdata dtype
ValueError: Invalid ydata dtype
"""
return self._datalab.add_signal(
title, xdata, ydata, xunit, yunit, xlabel, ylabel, group_id, set_current
)
def add_image(
self,
title: str,
data: np.ndarray,
xunit: str = "",
yunit: str = "",
zunit: str = "",
xlabel: str = "",
ylabel: str = "",
zlabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add image data to DataLab.
Args:
title: Image title
data: Image data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
zunit: Z unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
zlabel: Z label. Defaults to ""
group_id: group id in which to add the image. Defaults to ""
set_current: if True, set the added image as current
Returns:
True if image was added successfully, False otherwise
Raises:
ValueError: Invalid data dtype
"""
return self._datalab.add_image(
title,
data,
xunit,
yunit,
zunit,
xlabel,
ylabel,
zlabel,
group_id,
set_current,
)
def add_object(
self, obj: SignalObj | ImageObj, group_id: str = "", set_current: bool = True
) -> None:
"""Add object to DataLab.
Args:
obj: Signal or image object
group_id: group id in which to add the object. Defaults to ""
set_current: if True, set the added object as current
"""
self._datalab.add_object(obj, group_id, set_current)
def calc(self, name: str, param: gds.DataSet | None = None) -> None:
"""Call computation feature ``name``
.. note::
This calls either the processor's ``compute_`` method (if it exists),
or the processor's ```` computation feature (if it is registered,
using the ``run_feature`` method).
It looks for the function in all panels, starting with the current one.
Args:
name: Compute function name
param: Compute function parameter. Defaults to None
Raises:
ValueError: unknown function
"""
return self._datalab.calc(name, param)
def get_object(
self, nb_id_title: int | str | None = None, panel: str | None = None
) -> SignalObj | ImageObj:
"""Get object (signal/image) from index.
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object)
panel: Panel name. Defaults to None (current panel)
Returns:
Object
Raises:
KeyError: if object not found
"""
return self._datalab.get_object(nb_id_title, panel)
def get_object_shapes(
self,
nb_id_title: int | str | None = None,
panel: str | None = None,
) -> list:
"""Get plot item shapes associated to object (signal/image).
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object)
panel: Panel name. Defaults to None (current panel)
Returns:
List of plot item shapes
"""
return self._datalab.get_object_shapes(nb_id_title, panel)
def add_annotations_from_items(
self, items: list, refresh_plot: bool = True, panel: str | None = None
) -> None:
"""Add object annotations (annotation plot items).
Args:
itemsTrue: annotation plot items
refresh_plotTrue: refresh plot. Defaults to True
panel: panel name (valid values: "signal", "image").
If None, current panel is used
"""
self._datalab.add_annotations_from_items(items, refresh_plot, panel)
def load_h5_workspace(
self, h5files: list[str] | str, reset_all: bool = False
) -> None:
"""Load HDF5 workspace files without showing file dialog.
This method loads one or more DataLab native HDF5 files directly, bypassing
the file dialog. It is safe to call from the internal console or any context
where Qt dialogs would cause threading issues.
Args:
h5files: Path(s) to HDF5 file(s). Can be a single path string or a list
of paths
reset_all: If True, reset workspace before loading. Defaults to False.
Raises:
ValueError: if file is not a DataLab native HDF5 file
"""
if isinstance(h5files, str):
h5files = [h5files]
self._datalab.load_h5_workspace(h5files, reset_all)
def save_h5_workspace(self, filename: str) -> None:
"""Save workspace to HDF5 file without showing file dialog.
This method saves the current workspace to a DataLab native HDF5 file
directly, bypassing the file dialog. It is safe to call from the internal
console or any context where Qt dialogs would cause threading issues.
Args:
filename: Path to the output HDF5 file
"""
self._datalab.save_h5_workspace(filename)
@contextmanager
def proxy_context(what: str) -> Generator[LocalProxy | RemoteProxy, None, None]:
"""Context manager handling DL proxy creation and destruction.
Args:
what: proxy type ("local" or "remote")
For remote proxy, the port can be specified as "remote:port"
Yields:
proxy
LocalProxy if what == "local"
RemoteProxy if what == "remote" or "remote:port"
Example:
with proxy_context("local") as proxy:
proxy.add_signal(...)
"""
assert what == "local" or what.startswith("remote"), "Invalid proxy type"
port = None
if ":" in what:
port = int(what.split(":")[1].strip())
if what == "local":
# pylint: disable=import-outside-toplevel, cyclic-import
from datalab.gui.main import DLMainWindow
with qth.datalab_app_context(exec_loop=True):
try:
win = DLMainWindow()
proxy = LocalProxy(win)
win.show()
yield proxy
finally:
pass
else:
try:
proxy = RemoteProxy(autoconnect=False)
proxy.connect(port)
yield proxy
finally:
proxy.disconnect()
DataLab-1.1.0/datalab/control/remote.py 0000664 0000000 0000000 00000121622 15140141176 0017722 0 ustar 00root root 0000000 0000000 # Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file.
"""
DataLab remote control
----------------------
This module provides utilities to control DataLab from a Python script (e.g. with
Spyder) or from a Jupyter notebook.
The :class:`RemoteClient` class provides the main interface to DataLab XML-RPC server.
"""
from __future__ import annotations
import functools
import sys
import threading
import time
import warnings
from collections.abc import Callable
from typing import TYPE_CHECKING
from xmlrpc.client import Binary, ServerProxy
from xmlrpc.server import SimpleXMLRPCServer
import guidata.dataset as gds
import numpy as np
from packaging.version import Version
from qtpy import QtCore as QC
from sigima.client import utils
from sigima.objects import ImageObj, SignalObj, create_image, create_signal
import datalab
from datalab.adapters_plotpy import items_to_json, json_to_items
from datalab.config import Conf, initialize
from datalab.control.baseproxy import AbstractDLControl, BaseProxy
from datalab.env import execenv
if TYPE_CHECKING:
from datalab.gui.main import DLMainWindow
# pylint: disable=invalid-name # Allows short reference names like x, y, ...
# pylint: disable=duplicate-code
def remote_call(func: Callable) -> object:
"""Decorator for method calling DataLab main window remotely"""
@functools.wraps(func)
def method_wrapper(*args, **kwargs):
"""Decorator wrapper function"""
self: RemoteServer = args[0] # extracting 'self' from method arguments
self.is_ready = False
output = func(*args, **kwargs)
while not self.is_ready:
QC.QCoreApplication.processEvents()
time.sleep(0.05)
# Check if an exception was raised and stored by the slot
if self.exception is not None:
exc = self.exception
self.exception = None # Clear the exception
raise exc
# For methods that use signal/slot pattern (like call_method),
# they return self.result which is set by the slot. The function
# itself returns this value, but it's set asynchronously, so we
# need to return the value AFTER waiting, not the initial return value.
# Since we wait above, self.result should now be set by the slot.
# If the function returned self.result, use the updated value.
if output is None:
# Return self.result which should be set by now
return self.result
return output
return method_wrapper
# Note: RemoteServer can't inherit from AbstractDLControl because it is a QThread
# and most of the methods are not returning expected data types
class RemoteServer(QC.QThread):
"""XML-RPC server QThread"""
SIG_SERVER_PORT = QC.Signal(int)
SIG_CLOSE_APP = QC.Signal()
SIG_RAISE_WINDOW = QC.Signal()
SIG_ADD_OBJECT = QC.Signal(object, str, bool)
SIG_ADD_GROUP = QC.Signal(str, str, bool)
SIG_LOAD_FROM_FILES = QC.Signal(list)
SIG_LOAD_FROM_DIRECTORY = QC.Signal(str)
SIG_SELECT_OBJECTS = QC.Signal(list, str)
SIG_SELECT_GROUPS = QC.Signal(list, str)
SIG_SELECT_ALL_GROUPS = QC.Signal(str)
SIG_DELETE_METADATA = QC.Signal(bool, bool)
SIG_SWITCH_TO_PANEL = QC.Signal(str)
SIG_TOGGLE_AUTO_REFRESH = QC.Signal(bool)
SIG_TOGGLE_SHOW_TITLES = QC.Signal(bool)
SIG_RESET_ALL = QC.Signal()
SIG_REMOVE_OBJECT = QC.Signal(bool)
SIG_SAVE_TO_H5 = QC.Signal(str)
SIG_OPEN_H5 = QC.Signal(list, bool, bool)
SIG_IMPORT_H5 = QC.Signal(str, bool)
SIG_LOAD_H5_WORKSPACE = QC.Signal(list, bool)
SIG_SAVE_H5_WORKSPACE = QC.Signal(str)
SIG_CALC = QC.Signal(str, object)
SIG_RUN_MACRO = QC.Signal(str)
SIG_STOP_MACRO = QC.Signal(str)
SIG_IMPORT_MACRO_FROM_FILE = QC.Signal(str)
SIG_CALL_METHOD = QC.Signal(str, list, object, dict)
def __init__(self, win: DLMainWindow) -> None:
QC.QThread.__init__(self)
self.port: int = None
self.is_ready = True
self.server: SimpleXMLRPCServer | None = None
self.win = win
self.result = None
self.exception = None
win.SIG_READY.connect(self.datalab_is_ready)
win.SIG_CLOSING.connect(self.shutdown_server)
self.SIG_CLOSE_APP.connect(win.close)
self.SIG_RAISE_WINDOW.connect(win.raise_window)
self.SIG_ADD_OBJECT.connect(win.add_object)
self.SIG_ADD_GROUP.connect(win.add_group)
self.SIG_LOAD_FROM_FILES.connect(win.load_from_files)
self.SIG_LOAD_FROM_DIRECTORY.connect(win.load_from_directory)
self.SIG_SELECT_OBJECTS.connect(win.select_objects)
self.SIG_SELECT_GROUPS.connect(win.select_groups)
self.SIG_SELECT_ALL_GROUPS.connect(lambda panel: win.select_groups(None, panel))
self.SIG_DELETE_METADATA.connect(win.delete_metadata)
self.SIG_SWITCH_TO_PANEL.connect(win.set_current_panel)
self.SIG_TOGGLE_AUTO_REFRESH.connect(win.toggle_auto_refresh)
self.SIG_TOGGLE_SHOW_TITLES.connect(win.toggle_show_titles)
self.SIG_RESET_ALL.connect(win.reset_all)
self.SIG_REMOVE_OBJECT.connect(win.remove_object)
self.SIG_SAVE_TO_H5.connect(win.save_to_h5_file)
self.SIG_OPEN_H5.connect(win.open_h5_files)
self.SIG_IMPORT_H5.connect(win.import_h5_file)
self.SIG_LOAD_H5_WORKSPACE.connect(win.load_h5_workspace)
self.SIG_SAVE_H5_WORKSPACE.connect(win.save_h5_workspace)
self.SIG_CALC.connect(win.calc)
self.SIG_RUN_MACRO.connect(win.run_macro)
self.SIG_STOP_MACRO.connect(win.stop_macro)
self.SIG_IMPORT_MACRO_FROM_FILE.connect(win.import_macro_from_file)
self.SIG_CALL_METHOD.connect(win.call_method_slot)
def serve(self) -> None:
"""Start server and serve forever"""
with SimpleXMLRPCServer(
("127.0.0.1", 0), logRequests=False, allow_none=True
) as server:
self.server = server
server.register_introspection_functions()
self.register_functions(server)
self.port = server.server_address[1]
self.notify_port(self.port)
with execenv.context(xmlrpcport=self.port):
server.serve_forever()
def shutdown_server(self) -> None:
"""Shutdown server"""
if self.server is not None:
self.server.shutdown()
self.server = None
def notify_port(self, port: int) -> None:
"""Notify automatically attributed port.
This method is called after the server port has been automatically
attributed. It notifies the port number to the main window.
Args:
port: Server port number
"""
self.SIG_SERVER_PORT.emit(port)
@classmethod
def check_remote_functions(cls) -> None:
"""Check if all AbstractDLControl methods are implemented in RemoteServer"""
mlist = []
for method in AbstractDLControl.get_public_methods():
if not hasattr(cls, method):
mlist.append(method)
if mlist:
raise RuntimeError(f"{cls} is missing some methods: {','.join(mlist)}")
def register_functions(self, server: SimpleXMLRPCServer) -> None:
"""Register functions"""
for name in AbstractDLControl.get_public_methods():
server.register_function(getattr(self, name))
def run(self) -> None:
"""Thread execution method"""
if "coverage" in sys.modules:
# The following is required to make coverage work with threading
# pylint: disable=protected-access
sys.settrace(threading._trace_hook)
self.serve()
def datalab_is_ready(self) -> None:
"""Called when DataLab is ready to process new requests"""
self.is_ready = True
@staticmethod
def get_version() -> str:
"""Return DataLab public version"""
return datalab.__version__
def close_application(self) -> None:
"""Close DataLab application"""
self.SIG_CLOSE_APP.emit()
def raise_window(self) -> None:
"""Raise DataLab window"""
self.SIG_RAISE_WINDOW.emit()
@remote_call
def get_current_panel(self) -> str:
"""Return current panel name.
Returns:
Panel name (valid values: 'signal', 'image', 'macro')
"""
return self.win.get_current_panel()
@remote_call
def set_current_panel(self, panel: str) -> None:
"""Switch to panel.
Args:
panel: Panel name (valid values: 'signal', 'image', 'macro')
"""
self.SIG_SWITCH_TO_PANEL.emit(panel)
@remote_call
def toggle_auto_refresh(self, state: bool) -> None:
"""Toggle auto refresh state.
Args:
state: True to enable auto refresh, False to disable it
"""
self.SIG_TOGGLE_AUTO_REFRESH.emit(state)
@remote_call
def toggle_show_titles(self, state: bool) -> None:
"""Toggle show titles state.
Args:
state: True to enable show titles, False to disable it
"""
self.SIG_TOGGLE_SHOW_TITLES.emit(state)
@remote_call
def reset_all(self) -> None:
"""Reset all application data"""
self.SIG_RESET_ALL.emit()
@remote_call
def remove_object(self, force: bool = False) -> None:
"""Remove current object from current panel.
Args:
force: if True, remove object without confirmation. Defaults to False.
"""
self.SIG_REMOVE_OBJECT.emit(force)
@remote_call
def save_to_h5_file(self, filename: str) -> None:
"""Save to a DataLab HDF5 file.
Args:
filename: HDF5 file name (with extension .h5)
"""
self.SIG_SAVE_TO_H5.emit(filename)
@remote_call
def open_h5_files(
self,
h5files: list[str] | None = None,
import_all: bool | None = None,
reset_all: bool | None = None,
) -> None:
"""Open a DataLab HDF5 file or import from any other HDF5 file.
Args:
h5files: HDF5 file names. Defaults to None.
import_all: Import all objects from HDF5 file. Defaults to None.
reset_all: Reset all application data. Defaults to None.
"""
import_all = True if import_all is None else import_all
reset_all = False if reset_all is None else reset_all
self.SIG_OPEN_H5.emit(h5files, import_all, reset_all)
@remote_call
def import_h5_file(self, filename: str, reset_all: bool | None = None) -> None:
"""Open DataLab HDF5 browser to Import HDF5 file.
Args:
filename: HDF5 file name
reset_all: Reset all application data. Defaults to None.
"""
reset_all = False if reset_all is None else reset_all
self.SIG_IMPORT_H5.emit(filename, reset_all)
@remote_call
def load_h5_workspace(self, h5files: list[str], reset_all: bool = False) -> None:
"""Load native DataLab HDF5 workspace files without any GUI elements.
This method can be safely called from scripts as it does not create
any Qt widgets, dialogs, or progress bars.
Args:
h5files: List of native DataLab HDF5 filenames
reset_all: Reset all application data before importing. Defaults to False.
Raises:
ValueError: If a file is not a valid native DataLab HDF5 file
"""
self.SIG_LOAD_H5_WORKSPACE.emit(h5files, reset_all)
@remote_call
def save_h5_workspace(self, filename: str) -> None:
"""Save current workspace to a native DataLab HDF5 file without GUI elements.
This method can be safely called from scripts as it does not create
any Qt widgets, dialogs, or progress bars.
Args:
filename: HDF5 filename to save to
Raises:
IOError: If file cannot be saved
"""
self.SIG_SAVE_H5_WORKSPACE.emit(filename)
@remote_call
def load_from_files(self, filenames: list[str]) -> None:
"""Open objects from files in current panel (signals/images).
Args:
filenames: list of file names
"""
self.SIG_LOAD_FROM_FILES.emit(filenames)
@remote_call
def load_from_directory(self, path: str) -> None:
"""Open objects from directory in current panel (signals/images).
Args:
path: directory path
"""
self.SIG_LOAD_FROM_DIRECTORY.emit(path)
@remote_call
def add_signal(
self,
title: str,
xbinary: Binary,
ybinary: Binary,
xunit: str = "",
yunit: str = "",
xlabel: str = "",
ylabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add signal data to DataLab.
Args:
title: Signal title
xbinary: X data
ybinary: Y data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
group_id: group id in which to add the signal. Defaults to ""
set_current: if True, set the added signal as current
Returns:
True if successful
"""
xdata = utils.rpcbinary_to_array(xbinary)
ydata = utils.rpcbinary_to_array(ybinary)
signal = create_signal(title, xdata, ydata)
signal.xunit = xunit or "" # In case xunit is None
signal.yunit = yunit or "" # In case yunit is None
signal.xlabel = xlabel or "" # In case xlabel is None
signal.ylabel = ylabel or "" # In case ylabel is None
self.SIG_ADD_OBJECT.emit(signal, group_id, set_current)
return True
@remote_call
def add_image(
self,
title: str,
zbinary: Binary,
xunit: str = "",
yunit: str = "",
zunit: str = "",
xlabel: str = "",
ylabel: str = "",
zlabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add image data to DataLab.
Args:
title: Image title
zbinary: Z data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
zunit: Z unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
zlabel: Z label. Defaults to ""
group_id: group id in which to add the image. Defaults to ""
set_current: if True, set the added image as current
Returns:
True if successful
"""
data = utils.rpcbinary_to_array(zbinary)
image = create_image(title, data)
image.xunit = xunit or "" # In case xunit is None
image.yunit = yunit or "" # In case yunit is None
image.zunit = zunit or "" # In case zunit is None
image.xlabel = xlabel or "" # In case xlabel is None
image.ylabel = ylabel or "" # In case ylabel is None
image.zlabel = zlabel or "" # In case zlabel is None
self.SIG_ADD_OBJECT.emit(image, group_id, set_current)
return True
@remote_call
def add_object(
self, obj_data: list[str], group_id: str = "", set_current: bool = True
) -> bool:
"""Add object to DataLab.
Args:
obj_data: Object data
group_id: Group id in which to add the object. Defaults to ""
set_current: if True, set the added object as current
Returns:
True if successful
"""
obj = utils.rpcjson_to_dataset(obj_data)
self.SIG_ADD_OBJECT.emit(obj, group_id, set_current)
return True
@remote_call
def get_sel_object_uuids(self, include_groups: bool = False) -> list[str]:
"""Return selected objects uuids.
Args:
include_groups: If True, also return objects from selected groups.
Returns:
List of selected objects uuids.
"""
return self.win.get_sel_object_uuids(include_groups)
@remote_call
def add_group(
self, title: str, panel: str | None = None, select: bool = False
) -> None:
"""Add group to DataLab.
Args:
title: Group title
panel: Panel name (valid values: "signal", "image"). Defaults to None.
select: Select the group after creation. Defaults to False.
"""
self.SIG_ADD_GROUP.emit(title, panel, select)
@remote_call
def select_objects(
self,
selection: list[int | str],
panel: str | None = None,
) -> None:
"""Select objects in current panel.
Args:
selection: List of object numbers (1 to N) or uuids to select
panel: panel name (valid values: "signal", "image").
If None, current panel is used. Defaults to None.
"""
self.SIG_SELECT_OBJECTS.emit(selection, panel)
@remote_call
def select_groups(
self, selection: list[int | str] | None = None, panel: str | None = None
) -> None:
"""Select groups in current panel.
Args:
selection: List of group numbers (1 to N), or list of group uuids,
or None to select all groups. Defaults to None.
panel: panel name (valid values: "signal", "image").
If None, current panel is used. Defaults to None.
"""
if selection is None:
self.SIG_SELECT_ALL_GROUPS.emit(panel)
else:
self.SIG_SELECT_GROUPS.emit(selection, panel)
@remote_call
def delete_metadata(
self, refresh_plot: bool = True, keep_roi: bool = False
) -> None:
"""Delete metadata of selected objects
Args:
refresh_plot: Refresh plot. Defaults to True.
keep_roi: Keep ROI. Defaults to False.
"""
self.SIG_DELETE_METADATA.emit(refresh_plot, keep_roi)
@remote_call
def calc(self, name: str, param_data: list[str] | None = None) -> bool:
"""Call computation feature ``name``
.. note::
This calls either the processor's ``compute_`` method (if it exists),
or the processor's ```` computation feature (if it is registered,
using the ``run_feature`` method).
It looks for the function in all panels, starting with the current one.
Args:
name: Compute function name
param_data: Compute function parameters. Defaults to None.
Returns:
True if successful, False otherwise
"""
if param_data is None:
param = None
else:
param = utils.rpcjson_to_dataset(param_data)
self.SIG_CALC.emit(name, param)
return True
@remote_call
def get_group_titles_with_object_info(
self,
) -> tuple[list[str], list[list[str]], list[list[str]]]:
"""Return groups titles and lists of inner objects uuids and titles.
Returns:
Groups titles, lists of inner objects uuids and titles
"""
return self.win.get_group_titles_with_object_info()
@remote_call
def get_object_titles(self, panel: str | None = None) -> list[str]:
"""Get object (signal/image) list for current panel.
Objects are sorted by group number and object index in group.
Args:
panel: panel name (valid values: "signal", "image", "macro").
If None, current data panel is used (i.e. signal or image panel).
Returns:
List of object titles
"""
return self.win.get_object_titles(panel)
@remote_call
def get_object(
self,
nb_id_title: int | str | None = None,
panel: str | None = None,
) -> list[str]:
"""Get object (signal/image) from index.
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object).
panel: Panel name. Defaults to None (current panel).
Returns:
Object
Raises:
KeyError: if object not found
"""
obj = self.win.get_object(nb_id_title, panel)
if obj is None:
return None
return utils.dataset_to_rpcjson(obj)
@remote_call
def get_object_uuids(
self, panel: str | None = None, group: int | str | None = None
) -> list[str]:
"""Get object (signal/image) uuid list for current panel.
Objects are sorted by group number and object index in group.
Args:
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
group: Group number, or group id, or group title.
Defaults to None (all groups).
Returns:
Object uuids
"""
return self.win.get_object_uuids(panel, group)
@remote_call
def get_object_shapes(
self,
nb_id_title: int | str | None = None,
panel: str | None = None,
) -> list:
"""Get plot item shapes associated to object (signal/image).
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object).
panel: Panel name. Defaults to None (current panel).
Returns:
List of plot item shapes
"""
items = self.win.get_object_shapes(nb_id_title, panel)
return items_to_json(items)
@remote_call
def add_annotations_from_items(
self, items_json: str, refresh_plot: bool = True, panel: str | None = None
) -> None:
"""Add object annotations (annotation plot items).
Args:
items_json: JSON string of annotation items
refresh_plot: refresh plot. Defaults to True.
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
"""
items = json_to_items(items_json)
if items:
self.win.add_annotations_from_items(items, refresh_plot, panel)
@remote_call
def add_label_with_title(
self, title: str | None = None, panel: str | None = None
) -> None:
"""Add a label with object title on the associated plot
Args:
title: Label title. Defaults to None.
If None, the title is the object title.
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
"""
self.win.add_label_with_title(title, panel)
@remote_call
def run_macro(self, number_or_title: int | str | None = None) -> None:
"""Run macro.
Args:
number: Number of the macro (starting at 1). Defaults to None (run
current macro, or does nothing if there is no macro).
"""
self.SIG_RUN_MACRO.emit(number_or_title)
@remote_call
def stop_macro(self, number_or_title: int | str | None = None) -> None:
"""Stop macro.
Args:
number: Number of the macro (starting at 1). Defaults to None (stop
current macro, or does nothing if there is no macro).
"""
self.SIG_STOP_MACRO.emit(number_or_title)
@remote_call
def import_macro_from_file(self, filename: str) -> None:
"""Import macro from file
Args:
filename: Filename.
"""
self.SIG_IMPORT_MACRO_FROM_FILE.emit(filename)
@remote_call
def call_method(
self,
method_name: str,
call_params: dict,
):
"""Call a public method on a panel or main window.
Method resolution order when panel is None:
1. Try main window (DLMainWindow)
2. If not found, try current panel (BaseDataPanel)
Args:
method_name: Name of the method to call
call_params: Dictionary with keys 'args' (list), 'panel' (str|None),
'kwargs' (dict). Defaults to empty for missing keys.
Returns:
The return value of the called method
Raises:
AttributeError: If the method does not exist or is not public
ValueError: If the panel name is invalid
"""
args = call_params.get("args", [])
panel = call_params.get("panel")
kwargs = call_params.get("kwargs", {})
self.result = None # Initialize result
self.SIG_CALL_METHOD.emit(method_name, args, panel, kwargs)
# The decorator waits for is_ready, then this returns self.result
# which was set by call_method_slot
return self.result
# =========================================================================
# Web API control methods
# =========================================================================
@remote_call
def start_webapi_server(
self,
host: str | None = None,
port: int | None = None,
) -> dict:
"""Start the Web API server.
Args:
host: Host address to bind to. Defaults to "127.0.0.1".
port: Port number. Defaults to auto-detect available port.
Returns:
Dictionary with "url" and "token" keys.
Raises:
RuntimeError: If Web API dependencies not installed or server
already running.
"""
# Delegate to main window method which is @remote_controlled
# This ensures SIG_READY is emitted for the @remote_call decorator
return self.win.start_webapi_server(host, port)
@remote_call
def stop_webapi_server(self) -> None:
"""Stop the Web API server."""
# Delegate to main window method which is @remote_controlled
return self.win.stop_webapi_server()
@remote_call
def get_webapi_status(self) -> dict:
"""Get Web API server status.
Returns:
Dictionary with "running", "url", and "token" keys.
"""
# Delegate to main window method which is @remote_controlled
return self.win.get_webapi_status()
RemoteServer.check_remote_functions()
# === Python 2.7 client side:
#
# # See doc/remotecontrol_py27.py for an almost complete Python 2.7
# # implementation of RemoteClient class
#
# import io
# from xmlrpclib import ServerProxy, Binary
# import numpy as np
# def array_to_binary(data):
# """Convert NumPy array to XML-RPC Binary object, with shape and dtype"""
# dbytes = io.BytesIO()
# np.save(dbytes, data, allow_pickle=False)
# return Binary(dbytes.getvalue())
# s = ServerProxy("http://127.0.0.1:8000")
# data = np.array([[3, 4, 5], [7, 8, 0]], dtype=np.uint16)
# s.add_image("toto", array_to_binary(data))
def get_datalab_xmlrpc_port() -> str:
"""Return DataLab current XML-RPC port
Returns:
XML-RPC port
Raises:
ConnectionRefusedError: DataLab has not yet been executed
"""
# The following is valid only when using Python 3.9+ with DataLab
# installed on the client side. In any other situation, please use the
# ``get_datalab_xmlrpc_port`` function from doc/remotecontrol_py27.py.
initialize()
try:
return Conf.main.rpc_server_port.get()
except RuntimeError as exc:
raise ConnectionRefusedError("DataLab has not yet been executed") from exc
class RemoteClient(BaseProxy):
"""Object representing a proxy/client to DataLab XML-RPC server.
This object is used to call DataLab functions from a Python script.
Examples:
Here is a simple example of how to use RemoteClient in a Python script
or in a Jupyter notebook:
>>> from datalab.remote import RemoteClient
>>> proxy = RemoteClient()
>>> proxy.connect()
Connecting to DataLab XML-RPC server...OK (port: 28867)
>>> proxy.get_version()
'1.0.0'
>>> proxy.add_signal("toto", np.array([1., 2., 3.]), np.array([4., 5., -1.]))
True
>>> proxy.get_object_titles()
['toto']
>>> proxy["toto"]
>>> proxy[1]
>>> proxy[1].data
array([1., 2., 3.])
"""
def __init__(self) -> None:
super().__init__()
self.port: str | None = None
self._datalab: ServerProxy
def set_port(self, port: str | None = None) -> None:
"""Set XML-RPC port to connect to.
Args:
port: XML-RPC port to connect to. If None, the port is automatically
retrieved from DataLab configuration.
"""
execenv.print(f"Setting XML-RPC port... [input:{port}] ", end="")
port_str = ""
if port is None:
port = execenv.xmlrpcport
port_str = f"→[execenv.xmlrpcport:{port}] "
if port is None:
port = get_datalab_xmlrpc_port()
port_str = f"→[Conf.main.rpc_server_port:{port}] "
execenv.print(port_str, end="")
self.port = port
if port is None:
execenv.print("KO")
raise ConnectionRefusedError("DataLab XML-RPC port is not set")
execenv.print("OK")
def __connect_to_server(self) -> None:
"""Connect to DataLab XML-RPC server.
Args:
port: XML-RPC port to connect to.
Raises:
ConnectionRefusedError: DataLab is currently not running
"""
self._datalab = ServerProxy(f"http://127.0.0.1:{self.port}", allow_none=True)
try:
version = self.get_version()
except ConnectionRefusedError as exc:
raise ConnectionRefusedError("DataLab is currently not running") from exc
# If DataLab version is not compatible with this client, show a warning
server_ver = Version(version)
client_ver = Version(datalab.__version__)
if server_ver < client_ver:
warnings.warn(
f"DataLab server version ({server_ver}) may not be fully compatible "
f"with this DataLab client version ({client_ver}).\n"
f"Please upgrade the server to {client_ver} or higher."
)
def connect(
self,
port: str | None = None,
timeout: float | None = None,
retries: int | None = None,
) -> None:
"""Try to connect to DataLab XML-RPC server.
Args:
port: XML-RPC port to connect to. If not specified,
the port is automatically retrieved from DataLab configuration.
timeout: Maximum time to wait for connection in seconds. Defaults to 5.0.
This is the total maximum wait time, not per retry.
retries: Number of retries. Defaults to 10. This parameter is deprecated
and will be removed in a future version (kept for backward compatibility).
Raises:
ConnectionRefusedError: Unable to connect to DataLab
ValueError: Invalid timeout (must be >= 0.0)
ValueError: Invalid number of retries (must be >= 1)
"""
timeout = 5.0 if timeout is None else timeout
retries = 10 if retries is None else retries # Kept for backward compatibility
if timeout < 0.0:
raise ValueError("timeout must be >= 0.0")
if retries < 1:
raise ValueError("retries must be >= 1")
execenv.print(f"Connecting to DataLab XML-RPC server... [port:{port}] ", end="")
# Use exponential backoff for more efficient retrying
start_time = time.time()
poll_interval = 0.1 # Start with 100ms
max_poll_interval = 1.0 # Cap at 1 second
while True:
try:
# Try to set the port - this may fail if DataLab hasn't written its
# config file yet, so we retry it in the loop
self.set_port(port)
self.__connect_to_server()
elapsed = time.time() - start_time
execenv.print(f"OK (port: {self.port}, connected in {elapsed:.1f}s)")
return
except (ConnectionRefusedError, OSError) as exc:
# Catch both ConnectionRefusedError and OSError (includes socket errors)
elapsed = time.time() - start_time
if elapsed >= timeout:
execenv.print("KO")
raise ConnectionRefusedError(
f"Unable to connect to DataLab after {elapsed:.1f}s"
) from exc
# Wait before next retry with exponential backoff
time.sleep(poll_interval)
poll_interval = min(poll_interval * 1.5, max_poll_interval)
def disconnect(self) -> None:
"""Disconnect from DataLab XML-RPC server."""
# This is not mandatory with XML-RPC, but if we change protocol in the
# future, it may be useful to have a disconnect method.
self._datalab = None
def is_connected(self) -> bool:
"""Return True if connected to DataLab XML-RPC server."""
if self._datalab is not None:
try:
self.get_version()
return True
except ConnectionRefusedError:
self._datalab = None
return False
def get_method_list(self) -> list[str]:
"""Return list of available methods."""
return self._datalab.system.listMethods()
# === Following methods should match the register functions in XML-RPC server
def add_signal(
self,
title: str,
xdata: np.ndarray,
ydata: np.ndarray,
xunit: str = "",
yunit: str = "",
xlabel: str = "",
ylabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add signal data to DataLab.
Args:
title: Signal title
xdata: X data
ydata: Y data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
group_id: group id in which to add the signal. Defaults to ""
set_current: if True, set the added signal as current
Returns:
True if signal was added successfully, False otherwise
Raises:
ValueError: Invalid xdata dtype
ValueError: Invalid ydata dtype
"""
obj = SignalObj()
obj.set_xydata(xdata, ydata)
obj.check_data()
xbinary = utils.array_to_rpcbinary(xdata)
ybinary = utils.array_to_rpcbinary(ydata)
return self._datalab.add_signal(
title, xbinary, ybinary, xunit, yunit, xlabel, ylabel, group_id, set_current
)
def add_image(
self,
title: str,
data: np.ndarray,
xunit: str = "",
yunit: str = "",
zunit: str = "",
xlabel: str = "",
ylabel: str = "",
zlabel: str = "",
group_id: str = "",
set_current: bool = True,
) -> bool: # pylint: disable=too-many-arguments
"""Add image data to DataLab.
Args:
title: Image title
data: Image data
xunit: X unit. Defaults to ""
yunit: Y unit. Defaults to ""
zunit: Z unit. Defaults to ""
xlabel: X label. Defaults to ""
ylabel: Y label. Defaults to ""
zlabel: Z label. Defaults to ""
group_id: group id in which to add the image. Defaults to ""
set_current: if True, set the added image as current
Returns:
True if image was added successfully, False otherwise
Raises:
ValueError: Invalid data dtype
"""
obj = ImageObj()
obj.data = data
obj.check_data()
zbinary = utils.array_to_rpcbinary(data)
return self._datalab.add_image(
title,
zbinary,
xunit,
yunit,
zunit,
xlabel,
ylabel,
zlabel,
group_id,
set_current,
)
def add_object(
self,
obj: SignalObj | ImageObj,
group_id: str = "",
set_current: bool = True,
) -> None:
"""Add object to DataLab.
Args:
obj: Signal or image object
group_id: group id in which to add the object. Defaults to ""
set_current: if True, set the added object as current
"""
obj_data = utils.dataset_to_rpcjson(obj)
self._datalab.add_object(obj_data, group_id, set_current)
def calc(self, name: str, param: gds.DataSet | None = None) -> None:
"""Call computation feature ``name``
.. note::
This calls either the processor's ``compute_`` method (if it exists),
or the processor's ```` computation feature (if it is registered,
using the ``run_feature`` method).
It looks for the function in all panels, starting with the current one.
Args:
name: Compute function name
param: Compute function parameter. Defaults to None.
Raises:
ValueError: unknown function
"""
if param is None:
return self._datalab.calc(name)
return self._datalab.calc(name, utils.dataset_to_rpcjson(param))
def get_object(
self,
nb_id_title: int | str | None = None,
panel: str | None = None,
) -> SignalObj | ImageObj:
"""Get object (signal/image) from index.
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object).
panel: Panel name. Defaults to None (current panel).
Returns:
Object
Raises:
KeyError: if object not found
"""
param_data = self._datalab.get_object(nb_id_title, panel)
if param_data is None:
return None
return utils.rpcjson_to_dataset(param_data)
def get_object_shapes(
self,
nb_id_title: int | str | None = None,
panel: str | None = None,
) -> list:
"""Get plot item shapes associated to object (signal/image).
Args:
nb_id_title: Object number, or object id, or object title.
Defaults to None (current object).
panel: Panel name. Defaults to None (current panel).
Returns:
List of plot item shapes
"""
items_json = self._datalab.get_object_shapes(nb_id_title, panel)
return json_to_items(items_json)
def add_annotations_from_items(
self, items: list, refresh_plot: bool = True, panel: str | None = None
) -> None:
"""Add object annotations (annotation plot items).
Args:
items: annotation plot items
refresh_plot: refresh plot. Defaults to True.
panel: panel name (valid values: "signal", "image").
If None, current panel is used.
"""
items_json = items_to_json(items)
if items_json is not None:
self._datalab.add_annotations_from_items(items_json, refresh_plot, panel)
def call_method(
self,
method_name: str,
*args,
panel: str | None = None,
**kwargs,
):
"""Call a public method on a panel or main window.
Method resolution order when panel is None:
1. Try main window (DLMainWindow)
2. If not found, try current panel (BaseDataPanel)
Args:
method_name: Name of the method to call
*args: Positional arguments to pass to the method
panel: Panel name ("signal", "image", or None for auto-detection).
Defaults to None.
**kwargs: Keyword arguments to pass to the method
Returns:
The return value of the called method
Raises:
AttributeError: If the method does not exist or is not public
ValueError: If the panel name is invalid
"""
# Convert args/kwargs to single dict for XML-RPC serialization
# This avoids XML-RPC signature mismatch issues with default parameters
call_params = {
"args": list(args) if args else [],
"panel": panel,
"kwargs": dict(kwargs) if kwargs else {},
}
return self._datalab.call_method(method_name, call_params)
# === WebAPI Server Control Methods ===
def start_webapi_server(
self, host: str = "127.0.0.1", port: int = 8080
) -> dict[str, str | int]:
"""Start the WebAPI server.
Args:
host: Host address to bind to. Defaults to "127.0.0.1".
port: Port number. Defaults to 8080.
Returns:
Dictionary with server info including 'url' and 'token'.
"""
return self._datalab.start_webapi_server(host, port)
def stop_webapi_server(self) -> bool:
"""Stop the WebAPI server.
Returns:
True if server was stopped, False if it wasn't running.
"""
return self._datalab.stop_webapi_server()
def get_webapi_status(self) -> dict[str, str | int | bool]:
"""Get the current status of the WebAPI server.
Returns:
Dictionary with status info including 'running', 'url', and 'port'.
"""
return self._datalab.get_webapi_status()
DataLab-1.1.0/datalab/data/ 0000775 0000000 0000000 00000000000 15140141176 0015302 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/data/icons/ 0000775 0000000 0000000 00000000000 15140141176 0016415 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/data/icons/analysis.svg 0000664 0000000 0000000 00000014510 15140141176 0020762 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/ 0000775 0000000 0000000 00000000000 15140141176 0020240 5 ustar 00root root 0000000 0000000 DataLab-1.1.0/datalab/data/icons/analysis/delete_results.svg 0000664 0000000 0000000 00000012230 15140141176 0024002 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/fw1e2.svg 0000664 0000000 0000000 00000014340 15140141176 0021707 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/fwhm.svg 0000664 0000000 0000000 00000014335 15140141176 0021730 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/histogram.svg 0000664 0000000 0000000 00000004131 15140141176 0022755 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/peak_detect.svg 0000664 0000000 0000000 00000012350 15140141176 0023232 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/plot_results.svg 0000664 0000000 0000000 00000014666 15140141176 0023535 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/show_results.svg 0000664 0000000 0000000 00000010621 15140141176 0023522 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/analysis/stats.svg 0000664 0000000 0000000 00000003736 15140141176 0022130 0 ustar 00root root 0000000 0000000
DataLab-1.1.0/datalab/data/icons/apply.svg 0000664 0000000 0000000 00000000725 15140141176 0020267 0 ustar 00root root 0000000 0000000