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 bc0e74d..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 @@ -40,6 +42,11 @@ PDM is used as the project manager for EOS, making it easier to install dependen 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 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/eos/tasks/entities/test.py b/eos/tasks/entities/test.py new file mode 100644 index 0000000..c2470b3 --- /dev/null +++ b/eos/tasks/entities/test.py @@ -0,0 +1,121 @@ +import asyncio +from dataclasses import asdict +from datetime import datetime, timezone +from eos.persistence.async_db_interface import AsyncDbInterface +from eos.persistence.service_credentials import ServiceCredentials +from eos.tasks.entities.task import Task, TaskInput, TaskOutput, TaskStatus, TaskDeviceConfig, TaskModel +from eos.experiments.entities.experiment import ( + Experiment, + ExperimentStatus, + ExperimentExecutionParameters, + ExperimentModel, +) +from sqlalchemy.ext.asyncio import AsyncSession + + +async def main(): + # Initialize the database interface + db = AsyncDbInterface( + db_credentials=ServiceCredentials( + host="localhost", + port=5432, + username="eos-user", + password="eos-password", + ), + db_name="eos", + ) + + # Create tables + await db.create_tables() + + async with db.session_provider.get_session() as session: + # Create a pydantic Experiment + pydantic_experiment = Experiment( + id="exp-001", + type="analysis_experiment", + execution_parameters=ExperimentExecutionParameters(resume=False), + status=ExperimentStatus.CREATED, + labs=["lab-001"], + dynamic_parameters={"param1": {"value": "test"}}, + metadata={"created_by": "user123"}, + created_at=datetime.now(timezone.utc), + ) + + print("Pydantic Experiment:") + print(pydantic_experiment.model_dump()) + + # Convert pydantic Experiment to ExperimentModel + experiment_model = ExperimentModel( + id=pydantic_experiment.id, + type=pydantic_experiment.type, + execution_parameters=pydantic_experiment.execution_parameters.model_dump(), + status=pydantic_experiment.status, + labs=pydantic_experiment.labs, + running_tasks=pydantic_experiment.running_tasks, + completed_tasks=pydantic_experiment.completed_tasks, + dynamic_parameters=pydantic_experiment.dynamic_parameters, + experiment_metadata=pydantic_experiment.metadata, + start_time=pydantic_experiment.start_time, + end_time=pydantic_experiment.end_time, + created_at=pydantic_experiment.created_at, + ) + + # Add experiment to session and flush + session.add(experiment_model) + await session.flush() + + print(f"Experiment {experiment_model.id} has been created and stored in the database.") + + # Create a pydantic Task associated with the experiment + pydantic_task = Task( + id="task-001", + type="analysis", + experiment_id=pydantic_experiment.id, + devices=[TaskDeviceConfig(id="device-001", lab_id="lab-001")], + input=TaskInput(parameters={"param1": "value1"}), + output=TaskOutput(), + status=TaskStatus.CREATED, + metadata={"created_by": "user123"}, + created_at=datetime.now(timezone.utc), + ) + + print("\nPydantic Task:") + print(pydantic_task.model_dump()) + + # Convert pydantic Task to TaskModel + task_model = TaskModel( + id=pydantic_task.id, + type=pydantic_task.type, + experiment_id=pydantic_task.experiment_id, + devices=[asdict(device) for device in pydantic_task.devices], + input=pydantic_task.input.model_dump(), + output=pydantic_task.output.model_dump(), + status=pydantic_task.status.value, + task_metadata=pydantic_task.metadata, + start_time=pydantic_task.start_time, + end_time=pydantic_task.end_time, + created_at=pydantic_task.created_at, + ) + + # Add task to session and flush + session.add(task_model) + await session.flush() + + print(f"Task {task_model.id} has been created and stored in the database.") + + # Verify that both experiment and task were stored by querying them + stored_experiment = await session.get(ExperimentModel, experiment_model.id) + print(f"\nRetrieved experiment: {stored_experiment.id}, status: {stored_experiment.status}") + + stored_task = await session.get(TaskModel, task_model.id) + print(f"Retrieved task: {stored_task.id}, status: {stored_task.status}") + + # Commit the transaction + await session.commit() + + # Close the database connection + await db.close() + + +if __name__ == "__main__": + asyncio.run(main()) 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.