Skip to content

📣 Release Notes - 0.7.0

0.7.0 is a huge release with quite some breaking changes as we are working towards a 1.0.0 release.

⏰ For some statistics! This release contains 344 changed files with 16,423 additions and 9,679 deletions.

💥 Breaking Change

Breaking changes happen, we aim to help create a migration guide that helps you transition from the old version to the new one. Please read through this from top to bottom and apply the required changes accordingly.

Dropping support for Python 3.8 and 3.9

We are dropping support for Python 3.8 and 3.9 as they are nearing end of life. This means that to use Composabl, you will have to utilize Python 3.10 or 3.11 going foward. There are several reasons that made us make this decision:

  • Python 3.8 and 3.9 are nearing end of life
  • Pickling of certain aspects to the SDK is not possible in 3.8 and 3.9
  • Ray is dropping support for 3.8
  • Async/await support is not completely possible in 3.8 or 3.9

Standardizing Namings

To conform to the Machine Teaching standard and to make it clearer what a Skill is, we renamed the following and moved it from namespaces:

ClassNameOld NamespaceNew Namespace
SkillControlleragent.controlleragent.skill.skill_controller
SkillTeacheragent.teachingagent.skill.skill_teacher
ServerComposablcomposabl_core.grpc.servercomposabl_core.networking.server_composabl

Async Implementation / Standardizing API Specification

We implemented async/await within the codebase for optimized I/O operations and to improve the overall performance of the SDK. This change is a breaking change as it requires a substantial overhaul of the existing codebase. This means that the following will change on the implementation side:

[Simulator] ServerComposabl

We have renamed the methods to follow snake-case and made them async/await compatible:

python
class ServerComposabl(abc.ABC):
    @abc.abstractmethod
    async def make(self, env_id: str, env_init: Dict[str, Any]) -> EnvSpec:
        raise NotImplementedError

    @abc.abstractmethod
    async def observation_space_info(self) -> Space:
        raise NotImplementedError

    @abc.abstractmethod
    async def action_space_info(self) -> Space:
        raise NotImplementedError

    @abc.abstractmethod
    async def action_space_sample(self) -> Union[Any, List[Any]]:
        raise NotImplementedError

    @abc.abstractmethod
    async def reset(self) -> Tuple[Any, Dict[str, Any]]:
        raise NotImplementedError

    @abc.abstractmethod
    async def step(self, action) -> Tuple[Any, SupportsFloat, bool, bool, Dict[str, Any]]:
        raise NotImplementedError

    @abc.abstractmethod
    async def close(self):
        raise NotImplementedError

    @abc.abstractmethod
    async def set_scenario(self, scenario):
        raise NotImplementedError

    @abc.abstractmethod
    async def get_scenario(self):
        raise NotImplementedError

    @abc.abstractmethod
    async def set_render_mode(self, render_mode):
        raise NotImplementedError

    @abc.abstractmethod
    async def get_render_mode(self, render_mode):
        raise NotImplementedError

    @abc.abstractmethod
    async def get_render(self, render_mode):
        raise NotImplementedError

[Simulator] ServerComposabl

We have renamed the methods to follow snake-case and made them async/await compatible:

python
class ServerComposabl(abc.ABC):
    @abc.abstractmethod
    async def make(self, env_id: str, env_init: Dict[str, Any]) -> EnvSpec:
        raise NotImplementedError

    @abc.abstractmethod
    async def observation_space_info(self) -> Space:
        raise NotImplementedError

    @abc.abstractmethod
    async def action_space_info(self) -> Space:
        raise NotImplementedError

    @abc.abstractmethod
    async def action_space_sample(self) -> Union[Any, List[Any]]:
        raise NotImplementedError

    @abc.abstractmethod
    async def reset(self) -> Tuple[Any, Dict[str, Any]]:
        raise NotImplementedError

    @abc.abstractmethod
    async def step(self, action) -> Tuple[Any, SupportsFloat, bool, bool, Dict[str, Any]]:
        raise NotImplementedError

    @abc.abstractmethod
    async def close(self):
        raise NotImplementedError

    @abc.abstractmethod
    async def set_scenario(self, scenario):
        raise NotImplementedError

    @abc.abstractmethod
    async def get_scenario(self):
        raise NotImplementedError

    @abc.abstractmethod
    async def set_render_mode(self, render_mode):
        raise NotImplementedError

    @abc.abstractmethod
    async def get_render_mode(self, render_mode):
        raise NotImplementedError

    @abc.abstractmethod
    async def get_render(self, render_mode):
        raise NotImplementedError

[Agent Creation] SkillController

We have renamed the methods to follow snake-case and made them async/await compatible:

python
class SkillController(ABC):
    def __init__(self):
        self.action_space = None
        self.observation_space = None
        pass

    @abstractmethod
    async def compute_action(self, transformed_obs, action):
        pass

    @abstractmethod
    async def transform_obs(self, obs):
        pass

    @abstractmethod
    async def filtered_observation_space(self):
        pass

    @abstractmethod
    async def compute_success_criteria(self, transformed_obs, action):
        pass

    @abstractmethod
    async def compute_termination(self, transformed_obs, action):
        pass

[Agent Creation] SkillTeacher

We have renamed the methods to follow snake-case and made them async/await compatible:

python
class SkillTeacher(ABC):
    def __init__(self):
        pass

    @abstractmethod
    async def compute_reward(self, transformed_obs, action, sim_reward):
        pass

    @abstractmethod
    async def compute_action_mask(self, transformed_obs, action):
        pass

    @abstractmethod
    async def compute_success_criteria(self, transformed_obs, action) -> bool:
        pass

    @abstractmethod
    async def compute_termination(self, transformed_obs, action) -> bool:
        pass

    @abstractmethod
    async def transform_obs(self, obs, action):
        pass

    @abstractmethod
    async def transform_action(self, transformed_obs, action):
        pass

    @abstractmethod
    async def filtered_observation_space(self) -> List[str]:
        pass

[Agent Creation] SkillCoach

We have renamed the methods to follow snake-case and made them async/await compatible:

python
class SkillCoach(ABC):
    def __init__(self):
        pass

    @abstractmethod
    async def compute_reward(self, transformed_obs, action, sim_reward):
        """
        Computes the reward for the given transformed observation and action
        :param transformed_obs: The transformed observation
        :param action: The actions dict
        :param sim_reward: The reward from the simulation
        :return: The reward, as a dictionary, with each key the sub-skill name and the value the reward
        """
        pass

    @abstractmethod
    async def compute_action_mask(self, transformed_obs, action):
        pass

    @abstractmethod
    async def compute_success_criteria(self, transformed_obs, action) -> bool:
        pass

    @abstractmethod
    async def compute_termination(self, transformed_obs, action) -> bool:
        pass

    @abstractmethod
    async def transform_action(self, transformed_obs, action):
        pass

[Agent Creation] Perceptors

Perceptors have more changes as we support portable perceptors as well. This brings the following breaking changes:

  • We have renamed the methods to follow snake-case
  • We have made the methods async/await compatible where required
  • We have created a new PerceptorImpl class that specifies the API implementation for the perceptor which you should inherit from
  • compute now receives the Observation Specification as the first argument (i.e., compute(self, observation) becomes compute(self, observation_spec, observation))
python
class PerceptorImpl(ABC):
    def __init__(self):
        pass

    @abc.abstractmethod
    async def compute(self, obs_spec, obs):
        """
        Compute the return value for this perceptor based on the observation space
        """
        raise NotImplementedError

    @abc.abstractmethod
    def filtered_observation_space(self, obs):
        """
        Return the observation space part that is needed
        """
        raise NotImplementedError

🚀 Features

Besides breaking changes, we also have added more exciting new features to the SDK.

CLI - Skills, Perceptors and Simulators Templates

You can now create new skills, perceptors and simulators through the CLI! For this you can simply use the below, which will prompt you with the required information.

bash
# Create a new skill
composabl skill create

# Create a new perceptor
composabl perceptor create

# Create a new simulator
composabl sim create

Sensor Mapping

Sensors were simple, requiring you to follow the observation spec order in which they appeared and map them out through keys and descriptions. We have now added a feature that allows you to create a Sensor from the entire observation space and narrow it down as much as required.

This would allow easier support for more complex sensors such as LIDARs, Cameras, ... requiring huge observation space mappings otherwise, which you can now simply define through one sensor, or a sensor per color channel for a camera.

python
# LIDAR Example
sensor_lidar = Sensor("lidar", "LIDAR", mapping=lambda obs: obs["lidar"])

# Camera Example
sensor_camera_r = Sensor("r", "red channel", mapping=lambda obs: obs["img"][0])
sensor_camera_g = Sensor("g", "green channel", mapping=lambda obs: obs["img"][1])
sensor_camera_b = Sensor("b", "blue channel", mapping=lambda obs: obs["img"][2])

Portable Skills

As we prepare for the 1.0.0 release and marketplace support coming by this summer, we are introducing portable skills. This means that you can now create a skill, package it up and tell the SDK to use it from a pre-defined URL. This allows you to:

  1. Create and Share Skills through the Marketplace for anyone to use
  2. Create more complex skills with pre-packed logic (e.g., a Machine Learning) model that could not be serialized before

To use this, you can create an Agent and add a skill from a URL:

python
# Create a Portable Skill from a URL
# this is great for sharing with others
s = Skill("my-skill", "https://example.com/composabl-skill-my-skill-0.0.1.tar.gz")

# Create a Portable Skill from a Directory
# this is great for testing locally
s = Skill("my-skill", "/tmp/composabl-skill-my-skill-0.0.1.tar.gz")

# Create a Portable Skill - Alternative
# note: we use SkillController as the base class API Spec to use for the portable skill
s = Skill("my-skill", SkillController, SkillOptions.model_validate({
    "remote_address": "https://example.com/composabl-skill-my-skill-0.0.1.tar.gz"
}))

Implementing a Skill is just as easy as creating a Python package and creating a tar.gz archive from it:

⚠️ You should prefix a skill with composabl-skill- to avoid name clashes with other packages

bash
composabl-skill-cstr-drl-py
├── README.md
├── composabl_skill_cstr_drl_py
   ├── __init__.py
   └── teacher.py
└── pyproject.toml

In the pyproject.toml we then define an entrypoint and type under the [composabl] section:

toml
[project]
name = "composabl-skill-cstr-drl-py"
version = "0.1.0"
description = "A Composabl Demo Portable Teacher Skill"
authors = [{ name = "Xavier Geerinck", email = "xavier@composabl.io" }]
dependencies = [
    "composabl-core"
]

[composabl]
type = "teacher"
entrypoint = "composabl_skill_cstr_drl_py.teacher:Teacher"

Portable Perceptors

As we prepare for the 1.0.0 release and marketplace support coming by this summer, we are introducing portable perceptors. This means that you can now create a perceptor, package it up and tell the SDK to use it from a pre-defined URL. This allows you to:

  1. Create and Share Perceptors through the Marketplace for anyone to use
  2. Create more complex perceptors with pre-packed logic (e.g., a Machine Learning) model that could not be serialized before

To use this, you can create an Agent and add a perceptor from a URL:

python
# Create a Portable Perceptor from a URL
# this is great for sharing with others
p = Perceptor("my-perceptor", "https://example.com/composabl-perceptor-my-perceptor-0.0.1.tar.gz")

# Create a Portable Perceptor from a Directory
# this is great for testing locally
p = Perceptor("my-perceptor", "/tmp/composabl-perceptor-my-perceptor-0.0.1.tar.gz")

# Create a Portable Perceptor - Alternative
# note: we use PerceptorImpl as the base class API Spec to use for the portable perceptor
p = Perceptor("my-perceptor", PerceptorImpl, PerceptorOptions.model_validate({
    "remote_address": "https://example.com/composabl-perceptor-my-perceptor-0.0.1.tar.gz"
}))

Implementing a Perceptor is just as easy as creating a Python package and creating a tar.gz archive from it:

⚠️ You should prefix a perceptor with composabl-perceptor- to avoid name clashes with other packages

bash
composabl-perceptor-my-perceptor
├── README.md
├── composabl_perceptor_my_perceptor
   ├── __init__.py
   └── perceptor.py
└── pyproject.toml

In the pyproject.toml we then define an entrypoint and type under the [composabl] section:

toml
[project]
name = "composabl-perceptor-my-perceptor"
version = "0.1.0"
description = "A Composabl Demo Portable Teacher Perceptor"
authors = [{ name = "Xavier Geerinck", email = "xavier@composabl.io" }]
dependencies = [
    "composabl-core"
]

[composabl]
type = "teacher"
entrypoint = "composabl_perceptor_my_perceptor.perceptor:MyPerceptor"

HTTP Support

We only supported Simulators to be implemented through the gRPC protocol before. We are changing this and will be allowing the HTTP protocol as well (through Protobuf). This means that you can now create a simulator that is accessible through HTTP and use it within the Composabl SDK.

As simulators are supposed to run quickly, the performance is important as well. Below you can find a comparison between gRPC and HTTP:

gRPC

HTTP

Thus representing a small slow-down over HTTP, which is still acceptable at large scale. If performance is an important factor, we recommend to keep using gRPC.

As for supporting HTTP, this would still mean using our ServerComposabl specification, but we change a small runtime flag. Internally, we will then create the server over HTTP instead of gRPC for you!

python
r = Runtime({
    "target": {
        "local": {
            "address": "localhost:1337",
            "protocol": "http"
        }
    }
})

Environment Input Validation

Upon creating a Simulator integration (Environment), we now validate to ensure that the correct implementation was used:

  • Are all methods async?
  • Are all methods implemented?
  • Are the required parameter used in the correct order?

Agent Input Validation

Upon creating an Agent, we now validate to ensure that the correct implementation was used:

  • Are all methods async?
  • Are all methods implemented?
  • Are the required parameter used in the correct order?

🔧 Maintenance

  • Ray has been upgraded to 2.9.3
  • Torch has been upgraded to 2.2.1 and above