diff --git a/.gitignore b/.gitignore index 3a46e4d..48f15ed 100644 --- a/.gitignore +++ b/.gitignore @@ -170,4 +170,5 @@ cython_debug/ /config.yml /user/* +!/user/example/ !/user/.gitkeep diff --git a/config.example.yml b/config.example.yml index 108d0c4..6c7a388 100644 --- a/config.example.yml +++ b/config.example.yml @@ -1,10 +1,9 @@ user_dir: ./user labs: - - lab1 - - lab2 + - multiplication_lab experiments: - - experiment1 - - experiment2 + - optimize_multiplication + log_level: INFO # EOS orchestrator's internal web API server configuration @@ -12,14 +11,14 @@ web_api: host: localhost port: 8070 -# EOS database configuration +# EOS database (MongoDB) configuration db: host: localhost port: 27017 username: "" password: "" -# EOS file database configuration +# EOS file database (MinIO) configuration file_db: host: localhost port: 9004 diff --git a/docs/user-guide/configuration.rst b/docs/user-guide/configuration.rst index eed2eb0..62fde96 100644 --- a/docs/user-guide/configuration.rst +++ b/docs/user-guide/configuration.rst @@ -5,7 +5,8 @@ After installation, you need to configure external services such as MongoDB and 1. Configure External Services ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -We provide a Docker Compose file that can run the external services. +We provide a Docker Compose file that can run the external services. You do not need to install external services +manually, just provide configuration values and Docker Compose will take care of the rest. Copy the example environment file: @@ -26,3 +27,6 @@ Copy the example configuration file: cp config.example.yml config.yml Edit `config.yml`. Ensure that credentials are provided for the MongoDB and MinIO services. + +By default, EOS loads the "multiplication_lab" laboratory and the "optimize_multiplication" experiment from an example +EOS package. Feel free to change this. diff --git a/docs/user-guide/installation.rst b/docs/user-guide/installation.rst index 5eeeb5d..6fd7aa0 100644 --- a/docs/user-guide/installation.rst +++ b/docs/user-guide/installation.rst @@ -18,6 +18,8 @@ We provide a Docker Compose file that can set up all of these services for you. ^^^^^^^^^^^^^^ PDM is used as the project manager for EOS, making it easier to install dependencies and build it. +See the `PDM documentation `_ for more information or if you encounter any issues. + .. tab-set:: .. tab-item:: Linux/Mac @@ -38,7 +40,20 @@ PDM is used as the project manager for EOS, making it easier to install dependen git clone https://github.com/UNC-Robotics/eos -3. Install Dependencies +3. Make a Virtual Environment +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +We create a virtual environment to isolate the dependencies of EOS from the rest of the system. The virtual environment +is created in a ``env`` directory inside the EOS repository directory. Feel free to use PDM to manage the virtual +environment instead. Other sections of the documentation will assume that you are using a virtual environment located +inside the EOS repository directory. + +.. code-block:: shell + + cd eos # Navigate to the cloned repository + python3 -m venv env + source env/bin/activate + +4. Install Dependencies ^^^^^^^^^^^^^^^^^^^^^^^ Navigate to the cloned repository and run: diff --git a/eos/cli/pkg_cli.py b/eos/cli/pkg_cli.py index 50bff34..0300c18 100644 --- a/eos/cli/pkg_cli.py +++ b/eos/cli/pkg_cli.py @@ -15,12 +15,18 @@ def create_package( ) -> None: """Create a new package with the specified name in the user directory.""" package_dir = Path(user_dir) / name - subdirs = ["common", "devices", "tasks", "labs", "experiments"] + subdirs = ["devices", "tasks", "labs", "experiments"] try: package_dir.mkdir(parents=True, exist_ok=False) for subdir in subdirs: (package_dir / subdir).mkdir() + + # Create README.md with just the package name + readme_content = f"# {name}" + readme_path = package_dir / "README.md" + readme_path.write_text(readme_content) + typer.echo(f"Successfully created package '{name}' in {package_dir}") except FileExistsError: typer.echo(f"Error: Package '{name}' already exists in {user_dir}", err=True) diff --git a/user/example/README.md b/user/example/README.md new file mode 100644 index 0000000..cbc06ed --- /dev/null +++ b/user/example/README.md @@ -0,0 +1,17 @@ +# Example EOS Package +This is a very simple EOS package that implements an experiment for finding the smallest number that when multiplied by +two factors is as close as possible to 1024. + +## Experiments +The package contains the **optimize_multiplication** experiment which works as explained above. + +## Laboratories +The package defines a very basic laboratory containing a "multiplier" and an "analyzer" device. + +## Devices +1. **Multiplier**: Provides a function for multiplying two numbers. +2. **Analyzer**: Provides a function for producing a score on how close we are to the objective of the experiment. + +## Tasks +1. **Multiply**: Multiplies two numbers using the multiplier device. +2. **Score Multiplication**: Scores the multiplication using the analyzer device. diff --git a/user/example/devices/analyzer/device.py b/user/example/devices/analyzer/device.py new file mode 100644 index 0000000..9d43be7 --- /dev/null +++ b/user/example/devices/analyzer/device.py @@ -0,0 +1,19 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class AnalyzerDevice(BaseDevice): + """Analyzes the multiplication result to produce a loss.""" + + async def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + async def _cleanup(self) -> None: + pass + + async def _report(self) -> dict[str, Any]: + pass + + def analyze_result(self, number: int, product: int) -> int: + return number + 100 * abs(product - 1024) diff --git a/user/example/devices/analyzer/device.yml b/user/example/devices/analyzer/device.yml new file mode 100644 index 0000000..a214e23 --- /dev/null +++ b/user/example/devices/analyzer/device.yml @@ -0,0 +1,2 @@ +type: analyzer +description: A device for analyzing the result of the multiplication of some numbers and computing a loss. diff --git a/user/example/devices/multiplier/device.py b/user/example/devices/multiplier/device.py new file mode 100644 index 0000000..eed5905 --- /dev/null +++ b/user/example/devices/multiplier/device.py @@ -0,0 +1,19 @@ +from typing import Any + +from eos.devices.base_device import BaseDevice + + +class MultiplierDevice(BaseDevice): + """Multiplies two numbers.""" + + async def _initialize(self, initialization_parameters: dict[str, Any]) -> None: + pass + + async def _cleanup(self) -> None: + pass + + async def _report(self) -> dict[str, Any]: + pass + + def multiply(self, a: int, b: int) -> int: + return a * b diff --git a/user/example/devices/multiplier/device.yml b/user/example/devices/multiplier/device.yml new file mode 100644 index 0000000..efe0ee4 --- /dev/null +++ b/user/example/devices/multiplier/device.yml @@ -0,0 +1,2 @@ +type: multiplier +description: A device for multiplying two numbers diff --git a/user/example/experiments/optimize_multiplication/experiment.yml b/user/example/experiments/optimize_multiplication/experiment.yml new file mode 100644 index 0000000..9f91aa4 --- /dev/null +++ b/user/example/experiments/optimize_multiplication/experiment.yml @@ -0,0 +1,35 @@ +type: optimize_multiplication +description: An experiment for finding the smallest number that when multiplied by two factors yields 1024 + +labs: + - multiplication_lab + +tasks: + - id: mult_1 + type: Multiplication + devices: + - lab_id: multiplication_lab + id: multiplier + parameters: + number: eos_dynamic + factor: eos_dynamic + + - id: mult_2 + type: Multiplication + devices: + - lab_id: multiplication_lab + id: multiplier + dependencies: [ mult_1 ] + parameters: + number: mult_1.product + factor: eos_dynamic + + - id: score_multiplication + type: Score Multiplication + devices: + - lab_id: multiplication_lab + id: analyzer + dependencies: [ mult_1, mult_2 ] + parameters: + number: mult_1.number + product: mult_2.product diff --git a/user/example/experiments/optimize_multiplication/optimizer.py b/user/example/experiments/optimize_multiplication/optimizer.py new file mode 100644 index 0000000..aa96a80 --- /dev/null +++ b/user/example/experiments/optimize_multiplication/optimizer.py @@ -0,0 +1,27 @@ +from bofire.data_models.acquisition_functions.acquisition_function import qLogNEI +from bofire.data_models.enum import SamplingMethodEnum +from bofire.data_models.features.continuous import ContinuousOutput +from bofire.data_models.features.discrete import DiscreteInput +from bofire.data_models.objectives.identity import MinimizeObjective + +from eos.optimization.abstract_sequential_optimizer import AbstractSequentialOptimizer +from eos.optimization.sequential_bayesian_optimizer import BayesianSequentialOptimizer + + +def eos_create_campaign_optimizer() -> tuple[dict, type[AbstractSequentialOptimizer]]: + constructor_args = { + "inputs": [ + DiscreteInput(key="mult_1.number", values=list(range(2, 34))), + DiscreteInput(key="mult_1.factor", values=list(range(2, 18))), + DiscreteInput(key="mult_2.factor", values=list(range(2, 18))), + ], + "outputs": [ + ContinuousOutput(key="score_multiplication.loss", objective=MinimizeObjective(w=1.0)), + ], + "constraints": [], + "acquisition_function": qLogNEI(), + "num_initial_samples": 5, + "initial_sampling_method": SamplingMethodEnum.SOBOL, + } + + return constructor_args, BayesianSequentialOptimizer diff --git a/user/example/labs/multiplication_lab/lab.yml b/user/example/labs/multiplication_lab/lab.yml new file mode 100644 index 0000000..4c4ccf0 --- /dev/null +++ b/user/example/labs/multiplication_lab/lab.yml @@ -0,0 +1,10 @@ +type: multiplication_lab +description: An example laboratory for testing multiplication + +devices: + multiplier: + type: multiplier + computer: eos_computer + analyzer: + type: analyzer + computer: eos_computer diff --git a/user/example/tasks/multiplication/task.py b/user/example/tasks/multiplication/task.py new file mode 100644 index 0000000..9e18aaf --- /dev/null +++ b/user/example/tasks/multiplication/task.py @@ -0,0 +1,15 @@ +from eos.tasks.base_task import BaseTask + + +class MultiplicationTask(BaseTask): + async def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + multiplier = devices.get_all_by_type("multiplier")[0] + product = multiplier.multiply(parameters["number"], parameters["factor"]) + output_parameters = {"product": product} + + return output_parameters, None, None diff --git a/user/example/tasks/multiplication/task.yml b/user/example/tasks/multiplication/task.yml new file mode 100644 index 0000000..7f78d7f --- /dev/null +++ b/user/example/tasks/multiplication/task.yml @@ -0,0 +1,21 @@ +type: Multiplication +description: This task takes a number and a factor and multiplies them together using a "multiplier" device. + +device_types: + - multiplier + +input_parameters: + number: + type: integer + unit: none + description: The number to multiply. + factor: + type: integer + unit: none + description: The factor to multiply the number by. + +output_parameters: + product: + type: integer + unit: none + description: The product of the number and the factor. diff --git a/user/example/tasks/score_multiplication/task.py b/user/example/tasks/score_multiplication/task.py new file mode 100644 index 0000000..a6f4046 --- /dev/null +++ b/user/example/tasks/score_multiplication/task.py @@ -0,0 +1,15 @@ +from eos.tasks.base_task import BaseTask + + +class ScoreMultiplicationTask(BaseTask): + async def _execute( + self, + devices: BaseTask.DevicesType, + parameters: BaseTask.ParametersType, + containers: BaseTask.ContainersType, + ) -> BaseTask.OutputType: + analyzer = devices.get_all_by_type("analyzer")[0] + loss = analyzer.analyze_result(parameters["number"], parameters["product"]) + output_parameters = {"loss": loss} + + return output_parameters, None, None diff --git a/user/example/tasks/score_multiplication/task.yml b/user/example/tasks/score_multiplication/task.yml new file mode 100644 index 0000000..f0080e6 --- /dev/null +++ b/user/example/tasks/score_multiplication/task.yml @@ -0,0 +1,21 @@ +type: Score Multiplication +description: Scores multiplication based on how close the product is to 1024 and how small the initial number is using an "analyzer" device. + +device_types: + - analyzer + +input_parameters: + number: + type: integer + unit: none + description: The number that was multiplied with some factors. + product: + type: integer + unit: none + description: The final product after multiplying with some factors. + +output_parameters: + loss: + type: integer + unit: none + description: The multiplication loss. Captures how far the product is from 1024 and how large the initial number is.