diff --git a/adding_devices.md b/adding_devices.md deleted file mode 100644 index c549ee9..0000000 --- a/adding_devices.md +++ /dev/null @@ -1,120 +0,0 @@ -# Adding Support for New Devices - -This guide outlines the process of adding support for new devices to the ExEngine framework. - -## 1. Inherit from the Device Base Class - -All new devices should inherit from the `Device` base class or one or more of its subclasses (see [exengine.kernel.device_types_base](https://github.com/micro-manager/ExEngine/blob/main/src/exengine/kernel/device_types_base.py)) - -```python -from exengine.kernel.device_types_base import Device - -class ANewDevice(Device): - def __init__(self, name): - super().__init__(name) - # Your device-specific initialization code here -``` - -## 2. Implement Device Functionality - -Devices can expose functionality through properties and methods. The base `Device` class primarily uses properties. - -Properties are essentially attributes with additional capabilities. They can have special characteristics, which are defined by implementing abstract methods in the `Device` class: - -- Allowed values: Properties can have a finite set of allowed values. -- Read-only status: Properties can be set as read-only. -- Limits: Numeric properties can have upper and lower bounds. -- Triggerability: Properties can be hardware-triggerable. - -Here's an example of implementing these special characteristics: - -```python -class ANewDevice(Device): - def get_allowed_property_values(self, property_name: str) -> List[str]: - if property_name == "mode": - return ["fast", "slow", "custom"] - return [] - - def is_property_read_only(self, property_name: str) -> bool: - return property_name in ["serial_number", "firmware_version"] - - def get_property_limits(self, property_name: str) -> Tuple[float, float]: - if property_name == "exposure_time": - return (0.001, 10.0) # seconds - return None - - def is_property_hardware_triggerable(self, property_name: str) -> bool: - return property_name in ["position", "gain"] - - # Implement other abstract methods... -``` - -## 3. Use Specialized Device Types - -There are specialized device types that standardize functionalities through methods. For example, a camera device type will have methods for taking images, setting exposure time, etc. Inheriting from one or more of these devices is recommended whenever possible, as it ensures compatibility with existing workflows and events. - -Specilzed device types implement functionality through abstract methods that must be implemented by subclasses. For example: - -```python -from exengine.kernel.device_types_base import Detector - - -# TODO: may change this API in the future -class ANewCameraDevice(Detector): - def set_exposure(self, exposure: float) -> None: - - # Implementation here - - def get_exposure(self) -> float: -# Implementation here - -# Implement other camera-specific methods... -``` - - - -# Advanced Topics - -## What inheritance from `Device` provides - -Inheriting from the `Device` class or its subclasses provides two main benefits: - -1. Compatibility with events for specialized devices in the ExEngine framework, reducing the need to write hardware control code from scratch. -2. **Thread safety**. All calls to devices that may interact with hardware are automatically rereouted to a common thread. This enables code from various parts of a program to interact with a device that may not be thread safe itself. As a result, there is no need to worry about threading and synchronization concerns in devices, thereby simplifying device control code and the process of adding new devices. -3. The ability to monitor all inputs and outputs from devices. Since all calls to devices pass through the execution engine, a complete accounting of the commands sent to hardware and the data recieved from it can be generated, without having to write more complex code. - -#### Bypassing the Executor - -In some cases, you may have attributes or methods that don't interact with hardware and don't need to go through the executor. You can bypass the executor for specific attributes or for the entire device: - -1. Specify attributes to bypass in the Device constructor: - -```python -class MyNewDevice(Device): - def __init__(self, name): - super().__init__(name, no_executor_attrs=('_some_internal_variable', 'some_method')) - # This will be executed on the calling thread like a normal attribute - self._some_internal_variable = 0 - - def some_method(self): - # This method will be executed directly on the calling thread - pass -``` - -2. Bypass the executor for all attributes and methods: - -```python -class MyNewDevice(Device): - def __init__(self, name): - super().__init__(name, no_executor=True) - # All attributes and methods in this class will bypass the executor - self._some_internal_variable = 0 - - def some_method(self): - # This method will be executed directly on the calling thread - pass -``` - -Using the first approach allows you to selectively bypass the executor for specific attributes or methods, while the second approach bypasses the executor for the entire device. -Note that when using no_executor_attrs, you need to specify the names of the attributes or methods as strings in a sequence (e.g., tuple or list) passed to the no_executor_attrs parameter in the super().__init__() call. -These approaches provide flexibility in controlling which parts of your device interact with the executor, allowing for optimization where direct access is safe and beneficial. \ No newline at end of file diff --git a/docs/add_devices.rst b/docs/add_devices.rst new file mode 100644 index 0000000..5fa3e48 --- /dev/null +++ b/docs/add_devices.rst @@ -0,0 +1,128 @@ +.. _add_devices: + +############################## +Adding Support for New Devices +############################## + +This guide outlines the process of adding support for new devices to the ExEngine framework. + +1. Inherit from the Device Base Class +========================================== + +All new devices should inherit from the ``Device`` base class or one or more of its subclasses (see `exengine.kernel.device_types_base `_) + +.. code-block:: python + + from exengine.kernel.device_types_base import Device + + class ANewDevice(Device): + def __init__(self, name): + super().__init__(name) + # Your device-specific initialization code here + +2. Implement Device Functionality +========================================== + +Devices can expose functionality through properties and methods. The base ``Device`` class primarily uses properties. + +Properties are essentially attributes with additional capabilities. They can have special characteristics, which are defined by implementing abstract methods in the ``Device`` class: + +- Allowed values: Properties can have a finite set of allowed values. +- Read-only status: Properties can be set as read-only. +- Limits: Numeric properties can have upper and lower bounds. +- Triggerability: Properties can be hardware-triggerable. + +Here's an example of implementing these special characteristics: + +.. code-block:: python + + class ANewDevice(Device): + def get_allowed_property_values(self, property_name: str) -> List[str]: + if property_name == "mode": + return ["fast", "slow", "custom"] + return [] + + def is_property_read_only(self, property_name: str) -> bool: + return property_name in ["serial_number", "firmware_version"] + + def get_property_limits(self, property_name: str) -> Tuple[float, float]: + if property_name == "exposure_time": + return (0.001, 10.0) # seconds + return None + + def is_property_hardware_triggerable(self, property_name: str) -> bool: + return property_name in ["position", "gain"] + + # Implement other abstract methods... + +3. Use Specialized Device Types +========================================== + +There are specialized device types that standardize functionalities through methods. For example, a camera device type will have methods for taking images, setting exposure time, etc. Inheriting from one or more of these devices is recommended whenever possible, as it ensures compatibility with existing workflows and events. + +Specialized device types implement functionality through abstract methods that must be implemented by subclasses. For example: + +.. code-block:: python + + from exengine.kernel.device_types_base import Detector + + # TODO: may change this API in the future + class ANewCameraDevice(Detector): + def set_exposure(self, exposure: float) -> None: + # Implementation here + + def get_exposure(self) -> float: + # Implementation here + + # Implement other camera-specific methods... + +Advanced Topics +=============== + +What inheritance from ``Device`` provides +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Inheriting from the ``Device`` class or its subclasses provides two main benefits: + +1. Compatibility with events for specialized devices in the ExEngine framework, reducing the need to write hardware control code from scratch. +2. **Thread safety**. All calls to devices that may interact with hardware are automatically rerouted to a common thread. This enables code from various parts of a program to interact with a device that may not be thread safe itself. As a result, there is no need to worry about threading and synchronization concerns in devices, thereby simplifying device control code and the process of adding new devices. +3. The ability to monitor all inputs and outputs from devices. Since all calls to devices pass through the execution engine, a complete accounting of the commands sent to hardware and the data received from it can be generated, without having to write more complex code. + +Bypassing the Executor +^^^^^^^^^^^^^^^^^^^^^^ + +In some cases, you may have attributes or methods that don't interact with hardware and don't need to go through the executor. You can bypass the executor for specific attributes or for the entire device: + +1. Specify attributes to bypass in the Device constructor: + + .. code-block:: python + + class MyNewDevice(Device): + def __init__(self, name): + super().__init__(name, no_executor_attrs=('_some_internal_variable', 'some_method')) + # This will be executed on the calling thread like a normal attribute + self._some_internal_variable = 0 + + def some_method(self): + # This method will be executed directly on the calling thread + pass + +2. Bypass the executor for all attributes and methods: + + .. code-block:: python + + class MyNewDevice(Device): + def __init__(self, name): + super().__init__(name, no_executor=True) + # All attributes and methods in this class will bypass the executor + self._some_internal_variable = 0 + + def some_method(self): + # This method will be executed directly on the calling thread + pass + +Using the first approach allows you to selectively bypass the executor for specific attributes or methods, while the second approach bypasses the executor for the entire device. + +Note that when using ``no_executor_attrs``, you need to specify the names of the attributes or methods as strings in a sequence (e.g., tuple or list) passed to the ``no_executor_attrs`` parameter in the ``super().__init__()`` call. + +These approaches provide flexibility in controlling which parts of your device interact with the executor, allowing for optimization where direct access is safe and beneficial. \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py index 2723675..94319b7 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,7 +13,14 @@ # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration -extensions = [] +import sphinx_rtd_theme + + +extensions = [ + 'sphinx_rtd_theme', + 'sphinx_togglebutton', +] + templates_path = ['_templates'] exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] @@ -23,5 +30,9 @@ # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' -html_static_path = ['_static'] +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] \ No newline at end of file diff --git a/docs/examples.rst b/docs/examples.rst new file mode 100644 index 0000000..2286b95 --- /dev/null +++ b/docs/examples.rst @@ -0,0 +1,12 @@ +.. _examples: + +############################## +Examples +############################## + +.. toctree:: + :maxdepth: 1 + + micro-manager_backend_example + + diff --git a/docs/extending.rst b/docs/extending.rst new file mode 100644 index 0000000..8188588 --- /dev/null +++ b/docs/extending.rst @@ -0,0 +1,11 @@ +.. _extending: + +############################## +Extending ExEngine +############################## + +.. toctree:: + :maxdepth: 1 + + add_devices + diff --git a/docs/index.rst b/docs/index.rst index 614decb..8f7e9dd 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,17 +1,20 @@ -.. ExEngine documentation master file, created by - sphinx-quickstart on Sat Jul 27 07:24:24 2024. - You can adapt this file completely to your liking, but it should at least - contain the root `toctree` directive. +##################################################### +ExEngine: An execution engine for microscope control +##################################################### -ExEngine documentation -====================== +.. toggle:: Click to expand + + This is the collapsible content. It is hidden by default, until the toggle button is clicked. -Add your content using ``reStructuredText`` syntax. See the -`reStructuredText `_ -documentation for details. .. toctree:: - :maxdepth: 2 + :maxdepth: 1 :caption: Contents: + motivation + overview + setup + usage + extending + examples \ No newline at end of file diff --git a/docs/micro-manager_backend_example.rst b/docs/micro-manager_backend_example.rst new file mode 100644 index 0000000..5992375 --- /dev/null +++ b/docs/micro-manager_backend_example.rst @@ -0,0 +1,92 @@ +.. _micro-manager_backend_example: + +################################################################## +Using ExEngine with Micro-Manager Backend +################################################################## + +Installation +------------ + +1. Clone the repository +2. Install ExEngine, specifying which device and data storage backends: + + .. code-block:: bash + + pip install -e ".[micromanager, ndstorage]" + +3. Install Micro-Manager: + + .. code-block:: python + + from mmpycorex import download_and_install_mm + download_and_install_mm() + +Running the ExEngine +-------------------- + +.. code-block:: python + + from mmpycorex import create_core_instance, download_and_install_mm, terminate_core_instances + from exengine.kernel.executor import ExecutionEngine + from exengine.kernel.data_coords import DataCoordinates + from exengine.kernel.ex_event_base import DataHandler + from exengine.backends.micromanager.mm_device_implementations import MicroManagerCamera, MicroManagerSingleAxisStage + from exengine.storage_backends.NDTiffandRAM import NDRAMStorage + from exengine.events.detector_events import StartCapture, ReadoutData + + # Start Micro-Manager core instance with Demo config + create_core_instance() + executor = ExecutionEngine() + + # Get access to the micro-manager devices + camera = MicroManagerCamera() + z_stage = MicroManagerSingleAxisStage() + +Example 1: Use the ExEngine to Acquire a Timelapse +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + # Capture 100 images on the camera + num_images = 100 + data_handler = DataHandler(storage=NDRAMStorage()) + start_capture_event = StartCapture(num_images=num_images, detector=camera) + readout_images_event = ReadoutData(num_images=num_images, detector=camera, + data_coordinates_iterator=[DataCoordinates(time=t) for t in range(num_images)], + data_handler=data_handler) + _ = executor.submit(start_capture_event) + future = executor.submit(readout_images_event) + + # Block until all images have been read out + future.await_execution() + + # Tell the data handler no more images are expected + data_handler.finish() + +Example 2: Create Series of Events with Multi-D Function +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + from exengine.events.multi_d_events import multi_d_acquisition_events + + data_handler = DataHandler(storage=NDRAMStorage()) + events = multi_d_acquisition_events(z_start=0, z_end=10, z_step=2) + futures = executor.submit(events) + + # Wait until the final event finished + futures[-1].await_execution() + + # Tell the data handler no more images are expected + data_handler.finish() + +Shutdown +^^^^^^^^ + +.. code-block:: python + + # Shutdown the engine + executor.shutdown() + + # Shutdown micro-manager + terminate_core_instances() \ No newline at end of file diff --git a/docs/motivation.rst b/docs/motivation.rst new file mode 100644 index 0000000..9fe8cf2 --- /dev/null +++ b/docs/motivation.rst @@ -0,0 +1,30 @@ +.. _motivation: + +########### +Motivation +########### + +Challenges in Modern Microscopy +=============================== + +Advanced microscopes require sophisticated software to function effectively. As microscopy techniques evolve, so do the challenges of control software: + +1. **Scalability**: + The evolution from proof-of-concept to production systems in microscopy creates a dynamic set of software requirements. In the early stages, prototype systems demand low-level hardware control and high customizability. As systems mature, the focus shifts: the hardware control layer needs to stabilize, while flexibility becomes crucial for higher-level experimental design. + + This transition often results in significant software discontinuities. A common pattern illustrates this challenge: many novel microscopy systems are initially developed using LabVIEW for its rapid prototyping capabilities. However, as these systems advance towards production, teams frequently find themselves completely rewriting the control software, often transitioning to platforms like Micro-Manager. + + +2. **AI-driven Smart Microscopy**: + Most current user interfaces for microscopes are designed for human operators, AI shows promise in automating complex workflows. However, we lack standardized methods to accurately capture and describe routine and complex experimental workflows, hindering their potential for automation. + +3. **Ecosystem Lock-in**: + Current solutions often create vertically integrated control packages, tying users to specific ecosystems. These typically include: + + a. A device control layer + b. A layer for controlling and synchronizing multiple devices + c. A graphical user interface + + While device layers may be reusable, the acquisition engine layer is often bespoke and hard coded for specific device libraries, limiting interoperability. + + diff --git a/docs/overview.rst b/docs/overview.rst new file mode 100644 index 0000000..4424992 --- /dev/null +++ b/docs/overview.rst @@ -0,0 +1,29 @@ +.. _overview: + +################## +ExEngine Overview +################## + +ExEngine is designed as a general-purpose acquisition engine layer, separating hardware control execution from its specification. + + +ExEngine strikes a balance between simplicity and expressiveness, enabling researchers to create sophisticated microscopy experiments without getting bogged down in low-level details. It provides a foundation for building scalable, interoperable, and future-proof microscopy control systems. + + +Design Principles +""""""""""""""""" + +- **Backend-agnosticism**: While Micro-Manager is supported, ExEngine can seamlessly integrate with any (or multiple) hardware control backends, providing equal functionality and performance across different systems. +- **Non-obtrusiveness**: Minimal assumptions about hardware devices, allowing new device support to be added with maximum flexibility and minimal effort. +- **Versatility**: Multiple sets of independently usable features and levels of integration possibilities. +- **Extensibility**: Hardware setup is abstracted away from execution capabilities, allowing new capabilities with entirely different functionality to be added. + + +Key features +"""""""""""" + +1. **Broad and extensible hardware support** Supports the full suite of hardware in micro-manager, but is not designed specially for them. More backends can allow more devices to be used, and used in combination + +2. **Parallelization and thread safety** You need parallelism with high performance software, but this often leads to hard to diagnose problems because devices that were designed with concurrency in mind get accessed from multiple places in the software, leading to problems. ExEngine can automatically handle this by rerouting calls to devices through a common pool of threads. Significantly, this happens automatically under the hood, adding little to no complexity to writing control code. + +3. **Built for humans and AI**: Enables complete tracking of commands sent to hardware and data received, without complex additional code. This facilitates tracking and automation of microscopes using AI. diff --git a/docs/requirements.txt b/docs/requirements.txt index e69de29..8f2c027 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +sphinx-rtd-theme +sphinx-togglebutton \ No newline at end of file diff --git a/docs/setup.rst b/docs/setup.rst new file mode 100644 index 0000000..2d7f761 --- /dev/null +++ b/docs/setup.rst @@ -0,0 +1,8 @@ +.. _setup: + +################## +Installation/setup +################## + + +- Install `exengine` \ No newline at end of file diff --git a/docs/usage.rst b/docs/usage.rst new file mode 100644 index 0000000..20f2329 --- /dev/null +++ b/docs/usage.rst @@ -0,0 +1,67 @@ +.. _usage: + +###### +Usage +###### + +ExEngine is built around four key abstractions: Devices, Events, Futures, and Notifications. + +Devices +======= +Devices in ExEngine are software representations of hardware components in a microscope system. When possible, they provide a consistent way to interact with diverse equipment, abstracting away the complexities of individual hardware implementations. When not possible, devices can additionally expose specialized APIs specific to individual components. + +ExEngine supports multiple **backends** - individual devices or libraries of devices (e.g., Micro-Manager). The method to create devices depends on the specific backend in use. + + +Here's a minimal example using the Micro-Manager backend: + +.. code-block:: python + + from mmpycorex import create_core_instance + from exengine.kernel.executor import ExecutionEngine + from exengine.backends.micromanager.mm_device_implementations import MicroManagerSingleAxisStage + + # Create the ExecutionEngine + executor = ExecutionEngine() + + # Initialize Micro-Manager core + create_core_instance(config_file='MMConfig_demo.cfg') + + # Access Micro-Manager device + z_stage = MicroManagerSingleAxisStage() + + z_stage.set_position(1234.56) + + +Device Hierarchies +"""""""""""""""""" + +Devices in ExEngine exist in hierarchies. All devices must inherit from the exengine.Device base class. Further functionality can be standardized by inheriting from one or more specific device type classes. For example, ``MicroManagerSingleAxisStage`` is a subclass of ``exengine.SingleAxisPositioner``, which is itself a subclass of ``exengine.Device``. + +The advantage of this hierarchical structure is that it standardizes functionality, allowing code to be written for generic device types (e.g., a single axis positioner) that will work with many different device libraries. This approach enables a common API across different libraries of devices, similar to Micro-Manager's device abstraction layer but on a meta-level - spanning multiple device ecosystems rather than just devices within a single project. However, ExEngine's device system is designed to be adaptable. While adhering to the standardized interfaces offers the most benefits, users can still leverage many advantages of the system without implementing these specialized APIs. + + +TODO: thread standardization features of devices (and how to turn off) + +TODO: calling functions on devices directly + +TODO: link to guide to adding backends + +TODO: transition to more complex with events + + +Events +====== +Events are modular units of instructions and or computation. They can be as simple as a single command like moving the position of a hardware device, or contain multiple steps and computation. They provide building blocks to create more complex experimental workflows. + + +TODO: what else should be said about events? + + +Notifications +============= +Notifications provide a mechanism for asynchronous communication within the system. They allow devices, events, and other components to broadcast updates about their status or important occurrences. This feature enables reactive programming patterns, allowing your software to respond dynamically to changes in the system state or experimental conditions. + +Futures +======= +Futures represent the outcome of asynchronous operations. They provide a way to handle long-running tasks without blocking the main execution thread. Futures allow you to submit events for execution and then either wait for their completion or continue with other tasks, checking back later for results. This enables efficient, non-blocking execution of complex workflows. diff --git a/micro-manager_setup.md b/micro-manager_setup.md deleted file mode 100644 index a527163..0000000 --- a/micro-manager_setup.md +++ /dev/null @@ -1,83 +0,0 @@ - # Setup and examples for using the ExEngine with a micro-manager backend - - - clone the repository - - install it, specifying which device and data storage backends - `pip install -e ".[micromanager, ndstorage]"` - - install Micro-Manager - -```python -from mmpycorex import download_and_install_mm -download_and_install_mm() -``` - - -## Running the ExEngine - -```python -from mmpycorex import create_core_instance, download_and_install_mm, terminate_core_instances -from exengine.kernel.executor import ExecutionEngine -from exengine.kernel.data_coords import DataCoordinates -from exengine.kernel.ex_event_base import DataHandler -from exengine.backends.micromanager.mm_device_implementations import MicroManagerCamera, MicroManagerSingleAxisStage -from exengine.storage_backends.NDTiffandRAM import NDRAMStorage -from exengine.events.detector_events import StartCapture, ReadoutData - - -# Start Micro-Manager core instance with Demo config -create_core_instance() - -executor = ExecutionEngine() - - -# get access to the micro-manager devices -camera = MicroManagerCamera() -z_stage = MicroManagerSingleAxisStage() - -``` - - -### Example 1: Use the exengine to acquire a timelapse - -```python -# Capture 100 images on the camera -num_images = 100 -data_handler = DataHandler(storage=NDRAMStorage()) - -start_capture_event = StartCapture(num_images=num_images, detector=camera) -readout_images_event = ReadoutData(num_images=num_images, detector=camera, - data_coordinates_iterator=[DataCoordinates(time=t) for t in range(num_images)], - data_handler=data_handler) - -_ = executor.submit(start_capture_event) -future = executor.submit(readout_images_event) - -# block until all images have been read out -future.await_execution() - -# Tell the data handler no more images are expected -data_handler.finish() - -``` - -### Example 2: create series of events with multi-d function -```python -from exengine.events.multi_d_events import multi_d_acquisition_events - -data_handler = DataHandler(storage=NDRAMStorage()) -events = multi_d_acquisition_events(z_start=0, z_end=10, z_step=2) - -futures = executor.submit(events) -# wait until the final event finished -futures[-1].await_execution() - -# Tell the data handler no more images are expected -data_handler.finish() -``` - - -```python -# shutdown the engine -executor.shutdown() -# shutdown micro-manager -terminate_core_instances() -``` diff --git a/pyproject.toml b/pyproject.toml index 51b3405..b55d258 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,8 @@ micromanager = ["mmpycorex"] # storage backends ndstorage = ["ndstorage"] +# documentation +docs = ["sphinx"] [project.urls] Home = "https://github.com/micro-manager/ExEngine" diff --git a/readthedocs.yaml b/readthedocs.yaml index 80c6a70..8d54f82 100644 --- a/readthedocs.yaml +++ b/readthedocs.yaml @@ -4,7 +4,6 @@ version: 2 sphinx: configuration: docs/conf.py - # Specify the OS and Python version used by Read the Docs build: os: ubuntu-20.04 diff --git a/src/exengine/kernel/scratch.py b/src/exengine/kernel/scratch.py deleted file mode 100644 index 5473adf..0000000 --- a/src/exengine/kernel/scratch.py +++ /dev/null @@ -1,4 +0,0 @@ -# print the current python version - -import sys -print(sys.version) \ No newline at end of file